lecodes-sdk 0.20.2 → 1.1.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 (129) hide show
  1. package/dist/global.d.ts +35 -9
  2. package/dist/types/animate/tween/Animation.d.ts +69 -0
  3. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  4. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  5. package/dist/types/animate/tween/easing.d.ts +29 -0
  6. package/dist/types/animate/tween/spec.d.ts +178 -0
  7. package/dist/types/canvas/Canvas.d.ts +2 -0
  8. package/dist/types/g2/Node2D.d.ts +16 -0
  9. package/dist/types/g2/Sprite.d.ts +11 -1
  10. package/dist/types/gl/Camera.d.ts +15 -1
  11. package/dist/types/gl/DecalSet.d.ts +48 -3
  12. package/dist/types/gl/Foliage.d.ts +47 -0
  13. package/dist/types/gl/Geometry.d.ts +36 -0
  14. package/dist/types/gl/Light.d.ts +25 -7
  15. package/dist/types/gl/Lightmap.d.ts +90 -51
  16. package/dist/types/gl/Material.d.ts +32 -20
  17. package/dist/types/gl/Mesh.d.ts +7 -1
  18. package/dist/types/gl/Model.d.ts +41 -5
  19. package/dist/types/gl/Node.d.ts +18 -0
  20. package/dist/types/gl/Particles.d.ts +53 -1
  21. package/dist/types/gl/Scene.d.ts +21 -1
  22. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  23. package/dist/types/gl/animation/Animator.d.ts +27 -0
  24. package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
  25. package/dist/types/gl/animation/IK.d.ts +109 -0
  26. package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
  27. package/dist/types/gl/animation/Warp.d.ts +2 -1
  28. package/dist/types/gl/animation/core.d.ts +35 -4
  29. package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
  30. package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
  31. package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
  32. package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
  33. package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
  34. package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
  35. package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
  36. package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
  37. package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
  38. package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
  39. package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
  40. package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
  41. package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
  42. package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
  43. package/dist/types/inject.d.ts +35 -27
  44. package/dist/types/runtime/files.d.ts +24 -1
  45. package/dist/types/scene/defineScene.d.ts +50 -31
  46. package/dist/types/ui/UIButton.d.ts +3 -1
  47. package/dist/types/ui/UIInput.d.ts +5 -1
  48. package/dist/types/ui/UINode.d.ts +24 -24
  49. package/dist/types.json +1 -1
  50. package/package.json +1 -1
  51. package/prompts/README.md +142 -142
  52. package/prompts/core-design.md +27 -4
  53. package/prompts/core.md +35 -6
  54. package/prompts/select.ts +19 -4
  55. package/src/animate/tween/Animation.ts +378 -0
  56. package/src/animate/tween/Timeline.ts +175 -0
  57. package/src/animate/tween/animateValue.ts +100 -0
  58. package/src/animate/tween/easing.ts +172 -0
  59. package/src/animate/tween/spec.ts +479 -0
  60. package/src/bridges.d.ts +1760 -1481
  61. package/src/canvas/Canvas.ts +21 -0
  62. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  63. package/src/compile/__tests__/compile.test.ts +11 -0
  64. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  65. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  66. package/src/compile/bundler.ts +34 -4
  67. package/src/compile/compileProject.ts +31 -1
  68. package/src/compile/detectEntry.ts +8 -3
  69. package/src/compile/header.ts +6 -3
  70. package/src/compile/index.ts +3 -0
  71. package/src/compile/sceneEditor.ts +42 -1
  72. package/src/compile/serverSplit.ts +9 -3
  73. package/src/g2/Node2D.ts +38 -0
  74. package/src/g2/Sprite.ts +20 -1
  75. package/src/gl/Camera.ts +34 -1
  76. package/src/gl/DecalSet.ts +132 -5
  77. package/src/gl/Foliage.ts +102 -0
  78. package/src/gl/Geometry.ts +109 -0
  79. package/src/gl/Light.ts +46 -16
  80. package/src/gl/Lightmap.ts +440 -249
  81. package/src/gl/Material.ts +69 -36
  82. package/src/gl/Mesh.ts +120 -102
  83. package/src/gl/Model.ts +167 -124
  84. package/src/gl/Node.ts +40 -1
  85. package/src/gl/Particles.ts +82 -5
  86. package/src/gl/Scene.ts +35 -2
  87. package/src/gl/animation/AnimationClip.ts +52 -0
  88. package/src/gl/animation/Animator.ts +42 -2
  89. package/src/gl/animation/DynamicBone.ts +482 -0
  90. package/src/gl/animation/IK.ts +214 -0
  91. package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
  92. package/src/gl/animation/Playback.ts +5 -4
  93. package/src/gl/animation/Warp.ts +5 -2
  94. package/src/gl/animation/core.ts +65 -4
  95. package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
  96. package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
  97. package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
  98. package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
  99. package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
  100. package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
  101. package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
  102. package/src/gl/physics/Ragdoll.ts +451 -0
  103. package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
  104. package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
  105. package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
  106. package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
  107. package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
  108. package/src/gl/vehicle/Vehicle.ts +666 -0
  109. package/src/gl/vehicle/Wheel.ts +290 -0
  110. package/src/inject.ts +236 -224
  111. package/src/runtime/files.ts +32 -2
  112. package/src/scene/defineScene.ts +92 -66
  113. package/src/scene/level.ts +2 -2
  114. package/src/ui/UIButton.ts +2 -2
  115. package/src/ui/UIInput.ts +3 -3
  116. package/src/ui/UINode.ts +61 -36
  117. package/dist/types/animate/animate.d.ts +0 -20
  118. package/dist/types/gl/Gearbox.d.ts +0 -86
  119. package/dist/types/gl/IK.d.ts +0 -53
  120. package/dist/types/gl/Ragdoll.d.ts +0 -86
  121. package/dist/types/gl/Vehicle.d.ts +0 -191
  122. package/dist/types/gl/Wheel.d.ts +0 -95
  123. package/src/animate/animate.ts +0 -238
  124. package/src/gl/Gearbox.ts +0 -212
  125. package/src/gl/IK.ts +0 -193
  126. package/src/gl/Ragdoll.ts +0 -270
  127. package/src/gl/Vehicle.ts +0 -473
  128. package/src/gl/Wheel.ts +0 -240
  129. /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/bridges.d.ts CHANGED
@@ -1,1481 +1,1760 @@
1
- // The host ABI. These four objects are injected onto the global scope by the runtime host
2
- // (web viewer, desktop, iOS) before any user/SDK code runs. The SDK is a thin, typed re-skin over
3
- // them — it never bundles them; it only references them as free globals. Faithfully tracks the
4
- // surface in packages/worker/src/global.d.ts (the proven contract).
5
-
6
- declare global {
7
-
8
- // ---- 3D engine (Filament / creator-gl) -------------------------------------------------------
9
- var _creator: {
10
- backend: string
11
-
12
- createScene(): number
13
- createOverlayScene(sceneId: number): number
14
- createGLView(): void
15
- openScene(sceneId: number): void
16
- closeScene(): void
17
- launchAR(sceneId: number, onComplete: () => void, onReject: () => void): void
18
- stopAR(): void
19
- launchVR(sceneId: number, onComplete: () => void, onReject: (err?: unknown) => void): void
20
- stopVR(): void
21
- addEntityToScene(sceneId: number, entityId: number): void
22
- removeEntityFromScene(sceneId: number, entityId: number): void
23
- getSceneMaterial(sceneId: number): number
24
- warmRender(sceneId: number, onComplete: () => void): void
25
-
26
- createEntity(): number
27
- cloneEntity(entityId: number): number
28
- destroyEntity(entityId: number): void
29
-
30
- setMatrix(entityId: number, mat: Float32Array): void
31
- setWorldMatrix(entityId: number, mat: Float32Array): void
32
- getChildren(entityId: number): number[]
33
- getChildCount(entityId: number): number
34
- getChild(entityId: number, index: number): number
35
-
36
- setPosition(entityId: number, x: number, y: number, z: number): void
37
- setQuaternion(entityId: number, x: number, y: number, z: number, w: number): void
38
- setEulerAngles(entityId: number, x: number, y: number, z: number, order: number): void
39
- setScale(entityId: number, x: number, y: number, z: number): void
40
-
41
- addChildren(parentEntityId: number, entityIds: number[]): void
42
- setParent(entityId: number, parentEntityId: number, worldPositionStays: boolean): void
43
- setParentNull(entityId: number, worldPositionStays: boolean): void
44
- getParent(entityId: number): number
45
- traverse(entityId: number, callback: (entityId: number) => void): void
46
- /** Descendant of `entityId` by name, with the Animator's binding rule: exact name first, then the
47
- * short name (after the last `:` / `|` — Mixamo `mixamorig:Hips`); a skin joint beats a plain node
48
- * of the same name (merged GLBs carrying a leftover skeleton copy), otherwise first in tree order.
49
- * 0 when absent. Optional: hosts that predate it fall back to the SDK's traverse walk (node.bone). */
50
- findNode?(entityId: number, name: string): number
51
-
52
- createSunLight(entityId: number, x: number, y: number, z: number, intensity: number, color: number, shadowsQuality: number, shadowDistance: number): void
53
- /** Punctual light. `intensity` is luminous POWER in lumens; `falloff` is the metres of
54
- * influence (filament's own default is 1 m, i.e. invisible, so it is always passed). */
55
- createPointLight?(entityId: number, intensity: number, color: number, falloff: number, castShadows: boolean): void
56
- /** Live intensity for any light (sun: lux, point: lumens) — a flash animates instead of rebuilding. */
57
- setLightIntensity?(entityId: number, intensity: number): void
58
- setLightColor?(entityId: number, color: number): void
59
- /** `iblFetchId` (optional, -1 = none) names the scene's own probe; without it the host looks for a project-wide `ibl.ktx`. */
60
- setDefaultIbl(sceneId: number, intensity: number, iblFetchId?: number): void
61
- setBloomOptions(sceneId: number, enabled: boolean, strength: number, quality: number): void
62
- setToneMapping?(sceneId: number, mode: number): void
63
- /** Exposure of the scene's camera — filament's physical model: `aperture` f-stops,
64
- * `shutterSpeed` seconds, `sensitivity` ISO (default f/16, 1/125 s, ISO 100 = EV100 15, bright
65
- * sunlight). ISO ×2 = one stop brighter. Scene-referred light follows it; particle emission is
66
- * post-exposure and does not. Optional: hosts that predate it keep the fixed default. */
67
- setCameraExposure?(sceneId: number, aperture: number, shutterSpeed: number, sensitivity: number): void
68
- /** Display output range (macOS/iOS EDR, `device.hdr`). `getDisplayHeadroom` is what the engine
69
- * renders to right now: the screen's peak as a multiple of SDR white, quantised to half-stops;
70
- * 0 or 1 = SDR. Live — it ramps up from 1 over the first seconds, follows brightness and the
71
- * screen under the window. `getDisplayMaxHeadroom` is the peak the surface can reach at all
72
- * (1 = SDR), constant once the surface exists: the value to branch content on. Optional: hosts
73
- * that predate it, or render 8-bit, are SDR. */
74
- getDisplayHeadroom?(): number
75
- getDisplayMaxHeadroom?(): number
76
- /** HDR look, engine-wide (creator.h `setHdrStrength` / `setHdrPaperWhite`): `strength` 0..1 =
77
- * how much of the picture reaches for the headroom (0 only what SDR clipped, 1 nearly
78
- * everything), `paperWhite` 1..8 = where the operator's white lands as a multiple of SDR white
79
- * (the "HDR brightness" of a console calibration screen; clamped to the headroom). A host may
80
- * pin either from its environment — the getters report what is in force. Optional. */
81
- setHdrStrength?(strength: number): void
82
- getHdrStrength?(): number
83
- setHdrPaperWhite?(paperWhite: number): void
84
- getHdrPaperWhite?(): number
85
- /** Screen-space ambient occlusion: the contact darkening in creases and where objects meet the
86
- * ground. `radius` is world-space metres, `power` the falloff contrast, `quality` 0 LOW … 3
87
- * ULTRA (sample count — not the buffer resolution, which stays half-res).
88
- * Optional: hosts that predate it render without AO and the SDK skips the call. */
89
- setAmbientOcclusionOptions?(sceneId: number, enabled: boolean, intensity: number, radius: number, power: number, quality: number): void
90
- /** Distance fog / aerial perspective. `color` is packed 0xRRGGBB used as a TINT on the
91
- * in-scattered ambient — the engine multiplies it by the environment luminance, so white means
92
- * "as bright as the ambient" and it is NOT the absolute-radiance convention `setSkybox` uses.
93
- * `density` = extinction per metre at `height`, `heightFalloff` 1/m (0 = uniform),
94
- * `cutOff` <= 0 = apply at every distance (the skybox included — that is what blends the
95
- * horizon), `fromIbl` = take the colour from the environment in the view direction and tint
96
- * it by `color`.
97
- * Optional: hosts that predate it render without fog and the SDK skips the call. */
98
- setFogOptions?(sceneId: number, enabled: boolean, color: number, distance: number, density: number, height: number, heightFalloff: number, maxOpacity: number, cutOff: number, fromIbl: boolean): void
99
- setSkybox(sceneId: number, color: number): void
100
- /** Draw the scene's IBL environment as the sky instead of a flat colour. No-op until the IBL
101
- * exists (setDefaultIbl runs first). Optional: hosts that predate it keep the flat skybox. */
102
- setSkyboxFromEnvironment?(sceneId: number): void
103
- /** The sky from its own KTX1 cubemap. `fetchId` is a local-resource id (`_creatorUtils.fetchLocal`
104
- * / an `asset()` handle), pointing at cmgen's `<name>_skybox.ktx` — the sharp single-mip file,
105
- * not the roughness-prefiltered `_ibl.ktx` beside it. Optional: hosts that predate it keep the
106
- * flat skybox. */
107
- setSkyboxTexture?(sceneId: number, fetchId: number): void
108
- setSceneMultiSampleAntiAliasing(sceneId: number, enabled: boolean, scale: number): void
109
- /** Filament View::setStencilBufferEnabled (Scene.stencil). Optional. */
110
- setSceneStencil?(sceneId: number, enabled: boolean): void
111
- /** Render resolution: `renderScale` (0.25–1) = fixed 3D-buffer scale vs the viewport (the UI is
112
- * untouched; a host that owns the 3D texture resizes it, one rendering into the swapchain may
113
- * ignore it), `dynamicResolution` = engine-adaptive scaling under that down to `minScale`.
114
- * Optional: older hosts predate it and the SDK skips the call. */
115
- setSceneRenderOptions?(sceneId: number, renderScale: number, dynamicResolution: boolean, minScale: number): void
116
- /** Anisotropic filtering for every texture bound FROM HERE ON (1 = off, 2 = the engine default,
117
- * 16 = max; clamped). Engine-wide rather than per scene, because a sampler is baked when its
118
- * texture is bound — a glTF binds during `Model.load`, and two scenes cannot disagree about a
119
- * texture they share. Call it before loading the assets it should apply to; `SceneOptions.
120
- * anisotropy` does that automatically (a scene's env runs before its nodes build). Optional:
121
- * hosts that predate it keep isotropic filtering and the SDK skips the call. */
122
- setTextureAnisotropy?(level: number): void
123
- /** Engine-wide cap on texture size (`Texture.maxSize` / `SceneOptions.maxTextureSize`): a
124
- * KTX2 wider or taller than `size` loses its top mip levels on load, a glTF PNG/JPEG is
125
- * downsampled; 0 = no cap. Reaches textures created AFTER the call — a loaded level keeps
126
- * its textures, so a settings menu applies it on the next level load. `createTexture` flag
127
- * 2 (FULL_SIZE) exempts one texture (lightmap pages). Optional: hosts that predate it load
128
- * full-size textures and the SDK skips the call. */
129
- setTextureMaxSize?(size: number): void
130
- /** Depth-reading effects on / off (`scene.setDepthEffects`): soft particles (`depthFade`) and
131
- * projected decals read the scene depth, which costs a half-res depth pre-pass of every opaque
132
- * draw. Off = no pre-pass, hard-edged particles, decal sets draw nothing. Engine-wide, live.
133
- * Optional: hosts that predate it keep the effects and the SDK skips the call. */
134
- setDepthEffects?(enabled: boolean): void
135
- /** LOD distance (`scene.setLodBias`): the LOD pass' screen-size thresholds × `bias` — 2 = every
136
- * level switches at half the distance, 0.5 = full detail twice as far; 1 = the defaults
137
- * (0.30 / 0.12 / 0.05 of the viewport height). Engine-wide, live, clamped 0.25..8. Optional:
138
- * hosts without the LOD pass ignore it and the SDK skips the call. */
139
- setLodBias?(bias: number): void
140
- setMaterialGlobalParameter(sceneId: number, i: number, x: number, y: number, z: number, w: number): void
141
- getCameraFov(sceneId: number, fovType: number): number
142
- /** The scene camera's projection: VERTICAL fov in degrees + near/far clip distances (defaults
143
- * 60 / 0.01 / 1000). Aspect stays host-owned — the host STORES these per scene and re-applies
144
- * them whenever the viewport changes, so one call outlives every resize. Optional: hosts that
145
- * predate it keep the fixed defaults and the SDK feature-detects (Camera.setProjection). */
146
- setCameraProjection?(sceneId: number, fov: number, near: number, far: number): void
147
-
148
- getMatrix(entityId: number, mat: Float32Array): boolean
149
- getWorldMatrix(entityId: number, mat: Float32Array): void
150
- getWorldPosition(entityId: number, mat: Float32Array): void
151
- getWorldDirection(entityId: number, mode: number, mat: Float32Array): void
152
-
153
- getWorldMatrixInverse(entityId: number, mat: Float32Array): void
154
- getCameraViewDirection(sceneId: number, screenX: number, screenY: number, mat: Float32Array): void
155
-
156
- /** `uv1` (optional, 2 floats per vertex) is the lightmap UV set (Geometry.uv1); absent → the
157
- * host duplicates `uv` into UV1. Hosts that predate the argument ignore it. */
158
- /** `colors` (Geometry.colors) is 4 bytes RGBA per vertex for a `requires: [color]` material;
159
- * a host that predates it draws the mesh white. */
160
- setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array, colors?: Uint8Array): void
161
- // Terrain (gl/Terrain.ts ↔ creator-gl/src/terrain.cpp, docs/terrain-plan.md §1.4): a heightmap grid of
162
- // sizeX × sizeZ samples `cellSize` apart (+X across columns, +Z across rows, height on +Y, sample 0 at
163
- // the node origin), drawn as one renderable per `chunk`×`chunk` cells on internal children of the
164
- // entity (u16 indices → chunk ≤ 255). `holes` = one byte per sample, 1 = hole (a triangle exists only
165
- // when its three samples are valid — the same rule as the Jolt height field), null = none. UV0 = local
166
- // XZ in metres, UV1 = the terrain's unit square. Returns the chunk count (0 = refused). terrainUpdate
167
- // re-reads the FULL arrays and rebuilds the chunks the sample rectangle touches. All optional: a host
168
- // without them gets the SDK's own chunk meshes through setMesh (gl/terrainMesh.ts).
169
- terrainCreate?(entityId: number, materialId: number, heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, chunk: number, holes: Uint8Array | null): number
170
- terrainUpdate?(entityId: number, heights: Float32Array, holes: Uint8Array | null, x0: number, z0: number, w: number, h: number): void
171
- terrainSetMaterial?(entityId: number, materialId: number): void
172
- terrainSetShadows?(entityId: number, cast: boolean, receive: boolean): void
173
- // Instanced mesh (gl/InstancedMesh.ts ↔ creator-gl/src/instanced.cpp): `count` transforms,
174
- // column-major 4×4 local to the node, all-zero = hidden. setInstanceTransforms writes
175
- // matrices.length / 16 of them starting at `first` (a subarray view is fine — consumed synchronously).
176
- createInstancedMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, count: number): void
177
- setInstanceTransforms(entityId: number, matrices: Float32Array, first: number): void
178
- setInstancedMeshMaterial(entityId: number, materialId: number): void
179
- setInstancedMeshShadows(entityId: number, cast: boolean, receive: boolean): void
180
- setCulling(entityId: number, culling: boolean): void
181
- /** Filament RenderableManager::setPriority — the coarse draw order, 0..7 (Mesh.renderPriority).
182
- * Optional: an older host leaves everything at the default 4. */
183
- setRenderPriority?(entityId: number, priority: number): void
184
- /** Filament MaterialInstance::setDepthCulling / setDepthWrite — per-instance overrides of the
185
- * depth state baked into the shader package (Material.depthTest / depthWrite). Optional. */
186
- setMaterialDepthTest?(materialInstanceId: number, enable: boolean): void
187
- setMaterialDepthWrite?(materialInstanceId: number, enable: boolean): void
188
- /** Filament MaterialInstance::setCullingMode: 0 none (double-sided), 1 front, 2 back. Optional. */
189
- setMaterialCulling?(materialInstanceId: number, mode: number): void
190
- /** Filament MaterialInstance stencil state in one call (Material.stencil): `test` 0 always, 1 never,
191
- * 2 less, 3 lessEqual, 4 greater, 5 greaterEqual, 6 equal, 7 notEqual; the ops 0 keep, 1 zero,
192
- * 2 replace, 3 increment, 4 decrement, 5 invert. Optional. */
193
- setMaterialStencil?(materialInstanceId: number, write: boolean, test: number, ref: number, onPass: number, onFail: number, onDepthFail: number, readMask: number, writeMask: number): void
194
- setCastShadows(entityId: number, culling: boolean): void
195
- setReceiveShadows(entityId: number, culling: boolean): void
196
-
197
- /** Decode a fetched image into a texture. `flags` (optional, Texture.load): bit 1 = LINEAR data
198
- * (a normal map — store RGBA8, not sRGB); unset / absent = colour, sRGB. A host that ignores
199
- * it loads colour correctly and normal maps wrongly. KTX2 decides by its own header. */
200
- createTexture(systemId: number, onComplete: (id: number, width: number, height: number) => void, onReject: () => void, flags?: number): void
201
- // Texture from a baked _creatorCanvas surface (RGBA8, already rasterized — synchronous, no decode).
202
- createTextureFromCanvas(surfaceId: number): number
203
- updateTextureFromCanvas(texId: number, surfaceId: number): void
204
- /** A texture from raw UBYTE pixels — `channels` 1–4, row-major, `width*height*channels` bytes, no
205
- * mips; `srgb` selects the sRGB internal format for 3/4 channels (colour) vs linear (data — a
206
- * terrain's control map). The bytes are copied. Returns 0xFFFFFFFF on failure. `updateTexturePixels`
207
- * re-uploads a sub-rectangle with the creation channel count. Optional (Texture.fromPixels throws). */
208
- createTexturePixels?(width: number, height: number, channels: number, data: Uint8Array, srgb: boolean): number
209
- updateTexturePixels?(textureId: number, x: number, y: number, width: number, height: number, data: Uint8Array): void
210
-
211
- createMaterial(systemId: number): number
212
- createMaterialS(name: string): number
213
- setMaterial(entityId: number, materialId: number, index: number): void
214
- getMaterial(entityId: number, index: number): number
215
-
216
- setUniformRgb(materialId: number, uniform: string, color: number): void
217
- setUniformRgba(materialId: number, uniform: string, color: number): void
218
- setUniformFloat(materialId: number, uniform: string, value: number): void
219
- setUniformBoolean(materialId: number, uniform: string, value: boolean): void
220
- setUniformTexture(materialId: number, uniform: string, systemId: number, wrapS: number, wrapT: number): void
221
- setUniformArray(materialId: number, uniform: string, value: Float32Array): void
222
- setUniformNull(materialId: number, uniform: string): void
223
-
224
- attachCameraToAR(sceneId: number, cameraEntityId: number, onProjectionChange: (mat: Float32Array, fov: number) => void): void
225
- getDisplaySize(): Float32Array
226
-
227
- setVisible(entityId: number, visible: boolean): void
228
- isVisible(entityId: number): boolean
229
-
230
- createGlb(systemId: number, onComplete: (buff: number) => void, onReject: () => void): void
231
- setGlbCulling(entityId: number, culling: boolean): void
232
- /** Optional. True when the host refits a skinned model's frustum-culling bounds to its joints every
233
- * frame (creator-gl animation/skinning.cpp), i.e. culling is safe for animated GLBs. Model.load
234
- * defaults culling ON only where this returns true; absent / false = the always-draw default. */
235
- skinnedCullingSupported?(): boolean
236
- /** Optional. Level-of-detail override for a GLB instance (docs/lod-plan.md): `mesh` / `anim` are -1 for
237
- * automatic (the engine picks by screen size and visibility) or a forced level 0..3. Backs Model.lod
238
- * and Animator.lod; a host without it (no LOD pass) leaves everything at full detail. */
239
- setLod?(entityId: number, mesh: number, anim: number): void
240
-
241
- // Render-synced aspect update(dt) dispatch, called from inside render(). Early = before the physics
242
- // step; late = after animations/particles, just before draw. Single-slot per phase (the SDK's Aspect
243
- // dispatcher registers one callback per phase that iterates its ordered updater list).
244
- setEarlyUpdate(cb: (dt: number) => void): void
245
- setLateUpdate(cb: (dt: number) => void): void
246
- /** FIXED phase - the SDK's `updateFixed`: once per physics substep with dt = 1/60 exactly, BEFORE
247
- * that substep's Jolt step (move commands / velocities written here feed the same step). Under
248
- * `setTimeScale` the NUMBER of substeps changes, never the dt; at most 4 per frame. `null` clears it -
249
- * registered lazily, only once an aspect declares the phase (one flag check per substep otherwise).
250
- * Optional: a host without it gets the SDK's own 1/60 accumulator inside the early phase. */
251
- setFixedUpdate?(cb: ((dt: number) => void) | null): void
252
- /** The engine's clock multiplier for physics / animators / particles (1 normal, 0 frozen) — the
253
- * SDK's `Time.scale` / `Time.paused` pushed down so the sims stay in step with game code. The
254
- * two aspect phases still receive the RAW wall-clock dt (the SDK scales it itself). Optional:
255
- * a host without it keeps its sims at wall-clock speed. */
256
- setTimeScale?(scale: number): void
257
- // Physics contact/sensor events (drained after each step): a/b = entity ids, type 0 enter / 1 exit.
258
- setOnPhysicsEvent(cb: (a: number, b: number, type: number) => void): void
259
-
260
- // ---- Animation system (docs/animation-v2-plan.md): AnimationClip + Animator — THE skeletal animation
261
- // path, evaluated by creator-anim on every host. A clip set is parsed straight from GLB bytes (tracks
262
- // target bone NAMES, no entities); a model's embedded clips are registered by createGlb (getGlbClipSet,
263
- // 0 = none). An Animator binds clips to one node hierarchy by name; the native evaluator runs the WHOLE
264
- // per-frame loop (sources, transitions, blend spaces, one-shot hand-over, root motion) — the SDK only
265
- // issues play/stop/blend calls and has no tick. Skinning is flushed AFTER the late phase so IK /
266
- // procedural joint writes land in the skin.
267
- // TRANSITIONS ARE INERTIAL, NOT CROSSFADES: a layer evaluates exactly ONE source (its one-shot, its
268
- // loop, or nothing); a switch records the difference between the pose the layer SHOWED and the pose the
269
- // new source shows and decays it away over `fade` seconds (halflife = 0.4 × fade). A replaced source is
270
- // silent from that moment — it costs nothing, its weight is 0 at once, and it may be replaced again
271
- // mid-transition (the offset is re-recorded from the displayed pose). `fade` 0 = cut.
272
- loadClips(systemId: number, onComplete: (clipSetId: number) => void, onReject: (err: unknown) => void): void
273
- // names = newline-joined track targets; data = [trackCount, duration (<=0 → max key time), (path 0 T/1 R/2 S,
274
- // interp 0 linear/1 step/2 cubic, comps, keyCount, times…, values…)*]. Returns a one-clip set id (0 = bad).
275
- createClipFromTracks(names: string, data: Float32Array): number
276
- getClipSetInfo(clipSetId: number): { name: string, duration: number, trackCount: number }[]
277
- // A clip DERIVED from one of the set as a NEW single-clip set (index 0) — AnimationClip.from(clip, { mirror,
278
- // from, to }): the mirror (left ↔ right on the set's own rig, the GLB's node tree: contacts swapped, heading
279
- // negated), then the [start, end] window re-timed to 0 (start < 0 = whole, end < 0 = the clip's end; boundary
280
- // values interpolated in, events re-timed). 0 = bad id / range, or a mirror asked of a set without a rig.
281
- deriveClip(clipSetId: number, clip: number, mirror: boolean, start: number, end: number): number
282
- // Clip events on the CLIP: normalized times (sorted ascending). Every slot bound to the clip, in every
283
- // animator, fires slot event type 4 + i on crossing event i. Empty = clear.
284
- setClipEvents(clipSetId: number, clip: number, times: Float32Array): void
285
- getGlbClipSet(entityId: number): number
286
- destroyClipSet(clipSetId: number): void
287
- animatorCreate(entityId: number): number // 0 = not a transform node
288
- animatorDestroy(animatorId: number): void // leaves the skeleton in rest pose
289
- // One (clip, layer) slot, silent until played / made a blend member. slot index or -1.
290
- animatorBind(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number
291
- animatorBoundTracks(animatorId: number, slot: number): number
292
- // The layer's LOOP: member slots + positions (dims 1: x per member, 2: x,y; one member = a plain looping
293
- // clip). The members share ONE cycle clock, each placed on it LINEARLY by `phases` — two floats per member,
294
- // (offset, cycles): φ(t) = offset + cycles · t / duration, 0 at a left-foot-down (measured offline
295
- // from the clip's foot marks). An empty array, or cycles <= 0 for a member = normalized time (0, 1) and no
296
- // cycle to phase-match to — the clock never reads the clip's marks. animatorSetBlendValue picks the mix (1D linear between neighbours /
297
- // 2D gradient band). Setting it takes the layer over from whatever it shows — the previous loop and any
298
- // one-shot on it go silent — with a transition of `fade`. Empty = no loop. speed = the members' rate.
299
- animatorSetBlend(animatorId: number, layer: number, dims: 1 | 2, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases: Float32Array): void
300
- animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void
301
- // Play a slot as the layer's one-shot, transitioned in over fadeIn (0 = cut) from whatever the layer
302
- // showed (playing a blend MEMBER brings the whole blend back instead). A non-looping one-shot hands the
303
- // layer back to the loop at the end, the hand-over firing fadeOut seconds BEFORE the end and the return
304
- // transition taking fadeOut; fadeOut < 0 = not given (cut back; on a loopless layer hold the last frame).
305
- // restart = rewind even if already playing (a restart is a new source: it transitions from the pose shown).
306
- animatorPlay(animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean): void
307
- // TURN WARP for the slot's current play (right after animatorPlay): the node's turn from the clip's baked heading is
308
- // scaled to `radians` total; the pose keeps its own turn, the extra pivots about the planted foot. enabled false =
309
- // the clip's own turn. Optional: older hosts lack it.
310
- animatorSlotSetTurn?(animatorId: number, slot: number, radians: number, enabled: boolean): void
311
- // Release over `fade` (a transition toward what is left — the loop, or the rest pose): one slot; a
312
- // layer's one-shot + loop (slot -1); everything (layer -1).
313
- animatorStop(animatorId: number, layer: number, slot: number, fade: number): void
314
- animatorSeek(animatorId: number, slot: number, time: number): void // seeking a blend member moves the blend
315
- animatorGetSlotTime(animatorId: number, slot: number): number
316
- animatorGetSlotWeight(animatorId: number, slot: number): number // 1 = the layer's source, a loop member = its share, else 0
317
- // Layer config: weight 0–1; additive = each slot's DELTA vs its clip's first frame on top of the layers
318
- // below; maskRoot = bone name(s, '\n'-separated) whose subtrees the layer drives ("" = all).
319
- animatorSetLayer(animatorId: number, layer: number, weight: number, additive: boolean, maskRoot: string): void
320
- // ANTICIPATION for the layer's next source change: the transition starts with -amount x the new source's
321
- // joint velocity (a wind-up against the coming motion), consumed by that switch. Optional: older hosts lack it.
322
- animatorSetLayerAnticipation?(animatorId: number, layer: number, amount: number): void
323
- animatorSetGlobal(animatorId: number, speed: number, paused: boolean): void
324
- // Root motion: "" off, "*" auto (the shallowest joint a base-layer clip translates), else a bone name. The
325
- // root's horizontal travel (animator-node frame) is stripped from the pose and, per apply: 0 accumulated
326
- // only (animatorGetRootMotion copies + clears out[3]), 1 added to the node's transform, 2 fed to the
327
- // CharacterController on the node or an ancestor as a world velocity (falls back to 1 without one).
328
- /** `apply` bits 0–1: 0 accumulate only / 1 move the node / 2 feed the CharacterController; bit 2 (+4): the
329
- * root joint's yaw about the node's up is root motion too (off the pose, onto the node the travel lands on). */
330
- animatorSetRootMotion(animatorId: number, bone: string, apply: number): void
331
- animatorGetRootMotion(animatorId: number, out: Float32Array): void
332
- // Slot events: 0 completed (non-loop end) / 1 loop wrapped / 2 settled (no longer a source, after a stop
333
- // or a replacement) / 3 HAND-OVER (a one-shot's return starts — for the SDK the clip is over; what the app
334
- // starts in response takes the layer over instead) / 4+i clip event i. Single-slot (routed by animatorId).
335
- setOnAnimatorEvent(callback: (animatorId: number, slot: number, type: number) => void): void
336
- // ---- contacts, phase, root curves (docs/animation-v2-plan.md §2.5–2.6) ----
337
- // Every clip bound to a skeleton is baked once against it: when each foot is planted, the gait phase
338
- // φ(t) (0 at a left-foot-down, 0.5 at a right-foot-down, unwrapped over the clip; absent for a clip
339
- // with no gait cycle) and the root's cumulative travel / yaw / speed. A controller asks these instead
340
- // of shipping measured tables of its own.
341
- // The feet: '\n'-joined bone names per side (foot[, toe/ball]); both "" = classify by name. Re-bakes.
342
- animatorSetFeet(animatorId: number, left: string, right: string): void
343
- // One curve of a slot's clip at `time` seconds (< 0 = the slot's clock now): which 0 φ (-1 = no gait)
344
- // / 1 travel (m) / 2 yaw (rad, + = left) / 3 speed (m/s) / 4-5 unit travel direction x / z
345
- // (model space, held through stills — integrate dir × d(travel) for the root's 2D path).
346
- animatorSlotCurveAt(animatorId: number, slot: number, which: 0 | 1 | 2 | 3 | 4 | 5 | 6, time: number): number
347
- // out ← [curve sample dt, total travel (m), mean speed (m/s), in-place flag]. False = unbound slot.
348
- animatorSlotCurveInfo(animatorId: number, slot: number, out: Float32Array): boolean
349
- animatorSlotTurn(animatorId: number, slot: number): number // the clip's total root yaw, rad
350
- // The first time the clip has turned `yaw` radians — where a turn is entered by a body already
351
- // that far into the same turn, so the two read as one move.
352
- animatorSlotTimeAtTurn(animatorId: number, slot: number, yaw: number): number
353
- // What the LAYER shows, as a cycle phase in [0, 1) — its loop's clock, or its one-shot's φ; -1 = none.
354
- animatorLayerPhase(animatorId: number, layer: number): number
355
- // Seek the slot to the first time whose φ ≡ phase (mod 1); a blend member moves its whole group.
356
- animatorSeekPhase(animatorId: number, slot: number, phase: number): void
357
- // How far the slot's pose is from `target`'s at the same cycle phase, metres (joint distance +
358
- // the velocity difference over 0.1 s, the planted foot weighted most) — what handing over to that
359
- // clip would hand the inertializer. Fills `out` at the curve rate, returns the sample count.
360
- // align: 0 = compare at the same cycle phase, 1 = at the same time (two clips that both begin
361
- // from standing have no shared cycle to line up on).
362
- animatorSlotFit(animatorId: number, slot: number, target: number, align: 0 | 1, out: Float32Array): number
363
- // The earliest time in the slot where that hand-over costs no more than `tolerance` metres;
364
- // atContact snaps to the next foot-down at or after it. -1 = never that close.
365
- animatorSlotExit(animatorId: number, slot: number, target: number, tolerance: number, atContact: boolean, align: 0 | 1): number
366
- // CYCLE ALIGNMENT by pose, no marks: given slot a's cycle (offA, cyclesA), the (offset, cycles) of slot b
367
- // under which the two loops show the same pose at the same gait phase — b's cycle count searched over
368
- // cyclesA × {1/3 … 3}, its offset on a fine grid. out = [offset, cycles, score (metres), margin (runner-up
369
- // ≥ 0.2 cycle away minus the best; ~0 = ambiguous)]. Returns 1, or 0 when there is nothing to compare.
370
- animatorSlotAlign(animatorId: number, a: number, b: number, offA: number, cyclesA: number, out: Float32Array): number
371
- // Contact spans as (side 0 left / 1 right, from, to, atX, atY, atZ) sextuplets (seconds, model space);
372
- // returns the span count, filling `out` up to its capacity.
373
- animatorSlotContacts(animatorId: number, slot: number, out: Float32Array): number
374
- // A CLIMBING clip's tread levels (model-space plant heights, sorted ascending): out[0] = the
375
- // riser (median level spacing), out[1..] = the levels, filled up to out's capacity. Returns the
376
- // level count — 0 for a flat clip.
377
- animatorSlotTreads(animatorId: number, slot: number, out: Float32Array): number
378
- // The clip's baked physics at `time` seconds (< 0 = the slot's current time), unit body mass,
379
- // model space: out11 = COM position xyz, COM velocity xyz (= linear momentum per kg), angular
380
- // momentum about the COM xyz, then per-foot support left/right (contact-gated, seesaw split,
381
- // scaled by the vertical force proxy — > 1 on a landing, 0 in flight). 0 = no body segments
382
- // classified on this skeleton.
383
- animatorSlotPhysics(animatorId: number, slot: number, time: number, out: Float32Array): number
384
- // The clip's MATCHING FEATURE ROW at `time` (37 floats, the clip's heading frame at that time —
385
- // x lateral (+ left), y up, z forward): 0–5 feet positions (relative to the pelvis' ground
386
- // point), 6–11 feet velocities, 12–14 pelvis velocity, 15 pelvis height, 16–18 COM velocity,
387
- // 19–20 support L/R, 21–22 contact phase L/R, 23 yaw angular momentum, 24–31 the clip's own
388
- // path 0.3/0.6/1.0/1.5 s ahead as (lateral, forward) pairs, 32–35 facing change at those
389
- // horizons (rad, + = left), 36 cyclic flag. Returns 37, or 0 without a leg chain.
390
- animatorSlotFeatures(animatorId: number, slot: number, time: number, out: Float32Array): number
391
- // The calibrated knee HINGE AXIS of a side (0 left / 1 right): a unit vector in the thigh's local
392
- // frame — a skeleton property, measured over every bound clip's knee rotation track. out8 = axis
393
- // xyz, spread mean (rad), spread max (rad), measurement count, 0, 0. 0 = no leg / no knee motion.
394
- animatorKneeAxis(animatorId: number, side: number, out: Float32Array): number
395
- // The knee's bend plane of a slot's clip at `time` seconds (< 0 = the slot's current time),
396
- // predicted from the hinge axis + the clip's own thigh rotation — continuous even where the leg
397
- // is straight. out6 = pole xyz (unit, model space, toward the knee — a two-bone solver's bend
398
- // direction), then the plane normal xyz. 0 = uncalibrated / no leg.
399
- animatorSlotKneePole(animatorId: number, slot: number, side: number, time: number, out: Float32Array): number
400
- // Step warp v2: stride scales the feet's travel-direction offsets from the hips (uniform through
401
- // stance and swing), lift = metres ADDED to their height (swing-gated by the contact marks;
402
- // 0 = neutral, negative = a shuffle; half of what it adds raises the pelvis, capped by the
403
- // planted legs' remaining extension), pitchDeg rotates each foot about its lateral axis
404
- // (+ = toes up), slopeDeg the invisible staircase (+ = ascending: foot heights follow the
405
- // incline + the feet auto-pitch; the HOST climbs the body at tan(slope) × the stride-scaled
406
- // travel, which holds each planted foot's world height constant on its tread). Solved in the
407
- // calibrated knee hinge plane with a soft reach; the pelvis lowers by any leg's overreach
408
- // (marks-weighted, spring-followed, zero when nothing overreaches). 1/0/0/0 = identity;
409
- // on false = off.
410
- animatorSetStepWarp(animatorId: number, on: boolean, stride: number, lift: number, pitchDeg: number, slopeDeg: number): void
411
- // Footsteps: a contact that BEGAN this evaluation on what the base layer shows — side 0 left / 1 right,
412
- // x/y/z = the foot's WORLD position at the plant. Fired after the frame's transform commit.
413
- setOnAnimatorStep(callback: (animatorId: number, side: number, x: number, y: number, z: number) => void): void
414
-
415
- // ---- drives + warping ----
416
- // The character's world velocity this frame: the speed the stride warp fits the stride to and the
417
- // direction the orientation warp turns the lower body toward.
418
- animatorSetMotion(animatorId: number, vx: number, vy: number, vz: number): void
419
- // Cumulative capsule travel (m) and yaw (rad, + = left) — what a slot's distance / angle drive reads.
420
- animatorSetDriveInput(animatorId: number, distance: number, angle: number): void
421
- // Drive a slot's clock by a quantity instead of time: 0 time / 1 distance / 2 angle. `entry` is the
422
- // curve value that corresponds to NOW (a start 0; a stop with D metres left: total travel − D), so
423
- // the clip is entered where it already agrees with the body. Stalls fall back to time.
424
- animatorSetSlotDrive(animatorId: number, slot: number, mode: 0 | 1 | 2, entry: number): void
425
- // [stride on, stride min, stride max, orient on, orient max°, orient time, min speed, pelvis drop]
426
- animatorSetWarpParams(animatorId: number, params: Float32Array): void
427
-
428
- // ---- feet: foot lock + ground IK (docs/animation-v2-plan.md §2.9) ----
429
- // The feet stage runs in WORLD space inside the evaluation, after the clips are composited: a foot
430
- // the shown clip calls planted is pinned where it landed and the leg re-solved to keep it there
431
- // while the body moves on (the lock), and each foot is put on the ground the ENGINE probed under it,
432
- // the pelvis lowered so the leg reaches (ground IK). The engine casts the probe rays itself against
433
- // what a character can stand on (static + moving solids; never the character's own bodies, debris
434
- // or sensors) and reads the CharacterController's ground state — nothing per frame from the SDK.
435
- // [ik on, lock on, pelvis drop m, unlock distance m, lock-in s, lock-out s, pelvis spring s,
436
- // align to ground normal 0..1, probe half-length m, detector max speed m/s, detector max height m]
437
- // Optional: a host without it has no feet stage (the legs stay as animated).
438
- animatorSetFeetParams?(animatorId: number, params: Float32Array): void
439
- // One foot after this frame's evaluation, side 0 left / 1 right: out ← [locked, lock weight,
440
- // anchor xyz, target xyz] (world) — a debug overlay's beam under the foot. False = no such foot.
441
- animatorFootState?(animatorId: number, side: number, out: Float32Array): boolean
442
-
443
- // ---- locomotion: the movement model + the clip selector, both engine-side ----
444
- // The intent (`locoSetInput`) becomes a desired velocity; the simulated velocity springs toward it
445
- // one fixed substep at a time (`locoStep`, given the capsule's place), and `locoUpdate` — called
446
- // before the animators are evaluated — picks and drives what the base layer shows. The host moves
447
- // the character with the velocity and facing it reads back: in displacement `code` the animation
448
- // never moves the body, so "how fast am I" is an input to the animation, not an output of it.
449
- locoCreate(animatorId: number): number
450
- locoDestroy(loco: number): void
451
- // [halflife walk, halflife run, halflife facing, speed walk, speed run, speed sprint, deadzone,
452
- // turn min°, turn big°, blend, stop blend, start tap, resume, predict, displacement (0 code /
453
- // 1 data / 2 hybrid), mode (0 rules / 1 matching), match interval, match blend, spin min°, turn
454
- // rate cap (deg/s, 0 = uncapped), halflife braking, hybrid adjustment clamp (m/s)]
455
- locoSetParams(loco: number, params: Float32Array): void
456
- locoDefaults(out: Float32Array): void
457
- // Register a bound slot: kind 0 idle / 1 gait / 2 start / 3 stop / 4 turn / 5 match / 6 spin
458
- // (a turn on the spot); `angle` degrees (+ = left) for the starts / turns / spins, `speed` m/s for
459
- // a gait (0 = the clip's own measured speed), `gait` the gait a transition belongs to (0 walk /
460
- // 1 run / 2 sprint, -1 = any) — a walking body plays the walking starts, stops and turns.
461
- locoSetEntry(loco: number, slot: number, kind: number, angle: number, speed: number, gait: number): void
462
- locoClearSet(loco: number): void
463
- // [dir x, dir z, magnitude 0–1, face x, face z, gait 0 walk / 1 run / 2 sprint] — the facing is
464
- // independent of the movement: a released key with a heading still owed turns the body on the spot.
465
- locoSetInput(loco: number, input: Float32Array): void
466
- // One simulation step at the capsule's world place — from the fixed substep loop.
467
- locoStep(loco: number, dt: number, px: number, py: number, pz: number): void
468
- // Run the selector for this frame — before the animators are evaluated.
469
- locoUpdate(loco: number, dt: number): void
470
- // out ← [state (0 idle / 1 start / 2 move / 3 turn / 4 stop / 5 spin), speed, vel x/y/z, yaw (rad,
471
- // 0 = +Z, + = toward +X), yaw rate, phase, state sequence, then 3 × (x, z, dir x, dir z) predicted
472
- // at +0.2 / +0.4 / +0.7 s].
473
- locoRead(loco: number, out: Float32Array): void
474
- // Why the last STOP was the one played: one row of 9 floats per candidate weighed — [slot, entry
475
- // time, metres it still travels, metres the body needs, seconds to its next foot-down, foot-downs
476
- // left, flags (1 = its phase matched, 2 = at/after its first foot-down, 4 = played, 8 = entered
477
- // ahead of that foot-down by its pose), score, pose distance from what showed (m, -1 = not
478
- // measured)]. Returns the rows written. A debug read; absent on hosts predating it.
479
- locoStopReport?(loco: number, out: Float32Array): number
480
- // Build the motion-matching database over the registered set (selector mode 1); returns frames.
481
- locoBuildDatabase(loco: number): number
482
-
483
- /** Subtree AABB in the entity's OWN local space (its own transform excluded) as
484
- * [minX,minY,minZ, maxX,maxY,maxZ] — zeros for an empty / not-yet-loaded subtree. What
485
- * `Shape.fit()` measures. Optional: absent on hosts predating the binding. */
486
- computeBoundingBox?(entityId: number): Float32Array
487
- setColliderFromMesh(entityId: number, meshEntityId: number, form: number): void
488
- setColliderBox(entityId: number, centerX: number, centerY: number, centerZ: number, sizeX: number, sizeY: number, sizeZ: number): void
489
- setColliderSphere(entityId: number, centerX: number, centerY: number, centerZ: number, radius: number): void
490
-
491
- // JoltPhysics. motionType: 0 static / 1 kinematic / 2 dynamic.
492
- physicsHasSupport(): boolean
493
- /** Lightmap bake (docs/lightmap-plan.md) — a dev-time tool compiled into the desktop host only;
494
- * `lightmapBake` exists only where `lightmapHasSupport()` is true. Writes the raster (lightmap.png, an intermediate the CLI turns into lightmap.ktx2)
495
- * — one per atlas PAGE (`lightmap_<n>.png`, up to `maxPages` of `size` before the texel coarsens) —, the point lights' baked
496
- * irradiance (`lightmap-light[_<n>].png`, sRGB(E / lightScale), `lightRays` shadow rays toward a `lightRadius`-metre disc) when the
497
- * scene holds point lights, lightmap.bake (rects, pages, the baked lights) and lightmap.volume (`volumeCell` > 0)
498
- * into outDir (R = sun visibility, G = ambient occlusion) for the given static instances (GLB roots /
499
- * Mesh entities; `keysJoined` = one key per id, '
500
- '-joined). Synchronous; progress on stdout. */
501
- lightmapHasSupport?(): boolean
502
- /** Lightmap consumption (docs/lightmap-plan.md §3): the next createGlb takes lightmap.filamat (the material
503
- * behind that instance id) instead of the ubershader and keeps TEXCOORD_1; UINT32_MAX clears. */
504
- setNextGlbLightmapped?(materialInstanceId: number): void
505
- /** The level-wide knobs every lightmap-material instance shares (applied to the ones that exist and to every one
506
- * created afterwards): the sun-mask shadow math (ambientScale, sunStrength), how much baked AO applies (aoStrength,
507
- * 1 = all) and the light atlas' / volume's physical irradiance per encoded 1.0 (lightScale; 0 = no baked lights).
508
- * volumeLightScale = the light VOLUME's own scale (movers); omitted or < 0 = same as lightScale. lightGamma = the light
509
- * store's curve, texel = sRGB(pow(E / lightScale, 1 / lightGamma)) — 1 (default) for the linear v2 store, 2 for the
510
- * square-root store bakes write since 2026-08-31 (the .bake's `lightGamma`). */
511
- lightmapSetOptions?(ambientScale: number, sunStrength: number, aoStrength: number, lightScale: number, volumeLightScale?: number, lightGamma?: number): void
512
- /** Bind the atlas page + this instance's rect (uv1 * [sx, sy] + [ox, oy]) and, unless 0xFFFFFFFF, the matching page of
513
- * the light atlas; the sun / IBL terms come from filament's per-frame uniforms inside the shader. Returns how many
514
- * material instances took it (0 = not loaded lightmapped). */
515
- lightmapApply?(entityId: number, textureId: number, sx: number, sy: number, ox: number, oy: number, lightTextureId: number): number
516
- /** Shadow flags on every renderable of a GLB instance (Mesh has setCastShadows/setReceiveShadows). */
517
- setGlbShadows?(entityId: number, cast: boolean, receive: boolean): void
518
- lightmapBake?(outDir: string, entityIds: Uint32Array, keysJoined: string, size: number, texel: number, sunRays: number, aoRays: number, aoDistance: number, bias: number, sunAngleDeg: number, volumeCell: number, maxPages: number, lightRays: number, lightRadius: number, bounceAlbedo?: number, bounceDistance?: number, bounceRays?: number, coplanarSkip?: number, denoise?: number): boolean
519
- /** Light VOLUME for dynamic objects (docs/lightmap-plan.md §9). `lightmapVolumeLoad` turns a fetched
520
- * lightmap.volume (`fetchSystemId` from `_creatorUtils.fetch`) into RGBA8 3D textures and returns its
521
- * header as JSON — `{"texture":id,"dims":[nx,ny,nz],"min":[x,y,z],"cell":m,"light"?:id,"faces"?:n}` (`light` = the
522
- * point lights' block: a version-3 volume's six ambient-cube faces (+X −X +Y −Y +Z −Z, `faces` 6), a version-2
523
- * volume's one directionless value (`faces` 1) — uploaded either way as six face slabs stacked along z, so the
524
- * light texture's depth is nz × 6) — or "" on failure. */
525
- lightmapVolumeLoad?(fetchSystemId: number): string
526
- /** Make those textures THE volume (`textureId` 0xFFFFFFFF clears; `lightTextureId` 0xFFFFFFFF = no baked lights):
527
- * every lightmap-material instance without an atlas rect — loaded already or later — samples it at the pixel's
528
- * world position (+ half a `cell` along the normal). `size` = dims × cell. Returns how many instances switched. */
529
- lightmapVolumeSet?(textureId: number, lightTextureId: number, minX: number, minY: number, minZ: number, sizeX: number, sizeY: number, sizeZ: number, cell: number): number
530
- physicsConfigure(gx: number, gy: number, gz: number, maxBodies: number): void
531
- setInterpolation(enabled: boolean): void
532
- // Shape/Physics/Trigger aspects: build a shape once, create bodies from it.
533
- physicsBuildBox(hx: number, hy: number, hz: number): number
534
- physicsBuildSphere(radius: number): number
535
- physicsBuildCylinder(halfHeight: number, radius: number): number
536
- physicsBuildCapsule(halfHeight: number, radius: number): number
537
- /** Mesh shape from node-local triangles. convex=false → triangle mesh (static/kinematic/pick/character
538
- * only; physicsCreateBody returns 0 for a dynamic one), convex=true → convex hull (any motion).
539
- * (sx,sy,sz) = world scale, applied natively. Returns 0 if the shape can't be built. */
540
- physicsBuildMesh(vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: number, sy: number, sz: number): number
541
- /** Same from a loaded GLB root (non-skinned primitives, bind pose, baked sub-node transforms; cached per asset). */
542
- physicsBuildMeshFromEntity(entityId: number, convex: boolean, sx: number, sy: number, sz: number): number
543
- /** Terrain collider (Shape { heightfield: true }, docs/terrain-plan.md §1.4): a Jolt HeightFieldShape over
544
- * the grid `terrainCreate` draws (same arrays, same hole rule). Static / kinematic / pick / character
545
- * ground only. (sx,sy,sz) = world scale. `physicsUpdateHeightField` rewrites a sample rectangle in
546
- * place from the FULL arrays (live bodies keep the shape); heights beyond the range chosen at build
547
- * time (25 % headroom) clamp — the SDK rebuilds the shape when an edit leaves that range. Optional. */
548
- physicsBuildHeightField?(heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, holes: Uint8Array | null, sx: number, sy: number, sz: number): number
549
- physicsUpdateHeightField?(shapeId: number, heights: Float32Array, sizeX: number, sizeZ: number, x0: number, z0: number, w: number, h: number, holes: Uint8Array | null): void
550
- /** A loaded GLB root's triangle soup — the one `physicsBuildMeshFromEntity` collides — as 9 floats per
551
- * triangle in the asset root's space (empty when the entity is not a GLB). `Terrain.conform` stamps a
552
- * road model into the ground with it. Optional. */
553
- glbTriangles?(entityId: number): Float32Array
554
- /** Offset a built shape's centre from the node's origin (Shape `origin`) — world units in the
555
- * body's rotated, UNSCALED frame, sitting outside a mesh shape's scale wrapper. Sets rather
556
- * than accumulates; (0,0,0) clears it. Optional: an older host just centres on the node. */
557
- physicsSetShapeOrigin?(shapeId: number, x: number, y: number, z: number): void
558
- /** Swap a live body's shape, keeping its id, velocity and transform (Shape.fit / a re-attach).
559
- * updateMass recomputes the inertia tensor. Refuses a triangle mesh on a dynamic body. */
560
- physicsSetBodyShape?(bodyId: number, shapeId: number, updateMass: boolean): void
561
- physicsDestroyShape(shapeId: number): void
562
- /** sensor = trigger (overlap events, no response); pickOnly = raycast-only, non-colliding body. */
563
- physicsCreateBody(entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number
564
- physicsSetPickable(bodyId: number, pickable: boolean): void
565
- /** Surface friction of one body (0 = ice, ~1 = grippy asphalt). Values COMBINE as sqrt(a * b), so
566
- * a low value on either side dominates. New bodies start at 0.6 — a neutral solid surface.
567
- * The vehicle wheel cast reads the GROUND body's value — this is what caps a car's cornering. */
568
- physicsSetFriction(bodyId: number, friction: number): void
569
- physicsGetFriction(bodyId: number): number
570
- /** Ray vs pickable bodies → hit entity id (0 = miss); fills `out` = [px,py,pz,nx,ny,nz,fraction]. */
571
- physicsRaycast(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDist: number, out?: Float32Array): number
572
- /** Dev-time dump of the static collision geometry (docs/navmesh-plan.md §4) — the input of
573
- * `lecodes navmesh bake`: every STATIC solid body's triangles in world space, as an NGEO file at
574
- * `outPath`. `entityIds`/`areas` are per-entity overrides (area 0..15, 255 unwalkable, 254 skip).
575
- * Returns the triangle count (-1 = failed). Desktop hosts with physics only. */
576
- physicsStaticGeometry?(outPath: string, entityIds: Uint32Array, areas: Uint8Array): number
577
- // CharacterController (Jolt CharacterVirtual). The character steps on the engine's FIXED clock like
578
- // every body (frame-rate independent) and is render-interpolated; the engine owns its gravity, so
579
- // the SDK never ticks it. Both velocity halves are LATCHED STATE, never one-shot events — JS runs
580
- // once per frame while the sim runs 0..4 sub-steps, so a one-shot would double-apply or vanish.
581
- // groundState: 0 OnGround / 1 OnSteepGround / 2 NotSupported / 3 InAir.
582
- characterCreate(entityId: number, shapeId: number, maxSlopeDeg: number): number
583
- characterDestroy(charId: number): void
584
- /** This frame's HORIZONTAL command (world units/s), cleared once a step consumes it — no command
585
- * means standing still, not coasting. Held across the frame's sub-steps, and it takes the axis
586
- * back from a latched velocity: the last writer owns X/Z. */
587
- characterMove(charId: number, x: number, z: number): void
588
- /** The same command including the vertical — free mode (gravityScale 0): swimming / flying. */
589
- characterMoveFree(charId: number, x: number, y: number, z: number): void
590
- /** Seed the LATCHED ballistic vertical (jump / dash). No ground check. */
591
- characterSetVerticalVelocity(charId: number, vy: number): void
592
- /** Latch the whole velocity — it persists until a characterMove takes the axis back (knockback,
593
- * wall jump, launch pad, weightless flight). Gravity still acts on the vertical. */
594
- characterSetVelocity(charId: number, x: number, y: number, z: number): void
595
- /** Multiplier over the world gravity; 0 = free mode, which ALSO disables stick-to-floor + stairs. */
596
- characterSetGravityScale(charId: number, scale: number): void
597
- characterSetMaxSlope(charId: number, maxSlopeDeg: number): void
598
- /** Swap the collider live (crouch / stand up), keeping the FEET planted. Returns false when the new
599
- * shape doesn't fit where the character stands — nothing changed, so the caller retries later and
600
- * that retry is an exact headroom test. Optional: a host without it can't resize a character. */
601
- characterSetShape?(charId: number, shapeId: number): boolean
602
- /** The velocity the solver ENDED UP with after the last step (post-collision), not the command. */
603
- characterGetVelocity(charId: number, out: Float32Array): void
604
- characterGetGroundState(charId: number): number
605
- /** Discontinuous move (spawn / respawn / teleport) — also resets the interpolation pair. */
606
- characterSetPosition(charId: number, x: number, y: number, z: number): void
607
- // Vehicle (Jolt VehicleConstraint + WheeledVehicleController). One settings BLOB, so tuning knobs
608
- // never grow this ABI — layout (floats):
609
- // header[21]: version(5), mass, comAuto, comY,
610
- // engTorque, engMaxRpm, engIdleRpm, engInertia, engBraking, clutchStrength,
611
- // diffRatio (<= 0 = a fully OPEN differential),
612
- // antiRoll (the bar's stiffness as a FRACTION of the wheel spring; 0 = no bars),
613
- // maxTiltDeg,
614
- // steerLockDeg, steerAtSpeedDeg, steerSpeedMs, steerRateDeg (speed-sensitive
615
- // steering: the lock falls to steerAtSpeedDeg by steerSpeedMs and the wheels turn
616
- // no faster than steerRateDeg per second; 0/0/0 = the raw lock, instantly),
617
- // aeroDownforce, aeroDrag (each a fraction of the car's own WEIGHT at 30 m/s, scaled
618
- // by v² from there; 0/0 = no aero, Jolt's own behaviour),
619
- // wheelCount, curveCount
620
- // + curveCount * 2: the engine's normalized torque curve (x = rpm/maxRpm, y = torque/maxTorque);
621
- // 0 points keeps Jolt's default
622
- // + wheelCount * 15: px, py, pz, radius, width, maxSteerDeg, drive, brakeTorque, handBrakeTorque,
623
- // axle, travel, stiffness, damping, grip, tireCurve (0 = road, 1 = arcade)
624
- // NO gear list and no wheel node ids: the gearbox is the SDK's (see vehicleSetTransmission) and the
625
- // SDK poses the wheel nodes itself. Chassis space is forward -Z / up +Y (matching node.forward);
626
- // wheel positions are suspension attachment points in unscaled chassis space; `axle` pairs wheels
627
- // for the differentials + anti-roll bars.
628
- vehicleCreate(entityId: number, shapeId: number, settings: Float32Array): number
629
- vehicleDestroy(vehicleId: number): void
630
- /** forward/right in [-1,1], brake/handBrake in [0,1]. Sticky; any non-zero input wakes the car. */
631
- vehicleSetInput(vehicleId: number, forward: number, right: number, brake: number, handBrake: number): void
632
- /** The SDK's gearbox, latched and applied once per fixed step: `ratio` is the ONE ratio the car is
633
- * running (0 = neutral, negative = reverse — the engine never sees a gear list) and `clutch` is
634
- * the 0..1 shift envelope scaling the clutch in the coupled engine/wheel solve. */
635
- vehicleSetTransmission(vehicleId: number, ratio: number, clutch: number): void
636
- /** Re-apply the TUNABLE half of the blob (same layout) to a live car — differential, per-wheel grip,
637
- * engine torque/RPM/curve, clutch strength, steering lock/taper/rate, brake torques, tilt limit.
638
- * Structural values (mass, centre of mass, wheel geometry, driven wheels, suspension, anti-roll)
639
- * are ignored: those need a re-create. */
640
- vehicleSetTuning?(vehicleId: number, settings: Float32Array): void
641
- /** out = [speed, rpm, wheelsInContact, vx, vy, vz] + per wheel
642
- * [contact, slipLong, slipAngleDeg, suspensionLength, steerDeg, spin]. The last three per wheel are
643
- * its VISUAL POSE — the SDK's Wheel aspect builds the node transform from them. */
644
- vehicleGetState(vehicleId: number, out: Float32Array): void
645
- /** Teleport upright and clear all motion (velocities, engine RPM, gear, wheel spin). */
646
- vehicleReset(vehicleId: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number): void
647
- /** The chassis rigid body, for the plain body calls (physicsApplyImpulse, …). 0 if unknown. */
648
- vehicleBodyId(vehicleId: number): number
649
- // Ragdoll (Jolt Ragdoll): a Model's skeleton handed to physics — one dynamic body per listed bone
650
- // (a capsule from the bone's origin to the next joint) joined to its parent part by a swing-twist
651
- // constraint. Built ONCE from the pose the bones are in (that pose is the joints' neutral for the
652
- // limits) and kept out of the world until ragdollActivate, which takes the bones' CURRENT pose,
653
- // adds the bodies and gives every body one velocity; from then on the engine writes the bodies'
654
- // poses onto the bone entities every frame AFTER the animator (bones not listed keep their
655
- // animated local pose under the ragdolled parents — fingers, toes, a spine bone between two
656
- // parts), until ragdollDeactivate. Every body carries its bone entity as user data, so
657
- // physicsRaycast / contact events name the bone. Optional: a host without it has no ragdolls.
658
- // bones[partCount * 2]: bone entity, `to` entity (0 = a leaf: `length` along the parent's line;
659
- // a leaf ROOT points along the model's up)
660
- // settings — header[12]: version(2), partCount, stride(12), friction, linearDamping,
661
- // angularDamping (0 = Jolt's own), collide (0 = everything, 1 = the STATIC world and
662
- // other such ragdolls only: dynamic bodies, character controllers and vehicles pass
663
- // through — Jolt's DEBRIS object/broadphase layer), freeze (1 = once every part is
664
- // asleep the bodies turn STATIC where they lie: the last pose keeps being written
665
- // onto the bones, raycasts still hit, nothing wakes them, the solver skips them;
666
- // ragdollActive reads false, ragdollDeactivate / ragdollActivate still work —
667
- // activate makes the parts dynamic again), freezeAfter (seconds in the world after which the freeze happens
668
- // regardless; 0 = no cap), reserved x3. A version-1 blob (header[8], stops after
669
- // angularDamping) is still accepted = collide everything, never freeze.
670
- // + partCount * 12: parentIndex (-1 = the root; always < the part's own index), radius, length
671
- // (0 = up to the `to` bone), mass (kg; 0 = from the volume), swingDeg (cone
672
- // half-angle), twistDeg (half-angle), hinge (0/1), hingeAxisX/Y/Z (in the MODEL
673
- // node's space), hingeMinDeg, hingeMaxDeg — a hinge is a swing-twist whose cone is
674
- // flat (2°) across the axis and [min, max] about it (a knee, an elbow)
675
- ragdollCreate?(rootEntityId: number, bones: Uint32Array, settings: Float32Array): number
676
- ragdollDestroy?(ragdollId: number): void
677
- /** Pose the bodies from the bones' current transforms, add them to the world, set every body's
678
- * linear velocity. Already active = re-wake + velocity. false = unknown id / no world. */
679
- ragdollActivate?(ragdollId: number, vx: number, vy: number, vz: number): boolean
680
- /** Take the bodies out of the world; the animator owns the bones again from the next frame. */
681
- ragdollDeactivate?(ragdollId: number): void
682
- /** In the world AND at least one body still awake (a settled ragdoll reads false). */
683
- ragdollActive?(ragdollId: number): boolean
684
- /** The rigid body of part `index` (for physicsApplyImpulseAt / velocities). 0 if unknown. */
685
- ragdollBodyId?(ragdollId: number, index: number): number
686
- // Legacy coupled shape+body (still used by the worker RigidBody).
687
- physicsCreateBox(entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number
688
- physicsCreateSphere(entityId: number, radius: number, motionType: number, mass: number): number
689
- physicsCreateCylinder(entityId: number, halfHeight: number, radius: number, motionType: number, mass: number): number
690
- physicsSetLinearVelocity(bodyId: number, x: number, y: number, z: number): void
691
- physicsGetLinearVelocity(bodyId: number, out: Float32Array): void
692
- /** Angular velocity about each world axis, RADIANS/second — Jolt's unit; the SDK exposes degrees.
693
- * The only way to stop a spin: a position write leaves both velocities untouched. */
694
- physicsSetAngularVelocity?(bodyId: number, x: number, y: number, z: number): void
695
- physicsGetAngularVelocity?(bodyId: number, out: Float32Array): void
696
- physicsApplyImpulse(bodyId: number, x: number, y: number, z: number): void
697
- physicsApplyImpulseAt?(bodyId: number, x: number, y: number, z: number, px: number, py: number, pz: number): void
698
- physicsSetBodyPosition(bodyId: number, x: number, y: number, z: number): void
699
- /** The rotation twin (normalized host-side). Both snap the body's render-interpolation pair, so a
700
- * discontinuous move is drawn as one rather than as a one-frame slide/spin across the gap. */
701
- physicsSetBodyRotation?(bodyId: number, qx: number, qy: number, qz: number, qw: number): void
702
- physicsRemoveBody(bodyId: number): void
703
-
704
- createMediaPlayerTexture(id: number): number
705
-
706
- getName(entityId: number): string
707
- setName(entityId: number, name: string): void
708
-
709
- hasMesh(entityId: number): boolean
710
-
711
- createARController(sceneId: number, cameraId: number, mode: string, onTrack: (entityId: number, track: boolean) => void): void
712
- createRootAnchor(sceneId: number): number
713
- createAnchor(sceneId: number, physicalWidth: number, systemId: number): number
714
-
715
- // Particles. All emitter/curve config crosses as ONE Float32Array of [tag, payloadLen,
716
- // ...payload] records, parsed once in creator-particles (CPART_TAG_* in creator-particles.h; the
717
- // SDK mirror is in gl/Particles.ts, guarded by sdk/tests/particles-tags.test.ts). Unknown
718
- // tags skip by length. maxParticles 0 = default (1000).
719
- createParticleSystem(entityId: number, materialInstanceId: number, maxParticles: number): void
720
- spawnParticles(entityId: number, count: number): void
721
- setParticleSystemConfig(entityId: number, data: Float32Array): void
722
-
723
- // Projected decals (creator-gl src/decals.h; SDK gl/DecalSet.ts). A set on an entity = one
724
- // renderable of `capacity` (0 = 256) unit boxes drawn with the material instance (decal.filamat)
725
- // — each box projects its atlas cell onto the opaque scene behind it through the scene depth
726
- // buffer. A record is 23 floats in the SET entity's space: X Y Z axes scaled by the box's
727
- // width / height / depth (Z = out of the surface), centre, atlas rect u0 v0 u1 v1, tint rgba,
728
- // life (s, 0 = forever), fadeIn (s), fadeOut (s). addDecal returns the slot (0xFFFFFFFF = no
729
- // set; a full set recycles its oldest); updateDecal keeps the slot's birth time. Optional:
730
- // a host without them draws no decals (the SDK warns once).
731
- createDecalSet?(entityId: number, materialInstanceId: number, capacity: number): void
732
- addDecal?(entityId: number, record: Float32Array): number
733
- updateDecal?(entityId: number, slot: number, record: Float32Array): void
734
- removeDecal?(entityId: number, slot: number): void
735
- clearDecals?(entityId: number): void
736
- decalCount?(entityId: number): number
737
-
738
- createNoise(): number
739
- setNoiseFrequency(noiseId: number, frequency: number): void
740
- setNoiseOctaves(noiseId: number, octaves: number): void
741
- setNoiseFractalLunacrity(noiseId: number, lunacrity: number): void
742
- setNoiseFractalGain(noiseId: number, fractalGain: number): void
743
- getNoise2D(noiseId: number, x: number, y: number): number
744
- getNoise3D(noiseId: number, x: number, y: number, z: number): number
745
-
746
- captureImage(sceneId: number, onComplete: (bufferId: number, name: string, size: number) => void, onReject: () => void): void
747
- }
748
-
749
- // ---- 2D engine (sokol / creator-2d) ----------------------------------------------------------
750
- // A 2D-only project loads ONLY creator2d.wasm — never Filament.
751
- var _creator2d: {
752
- backend: string
753
- version(): number
754
- render(nowMs: number): void
755
- onUpdate(cb: (dt: number) => void): void
756
- offUpdate(cb: (dt: number) => void): void
757
- // Render-synced aspect update(dt) dispatch, called from inside c2dRender. Early = before the
758
- // physics step; late = after animations, just before draw. Single-slot per phase (the SDK's
759
- // Aspect dispatcher registers one callback that iterates its ordered updater list).
760
- setEarlyUpdate(cb: (dt: number) => void): void
761
- setLateUpdate(cb: (dt: number) => void): void
762
- /** Clock multiplier for the 2D physics step + sprite animations (see `_creator.setTimeScale`). */
763
- setTimeScale?(scale: number): void
764
- setOnAnimEvent(cb: (ev: { entityId: number, clipId: number, type: number }) => void): void
765
- // Raw scene pointer events from the host (canvas / iOS view). phase: 0 down, 1 move, 2 up, 3 cancel;
766
- // x,y in logical (CSS px / iOS point) coords relative to the surface. The SDK hit-tests + tracks.
767
- setOnPointer(cb: (phase: number, pointerId: number, x: number, y: number) => void): void
768
- setClearColor(r: number, g: number, b: number, a: number): void
769
-
770
- createScene(): number
771
- destroyScene(sceneId: number): void
772
- openScene(sceneId: number): void
773
- closeScene(): void
774
- sceneSetClearColor(sceneId: number, r: number, g: number, b: number, a: number): void
775
- addEntityToScene(sceneId: number, entityId: number): void
776
- removeEntityFromScene(sceneId: number, entityId: number): void
777
- setLayerYSort(sceneId: number, layer: number, enabled: boolean): void
778
-
779
- cameraSetPosition(sceneId: number, x: number, y: number): void
780
- cameraSetZoom(sceneId: number, zoom: number): void
781
- cameraSetRotation(sceneId: number, deg: number): void
782
- cameraScreenToWorld(sceneId: number, sx: number, sy: number): Float32Array
783
- cameraWorldToScreen(sceneId: number, wx: number, wy: number): Float32Array
784
-
785
- createEntity(): number
786
- destroyEntity(entityId: number): void
787
- setPosition(entityId: number, x: number, y: number): void
788
- setRotation(entityId: number, deg: number): void
789
- setScale(entityId: number, sx: number, sy: number): void
790
- setLayer(entityId: number, layer: number): void
791
- setZ(entityId: number, z: number): void
792
- setVisible(entityId: number, visible: boolean): void
793
- getPosition(entityId: number): Float32Array
794
-
795
- setParent(childId: number, parentId: number, keepWorld: boolean): void
796
- getParent(entityId: number): number
797
- getChildCount(entityId: number): number
798
- getChild(entityId: number, index: number): number
799
- getWorldPosition(entityId: number): Float32Array
800
- getWorldMatrix(entityId: number): Float32Array
801
- worldToLocal(entityId: number, wx: number, wy: number): Float32Array
802
- localToWorld(entityId: number, lx: number, ly: number): Float32Array
803
- getLocalTransform(entityId: number): Float32Array
804
-
805
- setSprite(entityId: number, textureId: number): void
806
- setSpriteFrame(entityId: number, u0: number, v0: number, u1: number, v1: number): void
807
- setSpriteFramePx(entityId: number, px: number, py: number, pw: number, ph: number): void
808
- setSpriteSize(entityId: number, w: number, h: number): void
809
- setSpriteAnchor(entityId: number, ax: number, ay: number): void
810
- setSpriteColor(entityId: number, r: number, g: number, b: number): void
811
- setSpriteOpacity(entityId: number, a: number): void
812
- setSpriteFlip(entityId: number, flipX: boolean, flipY: boolean): void
813
-
814
- createTexture(systemId: number, onComplete: (texId: number, w: number, h: number) => void, onReject: (e: any) => void): void
815
- getTextureWidth(texId: number): number
816
- getTextureHeight(texId: number): number
817
- destroyTexture(texId: number): void
818
- setDefaultFilter(linear: boolean): void
819
-
820
- // Textures from a _creatorCanvas surface (RGBA8, already rasterized — no decode, so synchronous).
821
- // See docs/canvas-contract.md.
822
- createTextureFromCanvas(surfaceId: number): number
823
- updateTextureFromCanvas(texId: number, surfaceId: number): void
824
-
825
- defineAnimation(entityId: number, frames: Float32Array, fps: number, loop: boolean): number
826
- playAnimation(entityId: number, clipId: number): void
827
- stopAnimation(entityId: number): void
828
- setAnimationSpeed(entityId: number, speed: number): void
829
- getAnimationFrame(entityId: number): number
830
-
831
- createTilemap(textureId: number, cols: number, rows: number, tileW: number, tileH: number, atlasCols: number, atlasRows: number, data: Int32Array): number
832
- setTile(tilemapId: number, x: number, y: number, index: number): void
833
-
834
- bulkSetPositions(ids: Uint32Array, xy: Float32Array, count: number): void
835
-
836
- // physics (Box2D v3; present only in CREATOR_2D_PHYSICS builds — physicsHasSupport() reports it).
837
- // See docs/2d-physics-plan.md. physicsHasSupport() reports the BUILD, not the world: the world is
838
- // created lazily by the first physicsCreateBody, so configure() is optional.
839
- physicsHasSupport(): boolean
840
- /** Live and non-destructive: an existing world keeps its bodies and takes the new gravity.
841
- * pixelsPerMeter only applies to a world that doesn't exist yet (global Box2D tolerance). */
842
- physicsConfigure(gx: number, gy: number, pixelsPerMeter: number, subStepCount: number): void
843
- setInterpolation(enabled: boolean): void
844
- /** type 0 contactBegin / 1 contactEnd / 2 sensorBegin / 3 sensorEnd. A contact BEGIN carries the
845
- * manifold — world point, normal pointing A→B, and the approach speed at impact; the other three
846
- * carry zeros. (The begin manifold is pre-solve, so its impulses are all zero — `speed` is the
847
- * pre-solve relative normal velocity, which is the number an impact actually wants.) */
848
- setOnPhysicsEvent(cb: (entityA: number, entityB: number, type: number, px: number, py: number, nx: number, ny: number, speed: number) => void): void
849
- physicsCreateBody(entityId: number, motionType: number): number
850
- physicsRemoveBody(bodyHandle: number): void
851
- // Shapes carry no `density` (built at density 1, so mass == area — physicsSetMass overrides) and
852
- // do carry the collision filter. category/mask are 32-bit; 0 means "everything".
853
- physicsAddBox(bodyHandle: number, hw: number, hh: number, ox: number, oy: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
854
- physicsAddCircle(bodyHandle: number, radius: number, ox: number, oy: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
855
- physicsAddCapsule(bodyHandle: number, x1: number, y1: number, x2: number, y2: number, radius: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
856
- physicsAddSegment(bodyHandle: number, x1: number, y1: number, x2: number, y2: number, friction: number, bounce: number, category: number, mask: number): void
857
- physicsAddPolygon(bodyHandle: number, pts: Float32Array, count: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
858
- /** A polyline of connected segments — long CONCAVE surfaces in one seam-free piece. STATIC bodies
859
- * only, >= 4 points; the SDK synthesises the tangent points an open chain needs, because Box2D's
860
- * first and last edges do not collide. One-sided: solid on the right of the point order. */
861
- physicsAddChain(bodyHandle: number, pts: Float32Array, count: number, isLoop: boolean, friction: number, bounce: number, category: number, mask: number): void
862
- /** Destroy every shape on a body, keeping the body (id, transform, velocity) — the first half of a
863
- * live collider rebuild. */
864
- physicsClearShapes(bodyHandle: number): void
865
- /** Contact events are OFF per body until this turns them on — otherwise every crate-on-crate pair
866
- * crosses the bridge every frame. The SDK enables it on the first 'enter'/'exit' listener. */
867
- physicsSetContactEvents(bodyHandle: number, enabled: boolean): void
868
- physicsSetLinearVelocity(bodyHandle: number, x: number, y: number): void
869
- physicsGetLinearVelocity(bodyHandle: number): Float32Array
870
- physicsSetAngularVelocity(bodyHandle: number, degPerSec: number): void
871
- physicsGetAngularVelocity(bodyHandle: number): number
872
- physicsApplyLinearImpulse(bodyHandle: number, x: number, y: number): void
873
- /** Impulse at a WORLD point — the lever arm becomes angular impulse (a central impulse never
874
- * spins a body). Mirrors 3D physicsApplyImpulseAt. */
875
- physicsApplyImpulseAt(bodyHandle: number, x: number, y: number, px: number, py: number): void
876
- physicsApplyForce(bodyHandle: number, x: number, y: number): void
877
- /** Teleport the body to its ENTITY's current world transform — how node.position / .rotation reach
878
- * it. The parent-chain math stays native instead of being re-derived in TS. Snaps prev/cur so the
879
- * renderer doesn't interpolate across the jump; the body KEEPS its velocity. */
880
- physicsSetFromEntity(bodyHandle: number): void
881
- physicsSetFixedRotation(bodyHandle: number, enabled: boolean): void
882
- physicsSetGravityScale(bodyHandle: number, scale: number): void
883
- physicsSetLinearDamping(bodyHandle: number, damping: number): void
884
- physicsSetAngularDamping(bodyHandle: number, damping: number): void
885
- physicsSetBullet(bodyHandle: number, enabled: boolean): void
886
- physicsSetEnabled(bodyHandle: number, enabled: boolean): void
887
- physicsSetAwake(bodyHandle: number, awake: boolean): void
888
- /** Scales the shape-derived mass data, so the rotational inertia keeps its ratio. <= 0 restores
889
- * the area-derived default. */
890
- physicsSetMass(bodyHandle: number, mass: number): void
891
- physicsGetMass(bodyHandle: number): number
892
- physicsSetMotionType(bodyHandle: number, motionType: number): void
893
- physicsSetFriction(bodyHandle: number, friction: number): void
894
- physicsSetBounce(bodyHandle: number, bounce: number): void
895
- physicsSetFilter(bodyHandle: number, category: number, mask: number): void
896
- // Queries. `mask` = the union of group categories the query may hit (0 = everything); `ignore` is
897
- // a list of ENTITY ids to skip, passed as floats because ids are small ints and that reuses the
898
- // one buffer helper. Box2D's closest-ray API has no exclusion, hence the explicit list.
899
- physicsRaycastClosest(x0: number, y0: number, x1: number, y1: number, mask: number, ignore: Float32Array, ignoreCount: number): Float32Array
900
- /** All hits along the segment, nearest first: maxHits records of [entityId, px, py, nx, ny, fraction]. */
901
- physicsRaycastAll(x0: number, y0: number, x1: number, y1: number, mask: number, ignore: Float32Array, ignoreCount: number, maxHits: number): Float32Array
902
- /** Overlap an arbitrary convex shape given as a point cloud + radius (Box2D's b2ShapeProxy form),
903
- * so one call covers circle / capsule / box / polygon — and the swept test for a moving circle.
904
- * Returns up to maxHits entity ids. */
905
- physicsOverlap(pts: Float32Array, count: number, radius: number, mask: number, ignore: Float32Array, ignoreCount: number, maxHits: number): Float32Array
906
- /** Topmost by DRAW order (layer, then z) — not whatever the broadphase hands back first. */
907
- physicsOverlapPoint(x: number, y: number, mask: number, ignore: Float32Array, ignoreCount: number): number
908
- // CharacterController2D (Box2D's kinematic mover). The character steps on the engine's FIXED clock
909
- // inside the same loop as the bodies, so the SDK does no per-frame work. Capsule ends are in the
910
- // character's own frame. It also owns a hidden kinematic "shadow" body so raycasts, overlaps and
911
- // sensors see it — solid shapes strip the reserved character bit from their mask, sensors add it.
912
- characterCreate(entityId: number, x1: number, y1: number, x2: number, y2: number, radius: number, maxSlopeDeg: number, category: number, mask: number): number
913
- characterDestroy(charId: number): void
914
- /** The HORIZONTAL command, world units/s — a per-frame command that EXPIRES once a step consumes it. */
915
- characterMove(charId: number, x: number): void
916
- /** Free mode (gravityScale 0): both axes as one expiring command. */
917
- characterMoveFree(charId: number, x: number, y: number): void
918
- characterSetVerticalVelocity(charId: number, vy: number): void
919
- /** LATCH the whole velocity — it persists until a characterMove takes the axis back. */
920
- characterSetVelocity(charId: number, x: number, y: number): void
921
- characterSetGravityScale(charId: number, scale: number): void
922
- characterSetMaxSlope(charId: number, maxSlopeDeg: number): void
923
- characterSetFilter(charId: number, category: number, mask: number): void
924
- /** Ignore this entity's one-way surfaces (0 = none) — dropping through a semisolid platform. */
925
- characterSetDropThrough(charId: number, entityId: number): void
926
- /** Resize keeping the FEET planted; false = it didn't fit and NOTHING changed, so the caller
927
- * retries later and that retry is an exact headroom test. */
928
- characterSetCapsule(charId: number, x1: number, y1: number, x2: number, y2: number, radius: number): boolean
929
- /** Teleport to the entity's world transform (how node.position reaches a character). Drops fall speed. */
930
- characterSetFromEntity(charId: number): void
931
- /** out8 = [velX, velY, groundState (0 ground / 1 slope / 2 air), normalX, normalY, groundEntity,
932
- * collisionCount, 0]. The velocity is MEASURED, not commanded. */
933
- characterGetState(charId: number, out: Float32Array): void
934
- /** maxN records of [entityId, normalX, normalY] — what the mover pushed out of this step. */
935
- characterGetCollisions(charId: number, maxN: number): Float32Array
936
- /** Mark a body's surfaces one-way: solid only from the side (nx, ny) points to, within arcDeg.
937
- * Honoured by the mover and, via a native pre-solve callback, by ordinary rigid bodies. */
938
- physicsSetOneWay(bodyHandle: number, nx: number, ny: number, arcDeg: number, enabled: boolean): void
939
- pickSprite(sceneId: number, wx: number, wy: number): number
940
- }
941
-
942
- // ---- multiplayer transport (creator-net / yojimbo) -------------------------------------------
943
- // A byte pipe pumped on the JS thread (docs/multiplayer-plan.md). Present only on hosts built
944
- // with CREATOR_PKG_NET (desktop, the 3d/full Android variants, iOS); the SDK feature-detects it.
945
- // One endpoint per process: `listen` (server / host) or `connect` (client), never both.
946
- var _creatorNet: {
947
- /** What the exe was launched as (`--server --port N` / `--connect ADDR`), or null. */
948
- launch(): { role: string, address: string, port: number, maxClients: number } | null
949
- /** Serve on every interface at `port` (wildcard public address). */
950
- listen(port: number, maxClients: number): boolean
951
- /** Insecure (dev / LAN) connect to "ip:port"; the outcome arrives as a poll record. */
952
- connect(address: string): boolean
953
- disconnect(): void
954
- kick(client: number): void
955
- /** Queue a payload: channel 0 reliable-ordered, 1 unreliable; `client` ignored on a client.
956
- * A string travels as UTF-8 text and arrives as a string; bytes arrive as an ArrayBuffer. */
957
- send(client: number, channel: number, data: string | ArrayBuffer | Uint8Array): boolean
958
- /** Pump once and return every record since the last call as a flat array of 4-tuples
959
- * [kind, client, arg, payload] — kind 1 connected · 2 disconnected · 3 message · 4 connectFailed;
960
- * arg = channel (message) or reason code; payload = string | ArrayBuffer | null. */
961
- poll(): any[] | null
962
- /** 0 idle · 1 client · 2 server */
963
- mode(): number
964
- /** Client: 0 disconnected · 1 connecting · 2 connected · 3 failed. A listening server: 2. */
965
- state(): number
966
- /** [connected, rttMs, lossPct, sentKbps, receivedKbps] for a slot (server) / the link (client). */
967
- clientInfo(client: number): Float32Array
968
- /** yojimbo network simulator on this side of the wire. */
969
- simulate(latencyMs: number, jitterMs: number, lossPercent: number): void
970
- reasonString(reason: number, serverSide: boolean): string
971
- }
972
-
973
- // ---- navigation meshes (creator-nav / Recast+Detour) -----------------------------------------
974
- // Runtime only (docs/navmesh-plan.md §5): load a baked `.navmesh` (LNAV) from a fetch system id,
975
- // query it, run a DetourCrowd of agents one fixed step at a time. Present on hosts built with
976
- // CREATOR_PKG_NAV (desktop, the 3d/full Android variants, iOS) and on every web host through the
977
- // wasm build; the SDK feature-detects it. Parameter blocks are fixed-order Float32Arrays
978
- // (creator-nav.h CNAV_AGENT_* / CNAV_READ_*). Handles (meshId / crowdId, from 1) die with the world.
979
- var _creatorNav: {
980
- /** Web hosts only: loads the wasm lazily. Absent on native hosts — call it when present. */
981
- ready?(): Promise<void>
982
- lastError(): string
983
- /** LNAV bytes already fetched (the createGlb pattern) → meshId, 0 on failure. */
984
- load(systemId: number): number
985
- unload(meshId: number): void
986
- /** The file's JSON header: agent size, cell, bounds, stats, `meta` (the CLI's provenance). */
987
- header(meshId: number): string
988
- polyCount(meshId: number): number
989
- setAreaCost(meshId: number, area: number, cost: number): void
990
- /** Straight path from → to into `out` (xyz per corner); returns the corner count, NEGATIVE when
991
- * the path is partial (the target is unreachable — it ends at the closest polygon). `ex <= 0`
992
- * = default search extents. */
993
- findPath(meshId: number, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, ex: number, ey: number, ez: number, include: number, exclude: number, maxCorners: number, out: Float32Array): number
994
- nearest(meshId: number, x: number, y: number, z: number, ex: number, ey: number, ez: number, include: number, exclude: number, out: Float32Array): boolean
995
- /** Walkability ray along the surface; true = BLOCKED, out = [hit xyz, wall normal xyz]. */
996
- raycast(meshId: number, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, include: number, exclude: number, out: Float32Array): boolean
997
- /** `radius <= 0` = anywhere on the mesh; deterministic per seed. */
998
- randomPoint(meshId: number, cx: number, cy: number, cz: number, radius: number, seed: number, include: number, exclude: number, out: Float32Array): boolean
999
- /** The polygon mesh as a triangle soup (xyz per vertex) for debug drawing. */
1000
- debugTriangles(meshId: number): Float32Array
1001
- crowdCreate(meshId: number, maxAgents: number, maxRadius: number): number
1002
- crowdDestroy(crowdId: number): void
1003
- /** → the agent's slot (0..maxAgents-1), -1 when full / off the mesh. */
1004
- crowdAdd(crowdId: number, x: number, y: number, z: number, params: Float32Array): number
1005
- crowdRemove(crowdId: number, agent: number): void
1006
- crowdSetParams(crowdId: number, agent: number, params: Float32Array): void
1007
- crowdSetTarget(crowdId: number, agent: number, x: number, y: number, z: number): boolean
1008
- crowdSetVelocity(crowdId: number, agent: number, vx: number, vy: number, vz: number): void
1009
- crowdResetTarget(crowdId: number, agent: number): void
1010
- crowdWarp(crowdId: number, agent: number, x: number, y: number, z: number): boolean
1011
- /** Once per fixed step. */
1012
- crowdUpdate(crowdId: number, dt: number): void
1013
- /** Every slot into `out` (CNAV_AGENT_READ_STRIDE floats each); returns maxAgents. */
1014
- crowdRead(crowdId: number, out: Float32Array): number
1015
- }
1016
-
1017
- // ---- Game audio (creator-audio) ----------------------------------------------------------------
1018
- // docs/audio-plan.md. Whole clips decoded up front, a fixed voice pool (play allocates nothing),
1019
- // 3D sources the engine follows per frame, mixer buses with insert effects, reverb zones,
1020
- // occlusion. OPTIONAL as a block: the SDK gates on `typeof _creatorAudio !== 'undefined' &&
1021
- // hasSupport()` and stays inert (silent Sound, inert Voice) without it. Present on hosts built with
1022
- // CREATOR_PKG_AUDIO (desktop first; Android / Apple pending — parity `audio-game`). The headless
1023
- // renderer provides a RECORDER (plays are logged, nothing is mixed). Every parameter block is a
1024
- // fixed-order Float32Array (creator-audio.h CAUD_SRC_* / the effect param orders).
1025
- var _creatorAudio: {
1026
- hasSupport(): boolean
1027
- /** Decode the bytes behind a fetch system id (WAV / MP3 / FLAC / OGG Vorbis) on the engine's
1028
- * loader thread; mono unless `stereo`. onDone(clipId, durationSeconds, channels). */
1029
- loadClip(systemId: number, stereo: boolean, onDone: (clipId: number, duration: number, channels: number) => void, onReject: (message: string) => void): void
1030
- releaseClip(clipId: number): void
1031
- /** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. */
1032
- busId(name: string): number
1033
- /** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. */
1034
- createBus(name: string): number
1035
- setBusVolume(bus: number, volume: number): void
1036
- setBusMuted(bus: number, muted: boolean): void
1037
- /** kind 1 reverb [roomSize, damping, width, mix, preDelay] · 2 echo [delay, decay, mix] ·
1038
- * 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. */
1039
- setBusEffect(bus: number, kind: number, params: Float32Array): void
1040
- stopBus(bus: number, fade: number): void
1041
- /** A 3D emitter. `attachSource` binds it to an entity the engine follows every frame;
1042
- * `setSourcePosition` places an unbound one (playAt). */
1043
- createSource(): number
1044
- attachSource(sourceId: number, entityId: number): void
1045
- /** [minDistance, maxDistance, rolloff (0 none 1 inverse 2 linear 3 exp), coneInnerDeg,
1046
- * coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. */
1047
- setSourceParams(sourceId: number, params: Float32Array): void
1048
- setSourcePosition(sourceId: number, x: number, y: number, z: number): void
1049
- stopSource(sourceId: number, fade: number): void
1050
- sourceVoices(sourceId: number): number
1051
- destroySource(sourceId: number): void
1052
- /** The listener entity; 0 = the active scene camera (the default). */
1053
- setListener(entityId: number): void
1054
- setListenerOptions(dopplerFactor: number): void
1055
- /** HRTF binaural rendering for the `maxVoices` nearest spatial voices (headphones); the rest keep panning. */
1056
- setHrtf(enabled: boolean, maxVoices: number): void
1057
- /** Whether the engine's time scale (Time.scale) also pitches the sfx bus. Default false (pause = mute only). */
1058
- setTimeScalePitch(enabled: boolean): void
1059
- /** → a voice token (0 = nothing played: the pool refused, the clip is not ready). bus -1 = the
1060
- * source's bus (sfx for 2D). pan is 2D only. */
1061
- play(clipId: number, sourceId: number, bus: number, volume: number, pitch: number, loop: boolean, priority: number, fadeIn: number, startAt: number, pan: number): number
1062
- setVoiceVolume(token: number, volume: number): void
1063
- setVoicePitch(token: number, pitch: number): void
1064
- setVoicePan(token: number, pan: number): void
1065
- stopVoice(token: number, fade: number): void
1066
- voicePlaying(token: number): boolean
1067
- voiceTime(token: number): number
1068
- /** THE ended listener (single slot per JS world): called once per tick with the tokens of the
1069
- * voices that ended since the previous tick — natural end, stop, steal. */
1070
- setOnEnded(callback: ((tokens: Float32Array) => void) | null): void
1071
- stopAll(fade: number): void
1072
- /** A reverb volume: shape 0 box (dims = full size) / 1 sphere (dims[0] = diameter), in the
1073
- * entity's local units; `blend` metres of crossfade inside the border; reverb params as for
1074
- * setBusEffect kind 1. The listener inside blends the zone's reverb onto `bus`. */
1075
- createZone(): number
1076
- attachZone(zoneId: number, entityId: number): void
1077
- setZone(zoneId: number, shape: number, dims: Float32Array, blend: number, bus: number, reverb: Float32Array): void
1078
- destroyZone(zoneId: number): void
1079
- /** [voicesPlaying, voicesMono, voicesStereo, stolen, clips, clipBytes, peak, sampleRate, listenerZone, zoneBlend, hrtfVoices]. */
1080
- stats(): Float32Array
1081
- }
1082
-
1083
- // ---- UI engine (creator-ui) ------------------------------------------------------------------
1084
- var _creatorUI: {
1085
- // Global back-press fallback (app.onBackPressed): a PROPERTY the SDK writes, not a call —
1086
- // the engine reads it off this world's _creatorUI object at the END of its back chain
1087
- // (widget → destination → pager → router → this), so it dies with the world's context.
1088
- // Old hosts never read it and the handler is silently inert there.
1089
- _backButtonCallback?: () => void
1090
- openScreen(screen: object): void
1091
- closeScreen(): void
1092
- updateStyle(nodeId: number, prop: string, value: any): void
1093
- mergeStyle(nodeId: number, style: object): void
1094
- // Toggle a user style class (a `$`-block declared in .style()) on a node. `name` arrives
1095
- // WITHOUT the leading `$`. Class state CASCADES down the node tree: a class set on a node is
1096
- // active on all its descendants within the same root (screens/widgets are separate roots) —
1097
- // implemented ONCE in creator-ui's setNodeClass (docs/style-class-cascade-plan.md); hosts must
1098
- // NOT layer their own inheritance on top. `$pressed`/`$focused` are reserved cascading classes
1099
- // hosts toggle alongside the built-in onPressed/onFocused (creator-ui setNodePressed/Focused).
1100
- setClass(nodeId: number, name: string, enabled: boolean): void
1101
-
1102
- // Theme variables (docs/ui-theme-plan.md). MERGES `values` into the app theme table and
1103
- // re-resolves live styles: keys are var names (`var(--name)` in style values reads them;
1104
- // the four `comfort-*` keys are the comfort knobs), null removes a key, numbers are lengths
1105
- // (px). Env names (safe-*, vw/vh/vmin/vmax) are reserved — hosts ignore writes to them.
1106
- // Optional during rollout: the SDK feature-detects.
1107
- setTheme?(values: Record<string, string | number | null>): void
1108
-
1109
- // Content-property push. Props: "value" (input/textarea text), "focus" (boolean — focus/blur
1110
- // the input, opening/dismissing the keyboard; rides this channel so programmatic focus needs
1111
- // no new ABI method), plus element-specific ones ("src", "text", …).
1112
- // An image "src" (here and at creation) is a url string, { _id } (a host buffer), { svg },
1113
- // { canvasSurface } (a baked Canvas), or { scene2d: sceneId } — a LIVE 2D scene the host draws
1114
- // into the node's laid-out box every frame with the scene's own camera (native: c2dDrawSceneGL
1115
- // into a per-node framebuffer; no intrinsic size, the scene needs no openScene). The engine's
1116
- // simulation must still step each frame for it (native: c2dTick when nothing is presented).
1117
- updateNode(nodeId: number, prop: string, value: any): void
1118
- setSourceRect(nodeId: number, x: number, y: number, w: number, h: number): void
1119
-
1120
- // Canvas.update() → live-refresh every UIImage(canvas) node currently showing this baked surface:
1121
- // the host re-reads the re-rasterized pixels into the image view WITHOUT a relayout (the surface
1122
- // keeps its id and size across an update; a resize goes through a full `src` re-assign). Optional
1123
- // during rollout — the SDK feature-detects and falls back to re-assigning `src`.
1124
- refreshCanvasSurface?(surfaceId: number): void
1125
-
1126
- // Font registration is fire-and-forget in practice: hosts load the face, then re-measure /
1127
- // re-layout when it lands (docs/font-system-plan.md — swap is the fallback; boot faces are
1128
- // prefetched via the bundle's `// fonts:` header). The callbacks stay on the wire for old
1129
- // bundles that gate on them — hosts must keep invoking onComplete. Repeated registration of
1130
- // the same (family, weight, style) is a cheap no-op — hosts dedupe.
1131
- registerFont(fontFamily: string, url: string, options: any, onComplete: () => void, onReject: () => void): void
1132
- // Preferred container format for CDN-served faces: "woff2" on browser hosts, absent (= "ttf")
1133
- // on native/headless. The font() macro's generated registration code reads it to pick the
1134
- // file extension; the `// fonts:` header carries a `{fmt}` placeholder for the same choice.
1135
- fontFormat?: "woff2" | "ttf"
1136
-
1137
- isButtonPressed(nodeId: number): boolean
1138
-
1139
- registerTouchStartEvent(callback: (...args: any) => void): void
1140
- registerTouchEndEvent(callback: (...args: any) => void): void
1141
- registerResizeEvent(callback: (...args: any) => void): void
1142
- // Current display size [width, height] in logical px — the same values the resize event delivers.
1143
- // Surfaced as device.width / device.height. Optional: hosts that don't track it (headless) omit it,
1144
- // and the getters fall back to 0.
1145
- getDisplaySize?(): [number, number]
1146
-
1147
- getTextValue(nodeId: number): string
1148
-
1149
- // Absolute device-space rect [left, top, width, height] of a laid-out node, in logical px —
1150
- // the same space UIWidget top/left position in — INCLUDING scroll offsets: computed live at
1151
- // call time, never cached from layout events. null (or undefined) when the node isn't
1152
- // mounted/laid out. Backs el.getBoundingClientRect(), the widget-anchoring primitive
1153
- // (dropdowns, popovers, tooltips — docs/ui-components-plan.md §3.2). Optional: the SDK
1154
- // feature-detects and returns null on hosts without it.
1155
- getBoundingClientRect?(nodeId: number): [number, number, number, number] | null
1156
-
1157
- insertNode(index: number, nodeId: number, childNode: any): void
1158
- removeNode(nodeId: number, childNode: any): void
1159
- setContent(nodeId: number, oldChildren: any[], children: any[]): void
1160
-
1161
- animateTo(nodeId: number, style: any): void
1162
- animateFrom(nodeId: number, style: any): void
1163
- toast(msg: string): void
1164
-
1165
- // Router over destinations. On hosts that implement openView (below), `screen`/`view` is any
1166
- // view descriptor (not just a screen object) and the extra `transition` args apply; legacy
1167
- // hosts receive screen objects only and ignore the extras.
1168
- routerOpen(view: any, onChange: (view: any) => void, showBackButton: boolean): void
1169
- routerPush(view: any, transition?: string | object): void
1170
- routerReplace(view: any, transition: string | object): void
1171
- routerPop(index: number, transition?: string | object): void
1172
- routerHide(): void
1173
- routerRestore(): void
1174
-
1175
- // ---- Presentable navigation (docs/navigation-presentable-plan.md) --------------------------
1176
- // One visible destination at a time. `view` is either a screen node object (type "screen")
1177
- // or a descriptor:
1178
- // { type: "scene3d", sceneId } { type: "scene2d", sceneId }
1179
- // { type: "native", viewName, params, viewId } { type: "videoView", node }
1180
- // Descriptors carry `_p` (the SDK Presentable instance): hand the SAME object back through
1181
- // router onChange, and fire its `ol`/`cl` arrays on present/dismiss (screens carry ol/cl
1182
- // themselves). Presenting a scene descriptor includes activating the engine (the openScene
1183
- // equivalent); while covered, scene rendering pauses. `transition` is a TransitionName string
1184
- // or a TransitionSpec object (see sdk/src/ui/presentable.ts).
1185
- // Optional during the migration: the SDK feature-detects openView and falls back to
1186
- // openScreen/openScene; hosts implementing openView should keep openScreen/routerPush(screen)
1187
- // as thin aliases into it for previously compiled bundles.
1188
- openView?(view: object, transition: string | object): void
1189
- closeView?(): void
1190
-
1191
- // NativeView plugin channel (host `registerView(name, factory)` capabilities). `args`, the
1192
- // result, and event payloads are JSON strings. Events are delivered by invoking the arrays in
1193
- // the element's `nvl[event]` or via its `_emitViewEvent(event, json)`.
1194
- isViewSupported?(name: string): boolean
1195
- viewCall?(viewId: number, method: string, args: string, onComplete: (result?: string) => void, onError: (err: string) => void): void
1196
-
1197
- // `owner` (a view descriptor) binds the widget to a destination: it mounts inside the owner's
1198
- // page — shows/hides with it and rides its transition. Omitted = global overlay (legacy
1199
- // behavior; legacy hosts ignore the arg).
1200
- showWidget(widget: object, owner?: object): void
1201
- hideWidget(widget: object): void
1202
-
1203
- vlistMount(jsNode: any, key: string, subtree: any): void
1204
- vlistSetKeys(jsNode: any, keys: string[], estimates: number[]): void
1205
- vlistInsertKeys(jsNode: any, index: number, keys: string[], estimates: number[]): void
1206
- vlistInvalidate(jsNode: any, k: string): void
1207
- vlistRemoveKeys(jsNode: any, keys: string[]): void
1208
- command(jsNode: any, command: string, ...args: any): void
1209
- }
1210
-
1211
- // ---- Platform utilities ----------------------------------------------------------------------
1212
- var _creatorUtils: {
1213
- // `fetchOptions.body` is either a string or a FormData: an object carrying `_entries`, an array
1214
- // of [key, value, filename?]. A `value` object carrying a numeric `_id` is binary (that's the
1215
- // host buffer id — SDK `File` and `FetchResponse` both expose it under that key); anything else
1216
- // is a text field. Same `_id` convention as an image `src` object (see _creatorUI.updateNode).
1217
- fetch(url: string, fetchOptions: any, onComplete: (systemId: number, statusCode: number) => void, onReject: () => void): void
1218
- fetchLocal(path: string): number
1219
- fetchToJson(systemId: number): any
1220
- fetchSlice(systemId: number, start: number, end: number): any
1221
- fetchToText(systemId: number): string
1222
- /** The fetched bytes as a Uint8Array copy (a `.terrain` file, any binary the SDK parses itself).
1223
- * Optional: an older host has only the text/JSON readers (Terrain.load throws there). */
1224
- fetchToBytes?(systemId: number): Uint8Array
1225
- disposeFetch(systemId: number): void
1226
- // ---- Local filesystem (the SDK `files` global) ----------------------------------------------
1227
- // The read/write/delete half of `fetchLocal`, for hosts where the app OWNS a filesystem
1228
- // (desktop). All five are OPTIONAL and stand or fall together: a sandboxed host (web, mobile)
1229
- // implements none of them and `files.supported` is false there.
1230
- //
1231
- // ASYNC, like `fetch`: file IO must not block the frame, so each takes an onComplete/onReject
1232
- // pair (the SDK turns them into a promise) and the host does the work off the JS thread. Hosts
1233
- // must keep the ops ORDERED — two appends to one file settle in call order — so a queue, not a
1234
- // thread per call. Exactly one handle is ever settled.
1235
- //
1236
- // PATH POLICY, identical for all five and owned by the host: an ABSOLUTE path is used verbatim;
1237
- // a RELATIVE one is resolved against the app's OWN folder — never the process cwd — which for a
1238
- // packaged desktop app is the exe's directory, one of the directories `fetchLocal` searches by
1239
- // name. So `writeLocal("save.json", …)` and `fetchLocal("save.json")` are a pair.
1240
- /** Write a file, creating the parent folders. `append` adds to the end instead of replacing.
1241
- * `data` is text (written as UTF-8), raw bytes (Uint8Array / ArrayBuffer), or a host buffer
1242
- * object carrying `_id` (an SDK `File` / `FetchResponse` — so a picked or generated file is
1243
- * saved as it is); the bytes are only valid for the duration of the call, so a host that
1244
- * defers the write copies them first. The core drops the path's cached `fetchLocal` id, so
1245
- * the next read sees the new bytes. */
1246
- writeLocal?(path: string, data: string | Uint8Array | ArrayBuffer | { _id: number }, append: boolean, onComplete: () => void, onReject: (err: Error) => void): void
1247
- /** The whole file as bytes, or null when there is no such file. Unlike `fetchLocal` this
1248
- * re-reads every call and holds nothing — no buffer id to dispose. Only a real failure
1249
- * (an unreadable file) rejects; "not there" is a null result. */
1250
- readLocalBytes?(path: string, onComplete: (bytes: Uint8Array | null) => void, onReject: (err: Error) => void): void
1251
- /** The whole file decoded as UTF-8 text, or null when there is no such file. */
1252
- readLocalText?(path: string, onComplete: (text: string | null) => void, onReject: (err: Error) => void): void
1253
- /** Delete a file, or an EMPTY directory; `recursive` also deletes a directory's contents.
1254
- * Deleting something that is already gone SUCCEEDS — the post-condition is what matters. */
1255
- deleteLocal?(path: string, recursive: boolean, onComplete: () => void, onReject: (err: Error) => void): void
1256
- /** Create a directory and any missing parents. An existing directory is success. */
1257
- mkdirLocal?(path: string, onComplete: () => void, onReject: (err: Error) => void): void
1258
- openFilePicker(onComplete: (res: any) => void, onReject: () => void, multiple: boolean, accept?: string): void
1259
- // Present the OS media share sheet for a host buffer (`systemId` — a File/response id), with an
1260
- // optional caption. iOS: the share sheet also offers "Save to Files"/Photos. Web: saves (downloads)
1261
- // the file. Backs the SDK `share(media, text?)`.
1262
- shareMedia(systemId: number, text?: string): void
1263
-
1264
- /** Prompt for camera permission (resolve = granted, reject = denied). Shared by everything that
1265
- * needs the camera — ARScene.prepare, QRScanner, CameraView — hence a platform utility rather
1266
- * than a 3D-engine call (it moved off `_creator` 2026-07-15). Optional: hosts whose view
1267
- * factory prompts on its own (web getUserMedia) don't implement it and the SDK resolves
1268
- * immediately, so callers go through `_requestCameraPermission()`. */
1269
- requestCamera?(onComplete: () => void, onReject: () => void): void
1270
-
1271
- // (openScanner/closeScanner removed 2026-07-15 — QRScanner rides the registered "qrScanner"
1272
- // NativeView; hosts keep the old handlers only for previously compiled bundles.)
1273
- // ---- Input (docs/input-plan.md — keyboard / mouse / gamepad behind the SDK `Input` global) ----
1274
- // Design: a BUTTON is anything that goes down and up — keyboard keys, mouse buttons, gamepad
1275
- // buttons — and they share ONE code vocabulary (KeyboardEvent.code + "MouseLeft|Right|Middle|
1276
- // Back|Forward" + the W3C standard-gamepad "GamepadSouth|East|West|North|L1|R1|L2|R2|Select|
1277
- // Start|L3|R3|Up|Down|Left|Right"), one held-poll and one event pair. Continuous signals (mouse
1278
- // motion, sticks, triggers) are numbered channels behind one poll. Bindings/axes/edge detection
1279
- // are SDK-side; hosts only report physical state.
1280
- /** Is this button held right now? `gamepad` (0..3) scopes a Gamepad* code to one pad; omitted =
1281
- * any connected pad. Keyboard/mouse codes ignore it. */
1282
- inputKey: (code: string, gamepad?: number) => boolean
1283
- /** Continuous input poll by channel id (SDK `InputChannel`): 0 MouseX, 1 MouseY (logical px,
1284
- * last known cursor position inside the viewport), 2 MouseDX, 3 MouseDY (motion accumulated
1285
- * during the PREVIOUS frame — unaccelerated/raw where the OS offers it, counts while locked),
1286
- * 4 WheelX, 5 WheelY (wheel notches, previous frame), 6 PointerLocked (0/1), 7 GamepadCount,
1287
- * 8 PointerOnUI (0/1: the primary pointer went DOWN on something the UI claimed — an
1288
- * interactive node, a scrollable, an editable, a modal backdrop — and is still held; stays 1
1289
- * wherever the cursor goes until the release, so a drag that started on a HUD control never
1290
- * becomes camera look; always 0 while pointer-locked and for touch-only hosts without a UI claim);
1291
- * gamepad p at 16 + 16·p: +0 Connected, +1 LeftX, +2 LeftY, +3 RightX, +4 RightY (−1..1,
1292
- * +Y = down like the web), +5 LeftTrigger, +6 RightTrigger (0..1). Unknown channel → 0.
1293
- * "Previous frame" = the host's own frame boundary (the event pump / rAF / fixed step), so
1294
- * every read within one frame agrees. Optional: a host without it has no pointer/gamepad. */
1295
- inputRead?(channel: number): number
1296
- /** Request (true) / release (false) pointer lock: hide + confine the cursor, keep MouseDX/DY
1297
- * flowing (FPS look). Returns whether the request was accepted now (web needs a user gesture);
1298
- * the eventual state is channel 6. Hosts release on focus loss and re-acquire on focus. */
1299
- inputSetPointerLock?(on: boolean): boolean
1300
- /** Register THE input-event listener (single slot per JS world, like registerAppEvent — the SDK
1301
- * fans out). kind: 0 keydown, 1 keyup, 2 gamepadconnected, 3 gamepaddisconnected. `code` is the
1302
- * button code above ("" for connect events); `gamepad` the pad index for Gamepad* codes /
1303
- * connect events, −1 otherwise; `repeat` 1 for an OS auto-repeat keydown. Delivered on the JS
1304
- * thread no later than the end of the frame it arrived in — i.e. always before the NEXT
1305
- * setLoop tick (creator-pkg drains its dispatch queue after the frame's timers, so a poll can
1306
- * see the new state one tick before the event; web/headless deliver synchronously). */
1307
- registerInputEvent?(callback: (kind: number, code: string, gamepad: number, repeat: number) => void): void
1308
- // ---- World control (run/restart/quit — the platform's `location` analog) --------------------
1309
- // Worlds carry two host-tracked bits: an IDENTITY (project uuid) and a TRUST flag. The host's
1310
- // own boot world (the bundled launcher) is trusted; a world created by `run` is trusted only
1311
- // when its declared uuid is on the host's trusted-launcher allowlist AND the caller was
1312
- // trusted itself. Push registration attribution reads the identity (docs/push-plan.md).
1313
- /** Swap in a new project world. `projectUuid` declares the new world's identity — honored
1314
- * ONLY when the calling world is trusted (launchers); silently dropped otherwise. `url`
1315
- * (any caller — the caller already controls the code it runs) is the new world's
1316
- * launchUrl: a string sets it, null clears it, omitted leaves it unchanged. */
1317
- run(systemId: number, onReject: () => void, projectUuid?: string, url?: string | null): void
1318
- /** Re-run the CURRENT world's bundle in a fresh world — `location.reload()`. Identity and
1319
- * trust are inherited (the host re-evaluates bytes it already holds, so nothing is
1320
- * spoofable). `url`: string sets the new launchUrl, null clears it, omitted keeps the
1321
- * current one (a plain reload). If the re-run throws, the current world stays live. */
1322
- restart?(url?: string | null): void
1323
- /** Leave the current app. A world launched by `run` returns to the caller's boot bundle
1324
- * (launch URL cleared, boot identity/trust restored). The boot world itself has no caller —
1325
- * the host's app-exit hook fires instead (Android backgrounds the task; hosts that can't
1326
- * exit programmatically, iOS/desktop, register nothing and the call is a no-op). */
1327
- quit?(): void
1328
-
1329
- animate(callback: (val: number) => void, duration: number): number
1330
- stopAnimation(id: number): void
1331
- pauseAnimation(id: number): void
1332
- resumeAnimation(id: number): void
1333
-
1334
- localStorageGetValue(key: string): string | null
1335
- localStorageSetValue(key: string, value: string): void
1336
- localStorageRemoveValue(key: string): void
1337
-
1338
- websocketOpen(url: string, onWebsocketMessage: (channel: string, data: any) => void, headers?: any): number
1339
- websocketSend(id: number, message: string | ArrayBuffer): void
1340
- websocketClose(id: number): void
1341
-
1342
- // ---- Service channel (headless registerService capabilities) --------------------------------
1343
- // The UI-less sibling of the NativeView channel (docs/service-channel-plan.md): the host
1344
- // registers named service factories (`registerService("geolocation", factory)`) and the SDK
1345
- // talks to them over the same JSON call/event protocol as viewCall. All four are optional —
1346
- // feature-detect with typeof; hosts that didn't opt in simply don't have them. Sessions must
1347
- // open cheaply (no permission prompt, no I/O) — prompts belong in the first call that needs
1348
- // them, so a rejected call is the failure surface, never a hung open.
1349
- isServiceSupported?(name: string): boolean
1350
- /** Open a session against the registered factory. `params` is a JSON string. Returns the
1351
- * host-allocated session id, or -1 when no factory is registered under `name`. `onEvent`
1352
- * delivers service events (payload as a JSON string, or omitted) until serviceClose. */
1353
- serviceOpen?(name: string, params: string, onEvent: (event: string, dataJson?: string) => void): number
1354
- /** Invoke a method on a live session — args/result JSON-encoded, exactly like viewCall. */
1355
- serviceCall?(serviceId: number, method: string, args: string,
1356
- onComplete: (result?: string) => void, onError: (err: string) => void): void
1357
- serviceClose?(serviceId: number): void
1358
-
1359
- // ---- App lifecycle & environment (docs/app-lifecycle-plan.md) --------------------------------
1360
- // One push channel + synchronous pulls. All optional — typeof feature-detect; the SDK wrappers
1361
- // fall back cleanly (state "active", launchUrl null, online true) on hosts that lack them.
1362
- /** Register THE app-event listener (single slot per JS world — the SDK fans out; the host/core
1363
- * scopes the stored callback to the world and frees it on teardown). Events: "pause" / "resume"
1364
- * (app left / returned to the foreground), "url" (a link arrived while running; data = the URL),
1365
- * "online" / "offline" (connectivity changed). "pause" is delivered BEFORE the host halts the
1366
- * frame loop (the dispatch queue must still drain). */
1367
- registerAppEvent?(callback: (event: string, data?: string) => void): void
1368
- /** Current lifecycle state: "active" | "background". Surfaced as app.state. */
1369
- appState?(): string
1370
- /** The URL the app was (most recently) opened with, or null — the cold-start deep link. A warm
1371
- * link updates this value first, then fires the "url" event. Surfaced as app.launchUrl. */
1372
- getLaunchUrl?(): string | null
1373
- /** Current connectivity (navigator.onLine semantics — best-effort: false only when the platform
1374
- * is sure there is no network). Surfaced as device.online. */
1375
- isOnline?(): boolean
1376
- /** Open a URL in the system browser / external handler (fire-and-forget). Backs openURL(). */
1377
- openUrl?(url: string): void
1378
- /** Lock the interface orientation: "landscape" (either landscape direction — sensor
1379
- * landscape, what a horizontal game wants), "portrait" (upright only) or "auto" (release —
1380
- * the device's rotation rules apply again). Fire-and-forget; the rotation itself is
1381
- * animated by the platform and lands as an ordinary resize. PER WORLD: the host resets the
1382
- * lock to "auto" on every world swap BEFORE the new bundle evaluates, so a game may lock at
1383
- * synchronous top level and the launcher always comes back unlocked. Optional — hosts
1384
- * without a rotatable screen (desktop, headless, macOS) omit it. Backs app.setOrientation. */
1385
- setOrientation?(mode: string): void
1386
- /** Write text to the system clipboard (fire-and-forget). Backs clipboard.write. */
1387
- clipboardWrite?(text: string): void
1388
- /** Read text from the system clipboard — async because web needs a permission prompt; native
1389
- * hosts may complete synchronously. Reject = no access / nothing readable. Backs clipboard.read. */
1390
- clipboardRead?(onComplete: (text: string) => void, onReject: (err: string) => void): void
1391
-
1392
- // ---- Keyboard inset (docs/input-upgrades-plan.md §7) ------------------------------------------
1393
- /** On-screen keyboard height in logical px currently overlapping the app viewport, 0 when
1394
- * hidden. The RAW overlap, independent of the focused input's keyboardShrink policy (with
1395
- * shrink the layout already avoids it; the main consumer is overlay mode). Surfaced as
1396
- * app.keyboardHeight. Optional — hosts without an on-screen keyboard omit it. */
1397
- keyboardHeight?(): number
1398
- /** Register THE keyboard listener (single slot per JS world, like registerAppEvent — the SDK
1399
- * fans out; a separate channel because the string one can't carry two numbers). Fires on every
1400
- * keyboard frame change with (height — as above, duration — the platform's animation duration
1401
- * in ms, 0 where none). Surfaced as the app "keyboard" event. */
1402
- registerKeyboardEvent?(callback: (height: number, duration: number) => void): void
1403
-
1404
- // (camera* removed 2026-07-15 — CameraView rides the registered "camera" NativeView; hosts
1405
- // keep the old handlers only for previously compiled bundles.)
1406
-
1407
- createAudioPlayer(src: string, onFinished: () => void): number
1408
- createVideoPlayer(src: string, onFinished: () => void): number
1409
- updateMediaPlayerVolume(id: number, volume: number): void
1410
- updateMediaPlayerLoop(id: number, loop: boolean): void
1411
- updateMediaPlayerPlaying(id: number, play: boolean): void
1412
- getMediaPlayerTime(id: number): number
1413
- setMediaPlayerTime(id: number, time: number): void
1414
- getMediaPlayerDuration(id: number): number
1415
- removeMediaPlayer(id: number): void
1416
-
1417
- /** The host bucket surfaced as `device.platform`. Apps BRANCH on it (mouse-look vs on-screen
1418
- * sticks, hover affordances, key hints), so it names the INPUT MODEL, not the OS — one of
1419
- * "web" | "ios" | "android" | "desktop". macOS reports "desktop", not "macos": a project
1420
- * testing `device.platform === "desktop"` must get the same answer on every desktop host.
1421
- * OS/version detail belongs in the host's own systemInfo callback, never here. Every host
1422
- * must define it — where it was missing (Android before 2026-07-30, iOS before 2026-08-25)
1423
- * the SDK read undefined and no platform branch in any app could ever match. */
1424
- platform: string
1425
- language: string
1426
- /** Display device-pixel ratio (physical px per CSS/logical px): 1 on standard displays, 2–3 on
1427
- * retina / iOS. Host-provided (web: window.devicePixelRatio; iOS: UIScreen.scale). Surfaced as
1428
- * device.pixelRatio — for baking a Canvas at the right resolution without hardcoding it. */
1429
- pixelRatio: number
1430
- /** Opt into the precise-touch system (full-rate coalesced sampling) where the host supports it — iOS
1431
- * coalesced touches. Surfaced as device.setPreciseTouch. Optional: hosts without a coalesced-input
1432
- * concept (web, headless) simply don't implement it, so callers guard with `?.`. */
1433
- setPreciseTouch?(enabled: boolean): void
1434
- /** Fire a one-shot semantic haptic (device.vibrate). `style` is a HapticStyle string. Optional:
1435
- * hosts without haptic hardware (web, headless, iPad / older iPhones) simply don't implement it,
1436
- * so callers guard with `?.`. */
1437
- vibrate?(style: string): void
1438
-
1439
- // ---- device.motion (orientation sensor; pull-per-frame model) --------------------------------
1440
- /** Does this device have the motion sensors (gyro)? Surfaced as device.motion.available. */
1441
- motionAvailable?(): boolean
1442
- /** Start fused device-motion updates at `interval` seconds; returns whether it started (false =
1443
- * no sensor / permission denied). May return a Promise on hosts with an async permission prompt
1444
- * (web). Surfaced as device.motion.start. */
1445
- motionStart?(interval: number): boolean | Promise<boolean>
1446
- /** Stop updates and release the sensor. Surfaced as device.motion.stop. */
1447
- motionStop?(): void
1448
- /** The freshest raw sample in the DEVICE frame: `[qx,qy,qz,qw, gx,gy,gz, interfaceOrientation]`
1449
- * (orientation code: 0 portrait, 1 landscapeLeft, 2 landscapeRight, 3 upsideDown), or null until
1450
- * the first sample / when not running. The SDK converts it to world/screen space. */
1451
- getMotionSample?(): number[] | null
1452
- }
1453
-
1454
- // ---- 2D canvas (core; native-per-platform — see docs/canvas-contract.md) ---------------------
1455
- // Immediate-mode 2D drawing that bakes to a texture, shared by 2D/3D/UI. The SDK Canvas records a
1456
- // command buffer and flushes it here once per bake (opcodes documented in the contract).
1457
- var _creatorCanvas: {
1458
- // Rasterize `cmd` (cmdLen floats) into surface `surfaceId` (0 = create), sized w×h logical @
1459
- // `scale` device px. Clears then replays the whole buffer. Returns the surface id.
1460
- rasterize(surfaceId: number, w: number, h: number, scale: number,
1461
- cmd: Float32Array, cmdLen: number, refs: string[]): number
1462
- // [width, ascent, descent] in logical px for `text` in `font`, measured on this platform.
1463
- measureText(text: string, font: string): Float32Array
1464
- // Copy surface `surfaceId`'s pixels into a NEW standalone surface; returns the new id. Backs
1465
- // Canvas.toBitmap() — an immutable snapshot the source canvas can no longer touch.
1466
- snapshot(surfaceId: number): number
1467
- // Decode an image source into a NEW standalone surface (device px) — backs Canvas.loadImage() for
1468
- // Canvas.drawImage(). `bufferId < 0` → rasterize the SVG markup in `svg`; otherwise decode the
1469
- // encoded image bytes in host buffer `bufferId` (an already-fetched FetchResponse). Async, because
1470
- // turning SVG / encoded bytes into pixels needs a decode on web (createImageBitmap / <img>). Returns
1471
- // (surfaceId, width, height) in device px via onComplete, or onReject on failure.
1472
- loadImage(svg: string, bufferId: number,
1473
- onComplete: (surfaceId: number, width: number, height: number) => void, onReject: () => void): void
1474
- // Encode surface `surfaceId` to an image ('image/png' | 'image/jpeg'), register the bytes as a host
1475
- // buffer, and return (bufferId, size) — the SDK wraps it in a File. Backs Canvas/Bitmap.toFile().
1476
- toFile(surfaceId: number, type: string, onComplete: (bufferId: number, size: number) => void, onReject: () => void): void
1477
- destroySurface(surfaceId: number): void
1478
- }
1479
- }
1480
-
1481
- export {}
1
+ // The host ABI. These four objects are injected onto the global scope by the runtime host
2
+ // (web viewer, desktop, iOS) before any user/SDK code runs. The SDK is a thin, typed re-skin over
3
+ // them — it never bundles them; it only references them as free globals. Faithfully tracks the
4
+ // surface in packages/worker/src/global.d.ts (the proven contract).
5
+
6
+ declare global {
7
+
8
+ // ---- 3D engine (Filament / creator-gl) -------------------------------------------------------
9
+ var _creator: {
10
+ backend: string
11
+
12
+ createScene(): number
13
+ createOverlayScene(sceneId: number): number
14
+ createGLView(): void
15
+ openScene(sceneId: number): void
16
+ closeScene(): void
17
+ launchAR(sceneId: number, onComplete: () => void, onReject: () => void): void
18
+ stopAR(): void
19
+ launchVR(sceneId: number, onComplete: () => void, onReject: (err?: unknown) => void): void
20
+ stopVR(): void
21
+ addEntityToScene(sceneId: number, entityId: number): void
22
+ removeEntityFromScene(sceneId: number, entityId: number): void
23
+ getSceneMaterial(sceneId: number): number
24
+ warmRender(sceneId: number, onComplete: () => void): void
25
+ /** Compile every shader variant the scene's materials can need — sun / shadows / fog / skinning
26
+ * and the dynamic-light key — on the backend's compiler threads, then call back. Materials
27
+ * loaded later are queued in the background as they are created. Optional: a host without it
28
+ * resolves at once (the SDK feature-detects). */
29
+ precompileShaders?(sceneId: number, onComplete: () => void): void
30
+
31
+ createEntity(): number
32
+ cloneEntity(entityId: number): number
33
+ destroyEntity(entityId: number): void
34
+
35
+ setMatrix(entityId: number, mat: Float32Array): void
36
+ setWorldMatrix(entityId: number, mat: Float32Array): void
37
+ getChildren(entityId: number): number[]
38
+ getChildCount(entityId: number): number
39
+ getChild(entityId: number, index: number): number
40
+
41
+ setPosition(entityId: number, x: number, y: number, z: number): void
42
+ setQuaternion(entityId: number, x: number, y: number, z: number, w: number): void
43
+ setEulerAngles(entityId: number, x: number, y: number, z: number, order: number): void
44
+ setScale(entityId: number, x: number, y: number, z: number): void
45
+
46
+ addChildren(parentEntityId: number, entityIds: number[]): void
47
+ setParent(entityId: number, parentEntityId: number, worldPositionStays: boolean): void
48
+ setParentNull(entityId: number, worldPositionStays: boolean): void
49
+ getParent(entityId: number): number
50
+ traverse(entityId: number, callback: (entityId: number) => void): void
51
+ /** Descendant of `entityId` by name, with the Animator's binding rule: exact name first, then the
52
+ * short name (after the last `:` / `|` — Mixamo `mixamorig:Hips`); a skin joint beats a plain node
53
+ * of the same name (merged GLBs carrying a leftover skeleton copy), otherwise first in tree order.
54
+ * 0 when absent. Optional: hosts that predate it fall back to the SDK's traverse walk (node.bone). */
55
+ findNode?(entityId: number, name: string): number
56
+
57
+ createSunLight(entityId: number, x: number, y: number, z: number, intensity: number, color: number, shadowsQuality: number, shadowDistance: number): void
58
+ /** Punctual light. `intensity` is luminous POWER in lumens; `falloff` is the metres of
59
+ * influence (filament's own default is 1 m, i.e. invisible, so it is always passed). */
60
+ createPointLight?(entityId: number, intensity: number, color: number, falloff: number, castShadows: boolean): void
61
+ /** The lightmap bake takes this point light as a RECTANGLE of `width` x `height` metres (an area light: a ceiling
62
+ * panel) in the light's local XZ plane, emitting along its local -Y with the same lumens; 0, 0 = a point again. */
63
+ setLightBakeArea?(entityId: number, width: number, height: number): void
64
+ /** Live intensity for any light (sun: lux, point: lumens) — a flash animates instead of rebuilding. */
65
+ setLightIntensity?(entityId: number, intensity: number): void
66
+ setLightColor?(entityId: number, color: number): void
67
+ /** `iblFetchId` (optional, -1 = none) names the scene's own probe; without it the host looks for a project-wide `ibl.ktx`. */
68
+ setDefaultIbl(sceneId: number, intensity: number, iblFetchId?: number): void
69
+ /** The environment's intensity in lux, live (`scene.environmentIntensity`) — `setDefaultIbl` is
70
+ * the only other way to change it and it rebuilds the cubemap and the IndirectLight from the
71
+ * ktx every call, so it can never carry a slider. No-op until the scene has an IBL. Optional:
72
+ * hosts that predate it keep whatever `setDefaultIbl` set at open. */
73
+ setEnvironmentIntensity?(sceneId: number, intensity: number): void
74
+ setBloomOptions(sceneId: number, enabled: boolean, strength: number, quality: number): void
75
+ setToneMapping?(sceneId: number, mode: number): void
76
+ /** Exposure of the scene's camera — filament's physical model: `aperture` f-stops,
77
+ * `shutterSpeed` seconds, `sensitivity` ISO (default f/16, 1/125 s, ISO 100 = EV100 15, bright
78
+ * sunlight). ISO ×2 = one stop brighter. Scene-referred light follows it; particle emission is
79
+ * post-exposure and does not. Optional: hosts that predate it keep the fixed default. */
80
+ setCameraExposure?(sceneId: number, aperture: number, shutterSpeed: number, sensitivity: number): void
81
+ /** Display output range (macOS/iOS EDR, `device.hdr`). `getDisplayHeadroom` is what the engine
82
+ * renders to right now: the screen's peak as a multiple of SDR white, quantised to half-stops;
83
+ * 0 or 1 = SDR. Live — it ramps up from 1 over the first seconds, follows brightness and the
84
+ * screen under the window. `getDisplayMaxHeadroom` is the peak the surface can reach at all
85
+ * (1 = SDR), constant once the surface exists: the value to branch content on. Optional: hosts
86
+ * that predate it, or render 8-bit, are SDR. */
87
+ getDisplayHeadroom?(): number
88
+ getDisplayMaxHeadroom?(): number
89
+ /** HDR look, engine-wide (creator.h `setHdrStrength` / `setHdrPaperWhite`): `strength` 0..1 =
90
+ * how much of the picture reaches for the headroom (0 only what SDR clipped, 1 nearly
91
+ * everything), `paperWhite` 1..8 = where the operator's white lands as a multiple of SDR white
92
+ * (the "HDR brightness" of a console calibration screen; clamped to the headroom). A host may
93
+ * pin either from its environment — the getters report what is in force. Optional. */
94
+ setHdrStrength?(strength: number): void
95
+ getHdrStrength?(): number
96
+ setHdrPaperWhite?(paperWhite: number): void
97
+ getHdrPaperWhite?(): number
98
+ /** Screen-space ambient occlusion: the contact darkening in creases and where objects meet the
99
+ * ground. `radius` is world-space metres, `power` the falloff contrast, `quality` 0 LOW … 3
100
+ * ULTRA (sample count — not the buffer resolution, which stays half-res).
101
+ * Optional: hosts that predate it render without AO and the SDK skips the call. */
102
+ setAmbientOcclusionOptions?(sceneId: number, enabled: boolean, intensity: number, radius: number, power: number, quality: number): void
103
+ /** Distance fog / aerial perspective. `color` is packed 0xRRGGBB used as a TINT on the
104
+ * in-scattered ambient — the engine multiplies it by the environment luminance, so white means
105
+ * "as bright as the ambient" and it is NOT the absolute-radiance convention `setSkybox` uses.
106
+ * `density` = extinction per metre at `height`, `heightFalloff` 1/m (0 = uniform),
107
+ * `cutOff` <= 0 = apply at every distance (the skybox included — that is what blends the
108
+ * horizon), `fromIbl` = take the colour from the environment in the view direction and tint
109
+ * it by `color`.
110
+ * Optional: hosts that predate it render without fog and the SDK skips the call. */
111
+ setFogOptions?(sceneId: number, enabled: boolean, color: number, distance: number, density: number, height: number, heightFalloff: number, maxOpacity: number, cutOff: number, fromIbl: boolean): void
112
+ setSkybox(sceneId: number, color: number): void
113
+ /** Draw the scene's IBL environment as the sky instead of a flat colour. No-op until the IBL
114
+ * exists (setDefaultIbl runs first). Optional: hosts that predate it keep the flat skybox. */
115
+ setSkyboxFromEnvironment?(sceneId: number): void
116
+ /** The sky from its own KTX1 cubemap. `fetchId` is a local-resource id (`_creatorUtils.fetchLocal`
117
+ * / an `asset()` handle), pointing at cmgen's `<name>_skybox.ktx` — the sharp single-mip file,
118
+ * not the roughness-prefiltered `_ibl.ktx` beside it. Optional: hosts that predate it keep the
119
+ * flat skybox. */
120
+ setSkyboxTexture?(sceneId: number, fetchId: number): void
121
+ setSceneMultiSampleAntiAliasing(sceneId: number, enabled: boolean, scale: number): void
122
+ /** Filament View::setStencilBufferEnabled (Scene.stencil). Optional. */
123
+ setSceneStencil?(sceneId: number, enabled: boolean): void
124
+ /** Render resolution: `renderScale` (0.25–1) = fixed 3D-buffer scale vs the viewport (the UI is
125
+ * untouched; a host that owns the 3D texture resizes it, one rendering into the swapchain may
126
+ * ignore it), `dynamicResolution` = engine-adaptive scaling under that down to `minScale`.
127
+ * Optional: older hosts predate it and the SDK skips the call. */
128
+ setSceneRenderOptions?(sceneId: number, renderScale: number, dynamicResolution: boolean, minScale: number): void
129
+ /** Anisotropic filtering for every texture bound FROM HERE ON (1 = off, 2 = the engine default,
130
+ * 16 = max; clamped). Engine-wide rather than per scene, because a sampler is baked when its
131
+ * texture is bound — a glTF binds during `Model.load`, and two scenes cannot disagree about a
132
+ * texture they share. Call it before loading the assets it should apply to; `SceneOptions.
133
+ * anisotropy` does that automatically (a scene's env runs before its nodes build). Optional:
134
+ * hosts that predate it keep isotropic filtering and the SDK skips the call. */
135
+ setTextureAnisotropy?(level: number): void
136
+ /** Engine-wide cap on texture size (`Texture.maxSize` / `SceneOptions.maxTextureSize`): a
137
+ * KTX2 wider or taller than `size` loses its top mip levels on load, a glTF PNG/JPEG is
138
+ * downsampled; 0 = no cap. Reaches textures created AFTER the call — a loaded level keeps
139
+ * its textures, so a settings menu applies it on the next level load. `createTexture` flag
140
+ * 2 (FULL_SIZE) exempts one texture (lightmap pages). Optional: hosts that predate it load
141
+ * full-size textures and the SDK skips the call. */
142
+ setTextureMaxSize?(size: number): void
143
+ /** Depth-reading effects on / off (`scene.setDepthEffects`): soft particles (`depthFade`) and
144
+ * projected decals read the scene depth, which costs a half-res depth pre-pass of every opaque
145
+ * draw. Off = no pre-pass, hard-edged particles, decal sets draw nothing. Engine-wide, live.
146
+ * Optional: hosts that predate it keep the effects and the SDK skips the call. */
147
+ setDepthEffects?(enabled: boolean): void
148
+ /** LOD distance (`scene.setLodBias`): the LOD pass' screen-size thresholds × `bias` — 2 = every
149
+ * level switches at half the distance, 0.5 = full detail twice as far; 1 = the defaults
150
+ * (0.30 / 0.12 / 0.05 of the viewport height). Engine-wide, live, clamped 0.25..8. Optional:
151
+ * hosts without the LOD pass ignore it and the SDK skips the call. */
152
+ setLodBias?(bias: number): void
153
+ setMaterialGlobalParameter(sceneId: number, i: number, x: number, y: number, z: number, w: number): void
154
+ getCameraFov(sceneId: number, fovType: number): number
155
+ /** The scene camera's projection: VERTICAL fov in degrees + near/far clip distances (defaults
156
+ * 60 / 0.01 / 1000). Aspect stays host-owned — the host STORES these per scene and re-applies
157
+ * them whenever the viewport changes, so one call outlives every resize. Optional: hosts that
158
+ * predate it keep the fixed defaults and the SDK feature-detects (Camera.setProjection). */
159
+ setCameraProjection?(sceneId: number, fov: number, near: number, far: number): void
160
+
161
+ getMatrix(entityId: number, mat: Float32Array): boolean
162
+ getWorldMatrix(entityId: number, mat: Float32Array): void
163
+ getWorldPosition(entityId: number, mat: Float32Array): void
164
+ getWorldDirection(entityId: number, mode: number, mat: Float32Array): void
165
+
166
+ getWorldMatrixInverse(entityId: number, mat: Float32Array): void
167
+ getCameraViewDirection(sceneId: number, screenX: number, screenY: number, mat: Float32Array): void
168
+
169
+ /** `uv1` (optional, 2 floats per vertex) is the lightmap UV set (Geometry.uv1); absent → the
170
+ * host duplicates `uv` into UV1. Hosts that predate the argument ignore it. */
171
+ /** `colors` (Geometry.colors) is 4 bytes RGBA per vertex for a `requires: [color]` material;
172
+ * a host that predates it draws the mesh white. */
173
+ setMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, meshType: number, uv1?: Float32Array, colors?: Uint8Array): void
174
+ // Terrain (gl/Terrain.ts ↔ creator-gl/src/terrain.cpp, docs/terrain-plan.md §1.4): a heightmap grid of
175
+ // sizeX × sizeZ samples `cellSize` apart (+X across columns, +Z across rows, height on +Y, sample 0 at
176
+ // the node origin), drawn as one renderable per `chunk`×`chunk` cells on internal children of the
177
+ // entity (u16 indices → chunk ≤ 255). `holes` = one byte per sample, 1 = hole (a triangle exists only
178
+ // when its three samples are valid — the same rule as the Jolt height field), null = none. UV0 = local
179
+ // XZ in metres, UV1 = the terrain's unit square. Returns the chunk count (0 = refused). terrainUpdate
180
+ // re-reads the FULL arrays and rebuilds the chunks the sample rectangle touches. All optional: a host
181
+ // without them gets the SDK's own chunk meshes through setMesh (gl/terrainMesh.ts).
182
+ terrainCreate?(entityId: number, materialId: number, heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, chunk: number, holes: Uint8Array | null): number
183
+ terrainUpdate?(entityId: number, heights: Float32Array, holes: Uint8Array | null, x0: number, z0: number, w: number, h: number): void
184
+ terrainSetMaterial?(entityId: number, materialId: number): void
185
+ terrainSetShadows?(entityId: number, cast: boolean, receive: boolean): void
186
+ // Instanced mesh (gl/InstancedMesh.ts ↔ creator-gl/src/instanced.cpp): `count` transforms,
187
+ // column-major 4×4 local to the node, all-zero = hidden. setInstanceTransforms writes
188
+ // matrices.length / 16 of them starting at `first` (a subarray view is fine — consumed synchronously).
189
+ createInstancedMesh(entityId: number, materialId: number, vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array, count: number): void
190
+ setInstanceTransforms(entityId: number, matrices: Float32Array, first: number): void
191
+ setInstancedMeshMaterial(entityId: number, materialId: number): void
192
+ setInstancedMeshShadows(entityId: number, cast: boolean, receive: boolean): void
193
+ setCulling(entityId: number, culling: boolean): void
194
+ /** Filament RenderableManager::setPriority — the coarse draw order, 0..7 (Mesh.renderPriority).
195
+ * Optional: an older host leaves everything at the default 4. */
196
+ setRenderPriority?(entityId: number, priority: number): void
197
+ /** Filament MaterialInstance::setDepthCulling / setDepthWrite — per-instance overrides of the
198
+ * depth state baked into the shader package (Material.depthTest / depthWrite). Optional. */
199
+ setMaterialDepthTest?(materialInstanceId: number, enable: boolean): void
200
+ setMaterialDepthWrite?(materialInstanceId: number, enable: boolean): void
201
+ /** Filament MaterialInstance::setCullingMode: 0 none (double-sided), 1 front, 2 back. Optional. */
202
+ setMaterialCulling?(materialInstanceId: number, mode: number): void
203
+ /** Filament MaterialInstance stencil state in one call (Material.stencil): `test` 0 always, 1 never,
204
+ * 2 less, 3 lessEqual, 4 greater, 5 greaterEqual, 6 equal, 7 notEqual; the ops 0 keep, 1 zero,
205
+ * 2 replace, 3 increment, 4 decrement, 5 invert. Optional. */
206
+ setMaterialStencil?(materialInstanceId: number, write: boolean, test: number, ref: number, onPass: number, onFail: number, onDepthFail: number, readMask: number, writeMask: number): void
207
+ setCastShadows(entityId: number, culling: boolean): void
208
+ setReceiveShadows(entityId: number, culling: boolean): void
209
+
210
+ /** Decode a fetched image into a texture. `flags` (optional, Texture.load): bit 1 = LINEAR data
211
+ * (a normal map — store RGBA8, not sRGB); unset / absent = colour, sRGB. A host that ignores
212
+ * it loads colour correctly and normal maps wrongly. KTX2 decides by its own header. */
213
+ createTexture(systemId: number, onComplete: (id: number, width: number, height: number) => void, onReject: () => void, flags?: number): void
214
+ // Texture from a baked _creatorCanvas surface (RGBA8, already rasterized — synchronous, no decode).
215
+ createTextureFromCanvas(surfaceId: number): number
216
+ updateTextureFromCanvas(texId: number, surfaceId: number): void
217
+ /** A texture from raw UBYTE pixels — `channels` 1–4, row-major, `width*height*channels` bytes, no
218
+ * mips; `srgb` selects the sRGB internal format for 3/4 channels (colour) vs linear (data — a
219
+ * terrain's control map). The bytes are copied. Returns 0xFFFFFFFF on failure. `updateTexturePixels`
220
+ * re-uploads a sub-rectangle with the creation channel count. Optional (Texture.fromPixels throws). */
221
+ createTexturePixels?(width: number, height: number, channels: number, data: Uint8Array, srgb: boolean): number
222
+ updateTexturePixels?(textureId: number, x: number, y: number, width: number, height: number, data: Uint8Array): void
223
+
224
+ createMaterial(systemId: number): number
225
+ createMaterialS(name: string): number
226
+ setMaterial(entityId: number, materialId: number, index: number): void
227
+ getMaterial(entityId: number, index: number): number
228
+
229
+ setUniformRgb(materialId: number, uniform: string, color: number): void
230
+ setUniformRgba(materialId: number, uniform: string, color: number): void
231
+ setUniformFloat(materialId: number, uniform: string, value: number): void
232
+ setUniformBoolean(materialId: number, uniform: string, value: boolean): void
233
+ setUniformTexture(materialId: number, uniform: string, systemId: number, wrapS: number, wrapT: number): void
234
+ setUniformArray(materialId: number, uniform: string, value: Float32Array): void
235
+ setUniformNull(materialId: number, uniform: string): void
236
+
237
+ attachCameraToAR(sceneId: number, cameraEntityId: number, onProjectionChange: (mat: Float32Array, fov: number) => void): void
238
+ getDisplaySize(): Float32Array
239
+
240
+ setVisible(entityId: number, visible: boolean): void
241
+ isVisible(entityId: number): boolean
242
+
243
+ createGlb(systemId: number, onComplete: (buff: number) => void, onReject: () => void): void
244
+ setGlbCulling(entityId: number, culling: boolean): void
245
+ /** Optional. True when the host refits a skinned model's frustum-culling bounds to its joints every
246
+ * frame (creator-gl animation/skinning.cpp), i.e. culling is safe for animated GLBs. Model.load
247
+ * defaults culling ON only where this returns true; absent / false = the always-draw default. */
248
+ skinnedCullingSupported?(): boolean
249
+ /** Optional. Level-of-detail override for a GLB instance (docs/lod-plan.md): `mesh` / `anim` are -1 for
250
+ * automatic (the engine picks by screen size and visibility) or a forced level 0..3. Backs Model.lod
251
+ * and Animator.lod; a host without it (no LOD pass) leaves everything at full detail. */
252
+ setLod?(entityId: number, mesh: number, anim: number): void
253
+
254
+ // Render-synced aspect update(dt) dispatch, called from inside render(). Early = before the physics
255
+ // step; late = after animations/particles, just before draw. Single-slot per phase (the SDK's Aspect
256
+ // dispatcher registers one callback per phase that iterates its ordered updater list).
257
+ setEarlyUpdate(cb: (dt: number) => void): void
258
+ setLateUpdate(cb: (dt: number) => void): void
259
+ /** FIXED phase - the SDK's `updateFixed`: once per physics substep with dt = 1/60 exactly, BEFORE
260
+ * that substep's Jolt step (move commands / velocities written here feed the same step). Under
261
+ * `setTimeScale` the NUMBER of substeps changes, never the dt; at most 4 per frame. `null` clears it -
262
+ * registered lazily, only once an aspect declares the phase (one flag check per substep otherwise).
263
+ * Optional: a host without it gets the SDK's own 1/60 accumulator inside the early phase. */
264
+ setFixedUpdate?(cb: ((dt: number) => void) | null): void
265
+ /** The engine's clock multiplier for physics / animators / particles (1 normal, 0 frozen) — the
266
+ * SDK's `Time.scale` / `Time.paused` pushed down so the sims stay in step with game code. The
267
+ * two aspect phases still receive the RAW wall-clock dt (the SDK scales it itself). Optional:
268
+ * a host without it keeps its sims at wall-clock speed. */
269
+ setTimeScale?(scale: number): void
270
+ // Physics contact/sensor events (drained after each step): a/b = entity ids, type 0 enter / 1 exit.
271
+ setOnPhysicsEvent(cb: (a: number, b: number, type: number) => void): void
272
+
273
+ // ---- Animation system (docs/animation-v2-plan.md): AnimationClip + Animator — THE skeletal animation
274
+ // path, evaluated by creator-anim on every host. A clip set is parsed straight from GLB bytes (tracks
275
+ // target bone NAMES, no entities); a model's embedded clips are registered by createGlb (getGlbClipSet,
276
+ // 0 = none). An Animator binds clips to one node hierarchy by name; the native evaluator runs the WHOLE
277
+ // per-frame loop (sources, transitions, blend spaces, one-shot hand-over, root motion) — the SDK only
278
+ // issues play/stop/blend calls and has no tick. Skinning is flushed AFTER the late phase so IK /
279
+ // procedural joint writes land in the skin.
280
+ // TRANSITIONS ARE INERTIAL, NOT CROSSFADES: a layer evaluates exactly ONE source (its one-shot, its
281
+ // loop, or nothing); a switch records the difference between the pose the layer SHOWED and the pose the
282
+ // new source shows and decays it away over `fade` seconds (halflife = 0.4 × fade). A replaced source is
283
+ // silent from that moment — it costs nothing, its weight is 0 at once, and it may be replaced again
284
+ // mid-transition (the offset is re-recorded from the displayed pose). `fade` 0 = cut.
285
+ loadClips(systemId: number, onComplete: (clipSetId: number) => void, onReject: (err: unknown) => void): void
286
+ // names = newline-joined track targets; data = [trackCount, duration (<=0 → max key time), (path 0 T/1 R/2 S,
287
+ // interp 0 linear/1 step/2 cubic, comps, keyCount, times…, values…)*]. Returns a one-clip set id (0 = bad).
288
+ createClipFromTracks(names: string, data: Float32Array): number
289
+ getClipSetInfo(clipSetId: number): { name: string, duration: number, trackCount: number }[]
290
+ // A clip DERIVED from one of the set as a NEW single-clip set (index 0) — AnimationClip.from(clip, { mirror,
291
+ // from, to }): the mirror (left ↔ right on the set's own rig, the GLB's node tree: contacts swapped, heading
292
+ // negated), then the [start, end] window re-timed to 0 (start < 0 = whole, end < 0 = the clip's end; boundary
293
+ // values interpolated in, events re-timed). 0 = bad id / range, or a mirror asked of a set without a rig.
294
+ deriveClip(clipSetId: number, clip: number, mirror: boolean, start: number, end: number): number
295
+ // Clip events on the CLIP: normalized times (sorted ascending). Every slot bound to the clip, in every
296
+ // animator, fires slot event type 4 + i on crossing event i. Empty = clear.
297
+ setClipEvents(clipSetId: number, clip: number, times: Float32Array): void
298
+ getGlbClipSet(entityId: number): number
299
+ destroyClipSet(clipSetId: number): void
300
+ animatorCreate(entityId: number): number // 0 = not a transform node
301
+ animatorDestroy(animatorId: number): void // leaves the skeleton in rest pose
302
+ // One (clip, layer) slot, silent until played / made a blend member. slot index or -1.
303
+ animatorBind(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number
304
+ animatorBoundTracks(animatorId: number, slot: number): number
305
+ // The layer's LOOP: member slots + positions (dims 1: x per member, 2: x,y; one member = a plain looping
306
+ // clip). The members share ONE cycle clock, each placed on it LINEARLY by `phases` — two floats per member,
307
+ // (offset, cycles): φ(t) = offset + cycles · t / duration, 0 at a left-foot-down (measured offline
308
+ // from the clip's foot marks). An empty array, or cycles <= 0 for a member = normalized time (0, 1) and no
309
+ // cycle to phase-match to — the clock never reads the clip's marks. animatorSetBlendValue picks the mix (1D linear between neighbours /
310
+ // 2D gradient band). Setting it takes the layer over from whatever it shows — the previous loop and any
311
+ // one-shot on it go silent — with a transition of `fade`. Empty = no loop. speed = the members' rate.
312
+ animatorSetBlend(animatorId: number, layer: number, dims: 1 | 2, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases: Float32Array): void
313
+ animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void
314
+ // Play a slot as the layer's one-shot, transitioned in over fadeIn (0 = cut) from whatever the layer
315
+ // showed (playing a blend MEMBER brings the whole blend back instead). A non-looping one-shot plays to its
316
+ // end and hands the layer back to the loop THERE, the return transition taking fadeOut from its last pose
317
+ // (0 = cut back; on a loopless layer hold the last frame).
318
+ // restart = rewind even if already playing (a restart is a new source: it transitions from the pose shown).
319
+ animatorPlay(animatorId: number, slot: number, loop: boolean, speed: number, fadeIn: number, fadeOut: number, restart: boolean): void
320
+ // TURN WARP for the slot's current play (right after animatorPlay): the node's turn from the clip's baked heading is
321
+ // scaled to `radians` total; the pose keeps its own turn, the extra pivots about the planted foot. enabled false =
322
+ // the clip's own turn. Optional: older hosts lack it.
323
+ animatorSlotSetTurn?(animatorId: number, slot: number, radians: number, enabled: boolean): void
324
+ // WINDOW of the slot's current play (right after animatorPlay): the one-shot runs [start, end] clip seconds — enters
325
+ // at start (at end when backwards) if the play rewound, completes + hands over at end, clip events outside never fire.
326
+ // end < 0 = the clip's end. Loops ignore it. Optional: older hosts lack it (the whole clip plays).
327
+ animatorSlotSetWindow?(animatorId: number, slot: number, start: number, end: number): void
328
+ // Release over `fade` (a transition toward what is left — the loop, or the rest pose): one slot; a
329
+ // layer's one-shot + loop (slot -1); everything (layer -1).
330
+ animatorStop(animatorId: number, layer: number, slot: number, fade: number): void
331
+ animatorSeek(animatorId: number, slot: number, time: number): void // seeking a blend member moves the blend
332
+ animatorGetSlotTime(animatorId: number, slot: number): number
333
+ animatorGetSlotWeight(animatorId: number, slot: number): number // 1 = the layer's source, a loop member = its share, else 0
334
+ // Layer config: weight 0–1; additive = each slot's DELTA vs its clip's first frame on top of the layers
335
+ // below; maskRoot = bone name(s, '\n'-separated) whose subtrees the layer drives ("" = all).
336
+ animatorSetLayer(animatorId: number, layer: number, weight: number, additive: boolean, maskRoot: string): void
337
+ // ANTICIPATION for the layer's next source change: the transition starts with -amount x the new source's
338
+ // joint velocity (a wind-up against the coming motion), consumed by that switch. Optional: older hosts lack it.
339
+ animatorSetLayerAnticipation?(animatorId: number, layer: number, amount: number): void
340
+ animatorSetGlobal(animatorId: number, speed: number, paused: boolean): void
341
+ // Root motion: "" off, "*" auto (the shallowest joint a base-layer clip translates), else a bone name. The
342
+ // root's horizontal travel (animator-node frame) is stripped from the pose and, per apply: 0 accumulated
343
+ // only (animatorGetRootMotion copies + clears out[3]), 1 added to the node's transform, 2 fed to the
344
+ // CharacterController on the node or an ancestor as a world velocity (falls back to 1 without one).
345
+ /** `apply` bits 0–1: 0 accumulate only / 1 move the node / 2 feed the CharacterController; bit 2 (+4): the
346
+ * root joint's yaw about the node's up is root motion too (off the pose, onto the node the travel lands on). */
347
+ animatorSetRootMotion(animatorId: number, bone: string, apply: number): void
348
+ animatorGetRootMotion(animatorId: number, out: Float32Array): void
349
+ // Slot events: 0 completed (non-loop end) / 1 loop wrapped / 2 settled (no longer a source, after a stop
350
+ // or a replacement) / 3 HAND-OVER (a one-shot's return starts — for the SDK the clip is over; what the app
351
+ // starts in response takes the layer over instead) / 4+i clip event i. Single-slot (routed by animatorId).
352
+ setOnAnimatorEvent(callback: (animatorId: number, slot: number, type: number) => void): void
353
+ // ---- contacts, phase, root curves (docs/animation-v2-plan.md §2.5–2.6) ----
354
+ // Every clip bound to a skeleton is baked once against it: when each foot is planted, the gait phase
355
+ // φ(t) (0 at a left-foot-down, 0.5 at a right-foot-down, unwrapped over the clip; absent for a clip
356
+ // with no gait cycle) and the root's cumulative travel / yaw / speed. A controller asks these instead
357
+ // of shipping measured tables of its own.
358
+ // The feet: '\n'-joined bone names per side (foot[, toe/ball]); both "" = classify by name. Re-bakes.
359
+ animatorSetFeet(animatorId: number, left: string, right: string): void
360
+ // One curve of a slot's clip at `time` seconds (< 0 = the slot's clock now): which 0 φ (-1 = no gait)
361
+ // / 1 travel (m) / 2 yaw (rad, + = left) / 3 speed (m/s) / 4-5 unit travel direction x / z
362
+ // (model space, held through stills — integrate dir × d(travel) for the root's 2D path).
363
+ animatorSlotCurveAt(animatorId: number, slot: number, which: 0 | 1 | 2 | 3 | 4 | 5 | 6, time: number): number
364
+ // out ← [curve sample dt, total travel (m), mean speed (m/s), in-place flag]. False = unbound slot.
365
+ animatorSlotCurveInfo(animatorId: number, slot: number, out: Float32Array): boolean
366
+ animatorSlotTurn(animatorId: number, slot: number): number // the clip's total root yaw, rad
367
+ // The first time the clip has turned `yaw` radians — where a turn is entered by a body already
368
+ // that far into the same turn, so the two read as one move.
369
+ animatorSlotTimeAtTurn(animatorId: number, slot: number, yaw: number): number
370
+ // What the LAYER shows, as a cycle phase in [0, 1) — its loop's clock, or its one-shot's φ; -1 = none.
371
+ animatorLayerPhase(animatorId: number, layer: number): number
372
+ // Seek the slot to the first time whose φ ≡ phase (mod 1); a blend member moves its whole group.
373
+ animatorSeekPhase(animatorId: number, slot: number, phase: number): void
374
+ // How far the slot's pose is from `target`'s at the same cycle phase, metres (joint distance +
375
+ // the velocity difference over 0.1 s, the planted foot weighted most) — what handing over to that
376
+ // clip would hand the inertializer. Fills `out` at the curve rate, returns the sample count.
377
+ // align: 0 = compare at the same cycle phase, 1 = at the same time (two clips that both begin
378
+ // from standing have no shared cycle to line up on).
379
+ animatorSlotFit(animatorId: number, slot: number, target: number, align: 0 | 1, out: Float32Array): number
380
+ // The earliest time in the slot where that hand-over costs no more than `tolerance` metres;
381
+ // atContact snaps to the next foot-down at or after it. -1 = never that close.
382
+ animatorSlotExit(animatorId: number, slot: number, target: number, tolerance: number, atContact: boolean, align: 0 | 1): number
383
+ // CYCLE ALIGNMENT by pose, no marks: given slot a's cycle (offA, cyclesA), the (offset, cycles) of slot b
384
+ // under which the two loops show the same pose at the same gait phase — b's cycle count searched over
385
+ // cyclesA × {1/3 … 3}, its offset on a fine grid. out = [offset, cycles, score (metres), margin (runner-up
386
+ // ≥ 0.2 cycle away minus the best; ~0 = ambiguous)]. Returns 1, or 0 when there is nothing to compare.
387
+ animatorSlotAlign(animatorId: number, a: number, b: number, offA: number, cyclesA: number, out: Float32Array): number
388
+ // Contact spans as (side 0 left / 1 right, from, to, atX, atY, atZ) sextuplets (seconds, model space);
389
+ // returns the span count, filling `out` up to its capacity.
390
+ animatorSlotContacts(animatorId: number, slot: number, out: Float32Array): number
391
+ // A CLIMBING clip's tread levels (model-space plant heights, sorted ascending): out[0] = the
392
+ // riser (median level spacing), out[1..] = the levels, filled up to out's capacity. Returns the
393
+ // level count — 0 for a flat clip.
394
+ animatorSlotTreads(animatorId: number, slot: number, out: Float32Array): number
395
+ // The clip's baked physics at `time` seconds (< 0 = the slot's current time), unit body mass,
396
+ // model space: out11 = COM position xyz, COM velocity xyz (= linear momentum per kg), angular
397
+ // momentum about the COM xyz, then per-foot support left/right (contact-gated, seesaw split,
398
+ // scaled by the vertical force proxy — > 1 on a landing, 0 in flight). 0 = no body segments
399
+ // classified on this skeleton.
400
+ animatorSlotPhysics(animatorId: number, slot: number, time: number, out: Float32Array): number
401
+ // The clip's MATCHING FEATURE ROW at `time` (37 floats, the clip's heading frame at that time —
402
+ // x lateral (+ left), y up, z forward): 0–5 feet positions (relative to the pelvis' ground
403
+ // point), 6–11 feet velocities, 12–14 pelvis velocity, 15 pelvis height, 16–18 COM velocity,
404
+ // 19–20 support L/R, 21–22 contact phase L/R, 23 yaw angular momentum, 24–31 the clip's own
405
+ // path 0.3/0.6/1.0/1.5 s ahead as (lateral, forward) pairs, 32–35 facing change at those
406
+ // horizons (rad, + = left), 36 cyclic flag. Returns 37, or 0 without a leg chain.
407
+ animatorSlotFeatures(animatorId: number, slot: number, time: number, out: Float32Array): number
408
+ // The calibrated knee HINGE AXIS of a side (0 left / 1 right): a unit vector in the thigh's local
409
+ // frame — a skeleton property, measured over every bound clip's knee rotation track. out8 = axis
410
+ // xyz, spread mean (rad), spread max (rad), measurement count, 0, 0. 0 = no leg / no knee motion.
411
+ animatorKneeAxis(animatorId: number, side: number, out: Float32Array): number
412
+ // The knee's bend plane of a slot's clip at `time` seconds (< 0 = the slot's current time),
413
+ // predicted from the hinge axis + the clip's own thigh rotation — continuous even where the leg
414
+ // is straight. out6 = pole xyz (unit, model space, toward the knee — a two-bone solver's bend
415
+ // direction), then the plane normal xyz. 0 = uncalibrated / no leg.
416
+ animatorSlotKneePole(animatorId: number, slot: number, side: number, time: number, out: Float32Array): number
417
+ // Step warp v2: stride scales the feet's travel-direction offsets from the hips (uniform through
418
+ // stance and swing), lift = metres ADDED to their height (swing-gated by the contact marks;
419
+ // 0 = neutral, negative = a shuffle; half of what it adds raises the pelvis, capped by the
420
+ // planted legs' remaining extension), pitchDeg rotates each foot about its lateral axis
421
+ // (+ = toes up), slopeDeg the invisible staircase (+ = ascending: foot heights follow the
422
+ // incline + the feet auto-pitch; the HOST climbs the body at tan(slope) × the stride-scaled
423
+ // travel, which holds each planted foot's world height constant on its tread). Solved in the
424
+ // calibrated knee hinge plane with a soft reach; the pelvis lowers by any leg's overreach
425
+ // (marks-weighted, spring-followed, zero when nothing overreaches). 1/0/0/0 = identity;
426
+ // on false = off.
427
+ animatorSetStepWarp(animatorId: number, on: boolean, stride: number, lift: number, pitchDeg: number, slopeDeg: number): void
428
+ // Footsteps: a contact that BEGAN this evaluation on what the base layer shows — side 0 left / 1 right,
429
+ // x/y/z = the foot's WORLD position at the plant. Fired after the frame's transform commit.
430
+ setOnAnimatorStep(callback: (animatorId: number, side: number, x: number, y: number, z: number) => void): void
431
+
432
+ // ---- drives + warping ----
433
+ // The character's world velocity this frame: the speed the stride warp fits the stride to and the
434
+ // direction the orientation warp turns the lower body toward.
435
+ animatorSetMotion(animatorId: number, vx: number, vy: number, vz: number): void
436
+ // Cumulative capsule travel (m) and yaw (rad, + = left) — what a slot's distance / angle drive reads.
437
+ animatorSetDriveInput(animatorId: number, distance: number, angle: number): void
438
+ // Drive a slot's clock by a quantity instead of time: 0 time / 1 distance / 2 angle. `entry` is the
439
+ // curve value that corresponds to NOW (a start 0; a stop with D metres left: total travel − D), so
440
+ // the clip is entered where it already agrees with the body. Stalls fall back to time.
441
+ animatorSetSlotDrive(animatorId: number, slot: number, mode: 0 | 1 | 2, entry: number): void
442
+ // [stride on, stride min, stride max, orient on, orient max°, orient time, min speed, pelvis drop]
443
+ animatorSetWarpParams(animatorId: number, params: Float32Array): void
444
+
445
+ // ---- feet: foot lock + ground IK (docs/animation-v2-plan.md §2.9) ----
446
+ // The feet stage runs in WORLD space inside the evaluation, after the clips are composited: a foot
447
+ // the shown clip calls planted is pinned where it landed and the leg re-solved to keep it there
448
+ // while the body moves on (the lock), and each foot is put on the ground the ENGINE probed under it,
449
+ // the pelvis lowered so the leg reaches (ground IK). The engine casts the probe rays itself against
450
+ // what a character can stand on (static + moving solids; never the character's own bodies, debris
451
+ // or sensors) and reads the CharacterController's ground state — nothing per frame from the SDK.
452
+ // [ik on, lock on, pelvis drop m, unlock distance m, lock-in s, lock-out s, pelvis spring s,
453
+ // align to ground normal 0..1, probe half-length m, detector max speed m/s, detector max height m]
454
+ // Optional: a host without it has no feet stage (the legs stay as animated).
455
+ animatorSetFeetParams?(animatorId: number, params: Float32Array): void
456
+ // One foot after this frame's evaluation, side 0 left / 1 right: out ← [locked, lock weight,
457
+ // anchor xyz, target xyz] (world) — a debug overlay's beam under the foot. False = no such foot.
458
+ animatorFootState?(animatorId: number, side: number, out: Float32Array): boolean
459
+
460
+ // ---- IK chains + sockets (creator-anim anchors.cpp: the gun-master rig's hands) ----
461
+ // The chains are solved in the engine's LATE pass — after the app's update(dt) phase, before the
462
+ // dynamic bones and the skin flush — on the joints as the app left them: a world-target chain reaches
463
+ // the point / rotation given (or its target NODES, read by the engine then), an ANCHORED chain rides
464
+ // a live socket, keeping the animated end bone's pose relative to the same socket on the take's
465
+ // donor and following the playing clips' anchor spans between sockets. Nothing per frame from the
466
+ // SDK. Optional: a host without them has inert IK (the SDK warns once).
467
+ // kind 0 two-bone (the chain = the end entity, its parent, its grandparent) / 1 look-at. 0 = the
468
+ // entity is no joint of the animator.
469
+ animatorIkCreate?(animatorId: number, kind: 0 | 1, endEntityId: number): number
470
+ animatorIkDestroy?(animatorId: number, ik: number): void
471
+ // CANIM_IK_* order: [target xyz, pole xyz, has pole, weight, rotation xyzw, rotation weight, look-at
472
+ // axis xyz, look-at limit°, enabled, anchor time s].
473
+ animatorIkSet?(animatorId: number, ik: number, params: Float32Array): void
474
+ // Nodes the engine reads every late pass (0 = none): the target's world position, the pole's, the
475
+ // rotation node's world rotation — in place of the fixed floats above.
476
+ animatorIkNodes?(animatorId: number, ik: number, targetEntity: number, poleEntity: number, rotationEntity: number): void
477
+ // Anchor the chain to the live socket ('' = a world-target chain).
478
+ animatorIkAnchor?(animatorId: number, ik: number, socket: string): void
479
+ // out ← [reach error m, anchor delta m, anchor delta rad, applied weight]. False = no such chain.
480
+ animatorIkState?(animatorId: number, ik: number, out: Float32Array): boolean
481
+ // A socket: set '' = live, else a donor's name; on joint `jointEntityId` (a bone of the animator). A
482
+ // NODE socket (nodeEntityId != 0) is that entity, read every late pass (its world → the joint's
483
+ // space); else trs = [t.xyz, r.xyzw] fixed in the joint's space. No node and null trs = remove.
484
+ animatorSetSocket?(animatorId: number, set: string, name: string, jointEntityId: number, nodeEntityId: number, trs: Float32Array | null): void
485
+ // A clip's anchor spans for one bone: the donor set, one '\n'-joined socket name per span ('' = the
486
+ // joint itself), spans = (from, to, rotation 0..1) per span in NORMALIZED time. Empty spans + donor
487
+ // '' = remove; a clip without data takes no part in the anchor blend.
488
+ setClipAnchors?(clipSetId: number, clip: number, bone: string, donor: string, sockets: string, spans: Float32Array): void
489
+
490
+ // ---- locomotion: the movement model + the clip selector, both engine-side ----
491
+ // The intent (`locoSetInput`) becomes a desired velocity; the simulated velocity springs toward it
492
+ // one fixed substep at a time (`locoStep`, given the capsule's place), and `locoUpdate` — called
493
+ // before the animators are evaluated — picks and drives what the base layer shows. The host moves
494
+ // the character with the velocity and facing it reads back: in displacement `code` the animation
495
+ // never moves the body, so "how fast am I" is an input to the animation, not an output of it.
496
+ locoCreate(animatorId: number): number
497
+ locoDestroy(loco: number): void
498
+ // [halflife walk, halflife run, halflife facing, speed walk, speed run, speed sprint, deadzone,
499
+ // turn min°, turn big°, blend, stop blend, start tap, resume, predict, displacement (0 code /
500
+ // 1 data / 2 hybrid), mode (0 rules / 1 matching), match interval, match blend, spin min°, turn
501
+ // rate cap (deg/s, 0 = uncapped), halflife braking, hybrid adjustment clamp (m/s)]
502
+ locoSetParams(loco: number, params: Float32Array): void
503
+ locoDefaults(out: Float32Array): void
504
+ // Register a bound slot: kind 0 idle / 1 gait / 2 start / 3 stop / 4 turn / 5 match / 6 spin
505
+ // (a turn on the spot); `angle` degrees (+ = left) for the starts / turns / spins, `speed` m/s for
506
+ // a gait (0 = the clip's own measured speed), `gait` the gait a transition belongs to (0 walk /
507
+ // 1 run / 2 sprint, -1 = any) — a walking body plays the walking starts, stops and turns.
508
+ locoSetEntry(loco: number, slot: number, kind: number, angle: number, speed: number, gait: number): void
509
+ locoClearSet(loco: number): void
510
+ // [dir x, dir z, magnitude 0–1, face x, face z, gait 0 walk / 1 run / 2 sprint] — the facing is
511
+ // independent of the movement: a released key with a heading still owed turns the body on the spot.
512
+ locoSetInput(loco: number, input: Float32Array): void
513
+ // One simulation step at the capsule's world place — from the fixed substep loop.
514
+ locoStep(loco: number, dt: number, px: number, py: number, pz: number): void
515
+ // Run the selector for this frame — before the animators are evaluated.
516
+ locoUpdate(loco: number, dt: number): void
517
+ // out ← [state (0 idle / 1 start / 2 move / 3 turn / 4 stop / 5 spin), speed, vel x/y/z, yaw (rad,
518
+ // 0 = +Z, + = toward +X), yaw rate, phase, state sequence, then 3 × (x, z, dir x, dir z) predicted
519
+ // at +0.2 / +0.4 / +0.7 s].
520
+ locoRead(loco: number, out: Float32Array): void
521
+ // Why the last STOP was the one played: one row of 9 floats per candidate weighed — [slot, entry
522
+ // time, metres it still travels, metres the body needs, seconds to its next foot-down, foot-downs
523
+ // left, flags (1 = its phase matched, 2 = at/after its first foot-down, 4 = played, 8 = entered
524
+ // ahead of that foot-down by its pose), score, pose distance from what showed (m, -1 = not
525
+ // measured)]. Returns the rows written. A debug read; absent on hosts predating it.
526
+ locoStopReport?(loco: number, out: Float32Array): number
527
+ // Build the motion-matching database over the registered set (selector mode 1); returns frames.
528
+ locoBuildDatabase(loco: number): number
529
+
530
+ /** Subtree AABB in the entity's OWN local space (its own transform excluded) as
531
+ * [minX,minY,minZ, maxX,maxY,maxZ] — zeros for an empty / not-yet-loaded subtree. What
532
+ * `Shape.fit()` measures. Optional: absent on hosts predating the binding. */
533
+ computeBoundingBox?(entityId: number): Float32Array
534
+ /** DEBUG pick: the closest TRIANGLE under a screen point (logical px) among the loaded GLB instances —
535
+ * entityId = one Model's root, 0 = every instance — CPU-skinned with the joints' CURRENT pose, both
536
+ * faces. JSON string (`Model.pickTriangle` parses it: node, mesh, primitive, triangle, hit point,
537
+ * the three vertices with their raw JOINTS_0/WEIGHTS_0 pairs), "" on a miss. One full CPU skin per
538
+ * call — click-rate only. Optional: a debug tool, hosts may lack it. */
539
+ pickTriangle?(entityId: number, screenX: number, screenY: number): string
540
+ setColliderFromMesh(entityId: number, meshEntityId: number, form: number): void
541
+ setColliderBox(entityId: number, centerX: number, centerY: number, centerZ: number, sizeX: number, sizeY: number, sizeZ: number): void
542
+ setColliderSphere(entityId: number, centerX: number, centerY: number, centerZ: number, radius: number): void
543
+
544
+ // JoltPhysics. motionType: 0 static / 1 kinematic / 2 dynamic.
545
+ physicsHasSupport(): boolean
546
+ /** Lightmap bake (packages/creator-bake) — a dev-time tool compiled into the desktop host only; `lightmapBake` exists
547
+ * only where `lightmapHasSupport()` is true. Describes the given static instances (GLB roots / Mesh entities;
548
+ * `keysJoined` = one key per id, '
549
+ '-joined), the scene's brightest sun, its point lights (spherical emitters of
550
+ * `lightRadius` metres) and its IBL to the bake, which traces them on the NVIDIA card — direct light plus
551
+ * `bounceRays` Lambertian paths of `bounces` vertices per texel (0 / 0 = direct only) with `patchSamples` next-event
552
+ * samples over the bright patches of a first direct pass — and writes
553
+ * `<outDir>/<stem>.bake` + `<stem>-light[_n].ktx2` (irradiance in lux / lightScale, BC6H) + `<stem>-aux[_n].ktx2`
554
+ * (sun / sky visibility + light direction); `split` adds `<stem>-direct[_n]` / `-indirect[_n]` page sets for the
555
+ * SDK's two views; `denoise` runs OptiX's AI denoiser over 0 nothing, 1 the indirect part, 2 + the sky and the lamps, 3 + the sun; `filter` = the texel's reconstruction filter radius in texels (1 = ±1 texel tent, 0.5 = within the texel, 0 = the centre alone);
556
+ * `probeSpacing` > 0 places REFLECTION PROBES on a grid of that many metres and writes `<stem>-probes.ktx2` (a BC6H atlas of octahedral
557
+ * maps: one column of roughness-level tiles per probe, captured with `probeSize`² cube faces and `probeRays` paths per texel; the scene's
558
+ * IBL cubemap is what an escaping ray sees; `probeIndoor` keeps only the probes under a roof (an open share of the sphere of at most 15 %) — the
559
+ * outdoors reflects the sky through the texel's baked sky visibility instead; `volumeSpacing` (metres, 0 = none) bakes THE LIGHT GRID for movers — `<name>.lgrid`, named in the .bake's `volume`: per cell
560
+ * in free space an ambient cube without the direct sun, links between neighbours a ray connects (what `setAmbientCube`
561
+ * is fed from); `minSize` (metres, 0 = off) leaves every instance smaller than that along every axis out of the atlas — an occluder
562
+ * only, `"small": true` + its `light` (an ambient cube baked where it stands) in the .bake, applied by lightmapObjectApply; `probeLayout` 0 = ROOMS (indoor points slide to their
563
+ * room's middle and merge: one probe per room), 1 = GRID; `probeBox` = 6 floats, min xyz + max xyz, keeps the grid inside that box — null = the
564
+ * receivers' bounds) and the grid into the .bake; `debugDir` (optional) gets the float layers per page. Synchronous; progress on stdout as
565
+ * "[lightmap] …" lines, "[lightmap] done" on success. */
566
+ lightmapHasSupport?(): boolean
567
+ lightmapBake?(outDir: string, stem: string, entityIds: Uint32Array, keysJoined: string, size: number, texel: number, pages: number,
568
+ sunRays: number, skyRays: number, lightRays: number, lightRadius: number, sunAngleDeg: number, bias: number, seed: number,
569
+ bounceRays: number, bounces: number, patchSamples: number, split: boolean, denoise: number, filter: number,
570
+ probeSpacing: number, probeSize: number, probeRays: number, probeIndoor: boolean, probeLayout: number, probeBox: Float32Array | null, minSize: number, volumeSpacing: number, emissiveNits: number, detailTexels: number, debugDir?: string,
571
+ transmit?: string): boolean
572
+ /** Lightmap consumption: the next createGlb takes lightmap.filamat (the material behind that instance id) for its
573
+ * OPAQUE materials and the masked twin for its MASK materials instead of the ubershader, and keeps TEXCOORD_1;
574
+ * BLEND (glass) materials always keep the ubershader. 0xFFFFFFFF clears. The same call arms the foliage tier. */
575
+ setNextGlbLightmapped?(materialInstanceId: number, maskedMaterialInstanceId?: number): void
576
+ /** The level-wide scale every lightmap-material instance shares (present and future): lux per encoded 1.0 of the
577
+ * light atlas — the .bake's `lightScale` times whatever multiplier a debug knob wants. `emissiveNits` = the bake's
578
+ * radiance of emission 1.0 (the .bake's `emissiveNits`): over 0 the statics' glow is drawn scene-referred with it -
579
+ * a panel looks as bright as the light it gives; 0 = screen-referred. */
580
+ lightmapSetOptions?(lightScale: number, emissiveNits?: number): void
581
+ /** A MOVER'S SUN SHADOW ON THE BAKED STATICS: (dx, dy, dz) = TO the sun, (r, g, b) = its illuminance in lux, `on` =
582
+ * the level's aux pages end in the bake's SUN MAP (the .bake's `sunMap`; never true without it). The lightmap
583
+ * material takes the sun's part out of the baked light where the real-time shadow map says "in shadow". */
584
+ lightmapSunSet?(dx: number, dy: number, dz: number, r: number, g: number, b: number, on: boolean, levels?: number): void
585
+ /** The lightmap material's DATA views, level-wide: 0 = the picture, 1 lighting only (a white diffuse material under the camera's exposure), 2 albedo, 3 geometric normal,
586
+ * 4 shading normal, 5 sun visibility, 6 sky visibility, 7 the atlas texel checker (tinted per instance), 8 light
587
+ * direction, 9 false-colour log10 lux over [p1, p2], 10 relief (E on the shading normal / E), 13 what the specular
588
+ * reflects (the probe along the reflection, or the sky + baked light without one), 14 the probe map (grey = the probes' share of the reflection, tinted by the texel's pair), 15 the sky's path of the reflection alone, 16 the probes' path alone. */
589
+ lightmapDebug?(mode: number, p0?: number, p1?: number, p2?: number, p3?: number): void
590
+ /** The level's reflection probes, level-wide (every lightmap-material instance, present and future): the octahedral
591
+ * atlas as a loaded texture (`<stem>-probes.ktx2`, `Texture.load` like a page), its layout (`oct` = level 0's tile edge,
592
+ * `levels`, `perRow`), the probe count and the range the probe maps' records decode with (the .bake's `probes.encode`:
593
+ * lo xyz, size xyz). WHICH probes a pixel reflects is baked into the pages: every aux page carries its probe map
594
+ * under the page (per 4 x 4 block of texels the two probes those texels SEE — rays at bake time — and their weight).
595
+ * Returns the probe count; an invalid texture id clears. */
596
+ lightmapProbesSet?(atlasTextureId: number, oct: number, levels: number, perRow: number, count: number,
597
+ lox: number, loy: number, loz: number, sx: number, sy: number, sz: number): number
598
+ /** Bind the light page + the aux page + this instance's rect (uv1 * [sx, sy] + [ox, oy]) on every lightmap-material
599
+ * instance of the entity (a GLB instance's renderables, or a Mesh entity's own) and move those renderables to light
600
+ * channel 1 alone — a baked static takes nothing from the real-time sun and lamps (channel 0). Returns how many
601
+ * material instances took it (0 = not loaded lightmapped). */
602
+ lightmapApply?(entityId: number, lightTextureId: number, auxTextureId: number, sx: number, sy: number, ox: number, oy: number, group?: number): number
603
+ /** A SMALL STATIC the atlas left out (the bake's `minSize`: `small` + `light` in the .bake): no rect — the light
604
+ * baked where it stands goes into its material parameters. `data` = 27 floats: the ambient cube — per side of its
605
+ * bounds (+x -x +y -y +z -z) the irradiance rgb in lux and the sky visibility — then probe a, probe b, a's weight
606
+ * (65535 = no probe, 65534 = the sky). The textures = any page of the level (the probes' records sit under every
607
+ * aux page). Returns the material instances that took it (0 = not loaded lightmapped). */
608
+ lightmapObjectApply?(entityId: number, lightTextureId: number, auxTextureId: number, data: Float32Array, group?: number): number
609
+ /** BAKED AMBIENT LIGHT for a mover, or anything on a standard or custom LIT shader: every renderable of the GLB
610
+ * instance (or the Mesh entity) takes its diffuse indirect light from `data`'s ambient cube instead of the scene's
611
+ * IBL, and the IBL's reflections through the sky visibility. `data` = 25 or 26 floats: six sides (+x -x +y -y +z -z) of
612
+ * irradiance rgb in lux + one unused float each, the sky visibility (0 a room .. 1 the open sky), then optionally the
613
+ * baked SUN visibility (1 when absent): the real-time directional light on it is multiplied by it — the baked
614
+ * statics' shadow, they cast none in real time while a light grid is loaded. Lamps stay as they are. Cheap per frame. `null` = back to the IBL. Returns the renderables touched. */
615
+ setAmbientCube?(entityId: number, data: Float32Array | null): number
616
+ /** THE LIGHT GRID for movers (`lecodes lightmap bake` writes `<name>.lgrid`, the .bake's `volume` names it):
617
+ * `lightVolumeLoad(fetchId)` hands the engine the fetched file (false = not a grid); `lightVolumeTrack(entityId, on)`
618
+ * marks a GLB instance root or a Mesh entity as a MOVER — every frame the engine gives it the ambient cube of the
619
+ * place it is at (`setAmbientCube`), blended from the reachable cells around it and eased over ~0.12 s;
620
+ * `fixed` = a STATIC the atlas left out (glass, a material the lightmap shader declines): sampled once a grid, and
621
+ * ignored when every primitive of it is baked;
622
+ * `lightVolumeSample(x, y, z)` = that cube at a point (26 floats, null = no grid or no valid cell near), for tools;
623
+ * `lightVolumeClear()` drops the grid and every mover's cube. */
624
+ lightVolumeLoad?(fetchId: number): boolean
625
+ lightVolumeTrack?(entityId: number, on: boolean, fixed?: boolean): void
626
+ lightVolumeClear?(): void
627
+ lightVolumeSample?(x: number, y: number, z: number): Float32Array | null
628
+ /** A light's channels (Filament's, 0..7). Every light is on channel 0 at creation; one that must reach BAKED statics
629
+ * too (an unbaked lamp, a muzzle flash) is added to channel 1. */
630
+ setLightChannel?(entityId: number, channel: number, enable: boolean): void
631
+ /** Shadow flags on every renderable of a GLB instance (Mesh has setCastShadows/setReceiveShadows). */
632
+ setGlbShadows?(entityId: number, cast: boolean, receive: boolean): void
633
+ /** Foliage — the vegetation tier (`foliage.filamat` / `foliage-masked.filamat`, armed per model
634
+ * through setNextGlbLightmapped; the engine knows the tier by its `benders` parameter). Level-wide wind: direction on
635
+ * the ground (normalised by the host), strength = metres of sway at bendHeight, speed rad/s, gust 0..1. Applies to
636
+ * every foliage instance, present and future. */
637
+ foliageSetWind?(dirX: number, dirZ: number, strength: number, speed: number, gust?: number): void
638
+ /** Level-wide sway options: bendHeight = metres above the instance origin where the full sway / touch applies,
639
+ * touchStrength = metres a bender pushes, variation 0..1 = per-copy tint, sunWrap 0..1 = wrapped Lambert. */
640
+ foliageSetOptions?(bendHeight: number, touchStrength: number, variation: number, sunWrap: number): void
641
+ /** An entity whose live world position bends the vegetation within `radius` metres; <= 0 removes it. The host
642
+ * writes the 8 benders nearest the camera to the shader each frame. */
643
+ foliageSetBender?(entityId: number, radius: number): void
644
+ /** Distance fade for every instance of the entity's ASSET (present and future): the cards thin out between `start`
645
+ * and `end` metres from the camera and past `end` the LOD pass takes the instance out of the scene. end 0 = none. */
646
+ foliageSetFade?(entityId: number, start: number, end: number): void
647
+ physicsConfigure(gx: number, gy: number, gz: number, maxBodies: number): void
648
+ setInterpolation(enabled: boolean): void
649
+ // Shape/Physics/Trigger aspects: build a shape once, create bodies from it.
650
+ physicsBuildBox(hx: number, hy: number, hz: number): number
651
+ physicsBuildSphere(radius: number): number
652
+ physicsBuildCylinder(halfHeight: number, radius: number): number
653
+ physicsBuildCapsule(halfHeight: number, radius: number): number
654
+ /** Mesh shape from node-local triangles. convex=false → triangle mesh (static/kinematic/pick/character
655
+ * only; physicsCreateBody returns 0 for a dynamic one), convex=true → convex hull (any motion).
656
+ * (sx,sy,sz) = world scale, applied natively. Returns 0 if the shape can't be built. */
657
+ physicsBuildMesh(vertices: Float32Array, indices: Uint32Array, convex: boolean, sx: number, sy: number, sz: number): number
658
+ /** Same from a loaded GLB root (non-skinned primitives, bind pose, baked sub-node transforms; cached per asset). */
659
+ physicsBuildMeshFromEntity(entityId: number, convex: boolean, sx: number, sy: number, sz: number): number
660
+ /** Terrain collider (Shape { heightfield: true }, docs/terrain-plan.md §1.4): a Jolt HeightFieldShape over
661
+ * the grid `terrainCreate` draws (same arrays, same hole rule). Static / kinematic / pick / character
662
+ * ground only. (sx,sy,sz) = world scale. `physicsUpdateHeightField` rewrites a sample rectangle in
663
+ * place from the FULL arrays (live bodies keep the shape); heights beyond the range chosen at build
664
+ * time (25 % headroom) clamp — the SDK rebuilds the shape when an edit leaves that range. Optional. */
665
+ physicsBuildHeightField?(heights: Float32Array, sizeX: number, sizeZ: number, cellSize: number, holes: Uint8Array | null, sx: number, sy: number, sz: number): number
666
+ physicsUpdateHeightField?(shapeId: number, heights: Float32Array, sizeX: number, sizeZ: number, x0: number, z0: number, w: number, h: number, holes: Uint8Array | null): void
667
+ /** A loaded GLB root's triangle soup — the one `physicsBuildMeshFromEntity` collides — as 9 floats per
668
+ * triangle in the asset root's space (empty when the entity is not a GLB). `Terrain.conform` stamps a
669
+ * road model into the ground with it. Optional. */
670
+ glbTriangles?(entityId: number): Float32Array
671
+ /** Offset a built shape's centre from the node's origin (Shape `origin`) — world units in the
672
+ * body's rotated, UNSCALED frame, sitting outside a mesh shape's scale wrapper. Sets rather
673
+ * than accumulates; (0,0,0) clears it. Optional: an older host just centres on the node. */
674
+ physicsSetShapeOrigin?(shapeId: number, x: number, y: number, z: number): void
675
+ /** Swap a live body's shape, keeping its id, velocity and transform (Shape.fit / a re-attach).
676
+ * updateMass recomputes the inertia tensor. Refuses a triangle mesh on a dynamic body. */
677
+ physicsSetBodyShape?(bodyId: number, shapeId: number, updateMass: boolean): void
678
+ physicsDestroyShape(shapeId: number): void
679
+ /** sensor = trigger (overlap events, no response); pickOnly = raycast-only, non-colliding body. */
680
+ physicsCreateBody(entityId: number, shapeId: number, motion: number, mass: number, sensor: boolean, pickOnly: boolean): number
681
+ physicsSetPickable(bodyId: number, pickable: boolean): void
682
+ /** Surface friction of one body (0 = ice, ~1 = grippy asphalt). Values COMBINE as sqrt(a * b), so
683
+ * a low value on either side dominates. New bodies start at 0.6 — a neutral solid surface.
684
+ * The vehicle wheel cast reads the GROUND body's value — this is what caps a car's cornering. */
685
+ physicsSetFriction(bodyId: number, friction: number): void
686
+ physicsGetFriction(bodyId: number): number
687
+ /** Ray vs pickable bodies → hit entity id (0 = miss); fills `out` = [px,py,pz,nx,ny,nz,fraction]. */
688
+ physicsRaycast(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDist: number, out?: Float32Array): number
689
+ /** Dev-time dump of the static collision geometry (docs/navmesh-plan.md §4) — the input of
690
+ * `lecodes navmesh bake`: every STATIC solid body's triangles in world space, as an NGEO file at
691
+ * `outPath`. `entityIds`/`areas` are per-entity overrides (area 0..15, 255 unwalkable, 254 skip).
692
+ * Returns the triangle count (-1 = failed). Desktop hosts with physics only. */
693
+ physicsStaticGeometry?(outPath: string, entityIds: Uint32Array, areas: Uint8Array): number
694
+ // CharacterController (Jolt CharacterVirtual). The character steps on the engine's FIXED clock like
695
+ // every body (frame-rate independent) and is render-interpolated; the engine owns its gravity, so
696
+ // the SDK never ticks it. Both velocity halves are LATCHED STATE, never one-shot events — JS runs
697
+ // once per frame while the sim runs 0..4 sub-steps, so a one-shot would double-apply or vanish.
698
+ // groundState: 0 OnGround / 1 OnSteepGround / 2 NotSupported / 3 InAir.
699
+ characterCreate(entityId: number, shapeId: number, maxSlopeDeg: number): number
700
+ characterDestroy(charId: number): void
701
+ /** This frame's HORIZONTAL command (world units/s), cleared once a step consumes it — no command
702
+ * means standing still, not coasting. Held across the frame's sub-steps, and it takes the axis
703
+ * back from a latched velocity: the last writer owns X/Z. */
704
+ characterMove(charId: number, x: number, z: number): void
705
+ /** The same command including the vertical — free mode (gravityScale 0): swimming / flying. */
706
+ characterMoveFree(charId: number, x: number, y: number, z: number): void
707
+ /** Seed the LATCHED ballistic vertical (jump / dash). No ground check. */
708
+ characterSetVerticalVelocity(charId: number, vy: number): void
709
+ /** Latch the whole velocity — it persists until a characterMove takes the axis back (knockback,
710
+ * wall jump, launch pad, weightless flight). Gravity still acts on the vertical. */
711
+ characterSetVelocity(charId: number, x: number, y: number, z: number): void
712
+ /** Multiplier over the world gravity; 0 = free mode, which ALSO disables stick-to-floor + stairs. */
713
+ characterSetGravityScale(charId: number, scale: number): void
714
+ characterSetMaxSlope(charId: number, maxSlopeDeg: number): void
715
+ /** Swap the collider live (crouch / stand up), keeping the FEET planted. Returns false when the new
716
+ * shape doesn't fit where the character stands — nothing changed, so the caller retries later and
717
+ * that retry is an exact headroom test. Optional: a host without it can't resize a character. */
718
+ characterSetShape?(charId: number, shapeId: number): boolean
719
+ /** The velocity the solver ENDED UP with after the last step (post-collision), not the command. */
720
+ characterGetVelocity(charId: number, out: Float32Array): void
721
+ characterGetGroundState(charId: number): number
722
+ /** Discontinuous move (spawn / respawn / teleport) — also resets the interpolation pair. */
723
+ characterSetPosition(charId: number, x: number, y: number, z: number): void
724
+ // Vehicle (Jolt VehicleConstraint + WheeledVehicleController). One settings BLOB, so tuning knobs
725
+ // never grow this ABI — layout (floats):
726
+ // header[34]: version(15), mass, comAuto, comY,
727
+ // engTorque, engMaxRpm, engIdleRpm (0 = no floor: the engine can stall, the game
728
+ // holds idle and cranks it), engInertia, engBraking, clutchStrength,
729
+ // diffRatio (<= 0 = a fully OPEN differential),
730
+ // antiRoll (the bar's stiffness as a FRACTION of the wheel spring; 0 = no bars),
731
+ // maxTiltDeg,
732
+ // aeroDownforce, aeroDrag (each a fraction of the car's own WEIGHT at 30 m/s, scaled
733
+ // by v² from there; 0/0 = no aero, Jolt's own behaviour),
734
+ // steerMode (0 = `steer` is the wheel angle as a fraction of the lock; 1 = `steer`
735
+ // is where the driver's HANDS aim a steering column the engine integrates every
736
+ // sub-step: I·θ̈ = T_hand + T_align·(1 − assist) + T_stop − damping·θ̇ − friction,
737
+ // T_hand = clamp(handStiffness·(steer·lock − θ), ±handTorque·steerForce),
738
+ // steerForce = the input's per-frame hold — the game's policy on when the hands let go,
739
+ // T_align = the steered tires' lateral force × (pneumatic trail collapsing to
740
+ // trailFloor × trail at the curve's peak + caster) — heavy at speed, light past
741
+ // the peak, self-centring, counter-steering in a slide),
742
+ // colInertia, colDamping, colFriction (the patch's dry friction, fades out by 1.5 m/s),
743
+ // colCaster, colTrail, colTrailFloor (the aligning torque's arms — read in BOTH
744
+ // modes; steerTorque is reported either way, the force-feedback signal),
745
+ // colHandTorque, colHandStiffness, colAssist (power steering),
746
+ // colRateDeg (the HANDS' top turning speed, deg/s — they cannot push a wheel that
747
+ // outruns them, they can still hold it; the free column is uncapped; 0 = none),
748
+ // colStopDeg, colStopTorque, colStopDamping (the end stop: over the last colStopDeg
749
+ // before the lock the rack pushes back colStopTorque × depth² N·m and damps by
750
+ // colStopDamping × depth N·m·s/rad — progressive, viscous rubber; 0 band = the
751
+ // hard clamp only),
752
+ // colBearing (the column's own dry friction, N·m, at any speed — rack and bearings),
753
+ // colHandRamp (seconds the hands' torque builds to colHandTorque over; 0 = instant),
754
+ // clutchCapacity (N·m the clutch passes before it slips, × the clutch scalar; 0 = Jolt's
755
+ // viscous clutch alone),
756
+ // wheelCount, curveCount
757
+ // + curveCount * 2: the engine's normalized torque curve (x = rpm/maxRpm, y = torque/maxTorque);
758
+ // 0 points keeps Jolt's default
759
+ // + per wheel, VARIABLE length: 15 fixed floats — px, py, pz, radius, width, maxSteerDeg,
760
+ // driven (the wheel's SHARE of the engine's torque: an axle's share is the sum
761
+ // of its two, the left/right split their ratio; every wheel 0 = no drive at
762
+ // all), axle, travel, stiffness, damping,
763
+ // traction (the longitudinal impulse clamp as a multiple of friction × load;
764
+ // 1 = the physical tire, Jolt's sample runs 10), circle (friction circle 0..1:
765
+ // the share of lateral capacity the longitudinal impulse in use takes away;
766
+ // 0 = the two axes independent), sideCount, forwardCount — then
767
+ // sideCount × (slip angle °, friction) and forwardCount × (slip ratio,
768
+ // friction). Tire curves are POINTS the SDK sends (its presets are SDK-side);
769
+ // 0 points keeps Jolt's own curve.
770
+ // In steerMode 0 the steering input is RAW: `steer` × each wheel's maxSteerDeg, per fixed step —
771
+ // any taper / rate limit is the game's. In mode 1 `steer` is where the hands aim the wheel.
772
+ // NO gear list, no pedals and no wheel node ids: the gearbox is the game's (a ratio + clutch in
773
+ // the input vector), the brakes are a torque per wheel in the same vector, and the game poses its
774
+ // wheel models itself from the state. Chassis space is forward -Z / up +Y (matching node.forward);
775
+ // wheel positions are suspension attachment points in unscaled chassis space; `axle` pairs wheels
776
+ // for the differentials + anti-roll bars.
777
+ vehicleCreate(entityId: number, shapeId: number, settings: Float32Array): number
778
+ vehicleDestroy(vehicleId: number): void
779
+ /** ONE input vector, latched and applied once per fixed step: [throttle 0..1, steer −1..1 (a
780
+ * fraction of the wheels' maxSteerDeg, applied as is; with a column, where the hands aim),
781
+ * steerForce 0..1 (with a column: how firmly the hands hold the wheel — the game's per-frame
782
+ * policy on letting go; without one, ignored), ratio (the ONE gear ratio the car runs: 0 =
783
+ * neutral, negative = reverse — the engine never sees a gear list), clutch 0..1 (scales the clutch
784
+ * in the coupled engine/wheel solve), brake_0 … brake_n (N·m of brake torque per wheel, this
785
+ * step)]. A shorter vector leaves the rest as it was. Sticky; a non-zero throttle, steer or brake
786
+ * wakes a sleeping car. */
787
+ vehicleSetInput(vehicleId: number, input: Float32Array): void
788
+ /** Re-apply the TUNABLE half of the blob (same layout) to a live car — differential, per-wheel tire
789
+ * curves / traction / circle, engine torque/RPM/curve, clutch strength + capacity, steer lock, the
790
+ * column, anti-roll stiffness, tilt limit. Structural values (mass, centre of mass, wheel geometry,
791
+ * the driven shares, suspension, whether an axle has a bar) are ignored: those need a re-create. */
792
+ vehicleSetTuning?(vehicleId: number, settings: Float32Array): void
793
+ /** out = [speed, rpm, wheelsInContact, vx, vy, vz, wx, wy, wz, steerDeg, steerTorque] (w = chassis
794
+ * angular velocity, rad/s; steerDeg = where the road wheels are, right-positive; steerTorque = the
795
+ * tires' self-aligning torque on the steering, N·m, + = pulls right — the force-feedback signal,
796
+ * reported in both steering modes)
797
+ * + per wheel [contact, slipLong, slipAngleDeg, suspensionLength, steerDeg, spin, fLat, fLong, vLat, vSlip, load]
798
+ * (fLat/fLong = the tire's forces, N, + = to its right / pushing the car forward; vLat/vSlip = the
799
+ * patch's sliding speeds, m/s, + = to the right / tread spinning up — force × speed is heat; load =
800
+ * the suspension's force on the wheel, N). slipLong is SIGNED (+ spinning up, − locking) and so is
801
+ * slipAngleDeg (+ the patch sliding to the tire's right). suspensionLength, steerDeg and spin are
802
+ * what a wheel model's pose is built from — by the game, the SDK only exposes them. */
803
+ vehicleGetState(vehicleId: number, out: Float32Array): void
804
+ /** Teleport upright and clear all motion (velocities, engine RPM, wheel spin); the input vector is
805
+ * zeroed — the SDK re-sends its own on the next early pass. */
806
+ vehicleReset(vehicleId: number, x: number, y: number, z: number, qx: number, qy: number, qz: number, qw: number): void
807
+ /** The chassis rigid body, for the plain body calls (physicsApplyImpulse, …). 0 if unknown. */
808
+ vehicleBodyId(vehicleId: number): number
809
+ // Ragdoll (Jolt Ragdoll): a Model's skeleton handed to physics — one dynamic body per listed bone
810
+ // (a capsule from the bone's origin to the next joint) joined to its parent part by a swing-twist
811
+ // constraint that carries a swing + twist MOTOR. Built ONCE from the pose the bones are in (that
812
+ // pose is the joints' neutral for the limits) and kept out of the world until ragdollActivate,
813
+ // which takes the bones' CURRENT pose and adds the bodies moving as the engine measured each bone
814
+ // over the last two frames (a swinging arm keeps swinging) plus a launch; from then on the engine
815
+ // writes the bodies' poses onto the bone entities every frame AFTER the animator, until
816
+ // ragdollDeactivate. POWERED (ragdollSetDrive): the motors pull every joint toward the relative
817
+ // rotation the ANIMATED pose shows (the engine hands the animator's pose to physics every frame,
818
+ // right after the animator wrote it and before the bodies overwrite it), and the root can be
819
+ // ANCHORED — the hips body kinematic on the animated hips — so a standing character stays in its
820
+ // animation while a hit jolts a limb and the motors bring it back; strength 0 with no anchor =
821
+ // limp. The skin joints under the hips that carry no part (the spine bones between two parts, the
822
+ // neck, the clavicles, fingers, toes) follow the animation while driven and HOLD their pose as the
823
+ // strength goes to 0 — a limp body never breathes with the loop playing underneath. Deactivating
824
+ // with a blend seeds the model's animator with the fallen pose, so whatever plays next
825
+ // transitions out of it (canimAnimatorSeedPose). Every body carries its bone entity as user
826
+ // data, so physicsRaycast / contact events name the bone. Optional: a host without it has no
827
+ // ragdolls.
828
+ // bones[partCount * 2]: bone entity, `to` entity (0 = a leaf: `length` along the parent's line;
829
+ // a leaf ROOT points along the model's up)
830
+ // settings — header[16]: version(3), partCount, stride(12), friction, linearDamping,
831
+ // angularDamping (0 = Jolt's own), collide (0 = everything, 1 = the STATIC world and
832
+ // other such ragdolls only: dynamic bodies, character controllers and vehicles pass
833
+ // through — Jolt's DEBRIS object/broadphase layer), freeze (1 = once every part of a
834
+ // LIMP body is asleep the bodies turn STATIC where they lie: the last pose keeps being
835
+ // written onto the bones, raycasts still hit, nothing wakes them, the solver skips
836
+ // them; ragdollActive reads false, ragdollDeactivate / ragdollActivate still work —
837
+ // activate makes the parts dynamic again), freezeAfter (seconds LIMP after which the
838
+ // freeze happens regardless; 0 = no cap), driveFrequency (Hz: the motors' position
839
+ // spring), driveDamping (ratio, 1 = critical), driveTorque (N·m per kg of the part:
840
+ // the motors' limit at strength 1), jointFriction (N·m per kg of the part: a torque
841
+ // that resists any joint motion, motor or not), limits (0 = the part cones below;
842
+ // 1 = LEARNED: at the first activation that finds clips bound to the model's
843
+ // animator the engine measures, per joint, how far the clips swing and twist it in
844
+ // its constraint frame and rebuilds the joints to those ranges — the motors never
845
+ // target a pose the limits forbid; no clips yet = the cones, measured at a later
846
+ // activation), limitsMargin (degrees added on each side of a learned range),
847
+ // reserved x1
848
+ // + partCount * 12: parentIndex (-1 = the root; always < the part's own index), radius, length
849
+ // (0 = up to the `to` bone), mass (kg; 0 = from the volume), swingDeg (cone
850
+ // half-angle), twistDeg (half-angle), hinge (0/1), hingeAxisX/Y/Z (in the MODEL
851
+ // node's space), hingeMinDeg, hingeMaxDeg — a hinge is a swing-twist whose cone is
852
+ // flat (2°) across the axis and [min, max] about it (a knee, an elbow)
853
+ ragdollCreate?(rootEntityId: number, bones: Uint32Array, settings: Float32Array): number
854
+ ragdollDestroy?(ragdollId: number): void
855
+ /** Pose the bodies from the bones' current transforms and add them to the world, each moving as
856
+ * its bone was measured over the last two frames, plus the launch (vx, vy, vz) on every body.
857
+ * Already active = re-wake + the launch. false = unknown id / no world. */
858
+ ragdollActivate?(ragdollId: number, vx: number, vy: number, vz: number): boolean
859
+ /** Take the bodies out of the world. The bones get the bodies' last pose written against the
860
+ * parents' CURRENT worlds (move the model node under the hips first), and with `blend` > 0 the
861
+ * model's animator is seeded with it: whatever plays next transitions from the fallen pose over
862
+ * `blend` seconds. */
863
+ ragdollDeactivate?(ragdollId: number, blend: number): void
864
+ /** In the world AND at least one body still awake (a settled ragdoll reads false). */
865
+ ragdollActive?(ragdollId: number): boolean
866
+ /** The rigid body of part `index` (for physicsApplyImpulseAt / velocities). 0 if unknown. */
867
+ ragdollBodyId?(ragdollId: number, index: number): number
868
+ /** The joint motors' strength (0..1 = the share of driveTorque; 0 = off = limp) and the root
869
+ * anchor (the hips body kinematic on the animated hips). Kept across activations; a driven or
870
+ * anchored body never freezes. */
871
+ ragdollSetDrive?(ragdollId: number, strength: number, anchor: boolean): void
872
+ // Dynamic bones (the SDK's DynamicBone; creator-anim canimDyn* behind creator-gl): secondary motion
873
+ // for tails, ears, hair and cloaks — a bone tree under one root simulated as Verlet particle chains
874
+ // (gravity, wind, drag, stiffness toward the animated shape, an angle cone, capsule / sphere
875
+ // colliders, a floor plane, bone length, neighbour links) in an engine stage AFTER the late phase
876
+ // and BEFORE the skin flush: the animator, an active ragdoll and late-phase JS bone writes are the
877
+ // input, the simulated local rotations the output. Bones the animator does not drive keep their
878
+ // bind pose as the target; driven ones follow their clip. Optional: a host without it shows the
879
+ // animation alone.
880
+ // bones[boneCount]: entity ids — the root first, every other bone a child of an earlier one.
881
+ // settings — header[28]: version(1), boneCount, boneStride(7), then the chain params: weight
882
+ // (0..1 animation → simulation; 0 = off, re-arms on the animated pose), follow (0..1 of
883
+ // the root's travel carried onto the particles; 0 = full whip), wind xyz (m/s², world),
884
+ // link (0..1 neighbour-link strength), rate (substep Hz, ≤ 4 substeps per frame),
885
+ // teleport (a root jump past this many metres resets the chain), floor (0/1), floor
886
+ // point xyz, floor normal xyz (the floor slots are overridden by dynamicBoneSetFloor),
887
+ // floorFriction (a Coulomb coefficient: a resting particle's slide loses up to friction · g · dt of
888
+ // speed per substep), iterations
889
+ // (constraint passes per substep, 1..8, default 4; a long rope may want 8), side (the
890
+ // cloth's outside, one-sided colliders: 0 none, 1 away from the root bone's axis, 2 the
891
+ // next xyz in the root bone's frame), side xyz, guideHold (0..1: guides re-applied after
892
+ // every constraint pass with this fraction of their weight; 0 = once before the passes),
893
+ // edges (1 = the bone segments and links collide with the capsules too, not only the particles),
894
+ // spin (> 0: the chain in the parent bone's rotating frame + its centrifugal / Euler forces × spin;
895
+ // 0 = the translational frame, no turn forces), spinInertia (s: the cloth's own rotation follows the body's with this time constant — behind on a start, past the back on a stop; 0 = glued)
896
+ // + boneCount × 7: radius (m), stiffness (0..1 per 1/60 s toward the animated shape), damping
897
+ // (0..1 velocity lost per 1/60 s), gravity (m/s² along world −Y), angleLimit (degrees off
898
+ // the animated direction, 0 = none), mass (relative, 0 = 1: a bone-length constraint
899
+ // moves its two ends in inverse proportion; the root is kinematic; < 0 = PINNED, the bone
900
+ // rides the animation), give (m, pinned bones: a soft pin — a collider may push the bone
901
+ // this far off its animated place, its local translation is written back too; 0 = hard)
902
+ // links: (a, b) bone index pairs held at their rest distance (a cloak's columns; two linked leaves
903
+ // link their virtual tips too, so a hem stays a hem); optional.
904
+ dynamicBoneCreate?(modelRootId: number, bones: Uint32Array, settings: Float32Array, links?: Uint16Array): number
905
+ dynamicBoneDestroy?(id: number): void
906
+ /** Retune a live chain: the same blob as create (the bone rows too when the count matches). */
907
+ dynamicBoneSet?(id: number, settings: Float32Array): void
908
+ /** The floor the particles stay above: mode 0 none / 1 the plane (point, normal) / 2 probe — the
909
+ * engine casts a ray from the chain root down every frame (the ground probe, the model root
910
+ * excluded) and uses the hit; no hit / no physics world = no floor that frame. */
911
+ dynamicBoneSetFloor?(id: number, mode: 0 | 1 | 2, x: number, y: number, z: number, nx: number, ny: number, nz: number): void
912
+ /** The next frame snaps the chain onto the animated pose (a teleport, a cut). */
913
+ dynamicBoneReset?(id: number): void
914
+ /** The colliders this chain collides with (dynamicBoneColliderCreate ids); replaces the list. */
915
+ dynamicBoneSetColliders?(id: number, colliderIds: Uint32Array): void
916
+ /** The particles' world positions (bones first, then the leaves' virtual tips), 3 floats each into
917
+ * `out`; returns the count written — a debug overlay. */
918
+ dynamicBoneParticles?(id: number, out: Float32Array): number
919
+ // guides: rows × 5 — bone index (in the chain's bone order), world x y z, weight — world points the
920
+ // bones' particles are drawn to before the constraints; null / empty clears; refreshed every frame
921
+ dynamicBoneTargets?(id: number, rows: Float32Array | null): void
922
+ /** A capsule a → b in the entity's local space (a == b = a sphere) that rides the entity — a bone
923
+ * of the body the chains must not pass through. Returns a collider id (0 on failure). */
924
+ /** `sided` 1 = one-sided: while the capsule moves away from the chain's `side` it puts a bone it has run into over to that side instead of carrying it (an arm under a cape); 0 = pushes to the nearest surface. */
925
+ dynamicBoneColliderCreate?(entityId: number, ax: number, ay: number, az: number, bx: number, by: number, bz: number, radius: number, sided: number): number
926
+ dynamicBoneColliderSet?(colliderId: number, ax: number, ay: number, az: number, bx: number, by: number, bz: number, radius: number, sided: number): void
927
+ dynamicBoneColliderDestroy?(colliderId: number): void
928
+ // Legacy coupled shape+body (still used by the worker RigidBody).
929
+ physicsCreateBox(entityId: number, hx: number, hy: number, hz: number, motionType: number, mass: number): number
930
+ physicsCreateSphere(entityId: number, radius: number, motionType: number, mass: number): number
931
+ physicsCreateCylinder(entityId: number, halfHeight: number, radius: number, motionType: number, mass: number): number
932
+ physicsSetLinearVelocity(bodyId: number, x: number, y: number, z: number): void
933
+ physicsGetLinearVelocity(bodyId: number, out: Float32Array): void
934
+ /** Angular velocity about each world axis, RADIANS/second — Jolt's unit; the SDK exposes degrees.
935
+ * The only way to stop a spin: a position write leaves both velocities untouched. */
936
+ physicsSetAngularVelocity?(bodyId: number, x: number, y: number, z: number): void
937
+ physicsGetAngularVelocity?(bodyId: number, out: Float32Array): void
938
+ physicsApplyImpulse(bodyId: number, x: number, y: number, z: number): void
939
+ physicsApplyImpulseAt?(bodyId: number, x: number, y: number, z: number, px: number, py: number, pz: number): void
940
+ physicsSetBodyPosition(bodyId: number, x: number, y: number, z: number): void
941
+ /** The rotation twin (normalized host-side). Both snap the body's render-interpolation pair, so a
942
+ * discontinuous move is drawn as one rather than as a one-frame slide/spin across the gap. */
943
+ physicsSetBodyRotation?(bodyId: number, qx: number, qy: number, qz: number, qw: number): void
944
+ physicsRemoveBody(bodyId: number): void
945
+
946
+ createMediaPlayerTexture(id: number): number
947
+
948
+ getName(entityId: number): string
949
+ setName(entityId: number, name: string): void
950
+
951
+ hasMesh(entityId: number): boolean
952
+
953
+ createARController(sceneId: number, cameraId: number, mode: string, onTrack: (entityId: number, track: boolean) => void): void
954
+ createRootAnchor(sceneId: number): number
955
+ createAnchor(sceneId: number, physicalWidth: number, systemId: number): number
956
+
957
+ // Particles. All emitter/curve config crosses as ONE Float32Array of [tag, payloadLen,
958
+ // ...payload] records, parsed once in creator-particles (CPART_TAG_* in creator-particles.h; the
959
+ // SDK mirror is in gl/Particles.ts, guarded by sdk/tests/particles-tags.test.ts). Unknown
960
+ // tags skip by length. maxParticles 0 = default (1000).
961
+ createParticleSystem(entityId: number, materialInstanceId: number, maxParticles: number): void
962
+ spawnParticles(entityId: number, count: number): void
963
+ setParticleSystemConfig(entityId: number, data: Float32Array): void
964
+
965
+ // Projected decals (creator-gl src/decals.h; SDK gl/DecalSet.ts). A set on an entity = one
966
+ // renderable of `capacity` (0 = 256) unit boxes drawn with the material instance (decal.filamat)
967
+ // — each box projects its atlas cell onto the opaque scene behind it through the scene depth
968
+ // buffer. A record is 27 floats in the SET entity's space: X Y Z axes scaled by the box's
969
+ // width / height / depth (Z = out of the surface), centre, atlas rect u0 v0 u1 v1, tint rgba,
970
+ // life (s, 0 = forever), fadeIn (s), fadeOut (s), capStart, capEnd (tilt of the image's bottom /
971
+ // top edge in unit space — mitred trail joints, 0 = square), alphaStart, alphaEnd (opacity
972
+ // multipliers at the bottom / top edge — a gradient along the image). addDecal returns the slot (0xFFFFFFFF = no
973
+ // set; a full set recycles its oldest); updateDecal keeps the slot's birth time. Optional:
974
+ // a host without them draws no decals (the SDK warns once).
975
+ createDecalSet?(entityId: number, materialInstanceId: number, capacity: number): void
976
+ addDecal?(entityId: number, record: Float32Array): number
977
+ updateDecal?(entityId: number, slot: number, record: Float32Array): void
978
+ removeDecal?(entityId: number, slot: number): void
979
+ clearDecals?(entityId: number): void
980
+ decalCount?(entityId: number): number
981
+
982
+ createNoise(): number
983
+ setNoiseFrequency(noiseId: number, frequency: number): void
984
+ setNoiseOctaves(noiseId: number, octaves: number): void
985
+ setNoiseFractalLunacrity(noiseId: number, lunacrity: number): void
986
+ setNoiseFractalGain(noiseId: number, fractalGain: number): void
987
+ getNoise2D(noiseId: number, x: number, y: number): number
988
+ getNoise3D(noiseId: number, x: number, y: number, z: number): number
989
+
990
+ captureImage(sceneId: number, onComplete: (bufferId: number, name: string, size: number) => void, onReject: () => void): void
991
+ }
992
+
993
+ // ---- 2D engine (sokol / creator-2d) ----------------------------------------------------------
994
+ // A 2D-only project loads ONLY creator2d.wasm — never Filament.
995
+ var _creator2d: {
996
+ backend: string
997
+ version(): number
998
+ render(nowMs: number): void
999
+ onUpdate(cb: (dt: number) => void): void
1000
+ offUpdate(cb: (dt: number) => void): void
1001
+ // Render-synced aspect update(dt) dispatch, called from inside c2dRender. Early = before the
1002
+ // physics step; late = after animations, just before draw. Single-slot per phase (the SDK's
1003
+ // Aspect dispatcher registers one callback that iterates its ordered updater list).
1004
+ setEarlyUpdate(cb: (dt: number) => void): void
1005
+ setLateUpdate(cb: (dt: number) => void): void
1006
+ /** Clock multiplier for the 2D physics step + sprite animations (see `_creator.setTimeScale`). */
1007
+ setTimeScale?(scale: number): void
1008
+ setOnAnimEvent(cb: (ev: { entityId: number, clipId: number, type: number }) => void): void
1009
+ // Raw scene pointer events from the host (canvas / iOS view). phase: 0 down, 1 move, 2 up, 3 cancel;
1010
+ // x,y in logical (CSS px / iOS point) coords relative to the surface. The SDK hit-tests + tracks.
1011
+ setOnPointer(cb: (phase: number, pointerId: number, x: number, y: number) => void): void
1012
+ setClearColor(r: number, g: number, b: number, a: number): void
1013
+
1014
+ createScene(): number
1015
+ destroyScene(sceneId: number): void
1016
+ openScene(sceneId: number): void
1017
+ closeScene(): void
1018
+ sceneSetClearColor(sceneId: number, r: number, g: number, b: number, a: number): void
1019
+ addEntityToScene(sceneId: number, entityId: number): void
1020
+ removeEntityFromScene(sceneId: number, entityId: number): void
1021
+ setLayerYSort(sceneId: number, layer: number, enabled: boolean): void
1022
+
1023
+ cameraSetPosition(sceneId: number, x: number, y: number): void
1024
+ cameraSetZoom(sceneId: number, zoom: number): void
1025
+ cameraSetRotation(sceneId: number, deg: number): void
1026
+ cameraScreenToWorld(sceneId: number, sx: number, sy: number): Float32Array
1027
+ cameraWorldToScreen(sceneId: number, wx: number, wy: number): Float32Array
1028
+
1029
+ createEntity(): number
1030
+ destroyEntity(entityId: number): void
1031
+ setPosition(entityId: number, x: number, y: number): void
1032
+ setRotation(entityId: number, deg: number): void
1033
+ setScale(entityId: number, sx: number, sy: number): void
1034
+ setLayer(entityId: number, layer: number): void
1035
+ setZ(entityId: number, z: number): void
1036
+ setVisible(entityId: number, visible: boolean): void
1037
+ getPosition(entityId: number): Float32Array
1038
+
1039
+ setParent(childId: number, parentId: number, keepWorld: boolean): void
1040
+ getParent(entityId: number): number
1041
+ getChildCount(entityId: number): number
1042
+ getChild(entityId: number, index: number): number
1043
+ getWorldPosition(entityId: number): Float32Array
1044
+ getWorldMatrix(entityId: number): Float32Array
1045
+ worldToLocal(entityId: number, wx: number, wy: number): Float32Array
1046
+ localToWorld(entityId: number, lx: number, ly: number): Float32Array
1047
+ getLocalTransform(entityId: number): Float32Array
1048
+
1049
+ setSprite(entityId: number, textureId: number): void
1050
+ setSpriteFrame(entityId: number, u0: number, v0: number, u1: number, v1: number): void
1051
+ setSpriteFramePx(entityId: number, px: number, py: number, pw: number, ph: number): void
1052
+ setSpriteSize(entityId: number, w: number, h: number): void
1053
+ setSpriteAnchor(entityId: number, ax: number, ay: number): void
1054
+ setSpriteColor(entityId: number, r: number, g: number, b: number): void
1055
+ setSpriteOpacity(entityId: number, a: number): void
1056
+ setSpriteFlip(entityId: number, flipX: boolean, flipY: boolean): void
1057
+
1058
+ createTexture(systemId: number, onComplete: (texId: number, w: number, h: number) => void, onReject: (e: any) => void): void
1059
+ getTextureWidth(texId: number): number
1060
+ getTextureHeight(texId: number): number
1061
+ destroyTexture(texId: number): void
1062
+ setDefaultFilter(linear: boolean): void
1063
+
1064
+ // Textures from a _creatorCanvas surface (RGBA8, already rasterized — no decode, so synchronous).
1065
+ // See docs/canvas-contract.md.
1066
+ createTextureFromCanvas(surfaceId: number): number
1067
+ updateTextureFromCanvas(texId: number, surfaceId: number): void
1068
+
1069
+ defineAnimation(entityId: number, frames: Float32Array, fps: number, loop: boolean): number
1070
+ playAnimation(entityId: number, clipId: number): void
1071
+ stopAnimation(entityId: number): void
1072
+ setAnimationSpeed(entityId: number, speed: number): void
1073
+ getAnimationFrame(entityId: number): number
1074
+
1075
+ createTilemap(textureId: number, cols: number, rows: number, tileW: number, tileH: number, atlasCols: number, atlasRows: number, data: Int32Array): number
1076
+ setTile(tilemapId: number, x: number, y: number, index: number): void
1077
+
1078
+ bulkSetPositions(ids: Uint32Array, xy: Float32Array, count: number): void
1079
+
1080
+ // physics (Box2D v3; present only in CREATOR_2D_PHYSICS builds — physicsHasSupport() reports it).
1081
+ // See docs/2d-physics-plan.md. physicsHasSupport() reports the BUILD, not the world: the world is
1082
+ // created lazily by the first physicsCreateBody, so configure() is optional.
1083
+ physicsHasSupport(): boolean
1084
+ /** Live and non-destructive: an existing world keeps its bodies and takes the new gravity.
1085
+ * pixelsPerMeter only applies to a world that doesn't exist yet (global Box2D tolerance). */
1086
+ physicsConfigure(gx: number, gy: number, pixelsPerMeter: number, subStepCount: number): void
1087
+ setInterpolation(enabled: boolean): void
1088
+ /** type 0 contactBegin / 1 contactEnd / 2 sensorBegin / 3 sensorEnd. A contact BEGIN carries the
1089
+ * manifold — world point, normal pointing A→B, and the approach speed at impact; the other three
1090
+ * carry zeros. (The begin manifold is pre-solve, so its impulses are all zero — `speed` is the
1091
+ * pre-solve relative normal velocity, which is the number an impact actually wants.) */
1092
+ setOnPhysicsEvent(cb: (entityA: number, entityB: number, type: number, px: number, py: number, nx: number, ny: number, speed: number) => void): void
1093
+ physicsCreateBody(entityId: number, motionType: number): number
1094
+ physicsRemoveBody(bodyHandle: number): void
1095
+ // Shapes carry no `density` (built at density 1, so mass == area — physicsSetMass overrides) and
1096
+ // do carry the collision filter. category/mask are 32-bit; 0 means "everything".
1097
+ physicsAddBox(bodyHandle: number, hw: number, hh: number, ox: number, oy: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
1098
+ physicsAddCircle(bodyHandle: number, radius: number, ox: number, oy: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
1099
+ physicsAddCapsule(bodyHandle: number, x1: number, y1: number, x2: number, y2: number, radius: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
1100
+ physicsAddSegment(bodyHandle: number, x1: number, y1: number, x2: number, y2: number, friction: number, bounce: number, category: number, mask: number): void
1101
+ physicsAddPolygon(bodyHandle: number, pts: Float32Array, count: number, friction: number, bounce: number, isSensor: boolean, category: number, mask: number): void
1102
+ /** A polyline of connected segments — long CONCAVE surfaces in one seam-free piece. STATIC bodies
1103
+ * only, >= 4 points; the SDK synthesises the tangent points an open chain needs, because Box2D's
1104
+ * first and last edges do not collide. One-sided: solid on the right of the point order. */
1105
+ physicsAddChain(bodyHandle: number, pts: Float32Array, count: number, isLoop: boolean, friction: number, bounce: number, category: number, mask: number): void
1106
+ /** Destroy every shape on a body, keeping the body (id, transform, velocity) — the first half of a
1107
+ * live collider rebuild. */
1108
+ physicsClearShapes(bodyHandle: number): void
1109
+ /** Contact events are OFF per body until this turns them on — otherwise every crate-on-crate pair
1110
+ * crosses the bridge every frame. The SDK enables it on the first 'enter'/'exit' listener. */
1111
+ physicsSetContactEvents(bodyHandle: number, enabled: boolean): void
1112
+ physicsSetLinearVelocity(bodyHandle: number, x: number, y: number): void
1113
+ physicsGetLinearVelocity(bodyHandle: number): Float32Array
1114
+ physicsSetAngularVelocity(bodyHandle: number, degPerSec: number): void
1115
+ physicsGetAngularVelocity(bodyHandle: number): number
1116
+ physicsApplyLinearImpulse(bodyHandle: number, x: number, y: number): void
1117
+ /** Impulse at a WORLD point — the lever arm becomes angular impulse (a central impulse never
1118
+ * spins a body). Mirrors 3D physicsApplyImpulseAt. */
1119
+ physicsApplyImpulseAt(bodyHandle: number, x: number, y: number, px: number, py: number): void
1120
+ physicsApplyForce(bodyHandle: number, x: number, y: number): void
1121
+ /** Teleport the body to its ENTITY's current world transform — how node.position / .rotation reach
1122
+ * it. The parent-chain math stays native instead of being re-derived in TS. Snaps prev/cur so the
1123
+ * renderer doesn't interpolate across the jump; the body KEEPS its velocity. */
1124
+ physicsSetFromEntity(bodyHandle: number): void
1125
+ physicsSetFixedRotation(bodyHandle: number, enabled: boolean): void
1126
+ physicsSetGravityScale(bodyHandle: number, scale: number): void
1127
+ physicsSetLinearDamping(bodyHandle: number, damping: number): void
1128
+ physicsSetAngularDamping(bodyHandle: number, damping: number): void
1129
+ physicsSetBullet(bodyHandle: number, enabled: boolean): void
1130
+ physicsSetEnabled(bodyHandle: number, enabled: boolean): void
1131
+ physicsSetAwake(bodyHandle: number, awake: boolean): void
1132
+ /** Scales the shape-derived mass data, so the rotational inertia keeps its ratio. <= 0 restores
1133
+ * the area-derived default. */
1134
+ physicsSetMass(bodyHandle: number, mass: number): void
1135
+ physicsGetMass(bodyHandle: number): number
1136
+ physicsSetMotionType(bodyHandle: number, motionType: number): void
1137
+ physicsSetFriction(bodyHandle: number, friction: number): void
1138
+ physicsSetBounce(bodyHandle: number, bounce: number): void
1139
+ physicsSetFilter(bodyHandle: number, category: number, mask: number): void
1140
+ // Queries. `mask` = the union of group categories the query may hit (0 = everything); `ignore` is
1141
+ // a list of ENTITY ids to skip, passed as floats because ids are small ints and that reuses the
1142
+ // one buffer helper. Box2D's closest-ray API has no exclusion, hence the explicit list.
1143
+ physicsRaycastClosest(x0: number, y0: number, x1: number, y1: number, mask: number, ignore: Float32Array, ignoreCount: number): Float32Array
1144
+ /** All hits along the segment, nearest first: maxHits records of [entityId, px, py, nx, ny, fraction]. */
1145
+ physicsRaycastAll(x0: number, y0: number, x1: number, y1: number, mask: number, ignore: Float32Array, ignoreCount: number, maxHits: number): Float32Array
1146
+ /** Overlap an arbitrary convex shape given as a point cloud + radius (Box2D's b2ShapeProxy form),
1147
+ * so one call covers circle / capsule / box / polygon — and the swept test for a moving circle.
1148
+ * Returns up to maxHits entity ids. */
1149
+ physicsOverlap(pts: Float32Array, count: number, radius: number, mask: number, ignore: Float32Array, ignoreCount: number, maxHits: number): Float32Array
1150
+ /** Topmost by DRAW order (layer, then z) — not whatever the broadphase hands back first. */
1151
+ physicsOverlapPoint(x: number, y: number, mask: number, ignore: Float32Array, ignoreCount: number): number
1152
+ // CharacterController2D (Box2D's kinematic mover). The character steps on the engine's FIXED clock
1153
+ // inside the same loop as the bodies, so the SDK does no per-frame work. Capsule ends are in the
1154
+ // character's own frame. It also owns a hidden kinematic "shadow" body so raycasts, overlaps and
1155
+ // sensors see it — solid shapes strip the reserved character bit from their mask, sensors add it.
1156
+ characterCreate(entityId: number, x1: number, y1: number, x2: number, y2: number, radius: number, maxSlopeDeg: number, category: number, mask: number): number
1157
+ characterDestroy(charId: number): void
1158
+ /** The HORIZONTAL command, world units/s — a per-frame command that EXPIRES once a step consumes it. */
1159
+ characterMove(charId: number, x: number): void
1160
+ /** Free mode (gravityScale 0): both axes as one expiring command. */
1161
+ characterMoveFree(charId: number, x: number, y: number): void
1162
+ characterSetVerticalVelocity(charId: number, vy: number): void
1163
+ /** LATCH the whole velocity — it persists until a characterMove takes the axis back. */
1164
+ characterSetVelocity(charId: number, x: number, y: number): void
1165
+ characterSetGravityScale(charId: number, scale: number): void
1166
+ characterSetMaxSlope(charId: number, maxSlopeDeg: number): void
1167
+ characterSetFilter(charId: number, category: number, mask: number): void
1168
+ /** Ignore this entity's one-way surfaces (0 = none) — dropping through a semisolid platform. */
1169
+ characterSetDropThrough(charId: number, entityId: number): void
1170
+ /** Resize keeping the FEET planted; false = it didn't fit and NOTHING changed, so the caller
1171
+ * retries later and that retry is an exact headroom test. */
1172
+ characterSetCapsule(charId: number, x1: number, y1: number, x2: number, y2: number, radius: number): boolean
1173
+ /** Teleport to the entity's world transform (how node.position reaches a character). Drops fall speed. */
1174
+ characterSetFromEntity(charId: number): void
1175
+ /** out8 = [velX, velY, groundState (0 ground / 1 slope / 2 air), normalX, normalY, groundEntity,
1176
+ * collisionCount, 0]. The velocity is MEASURED, not commanded. */
1177
+ characterGetState(charId: number, out: Float32Array): void
1178
+ /** maxN records of [entityId, normalX, normalY] — what the mover pushed out of this step. */
1179
+ characterGetCollisions(charId: number, maxN: number): Float32Array
1180
+ /** Mark a body's surfaces one-way: solid only from the side (nx, ny) points to, within arcDeg.
1181
+ * Honoured by the mover and, via a native pre-solve callback, by ordinary rigid bodies. */
1182
+ physicsSetOneWay(bodyHandle: number, nx: number, ny: number, arcDeg: number, enabled: boolean): void
1183
+ pickSprite(sceneId: number, wx: number, wy: number): number
1184
+ }
1185
+
1186
+ // ---- multiplayer transport (creator-net / yojimbo) -------------------------------------------
1187
+ // A byte pipe pumped on the JS thread (docs/multiplayer-plan.md). Present only on hosts built
1188
+ // with CREATOR_PKG_NET (desktop, the 3d/full Android variants, iOS); the SDK feature-detects it.
1189
+ // One endpoint per process: `listen` (server / host) or `connect` (client), never both.
1190
+ var _creatorNet: {
1191
+ /** What the exe was launched as (`--server --port N` / `--connect ADDR`), or null. */
1192
+ launch(): { role: string, address: string, port: number, maxClients: number } | null
1193
+ /** Serve on every interface at `port` (wildcard public address). */
1194
+ listen(port: number, maxClients: number): boolean
1195
+ /** Insecure (dev / LAN) connect to "ip:port"; the outcome arrives as a poll record. */
1196
+ connect(address: string): boolean
1197
+ disconnect(): void
1198
+ kick(client: number): void
1199
+ /** Queue a payload: channel 0 reliable-ordered, 1 unreliable; `client` ignored on a client.
1200
+ * A string travels as UTF-8 text and arrives as a string; bytes arrive as an ArrayBuffer. */
1201
+ send(client: number, channel: number, data: string | ArrayBuffer | Uint8Array): boolean
1202
+ /** Pump once and return every record since the last call as a flat array of 4-tuples
1203
+ * [kind, client, arg, payload] — kind 1 connected · 2 disconnected · 3 message · 4 connectFailed;
1204
+ * arg = channel (message) or reason code; payload = string | ArrayBuffer | null. */
1205
+ poll(): any[] | null
1206
+ /** 0 idle · 1 client · 2 server */
1207
+ mode(): number
1208
+ /** Client: 0 disconnected · 1 connecting · 2 connected · 3 failed. A listening server: 2. */
1209
+ state(): number
1210
+ /** [connected, rttMs, lossPct, sentKbps, receivedKbps] for a slot (server) / the link (client). */
1211
+ clientInfo(client: number): Float32Array
1212
+ /** yojimbo network simulator on this side of the wire. */
1213
+ simulate(latencyMs: number, jitterMs: number, lossPercent: number): void
1214
+ reasonString(reason: number, serverSide: boolean): string
1215
+ }
1216
+
1217
+ // ---- navigation meshes (creator-nav / Recast+Detour) -----------------------------------------
1218
+ // Runtime only (docs/navmesh-plan.md §5): load a baked `.navmesh` (LNAV) from a fetch system id,
1219
+ // query it, run a DetourCrowd of agents one fixed step at a time. Present on hosts built with
1220
+ // CREATOR_PKG_NAV (desktop, the 3d/full Android variants, iOS) and on every web host through the
1221
+ // wasm build; the SDK feature-detects it. Parameter blocks are fixed-order Float32Arrays
1222
+ // (creator-nav.h CNAV_AGENT_* / CNAV_READ_*). Handles (meshId / crowdId, from 1) die with the world.
1223
+ var _creatorNav: {
1224
+ /** Web hosts only: loads the wasm lazily. Absent on native hosts — call it when present. */
1225
+ ready?(): Promise<void>
1226
+ lastError(): string
1227
+ /** LNAV bytes already fetched (the createGlb pattern) → meshId, 0 on failure. */
1228
+ load(systemId: number): number
1229
+ unload(meshId: number): void
1230
+ /** The file's JSON header: agent size, cell, bounds, stats, `meta` (the CLI's provenance). */
1231
+ header(meshId: number): string
1232
+ polyCount(meshId: number): number
1233
+ setAreaCost(meshId: number, area: number, cost: number): void
1234
+ /** Straight path from → to into `out` (xyz per corner); returns the corner count, NEGATIVE when
1235
+ * the path is partial (the target is unreachable — it ends at the closest polygon). `ex <= 0`
1236
+ * = default search extents. */
1237
+ findPath(meshId: number, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, ex: number, ey: number, ez: number, include: number, exclude: number, maxCorners: number, out: Float32Array): number
1238
+ nearest(meshId: number, x: number, y: number, z: number, ex: number, ey: number, ez: number, include: number, exclude: number, out: Float32Array): boolean
1239
+ /** Walkability ray along the surface; true = BLOCKED, out = [hit xyz, wall normal xyz]. */
1240
+ raycast(meshId: number, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, include: number, exclude: number, out: Float32Array): boolean
1241
+ /** `radius <= 0` = anywhere on the mesh; deterministic per seed. */
1242
+ randomPoint(meshId: number, cx: number, cy: number, cz: number, radius: number, seed: number, include: number, exclude: number, out: Float32Array): boolean
1243
+ /** The polygon mesh as a triangle soup (xyz per vertex) for debug drawing. */
1244
+ debugTriangles(meshId: number): Float32Array
1245
+ crowdCreate(meshId: number, maxAgents: number, maxRadius: number): number
1246
+ crowdDestroy(crowdId: number): void
1247
+ /** → the agent's slot (0..maxAgents-1), -1 when full / off the mesh. */
1248
+ crowdAdd(crowdId: number, x: number, y: number, z: number, params: Float32Array): number
1249
+ crowdRemove(crowdId: number, agent: number): void
1250
+ crowdSetParams(crowdId: number, agent: number, params: Float32Array): void
1251
+ crowdSetTarget(crowdId: number, agent: number, x: number, y: number, z: number): boolean
1252
+ crowdSetVelocity(crowdId: number, agent: number, vx: number, vy: number, vz: number): void
1253
+ crowdResetTarget(crowdId: number, agent: number): void
1254
+ crowdWarp(crowdId: number, agent: number, x: number, y: number, z: number): boolean
1255
+ /** Once per fixed step. */
1256
+ crowdUpdate(crowdId: number, dt: number): void
1257
+ /** Every slot into `out` (CNAV_AGENT_READ_STRIDE floats each); returns maxAgents. */
1258
+ crowdRead(crowdId: number, out: Float32Array): number
1259
+ }
1260
+
1261
+ // ---- Game audio (creator-audio) ----------------------------------------------------------------
1262
+ // docs/audio-plan.md. Whole clips decoded up front, a fixed voice pool (play allocates nothing),
1263
+ // 3D sources the engine follows per frame, mixer buses with insert effects, reverb zones,
1264
+ // occlusion. OPTIONAL as a block: the SDK gates on `typeof _creatorAudio !== 'undefined' &&
1265
+ // hasSupport()` and stays inert (silent Sound, inert Voice) without it. Present on hosts built with
1266
+ // CREATOR_PKG_AUDIO (desktop first; Android / Apple pending — parity `audio-game`). The headless
1267
+ // renderer provides a RECORDER (plays are logged, nothing is mixed). Every parameter block is a
1268
+ // fixed-order Float32Array (creator-audio.h CAUD_SRC_* / the effect param orders).
1269
+ var _creatorAudio: {
1270
+ hasSupport(): boolean
1271
+ /** Decode the bytes behind a fetch system id (WAV / MP3 / FLAC / OGG Vorbis) on the engine's
1272
+ * loader thread; mono unless `stereo`. onDone(clipId, durationSeconds, channels). */
1273
+ loadClip(systemId: number, stereo: boolean, onDone: (clipId: number, duration: number, channels: number) => void, onReject: (message: string) => void): void
1274
+ releaseClip(clipId: number): void
1275
+ /** Bus id by name (-1 unknown); 0 master, 1 sfx, 2 music, 3 ui, 4 voice. */
1276
+ busId(name: string): number
1277
+ /** Creates (or finds) an app-defined bus under master. -1 when the 16 slots are full. */
1278
+ createBus(name: string): number
1279
+ setBusVolume(bus: number, volume: number): void
1280
+ setBusMuted(bus: number, muted: boolean): void
1281
+ /** kind 1 reverb [roomSize, damping, width, mix, preDelay] · 2 echo [delay, decay, mix] ·
1282
+ * 3 lowpass [cutoffHz]; an empty array turns the effect off. Changes are smoothed. */
1283
+ setBusEffect(bus: number, kind: number, params: Float32Array): void
1284
+ stopBus(bus: number, fade: number): void
1285
+ /** A 3D emitter. `attachSource` binds it to an entity the engine follows every frame;
1286
+ * `setSourcePosition` places an unbound one (playAt). */
1287
+ createSource(): number
1288
+ attachSource(sourceId: number, entityId: number): void
1289
+ /** [minDistance, maxDistance, rolloff (0 none 1 inverse 2 linear 3 exp), coneInnerDeg,
1290
+ * coneOuterDeg, coneOuterGain, doppler, spread, occlusion (0/1), bus]. */
1291
+ setSourceParams(sourceId: number, params: Float32Array): void
1292
+ setSourcePosition(sourceId: number, x: number, y: number, z: number): void
1293
+ stopSource(sourceId: number, fade: number): void
1294
+ sourceVoices(sourceId: number): number
1295
+ destroySource(sourceId: number): void
1296
+ /** The listener entity; 0 = the active scene camera (the default). */
1297
+ setListener(entityId: number): void
1298
+ setListenerOptions(dopplerFactor: number): void
1299
+ /** HRTF binaural rendering for the `maxVoices` nearest spatial voices (headphones); the rest keep panning. */
1300
+ setHrtf(enabled: boolean, maxVoices: number): void
1301
+ /** Whether the engine's time scale (Time.scale) also pitches the sfx bus. Default false (pause = mute only). */
1302
+ setTimeScalePitch(enabled: boolean): void
1303
+ /** → a voice token (0 = nothing played: the pool refused, the clip is not ready). bus -1 = the
1304
+ * source's bus (sfx for 2D). pan is 2D only. */
1305
+ play(clipId: number, sourceId: number, bus: number, volume: number, pitch: number, loop: boolean, priority: number, fadeIn: number, startAt: number, pan: number): number
1306
+ setVoiceVolume(token: number, volume: number): void
1307
+ setVoicePitch(token: number, pitch: number): void
1308
+ setVoicePan(token: number, pan: number): void
1309
+ stopVoice(token: number, fade: number): void
1310
+ voicePlaying(token: number): boolean
1311
+ voiceTime(token: number): number
1312
+ /** THE ended listener (single slot per JS world): called once per tick with the tokens of the
1313
+ * voices that ended since the previous tick — natural end, stop, steal. */
1314
+ setOnEnded(callback: ((tokens: Float32Array) => void) | null): void
1315
+ stopAll(fade: number): void
1316
+ /** A reverb volume: shape 0 box (dims = full size) / 1 sphere (dims[0] = diameter), in the
1317
+ * entity's local units; `blend` metres of crossfade inside the border; reverb params as for
1318
+ * setBusEffect kind 1. The listener inside blends the zone's reverb onto `bus`. */
1319
+ createZone(): number
1320
+ attachZone(zoneId: number, entityId: number): void
1321
+ setZone(zoneId: number, shape: number, dims: Float32Array, blend: number, bus: number, reverb: Float32Array): void
1322
+ destroyZone(zoneId: number): void
1323
+ /** [voicesPlaying, voicesMono, voicesStereo, stolen, clips, clipBytes, peak, sampleRate, listenerZone, zoneBlend, hrtfVoices]. */
1324
+ stats(): Float32Array
1325
+ }
1326
+
1327
+ // ---- UI engine (creator-ui) ------------------------------------------------------------------
1328
+ var _creatorUI: {
1329
+ // Global back-press fallback (app.onBackPressed): a PROPERTY the SDK writes, not a call —
1330
+ // the engine reads it off this world's _creatorUI object at the END of its back chain
1331
+ // (widget → destination → pager → router → this), so it dies with the world's context.
1332
+ // Old hosts never read it and the handler is silently inert there.
1333
+ _backButtonCallback?: () => void
1334
+ openScreen(screen: object): void
1335
+ closeScreen(): void
1336
+ updateStyle(nodeId: number, prop: string, value: any): void
1337
+ mergeStyle(nodeId: number, style: object): void
1338
+ // Toggle a user style class (a `$`-block declared in .style()) on a node. `name` arrives
1339
+ // WITHOUT the leading `$`. Class state CASCADES down the node tree: a class set on a node is
1340
+ // active on all its descendants within the same root (screens/widgets are separate roots) —
1341
+ // implemented ONCE in creator-ui's setNodeClass (docs/style-class-cascade-plan.md); hosts must
1342
+ // NOT layer their own inheritance on top. `$pressed`/`$focused` are reserved cascading classes
1343
+ // hosts toggle alongside the built-in onPressed/onFocused (creator-ui setNodePressed/Focused).
1344
+ setClass(nodeId: number, name: string, enabled: boolean): void
1345
+
1346
+ // Theme variables (docs/ui-theme-plan.md). MERGES `values` into the app theme table and
1347
+ // re-resolves live styles: keys are var names (`var(--name)` in style values reads them;
1348
+ // the four `comfort-*` keys are the comfort knobs), null removes a key, numbers are lengths
1349
+ // (px). Env names (safe-*, vw/vh/vmin/vmax) are reserved — hosts ignore writes to them.
1350
+ // Optional during rollout: the SDK feature-detects.
1351
+ setTheme?(values: Record<string, string | number | null>): void
1352
+
1353
+ // Content-property push. Props: "value" (input/textarea text), "focus" (boolean — focus/blur
1354
+ // the input, opening/dismissing the keyboard; rides this channel so programmatic focus needs
1355
+ // no new ABI method), plus element-specific ones ("src", "text", …).
1356
+ // An image "src" (here and at creation) is a url string, { _id } (a host buffer), { svg },
1357
+ // { canvasSurface } (a baked Canvas), or { scene2d: sceneId } — a LIVE 2D scene the host draws
1358
+ // into the node's laid-out box every frame with the scene's own camera (native: c2dDrawSceneGL
1359
+ // into a per-node framebuffer; no intrinsic size, the scene needs no openScene). The engine's
1360
+ // simulation must still step each frame for it (native: c2dTick when nothing is presented).
1361
+ updateNode(nodeId: number, prop: string, value: any): void
1362
+ setSourceRect(nodeId: number, x: number, y: number, w: number, h: number): void
1363
+
1364
+ // Canvas.update() → live-refresh every UIImage(canvas) node currently showing this baked surface:
1365
+ // the host re-reads the re-rasterized pixels into the image view WITHOUT a relayout (the surface
1366
+ // keeps its id and size across an update; a resize goes through a full `src` re-assign). Optional
1367
+ // during rollout — the SDK feature-detects and falls back to re-assigning `src`.
1368
+ refreshCanvasSurface?(surfaceId: number): void
1369
+
1370
+ // Font registration is fire-and-forget in practice: hosts load the face, then re-measure /
1371
+ // re-layout when it lands (docs/font-system-plan.md — swap is the fallback; boot faces are
1372
+ // prefetched via the bundle's `// fonts:` header). The callbacks stay on the wire for old
1373
+ // bundles that gate on them — hosts must keep invoking onComplete. Repeated registration of
1374
+ // the same (family, weight, style) is a cheap no-op — hosts dedupe.
1375
+ registerFont(fontFamily: string, url: string, options: any, onComplete: () => void, onReject: () => void): void
1376
+ // Preferred container format for CDN-served faces: "woff2" on browser hosts, absent (= "ttf")
1377
+ // on native/headless. The font() macro's generated registration code reads it to pick the
1378
+ // file extension; the `// fonts:` header carries a `{fmt}` placeholder for the same choice.
1379
+ fontFormat?: "woff2" | "ttf"
1380
+
1381
+ isButtonPressed(nodeId: number): boolean
1382
+
1383
+ registerTouchStartEvent(callback: (...args: any) => void): void
1384
+ registerTouchEndEvent(callback: (...args: any) => void): void
1385
+ registerResizeEvent(callback: (...args: any) => void): void
1386
+ // Current display size [width, height] in logical px — the same values the resize event delivers.
1387
+ // Surfaced as device.width / device.height. Optional: hosts that don't track it (headless) omit it,
1388
+ // and the getters fall back to 0.
1389
+ getDisplaySize?(): [number, number]
1390
+
1391
+ getTextValue(nodeId: number): string
1392
+
1393
+ // Absolute device-space rect [left, top, width, height] of a laid-out node, in logical px —
1394
+ // the same space UIWidget top/left position in — INCLUDING scroll offsets: computed live at
1395
+ // call time, never cached from layout events. null (or undefined) when the node isn't
1396
+ // mounted/laid out. Backs el.getBoundingClientRect(), the widget-anchoring primitive
1397
+ // (dropdowns, popovers, tooltips — docs/ui-components-plan.md §3.2). Optional: the SDK
1398
+ // feature-detects and returns null on hosts without it.
1399
+ getBoundingClientRect?(nodeId: number): [number, number, number, number] | null
1400
+
1401
+ insertNode(index: number, nodeId: number, childNode: any): void
1402
+ removeNode(nodeId: number, childNode: any): void
1403
+ setContent(nodeId: number, oldChildren: any[], children: any[]): void
1404
+
1405
+ // Legacy one-shot style tweens, implemented by the hosts that animate JS-side (web, Apple) and
1406
+ // ABSENT on creator-pkg hosts since 2026-09-14 (they have the tween core below; the SDK's fallback
1407
+ // only calls these where tweenCreate is missing): from the current values to `style` (animateTo)
1408
+ // / from `style` to the current values (animateFrom), meta keys duration / delay / loop /
1409
+ // loopMode / commit / layer in the bag.
1410
+ animateTo?(nodeId: number, style: any): void
1411
+ animateFrom?(nodeId: number, style: any): void
1412
+ toast(msg: string): void
1413
+
1414
+ // ---- Keyframe animation core (docs/timeline-plan.md; creator-tween) -------------------------
1415
+ // ONE evaluator for UI style props, 3D node transforms / lights / cameras and 2D sprites: the SDK
1416
+ // hands over a flat blob (layout in sdk/src/animate/tween/spec.ts — header, time-callbacks,
1417
+ // easing tables, tracks of keyframes; string-valued lanes index `strings`, a track's target lane
1418
+ // indexes `targets`: the element's `_id` handle for the UI domain, an entity / scene id for the
1419
+ // 3D and 2D domains) and gets an id.
1420
+ // Created = paused at t = 0, nothing applied. Ops: 0 play (from t = 0, a running one restarts),
1421
+ // 1 pause, 2 resume, 3 cancel (drop, values stay where they are, no commit), 4 finish (jump to
1422
+ // the end, commit, report finish). The host reports through the registered callback:
1423
+ // kind 0 = call (index = the blob's call slot, fired when playback crosses its time, in time
1424
+ // order, before finish), kind 1 = finish (the end was reached, one-shot only), kind 2 = a VALUE
1425
+ // track's frame (index = the track's target slot — unique per world, then the lanes as plain number arguments —
1426
+ // delivered synchronously after the evaluator's pass, never from inside it). Seek is in TOTAL
1427
+ // ms (iterations included); tweenGetTime reads the position inside the current iteration,
1428
+ // tweenGetElapsed the total. Clock 0 = host wall time (ui), 1 = game time (Time.scale, pause).
1429
+ // Optional: the SDK feature-detects tweenCreate and falls back to animateTo / animateFrom
1430
+ // (UI tracks, single values, timer clock) on hosts without it.
1431
+ tweenCreate?(data: Float32Array, strings: string[], targets: any[]): number
1432
+ tweenControl?(id: number, op: number): void
1433
+ tweenSeek?(id: number, ms: number): void
1434
+ tweenSetRate?(id: number, rate: number): void
1435
+ tweenGetTime?(id: number): number
1436
+ tweenGetElapsed?(id: number): number
1437
+ registerTweenEvent?(callback: (id: number, kind: number, index: number) => void): void
1438
+
1439
+ // Router over destinations. On hosts that implement openView (below), `screen`/`view` is any
1440
+ // view descriptor (not just a screen object) and the extra `transition` args apply; legacy
1441
+ // hosts receive screen objects only and ignore the extras.
1442
+ routerOpen(view: any, onChange: (view: any) => void, showBackButton: boolean): void
1443
+ routerPush(view: any, transition?: string | object): void
1444
+ routerReplace(view: any, transition: string | object): void
1445
+ routerPop(index: number, transition?: string | object): void
1446
+ routerHide(): void
1447
+ routerRestore(): void
1448
+
1449
+ // ---- Presentable navigation (docs/navigation-presentable-plan.md) --------------------------
1450
+ // One visible destination at a time. `view` is either a screen node object (type "screen")
1451
+ // or a descriptor:
1452
+ // { type: "scene3d", sceneId } { type: "scene2d", sceneId }
1453
+ // { type: "native", viewName, params, viewId } { type: "videoView", node }
1454
+ // Descriptors carry `_p` (the SDK Presentable instance): hand the SAME object back through
1455
+ // router onChange, and fire its `ol`/`cl` arrays on present/dismiss (screens carry ol/cl
1456
+ // themselves). Presenting a scene descriptor includes activating the engine (the openScene
1457
+ // equivalent); while covered, scene rendering pauses. `transition` is a TransitionName string
1458
+ // or a TransitionSpec object (see sdk/src/ui/presentable.ts).
1459
+ // Optional during the migration: the SDK feature-detects openView and falls back to
1460
+ // openScreen/openScene; hosts implementing openView should keep openScreen/routerPush(screen)
1461
+ // as thin aliases into it for previously compiled bundles.
1462
+ openView?(view: object, transition: string | object): void
1463
+ closeView?(): void
1464
+
1465
+ // NativeView plugin channel (host `registerView(name, factory)` capabilities). `args`, the
1466
+ // result, and event payloads are JSON strings. Events are delivered by invoking the arrays in
1467
+ // the element's `nvl[event]` or via its `_emitViewEvent(event, json)`.
1468
+ isViewSupported?(name: string): boolean
1469
+ viewCall?(viewId: number, method: string, args: string, onComplete: (result?: string) => void, onError: (err: string) => void): void
1470
+
1471
+ // `owner` (a view descriptor) binds the widget to a destination: it mounts inside the owner's
1472
+ // page — shows/hides with it and rides its transition. Omitted = global overlay (legacy
1473
+ // behavior; legacy hosts ignore the arg).
1474
+ showWidget(widget: object, owner?: object): void
1475
+ hideWidget(widget: object): void
1476
+
1477
+ vlistMount(jsNode: any, key: string, subtree: any): void
1478
+ vlistSetKeys(jsNode: any, keys: string[], estimates: number[]): void
1479
+ vlistInsertKeys(jsNode: any, index: number, keys: string[], estimates: number[]): void
1480
+ vlistInvalidate(jsNode: any, k: string): void
1481
+ vlistRemoveKeys(jsNode: any, keys: string[]): void
1482
+ command(jsNode: any, command: string, ...args: any): void
1483
+ }
1484
+
1485
+ // ---- Platform utilities ----------------------------------------------------------------------
1486
+ var _creatorUtils: {
1487
+ // `fetchOptions.body` is either a string or a FormData: an object carrying `_entries`, an array
1488
+ // of [key, value, filename?]. A `value` object carrying a numeric `_id` is binary (that's the
1489
+ // host buffer id — SDK `File` and `FetchResponse` both expose it under that key); anything else
1490
+ // is a text field. Same `_id` convention as an image `src` object (see _creatorUI.updateNode).
1491
+ fetch(url: string, fetchOptions: any, onComplete: (systemId: number, statusCode: number) => void, onReject: () => void): void
1492
+ fetchLocal(path: string): number
1493
+ fetchToJson(systemId: number): any
1494
+ fetchSlice(systemId: number, start: number, end: number): any
1495
+ fetchToText(systemId: number): string
1496
+ /** The fetched bytes as a Uint8Array copy (a `.terrain` file, any binary the SDK parses itself).
1497
+ * Optional: an older host has only the text/JSON readers (Terrain.load throws there). */
1498
+ fetchToBytes?(systemId: number): Uint8Array
1499
+ disposeFetch(systemId: number): void
1500
+ // ---- Local filesystem (the SDK `files` global) ----------------------------------------------
1501
+ // The read/write/delete half of `fetchLocal`, for hosts where the app OWNS a filesystem
1502
+ // (desktop). All seven are OPTIONAL and stand or fall together: a sandboxed host (web, mobile)
1503
+ // implements none of them and `files.supported` is false there.
1504
+ //
1505
+ // ASYNC, like `fetch`: file IO must not block the frame, so each takes an onComplete/onReject
1506
+ // pair (the SDK turns them into a promise) and the host does the work off the JS thread. Hosts
1507
+ // must keep the ops ORDERED — two appends to one file settle in call order — so a queue, not a
1508
+ // thread per call. Exactly one handle is ever settled.
1509
+ //
1510
+ // PATH POLICY, identical for all five and owned by the host: an ABSOLUTE path is used verbatim;
1511
+ // a RELATIVE one is resolved against the app's OWN folder — never the process cwd — which for a
1512
+ // packaged desktop app is the exe's directory, one of the directories `fetchLocal` searches by
1513
+ // name. So `writeLocal("save.json", …)` and `fetchLocal("save.json")` are a pair.
1514
+ /** Write a file, creating the parent folders. `append` adds to the end instead of replacing.
1515
+ * `data` is text (written as UTF-8), raw bytes (Uint8Array / ArrayBuffer), or a host buffer
1516
+ * object carrying `_id` (an SDK `File` / `FetchResponse` — so a picked or generated file is
1517
+ * saved as it is); the bytes are only valid for the duration of the call, so a host that
1518
+ * defers the write copies them first. The core drops the path's cached `fetchLocal` id, so
1519
+ * the next read sees the new bytes. */
1520
+ writeLocal?(path: string, data: string | Uint8Array | ArrayBuffer | { _id: number }, append: boolean, onComplete: () => void, onReject: (err: Error) => void): void
1521
+ /** The whole file as bytes, or null when there is no such file. Unlike `fetchLocal` this
1522
+ * re-reads every call and holds nothing — no buffer id to dispose. Only a real failure
1523
+ * (an unreadable file) rejects; "not there" is a null result. */
1524
+ readLocalBytes?(path: string, onComplete: (bytes: Uint8Array | null) => void, onReject: (err: Error) => void): void
1525
+ /** The whole file decoded as UTF-8 text, or null when there is no such file. */
1526
+ readLocalText?(path: string, onComplete: (text: string | null) => void, onReject: (err: Error) => void): void
1527
+ /** Delete a file, or an EMPTY directory; `recursive` also deletes a directory's contents.
1528
+ * Deleting something that is already gone SUCCEEDS — the post-condition is what matters. */
1529
+ deleteLocal?(path: string, recursive: boolean, onComplete: () => void, onReject: (err: Error) => void): void
1530
+ /** Create a directory and any missing parents. An existing directory is success. */
1531
+ mkdirLocal?(path: string, onComplete: () => void, onReject: (err: Error) => void): void
1532
+ /** List a directory (added 2026-09-11, same optional set). Settles with an array of entries —
1533
+ * `name` = basename, `path` = relative to the listed directory with `/` separators (equals
1534
+ * `name` unless `recursive`), `kind`, `size` in bytes (0 for a directory), `modified` in ms
1535
+ * since the epoch — SORTED by `path`, so two runs and two hosts agree. Symlinks are followed
1536
+ * for `kind`; anything that is neither file nor directory is skipped. A MISSING directory is a
1537
+ * null result (like the readers); a path that is a file, or an unreadable directory, rejects. */
1538
+ listLocal?(path: string, recursive: boolean, onComplete: (entries: { name: string; path: string; kind: "file" | "dir"; size: number; modified: number }[] | null) => void, onReject: (err: Error) => void): void
1539
+ /** One entry's metadata (`name`, `kind`, `size`, `modified` — see `listLocal`) for a file or a
1540
+ * directory, or null when there is nothing at `path`. */
1541
+ statLocal?(path: string, onComplete: (entry: { name: string; kind: "file" | "dir"; size: number; modified: number } | null) => void, onReject: (err: Error) => void): void
1542
+ openFilePicker(onComplete: (res: any) => void, onReject: () => void, multiple: boolean, accept?: string): void
1543
+ // Present the OS media share sheet for a host buffer (`systemId` — a File/response id), with an
1544
+ // optional caption. iOS: the share sheet also offers "Save to Files"/Photos. Web: saves (downloads)
1545
+ // the file. Backs the SDK `share(media, text?)`.
1546
+ shareMedia(systemId: number, text?: string): void
1547
+
1548
+ /** Prompt for camera permission (resolve = granted, reject = denied). Shared by everything that
1549
+ * needs the camera — ARScene.prepare, QRScanner, CameraView — hence a platform utility rather
1550
+ * than a 3D-engine call (it moved off `_creator` 2026-07-15). Optional: hosts whose view
1551
+ * factory prompts on its own (web getUserMedia) don't implement it and the SDK resolves
1552
+ * immediately, so callers go through `_requestCameraPermission()`. */
1553
+ requestCamera?(onComplete: () => void, onReject: () => void): void
1554
+
1555
+ // (openScanner/closeScanner removed 2026-07-15 — QRScanner rides the registered "qrScanner"
1556
+ // NativeView; hosts keep the old handlers only for previously compiled bundles.)
1557
+ // ---- Input (docs/input-plan.md — keyboard / mouse / gamepad behind the SDK `Input` global) ----
1558
+ // Design: a BUTTON is anything that goes down and up — keyboard keys, mouse buttons, gamepad
1559
+ // buttons — and they share ONE code vocabulary (KeyboardEvent.code + "MouseLeft|Right|Middle|
1560
+ // Back|Forward" + the W3C standard-gamepad "GamepadSouth|East|West|North|L1|R1|L2|R2|Select|
1561
+ // Start|L3|R3|Up|Down|Left|Right"), one held-poll and one event pair. Continuous signals (mouse
1562
+ // motion, sticks, triggers) are numbered channels behind one poll. Bindings/axes/edge detection
1563
+ // are SDK-side; hosts only report physical state.
1564
+ /** Is this button held right now? `gamepad` (0..3) scopes a Gamepad* code to one pad; omitted =
1565
+ * any connected pad. Keyboard/mouse codes ignore it. */
1566
+ inputKey: (code: string, gamepad?: number) => boolean
1567
+ /** Continuous input poll by channel id (SDK `InputChannel`): 0 MouseX, 1 MouseY (logical px,
1568
+ * last known cursor position inside the viewport), 2 MouseDX, 3 MouseDY (motion accumulated
1569
+ * during the PREVIOUS frame — unaccelerated/raw where the OS offers it, counts while locked),
1570
+ * 4 WheelX, 5 WheelY (wheel notches, previous frame), 6 PointerLocked (0/1), 7 GamepadCount,
1571
+ * 8 PointerOnUI (0/1: the primary pointer went DOWN on something the UI claimed — an
1572
+ * interactive node, a scrollable, an editable, a modal backdrop — and is still held; stays 1
1573
+ * wherever the cursor goes until the release, so a drag that started on a HUD control never
1574
+ * becomes camera look; always 0 while pointer-locked and for touch-only hosts without a UI claim);
1575
+ * gamepad p at 16 + 16·p: +0 Connected, +1 LeftX, +2 LeftY, +3 RightX, +4 RightY (−1..1,
1576
+ * +Y = down like the web), +5 LeftTrigger, +6 RightTrigger (0..1). Unknown channel → 0.
1577
+ * "Previous frame" = the host's own frame boundary (the event pump / rAF / fixed step), so
1578
+ * every read within one frame agrees. Optional: a host without it has no pointer/gamepad. */
1579
+ inputRead?(channel: number): number
1580
+ /** Request (true) / release (false) pointer lock: hide + confine the cursor, keep MouseDX/DY
1581
+ * flowing (FPS look). Returns whether the request was accepted now (web needs a user gesture);
1582
+ * the eventual state is channel 6. Hosts release on focus loss and re-acquire on focus. */
1583
+ inputSetPointerLock?(on: boolean): boolean
1584
+ /** Register THE input-event listener (single slot per JS world, like registerAppEvent — the SDK
1585
+ * fans out). kind: 0 keydown, 1 keyup, 2 gamepadconnected, 3 gamepaddisconnected. `code` is the
1586
+ * button code above ("" for connect events); `gamepad` the pad index for Gamepad* codes /
1587
+ * connect events, −1 otherwise; `repeat` 1 for an OS auto-repeat keydown. Delivered on the JS
1588
+ * thread no later than the end of the frame it arrived in — i.e. always before the NEXT
1589
+ * setLoop tick (creator-pkg drains its dispatch queue after the frame's timers, so a poll can
1590
+ * see the new state one tick before the event; web/headless deliver synchronously). */
1591
+ registerInputEvent?(callback: (kind: number, code: string, gamepad: number, repeat: number) => void): void
1592
+ // ---- World control (run/restart/quit — the platform's `location` analog) --------------------
1593
+ // Worlds carry two host-tracked bits: an IDENTITY (project uuid) and a TRUST flag. The host's
1594
+ // own boot world (the bundled launcher) is trusted; a world created by `run` is trusted only
1595
+ // when its declared uuid is on the host's trusted-launcher allowlist AND the caller was
1596
+ // trusted itself. Push registration attribution reads the identity (docs/push-plan.md).
1597
+ /** Swap in a new project world. `projectUuid` declares the new world's identity — honored
1598
+ * ONLY when the calling world is trusted (launchers); silently dropped otherwise. `url`
1599
+ * (any caller — the caller already controls the code it runs) is the new world's
1600
+ * launchUrl: a string sets it, null clears it, omitted leaves it unchanged. */
1601
+ run(systemId: number, onReject: () => void, projectUuid?: string, url?: string | null): void
1602
+ /** Re-run the CURRENT world's bundle in a fresh world — `location.reload()`. Identity and
1603
+ * trust are inherited (the host re-evaluates bytes it already holds, so nothing is
1604
+ * spoofable). `url`: string sets the new launchUrl, null clears it, omitted keeps the
1605
+ * current one (a plain reload). If the re-run throws, the current world stays live. */
1606
+ restart?(url?: string | null): void
1607
+ /** Leave the current app. A world launched by `run` returns to the caller's boot bundle
1608
+ * (launch URL cleared, boot identity/trust restored). The boot world itself has no caller —
1609
+ * the host's app-exit hook fires instead (Android backgrounds the task; hosts that can't
1610
+ * exit programmatically, iOS/desktop, register nothing and the call is a no-op). */
1611
+ quit?(): void
1612
+
1613
+ localStorageGetValue(key: string): string | null
1614
+ localStorageSetValue(key: string, value: string): void
1615
+ localStorageRemoveValue(key: string): void
1616
+
1617
+ websocketOpen(url: string, onWebsocketMessage: (channel: string, data: any) => void, headers?: any): number
1618
+ websocketSend(id: number, message: string | ArrayBuffer): void
1619
+ websocketClose(id: number): void
1620
+
1621
+ // ---- Service channel (headless registerService capabilities) --------------------------------
1622
+ // The UI-less sibling of the NativeView channel (docs/service-channel-plan.md): the host
1623
+ // registers named service factories (`registerService("geolocation", factory)`) and the SDK
1624
+ // talks to them over the same JSON call/event protocol as viewCall. All four are optional —
1625
+ // feature-detect with typeof; hosts that didn't opt in simply don't have them. Sessions must
1626
+ // open cheaply (no permission prompt, no I/O) — prompts belong in the first call that needs
1627
+ // them, so a rejected call is the failure surface, never a hung open.
1628
+ isServiceSupported?(name: string): boolean
1629
+ /** Open a session against the registered factory. `params` is a JSON string. Returns the
1630
+ * host-allocated session id, or -1 when no factory is registered under `name`. `onEvent`
1631
+ * delivers service events (payload as a JSON string, or omitted) until serviceClose. */
1632
+ serviceOpen?(name: string, params: string, onEvent: (event: string, dataJson?: string) => void): number
1633
+ /** Invoke a method on a live session — args/result JSON-encoded, exactly like viewCall. */
1634
+ serviceCall?(serviceId: number, method: string, args: string,
1635
+ onComplete: (result?: string) => void, onError: (err: string) => void): void
1636
+ serviceClose?(serviceId: number): void
1637
+
1638
+ // ---- App lifecycle & environment (docs/app-lifecycle-plan.md) --------------------------------
1639
+ // One push channel + synchronous pulls. All optional — typeof feature-detect; the SDK wrappers
1640
+ // fall back cleanly (state "active", launchUrl null, online true) on hosts that lack them.
1641
+ /** Register THE app-event listener (single slot per JS world — the SDK fans out; the host/core
1642
+ * scopes the stored callback to the world and frees it on teardown). Events: "pause" / "resume"
1643
+ * (app left / returned to the foreground), "url" (a link arrived while running; data = the URL),
1644
+ * "online" / "offline" (connectivity changed). "pause" is delivered BEFORE the host halts the
1645
+ * frame loop (the dispatch queue must still drain). */
1646
+ registerAppEvent?(callback: (event: string, data?: string) => void): void
1647
+ /** Current lifecycle state: "active" | "background". Surfaced as app.state. */
1648
+ appState?(): string
1649
+ /** The URL the app was (most recently) opened with, or null — the cold-start deep link. A warm
1650
+ * link updates this value first, then fires the "url" event. Surfaced as app.launchUrl. */
1651
+ getLaunchUrl?(): string | null
1652
+ /** Current connectivity (navigator.onLine semantics — best-effort: false only when the platform
1653
+ * is sure there is no network). Surfaced as device.online. */
1654
+ isOnline?(): boolean
1655
+ /** Open a URL in the system browser / external handler (fire-and-forget). Backs openURL(). */
1656
+ openUrl?(url: string): void
1657
+ /** Lock the interface orientation: "landscape" (either landscape direction — sensor
1658
+ * landscape, what a horizontal game wants), "portrait" (upright only) or "auto" (release —
1659
+ * the device's rotation rules apply again). Fire-and-forget; the rotation itself is
1660
+ * animated by the platform and lands as an ordinary resize. PER WORLD: the host resets the
1661
+ * lock to "auto" on every world swap BEFORE the new bundle evaluates, so a game may lock at
1662
+ * synchronous top level and the launcher always comes back unlocked. Optional — hosts
1663
+ * without a rotatable screen (desktop, headless, macOS) omit it. Backs app.setOrientation. */
1664
+ setOrientation?(mode: string): void
1665
+ /** Write text to the system clipboard (fire-and-forget). Backs clipboard.write. */
1666
+ clipboardWrite?(text: string): void
1667
+ /** Read text from the system clipboard — async because web needs a permission prompt; native
1668
+ * hosts may complete synchronously. Reject = no access / nothing readable. Backs clipboard.read. */
1669
+ clipboardRead?(onComplete: (text: string) => void, onReject: (err: string) => void): void
1670
+
1671
+ // ---- Keyboard inset (docs/input-upgrades-plan.md §7) ------------------------------------------
1672
+ /** On-screen keyboard height in logical px currently overlapping the app viewport, 0 when
1673
+ * hidden. The RAW overlap, independent of the focused input's keyboardShrink policy (with
1674
+ * shrink the layout already avoids it; the main consumer is overlay mode). Surfaced as
1675
+ * app.keyboardHeight. Optional — hosts without an on-screen keyboard omit it. */
1676
+ keyboardHeight?(): number
1677
+ /** Register THE keyboard listener (single slot per JS world, like registerAppEvent — the SDK
1678
+ * fans out; a separate channel because the string one can't carry two numbers). Fires on every
1679
+ * keyboard frame change with (height — as above, duration — the platform's animation duration
1680
+ * in ms, 0 where none). Surfaced as the app "keyboard" event. */
1681
+ registerKeyboardEvent?(callback: (height: number, duration: number) => void): void
1682
+
1683
+ // (camera* removed 2026-07-15 — CameraView rides the registered "camera" NativeView; hosts
1684
+ // keep the old handlers only for previously compiled bundles.)
1685
+
1686
+ createAudioPlayer(src: string, onFinished: () => void): number
1687
+ createVideoPlayer(src: string, onFinished: () => void): number
1688
+ updateMediaPlayerVolume(id: number, volume: number): void
1689
+ updateMediaPlayerLoop(id: number, loop: boolean): void
1690
+ updateMediaPlayerPlaying(id: number, play: boolean): void
1691
+ getMediaPlayerTime(id: number): number
1692
+ setMediaPlayerTime(id: number, time: number): void
1693
+ getMediaPlayerDuration(id: number): number
1694
+ removeMediaPlayer(id: number): void
1695
+
1696
+ /** The host bucket surfaced as `device.platform`. Apps BRANCH on it (mouse-look vs on-screen
1697
+ * sticks, hover affordances, key hints), so it names the INPUT MODEL, not the OS — one of
1698
+ * "web" | "ios" | "android" | "desktop". macOS reports "desktop", not "macos": a project
1699
+ * testing `device.platform === "desktop"` must get the same answer on every desktop host.
1700
+ * OS/version detail belongs in the host's own systemInfo callback, never here. Every host
1701
+ * must define it — where it was missing (Android before 2026-07-30, iOS before 2026-08-25)
1702
+ * the SDK read undefined and no platform branch in any app could ever match. */
1703
+ platform: string
1704
+ language: string
1705
+ /** Display device-pixel ratio (physical px per CSS/logical px): 1 on standard displays, 2–3 on
1706
+ * retina / iOS. Host-provided (web: window.devicePixelRatio; iOS: UIScreen.scale). Surfaced as
1707
+ * device.pixelRatio — for baking a Canvas at the right resolution without hardcoding it. */
1708
+ pixelRatio: number
1709
+ /** Opt into the precise-touch system (full-rate coalesced sampling) where the host supports it — iOS
1710
+ * coalesced touches. Surfaced as device.setPreciseTouch. Optional: hosts without a coalesced-input
1711
+ * concept (web, headless) simply don't implement it, so callers guard with `?.`. */
1712
+ setPreciseTouch?(enabled: boolean): void
1713
+ /** Fire a one-shot semantic haptic (device.vibrate). `style` is a HapticStyle string. Optional:
1714
+ * hosts without haptic hardware (web, headless, iPad / older iPhones) simply don't implement it,
1715
+ * so callers guard with `?.`. */
1716
+ vibrate?(style: string): void
1717
+
1718
+ // ---- device.motion (orientation sensor; pull-per-frame model) --------------------------------
1719
+ /** Does this device have the motion sensors (gyro)? Surfaced as device.motion.available. */
1720
+ motionAvailable?(): boolean
1721
+ /** Start fused device-motion updates at `interval` seconds; returns whether it started (false =
1722
+ * no sensor / permission denied). May return a Promise on hosts with an async permission prompt
1723
+ * (web). Surfaced as device.motion.start. */
1724
+ motionStart?(interval: number): boolean | Promise<boolean>
1725
+ /** Stop updates and release the sensor. Surfaced as device.motion.stop. */
1726
+ motionStop?(): void
1727
+ /** The freshest raw sample in the DEVICE frame: `[qx,qy,qz,qw, gx,gy,gz, interfaceOrientation]`
1728
+ * (orientation code: 0 portrait, 1 landscapeLeft, 2 landscapeRight, 3 upsideDown), or null until
1729
+ * the first sample / when not running. The SDK converts it to world/screen space. */
1730
+ getMotionSample?(): number[] | null
1731
+ }
1732
+
1733
+ // ---- 2D canvas (core; native-per-platform — see docs/canvas-contract.md) ---------------------
1734
+ // Immediate-mode 2D drawing that bakes to a texture, shared by 2D/3D/UI. The SDK Canvas records a
1735
+ // command buffer and flushes it here once per bake (opcodes documented in the contract).
1736
+ var _creatorCanvas: {
1737
+ // Rasterize `cmd` (cmdLen floats) into surface `surfaceId` (0 = create), sized w×h logical @
1738
+ // `scale` device px. Clears then replays the whole buffer. Returns the surface id.
1739
+ rasterize(surfaceId: number, w: number, h: number, scale: number,
1740
+ cmd: Float32Array, cmdLen: number, refs: string[]): number
1741
+ // [width, ascent, descent] in logical px for `text` in `font`, measured on this platform.
1742
+ measureText(text: string, font: string): Float32Array
1743
+ // Copy surface `surfaceId`'s pixels into a NEW standalone surface; returns the new id. Backs
1744
+ // Canvas.toBitmap() — an immutable snapshot the source canvas can no longer touch.
1745
+ snapshot(surfaceId: number): number
1746
+ // Decode an image source into a NEW standalone surface (device px) — backs Canvas.loadImage() for
1747
+ // Canvas.drawImage(). `bufferId < 0` → rasterize the SVG markup in `svg`; otherwise decode the
1748
+ // encoded image bytes in host buffer `bufferId` (an already-fetched FetchResponse). Async, because
1749
+ // turning SVG / encoded bytes into pixels needs a decode on web (createImageBitmap / <img>). Returns
1750
+ // (surfaceId, width, height) in device px via onComplete, or onReject on failure.
1751
+ loadImage(svg: string, bufferId: number,
1752
+ onComplete: (surfaceId: number, width: number, height: number) => void, onReject: () => void): void
1753
+ // Encode surface `surfaceId` to an image ('image/png' | 'image/jpeg'), register the bytes as a host
1754
+ // buffer, and return (bufferId, size) — the SDK wraps it in a File. Backs Canvas/Bitmap.toFile().
1755
+ toFile(surfaceId: number, type: string, onComplete: (bufferId: number, size: number) => void, onReject: () => void): void
1756
+ destroySurface(surfaceId: number): void
1757
+ }
1758
+ }
1759
+
1760
+ export {}