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.
- package/dist/global.d.ts +62 -0
- package/dist/host.d.ts +3 -0
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/gl/AudioSource.d.ts +60 -0
- package/dist/types/gl/AudioZone.d.ts +32 -0
- package/dist/types/gl/DecalSet.d.ts +103 -0
- package/dist/types/gl/Geometry.d.ts +5 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Locomotion.d.ts +3 -1
- package/dist/types/gl/Material.d.ts +86 -2
- package/dist/types/gl/Mesh.d.ts +11 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/SceneAudio.d.ts +11 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/Animator.d.ts +51 -183
- package/dist/types/gl/animation/Feet.d.ts +85 -0
- package/dist/types/gl/animation/Warp.d.ts +53 -0
- package/dist/types/gl/animation/core.d.ts +61 -17
- package/dist/types/gl/state.d.ts +0 -1
- package/dist/types/inject.d.ts +17 -2
- package/dist/types/plugins/map.d.ts +174 -0
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +1481 -1345
- package/src/compile/__tests__/compile.test.ts +12 -0
- package/src/compile/compileProject.ts +35 -15
- package/src/compile/index.ts +3 -0
- package/src/core/Aspect.ts +34 -9
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/AudioSource.ts +113 -0
- package/src/gl/AudioZone.ts +75 -0
- package/src/gl/DecalSet.ts +233 -0
- package/src/gl/Geometry.ts +5 -0
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +3 -2
- package/src/gl/Locomotion.ts +7 -5
- package/src/gl/Material.ts +152 -4
- package/src/gl/Mesh.ts +20 -1
- package/src/gl/Particles.ts +3 -3
- package/src/gl/Scene.ts +42 -8
- package/src/gl/SceneAudio.ts +26 -0
- package/src/gl/Texture.ts +43 -3
- package/src/gl/Vehicle.ts +5 -5
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +138 -329
- package/src/gl/animation/Feet.ts +134 -0
- package/src/gl/animation/Loop.ts +3 -1
- package/src/gl/animation/Warp.ts +96 -0
- package/src/gl/animation/core.ts +741 -670
- package/src/gl/state.ts +6 -6
- package/src/host.d.ts +3 -0
- package/src/inject.ts +23 -2
- package/src/plugins/map.ts +396 -0
- package/src/runtime/input.ts +6 -1
- 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
|
|
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 {
|
|
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
|
+
})()
|
package/src/runtime/input.ts
CHANGED
|
@@ -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
|
-
/**
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`),
|
|
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. */
|