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/src/gl/Light.ts
CHANGED
|
@@ -1,194 +1,257 @@
|
|
|
1
|
-
// Lights. A directional sun (the engine's primary light)
|
|
2
|
-
// scene like any node; `intensity` and `color` stay writable so a light can be animated.
|
|
3
|
-
|
|
4
|
-
import { Color, type ColorInput } from "../core/color"
|
|
5
|
-
import type { Vec3Like } from "../math/vec"
|
|
6
|
-
import { Node, type NodeTweenProps } from "./Node"
|
|
7
|
-
import { DOM_GL_LIGHT, colorValue, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
8
|
-
import type { Animation } from "../animate/tween/Animation"
|
|
9
|
-
|
|
10
|
-
/** Animatable light props on top of the node transform. */
|
|
11
|
-
export type LightTweenProps = NodeTweenProps & {
|
|
12
|
-
intensity?: number | number[],
|
|
13
|
-
color?: ColorInput | ColorInput[],
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type SunOptions = {
|
|
17
|
-
/** Light direction (points where the light travels). Defaults to a typical key-light angle. */
|
|
18
|
-
direction?: Vec3Like
|
|
19
|
-
/** Luminous intensity. */
|
|
20
|
-
intensity?: number
|
|
21
|
-
color?: ColorInput
|
|
22
|
-
/**
|
|
23
|
-
* Shadow quality (default 1):
|
|
24
|
-
* 0 - no shadows
|
|
25
|
-
* 1 - 1024 map, hard edges (one filtered tap; the cheap level, ~3 % of a frame on an iGPU)
|
|
26
|
-
* 2 - 1024 map, soft edges (variance shadows + blur; ~+15 % of a frame on an iGPU)
|
|
27
|
-
* 3 - 2048 map, contact-hardening soft edges (PCSS: sharp where the caster touches, softer
|
|
28
|
-
* away; discrete-GPU territory, ~+60 % of a frame on an iGPU)
|
|
29
|
-
* A lightmapped level already carries every static shadow, so 0 is a legitimate choice there.
|
|
30
|
-
* On a level with a baked light grid the variance / PCSS filters cannot be used (they need every
|
|
31
|
-
* receiver in the shadow map, and the baked statics are kept out of it): 3 renders as PCSS on the
|
|
32
|
-
* depth map - the same contact-hardening look - and 2 renders as 1.
|
|
33
|
-
*/
|
|
34
|
-
shadowsQuality?: 0 | 1 | 2 | 3
|
|
35
|
-
/**
|
|
36
|
-
* Metres from the camera within which the sun casts shadows (default 100). Shadows fade out by
|
|
37
|
-
* this distance and the shadow map covers only this range, so a shorter distance is crisper for
|
|
38
|
-
* the same quality and puts fewer casters in the shadow pass. 30-40 is plenty in first person.
|
|
39
|
-
*/
|
|
40
|
-
shadowDistance?: number
|
|
41
|
-
/**
|
|
42
|
-
* How many shadow-map cascades split `shadowDistance` (default 1). One map over the whole range gives a tall
|
|
43
|
-
* tree 5-10 cm shadow texels on its own trunk; 3 cascades put a tight map on the first metres and coarser
|
|
44
|
-
* ones behind, fit to their texel grids so the shadow does not swim as the camera moves. Each cascade is
|
|
45
|
-
* another caster pass, so it costs in proportion to what casts: forests, not a lightmapped level.
|
|
46
|
-
*/
|
|
47
|
-
shadowCascades?: 1 | 2 | 3 | 4
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type PointOptions = {
|
|
51
|
-
/**
|
|
52
|
-
* Luminous POWER in lumens — the same physical scale as the sun's lux and `environmentIntensity`,
|
|
53
|
-
* so a light keeps its look when the scene's exposure changes. A candle is ~12 lm, a bare 60 W
|
|
54
|
-
* bulb ~800, a car headlight ~1500, a fireball millions. Default 1000.
|
|
55
|
-
*/
|
|
56
|
-
intensity?: number
|
|
57
|
-
color?: ColorInput
|
|
58
|
-
/**
|
|
59
|
-
* Metres of influence — past it the light contributes nothing. This is the performance knob:
|
|
60
|
-
* overlapping point lights are the expensive case, so keep it as small as the look allows.
|
|
61
|
-
* Default 10.
|
|
62
|
-
*/
|
|
63
|
-
range?: number
|
|
64
|
-
/** Point-light shadows are a cubemap render per light; off by default. */
|
|
65
|
-
castShadows?: boolean
|
|
66
|
-
/**
|
|
67
|
-
* Whether `lecodes lightmap bake` bakes this light into the level's lightmap. A baked lamp lights the statics
|
|
68
|
-
* from the atlas and the MOVERS from the level's light grid, and is held dark in real time while that grid is
|
|
69
|
-
* loaded (without a grid it keeps lighting the movers live); a light
|
|
70
|
-
* that must reach the statics live too — a flicker, a lamp the player can shoot out, a muzzle flash —
|
|
71
|
-
* says `baked: false`: it is kept out of the bake and put on the statics' light channel. Default true.
|
|
72
|
-
*/
|
|
73
|
-
baked?: boolean
|
|
74
|
-
/**
|
|
75
|
-
* THE BAKE'S SHAPE of this lamp: `[width, height]` in metres = an AREA light - a rectangle in the light's local XZ
|
|
76
|
-
* plane, emitting along its local -Y (down, for an unrotated node) with the same lumens: a ceiling panel. The bake
|
|
77
|
-
* lights from the whole rectangle (soft shadows, light from where the panel is and not from a point inside the
|
|
78
|
-
* fixture). Real time it is still the point light above. Ignored with `baked: false`.
|
|
79
|
-
*/
|
|
80
|
-
bakeArea?: readonly [number, number]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
// Lights. A directional sun (the engine's primary light), punctual point lights and spot lights. Add one to a
|
|
2
|
+
// scene like any node; `intensity` and `color` stay writable so a light can be animated.
|
|
3
|
+
|
|
4
|
+
import { Color, type ColorInput } from "../core/color"
|
|
5
|
+
import type { Vec3Like } from "../math/vec"
|
|
6
|
+
import { Node, type NodeTweenProps } from "./Node"
|
|
7
|
+
import { DOM_GL_LIGHT, colorValue, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
8
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
9
|
+
|
|
10
|
+
/** Animatable light props on top of the node transform. */
|
|
11
|
+
export type LightTweenProps = NodeTweenProps & {
|
|
12
|
+
intensity?: number | number[],
|
|
13
|
+
color?: ColorInput | ColorInput[],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SunOptions = {
|
|
17
|
+
/** Light direction (points where the light travels). Defaults to a typical key-light angle. */
|
|
18
|
+
direction?: Vec3Like
|
|
19
|
+
/** Luminous intensity. */
|
|
20
|
+
intensity?: number
|
|
21
|
+
color?: ColorInput
|
|
22
|
+
/**
|
|
23
|
+
* Shadow quality (default 1):
|
|
24
|
+
* 0 - no shadows
|
|
25
|
+
* 1 - 1024 map, hard edges (one filtered tap; the cheap level, ~3 % of a frame on an iGPU)
|
|
26
|
+
* 2 - 1024 map, soft edges (variance shadows + blur; ~+15 % of a frame on an iGPU)
|
|
27
|
+
* 3 - 2048 map, contact-hardening soft edges (PCSS: sharp where the caster touches, softer
|
|
28
|
+
* away; discrete-GPU territory, ~+60 % of a frame on an iGPU)
|
|
29
|
+
* A lightmapped level already carries every static shadow, so 0 is a legitimate choice there.
|
|
30
|
+
* On a level with a baked light grid the variance / PCSS filters cannot be used (they need every
|
|
31
|
+
* receiver in the shadow map, and the baked statics are kept out of it): 3 renders as PCSS on the
|
|
32
|
+
* depth map - the same contact-hardening look - and 2 renders as 1.
|
|
33
|
+
*/
|
|
34
|
+
shadowsQuality?: 0 | 1 | 2 | 3
|
|
35
|
+
/**
|
|
36
|
+
* Metres from the camera within which the sun casts shadows (default 100). Shadows fade out by
|
|
37
|
+
* this distance and the shadow map covers only this range, so a shorter distance is crisper for
|
|
38
|
+
* the same quality and puts fewer casters in the shadow pass. 30-40 is plenty in first person.
|
|
39
|
+
*/
|
|
40
|
+
shadowDistance?: number
|
|
41
|
+
/**
|
|
42
|
+
* How many shadow-map cascades split `shadowDistance` (default 1). One map over the whole range gives a tall
|
|
43
|
+
* tree 5-10 cm shadow texels on its own trunk; 3 cascades put a tight map on the first metres and coarser
|
|
44
|
+
* ones behind, fit to their texel grids so the shadow does not swim as the camera moves. Each cascade is
|
|
45
|
+
* another caster pass, so it costs in proportion to what casts: forests, not a lightmapped level.
|
|
46
|
+
*/
|
|
47
|
+
shadowCascades?: 1 | 2 | 3 | 4
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PointOptions = {
|
|
51
|
+
/**
|
|
52
|
+
* Luminous POWER in lumens — the same physical scale as the sun's lux and `environmentIntensity`,
|
|
53
|
+
* so a light keeps its look when the scene's exposure changes. A candle is ~12 lm, a bare 60 W
|
|
54
|
+
* bulb ~800, a car headlight ~1500, a fireball millions. Default 1000.
|
|
55
|
+
*/
|
|
56
|
+
intensity?: number
|
|
57
|
+
color?: ColorInput
|
|
58
|
+
/**
|
|
59
|
+
* Metres of influence — past it the light contributes nothing. This is the performance knob:
|
|
60
|
+
* overlapping point lights are the expensive case, so keep it as small as the look allows.
|
|
61
|
+
* Default 10.
|
|
62
|
+
*/
|
|
63
|
+
range?: number
|
|
64
|
+
/** Point-light shadows are a cubemap render per light; off by default. */
|
|
65
|
+
castShadows?: boolean
|
|
66
|
+
/**
|
|
67
|
+
* Whether `lecodes lightmap bake` bakes this light into the level's lightmap. A baked lamp lights the statics
|
|
68
|
+
* from the atlas and the MOVERS from the level's light grid, and is held dark in real time while that grid is
|
|
69
|
+
* loaded (without a grid it keeps lighting the movers live); a light
|
|
70
|
+
* that must reach the statics live too — a flicker, a lamp the player can shoot out, a muzzle flash —
|
|
71
|
+
* says `baked: false`: it is kept out of the bake and put on the statics' light channel. Default true.
|
|
72
|
+
*/
|
|
73
|
+
baked?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* THE BAKE'S SHAPE of this lamp: `[width, height]` in metres = an AREA light - a rectangle in the light's local XZ
|
|
76
|
+
* plane, emitting along its local -Y (down, for an unrotated node) with the same lumens: a ceiling panel. The bake
|
|
77
|
+
* lights from the whole rectangle (soft shadows, light from where the panel is and not from a point inside the
|
|
78
|
+
* fixture). Real time it is still the point light above. Ignored with `baked: false`.
|
|
79
|
+
*/
|
|
80
|
+
bakeArea?: readonly [number, number]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type SpotOptions = {
|
|
84
|
+
/**
|
|
85
|
+
* Luminous power in lumens, as a point light's. The cone does not change the brightness: widen it and the
|
|
86
|
+
* same lumens light a larger patch at the same level. On its axis a spot is 4x brighter than a point light of
|
|
87
|
+
* the same lumens (its light is not spread over the whole sphere). Default 1000.
|
|
88
|
+
*/
|
|
89
|
+
intensity?: number
|
|
90
|
+
color?: ColorInput
|
|
91
|
+
/** Metres of influence along the cone, the performance knob as for a point light. Default 10. */
|
|
92
|
+
range?: number
|
|
93
|
+
/** The cone's full angle in degrees, up to 180: past it the light contributes nothing. Default 45. */
|
|
94
|
+
angle?: number
|
|
95
|
+
/** The full angle in degrees of the cone's fully lit core; the light fades from it out to `angle`. Default 0.75 x `angle`. */
|
|
96
|
+
innerAngle?: number
|
|
97
|
+
/** A spot's shadow is one 2D shadow map (a point light's is a cubemap); off by default. */
|
|
98
|
+
castShadows?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class Light extends Node {
|
|
102
|
+
private _intensity = 0
|
|
103
|
+
|
|
104
|
+
/** Tween `intensity` / `color` (a flash, a sunrise) and the transform — see {@link Node.animateTo}. */
|
|
105
|
+
override animateTo(props: LightTweenProps & TweenMeta): Animation { return super.animateTo(props) }
|
|
106
|
+
override animateFrom(props: LightTweenProps & TweenMeta): Animation { return super.animateFrom(props) }
|
|
107
|
+
/** @internal */
|
|
108
|
+
override _tweenChannel(prop: string): TweenChannel | null {
|
|
109
|
+
const id = () => this.id
|
|
110
|
+
if (prop === "intensity") return { domain: DOM_GL_LIGHT, id, value: floatValue, commit: (v) => { this._intensity = v as number } }
|
|
111
|
+
if (prop === "color") return { domain: DOM_GL_LIGHT, id, value: (v) => colorValue(v, Color.toRgba01) }
|
|
112
|
+
return super._tweenChannel(prop)
|
|
113
|
+
}
|
|
114
|
+
/** @internal PointOptions.baked — a candidate for the lightmap bake (default true). */
|
|
115
|
+
_baked = true
|
|
116
|
+
/** @internal every live point light, for the bake / the switch-off; a destroyed one leaves. */
|
|
117
|
+
static _points = new Set<Light>()
|
|
118
|
+
/** @internal sun direction as created (the engine keeps it). */
|
|
119
|
+
_direction: [number, number, number] = [ 0, -1, 0 ]
|
|
120
|
+
/** The most recently created sun. */
|
|
121
|
+
static lastSun: Light | null = null
|
|
122
|
+
/** @internal the colour as created / last set (a settings menu recreates a sun from these). */
|
|
123
|
+
_color: ColorInput = 0xffffff
|
|
124
|
+
/** @internal sun shadow options as created (creation-time in the engine). */
|
|
125
|
+
_shadowsQuality = 1
|
|
126
|
+
_shadowDistance = 100
|
|
127
|
+
_shadowCascades = 1
|
|
128
|
+
|
|
129
|
+
/** A directional sun light. */
|
|
130
|
+
static sun(options: SunOptions = {}): Light {
|
|
131
|
+
const light = new Light()
|
|
132
|
+
const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
|
|
133
|
+
light._direction = [ dx, dy, dz ]
|
|
134
|
+
light._intensity = options.intensity ?? 100000
|
|
135
|
+
light._color = options.color ?? 0xffffff
|
|
136
|
+
light._shadowsQuality = options.shadowsQuality ?? 1
|
|
137
|
+
light._shadowDistance = options.shadowDistance ?? 100
|
|
138
|
+
light._shadowCascades = options.shadowCascades ?? 1
|
|
139
|
+
Light.lastSun = light
|
|
140
|
+
_creator.createSunLight(
|
|
141
|
+
light.id, dx, dy, dz,
|
|
142
|
+
light._intensity,
|
|
143
|
+
Color.toPackedRgb(options.color ?? 0xffffff),
|
|
144
|
+
options.shadowsQuality ?? 1,
|
|
145
|
+
options.shadowDistance ?? 100,
|
|
146
|
+
light._shadowCascades,
|
|
147
|
+
)
|
|
148
|
+
return light
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A point light — a lamp, a muzzle flash, a fireball. Position it like any node.
|
|
153
|
+
*/
|
|
154
|
+
static point(options: PointOptions = {}): Light {
|
|
155
|
+
const light = new Light()
|
|
156
|
+
light._intensity = options.intensity ?? 1000
|
|
157
|
+
light._color = options.color ?? 0xffffff
|
|
158
|
+
light._baked = options.baked ?? true
|
|
159
|
+
Light._points.add(light)
|
|
160
|
+
_creator.createPointLight?.(
|
|
161
|
+
light.id,
|
|
162
|
+
light._intensity,
|
|
163
|
+
Color.toPackedRgb(options.color ?? 0xffffff),
|
|
164
|
+
options.range ?? 10,
|
|
165
|
+
options.castShadows ?? false,
|
|
166
|
+
)
|
|
167
|
+
if (options.bakeArea && light._baked) _creator.setLightBakeArea?.(light.id, options.bakeArea[0], options.bakeArea[1])
|
|
168
|
+
// an unbaked lamp lights the baked statics live: Filament channel 1 (see Lightmap)
|
|
169
|
+
if (!light._baked) _creator.setLightChannel?.(light.id, 1, true)
|
|
170
|
+
return light
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @internal a spot's cone, full angles in degrees (the engine takes half-angles in radians). */
|
|
174
|
+
_angle = 0
|
|
175
|
+
_innerAngle = 0
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A spot light — a flashlight, a headlight, a stage light. It shines along the node's forward (-Z): position
|
|
179
|
+
* it like any node and aim it with `lookAt`. Real time only: the lightmap bake takes no spot lights, so a spot
|
|
180
|
+
* lights the baked statics live too.
|
|
181
|
+
*/
|
|
182
|
+
static spot(options: SpotOptions = {}): Light {
|
|
183
|
+
const light = new Light()
|
|
184
|
+
light._intensity = options.intensity ?? 1000
|
|
185
|
+
light._color = options.color ?? 0xffffff
|
|
186
|
+
light._baked = false
|
|
187
|
+
light._angle = options.angle ?? 45
|
|
188
|
+
light._innerAngle = options.innerAngle ?? light._angle * 0.75
|
|
189
|
+
const [ inner, outer ] = light._cone()
|
|
190
|
+
_creator.createSpotLight?.(
|
|
191
|
+
light.id,
|
|
192
|
+
light._intensity,
|
|
193
|
+
Color.toPackedRgb(light._color),
|
|
194
|
+
options.range ?? 10,
|
|
195
|
+
inner, outer,
|
|
196
|
+
options.castShadows ?? false,
|
|
197
|
+
)
|
|
198
|
+
// the statics of a lightmapped level are on Filament channel 1 alone (see Lightmap)
|
|
199
|
+
_creator.setLightChannel?.(light.id, 1, true)
|
|
200
|
+
return light
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @internal the cone as the engine takes it: half-angles in radians, outer in (0, 90°], inner in [0, outer]. */
|
|
204
|
+
_cone(): [number, number] {
|
|
205
|
+
const rad = Math.PI / 360
|
|
206
|
+
const outer = Math.min(Math.max(this._angle, 0.1), 180) * rad
|
|
207
|
+
const inner = Math.min(Math.max(this._innerAngle, 0), this._angle) * rad
|
|
208
|
+
return [ Math.min(inner, outer), outer ]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A spot's full cone angle in degrees — live (a flashlight's focus). */
|
|
212
|
+
get angle(): number { return this._angle }
|
|
213
|
+
set angle(value: number) {
|
|
214
|
+
this._innerAngle *= this._angle > 0 ? value / this._angle : 0
|
|
215
|
+
this._angle = value
|
|
216
|
+
_creator.setLightCone?.(this.id, ...this._cone())
|
|
217
|
+
}
|
|
218
|
+
/** A spot's fully lit core, full angle in degrees — live. Setting `angle` scales it along. */
|
|
219
|
+
get innerAngle(): number { return this._innerAngle }
|
|
220
|
+
set innerAngle(value: number) {
|
|
221
|
+
this._innerAngle = value
|
|
222
|
+
_creator.setLightCone?.(this.id, ...this._cone())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Live intensity (sun: lux, point / spot: lumens) — animate a flash without rebuilding the light. */
|
|
226
|
+
get intensity(): number { return this._intensity }
|
|
227
|
+
set intensity(value: number) {
|
|
228
|
+
this._intensity = value
|
|
229
|
+
if (!this._heldDark) _creator.setLightIntensity?.(this.id, value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @internal A BAKED lamp is held dark while the level's LIGHT GRID is loaded (Lightmap): the statics have it in the
|
|
233
|
+
* atlas and the movers in the grid's ambient cubes - left burning it lit every mover and unbaked prop a second time,
|
|
234
|
+
* without a shadow (through walls), and put a hot spot on its own fixture. `intensity` keeps the authored value. */
|
|
235
|
+
_heldDark = false
|
|
236
|
+
_holdDark(dark: boolean): void {
|
|
237
|
+
if (this._heldDark === dark) return
|
|
238
|
+
this._heldDark = dark
|
|
239
|
+
_creator.setLightIntensity?.(this.id, dark ? 0 : this._intensity)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
destroy(): void {
|
|
243
|
+
Light._points.delete(this)
|
|
244
|
+
super.destroy()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
set color(value: ColorInput) {
|
|
248
|
+
this._color = value
|
|
249
|
+
_creator.setLightColor?.(this.id, Color.toPackedRgb(value))
|
|
250
|
+
}
|
|
251
|
+
get color(): ColorInput { return this._color }
|
|
252
|
+
/** Sun direction as created (the engine keeps it; a settings menu rebuilds a sun from it). */
|
|
253
|
+
get direction(): [number, number, number] { return this._direction }
|
|
254
|
+
/** Sun shadow options as created — creation-time in the engine, so a change means a new sun. */
|
|
255
|
+
get shadowsQuality(): number { return this._shadowsQuality }
|
|
256
|
+
get shadowDistance(): number { return this._shadowDistance }
|
|
257
|
+
}
|
package/src/gl/Material.ts
CHANGED
|
@@ -187,6 +187,9 @@ export class Material {
|
|
|
187
187
|
} else if ("_h" in (shader as any)) {
|
|
188
188
|
this.shader = shader as FetchResponse
|
|
189
189
|
this._h = _creator.createMaterial((shader as any)._h.id) // owned: dropped and unused → freed
|
|
190
|
+
// 0xFFFFFFFF = the engine refused the package: no shader in it for the running backend (a
|
|
191
|
+
// phone on Vulkan handed the mobile `_opengl` artifact — the host's log names what is missing).
|
|
192
|
+
if (this._h.id === 0xFFFFFFFF) throw new Error("Material: the engine refused the shader (no variant for its backend — see the host log)")
|
|
190
193
|
} else {
|
|
191
194
|
this.shader = "unknown"
|
|
192
195
|
this._h = shader as Handle // an instance the runtime already wrapped for us
|
package/src/gl/Scene.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem,
|
|
|
10
10
|
import { _bumpNavEpoch, _navEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
|
|
11
11
|
import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
|
|
12
12
|
import { TREE_DEST_SCENE3D, TREE_FLAG_BACK, ensureEmitter, registerDest, unregisterDest } from "../ui/tree"
|
|
13
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "../ui/UILayer"
|
|
13
14
|
import { device } from "../runtime/device"
|
|
14
15
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
15
16
|
import type { FetchResponse } from "../runtime/fetch"
|
|
@@ -478,10 +479,19 @@ export class Scene implements Presentable {
|
|
|
478
479
|
|
|
479
480
|
/** @internal The destination tuple (ui/presentable.ts). */
|
|
480
481
|
_dest(): DestTuple {
|
|
481
|
-
// Addressed by the router /
|
|
482
|
+
// Addressed by the router / its content layer: make sure the (kind, id) resolves back to us.
|
|
482
483
|
registerDest(TREE_DEST_SCENE3D, this._id, this)
|
|
483
484
|
return [TREE_DEST_SCENE3D, this._id, "", ""]
|
|
484
485
|
}
|
|
486
|
+
/** @internal The scene's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
487
|
+
_contentLayer?: LayerElement
|
|
488
|
+
/** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
|
|
489
|
+
* presented; touches outside a widget's box reach the scene. `scene.setContent([hud])`. */
|
|
490
|
+
setContent(content: UIWidgetContent): this {
|
|
491
|
+
ensureEmitter()
|
|
492
|
+
_setSurfaceContent(this, content)
|
|
493
|
+
return this
|
|
494
|
+
}
|
|
485
495
|
/** @internal presentation lifecycle from the runtime */
|
|
486
496
|
_emitOpen(): void { for (const cb of this.ol.slice()) cb() }
|
|
487
497
|
/** @internal */
|
package/src/host.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// JS-runtime globals provided by the host engine (web viewer / desktop / iOS), NOT by the SDK.
|
|
2
2
|
// Declared only so SDK and user code type-check — never bundled or injected.
|
|
3
3
|
|
|
4
|
+
// The one SDK type the macros return: an SVG image source, the same `SvgSource` a project has as
|
|
5
|
+
// a global (build-types.ts points this import into dist/types when it copies the file).
|
|
6
|
+
import type { SvgSource } from "./runtime/misc"
|
|
7
|
+
|
|
4
8
|
declare global {
|
|
5
9
|
function setTimeout(handler: (...args: any[]) => void, timeout?: number): number
|
|
6
10
|
function setInterval(handler: (...args: any[]) => void, timeout?: number): number
|
|
@@ -33,7 +37,7 @@ declare global {
|
|
|
33
37
|
function asset(path: `${string}.json`): any
|
|
34
38
|
/** Compile-time macro: `asset('./logo.svg')` yields the file as an SVG image source — for
|
|
35
39
|
* `UIImage(...)` and `bgImage`. */
|
|
36
|
-
function asset(path: `${string}.svg`):
|
|
40
|
+
function asset(path: `${string}.svg`): SvgSource
|
|
37
41
|
/** Compile-time macro: `asset('./hero.png')` is desugared by the bundler into the module import
|
|
38
42
|
* for that resource. The file must exist — a path that resolves to nothing fails the compile
|
|
39
43
|
* (`asset not found: ./hero.png (main.ts:3)`); there is no runtime fallback. Calls inside
|
|
@@ -53,7 +57,7 @@ declare global {
|
|
|
53
57
|
* (e.g. `"lucide:bell"`), string literal only. Recolor via `{ color }`: a hex LITERAL is baked
|
|
54
58
|
* into the SVG at compile time, a token/expression is applied as a tint (as is the `tintColor`
|
|
55
59
|
* style prop). */
|
|
56
|
-
function assetIcon(id: string, opts?: { color?: string }):
|
|
60
|
+
function assetIcon(id: string, opts?: { color?: string }): SvgSource
|
|
57
61
|
|
|
58
62
|
/** Type-only editor convenience: the style object type of a UI element. `Style<UIButton>` (or
|
|
59
63
|
* `Style<typeof myButton>`) is what you'd pass to `el.style(...)` — for typing reusable style
|
package/src/inject.ts
CHANGED
|
@@ -102,6 +102,7 @@ export {
|
|
|
102
102
|
UITabs, type UITabDef,
|
|
103
103
|
defineTabs, type TabDef, type TabsHandle,
|
|
104
104
|
UIWidget, type UIWidgetStyle,
|
|
105
|
+
UIOverlay, type UIWidgetContent,
|
|
105
106
|
UIModal, type UIModalStyle,
|
|
106
107
|
UIBottomSheet, type UIBottomSheetStyle,
|
|
107
108
|
UIPopover, type UIPopoverStyle,
|
package/src/runtime/misc.ts
CHANGED
|
@@ -18,3 +18,7 @@ export type SvgSourceValue = {
|
|
|
18
18
|
|
|
19
19
|
/** Wrap raw SVG XML so it can be used as an image source. */
|
|
20
20
|
export const SvgSource = (svg: string): SvgSourceValue => ({ svg, tintColor: null })
|
|
21
|
+
/** The type of the value: what `SvgSource(xml)`, `assetIcon("lucide:bell")` and `asset("./logo.svg")`
|
|
22
|
+
* all return, and what `UIImage` / `bgImage` take — ONE name for an SVG image source, global to a
|
|
23
|
+
* project like the function (host.d.ts names it for the macros). */
|
|
24
|
+
export type SvgSource = SvgSourceValue
|
package/src/ui/NativeView.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// wrapper convention.
|
|
11
11
|
|
|
12
12
|
import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
|
|
13
|
+
import { _setSurfaceContent, type LayerElement, type UIWidgetContent } from "./UILayer"
|
|
13
14
|
import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "./presentable"
|
|
14
15
|
import { _transitionId, TRANSITION_NONE } from "./transitions"
|
|
15
16
|
import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
|
|
@@ -45,6 +46,8 @@ export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableSty
|
|
|
45
46
|
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
46
47
|
on(event: string, callback: (data?: any) => void): this
|
|
47
48
|
off(event: string, callback: (data?: any) => void): this
|
|
49
|
+
/** The UI over the view when presented: widgets over it (`Presentable.setContent`). */
|
|
50
|
+
setContent(content: UIWidgetContent): this
|
|
48
51
|
/** @internal The destination tuple (Presentable). */
|
|
49
52
|
_dest(): DestTuple
|
|
50
53
|
}
|
|
@@ -85,6 +88,14 @@ export class NativeViewElement extends Element<"native"> implements Presentable
|
|
|
85
88
|
|
|
86
89
|
/** @internal */
|
|
87
90
|
_dest(): DestTuple { return [TREE_DEST_NATIVE, this._viewId, this.viewName, this._paramsJson] }
|
|
91
|
+
/** @internal The view's content layer (ui/UILayer.ts), made by the first setContent. */
|
|
92
|
+
_contentLayer?: LayerElement
|
|
93
|
+
/** The UI over the view when it is presented full screen — widgets (a header, a card, a sheet
|
|
94
|
+
* over a map), laid out over it; touches outside a widget's box reach the view. */
|
|
95
|
+
setContent(content: UIWidgetContent): this {
|
|
96
|
+
_setSurfaceContent(this, content)
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
88
99
|
|
|
89
100
|
open(options?: PresentOptions): void {
|
|
90
101
|
if (!tree().isViewSupported(this.viewName)) {
|
package/src/ui/UI.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { UIVideo, type UIVideoStyle } from './UIVideo'
|
|
|
10
10
|
export { UIInput, UITextArea, type UIInputStyle } from './UIInput'
|
|
11
11
|
|
|
12
12
|
export { UIWidget, type UIWidgetStyle } from './UIWidget'
|
|
13
|
+
export { UIOverlay, type UIWidgetContent } from './UILayer'
|
|
13
14
|
export { UIModal, type UIModalStyle } from './UIModal'
|
|
14
15
|
export { UIBottomSheet, type UIBottomSheetStyle } from './UIBottomSheet'
|
|
15
16
|
export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopover'
|
package/src/ui/UIBottomSheet.ts
CHANGED
|
@@ -67,6 +67,7 @@ export class BottomSheetElement extends ModalElement {
|
|
|
67
67
|
// The inherited show/hide pose animates ONLY the scrim: sheet geometry rides the
|
|
68
68
|
// sheetDetent style key, never the pose system.
|
|
69
69
|
this._transition = { duration: SHEET_ANIM_MS }
|
|
70
|
+
this._syncPose()
|
|
70
71
|
this._addFlags(TREE_FLAG_DETENT)
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -97,7 +98,7 @@ export class BottomSheetElement extends ModalElement {
|
|
|
97
98
|
setDetent(index: number): void {
|
|
98
99
|
const clamped = Math.max(0, Math.min(Math.floor(index), this._detents.length - 1))
|
|
99
100
|
this._detent = clamped
|
|
100
|
-
if (!this.
|
|
101
|
+
if (!this._shown) return // applied by the next show()
|
|
101
102
|
this.style({ sheetDetent: clamped } as UIBottomSheetStyle)
|
|
102
103
|
for (const callback of this._detentListeners) callback(clamped)
|
|
103
104
|
}
|
|
@@ -107,20 +108,20 @@ export class BottomSheetElement extends ModalElement {
|
|
|
107
108
|
return this
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
show(): void {
|
|
111
|
-
if (this.
|
|
111
|
+
override show(): void {
|
|
112
|
+
if (this._shown) return
|
|
112
113
|
// Before the mount, so the built node carries the target and hosts play the entrance to it.
|
|
113
114
|
this.style({ sheetDetent: this._detent } as UIBottomSheetStyle)
|
|
114
115
|
super.show()
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
hide(): void {
|
|
118
|
-
if (!this.
|
|
119
|
-
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out;
|
|
118
|
+
override hide(): void {
|
|
119
|
+
if (!this._shown) return
|
|
120
|
+
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out; the hide follows
|
|
120
121
|
super.hide()
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
dismissible(enabled: boolean): this {
|
|
124
|
+
override dismissible(enabled: boolean): this {
|
|
124
125
|
super.dismissible(enabled)
|
|
125
126
|
this.style({ sheetDismissible: enabled ? 1 : 0 } as UIBottomSheetStyle)
|
|
126
127
|
return this
|