lecodes-sdk 2.4.1 → 2.4.3

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 (58) hide show
  1. package/dist/types/core/Aspect.d.ts +34 -5
  2. package/dist/types/g2/OneWay2D.d.ts +0 -1
  3. package/dist/types/g2/Physics2D.d.ts +8 -6
  4. package/dist/types/g2/Scene2D.d.ts +1 -1
  5. package/dist/types/g2/Shape2D.d.ts +43 -15
  6. package/dist/types/gl/Model.d.ts +3 -2
  7. package/dist/types/gl/Scene.d.ts +1 -1
  8. package/dist/types/gl/animation/DynamicBone.d.ts +99 -35
  9. package/dist/types/gl/animation/IK.d.ts +0 -1
  10. package/dist/types/gl/nav/NavAgent.d.ts +3 -2
  11. package/dist/types/gl/physics/Physics.d.ts +4 -5
  12. package/dist/types/gl/physics/Shape.d.ts +40 -15
  13. package/dist/types/gl/vehicle/Vehicle.d.ts +54 -35
  14. package/dist/types/gl/vehicle/Wheel.d.ts +31 -10
  15. package/dist/types/plugins/gen/ar/contract.d.ts +152 -0
  16. package/dist/types/plugins/gen/ar/sdk/ar.d.ts +47 -21
  17. package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +17 -3
  18. package/dist/types/plugins/gen/audio-recorder/contract.d.ts +64 -0
  19. package/dist/types/plugins/gen/browser/contract.d.ts +108 -0
  20. package/dist/types/plugins/gen/browser/sdk/browser.d.ts +2 -1
  21. package/dist/types/plugins/gen/camera/contract.d.ts +27 -0
  22. package/dist/types/plugins/gen/geolocation/contract.d.ts +52 -0
  23. package/dist/types/plugins/gen/map/contract.d.ts +144 -0
  24. package/dist/types/plugins/gen/push/contract.d.ts +83 -0
  25. package/dist/types/plugins/gen/qr-scanner/contract.d.ts +16 -0
  26. package/dist/types/plugins/gen/speech/contract.d.ts +67 -0
  27. package/dist/types/plugins/gen/webrtc/contract.d.ts +342 -0
  28. package/dist/types/version.d.ts +1 -1
  29. package/dist/types.json +1 -1
  30. package/package.json +1 -1
  31. package/src/animate/tween/Timeline.ts +175 -175
  32. package/src/bridges/gl.d.ts +5 -3
  33. package/src/compile/compileProject.ts +45 -10
  34. package/src/compile/index.ts +7 -0
  35. package/src/compile/webHalf.ts +255 -0
  36. package/src/core/Aspect.ts +118 -35
  37. package/src/g2/OneWay2D.ts +0 -2
  38. package/src/g2/Physics2D.ts +8 -16
  39. package/src/g2/Scene2D.ts +1 -1
  40. package/src/g2/Shape2D.ts +51 -22
  41. package/src/gl/CameraPlace.ts +52 -52
  42. package/src/gl/Light.ts +257 -257
  43. package/src/gl/Mesh.ts +120 -120
  44. package/src/gl/Model.ts +3 -2
  45. package/src/gl/Scene.ts +1 -1
  46. package/src/gl/animation/DynamicBone.ts +549 -486
  47. package/src/gl/animation/IK.ts +0 -2
  48. package/src/gl/nav/NavAgent.ts +14 -12
  49. package/src/gl/physics/Physics.ts +4 -5
  50. package/src/gl/physics/Shape.ts +49 -23
  51. package/src/gl/scenarios.ts +291 -291
  52. package/src/gl/vehicle/Vehicle.ts +102 -58
  53. package/src/gl/vehicle/Wheel.ts +45 -11
  54. package/src/plugins/gen/ar/contract.d.ts +35 -1
  55. package/src/plugins/gen/ar/sdk/ar.gen.ts +25 -6
  56. package/src/plugins/gen/ar/sdk/ar.ts +102 -35
  57. package/src/plugins/gen/browser/sdk/browser.ts +11 -1
  58. package/src/version.ts +1 -1
@@ -16,14 +16,14 @@
16
16
  // horizontal plane the session reports, so the halves know no root.
17
17
 
18
18
  import {
19
- Geometry, Mat4, Material, Mesh, Node, Presentable, Quat, Scene, System, Vec3, _ensurePermission, _navEpoch, _toFile,
19
+ Geometry, Mat4, Material, Mesh, Node, Presentable, Quat, Router, Scene, System, Vec3, _ensurePermission, _navEpoch, _toFile,
20
20
  type ClickEvent, type DismissOptions, type FetchResponse, type File, type NodeEvents, type PresentOptions,
21
21
  type QuatLike, type SceneOptions, type TouchStartEvent, type Vec3Like,
22
22
  } from "../../../../plugin"
23
23
  import { ARBase, supports } from "./ar.gen"
24
24
  import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
25
25
  import type {
26
- ARErrorCode, ARMode, HitTarget, ImageTarget, PlaneChange, PlaneDetection, PlaneType, Pose, StartOptions,
26
+ ARErrorCode, ARMode, HitTarget, ImageTarget, PlaneChange, PlaneDetection, PlaneType, Pose, QRTarget, StartOptions,
27
27
  TrackingReason, TrackingState, WorldAlignment,
28
28
  } from "../contract"
29
29
 
@@ -60,12 +60,12 @@ export type ARError = { code: ARErrorCode, message: string }
60
60
  export type ARPlaneEvent = { plane: ARPlane, change: PlaneChange, mergedInto: ARPlane | null }
61
61
 
62
62
  export type ARSceneOptions = SceneOptions & {
63
- /** "world" (default): planes, hit tests, anchors, image targets. "images": image targets only —
64
- * lighter on iOS; no planes, no hit test, no `root`. */
63
+ /** "world" (default): planes, hit tests, anchors, image and QR targets. "images": image and QR
64
+ * targets only — lighter on iOS; no planes, no hit test, no `root`. */
65
65
  mode?: ARMode
66
66
  }
67
67
 
68
- export type AROpenOptions = PresentOptions & Omit<StartOptions, "mode" | "images"> & {
68
+ export type AROpenOptions = PresentOptions & Omit<StartOptions, "mode" | "images" | "codes"> & {
69
69
  /** The scene's ambient (its `environmentIntensity`) follows the estimated light; turns
70
70
  * `lightEstimation` on. */
71
71
  followLight?: boolean
@@ -76,6 +76,13 @@ export type ARSceneEvents = {
76
76
  plane: (event: ARPlaneEvent) => void
77
77
  light: (estimate: ARLightEstimate) => void
78
78
  error: (error: ARError) => void
79
+ /** The session ended on its own — the user left it (the back gesture of an immersive web session)
80
+ * or the system took it — and the scene leaves: popped when it is the router's top, else closed
81
+ * (`onClose` is where an app opens what comes next, as after `scene.close()`). Not an error. Sent
82
+ * before the scene leaves. (A `Router.pop()` of an AR scene by the app does NOT end its session:
83
+ * a covered scene comes back with it — on the web an immersive session then stays on; `close()`
84
+ * ends it.) */
85
+ end: (reason: "user" | "system") => void
79
86
  }
80
87
 
81
88
  export type AnchorEvents = NodeEvents & {
@@ -97,9 +104,10 @@ const wirePose = (p: ARPoseLike): Pose => {
97
104
  }
98
105
 
99
106
  /**
100
- * A node the AR session drives: it follows a place in the room (`scene.anchor(pose)`, `scene.root`)
101
- * or an image (`scene.createAnchor(image)`). Put content under it. `tracked` says whether the session
102
- * holds it now; `track` / `untrack` fire when that changes.
107
+ * A node the AR session drives: it follows a place in the room (`scene.anchor(pose)`, `scene.root`),
108
+ * an image (`scene.createAnchor(image)`) or a QR code (`scene.createQRAnchor(text, size)`). Put
109
+ * content under it. `tracked` says whether the session holds it now; `track` / `untrack` fire when
110
+ * that changes.
103
111
  */
104
112
  export class Anchor extends Node {
105
113
  /** @internal */
@@ -134,13 +142,16 @@ export class Anchor extends Node {
134
142
  }
135
143
 
136
144
  /**
137
- * An anchor that follows an image (`scene.createAnchor(image, physicalWidth)`): while tracked it sits on
138
- * the image — a rigid pose in metres, no scale: its centre, Y out of the image, the image in X (its
139
- * width) and Z (towards its bottom). How big the image is, is `extent`.
145
+ * An anchor that follows an image (`scene.createAnchor(image, physicalWidth)`) or a printed QR code
146
+ * (`scene.createQRAnchor(text, size)`, `code` its text): while tracked it sits on it — a rigid pose
147
+ * in metres, no scale: its centre, Y out of it, the image in X (its width; a code's top edge) and Z
148
+ * (towards its bottom). How big it is, is `extent`.
140
149
  */
141
150
  export class ImageAnchor extends Anchor {
142
151
  /** @internal */
143
152
  _extent: [number, number]
153
+ /** @internal The text of the QR code it follows; null for an image's. */
154
+ _code: string | null = null
144
155
 
145
156
  constructor(physicalWidth: number) {
146
157
  super()
@@ -149,8 +160,12 @@ export class ImageAnchor extends Anchor {
149
160
 
150
161
  /** [width, height] in metres, along the anchor's X and Z: the session's estimate of the printed
151
162
  * image, refined while it tracks it. Before the image is first found: [physicalWidth, 0] — the
152
- * height comes with the image's proportions, which the session reads, at the first `track`. */
163
+ * height comes with the image's proportions, which the session reads, at the first `track`. A QR
164
+ * code's is [size, size] from the start (a code is square). */
153
165
  get extent(): [number, number] { return [this._extent[0], this._extent[1]] }
166
+
167
+ /** The text of the QR code the anchor follows (`createQRAnchor`); null for an image's. */
168
+ get code(): string | null { return this._code }
154
169
  }
155
170
 
156
171
  /** A surface the session found: a floor, a table, a wall. Its node sits at the plane's centre with Y
@@ -265,11 +280,15 @@ export class ARScene extends Scene {
265
280
  private _shown: { promise: Promise<void>, resolve: () => void, reject: (error: Error) => void, epoch: number } | null = null
266
281
  /** The open's start while it is out (`_begin`). `closed`: close() ended its session meanwhile — the
267
282
  * half refuses a start its close cut short ("failed"), which is the close's doing: the open was
268
- * superseded, not failed. `images`: the image targets changed meanwhile, which the start did not
269
- * carry (it read them when it was sent) — the set goes to the session once it runs. */
270
- private _startOut: { session: ARBase, closed: boolean, images: boolean } | null = null
271
- /** A `setImages` is due at the end of this turn (`_setImages`). */
272
- private _imagesDue = false
283
+ * superseded, not failed. `images` / `codes`: the image / QR targets changed meanwhile, which the
284
+ * start did not carry (it read them when it was sent) — the set goes to the session once it runs. */
285
+ private _startOut: { session: ARBase, closed: boolean, images: boolean, codes: boolean } | null = null
286
+ /** A `setImages` / `setCodes` is due at the end of this turn (`_setTargets`). */
287
+ private readonly _due = { images: false, codes: false }
288
+ /** The QR targets (`createQRAnchor`). */
289
+ private readonly _codes: { anchor: ImageAnchor, text: string, size: number }[] = []
290
+ /** This host's half is older than the QR targets: said once. */
291
+ private _codesWarned = false
273
292
  private _options: AROpenOptions = {}
274
293
  private _root: Anchor | null = null
275
294
  /** The first horizontal plane's pose: where `root` is pinned. */
@@ -335,7 +354,34 @@ export class ARScene extends Scene {
335
354
  this.add(anchor)
336
355
  const id = (source as unknown as { _h: Handle })._h.id
337
356
  this._images.push({ anchor, image: _toFile({ systemId: id, name: "image", size: 0 }), width: physicalWidth, source })
338
- this._setImages()
357
+ this._setTargets("images")
358
+ return anchor
359
+ }
360
+
361
+ /** An anchor that follows a printed QR code whose text is `text`: the session finds the symbol in
362
+ * the camera image and keeps the anchor on it, as on an image — a rigid pose in metres (its centre,
363
+ * Y out of the symbol, X along its top edge, Z towards its bottom), `anchor.extent` = [size, size].
364
+ * `size` = the printed side of the symbol in metres (its modules, no quiet zone): how far it is
365
+ * comes from it. Hidden while not tracked. Before `open()`, or any time: on a running scene the set
366
+ * of codes is updated in place. The web finds codes first (WebXR); on a host whose `ar` plugin does
367
+ * not track them the anchor is never tracked, and the console says so. `text` null or "" = ANY
368
+ * code: the first symbol decoded (a test, a plaque whose text the app does not know). */
369
+ createQRAnchor(text: string | null, size: number): ImageAnchor {
370
+ const anchor = new ImageAnchor(size)
371
+ anchor._code = text ?? ""
372
+ anchor._extent = [size, size]
373
+ anchor._scene = this
374
+ anchor._hideUntracked = true
375
+ anchor.visible = false
376
+ this.add(anchor)
377
+ // a half older than the codes (contract version 2) has no setCodes, and its start reads no codes
378
+ if (ARScene.isSupported && !supports("setCodes")) {
379
+ if (!this._codesWarned) console.warn('ARScene: this host\'s "ar" plugin tracks no QR codes (it is older than contract version 2) — their anchors are never tracked')
380
+ this._codesWarned = true
381
+ return anchor
382
+ }
383
+ this._codes.push({ anchor, text: text ?? "", size })
384
+ this._setTargets("codes")
339
385
  return anchor
340
386
  }
341
387
 
@@ -486,6 +532,7 @@ export class ARScene extends Scene {
486
532
  const options: StartOptions = { ...rest, mode: this.mode }
487
533
  if (followLight) options.lightEstimation = true
488
534
  if (this._images.length) options.images = this._targets()
535
+ if (this._codes.length) options.codes = this._codeTargets()
489
536
  return options
490
537
  }
491
538
 
@@ -493,6 +540,10 @@ export class ARScene extends Scene {
493
540
  return this._images.map(t => ({ image: t.image, physicalWidth: t.width, entity: t.anchor }))
494
541
  }
495
542
 
543
+ private _codeTargets(): QRTarget[] {
544
+ return this._codes.map(t => ({ text: t.text, size: t.size, entity: t.anchor }))
545
+ }
546
+
496
547
  private async _begin(): Promise<void> {
497
548
  const epoch = _navEpoch()
498
549
  const superseded = (why = "another destination was opened") => new Error(`ARScene.open() superseded: ${why} while preparing`)
@@ -524,9 +575,19 @@ export class ARScene extends Scene {
524
575
  this._drop(session)
525
576
  this._emit("error", { code: p.code, message: p.message })
526
577
  })
578
+ session.on("ended", p => {
579
+ if (this._session !== session || !p) return
580
+ // over, not by close(): the scene goes with it — the user's back gesture, as the router's:
581
+ // popped when it is the router's top (the page under it shows), else closed (what it replaced
582
+ // comes back through its onClose — `open()` took that destination's place)
583
+ this._drop(session)
584
+ this._emit("end", p.reason)
585
+ if (Router.current === this) Router.pop()
586
+ else this.close()
587
+ })
527
588
  this._session = session
528
589
  this._baseIntensity = this.environmentIntensity
529
- const out = { session, closed: false, images: false }
590
+ const out = { session, closed: false, images: false, codes: false }
530
591
  this._startOut = out
531
592
  try {
532
593
  await session.start(this._startOptions())
@@ -545,8 +606,9 @@ export class ARScene extends Scene {
545
606
  throw superseded()
546
607
  }
547
608
  this._pinAll()
548
- // the image targets changed while the start was out: the running session takes the set now
549
- if (out.images) this._setImages()
609
+ // the image / QR targets changed while the start was out: the running session takes the set now
610
+ if (out.images) this._setTargets("images")
611
+ if (out.codes) this._setTargets("codes")
550
612
  }
551
613
 
552
614
  /** The session is over (closed, failed): every anchor untracked, the planes gone. */
@@ -571,32 +633,34 @@ export class ARScene extends Scene {
571
633
  this._planes.clear()
572
634
  for (const anchor of this._pinned) anchor._setTracked(false)
573
635
  for (const image of this._images) image.anchor._setTracked(false)
636
+ for (const code of this._codes) code.anchor._setTracked(false)
574
637
  this._root?._setTracked(false)
575
638
  if (this._options.followLight) this.environmentIntensity = this._baseIntensity
576
639
  }
577
640
 
578
- /** The image targets changed (one added or removed): a running session takes the whole set in place
579
- * (`setImages` — the tracking, the planes and the anchors stay). Sent at the end of the turn: several
580
- * targets added in a row are one set. While the open's start is out it waits for it (the start
581
- * carried the set it read when it was sent); before a session there is nothing to tell — the start
582
- * carries the set. Nothing waits for it: a set the half refuses is a console line, and an anchor
583
- * whose image the session did not take never tracks. */
584
- private _setImages(): void {
641
+ /** The image targets or the QR targets changed (one added or removed): a running session takes the
642
+ * whole set in place (`setImages` / `setCodes` — the tracking, the planes and the anchors stay). Sent
643
+ * at the end of the turn: several targets added in a row are one set. While the open's start is out
644
+ * it waits for it (the start carried the set it read when it was sent); before a session there is
645
+ * nothing to tell — the start carries the set. Nothing waits for it: a set the half refuses is a
646
+ * console line, and an anchor whose target the session did not take never tracks. */
647
+ private _setTargets(kind: "images" | "codes"): void {
585
648
  const session = this._session
586
649
  if (!session) return
587
650
  const out = this._startOut
588
- if (out && out.session === session) { out.images = true; return }
589
- if (this._imagesDue) return
590
- this._imagesDue = true
651
+ if (out && out.session === session) { out[kind] = true; return }
652
+ if (this._due[kind]) return
653
+ this._due[kind] = true
591
654
  void Promise.resolve().then(() => {
592
- this._imagesDue = false
655
+ this._due[kind] = false
593
656
  // closed meanwhile: a call on the closed channel would open a new session (the next open's start
594
657
  // carries the set)
595
658
  if (this._session !== session) return
596
- session.setImages(this._targets()).catch((e: unknown) => {
659
+ const sent = kind === "images" ? session.setImages(this._targets()) : session.setCodes(this._codeTargets())
660
+ sent.catch((e: unknown) => {
597
661
  // a close while it was out: the half's refusal is the close's, nothing failed
598
662
  if (this._session !== session) return
599
- console.warn(`ARScene: the session did not take the image targets (${e instanceof Error ? e.message : String(e)}) — their anchors are not tracked`)
663
+ console.warn(`ARScene: the session did not take the ${kind === "images" ? "image targets" : "QR codes"} (${e instanceof Error ? e.message : String(e)}) — their anchors are not tracked`)
600
664
  })
601
665
  })
602
666
  }
@@ -643,10 +707,13 @@ export class ARScene extends Scene {
643
707
  if (anchor === this._root) this._root = null
644
708
  const image = this._images.findIndex(t => t.anchor === anchor)
645
709
  if (image >= 0) this._images.splice(image, 1)
710
+ const code = this._codes.findIndex(t => t.anchor === anchor)
711
+ if (code >= 0) this._codes.splice(code, 1)
646
712
  this._wire("release", s => s.release(anchor)).catch(() => {})
647
713
  anchor._setTracked(false)
648
714
  this.remove(anchor)
649
- if (image >= 0) this._setImages()
715
+ if (image >= 0) this._setTargets("images")
716
+ if (code >= 0) this._setTargets("codes")
650
717
  }
651
718
 
652
719
  private _onPlane(p: { change: PlaneChange, id: number, type: PlaneType, pose: Pose, extent: number[], polygon: number[], mergedInto?: number }): void {
@@ -8,6 +8,9 @@
8
8
  // const back = await Browser.open(checkoutUrl, { returnUrl: [paid, failed], embedded: true })
9
9
  // a checkout in an embedded page,
10
10
  // resolved with the URL it reached
11
+ // (navigated to in the page, or
12
+ // opened by a bank's app through
13
+ // the app's verified link)
11
14
  //
12
15
  // It resolves with the return URL reached, or null when the user left. `returnUrl` says what ENDS
13
16
  // the page; `embedded` says WHERE it runs — the system sheet (shared logins, passkeys, Apple Pay;
@@ -38,7 +41,8 @@ export interface BrowserOpenOptions {
38
41
  /** What ends the page: a URL prefix, or several (a checkout's success and failure URLs). The
39
42
  * promise resolves with the URL reached. In the sheet every prefix must share one scheme — the
40
43
  * platform intercepts one; a custom scheme needs app.json `urlScheme`, an https one the app's
41
- * verified links. */
44
+ * verified links (app.json `appLinks`). Embedded, the view intercepts the navigation itself,
45
+ * and a verified https return from another app (a bank's) ends the page too. */
42
46
  returnUrl?: string | string[]
43
47
  /** Run the page in an embedded WebView under a close bar instead of the system sheet. The
44
48
  * WebView intercepts any return URL itself, https included. */
@@ -157,10 +161,16 @@ const openEmbedded = (url: string, options: BrowserOpenOptions): Promise<string
157
161
  if (routed) Router.pop()
158
162
  else { screen.close(); previous?.open() }
159
163
  }
164
+ // Two ways the return URL arrives: the page navigates to it inside the view (`stop`), or
165
+ // another app does — a bank's app the checkout handed off to sends the person back through the
166
+ // app's verified link (app.json "appLinks"), which reaches the app as its "url" event while the
167
+ // page still shows the bank's form. Both end the flow; `done` makes the second a no-op.
160
168
  let done = false
169
+ const off = stops.length ? _onAppUrl(back => { if (matching(back, stops)) finish(back) }) : () => {}
161
170
  const finish = (back: string | null): void => {
162
171
  if (done) return
163
172
  done = true
173
+ off()
164
174
  leave()
165
175
  resolve(back)
166
176
  }
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.4.1"
7
+ export const SDK_VERSION = "2.4.3"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {