lecodes-sdk 2.2.10 → 2.2.13

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 (84) hide show
  1. package/dist/global.d.ts +2 -0
  2. package/dist/host.d.ts +8 -2
  3. package/dist/types/canvas/Canvas.d.ts +6 -6
  4. package/dist/types/core/Aspect.d.ts +5 -0
  5. package/dist/types/g2/Physics2D.d.ts +3 -2
  6. package/dist/types/g2/Scene2D.d.ts +6 -0
  7. package/dist/types/gl/Scene.d.ts +6 -0
  8. package/dist/types/gl/Texture.d.ts +2 -7
  9. package/dist/types/gl/animation/DynamicBone.d.ts +3 -0
  10. package/dist/types/gl/animation/Locomotion.d.ts +3 -0
  11. package/dist/types/gl/physics/Physics.d.ts +2 -1
  12. package/dist/types/gl/physics/Ragdoll.d.ts +4 -0
  13. package/dist/types/gl/vehicle/Vehicle.d.ts +4 -0
  14. package/dist/types/inject.d.ts +1 -1
  15. package/dist/types/runtime/app.d.ts +4 -2
  16. package/dist/types/runtime/fetch.d.ts +3 -0
  17. package/dist/types/runtime/media.d.ts +3 -2
  18. package/dist/types/runtime/storage.d.ts +6 -1
  19. package/dist/types/server/db/marci/query.d.ts +8 -2
  20. package/dist/types/ui/UIButton.d.ts +2 -2
  21. package/dist/types/ui/UIContainer.d.ts +4 -2
  22. package/dist/types/ui/UINode.d.ts +1 -1
  23. package/dist/types/ui/UIScreen.d.ts +2 -2
  24. package/dist/types/ui/UIVideo.d.ts +3 -2
  25. package/dist/types/ui/router.d.ts +1 -1
  26. package/dist/types/version.d.ts +1 -1
  27. package/dist/types.json +1 -1
  28. package/package.json +2 -5
  29. package/src/bridges/media.d.ts +3 -1
  30. package/src/canvas/Canvas.ts +6 -6
  31. package/src/compile/assetIconMacro.ts +2 -1
  32. package/src/compile/assetMacro.ts +2 -1
  33. package/src/compile/compileProject.ts +28 -4
  34. package/src/compile/header.ts +17 -0
  35. package/src/compile/index.ts +2 -0
  36. package/src/core/Aspect.ts +2 -1
  37. package/src/g2/Physics2D.ts +6 -3
  38. package/src/g2/Scene2D.ts +9 -1
  39. package/src/gl/Scene.ts +9 -1
  40. package/src/gl/Texture.ts +2 -1
  41. package/src/gl/animation/DynamicBone.ts +6 -2
  42. package/src/gl/animation/Locomotion.ts +11 -1
  43. package/src/gl/physics/Physics.ts +3 -2
  44. package/src/gl/physics/Ragdoll.ts +8 -3
  45. package/src/gl/vehicle/Vehicle.ts +7 -2
  46. package/src/host.d.ts +8 -2
  47. package/src/inject.ts +1 -1
  48. package/src/runtime/app.ts +4 -2
  49. package/src/runtime/fetch.ts +6 -0
  50. package/src/runtime/media.ts +11 -4
  51. package/src/runtime/storage.ts +14 -4
  52. package/src/server/db/marci/query.ts +8 -2
  53. package/src/ui/UIButton.ts +2 -2
  54. package/src/ui/UIContainer.ts +4 -2
  55. package/src/ui/UINode.ts +1 -1
  56. package/src/ui/UIScreen.ts +2 -2
  57. package/src/ui/UIVideo.ts +3 -2
  58. package/src/ui/router.ts +4 -3
  59. package/src/version.ts +1 -1
  60. package/prompts/2d.md +0 -391
  61. package/prompts/3d-scene-files.md +0 -113
  62. package/prompts/3d-scene.md +0 -122
  63. package/prompts/3d.md +0 -299
  64. package/prompts/README.md +0 -142
  65. package/prompts/ar.md +0 -142
  66. package/prompts/canvas.md +0 -28
  67. package/prompts/compose.ts +0 -46
  68. package/prompts/concept.md +0 -49
  69. package/prompts/core-design.md +0 -98
  70. package/prompts/core.md +0 -294
  71. package/prompts/design.md +0 -218
  72. package/prompts/dist/2d-game.md +0 -1567
  73. package/prompts/dist/3d-app.md +0 -1712
  74. package/prompts/dist/ar-app.md +0 -1585
  75. package/prompts/dist/concept.md +0 -49
  76. package/prompts/dist/design.md +0 -521
  77. package/prompts/dist/ui-app.md +0 -1175
  78. package/prompts/index.md +0 -39
  79. package/prompts/intake-prompt.md +0 -33
  80. package/prompts/namer-prompt.md +0 -12
  81. package/prompts/router-prompt.md +0 -50
  82. package/prompts/select.ts +0 -126
  83. package/prompts/ui-design.md +0 -203
  84. package/prompts/ui.md +0 -813
package/package.json CHANGED
@@ -12,7 +12,6 @@
12
12
  "./inject": "./src/inject.ts",
13
13
  "./plugin": "./src/plugin.ts",
14
14
  "./server": "./src/server/host.ts",
15
- "./prompts/select": "./prompts/select.ts",
16
15
  "./global.d.ts": "./dist/global.d.ts",
17
16
  "./host.d.ts": "./dist/host.d.ts",
18
17
  "./editor.d.ts": "./dist/editor.d.ts",
@@ -30,12 +29,11 @@
30
29
  "test": "bun test --randomize",
31
30
  "typecheck": "tsc --noEmit",
32
31
  "docs:check": "node docs/check.mjs",
33
- "prompts:build": "bun prompts/compose.ts",
34
32
  "sync:marcidb": "bun scripts/sync-marcidb.ts",
35
33
  "prepublishOnly": "bun run build"
36
34
  },
37
35
  "dependencies": {
38
- "@letary/chisel": "^0.14.0"
36
+ "@letary/chisel": "^0.15.0"
39
37
  },
40
38
  "devDependencies": {
41
39
  "@types/bun": "^1.3.14",
@@ -45,11 +43,10 @@
45
43
  "marcidb-embedded": "^0.15.0",
46
44
  "sharp": "^0.35.2"
47
45
  },
48
- "version": "2.2.10",
46
+ "version": "2.2.13",
49
47
  "files": [
50
48
  "src",
51
49
  "dist",
52
- "prompts",
53
50
  "tests/helpers",
54
51
  "README.md"
55
52
  ],
@@ -18,7 +18,9 @@ import type { f32, f64, i32 } from "./types"
18
18
  declare global {
19
19
  /** @host media @optional */
20
20
  var _creatorMedia: {
21
- /** A player for `src` as an owned Handle (null = the host failed to create one). @custom */
21
+ /** A player for `src` as an owned Handle (null = the host failed to create one). `src` is a url
22
+ * or `"id:<bufferId>"` (a host buffer: a local asset, a File, a FetchResponse), which the host
23
+ * resolves. @custom */
22
24
  createAudio(src: string): Handle | null
23
25
  /** @custom */
24
26
  createVideo(src: string): Handle | null
@@ -115,14 +115,14 @@ export class Canvas extends Recorder {
115
115
  }
116
116
 
117
117
  /** Decode an image source into a standalone `Bitmap` you can blit with `drawImage()` — either an
118
- * `SvgSource(...)` (drawn by the core, painted by the host) or a `FetchResponse` holding already-fetched
119
- * image bytes (decoded; an SVG file is detected). This is async because the web host decodes with the
120
- * browser; `await` it ONCE, then `drawImage(bitmap, …)` is synchronous and the Bitmap is reusable. The
121
- * returned Bitmap owns a native surface at the image's natural pixel size (`pixelRatio` 1), freed
122
- * once nothing references the Bitmap (`destroy()` frees it early). Typical use:
118
+ * `SvgSource(...)` (drawn by the core, painted by the host) or a `FetchResponse` / `File` holding
119
+ * already-fetched image bytes (decoded; an SVG file is detected). This is async because the web host
120
+ * decodes with the browser; `await` it ONCE, then `drawImage(bitmap, …)` is synchronous and the Bitmap
121
+ * is reusable. The returned Bitmap owns a native surface at the image's natural pixel size
122
+ * (`pixelRatio` 1), freed once nothing references the Bitmap (`destroy()` frees it early). Typical use:
123
123
  * const logo = await canvas.loadImage(SvgSource(svg)) // or: await fetch(url) → the response
124
124
  * canvas.drawImage(logo, 0, 0) // sync, blit as often as you like */
125
- loadImage(source: SvgSourceValue | FetchResponse): Promise<Bitmap> {
125
+ loadImage(source: SvgSourceValue | FetchResponse | File): Promise<Bitmap> {
126
126
  // Discriminate without touching FetchResponse internals: only an SvgSource carries `.svg`.
127
127
  const isSvg = typeof (source as SvgSourceValue).svg === "string"
128
128
  const svg = isSvg ? (source as SvgSourceValue).svg : ""
@@ -244,7 +244,8 @@ const iconExpr = (svg: string, call: IconCall): string => {
244
244
  return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src
245
245
  }
246
246
 
247
- const escapeTemplate = (s: string) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
247
+ /** Text into a template literal: backslash, backtick and `${` escaped. */
248
+ export const escapeTemplate = (s: string) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${")
248
249
 
249
250
  // ---- the macro pass -----------------------------------------------------------------------------
250
251
 
@@ -92,7 +92,8 @@ export type AssetMacroRef = { path: string, index: number, length: number, line:
92
92
  * image, a sound, a video, a model, a font, raw bytes) plus `.svg` (an SvgSource, what UIImage
93
93
  * takes). Not `.json`, whose asset is its parsed data — a `.json` path in a string stays a string. */
94
94
  const BARE_PATH_EXT = /\.(png|jpe?g|webp|gif|avif|bmp|svg|mp3|wav|ogg|m4a|aac|flac|mp4|webm|mov|m4v|glb|gltf|ktx2|hdr|exr|ttf|otf|woff2?|bin|bake|filamat)$/i
95
- const CODE_EXT = /\.([tj]sx?|mjs|cjs|json)$/i
95
+ /** A module the bundler parses as code (and .json, which is data it understands): never a text asset. */
96
+ export const CODE_EXT = /\.([tj]sx?|mjs|cjs|json)$/i
96
97
  // What precedes an import specifier: the literal is a module path, never a resource reference.
97
98
  const IMPORT_BEFORE = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/
98
99
 
@@ -8,6 +8,8 @@ import { detectEntry } from "./detectEntry"
8
8
  import { buildHeader } from "./header"
9
9
  import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
10
10
  import { ClientPublishError, clientPublishes, stubServerEntries } from "./serverSplit"
11
+ import { CODE_EXT } from "./assetMacro"
12
+ import { escapeTemplate } from "./assetIconMacro"
11
13
 
12
14
  export type CompileFileType = "text" | "resource" | "shader"
13
15
 
@@ -145,12 +147,20 @@ export const entriesToFiles = (entries: CompileEntry[], opts: { publicUrl: strin
145
147
  files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
146
148
  }
147
149
  } else if (e.path.toLowerCase().endsWith(".svg")) {
148
- files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
150
+ // The file's text sits in a template literal: a backtick, `${` or a backslash in it (an
151
+ // editor's `<!-- exported by \`tool\` -->` comment) would otherwise end the literal and fail
152
+ // the compile with a chisel parse error that names nothing a person can act on.
153
+ files[e.path] = `export default SvgSource(\`${escapeTemplate(e.text ?? "")}\`)`
149
154
  } else if (e.path.toLowerCase().endsWith(".json")) {
150
155
  // A .json project file is DATA, not code: `import data from "./x.json"` — and `asset("./x.json")`,
151
156
  // which desugars to that import — yields the parsed value. The bundler would otherwise try to
152
157
  // parse the file as JS. JSON is an expression, so a default export is the whole translation.
153
158
  files[e.path] = `export default ${e.text?.trim() || "null"}`
159
+ } else if (!CODE_EXT.test(e.path)) {
160
+ // Any other text file (.txt, .csv, .md, .css, .html, .xml, .yml …) is its TEXT: `asset("./levels.csv")`
161
+ // yields the string. Handing the raw text to the bundler as a module parsed as JS failed at
162
+ // run time instead (`__asset_0 is not defined`), after a compile that said nothing.
163
+ files[e.path] = `export default ${JSON.stringify(e.text ?? "")}`
154
164
  } else {
155
165
  files[e.path] = e.text ?? ""
156
166
  }
@@ -158,13 +168,27 @@ export const entriesToFiles = (entries: CompileEntry[], opts: { publicUrl: strin
158
168
  return files
159
169
  }
160
170
 
161
- /** The ids app.json lists under `"plugins"` (id → version), in its order. */
171
+ /** app.json's `"plugins"` in either shape — `{ "camera": "*" }` (id → version spec) or
172
+ * `["camera"]` (ids, every version `*`) — as id → spec. Anything else is no plugin. The list
173
+ * form is what CLAUDE.md and most hands write first; reading it as "no plugins" dropped the
174
+ * plugin from a web build and a shell with one warning nobody tied to the shape. */
175
+ export const pluginSpecsOf = (plugins: unknown): Record<string, string> => {
176
+ if (Array.isArray(plugins)) return Object.fromEntries(plugins.filter((p): p is string => typeof p === "string").map((id) => [id, "*"]))
177
+ if (plugins && typeof plugins === "object") {
178
+ return Object.fromEntries(Object.entries(plugins as Record<string, unknown>).map(([id, v]) => [id, typeof v === "string" ? v : "*"]))
179
+ }
180
+ return {}
181
+ }
182
+
183
+ /** The ids of app.json's `"plugins"` (either shape), in its order. */
184
+ export const pluginIdsOf = (plugins: unknown): string[] => Object.keys(pluginSpecsOf(plugins))
185
+
186
+ /** The ids app.json lists under `"plugins"`, in its order. */
162
187
  export const pluginsOfAppJson = (entries: CompileEntry[]): string[] => {
163
188
  const app = entries.find(e => e.path.replace(/^[./\\]+/, "") === "app.json")
164
189
  if (!app?.text) return []
165
190
  try {
166
- const plugins = (JSON.parse(app.text) as { plugins?: unknown }).plugins
167
- return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? Object.keys(plugins) : []
191
+ return pluginIdsOf((JSON.parse(app.text) as { plugins?: unknown }).plugins)
168
192
  } catch {
169
193
  return [] // a broken app.json is reported by whoever reads it as a config
170
194
  }
@@ -28,6 +28,18 @@ export type HeaderOptions = {
28
28
  plugins?: string[]
29
29
  }
30
30
 
31
+ /** The optional engines of the game stack, by the bundled bridge calls that mean "in use": a body,
32
+ * a character, a vehicle or a ragdoll created, a ray cast, a sound loaded or played, the net
33
+ * bridge named, a navmesh call. Not the calls every 3D bundle carries (Node's body pose setters,
34
+ * Scene's audio listener). A `Physics.supported` probe pulls the Physics class whole, so it
35
+ * counts as use — the full variant is the safe side of that line. */
36
+ export const ENGINE_SIGNALS: readonly (readonly [name: "physics" | "net" | "nav" | "audio", re: RegExp])[] = [
37
+ ["physics", /_creator\.(?:physicsCreateBody|physicsConfigure|physicsRaycast|characterCreate|vehicleCreate|ragdollCreate)\(/],
38
+ ["net", /\b_creatorNet\b/], // the net core takes the bridge into a local once (core.ts bridge()): its calls never name it
39
+ ["nav", /_creatorNav\.\w+\(/],
40
+ ["audio", /_creatorAudio\.(?:loadClip|play)\(/], // a sound loaded or played; Scene's listener setters ride in every 3D bundle
41
+ ]
42
+
31
43
  export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}): string => {
32
44
  let header = `// sdk: ${SDK_VERSION}\n`
33
45
  header += `// name: ${name.slice(0, 20)}\n`
@@ -51,6 +63,11 @@ export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}
51
63
  // bundle, and nothing 2D works without a Scene2D anyway.
52
64
  if (code.includes("_creator2d.createScene")) header += `// 2d: enable\n`
53
65
 
66
+ // The game stack a shell's SDK variant must carry (the `3d` variant has no Jolt, net, nav or
67
+ // game audio; `2d` / `core` no net or nav): the CLI's resolveVariant reads these lines, so a 3D
68
+ // game with physics no longer gets a variant where Physics.configure throws on the device.
69
+ for (const [name, re] of ENGINE_SIGNALS) if (re.test(code)) header += `// ${name}: enable\n`
70
+
54
71
  if (usesAR(code, opts.plugins)) header += `// ar: enable\n`
55
72
 
56
73
  // Short, so with the engine lines: a host reads it before it registers anything.
@@ -68,6 +68,8 @@ export {
68
68
  compileProject,
69
69
  compileProjectWithMap,
70
70
  pluginsOfAppJson,
71
+ pluginSpecsOf,
72
+ pluginIdsOf,
71
73
  pluginsOfBundle,
72
74
  entriesToFiles,
73
75
  localAssetNames,
@@ -403,7 +403,8 @@ export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<
403
403
  }
404
404
 
405
405
  // ---- systems: aspects of a scene --------------------------------------------
406
- /** @internal What a scene needs to carry systems. Scene / Scene2D implement it over these helpers. */
406
+ /** What a scene needs to carry systems: Scene and Scene2D implement it over the helpers below. Public
407
+ * (never stripped from the public types: the `System` type names it). */
407
408
  export type SystemHost = { readonly _systems: Map<Function, Aspect<any, any, any>> }
408
409
 
409
410
  /** @internal `scene.system(Ctor, opts)`. */
@@ -231,9 +231,12 @@ export class Physics2D extends Aspect<"physics", Node2D> {
231
231
  }
232
232
  private static _gravity: [number, number] = [ 0, -980 ]
233
233
 
234
- /** Whether this build has physics support (CREATOR_2D_PHYSICS). NOTE: this reports the BUILD, not
235
- * the world — there is nothing else to guard, since the world creates itself. */
236
- static get supported(): boolean { return !!_creator2d.physicsHasSupport && _creator2d.physicsHasSupport() }
234
+ /** Whether this host has 2D physics (a 2D engine built with CREATOR_2D_PHYSICS). False, never a
235
+ * throw, where there is no 2D engine at all (Web Lite, a 3d / core engine variant). It reports
236
+ * the build, not the world — there is nothing else to guard, since the world creates itself. */
237
+ static get supported(): boolean {
238
+ return typeof _creator2d !== "undefined" && !!_creator2d && typeof _creator2d.physicsHasSupport === "function" && _creator2d.physicsHasSupport()
239
+ }
237
240
 
238
241
  /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
239
242
  * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
package/src/g2/Scene2D.ts CHANGED
@@ -62,6 +62,12 @@ export class Scene2D implements Presentable {
62
62
  private static _active: Scene2D | null = null
63
63
  static get active(): Scene2D | null { return Scene2D._active }
64
64
 
65
+ /** Whether this host has the 2D engine. False on Web Lite (UI and 3D only) and in a 3d / core
66
+ * engine variant of an app built with `lecodes app`; there `new Scene2D()` throws. */
67
+ static get supported(): boolean {
68
+ return typeof _creator2d !== "undefined" && !!_creator2d && typeof _creator2d.createScene === "function"
69
+ }
70
+
65
71
  /** @internal — scene-level pointer listeners (the catch-all; target = hit node or null). */
66
72
  readonly _clickListeners: Array<(ev: ClickEvent<Node2D | null>) => void> = []
67
73
  readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node2D | null>) => void> = []
@@ -70,6 +76,7 @@ export class Scene2D implements Presentable {
70
76
  private readonly _filter?: 'nearest' | 'linear'
71
77
 
72
78
  constructor(options: Scene2DOptions = {}) {
79
+ if (!Scene2D.supported) throw new Error("Scene2D: this host has no 2D engine (Web Lite runs UI and 3D apps; a 3d / core engine variant has no 2D) — check Scene2D.supported before creating one")
73
80
  installRenderSyncedFrames()
74
81
  this.id = _creator2d.createScene()
75
82
  this.camera = new Camera2D(this.id)
@@ -121,7 +128,8 @@ export class Scene2D implements Presentable {
121
128
  }
122
129
 
123
130
  // ---- systems: aspects of the scene (core/Aspect.ts `System<'x', Scene2D>`) ----
124
- /** @internal class → instance, the authoritative store (named accessors mirror this). */
131
+ /** class → instance, the authoritative store (named accessors mirror this). Kept in the public
132
+ * types: it is what makes a Scene2D a `SystemHost` there. */
125
133
  readonly _systems = new Map<Function, Aspect<any, any, any>>()
126
134
 
127
135
  /** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
package/src/gl/Scene.ts CHANGED
@@ -206,7 +206,14 @@ export class Scene implements Presentable {
206
206
  private static _active: Scene | null = null
207
207
  static get active(): Scene | null { return Scene._active }
208
208
 
209
+ /** Whether this host has the 3D engine. False where the bundle was marked `gl: disable` (no scene
210
+ * created anywhere in it) and in a 2d / core engine variant; there `new Scene()` throws. */
211
+ static get supported(): boolean {
212
+ return typeof _creator !== "undefined" && !!_creator && typeof _creator.createScene === "function"
213
+ }
214
+
209
215
  constructor(options: SceneOptions = {}) {
216
+ if (!Scene.supported) throw new Error("Scene: this host has no 3D engine (a 2d / core engine variant, or a bundle that creates no scene) — check Scene.supported before creating one")
210
217
  installRenderSyncedFrames()
211
218
  const parent = (options as any)._parent as Scene | undefined
212
219
  if (parent) {
@@ -366,7 +373,8 @@ export class Scene implements Presentable {
366
373
  }
367
374
 
368
375
  // ---- systems: aspects of the scene (core/Aspect.ts `System`) ----
369
- /** @internal class → instance, the authoritative store (named accessors mirror this). */
376
+ /** class → instance, the authoritative store (named accessors mirror this). Kept in the public
377
+ * types: it is what makes a Scene a `SystemHost` there. */
370
378
  readonly _systems = new Map<Function, Aspect<any, any, any>>()
371
379
 
372
380
  /** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
package/src/gl/Texture.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
2
2
 
3
3
  import { fetch, type FetchResponse, type File } from "../runtime/fetch"
4
+ import type { Canvas } from "../canvas/Canvas"
4
5
 
5
6
  /** `Texture.load` options. */
6
7
  export type TextureLoadOptions = {
@@ -57,7 +58,7 @@ export class Texture {
57
58
  // Wrap a Canvas's baked surface as a 3D texture (RGBA8, already rasterized → synchronous, no decode).
58
59
  // The id is cached on the canvas (opaque _tex3dId), so backing several materials shares one texture and
59
60
  // canvas.update() re-uploads it. Structural type avoids a Canvas import (keeps 3D free of 2D-canvas code).
60
- static fromCanvas(canvas: { _bakeSurface(): number, width: number, height: number, pixelRatio: number, _tex3dId: number }): Texture {
61
+ static fromCanvas(canvas: Canvas): Texture {
61
62
  if (!canvas._tex3dId) {
62
63
  const surfaceId = canvas._bakeSurface()
63
64
  canvas._tex3dId = _creator.createTextureFromCanvas(surfaceId)
@@ -54,8 +54,8 @@ const FLOOR_NONE = 0, FLOOR_PLANE = 1, FLOOR_PROBE = 2
54
54
 
55
55
  let warned = false
56
56
  const supported = (): boolean => {
57
- if (_creator.dynamicBoneCreate) return true
58
- if (!warned) { warned = true; console.warn("DynamicBone needs a newer host (no dynamicBoneCreate) — the animation shows as is") }
57
+ if (DynamicBone.supported) return true
58
+ if (!warned) { warned = true; console.warn("DynamicBone: this host has no dynamic bones (DynamicBone.supported is false) — the animation shows as is") }
59
59
  return false
60
60
  }
61
61
 
@@ -134,6 +134,10 @@ export class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", Node> {
134
134
  export class DynamicBone extends Aspect<"dynamicBone", Node> {
135
135
  static readonly aspect = "dynamicBone"
136
136
 
137
+ /** Whether this host simulates dynamic bones. False, never a throw, on Web Lite and in headless
138
+ * tests; there the aspect attaches, warns once, and `chain` stays empty. */
139
+ static get supported(): boolean { return typeof _creator !== "undefined" && !!_creator && typeof _creator.dynamicBoneCreate === "function" }
140
+
137
141
  /** Only these bones (by name) join the chain — the root always does, a bone left out takes its
138
142
  * subtree with it. Default: the root's whole subtree. */
139
143
  bones?: string[]
@@ -225,9 +225,15 @@ export type LocomotionEvents = {
225
225
  state: (state: LocomotionState) => void
226
226
  }
227
227
 
228
+ let locoWarned = false
229
+
228
230
  export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
229
231
  static readonly aspect = "loco"
230
232
 
233
+ /** Whether this host runs the locomotion selector. False, never a throw, on Web Lite and in
234
+ * headless tests; there the aspect attaches, warns once, and the body stays `'idle'`. */
235
+ static get supported(): boolean { return typeof _creator !== "undefined" && !!_creator && typeof _creator.locoCreate === "function" }
236
+
231
237
  /** The animator whose clips are shown. Default: the first one on this node or under it (a Model
232
238
  * child), which is the usual character — a capsule node with the model inside. */
233
239
  animator?: Animator
@@ -281,7 +287,11 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
281
287
  * walk, a slowed-down character). */
282
288
  rebuild(): void {
283
289
  const anim = this.anim
284
- if (!anim || !_creator.locoCreate) return
290
+ if (!Locomotion.supported) {
291
+ if (!locoWarned) { locoWarned = true; console.warn("Locomotion: this host has no locomotion engine (Locomotion.supported is false) — the character stays idle") }
292
+ return
293
+ }
294
+ if (!anim) return
285
295
  if (!this._loco) {
286
296
  const first = this.set.idle ?? (this.set.gaits ?? [])[0] // binding a clip is what creates the native animator
287
297
  if (first !== undefined) anim._slot(name(first))
@@ -209,9 +209,10 @@ export class Physics extends Aspect<"physics", Node> implements CompWriter {
209
209
  _creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
210
210
  }
211
211
 
212
- /** Whether this build has physics support (CREATOR_GL_PHYSICS). */
212
+ /** Whether this host has 3D physics (an engine built with CREATOR_GL_PHYSICS). False, never a
213
+ * throw, where there is no 3D engine at all. */
213
214
  static get supported(): boolean {
214
- return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
215
+ return typeof _creator !== "undefined" && !!_creator && typeof _creator.physicsHasSupport === "function" && _creator.physicsHasSupport()
215
216
  }
216
217
 
217
218
  /** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
@@ -45,7 +45,7 @@ import type { FieldMeta } from "../../core/fields"
45
45
  import { Mathf } from "../../math/Mathf"
46
46
  import { cx, cy, cz, Vec3, type Vec3Like } from "../../math/vec"
47
47
  import { Node } from "../Node"
48
- import { DEFAULT_FRICTION } from "./Physics"
48
+ import { DEFAULT_FRICTION, Physics } from "./Physics"
49
49
 
50
50
  export interface RagdollHinge {
51
51
  /** The hinge axis in the MODEL's space (its rest pose: +Z forward, +Y up, +X the model's left in
@@ -169,6 +169,11 @@ export const humanoidParts = (find: (role: string) => string | null): RagdollPar
169
169
  export class Ragdoll extends Aspect<"ragdoll", Node> {
170
170
  static readonly aspect = "ragdoll"
171
171
 
172
+ /** Whether this host can drop a body: 3D physics with ragdoll parts. False, never a throw,
173
+ * without physics or on an older host. A host that has it can still refuse the parts
174
+ * (`activate()` returns false). */
175
+ static get supported(): boolean { return Physics.supported && typeof _creator.ragdollCreate === "function" }
176
+
172
177
  /** The parts: `'humanoid'` (default) finds the standard bones by name; a list places bodies on any
173
178
  * bones — parents before children. */
174
179
  parts: RagdollPart[] | "humanoid" = "humanoid"
@@ -243,9 +248,9 @@ export class Ragdoll extends Aspect<"ragdoll", Node> {
243
248
  private _release: { left: number; blend: number } | null = null
244
249
 
245
250
  onAttach(): void {
246
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
251
+ if (!Physics.supported) return
247
252
  if (!_creator.ragdollCreate) {
248
- console.warn("Ragdoll needs a newer host (no ragdollCreate) — the body will not fall")
253
+ console.warn("Ragdoll: this host has no ragdoll parts (Ragdoll.supported is false) — the body will not fall")
249
254
  return
250
255
  }
251
256
  const find = (role: string): string | null => {
@@ -194,6 +194,11 @@ const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v >
194
194
  export class Vehicle extends Aspect<"vehicle", Node> {
195
195
  static readonly aspect = "vehicle"
196
196
 
197
+ /** Whether this host can drive a car: 3D physics with the vehicle model. False, never a throw,
198
+ * without physics or on an older host. A host that has it can still refuse a car's settings
199
+ * (`id` stays 0, with a warning). */
200
+ static get supported(): boolean { return Physics.supported && typeof _creator.vehicleCreate === "function" }
201
+
197
202
  /** Chassis mass in kg (default 1200). Wheels are massless in this model. */
198
203
  mass = 1200
199
204
  /** Centre of mass height in chassis space (metres, negative = lower). Left out, it drops to halfway
@@ -274,9 +279,9 @@ export class Vehicle extends Aspect<"vehicle", Node> {
274
279
  private _retunePending = false
275
280
 
276
281
  onAttach(): void {
277
- if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
282
+ if (!Physics.supported) return
278
283
  if (!_creator.vehicleCreate) {
279
- console.warn("Vehicle needs a newer host (no vehicleCreate) — the car will not move")
284
+ console.warn("Vehicle: this host has no vehicle model (Vehicle.supported is false) — the car will not move")
280
285
  return
281
286
  }
282
287
  const shape = this.node.get(Shape)
package/src/host.d.ts CHANGED
@@ -6,8 +6,11 @@
6
6
  import type { SvgSource } from "./runtime/misc"
7
7
 
8
8
  declare global {
9
- function setTimeout(handler: (...args: any[]) => void, timeout?: number): number
10
- function setInterval(handler: (...args: any[]) => void, timeout?: number): number
9
+ /** As in the browser, but the delay is REQUIRED (the native runtime throws without it) and no
10
+ * extra argument reaches the handler. Timers tick with the frame on every native host: one
11
+ * frame is the resolution. */
12
+ function setTimeout(handler: () => void, timeout: number): number
13
+ function setInterval(handler: () => void, timeout: number): number
11
14
  function clearTimeout(id: number): void
12
15
  function clearInterval(id: number): void
13
16
  /** Per-frame loop, driven by the host (viewer RAF / desktop runTick). `dt` is **seconds** since
@@ -38,6 +41,9 @@ declare global {
38
41
  /** Compile-time macro: `asset('./logo.svg')` yields the file as an SVG image source — for
39
42
  * `UIImage(...)` and `bgImage`. */
40
43
  function asset(path: `${string}.svg`): SvgSource
44
+ /** Compile-time macro: `asset('./levels.csv')` yields a text file's TEXT (.txt, .csv, .md, .css,
45
+ * .html, .xml, .yml — it ships inside the bundle). Parse it yourself. */
46
+ function asset(path: `${string}.${"txt" | "csv" | "md" | "css" | "html" | "xml" | "yml" | "yaml"}`): string
41
47
  /** Compile-time macro: `asset('./hero.png')` is desugared by the bundler into the module import
42
48
  * for that resource. The file must exist — a path that resolves to nothing fails the compile
43
49
  * (`asset not found: ./hero.png (main.ts:3)`); there is no runtime fallback. Calls inside
package/src/inject.ts CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  // ---- math ----
11
11
  export { Mathf } from "./math/Mathf"
12
- export { Vec2, Vec3 } from "./math/vec"
12
+ export { Vec2, Vec3, type Vec2Like, type Vec3Like } from "./math/vec"
13
13
  export { Quat } from "./math/quat"
14
14
  export { Mat4 } from "./math/mat4"
15
15
  export { Color } from "./core/color"
@@ -56,8 +56,10 @@ export const app = {
56
56
  sdkVersion: SDK_VERSION,
57
57
  /** The INSTALLED app: the store build this bundle runs in. After an over-the-air update the code
58
58
  * is newer than the binary, so a feature that needs native support added in a later store build
59
- * (a plugin, an SDK method) branches on `app.host.sdk` / `app.host.build` — and on `typeof` of
60
- * the method itself, which is the final word. Minor and patch of the SDK never gate anything. */
59
+ * (a plugin, an SDK method) branches on `app.host.sdk` / `app.host.build` — and on the feature's
60
+ * own check (`Push.isSupported`, `Physics.supported`), which is the final word. Not `typeof` of
61
+ * the global: every SDK wrapper is compiled into the bundle, so `typeof Push` is "object" on a
62
+ * host without the plugin. Minor and patch of the SDK never gate anything. */
61
63
  get host(): AppHost {
62
64
  return {
63
65
  version: _creatorApp.appVersion?.(),
@@ -21,6 +21,9 @@ export interface FetchResponse {
21
21
  readonly status: number
22
22
  json<T = any>(): T
23
23
  text(): string
24
+ /** The body's bytes, a copy — a binary format of your own (`asset('./levels/1.bin')`), parsed in
25
+ * code with a DataView. Synchronous, like `text()`. */
26
+ bytes(): Uint8Array
24
27
  /** @deprecated A no-op: the body is freed with the response's handle (owned handles). Apps written
25
28
  * against the pre-owned SDK still call it after `text()` / `json()`; remove the call. */
26
29
  dispose(): void
@@ -42,6 +45,9 @@ class FetchResponseImpl implements FetchResponse {
42
45
  text(): string {
43
46
  return _creatorFetch.toText(this._h.id)
44
47
  }
48
+ bytes(): Uint8Array {
49
+ return _creatorFetch.toBytes(this._h.id)
50
+ }
45
51
  /** @deprecated see the interface */
46
52
  dispose(): void {}
47
53
  }
@@ -7,10 +7,14 @@
7
7
  // then; a paused player you stop referencing is released at once. There is nothing to dispose. The
8
8
  // end-of-playback report comes by id through ONE emitter per world; a playing player is pinned so
9
9
  // the report finds it.
10
+ //
11
+ // `src` is a url, or a `File` / `FetchResponse`: those play from the host's buffer store
12
+ // (`"id:<bufferId>"`, the whole bytes in memory, no streaming).
10
13
 
11
14
  import { Emitter } from "../core/events"
12
15
  import { Pins } from "../core/pins"
13
16
  import { Texture } from "../gl/Texture"
17
+ import type { File, FetchResponse } from "./fetch"
14
18
 
15
19
  type MediaEvents = {
16
20
  /** Fired at each loop boundary while `loop` is true. */
@@ -30,6 +34,9 @@ const ensureEmitter = (): void => {
30
34
  emitterInstalled = true
31
35
  m.setEmitter((id) => { players.get(id)?._finished() })
32
36
  }
37
+ /** The wire spelling of a source: a url as is, a buffer (File / FetchResponse) as "id:<bufferId>". */
38
+ const wireSrc = (src?: string | File | FetchResponse): string =>
39
+ src == null ? "" : typeof src === "string" ? src : "id:" + (src as any)._h.id
33
40
 
34
41
  abstract class MediaPlayer extends Emitter<MediaEvents> {
35
42
  /** @internal The player's owned handle (`_h.id` = the host player id; -1 = no host player). */
@@ -92,18 +99,18 @@ abstract class MediaPlayer extends Emitter<MediaEvents> {
92
99
  }
93
100
 
94
101
  export class AudioPlayer extends MediaPlayer {
95
- constructor(src?: string) {
102
+ constructor(src?: string | File | FetchResponse) {
96
103
  super()
97
- this._src = src ?? ""
104
+ this._src = wireSrc(src)
98
105
  ensureEmitter()
99
106
  this._h = media()?.createAudio(this._src) ?? { id: -1 }
100
107
  }
101
108
  }
102
109
 
103
110
  export class VideoPlayer extends MediaPlayer {
104
- constructor(src?: string) {
111
+ constructor(src?: string | File | FetchResponse) {
105
112
  super()
106
- this._src = src ?? ""
113
+ this._src = wireSrc(src)
107
114
  ensureEmitter()
108
115
  this._h = media()?.createVideo(this._src) ?? { id: -1 }
109
116
  }
@@ -1,15 +1,25 @@
1
1
  // Key/value persistence backed by the host (`_creatorStorage`). Same shape as the web localStorage
2
- // subset projects use.
2
+ // subset projects use — including `localStorage['theme'] = 'dark'` / `localStorage.theme`, the
3
+ // browser habit: a Proxy routes any key that is not one of the three methods to the host, so the
4
+ // habit saves instead of setting a field on a plain object that nothing persists.
3
5
 
4
-
5
- export const localStorage = {
6
+ const methods = {
6
7
  getItem(key: string): string | null {
7
8
  return _creatorStorage.get(key)
8
9
  },
9
10
  setItem(key: string, value: string): void {
10
- _creatorStorage.set(key, value)
11
+ _creatorStorage.set(key, String(value))
11
12
  },
12
13
  removeItem(key: string): void {
13
14
  _creatorStorage.remove(key)
14
15
  },
15
16
  }
17
+
18
+ export type LocalStorage = typeof methods & { [key: string]: any }
19
+
20
+ export const localStorage: LocalStorage = new Proxy(methods as LocalStorage, {
21
+ get: (t, key) => typeof key === "string" && !(key in t) ? t.getItem(key) ?? undefined : (t as any)[key],
22
+ set: (t, key, value) => { if (typeof key === "string" && !(key in t)) t.setItem(key, value); return true },
23
+ deleteProperty: (t, key) => { if (typeof key === "string" && !(key in t)) t.removeItem(key); return true },
24
+ has: (t, key) => key in t || (typeof key === "string" && t.getItem(key) !== null),
25
+ })
@@ -173,7 +173,13 @@ export type AggregateResult<TModel, T> =
173
173
  // A lazily-executed operation. `await` runs it as a single request;
174
174
  // passing it into `db.$transaction([...])` bundles it into one atomic transaction
175
175
  declare const __op: unique symbol
176
- export type Op<T> = PromiseLike<T> & { readonly [__op]: T }
176
+ /** What a query or an op offers of a promise: `then`, `catch`, `finally` — the runtime object has
177
+ * all three (`op()` / the collection builder below), so `db.x.insert(…).catch(…)` type-checks. */
178
+ export type Settles<T> = PromiseLike<T> & {
179
+ catch<R = never>(onRejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T | R>
180
+ finally(onFinally?: (() => void) | null): Promise<T>
181
+ }
182
+ export type Op<T> = Settles<T> & { readonly [__op]: T }
177
183
 
178
184
  // A transport-neutral operation descriptor and the pluggable transport that runs it. The HTTP transport
179
185
  // is selected by passing a URL string; marcidb-embedded provides an in-process FFI transport.
@@ -229,7 +235,7 @@ export type IdArg<I> = I | BareId<I>
229
235
  * it to `$transaction` batches it; placing it in another query's select makes it a sub-select. `Sel` is the
230
236
  * projection: id + scalars until `select(...)` sets a shape.
231
237
  */
232
- export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends PromiseLike<Rows<T, Sel>[]> {
238
+ export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Settles<Rows<T, Sel>[]> {
233
239
  readonly [__op]: Rows<T, Sel>[]
234
240
  readonly [__sub]: T["name"]
235
241
  readonly [__sel]: Effective<T, Sel>
@@ -1,9 +1,9 @@
1
1
  import { ClickEvent, TouchStartEvent, LongPressEvent, MouseEnterEvent } from "../runtime/touch"
2
2
  import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType, MouseEnterEvent as MouseEnterEventType } from "../runtime/touch"
3
- import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
3
+ import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type TextStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
4
4
  import { TREE_FLAG_CLICK, TREE_FLAG_HOVER, TREE_FLAG_INTERACTIVE, TREE_FLAG_LONG_PRESS, tree } from "./tree"
5
5
 
6
- export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
6
+ export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & TextStyle & { rippleColor?: Color | "default" }
7
7
 
8
8
  /** A tappable container — row + centered children by default. Press feedback via the `$pressed`
9
9
  * class block or `rippleColor`. */
@@ -1,6 +1,8 @@
1
- import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
1
+ import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type TextStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
2
2
 
3
- export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
3
+ /** A container's style: its box and layout, plus the text props (`color`, `fontSize`, `textWrap` …)
4
+ * the texts inside inherit from it. */
5
+ export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle & TextStyle
4
6
 
5
7
  /** A vertical flex container — children stack top to bottom. */
6
8
  export interface UIColumn extends UIContainerBase<UIContainerStyle> {