lecodes-sdk 0.19.2 → 0.20.2

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 (67) hide show
  1. package/dist/global.d.ts +62 -0
  2. package/dist/host.d.ts +3 -0
  3. package/dist/types/audio/Bus.d.ts +45 -0
  4. package/dist/types/audio/Sound.d.ts +28 -0
  5. package/dist/types/audio/Voice.d.ts +27 -0
  6. package/dist/types/audio/audio.d.ts +83 -0
  7. package/dist/types/audio/support.d.ts +1 -0
  8. package/dist/types/gl/AudioSource.d.ts +60 -0
  9. package/dist/types/gl/AudioZone.d.ts +32 -0
  10. package/dist/types/gl/DecalSet.d.ts +103 -0
  11. package/dist/types/gl/Geometry.d.ts +5 -0
  12. package/dist/types/gl/Light.d.ts +7 -0
  13. package/dist/types/gl/Locomotion.d.ts +3 -1
  14. package/dist/types/gl/Material.d.ts +86 -2
  15. package/dist/types/gl/Mesh.d.ts +11 -0
  16. package/dist/types/gl/Scene.d.ts +23 -0
  17. package/dist/types/gl/SceneAudio.d.ts +11 -0
  18. package/dist/types/gl/Texture.d.ts +29 -1
  19. package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
  20. package/dist/types/gl/animation/Animator.d.ts +51 -183
  21. package/dist/types/gl/animation/Feet.d.ts +85 -0
  22. package/dist/types/gl/animation/Warp.d.ts +53 -0
  23. package/dist/types/gl/animation/core.d.ts +61 -17
  24. package/dist/types/gl/state.d.ts +0 -1
  25. package/dist/types/inject.d.ts +17 -2
  26. package/dist/types/plugins/map.d.ts +174 -0
  27. package/dist/types/runtime/input.d.ts +11 -0
  28. package/dist/types/ui/UIImage.d.ts +15 -5
  29. package/dist/types.json +1 -1
  30. package/package.json +1 -1
  31. package/src/audio/Bus.ts +102 -0
  32. package/src/audio/Sound.ts +96 -0
  33. package/src/audio/Voice.ts +102 -0
  34. package/src/audio/audio.ts +161 -0
  35. package/src/audio/support.ts +6 -0
  36. package/src/bridges.d.ts +1481 -1345
  37. package/src/compile/__tests__/compile.test.ts +12 -0
  38. package/src/compile/compileProject.ts +35 -15
  39. package/src/compile/index.ts +3 -0
  40. package/src/core/Aspect.ts +34 -9
  41. package/src/g2/Scene2D.ts +7 -0
  42. package/src/gl/AudioSource.ts +113 -0
  43. package/src/gl/AudioZone.ts +75 -0
  44. package/src/gl/DecalSet.ts +233 -0
  45. package/src/gl/Geometry.ts +5 -0
  46. package/src/gl/Light.ts +16 -0
  47. package/src/gl/Lightmap.ts +3 -2
  48. package/src/gl/Locomotion.ts +7 -5
  49. package/src/gl/Material.ts +152 -4
  50. package/src/gl/Mesh.ts +20 -1
  51. package/src/gl/Particles.ts +3 -3
  52. package/src/gl/Scene.ts +42 -8
  53. package/src/gl/SceneAudio.ts +26 -0
  54. package/src/gl/Texture.ts +43 -3
  55. package/src/gl/Vehicle.ts +5 -5
  56. package/src/gl/animation/AnimationClip.ts +43 -20
  57. package/src/gl/animation/Animator.ts +138 -329
  58. package/src/gl/animation/Feet.ts +134 -0
  59. package/src/gl/animation/Loop.ts +3 -1
  60. package/src/gl/animation/Warp.ts +96 -0
  61. package/src/gl/animation/core.ts +741 -670
  62. package/src/gl/state.ts +6 -6
  63. package/src/host.d.ts +3 -0
  64. package/src/inject.ts +23 -2
  65. package/src/plugins/map.ts +396 -0
  66. package/src/runtime/input.ts +6 -1
  67. package/src/ui/UIImage.ts +21 -7
package/src/gl/state.ts CHANGED
@@ -1,6 +1,6 @@
1
- // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
- // here by Scene.open()/close() and read by the touch dispatch system.
3
-
4
- import type { Scene } from "./Scene"
5
-
6
- export const glState: { activeScene: Scene | null, frame: number } = { activeScene: null, frame: 0 }
1
+ // Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
2
+ // here by Scene.open()/close() and read by the touch dispatch system.
3
+
4
+ import type { Scene } from "./Scene"
5
+
6
+ export const glState: { activeScene: Scene | null } = { activeScene: null }
package/src/host.d.ts CHANGED
@@ -29,6 +29,9 @@ declare global {
29
29
  error(...data: any[]): void
30
30
  }
31
31
 
32
+ /** Compile-time macro: `asset('./data.json')` yields the file's PARSED data (a .json file is
33
+ * data, not code — it ships inside the bundle). See the `string` overload for everything else. */
34
+ function asset(path: `${string}.json`): any
32
35
  /** Compile-time macro: `asset('./hero.png')` is desugared by the bundler into the module import
33
36
  * for that resource. The file must exist — a path that resolves to nothing fails the compile
34
37
  * (`asset not found: ./hero.png (main.ts:3)`); there is no runtime fallback. Calls inside
package/src/inject.ts CHANGED
@@ -44,6 +44,12 @@ export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, t
44
44
  // only ever imports its server functions (docs/backend-plan.md §4).
45
45
  export { __rpc, __channel, __serverOnly, RpcError, type ClientChannel } from "./runtime/rpc"
46
46
  export { AudioPlayer, VideoPlayer } from "./runtime/media"
47
+ // Game audio (docs/audio-plan.md): decoded clips, a voice pool, buses with effects; 3D through the
48
+ // AudioSource / AudioZone aspects (gl/) and scene.audio.
49
+ export { Sound, type SoundOptions } from "./audio/Sound"
50
+ export { Voice, type VoiceEvents } from "./audio/Voice"
51
+ export { Bus, REVERB_PRESETS, type ReverbPreset, type ReverbParams, type EchoParams } from "./audio/Bus"
52
+ export { audio, type PlaySoundOptions, type SpatialOptions, type Rolloff, type AudioStats } from "./audio/audio"
47
53
  export { openFilePicker, files, type FileData } from "./runtime/files"
48
54
  export { share } from "./runtime/share"
49
55
  export { ClickEvent, TouchStartEvent, LongPressEvent } from "./runtime/touch"
@@ -125,6 +131,11 @@ export { easeIn, easeOut, easeInOut } from "./animate/easings"
125
131
  export { QRScanner } from "./plugins/qr"
126
132
  export { CameraView, type CameraFacing } from "./plugins/camera"
127
133
  export { Geolocation, type GeoPosition, type GeoOptions, type GeoWatch } from "./plugins/geolocation"
134
+ export {
135
+ MapView, type MapOptions, type MapStyle, type MapStyleName, type MapCamera, type MapTap, type LngLat, type CameraMove, type FitOptions,
136
+ type MapPaddingValues, type MarkerItem, type MarkerTap, type MarkerLayer, type MarkerLayerOptions,
137
+ type LineLayer, type LineLayerOptions, type UserLocationOptions,
138
+ } from "./plugins/map"
128
139
  export { Push, type PushPayload, type PushStatus, type PushRegisterOptions, type PushEvent } from "./plugins/push"
129
140
  export { OAuth, type OAuthCredential, type OAuthProviderName } from "./plugins/oauth"
130
141
  export { Service } from "./plugins/service"
@@ -163,6 +174,9 @@ export { Model } from "./gl/Model"
163
174
  export { Shape } from "./gl/Shape"
164
175
  export { Physics } from "./gl/Physics"
165
176
  export { Trigger } from "./gl/Trigger"
177
+ export { AudioSource, type AudioCone } from "./gl/AudioSource"
178
+ export { AudioZone } from "./gl/AudioZone"
179
+ export { SceneAudio } from "./gl/SceneAudio"
166
180
  export { CharacterController } from "./gl/CharacterController"
167
181
  export { Vehicle } from "./gl/Vehicle"
168
182
  export type { DriveLayout, DifferentialMode, EngineConfig, SteeringConfig, AeroConfig } from "./gl/Vehicle"
@@ -174,11 +188,15 @@ export { Gearbox } from "./gl/Gearbox"
174
188
  export { AnimationClip } from "./gl/animation/AnimationClip"
175
189
  export type { ClipDef, ClipTrackDef, ClipKey } from "./gl/animation/AnimationClip"
176
190
  export { Animator } from "./gl/animation/Animator"
177
- export type { WarpOptions, FeetOptions, FootState } from "./gl/animation/Animator"
191
+ export type { LodMode } from "./gl/animation/Animator"
192
+ export { Feet } from "./gl/animation/Feet"
193
+ export type { FeetOptions, FootState } from "./gl/animation/Feet"
194
+ export { Warp } from "./gl/animation/Warp"
195
+ export type { WarpOptions, StepWarpOptions } from "./gl/animation/Warp"
178
196
  export { Playback } from "./gl/animation/Playback"
179
197
  export { Loop } from "./gl/animation/Loop"
180
198
  export { Layer } from "./gl/animation/Layer"
181
- export type { PlayOptions, StopOptions, LayerOptions, LoopOptions, LoopDef, BlendPosition, ActiveClip } from "./gl/animation/core"
199
+ export type { PlayOptions, StopOptions, LayerOptions, LoopOptions, LoopDef, BlendPosition, BlendMember, ActiveClip, ClipInfo, ClipCycle, ClipAlign, KneeAxisReport, StepHandler, ClipEventHandler } from "./gl/animation/core"
182
200
  export { IK } from "./gl/IK"
183
201
  export type { IKTwoBone, IKLookAt } from "./gl/IK"
184
202
  export { Light } from "./gl/Light"
@@ -196,8 +214,11 @@ export { Geometry } from "./gl/Geometry"
196
214
  export { InstancedMesh } from "./gl/InstancedMesh"
197
215
  export type { InstanceTransform } from "./gl/InstancedMesh"
198
216
  export { Texture } from "./gl/Texture"
217
+ export type { TextureLoadOptions } from "./gl/Texture"
199
218
  export { Camera } from "./gl/Camera"
200
219
  export { Ray } from "./gl/Ray"
201
220
  export { Plane } from "./gl/Plane"
202
221
  export { Noise } from "./gl/Noise"
203
222
  export { Particles, Trail, curve, colorCurve } from "./gl/Particles"
223
+ export { DecalSet } from "./gl/DecalSet"
224
+ export type { DecalOptions, DecalSetOptions, DecalSpawnOptions, DecalPlacement } from "./gl/DecalSet"
@@ -0,0 +1,396 @@
1
+ // MapView — a vector map (maplibre-native on iOS/Android) as a Presentable. A typed wrapper over
2
+ // the `NativeView("map")` capability (docs/map-plugin-plan.md): push it (`Router.push(map)`),
3
+ // open it fullscreen (`map.open()`), or embed it among a screen's children where the host builds
4
+ // the "native" layout node. The style is a URL, a bundled `asset()`, a read file or the style
5
+ // object (see MapStyle). Data goes in as named layers (`map.markers(name)`, `map.line(name)`)
6
+ // whose `set()` replaces their content; the camera is driven with `flyTo` / `fitPoints`; the user
7
+ // puck is fed by the app (`setUserLocation`) — the plugin never touches the location hardware.
8
+ //
9
+ // Host-OPTIONAL: it exists where the host registers a "map" view (the `map` plugin from
10
+ // lecodes-plugins) — gate on `MapView.isSupported`. Everything here is sugar over ONE JSON channel
11
+ // (`call` / `on`); the wire contract is the method/event table in docs/map-plugin-plan.md.
12
+ //
13
+ // Ordering: the native map exists only once presented and its style loaded — the plugin emits
14
+ // `ready` then. Data and camera calls issued before that are queued and replayed in order, so an
15
+ // app can build the map declaratively and push it later without a ready-dance of its own.
16
+
17
+ import { NativeView, NativeViewElement } from "../ui/NativeView"
18
+ import { _channelOn } from "../runtime/channel"
19
+ import type { FetchResponse } from "../runtime/fetch"
20
+
21
+ /** The ready-made styles a map falls back on — [OpenFreeMap](https://openfreemap.org): OSM data,
22
+ * no key, no registration, no request limits, commercial use allowed, and the whole stack is
23
+ * open-source if you'd rather self-host. `liberty` is the default. Credit them (and OSM) in your
24
+ * app: "© OpenFreeMap © OpenMapTiles, data from OpenStreetMap". A public free instance is a
25
+ * donation-funded service with no SLA — for a product with real traffic, run your own tiles and
26
+ * pass that style's URL instead. */
27
+ export type MapStyleName = "liberty" | "bright" | "positron" | "dark" | "fiord"
28
+
29
+ const NAMED_STYLES: Record<string, string> = {
30
+ liberty: "https://tiles.openfreemap.org/styles/liberty",
31
+ bright: "https://tiles.openfreemap.org/styles/bright",
32
+ positron: "https://tiles.openfreemap.org/styles/positron",
33
+ dark: "https://tiles.openfreemap.org/styles/dark",
34
+ fiord: "https://tiles.openfreemap.org/styles/fiord",
35
+ }
36
+
37
+ /**
38
+ * Where the map's style comes from:
39
+ *
40
+ * - a **name** — `"liberty"` (the default), `"positron"`, … see {@link MapStyleName}: a ready-made
41
+ * style on a free public tile server, so `MapView()` alone already draws a world map;
42
+ * - a **URL** — `"https://tiles.example.com/styles/city/style.json"`, the map fetches it;
43
+ * - a **bundled style** — `asset("./map/style.json")`: the file ships inside the app and the
44
+ * wrapper hands its text to the map, so the style itself needs no server (tiles, sprites and
45
+ * glyphs are still fetched from whatever urls it names);
46
+ * - an **already-read file** — a `FetchResponse` from `fetchLocal("style.json")` or
47
+ * `await fetch(url)` (a style downloaded once and cached in `files`);
48
+ * - the **style object** itself — the natural way to substitute a tile-server address at runtime:
49
+ * `{ ...style, sources: { openmaptiles: { type: "vector", url: `${server}/data/v3.json` } } }`.
50
+ *
51
+ * Whichever form: **every url INSIDE the style (`sources[].url`, `sprite`, `glyphs`) must be
52
+ * absolute.** maplibre-native, unlike maplibre-gl-js, resolves no relative ones — a style with
53
+ * them loads to an empty basemap (your layers still draw). A tileserver-gl instance emits
54
+ * relative urls until its `publicUrl` is configured.
55
+ */
56
+ // `string & {}` keeps the named suggestions in the editor while still accepting any URL.
57
+ export type MapStyle = MapStyleName | (string & {}) | FetchResponse | object
58
+
59
+ /** `[longitude, latitude]` — GeoJSON order, the same as the style, the tiles and your data. */
60
+ export type LngLat = [number, number]
61
+
62
+ export interface MapCamera {
63
+ center: LngLat
64
+ zoom: number
65
+ /** Degrees clockwise from north. */
66
+ bearing: number
67
+ /** Degrees from the vertical. */
68
+ pitch: number
69
+ }
70
+
71
+ export interface MapOptions {
72
+ /** The MapLibre style: a ready-made name (`"liberty"` — the default, `"positron"`, …), a URL,
73
+ * a style bundled with the app (`asset("./style.json")`), an already-read file
74
+ * (`fetchLocal("style.json")`, `await fetch(url)`) or the style object itself — see
75
+ * {@link MapStyle}. */
76
+ style?: MapStyle
77
+ center?: LngLat
78
+ zoom?: number
79
+ minZoom?: number
80
+ maxZoom?: number
81
+ bearing?: number
82
+ pitch?: number
83
+ /** Two-finger rotate gesture (default true). */
84
+ rotate?: boolean
85
+ /** Two-finger tilt gesture (default false — most city maps stay flat). */
86
+ tilt?: boolean
87
+ }
88
+
89
+ /** A tap on the map itself — not on a feature of a managed layer. */
90
+ export interface MapTap {
91
+ lngLat: LngLat
92
+ /** View-space point, px. */
93
+ point: [number, number]
94
+ }
95
+
96
+ export interface CameraMove {
97
+ zoom?: number
98
+ bearing?: number
99
+ pitch?: number
100
+ /** Animation length, ms (flyTo only; default 600). */
101
+ duration?: number
102
+ }
103
+
104
+ export interface MapPaddingValues {
105
+ top?: number | string
106
+ left?: number | string
107
+ bottom?: number | string
108
+ right?: number | string
109
+ }
110
+
111
+ export interface FitOptions {
112
+ /** Px around the points, or per edge. Added to the view padding set by `setPadding`. */
113
+ padding?: number | MapPaddingValues
114
+ maxZoom?: number
115
+ /** Default true. */
116
+ animate?: boolean
117
+ }
118
+
119
+ /** One marker. `id` comes back in `onTap`; `icon` names an image of the style's sprite; `color` /
120
+ * `title` feed the default layers; extra keys become feature properties. */
121
+ export interface MarkerItem {
122
+ id: string | number
123
+ lngLat: LngLat
124
+ icon?: string
125
+ color?: string
126
+ title?: string
127
+ [property: string]: any
128
+ }
129
+
130
+ export interface MarkerTap {
131
+ id: string | number
132
+ lngLat: LngLat
133
+ /** Every property of the tapped feature (the item's keys, `id` and `lngLat` excluded). */
134
+ properties: Record<string, any>
135
+ }
136
+
137
+ export interface MarkerLayerOptions {
138
+ /** Group nearby markers into clusters (a cluster tap zooms in). Default false. */
139
+ cluster?: boolean
140
+ /** Cluster radius, px (default 50). */
141
+ clusterRadius?: number
142
+ /** Zoom at which clusters stop forming (default: maxZoom − 1). */
143
+ clusterMaxZoom?: number
144
+ }
145
+
146
+ export interface LineLayerOptions {
147
+ color?: string
148
+ /** Px (default 4). */
149
+ width?: number
150
+ /** 0–1 (default 1). */
151
+ opacity?: number
152
+ }
153
+
154
+ export interface UserLocationOptions {
155
+ /** Horizontal accuracy radius, meters — drawn as the halo around the dot. */
156
+ accuracy?: number | null
157
+ /** Degrees clockwise from north — drawn as the direction wedge; null hides it. */
158
+ heading?: number | null
159
+ }
160
+
161
+ export interface MarkerLayer {
162
+ readonly name: string
163
+ /** Replace the layer's markers. */
164
+ set(items: MarkerItem[]): this
165
+ clear(): this
166
+ /** A marker (or any feature of this layer's source) was tapped. */
167
+ onTap(callback: (marker: MarkerTap) => void): this
168
+ }
169
+
170
+ export interface LineLayer {
171
+ readonly name: string
172
+ /** Replace the line with these vertices. */
173
+ set(coordinates: LngLat[]): this
174
+ clear(): this
175
+ }
176
+
177
+ export interface MapView extends NativeView {
178
+ /** The style loaded and the map is interactive (queued calls have been replayed). */
179
+ onReady(callback: () => void): this
180
+ /** A tap that hit no feature of a managed layer. */
181
+ onTap(callback: (tap: MapTap) => void): this
182
+ /** The camera settled after a gesture or an animation. */
183
+ onMove(callback: (camera: MapCamera) => void): this
184
+ /** The map reported a problem — a style that wouldn't load, a source it couldn't reach. Never
185
+ * fatal; with no handler the message goes to `console.error`, so it is never silent. */
186
+ onError(callback: (error: { message: string }) => void): this
187
+
188
+ /** A named marker layer (one GeoJSON source). If the style already declares a source with this
189
+ * name, its layers are used as-is and only the data is pushed; otherwise the plugin creates the
190
+ * source and default marker layers (colored dot, `icon`, `title` label; clusters on request). */
191
+ markers(name: string, options?: MarkerLayerOptions): MarkerLayer
192
+ /** A named line layer (one GeoJSON source) — same style-first rule as `markers`. */
193
+ line(name: string, options?: LineLayerOptions): LineLayer
194
+ /** Raw escape hatch: replace the data of any GeoJSON source in the style. */
195
+ setData(source: string, geojson: object): this
196
+
197
+ flyTo(center: LngLat, options?: CameraMove): this
198
+ jumpTo(center: LngLat, options?: CameraMove): this
199
+ /** Fit the camera to these points (padding + the view padding respected). */
200
+ fitPoints(points: LngLat[], options?: FitOptions): this
201
+ /** Content inset: the part of the view covered by your UI (`"40%"` = of the view's size).
202
+ * Camera operations center inside the remaining area. */
203
+ setPadding(padding: MapPaddingValues): this
204
+ getCamera(): Promise<MapCamera>
205
+
206
+ /** Move the user puck (the map draws it; the position comes from you — `Geolocation.watch`).
207
+ * `null` hides it. */
208
+ setUserLocation(lngLat: LngLat | null, options?: UserLocationOptions): this
209
+ }
210
+
211
+ type Queued = { method: string, args: any[] }
212
+ type LayerSpec = { kind: "markers" | "line", options: object }
213
+
214
+ /** Methods whose effect lives in the STYLE, not in the view: a style (re)load wipes them, so the
215
+ * wrapper keeps their last value and re-applies it on every `ready` instead of replaying them
216
+ * from the queue. Camera calls are not here — the camera survives a style reload. */
217
+ const STATEFUL = ["ensureLayer", "setData", "setPadding", "setUserLocation"]
218
+
219
+ class MapViewElement extends NativeViewElement {
220
+ private _ready = false
221
+ private _queue: Queued[] = []
222
+ private readonly _layers = new Map<string, MapLayerHandle>()
223
+ /** Style-lifetime state, re-applied on every `ready` (see STATEFUL). */
224
+ private readonly _specs = new Map<string, LayerSpec>()
225
+ private readonly _data = new Map<string, object>()
226
+ private _padding?: MapPaddingValues
227
+ private _user?: any[]
228
+ private readonly _readyCallbacks: (() => void)[] = []
229
+ private readonly _tapCallbacks: ((tap: MapTap) => void)[] = []
230
+ private readonly _moveCallbacks: ((camera: MapCamera) => void)[] = []
231
+ private readonly _errorCallbacks: ((error: { message: string }) => void)[] = []
232
+
233
+ constructor(options: MapOptions = {}) {
234
+ super("map", _styleParams(options))
235
+ _channelOn(this.nvl, "ready", () => this._onReady())
236
+ _channelOn(this.nvl, "tap", (data: any) => this._onTap(data))
237
+ _channelOn(this.nvl, "moveEnd", (data: any) => { for (const cb of [...this._moveCallbacks]) cb(data) })
238
+ _channelOn(this.nvl, "error", (data: any) => this._onError(data))
239
+ }
240
+
241
+ onReady(callback: () => void): this { this._readyCallbacks.push(callback); return this }
242
+ onTap(callback: (tap: MapTap) => void): this { this._tapCallbacks.push(callback); return this }
243
+ onMove(callback: (camera: MapCamera) => void): this { this._moveCallbacks.push(callback); return this }
244
+ onError(callback: (error: { message: string }) => void): this { this._errorCallbacks.push(callback); return this }
245
+
246
+ markers(name: string, options: MarkerLayerOptions = {}): MarkerLayer {
247
+ return this._layer(name, "markers", options) as MarkerLayer
248
+ }
249
+ line(name: string, options: LineLayerOptions = {}): LineLayer {
250
+ return this._layer(name, "line", options) as LineLayer
251
+ }
252
+ setData(source: string, geojson: object): this {
253
+ this._data.set(source, geojson)
254
+ this._send("setData", [source, geojson])
255
+ return this
256
+ }
257
+
258
+ flyTo(center: LngLat, options: CameraMove = {}): this { this._send("flyTo", [center, options]); return this }
259
+ jumpTo(center: LngLat, options: CameraMove = {}): this { this._send("jumpTo", [center, options]); return this }
260
+ fitPoints(points: LngLat[], options: FitOptions = {}): this { this._send("fitPoints", [points, options]); return this }
261
+ setPadding(padding: MapPaddingValues): this { this._padding = padding; this._send("setPadding", [padding]); return this }
262
+ getCamera(): Promise<MapCamera> { return this.call("getCamera") }
263
+
264
+ setUserLocation(lngLat: LngLat | null, options: UserLocationOptions = {}): this {
265
+ this._user = [lngLat, options]
266
+ this._send("setUserLocation", [lngLat, options])
267
+ return this
268
+ }
269
+
270
+ /** @internal queue until `ready`, then straight through. Rejections are logged, never thrown:
271
+ * a fire-and-forget camera/data call has no caller to reject to. */
272
+ _send(method: string, args: any[]): void {
273
+ if (!this._ready) { this._queue.push({ method, args }); return }
274
+ this.call(method, ...args).catch((e: Error) => console.error(`MapView.${method}: ${e.message}`))
275
+ }
276
+
277
+ private _layer(name: string, kind: "markers" | "line", options: object): MapLayerHandle {
278
+ let layer = this._layers.get(name)
279
+ if (!layer) {
280
+ layer = new MapLayerHandle(this, name)
281
+ this._layers.set(name, layer)
282
+ this._specs.set(name, { kind, options })
283
+ this._send("ensureLayer", [name, kind, options])
284
+ }
285
+ return layer
286
+ }
287
+
288
+ /** A style load — the first one or a reload — starts from a style that knows nothing about the
289
+ * app's layers, so re-apply them all, then whatever else waited in the queue. Without this a
290
+ * second `ready` (a host that loads its default style before the app's, a style swap) leaves a
291
+ * correct-looking map with no data on it. */
292
+ private _onReady(): void {
293
+ this._ready = true
294
+ const queue = this._queue
295
+ this._queue = []
296
+ for (const [name, spec] of this._specs) this._send("ensureLayer", [name, spec.kind, spec.options])
297
+ for (const [source, geojson] of this._data) this._send("setData", [source, geojson])
298
+ if (this._padding) this._send("setPadding", [this._padding])
299
+ if (this._user) this._send("setUserLocation", this._user)
300
+ for (const q of queue) if (STATEFUL.indexOf(q.method) < 0) this._send(q.method, q.args)
301
+ for (const cb of [...this._readyCallbacks]) cb()
302
+ }
303
+
304
+ private _onError(data: any): void {
305
+ // Unhandled is not unheard: a style the host couldn't load is exactly the failure that looks
306
+ // like "the map is blank and nothing happened".
307
+ if (this._errorCallbacks.length === 0) { console.error(`MapView: ${data?.message ?? "unknown error"}`); return }
308
+ for (const cb of [...this._errorCallbacks]) cb(data)
309
+ }
310
+
311
+ private _onTap(data: any): void {
312
+ const feature = data?.feature
313
+ const layer = feature ? this._layers.get(feature.source) : undefined
314
+ if (layer) {
315
+ layer._tap({ id: feature.id, lngLat: data.lngLat, properties: feature.properties ?? {} })
316
+ return
317
+ }
318
+ for (const cb of [...this._tapCallbacks]) cb({ lngLat: data.lngLat, point: data.point })
319
+ }
320
+ }
321
+
322
+ /** One named GeoJSON source on the map — the object behind both `MarkerLayer` and `LineLayer`. */
323
+ class MapLayerHandle {
324
+ private readonly _tapCallbacks: ((marker: MarkerTap) => void)[] = []
325
+ private readonly _map: MapViewElement
326
+ readonly name: string
327
+
328
+ constructor(map: MapViewElement, name: string) {
329
+ this._map = map
330
+ this.name = name
331
+ }
332
+
333
+ set(items: MarkerItem[] | LngLat[]): this {
334
+ this._map.setData(this.name, _toGeoJson(items))
335
+ return this
336
+ }
337
+ clear(): this {
338
+ this._map.setData(this.name, { type: "FeatureCollection", features: [] })
339
+ return this
340
+ }
341
+ onTap(callback: (marker: MarkerTap) => void): this {
342
+ this._tapCallbacks.push(callback)
343
+ return this
344
+ }
345
+ /** @internal */
346
+ _tap(marker: MarkerTap): void {
347
+ for (const cb of [...this._tapCallbacks]) cb(marker)
348
+ }
349
+ }
350
+
351
+ /** @internal `MapOptions` → host params. A style URL stays a url the map fetches itself; every
352
+ * other form is read HERE and crosses as `styleJson` text — one string, once, at creation. A
353
+ * bundled `asset()` is `"id:N"` in shell compiles (the host already holds the bytes) and a plain
354
+ * url in server compiles, so the same call works on every host. */
355
+ export const _styleParams = (options: MapOptions = {}): any => {
356
+ const { style = "liberty", ...rest } = options
357
+ if (typeof style === "string") {
358
+ if (!style.startsWith("id:")) return { ...rest, style: NAMED_STYLES[style] ?? style }
359
+ const text = _creatorUtils.fetchToText(+style.slice(3))
360
+ if (!text) throw new Error("MapView: the style asset is empty or missing")
361
+ return { ...rest, styleJson: text }
362
+ }
363
+ const response = style as FetchResponse
364
+ return { ...rest, styleJson: typeof response.text === "function" ? response.text() : JSON.stringify(style) }
365
+ }
366
+
367
+ /** @internal marker items → a FeatureCollection of Points; a coordinate list → one LineString. */
368
+ export const _toGeoJson = (items: MarkerItem[] | LngLat[]): object => {
369
+ if (items.length > 0 && Array.isArray(items[0])) {
370
+ return { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: items } }
371
+ }
372
+ return {
373
+ type: "FeatureCollection",
374
+ features: (items as MarkerItem[]).map(item => {
375
+ const { id, lngLat, ...properties } = item
376
+ return { type: "Feature", id, properties: { ...properties, id }, geometry: { type: "Point", coordinates: lngLat } }
377
+ }),
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Create a map view. `MapView.isSupported` reports whether this host registered a "map" view —
383
+ * check it before offering the feature (web, headless and shells without the plugin have none).
384
+ */
385
+ // PURE IIFE so an app that never uses the map tree-shakes the whole plugin away (see NativeView).
386
+ export const MapView: {
387
+ (options?: MapOptions): MapView
388
+ /** Whether this host registered a "map" view. */
389
+ readonly isSupported: boolean
390
+ } = /*#__PURE__*/ (() => {
391
+ const factory = (options: MapOptions = {}): MapView => new MapViewElement(options) as unknown as MapView
392
+ Object.defineProperty(factory, "isSupported", {
393
+ get: (): boolean => NativeView.isSupported("map"),
394
+ })
395
+ return factory as any
396
+ })()
@@ -12,7 +12,7 @@
12
12
  /** Continuous channel ids — the `_creatorUtils.inputRead` contract (bridges.d.ts). */
13
13
  export const InputChannel = {
14
14
  MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
15
- GamepadCount: 7,
15
+ GamepadCount: 7, PointerOnUI: 8,
16
16
  GamepadBase: 16, GamepadStride: 16,
17
17
  PadConnected: 0, PadLeftX: 1, PadLeftY: 2, PadRightX: 3, PadRightY: 4, PadLeftTrigger: 5, PadRightTrigger: 6,
18
18
  } as const
@@ -94,6 +94,11 @@ export const mouse = {
94
94
  * gesture (call it in a 'keydown' handler for MouseLeft); hosts drop it on focus loss and
95
95
  * re-acquire on focus. Escape does NOT unlock by itself on native hosts — call `unlock()`. */
96
96
  get locked(): boolean { return read(InputChannel.PointerLocked) !== 0 },
97
+ /** The primary button went down on a UI element (a button, a scrollable, an input, a modal
98
+ * backdrop) and is still held. Stays true wherever the cursor drifts until the release, so
99
+ * `if (!Input.mouse.onUI) look(Input.mouse.delta)` never turns a HUD drag into camera look.
100
+ * Always false while `locked`. A press that fell through to the scene reads false. */
101
+ get onUI(): boolean { return read(InputChannel.PointerOnUI) !== 0 },
97
102
  lock(): boolean { return _creatorUtils.inputSetPointerLock?.(true) ?? false },
98
103
  unlock(): void { _creatorUtils.inputSetPointerLock?.(false) },
99
104
  }
package/src/ui/UIImage.ts CHANGED
@@ -2,6 +2,7 @@ import type { FetchResponse } from "../runtime/fetch"
2
2
  import type { SvgSourceValue as SvgSource } from "../runtime/misc"
3
3
  import type { File } from "../runtime/fetch"
4
4
  import { Canvas, isCanvas } from "../canvas/Canvas"
5
+ import type { Scene2D } from "../g2/Scene2D"
5
6
  import { Element, type Color, type ElementStyle, type UIElementBase } from "./UINode"
6
7
 
7
8
  export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
@@ -10,19 +11,32 @@ export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "
10
11
  * docs/canvas-contract.md ("Pending"). */
11
12
  export type CanvasImageSource = { canvasSurface: number }
12
13
 
13
- /** Accepted image sources. A Canvas is baked to a surface marker on assign. */
14
- export type ImageSource = string | FetchResponse | File | SvgSource | Canvas
14
+ /** A live 2D scene as an image source: the host draws the scene into the node's box every frame
15
+ * (the scene's camera, projected into the box). No intrinsic size — give the node a width and
16
+ * height. */
17
+ export type Scene2DImageSource = { scene2d: number }
15
18
 
16
- const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | CanvasImageSource =>
19
+ /** Accepted image sources. A Canvas is baked to a surface marker on assign; a Scene2D becomes a
20
+ * live-scene marker. */
21
+ export type ImageSource = string | FetchResponse | File | SvgSource | Canvas | Scene2D
22
+
23
+ // An engine object that knows how to present itself as an image source (Scene2D._imageSrc). Duck-typed
24
+ // so the UI layer never imports an engine class — a UI-only bundle carries no engine bridge.
25
+ const hasImageSrc = (v: unknown): v is { _imageSrc(): Scene2DImageSource } =>
26
+ typeof v === "object" && v !== null && typeof (v as { _imageSrc?: unknown })._imageSrc === "function"
27
+
28
+ const toSrc = (src: ImageSource): ResolvedImageSource =>
17
29
  // _bakeSurfaceForUI (not _bakeSurface) also flags the canvas UI-bound, so canvas.update() later
18
30
  // refreshes this image node in place — that's what makes an animated UIImage(canvas) update.
19
- isCanvas(src) ? { canvasSurface: src._bakeSurfaceForUI() } : src
31
+ isCanvas(src) ? { canvasSurface: src._bakeSurfaceForUI() } : hasImageSrc(src) ? src._imageSrc() : src
20
32
 
21
- /** The stored/resolved image source (a Canvas is resolved to a surface marker). */
22
- export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource
33
+ /** The stored/resolved image source (a Canvas is resolved to a surface marker, a Scene2D to a
34
+ * live-scene marker). */
35
+ export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource | Scene2DImageSource
23
36
 
24
37
  /** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
25
- * (`assetIcon`), or a live Canvas (`canvas.update()` refreshes the node in place). */
38
+ * (`assetIcon`), a live Canvas (`canvas.update()` refreshes the node in place), or a live Scene2D
39
+ * (drawn into the box every frame — a minimap). */
26
40
  export interface UIImage extends UIElementBase<UIImageStyle> {
27
41
  readonly type: "image",
28
42
  /** The image source — writable to swap the picture in place. */