lecodes-sdk 2.0.8 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +3 -0
- package/dist/host.d.ts +6 -2
- package/dist/types/g2/Scene2D.d.ts +4 -0
- package/dist/types/gl/Light.d.ts +31 -7
- package/dist/types/gl/Scene.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -1
- package/dist/types/runtime/misc.d.ts +4 -0
- package/dist/types/ui/NativeView.d.ts +3 -0
- package/dist/types/ui/UI.d.ts +1 -0
- package/dist/types/ui/UILayer.d.ts +25 -0
- package/dist/types/ui/UIModal.d.ts +20 -16
- package/dist/types/ui/UIPopover.d.ts +5 -3
- package/dist/types/ui/UIVideo.d.ts +3 -0
- package/dist/types/ui/UIWidget.d.ts +25 -24
- package/dist/types/ui/presentable.d.ts +10 -0
- package/dist/types/ui/transitions.d.ts +2 -1
- package/dist/types/ui/tree.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/3d-scene-files.md +3 -3
- package/prompts/3d-scene.md +1 -1
- package/prompts/core.md +1 -1
- package/prompts/dist/3d-app.md +4 -4
- package/prompts/ui.md +3 -7
- package/src/bridges/gl.d.ts +5 -0
- package/src/bridges/tree.d.ts +24 -12
- package/src/chisel.ts +1 -1
- package/src/compile/assetMacro.ts +69 -4
- package/src/compile/bundler.ts +10 -3
- package/src/g2/Scene2D.ts +10 -0
- package/src/gl/Light.ts +257 -194
- package/src/gl/Material.ts +3 -0
- package/src/gl/Scene.ts +11 -1
- package/src/host.d.ts +6 -2
- package/src/inject.ts +1 -0
- package/src/runtime/misc.ts +4 -0
- package/src/ui/NativeView.ts +11 -0
- package/src/ui/UI.ts +1 -0
- package/src/ui/UIBottomSheet.ts +8 -7
- package/src/ui/UILayer.ts +88 -0
- package/src/ui/UIModal.ts +59 -43
- package/src/ui/UINode.ts +9 -1
- package/src/ui/UIPopover.ts +23 -9
- package/src/ui/UIVideo.ts +11 -0
- package/src/ui/UIWidget.ts +70 -44
- package/src/ui/presentable.ts +11 -1
- package/src/ui/transitions.ts +36 -7
- package/src/ui/tree.ts +4 -0
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +7 -3
package/package.json
CHANGED
|
@@ -66,10 +66,10 @@ export class Spinner extends Aspect<'spinner'> {
|
|
|
66
66
|
// ===== UI OVER AN OPEN SCENE =====
|
|
67
67
|
|
|
68
68
|
// Exactly one Presentable is visible at a time: UIScreen(...).open() REPLACES the scene. A HUD is
|
|
69
|
-
// a UIWidget
|
|
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
|
-
|
|
72
|
+
scene.setContent([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
|
-
UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
110
|
+
scene.setContent([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>
|
package/prompts/3d-scene.md
CHANGED
|
@@ -114,6 +114,6 @@ const hud = UIWidget(
|
|
|
114
114
|
scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
|
|
115
115
|
).style({ top: 'max(safe-top, 16px)', right: 16 })
|
|
116
116
|
|
|
117
|
+
scene.setContent([hud])
|
|
117
118
|
scene.open()
|
|
118
|
-
hud.attachTo(scene).show()
|
|
119
119
|
</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
|
|
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.
|
|
116
116
|
|
|
117
117
|
## Project context
|
|
118
118
|
|
package/prompts/dist/3d-app.md
CHANGED
|
@@ -775,8 +775,8 @@ const hud = UIWidget(
|
|
|
775
775
|
scoreLabel = UIText('Score: 0').style({ color: 'white', fontSize: 24, fontWeight: 700 }),
|
|
776
776
|
).style({ top: 'max(safe-top, 16px)', right: 16 })
|
|
777
777
|
|
|
778
|
+
scene.setContent([hud])
|
|
778
779
|
scene.open()
|
|
779
|
-
hud.attachTo(scene).show()
|
|
780
780
|
</file>
|
|
781
781
|
|
|
782
782
|
## Scene files (.scene.ts)
|
|
@@ -847,10 +847,10 @@ export class Spinner extends Aspect<'spinner'> {
|
|
|
847
847
|
// ===== UI OVER AN OPEN SCENE =====
|
|
848
848
|
|
|
849
849
|
// Exactly one Presentable is visible at a time: UIScreen(...).open() REPLACES the scene. A HUD is
|
|
850
|
-
// a UIWidget
|
|
850
|
+
// a UIWidget that is the scene's CONTENT — it shows and hides together with it:
|
|
851
851
|
const hud = UIWidget(UIText('Score: 0').style({ color: 'white', fontSize: 24 }))
|
|
852
852
|
.style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
853
|
-
|
|
853
|
+
scene.setContent([hud])
|
|
854
854
|
|
|
855
855
|
// ===== SCENE FILE MISTAKES — DO NOT DO THESE =====
|
|
856
856
|
|
|
@@ -888,7 +888,7 @@ const { scene, nodes } = await mainScene.open()
|
|
|
888
888
|
|
|
889
889
|
let taps = 0
|
|
890
890
|
const label = UIText('Tap the crate').style({ color: 'white', fontSize: 20, fontWeight: 700 })
|
|
891
|
-
UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })
|
|
891
|
+
scene.setContent([UIWidget(label).style({ top: 'max(safe-top, 16px)', left: 16 })])
|
|
892
892
|
|
|
893
893
|
nodes.crate.addEventListener('click', () => { label.text = `Taps: ${++taps}` })
|
|
894
894
|
</file>
|
package/prompts/ui.md
CHANGED
|
@@ -161,7 +161,7 @@ UIImage(src) // src: string url | FetchResponse | File | SvgSource | Canvas (se
|
|
|
161
161
|
// .setSourceRect(x, y, w, h) — crop to a sub-rect of the source (texture px); frame fills the box, objectFit
|
|
162
162
|
// ignored. Chainable, safe to call before mount. For atlases/spritesheets — swap the rect to change frame.
|
|
163
163
|
// Note: tintColor applies only to SVG sources — it replaces ALL fill and stroke colors (monochrome icons only)
|
|
164
|
-
// Note: a
|
|
164
|
+
// Note: a project file by its path as a string — UIImage("images/photo.png") — is the bundled asset (the compile resolves it; a path that names no file fails the compile); plain https:// strings are remote URLs
|
|
165
165
|
|
|
166
166
|
// UIVideo — the on-screen surface only; playback lives entirely on the VideoPlayer (see AUDIO / VIDEO above)
|
|
167
167
|
UIVideo(player)
|
|
@@ -466,6 +466,8 @@ UIImage(heart).style({ width: 24, height: 24, tintColor: "#666" }) // tintColor
|
|
|
466
466
|
// ===== ICONS (assetIcon) =====
|
|
467
467
|
// Use real icons instead of emoji. assetIcon("pack:name") resolves the icon at COMPILE time and
|
|
468
468
|
// inlines it as an image source (same shape as SvgSource) — no imports, no project files, offline.
|
|
469
|
+
// It returns an SvgSource — the same type an imported .svg and SvgSource(xml) give — so a component
|
|
470
|
+
// that takes an icon is typed (icon: SvgSource), never (icon: string).
|
|
469
471
|
UIImage(assetIcon("lucide:bell")).style({ width: 24, height: 24, tintColor: "#8a8f98" })
|
|
470
472
|
// The id must be a string literal; an unknown id is a compile error (with name suggestions).
|
|
471
473
|
// Recolor via the tintColor style or the { color } option (hex literal bakes in; expression tints).
|
|
@@ -520,12 +522,6 @@ UIRow(isGroup ? button : UIColumn()) // WRONG
|
|
|
520
522
|
// ✅ null is skipped in children — no phantom element
|
|
521
523
|
UIRow(isGroup ? button : null)
|
|
522
524
|
|
|
523
|
-
// ❌ pointing UIImage / bgImage at a project file by bare path — it won't resolve to the bundled asset
|
|
524
|
-
UIImage("./photo.png") // WRONG (a plain string works only for remote http(s) URLs)
|
|
525
|
-
// ✅ import the asset, or wrap its path in asset()
|
|
526
|
-
import photo from './photo.png'
|
|
527
|
-
UIImage(photo) // or: UIImage(asset('./photo.png'))
|
|
528
|
-
|
|
529
525
|
// ❌ callback-first edge callbacks on UIVirtualizedList
|
|
530
526
|
list.onEndReached(loadNextPage) // WRONG — threshold is the first argument, and required
|
|
531
527
|
// ✅ threshold (px from the edge) first
|
package/src/bridges/gl.d.ts
CHANGED
|
@@ -95,6 +95,11 @@ declare global {
|
|
|
95
95
|
/** Punctual light. `intensity` is luminous POWER in lumens; `falloff` is the metres of
|
|
96
96
|
* influence (filament's own default is 1 m, i.e. invisible, so it is always passed). */
|
|
97
97
|
createPointLight?(entityId: u32, intensity: f64, color: u32, falloff: f64, castShadows: boolean): void
|
|
98
|
+
/** Spot light: the point light's lumens and falloff in a cone along the entity's local -Z. `inner` / `outer` are
|
|
99
|
+
* HALF-angles in radians (outer ≤ π/2); the cone does not change the brightness. Real time only (the bake skips it). */
|
|
100
|
+
createSpotLight?(entityId: u32, intensity: f64, color: u32, falloff: f64, inner: f32, outer: f32, castShadows: boolean): void
|
|
101
|
+
/** A spot light's cone, live — half-angles in radians, as createSpotLight. */
|
|
102
|
+
setLightCone?(entityId: u32, inner: f32, outer: f32): void
|
|
98
103
|
/** The lightmap bake takes this point light as a RECTANGLE of `width` x `height` metres (an area light: a ceiling
|
|
99
104
|
* panel) in the light's local XZ plane, emitting along its local -Y with the same lumens; 0, 0 = a point again. */
|
|
100
105
|
setLightBakeArea?(entityId: u32, width: f32, height: f32): void
|
package/src/bridges/tree.d.ts
CHANGED
|
@@ -112,10 +112,20 @@ declare global {
|
|
|
112
112
|
isViewSupported(name: string): boolean
|
|
113
113
|
viewVersion(name: string): i32
|
|
114
114
|
viewCall(viewId: i32, method: string, args: value, onComplete: (result?: value) => void, onReject: (message: string) => void): void
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// ---- widgets (docs/plans/widgets-plan.md): a widget is a CHILD — of a screen root, of a
|
|
116
|
+
// destination's content layer, of the app layer — never a root of its own; `insert` /
|
|
117
|
+
// `remove` place it, `display` shows it, the runtime lays it out in the outermost presented
|
|
118
|
+
// destination's box (the portal) and mounts it under that root for the host. ------------
|
|
119
|
+
// The content LAYER of a destination: a "layer" root (a pass-through root laid out at the frame,
|
|
120
|
+
// shown while the destination is presented) whose widget children are the surface's UI — a HUD
|
|
121
|
+
// over a scene, a card over a map. `kind` TREE_DEST_NONE (id 0) = the APP layer, above every
|
|
122
|
+
// destination, always shown. 0 takes the layer away.
|
|
123
|
+
setLayer(kind: u8, id: f64, layer: TreeNodeId): void
|
|
124
|
+
// The pose a widget child enters from and leaves to: a transition id (defineTransition) whose
|
|
125
|
+
// ALONE roles the runtime plays as tracks on the widget when its resolved `display` flips —
|
|
126
|
+
// `none → flex` from the enter pose to rest, `flex → none` from rest to the exit pose, `none`
|
|
127
|
+
// taking effect when the tracks land. TRANSITION_NONE = no pose (the flip is instant).
|
|
128
|
+
setWidgetPose(id: TreeNodeId, transition: i32): void
|
|
119
129
|
|
|
120
130
|
// ---- pager (a "pager" node's children are its pages; TAB-MAJOR order, `lens` = per-tab stack
|
|
121
131
|
// lengths, `index` = the selected tab). A change of the current tab's stack plays a
|
|
@@ -251,11 +261,16 @@ export interface HostUI {
|
|
|
251
261
|
* call's arguments, valid for the call. Settle exactly one of the two callbacks through the
|
|
252
262
|
* dispatch bridge — `onComplete` with the result as ONE value, or with none. */
|
|
253
263
|
viewCall?(viewId: i32, method: string, args: value, onComplete: (result?: value) => void, onReject: (message: string) => void): void
|
|
254
|
-
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
/** A LAYER root is shown: a pass-through root the runtime lays out at the frame, above every
|
|
265
|
+
* destination (the app layer) or over the presented surface (a scene's / a native view's / a
|
|
266
|
+
* video's content layer). Its own box is never hit — only its children are — so a touch outside
|
|
267
|
+
* them reaches what is under the layer; it is painted after the destination, layers in open
|
|
268
|
+
* order. Its children are widgets (the runtime inserts them with insertNode, as under a screen
|
|
269
|
+
* root): a widget child with an overlay layer (`overlayColor`, any color — "transparent" too)
|
|
270
|
+
* intercepts every touch outside its own box within its root and reports it as the overlay tap. */
|
|
271
|
+
layerOpen(node: nodeid): void
|
|
272
|
+
/** The layer root is hidden (its view stays; releaseNode says when it is gone). */
|
|
273
|
+
layerClose(node: nodeid): void
|
|
259
274
|
/** Load a font face for `fontFamily` (weight 100–900, style 0 normal / 2 italic) from `url`; settle
|
|
260
275
|
* one of the callbacks through the dispatch bridge. */
|
|
261
276
|
registerFont(url: string, fontFamily: string, weight: i32, style: i32, onComplete: () => void, onReject: (err: string) => void): void
|
|
@@ -289,9 +304,6 @@ export interface HostUI {
|
|
|
289
304
|
* a child (covered by a push, a keepAlive page); a page that is released goes through
|
|
290
305
|
* removeNode right after. */
|
|
291
306
|
pagerDropPage?(node: nodeid, page: nodeid): void
|
|
292
|
-
/** A pager page root (`page`) was mounted under the pager or released (an attachTo widget names the
|
|
293
|
-
* page node as its owner). */
|
|
294
|
-
pagerPageMounted?(page: nodeid, mounted: boolean): void
|
|
295
307
|
/** Optional: the host may PLAY a UI tween track itself — a platform animation interpolating on its
|
|
296
308
|
* own clock (Core Animation on iOS) instead of one style write per runtime frame. `samples` holds
|
|
297
309
|
* the track evaluated at even points of its length, easing included, `lanes` floats each: 1 for a
|
package/src/chisel.ts
CHANGED
|
@@ -129,7 +129,7 @@ export const chiselVocab: ChiselVocab = {
|
|
|
129
129
|
"onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
|
|
130
130
|
"onEndReached", "onStartReached",
|
|
131
131
|
"append", "insert", "remove", "setContent", "keepAlive", "url",
|
|
132
|
-
"
|
|
132
|
+
"detents", "dismissible", "transition", "setSourceRect",
|
|
133
133
|
"push", "pop", "popToRoot", "replace", "select",
|
|
134
134
|
// UITabs / UIVirtualizedList / the native views (NativeView, CameraView, MapView, QRScanner)
|
|
135
135
|
"badge", "setData", "update", "prepend", "removeByKey",
|
|
@@ -20,7 +20,7 @@ export const hasAssetMacro = (src: string): boolean => src.includes("asset(")
|
|
|
20
20
|
* Not a full lexer: a regex LITERAL containing `asset(` would still be seen as code. That case
|
|
21
21
|
* doesn't occur in practice and behaves exactly as it did before this masking existed.
|
|
22
22
|
*/
|
|
23
|
-
const maskNonCode = (src: string): string => {
|
|
23
|
+
const maskNonCode = (src: string, onString?: (start: number, end: number) => void): string => {
|
|
24
24
|
const out = src.split("")
|
|
25
25
|
const blank = (from: number, to: number) => {
|
|
26
26
|
for (let k = from; k < to && k < out.length; k++) if (out[k] !== "\n") out[k] = " "
|
|
@@ -57,12 +57,15 @@ const maskNonCode = (src: string): string => {
|
|
|
57
57
|
if (c === "'" || c === "\"") {
|
|
58
58
|
const start = i
|
|
59
59
|
i++
|
|
60
|
+
let closed = false
|
|
60
61
|
while (i < src.length) {
|
|
61
62
|
const q = src[i]
|
|
62
63
|
if (q === "\\") { i += 2; continue }
|
|
63
64
|
i++
|
|
64
|
-
if (q === c
|
|
65
|
+
if (q === c) { closed = true; break }
|
|
66
|
+
if (q === "\n") break
|
|
65
67
|
}
|
|
68
|
+
if (closed && onString) onString(start, i)
|
|
66
69
|
blank(start, i); continue
|
|
67
70
|
}
|
|
68
71
|
if (c === "`") { i++; scanTemplate(); continue }
|
|
@@ -77,8 +80,70 @@ const maskNonCode = (src: string): string => {
|
|
|
77
80
|
return out.join("")
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
/** One `asset('…')` call site: the path as written, plus where it sits in the file.
|
|
81
|
-
|
|
83
|
+
/** One `asset('…')` call site: the path as written, plus where it sits in the file. A bare path
|
|
84
|
+
* literal (`collectPathLiterals`) is the same shape: `path` is then the RESOLVED root path of the
|
|
85
|
+
* file it names, or, with `missing`, the literal as written — a path-looking string that names no
|
|
86
|
+
* file, reported by the bundler the way a missing `asset()` is. */
|
|
87
|
+
export type AssetMacroRef = { path: string, index: number, length: number, line: number, missing?: true }
|
|
88
|
+
|
|
89
|
+
/** The file kinds a bare path literal may stand for: what `asset()` returns a REFERENCE for (an
|
|
90
|
+
* image, a sound, a video, a model, a font, raw bytes) plus `.svg` (an SvgSource, what UIImage
|
|
91
|
+
* takes). Not `.json`, whose asset is its parsed data — a `.json` path in a string stays a string. */
|
|
92
|
+
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
|
|
93
|
+
const CODE_EXT = /\.([tj]sx?|mjs|cjs|json)$/i
|
|
94
|
+
// What precedes an import specifier: the literal is a module path, never a resource reference.
|
|
95
|
+
const IMPORT_BEFORE = /(?:\bfrom|\bimport|\bimport\s*\(|\brequire\s*\()\s*$/
|
|
96
|
+
|
|
97
|
+
const normalizePath = (path: string): string => {
|
|
98
|
+
const out: string[] = []
|
|
99
|
+
for (const part of path.split("/")) {
|
|
100
|
+
if (part === "" || part === ".") continue
|
|
101
|
+
if (part === "..") { out.pop(); continue }
|
|
102
|
+
out.push(part)
|
|
103
|
+
}
|
|
104
|
+
return "/" + out.join("/")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Every string literal in `src` that is a PATH to a resource file of the project — `"images/hero.png"`,
|
|
109
|
+
* `"./hero.png"`, `"/sounds/tap.mp3"` — as the asset references they stand for: a bare path is the
|
|
110
|
+
* `asset('…')` call nobody had to write (2026-10-08: a model writes the path it put the file at and
|
|
111
|
+
* not the macro; the two now mean the same). Resolution: `./` and `../` from the file first, then
|
|
112
|
+
* from the project root (a model writes `./images/x.png` in `views/cart.ts` meaning the root);
|
|
113
|
+
* `/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.
|
|
119
|
+
*/
|
|
120
|
+
export const collectPathLiterals = (
|
|
121
|
+
src: string, filePath: string, exists: (rootPath: string) => boolean, taken: AssetMacroRef[] = [],
|
|
122
|
+
): AssetMacroRef[] => {
|
|
123
|
+
const refs: AssetMacroRef[] = []
|
|
124
|
+
const dir = filePath.slice(0, filePath.lastIndexOf("/"))
|
|
125
|
+
maskNonCode(src, (start, end) => {
|
|
126
|
+
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
|
|
132
|
+
const relative = value.startsWith("./") || value.startsWith("../")
|
|
133
|
+
const rooted = value.startsWith("/")
|
|
134
|
+
const candidates = rooted ? [ normalizePath(value) ]
|
|
135
|
+
: relative ? [ normalizePath(`${dir}/${value}`), normalizePath(value) ]
|
|
136
|
+
: [ normalizePath(value), normalizePath(`${dir}/${value}`) ]
|
|
137
|
+
const found = candidates.find(exists)
|
|
138
|
+
if (found === undefined && !relative && !rooted && !value.includes("/")) return
|
|
139
|
+
let line = 1
|
|
140
|
+
for (let k = 0; k < start; k++) if (src[k] === "\n") line++
|
|
141
|
+
refs.push(found !== undefined
|
|
142
|
+
? { path: found, index: start, length: end - start, line }
|
|
143
|
+
: { path: value, index: start, length: end - start, line, missing: true })
|
|
144
|
+
})
|
|
145
|
+
return refs
|
|
146
|
+
}
|
|
82
147
|
|
|
83
148
|
/**
|
|
84
149
|
* Every `asset('…')` macro call in `src`, skipping the ones inside comments and string literals
|
package/src/compile/bundler.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"
|
|
|
3
3
|
import { createRequire } from "node:module"
|
|
4
4
|
import { dirname, join, relative } from "node:path"
|
|
5
5
|
import { fileURLToPath } from "node:url"
|
|
6
|
-
import { collectAssetMacros, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
|
|
6
|
+
import { collectAssetMacros, collectPathLiterals, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
|
|
7
7
|
import { scanModulesFallback, type ScanModule } from "./detectEntry"
|
|
8
8
|
import { hasAspectExtends, injectAspectNames } from "./aspectMacro"
|
|
9
9
|
import { hasImportStatement, rewriteLibraryImports } from "./libraryImports"
|
|
@@ -178,6 +178,9 @@ const reachableFrom = (entry: string, sources: MemoryFiles): Set<string> => {
|
|
|
178
178
|
const assertAssetsExist = (refs: { file: string, ref: AssetMacroRef }[], files: MemoryFiles) => {
|
|
179
179
|
const missing: string[] = []
|
|
180
180
|
for (const { file, ref } of refs) {
|
|
181
|
+
// A path literal that named no file of the project (collectPathLiterals): the string said
|
|
182
|
+
// where a file is, and it is not there.
|
|
183
|
+
if (ref.missing) { missing.push(`file not found: ${ref.path} (${file.replace(/^\//, "")}:${ref.line})`); continue }
|
|
181
184
|
// Bare specifiers aren't paths at all — chisel reports those as bare imports.
|
|
182
185
|
if (!ref.path.startsWith("./") && !ref.path.startsWith("../") && !ref.path.startsWith("/")) continue
|
|
183
186
|
const dir = file.slice(0, file.lastIndexOf("/"))
|
|
@@ -216,8 +219,12 @@ const bundleProjectChisel = async (
|
|
|
216
219
|
if (/\.[tj]sx?$/.test(path)) {
|
|
217
220
|
if (hasImportStatement(src)) src = rewriteLibraryImports(src, slugs)
|
|
218
221
|
scanSources[path] = src
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
// `asset('…')` calls, then the bare path literals that name a resource of the project (the
|
|
223
|
+
// same reference without the macro); both desugar to the import the author could have written.
|
|
224
|
+
const macros = hasAssetMacro(src) ? collectAssetMacros(src, path) : []
|
|
225
|
+
const paths = collectPathLiterals(src, path, (rootPath) => files[rootPath] !== undefined, macros)
|
|
226
|
+
const found = macros.concat(paths).sort((a, b) => a.index - b.index)
|
|
227
|
+
if (found.length > 0) {
|
|
221
228
|
for (const ref of found) assetRefs.push({ file: path, ref })
|
|
222
229
|
src = desugarAssetMacros(src, path, found)
|
|
223
230
|
}
|
package/src/g2/Scene2D.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
|
|
|
6
6
|
import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
|
|
7
7
|
import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
|
|
8
8
|
import { TREE_DEST_SCENE2D, ensureEmitter, registerDest } from "../ui/tree"
|
|
9
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
|
|
9
10
|
import { cx, cy, type Vec2Like } from "../math/vec"
|
|
10
11
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
11
12
|
import { Camera2D } from "./Camera2D"
|
|
@@ -188,6 +189,15 @@ export class Scene2D implements Presentable {
|
|
|
188
189
|
registerDest(TREE_DEST_SCENE2D, this.id, this)
|
|
189
190
|
return [TREE_DEST_SCENE2D, this.id, "", ""]
|
|
190
191
|
}
|
|
192
|
+
/** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
193
|
+
_contentLayer?: LayerElement
|
|
194
|
+
/** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
|
|
195
|
+
* presented; touches outside a widget's box reach the scene. */
|
|
196
|
+
setContent(content: UIWidgetContent): this {
|
|
197
|
+
ensureEmitter()
|
|
198
|
+
_setSurfaceContent(this, content)
|
|
199
|
+
return this
|
|
200
|
+
}
|
|
191
201
|
/** @internal presentation lifecycle from the runtime */
|
|
192
202
|
_emitOpen(): void { for (const cb of this.ol.slice()) cb() }
|
|
193
203
|
/** @internal */
|