lecodes-sdk 2.2.4 → 2.2.6

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 (48) hide show
  1. package/dist/global.d.ts +2 -1
  2. package/dist/types/g2/Scene2D.d.ts +5 -4
  3. package/dist/types/gl/Scene.d.ts +5 -4
  4. package/dist/types/inject.d.ts +1 -1
  5. package/dist/types/ui/NativeView.d.ts +3 -3
  6. package/dist/types/ui/UI.d.ts +1 -1
  7. package/dist/types/ui/UILayer.d.ts +25 -18
  8. package/dist/types/ui/UINode.d.ts +13 -1
  9. package/dist/types/ui/UIScreen.d.ts +11 -1
  10. package/dist/types/ui/UIVideo.d.ts +3 -3
  11. package/dist/types/ui/UIWidget.d.ts +24 -15
  12. package/dist/types/ui/presentable.d.ts +7 -7
  13. package/dist/types/version.d.ts +1 -1
  14. package/dist/types.json +1 -1
  15. package/package.json +1 -1
  16. package/prompts/3d-scene-files.md +2 -2
  17. package/prompts/3d-scene.md +1 -1
  18. package/prompts/core.md +1 -1
  19. package/prompts/ui-design.md +6 -6
  20. package/src/animate/tween/Timeline.ts +175 -175
  21. package/src/chisel.ts +1 -1
  22. package/src/compile/assetMacro.ts +81 -13
  23. package/src/compile/resourceSlots.ts +45 -0
  24. package/src/g2/Scene2D.ts +9 -7
  25. package/src/gl/CameraPlace.ts +52 -52
  26. package/src/gl/Light.ts +257 -257
  27. package/src/gl/Mesh.ts +120 -120
  28. package/src/gl/Scene.ts +9 -7
  29. package/src/gl/animation/DynamicBone.ts +482 -482
  30. package/src/gl/scenarios.ts +291 -291
  31. package/src/inject.ts +1 -1
  32. package/src/ui/NativeView.ts +11 -9
  33. package/src/ui/UI.ts +1 -1
  34. package/src/ui/UILayer.ts +90 -42
  35. package/src/ui/UINode.ts +39 -12
  36. package/src/ui/UIPopover.ts +3 -3
  37. package/src/ui/UIScreen.ts +50 -2
  38. package/src/ui/UIVideo.ts +11 -9
  39. package/src/ui/UIWidget.ts +41 -18
  40. package/src/ui/presentable.ts +8 -8
  41. package/src/version.ts +1 -1
  42. package/tests/helpers/fakeTree.ts +6 -1
  43. package/prompts/dist/2d-game.md +0 -1567
  44. package/prompts/dist/3d-app.md +0 -1709
  45. package/prompts/dist/ar-app.md +0 -1585
  46. package/prompts/dist/concept.md +0 -49
  47. package/prompts/dist/design.md +0 -521
  48. package/prompts/dist/ui-app.md +0 -1175
package/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.15.0",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.2.4",
48
+ "version": "2.2.6",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
@@ -69,7 +69,7 @@ export class Spinner extends Aspect<'spinner'> {
69
69
  // a UIWidget that is the scene's CONTENT — it shows and hides together with it:
70
70
  const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
71
71
  .style({ top: 'max(safe-top, 16px)', left: 16 })
72
- scene.setContent([hud])
72
+ scene.widgets(hud)
73
73
 
74
74
  // ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
75
75
 
@@ -107,7 +107,7 @@ const { scene, nodes } = await mainScene.open()
107
107
 
108
108
  let taps = 0
109
109
  const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
110
- scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
110
+ scene.widgets(UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 }))
111
111
 
112
112
  nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
113
113
  </file>
@@ -117,6 +117,6 @@ const hud = UIWidget(
117
117
  scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
118
118
  ).style({ top: 'max(safe-top, 16px)', right: 16 })
119
119
 
120
- scene.setContent([hud])
120
+ scene.widgets(hud)
121
121
  scene.open()
122
122
  </file>
package/prompts/core.md CHANGED
@@ -112,7 +112,7 @@ Everything in this prompt is a global — no imports needed. A project's only im
112
112
 
113
113
  Imports of the project's own files are also managed automatically: if your <edit> or <replace> makes code reference another file's export, the import is added for you — never fall back to a whole <file> rewrite just to change import lines. When writing a complete <file>, include imports normally.
114
114
 
115
- Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable; a path that names no file fails the compile). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
115
+ Reference a project asset (image, font, video, .svg, .glb, …) by its path as a string LITERAL — `UIImage("images/hero.png")`, `"./x.png"` from the file, `"/x.png"` from the root — or with the inline `asset('./path')` macro; both are a compile-time equivalent of the import (a literal, never a variable). A path that names a file is that file anywhere — a table of rows `{ photo: "images/x.jpg" }` too; one that names no file fails the compile where the SDK takes a file (UIImage, bgImage, a tab icon, Model.load, Sound.load, a scene's model …) and stays a plain string elsewhere (a path in the app's own files, a storage key). A `.json` path stays a string. External `https://…` URLs are used directly as strings.
116
116
 
117
117
  ## Project context
118
118
 
@@ -51,8 +51,8 @@ UIText(text)
51
51
  UIImage(src) // src: string url | SvgSource | imported asset
52
52
  // extra styles: objectFit ("cover"|"contain"|"fill"), tintColor (SVG only), borderRadius (number px only on UIImage)
53
53
  // Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
54
- // Note: a bare relative path string won't resolve — plain strings are for https:// URLs; project files go
55
- // through import or asset('./photo.jpg')
54
+ // Note: a project file by its path as a string — UIImage("images/photo.jpg") — is the bundled asset (or
55
+ // asset('./images/photo.jpg')); a path that names no file fails the compile; https:// strings are remote URLs
56
56
 
57
57
  // UIButton — the only TAPPABLE container: a UIRow with children centered on both axes by default.
58
58
  // Anything clickable in the prototype (a card, a list row, an icon) — wrap it in a UIButton.
@@ -191,10 +191,10 @@ UIRow(isSelected ? check : UIColumn()) // WRONG
191
191
  // ✅ null is skipped in children — no phantom element
192
192
  UIRow(isSelected ? check : null)
193
193
 
194
- // ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
195
- UIImage("./photo.jpg") // WRONG (a plain string works only for remote http(s) URLs)
196
- // ✅ import the asset, or wrap its path in asset()
197
- UIImage(asset('./assets/avatars/alina.jpg'))
194
+ // ❌ pointing UIImage / bgImage at a project file that is not there — the compile fails: file not found
195
+ UIImage("assets/avatars/nobody.jpg") // WRONG (no such file in the project)
196
+ // ✅ the path of a file the project has (or the same path in asset()) — the bundled asset
197
+ UIImage("assets/avatars/alina.jpg")
198
198
 
199
199
  // ❌ setting a UIImage's content through bgImage
200
200
  // bgImage is a container background, not image content — pass the source to UIImage(...)
@@ -1,175 +1,175 @@
1
- // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
- // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
- // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
- // builder every target class delegates to (`tweenBag`).
5
-
6
- import { TweenAnimation, type Animation } from "./Animation"
7
- import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
- import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
-
10
- /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
- const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
-
13
- const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
- clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
-
16
- /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
- export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
- const meta = bag as TweenMeta
19
- const tracks: Track[] = []
20
- for (const [prop, value] of bagProps(bag)) {
21
- const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
- if (track) tracks.push(track)
23
- }
24
- return tracks
25
- }
26
-
27
- /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
- export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
- const meta = bag as TweenMeta
30
- const tracks = bagTracks(target, bag, 0, fromCurrent)
31
- let durationMs = 0
32
- for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
- const spec: TweenSpec = {
34
- clock: clockOf(meta.clock, target._tweenClock),
35
- durationMs, delayMs: 0,
36
- iterations: iterationsOf(meta.loop),
37
- pingPong: meta.loopMode !== "restart",
38
- rate: 1, tracks, calls: [],
39
- }
40
- return new TweenAnimation(spec).play()
41
- }
42
-
43
- export type TimelineOptions = {
44
- /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
- clock?: "ui" | "game",
46
- /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
- loop?: boolean | number,
48
- loopMode?: "restart" | "ping-pong",
49
- /** Wait before the first play, ms. */
50
- delay?: number,
51
- }
52
-
53
- /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
- export type TimelinePosition = number | string | [string, number]
55
-
56
- export type TimelineAddOptions = {
57
- at?: TimelinePosition,
58
- /** With several targets: each starts this many ms after the previous one. */
59
- stagger?: number,
60
- }
61
-
62
- /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
- * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
- * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
- * starts, the last one after it ends). */
66
- export class TimelineImpl extends TweenAnimation {
67
- private _labels = new Map<string, number>()
68
- private _end = 0
69
-
70
- constructor(options: TimelineOptions = {}) {
71
- super({
72
- clock: clockOf(options.clock, CLOCK_UI),
73
- durationMs: 0,
74
- delayMs: options.delay ?? 0,
75
- iterations: iterationsOf(options.loop),
76
- pingPong: options.loopMode !== "restart",
77
- rate: 1, tracks: [], calls: [],
78
- })
79
- }
80
-
81
- private _at(at: TimelinePosition | undefined): number {
82
- if (at === undefined) return this._end
83
- if (typeof at === "number") return at
84
- const name = Array.isArray(at) ? at[0] : at
85
- const offset = Array.isArray(at) ? at[1] : 0
86
- const base = this._labels.get(name)
87
- if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
- return base + offset
89
- }
90
-
91
- private _grow(endMs: number): void {
92
- if (endMs > this._end) this._end = endMs
93
- this._spec.durationMs = this._end
94
- }
95
-
96
- /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
- * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
- add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
- const at = this._at(options.at)
100
- const targets = Array.isArray(target) ? target : [target]
101
- const stagger = options.stagger ?? 0
102
- targets.forEach((t, i) => {
103
- const tracks = bagTracks(t, props, at + i * stagger, true)
104
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
- })
106
- return this
107
- }
108
-
109
- /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
- addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
- const at = this._at(options.at)
112
- const targets = Array.isArray(target) ? target : [target]
113
- const stagger = options.stagger ?? 0
114
- targets.forEach((t, i) => {
115
- const tracks = bagTracks(t, props, at + i * stagger, false)
116
- for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
- })
118
- return this
119
- }
120
-
121
- /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
- * every frame with the value. */
123
- animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
- const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
- this._values.set(slot, deliver)
126
- this._spec.tracks.push(track)
127
- this._grow(track.atMs + track.durMs)
128
- return this
129
- }
130
-
131
- /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
- * finish, never from a previous run. */
133
- call(at: TimelinePosition, fn: () => void): this {
134
- const t = this._at(at)
135
- this._spec.calls.push(t)
136
- this._calls.push(fn)
137
- this._grow(t)
138
- return this
139
- }
140
-
141
- /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
- label(name: string, at?: TimelinePosition): this {
143
- this._labels.set(name, this._at(at))
144
- return this
145
- }
146
-
147
- /** Named times, ms. */
148
- get labels(): Record<string, number> {
149
- const o: Record<string, number> = {}
150
- for (const [k, v] of this._labels) o[k] = v
151
- return o
152
- }
153
-
154
- /** Extend the timeline with stillness after its current end. */
155
- hold(ms: number): this {
156
- this._grow(this._end + Math.max(0, ms))
157
- return this
158
- }
159
-
160
- /** The current end, ms (tracks, calls and holds). */
161
- get end(): number { return this._end }
162
-
163
- /** `seek` also accepts a label. */
164
- override seek(at: number | string): this {
165
- return super.seek(typeof at === "string" ? this._at(at) : at)
166
- }
167
- }
168
-
169
- /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
- export type Timeline = TimelineImpl
171
-
172
- /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
- export function Timeline(options?: TimelineOptions): Timeline {
174
- return new TimelineImpl(options)
175
- }
1
+ // `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
2
+ // number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
3
+ // at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
4
+ // builder every target class delegates to (`tweenBag`).
5
+
6
+ import { TweenAnimation, type Animation } from "./Animation"
7
+ import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
8
+ import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
9
+
10
+ /** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
11
+ const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
12
+
13
+ const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
14
+ clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
15
+
16
+ /** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
17
+ export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
18
+ const meta = bag as TweenMeta
19
+ const tracks: Track[] = []
20
+ for (const [prop, value] of bagProps(bag)) {
21
+ const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
22
+ if (track) tracks.push(track)
23
+ }
24
+ return tracks
25
+ }
26
+
27
+ /** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
28
+ export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
29
+ const meta = bag as TweenMeta
30
+ const tracks = bagTracks(target, bag, 0, fromCurrent)
31
+ let durationMs = 0
32
+ for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
33
+ const spec: TweenSpec = {
34
+ clock: clockOf(meta.clock, target._tweenClock),
35
+ durationMs, delayMs: 0,
36
+ iterations: iterationsOf(meta.loop),
37
+ pingPong: meta.loopMode !== "restart",
38
+ rate: 1, tracks, calls: [],
39
+ }
40
+ return new TweenAnimation(spec).play()
41
+ }
42
+
43
+ export type TimelineOptions = {
44
+ /** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
45
+ clock?: "ui" | "game",
46
+ /** Repeat the whole timeline: `true` = forever, a number = cycles. */
47
+ loop?: boolean | number,
48
+ loopMode?: "restart" | "ping-pong",
49
+ /** Wait before the first play, ms. */
50
+ delay?: number,
51
+ }
52
+
53
+ /** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
54
+ export type TimelinePosition = number | string | [string, number]
55
+
56
+ export type TimelineAddOptions = {
57
+ at?: TimelinePosition,
58
+ /** With several targets: each starts this many ms after the previous one. */
59
+ stagger?: number,
60
+ }
61
+
62
+ /** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
63
+ * one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
64
+ * at any time is fully defined by the tracks (the first keyframe of a track holds before it
65
+ * starts, the last one after it ends). */
66
+ export class TimelineImpl extends TweenAnimation {
67
+ private _labels = new Map<string, number>()
68
+ private _end = 0
69
+
70
+ constructor(options: TimelineOptions = {}) {
71
+ super({
72
+ clock: clockOf(options.clock, CLOCK_UI),
73
+ durationMs: 0,
74
+ delayMs: options.delay ?? 0,
75
+ iterations: iterationsOf(options.loop),
76
+ pingPong: options.loopMode !== "restart",
77
+ rate: 1, tracks: [], calls: [],
78
+ })
79
+ }
80
+
81
+ private _at(at: TimelinePosition | undefined): number {
82
+ if (at === undefined) return this._end
83
+ if (typeof at === "number") return at
84
+ const name = Array.isArray(at) ? at[0] : at
85
+ const offset = Array.isArray(at) ? at[1] : 0
86
+ const base = this._labels.get(name)
87
+ if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
88
+ return base + offset
89
+ }
90
+
91
+ private _grow(endMs: number): void {
92
+ if (endMs > this._end) this._end = endMs
93
+ this._spec.durationMs = this._end
94
+ }
95
+
96
+ /** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
97
+ * `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
98
+ add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
99
+ const at = this._at(options.at)
100
+ const targets = Array.isArray(target) ? target : [target]
101
+ const stagger = options.stagger ?? 0
102
+ targets.forEach((t, i) => {
103
+ const tracks = bagTracks(t, props, at + i * stagger, true)
104
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
105
+ })
106
+ return this
107
+ }
108
+
109
+ /** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
110
+ addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
111
+ const at = this._at(options.at)
112
+ const targets = Array.isArray(target) ? target : [target]
113
+ const stagger = options.stagger ?? 0
114
+ targets.forEach((t, i) => {
115
+ const tracks = bagTracks(t, props, at + i * stagger, false)
116
+ for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
117
+ })
118
+ return this
119
+ }
120
+
121
+ /** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
122
+ * every frame with the value. */
123
+ animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
124
+ const { track, deliver, slot } = makeValueTrack(options, this._at(at))
125
+ this._values.set(slot, deliver)
126
+ this._spec.tracks.push(track)
127
+ this._grow(track.atMs + track.durMs)
128
+ return this
129
+ }
130
+
131
+ /** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
132
+ * finish, never from a previous run. */
133
+ call(at: TimelinePosition, fn: () => void): this {
134
+ const t = this._at(at)
135
+ this._spec.calls.push(t)
136
+ this._calls.push(fn)
137
+ this._grow(t)
138
+ return this
139
+ }
140
+
141
+ /** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
142
+ label(name: string, at?: TimelinePosition): this {
143
+ this._labels.set(name, this._at(at))
144
+ return this
145
+ }
146
+
147
+ /** Named times, ms. */
148
+ get labels(): Record<string, number> {
149
+ const o: Record<string, number> = {}
150
+ for (const [k, v] of this._labels) o[k] = v
151
+ return o
152
+ }
153
+
154
+ /** Extend the timeline with stillness after its current end. */
155
+ hold(ms: number): this {
156
+ this._grow(this._end + Math.max(0, ms))
157
+ return this
158
+ }
159
+
160
+ /** The current end, ms (tracks, calls and holds). */
161
+ get end(): number { return this._end }
162
+
163
+ /** `seek` also accepts a label. */
164
+ override seek(at: number | string): this {
165
+ return super.seek(typeof at === "string" ? this._at(at) : at)
166
+ }
167
+ }
168
+
169
+ /** The timeline handle type (see {@link TimelineImpl} for the members). */
170
+ export type Timeline = TimelineImpl
171
+
172
+ /** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
173
+ export function Timeline(options?: TimelineOptions): Timeline {
174
+ return new TimelineImpl(options)
175
+ }
package/src/chisel.ts CHANGED
@@ -130,7 +130,7 @@ export const chiselVocab: ChiselVocab = {
130
130
  "onScroll", "onScrollRelease", "onOverscroll", "onRefresh", "onSubmit",
131
131
  "onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
132
132
  "onEndReached", "onStartReached",
133
- "append", "insert", "remove", "setContent", "keepAlive", "url",
133
+ "append", "insert", "remove", "setContent", "widgets", "keepAlive", "url",
134
134
  "detents", "dismissible", "transition", "setSourceRect",
135
135
  "push", "pop", "popToRoot", "replace", "select",
136
136
  // UITabs / UIVirtualizedList's hand feed / the native views (NativeView, CameraView, MapView, QRScanner)
@@ -2,6 +2,8 @@
2
2
  // compile-time `asset('./x.png')` macro is desugared into a real import so it goes through the same
3
3
  // loader/resolver as a hand-written import.
4
4
 
5
+ import { resourceSlots } from "./resourceSlots"
6
+
5
7
  // Matches `asset("./img.png")` / `asset('./img.png')` calls whose argument is a single string
6
8
  // literal (handling escaped chars inside the literal). The lookbehind excludes property access —
7
9
  // a METHOD named asset (`ui.asset("model")`, InspectorUI) is not the macro.
@@ -104,6 +106,63 @@ const normalizePath = (path: string): string => {
104
106
  return "/" + out.join("/")
105
107
  }
106
108
 
109
+ // ---- the resource slots (resourceSlots.ts): is a literal what the SDK takes a file in? ----------
110
+
111
+ const OPENERS = "([{"
112
+ const CLOSERS = ")]}"
113
+ // The callee before a `(`: `UIImage`, `Model.load`, `new AudioPlayer` (whitespace allowed around the dots).
114
+ const CALLEE = /(?:\b(new)\s+)?([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*$/
115
+ // An object literal's entry up to its value: `bgImage: `, `"icon": `.
116
+ const ENTRY_KEY = /^\s*(["']?)([A-Za-z_$][\w$]*)\1\s*:/
117
+ // The target of an assignment: `img.src = `.
118
+ const SET_TARGET = /\.\s*([A-Za-z_$][\w$]*)\s*$/
119
+
120
+ /**
121
+ * Whether the literal at `start` sits in a resource slot: an argument of a call or a constructor of
122
+ * the list at its position, the value of a listed key of an object literal, or the right side of a
123
+ * listed property set. Walks OUT from the literal over `mask` (maskNonCode's: strings and comments
124
+ * blanked, so their brackets do not count) and lets the innermost call or object entry decide.
125
+ * Arrays (`Sound.load([…])`, `normals: […]`), grouping parens, ternaries and an arrow's body (its
126
+ * statements, `return "…"`) are seen through; an assignment to anything else ends the walk — the
127
+ * literal is that variable's or that property's, not a slot's.
128
+ */
129
+ const inResourceSlot = (src: string, mask: string, start: number): boolean => {
130
+ let depth = 0 // brackets closed between the literal and the walk (theirs, not its own)
131
+ let commas = 0 // the literal's argument / entry position in the bracket it sits in
132
+ let entry = -1 // the closest of those commas: where the literal's entry starts
133
+ for (let i = start - 1; i >= 0; i--) {
134
+ const c = mask[i]
135
+ if (CLOSERS.includes(c)) { depth++; continue }
136
+ if (OPENERS.includes(c)) {
137
+ if (depth > 0) { depth--; continue }
138
+ if (c === "(") {
139
+ // A call — the innermost decides; a keyword (`if (`, `return (`) is no slot of the list.
140
+ const m = CALLEE.exec(mask.slice(Math.max(0, i - 128), i))
141
+ if (m) {
142
+ const slots = m[1] ? resourceSlots.constructors[m[2]] : resourceSlots.calls[m[2].replace(/\s+/g, "")]
143
+ return !!slots && slots.includes(commas)
144
+ }
145
+ } else if (c === "{") {
146
+ // An object literal's entry — its key decides; no key: a block (an arrow's body), seen through.
147
+ const key = ENTRY_KEY.exec(src.slice((entry >= 0 ? entry : i) + 1, start))
148
+ if (key) return resourceSlots.keys.includes(key[2])
149
+ }
150
+ commas = 0
151
+ entry = -1
152
+ continue
153
+ }
154
+ if (depth > 0) continue
155
+ if (c === ",") { commas++; if (entry < 0) entry = i; continue }
156
+ if (c === ";") { commas = 0; entry = -1; continue } // a statement of a block: the block decides
157
+ // an assignment (not `==`, `!=`, `<=`, `>=`, `=>`): `img.src = "…"` is a slot, any other target is not
158
+ if (c === "=" && mask[i + 1] !== "=" && mask[i + 1] !== ">" && !"=!<>".includes(mask[i - 1] ?? "")) {
159
+ const target = SET_TARGET.exec(mask.slice(Math.max(0, i - 128), i))
160
+ return !!target && resourceSlots.assignments.includes(target[1])
161
+ }
162
+ }
163
+ return false
164
+ }
165
+
107
166
  /**
108
167
  * Every string literal in `src` that is a PATH to a resource file of the project — `"images/hero.png"`,
109
168
  * `"./hero.png"`, `"/sounds/tap.mp3"` — as the asset references they stand for: a bare path is the
@@ -111,37 +170,46 @@ const normalizePath = (path: string): string => {
111
170
  * not the macro; the two now mean the same). Resolution: `./` and `../` from the file first, then
112
171
  * from the project root (a model writes `./images/x.png` in `views/cart.ts` meaning the root);
113
172
  * `/x` from the root; `a/b.png` from the root first, then the file. `exists(rootPath)` says whether
114
- * the project has that file. A literal with a resource extension that starts with `./`, `../`, `/`
115
- * or holds a `/` and names NO file comes back `missing` — the bundler reports it when the module
116
- * is reached, the way it reports a missing `asset()`. A bare name (`"hero.png"`) that names no
117
- * file is left as the string it is. Import specifiers, `asset()` arguments (`taken`: the ranges
118
- * already claimed) and `.json` paths are never touched.
173
+ * the project has that file.
174
+ *
175
+ * A literal that names a file is its asset wherever it stands — a table of rows (`photo:
176
+ * "images/espresso.jpg"`) the screens draw and a seed stores. One with a resource extension that
177
+ * starts with `./`, `../`, `/` or holds a `/` and names NO file comes back `missing` only in a
178
+ * resource slot (resourceSlots.ts: `UIImage("…")`, `.style({ bgImage: "…" })`, `Model.load("…")` …) —
179
+ * the bundler reports it when the module is reached, the way it reports a missing `asset()`;
180
+ * anywhere else it is the string it says (2026-10-12: `files.write("photos/a.jpg")`, a storage key,
181
+ * `endsWith("/thumb.jpg")`). A bare name (`"hero.png"`) that names no file is left as it is.
182
+ * Import specifiers, `asset()` arguments (`taken`: the ranges already claimed) and `.json` paths are
183
+ * never touched.
119
184
  */
120
185
  export const collectPathLiterals = (
121
186
  src: string, filePath: string, exists: (rootPath: string) => boolean, taken: AssetMacroRef[] = [],
122
187
  ): AssetMacroRef[] => {
123
188
  const refs: AssetMacroRef[] = []
124
189
  const dir = filePath.slice(0, filePath.lastIndexOf("/"))
125
- maskNonCode(src, (start, end) => {
190
+ const literals: [ number, number ][] = []
191
+ const mask = maskNonCode(src, (start, end) => { literals.push([ start, end ]) })
192
+ for (const [ start, end ] of literals) {
126
193
  const value = src.slice(start + 1, end - 1)
127
- if (value.length === 0 || value.length > 256) return
128
- if (!BARE_PATH_EXT.test(value) || CODE_EXT.test(value)) return
129
- if (/[\s:*?"'`<>|\\]/.test(value)) return // a url, an icon id, a sentence, a glob
130
- if (taken.some((t) => start >= t.index && start < t.index + t.length)) return
131
- if (IMPORT_BEFORE.test(src.slice(Math.max(0, start - 32), start))) return
194
+ if (value.length === 0 || value.length > 256) continue
195
+ if (!BARE_PATH_EXT.test(value) || CODE_EXT.test(value)) continue
196
+ if (/[\s:*?"'`<>|\\]/.test(value)) continue // a url, an icon id, a sentence, a glob
197
+ if (taken.some((t) => start >= t.index && start < t.index + t.length)) continue
198
+ if (IMPORT_BEFORE.test(src.slice(Math.max(0, start - 32), start))) continue
132
199
  const relative = value.startsWith("./") || value.startsWith("../")
133
200
  const rooted = value.startsWith("/")
134
201
  const candidates = rooted ? [ normalizePath(value) ]
135
202
  : relative ? [ normalizePath(`${dir}/${value}`), normalizePath(value) ]
136
203
  : [ normalizePath(value), normalizePath(`${dir}/${value}`) ]
137
204
  const found = candidates.find(exists)
138
- if (found === undefined && !relative && !rooted && !value.includes("/")) return
205
+ if (found === undefined && !relative && !rooted && !value.includes("/")) continue
206
+ if (found === undefined && !inResourceSlot(src, mask, start)) continue
139
207
  let line = 1
140
208
  for (let k = 0; k < start; k++) if (src[k] === "\n") line++
141
209
  refs.push(found !== undefined
142
210
  ? { path: found, index: start, length: end - start, line }
143
211
  : { path: value, index: start, length: end - start, line, missing: true })
144
- })
212
+ }
145
213
  return refs
146
214
  }
147
215
 
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The resource SLOTS: where the SDK takes a reference to a file of the project — an image, a sound,
3
+ * a model, a font — the places `asset('…')` or an `https://` url is passed to.
4
+ *
5
+ * compile/assetMacro.ts reads them for the bare path literals (2026-10-12): a literal that names a
6
+ * file of the project is its asset ANYWHERE (a table of rows names its pictures that way — the
7
+ * screens draw them, a seed stores them); a path-looking literal that names NO file fails the
8
+ * compile only in a slot. Anywhere else it is the string it says: a path in the app's own files, a
9
+ * storage key, a suffix test, a test's expected text.
10
+ *
11
+ * Plain data, the names user code writes. A rename in the SDK is an edit here
12
+ * (tests/compile/resourceSlots.test.ts fails until it is made).
13
+ */
14
+ export const resourceSlots = {
15
+ /** A call as written — a global, or `Global.method` — and the argument positions that take a file. */
16
+ calls: {
17
+ UIImage: [ 0 ],
18
+ registerFont: [ 1 ],
19
+ fetchLocal: [ 0 ],
20
+ "Texture2D.load": [ 0 ],
21
+ "Texture.load": [ 0 ],
22
+ "Model.load": [ 0 ],
23
+ "AnimationClip.load": [ 0 ],
24
+ "AnimationClip.loadAll": [ 0 ],
25
+ "Geometry.load": [ 0 ],
26
+ "Material.load": [ 0 ],
27
+ "Terrain.load": [ 0 ],
28
+ "Sound.load": [ 0 ],
29
+ } as Record<string, number[]>,
30
+ /** `new X(…)` and the argument positions that take a file. */
31
+ constructors: {
32
+ AudioPlayer: [ 0 ],
33
+ VideoPlayer: [ 0 ],
34
+ } as Record<string, number[]>,
35
+ /** `x.<name> = "…"`: `img.src`, `scene.postProcessing.lut`. */
36
+ assignments: [ "src", "lut" ],
37
+ /** The keys of an object literal whose value is a file: `.style({ bgImage })`, `UITabs({ id: { icon } })`,
38
+ * `defineSpriteSheet({ image })`, `defineScene({ nodes: { x: { model } }, env: { ibl, skybox: { texture },
39
+ * lightmap: { data, light, … } } })`, `Terrain.create({ layers: [{ albedo }], normals })`, a material's
40
+ * `map` / `shader`, `Level.place({ build })`. The value may sit in an array, a ternary or an arrow's body. */
41
+ keys: [
42
+ "bgImage", "backgroundImage", "icon", "image", "texture", "model", "ibl", "lut", "albedo", "normals",
43
+ "map", "shader", "build", "data", "light", "aux", "direct", "indirect", "probes", "volume",
44
+ ],
45
+ }