lecodes-sdk 2.2.8 → 2.2.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.
Files changed (63) hide show
  1. package/dist/global.d.ts +42 -1
  2. package/dist/types/gl/Camera.d.ts +5 -2
  3. package/dist/types/gl/Node.d.ts +0 -7
  4. package/dist/types/gl/Scene.d.ts +6 -24
  5. package/dist/types/inject.d.ts +5 -1
  6. package/dist/types/plugin.d.ts +27 -0
  7. package/dist/types/plugins/gen/ar/sdk/ar.d.ts +228 -0
  8. package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +73 -0
  9. package/dist/types/plugins/gen/ar/sdk/controls.d.ts +28 -0
  10. package/dist/types/plugins/gen/audio-recorder/sdk/audio-recorder.d.ts +43 -0
  11. package/dist/types/plugins/gen/audio-recorder/sdk/audio-recorder.gen.d.ts +47 -0
  12. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +6 -2
  13. package/dist/types/plugins/gen/speech/sdk/speech.d.ts +44 -0
  14. package/dist/types/plugins/gen/speech/sdk/speech.gen.d.ts +39 -0
  15. package/dist/types/plugins/gen/webrtc/sdk/webrtc.d.ts +3 -0
  16. package/dist/types/runtime/permissions.d.ts +16 -0
  17. package/dist/types/version.d.ts +1 -1
  18. package/dist/types.json +1 -1
  19. package/package.json +1 -1
  20. package/prompts/ar.md +81 -48
  21. package/prompts/dist/2d-game.md +1567 -0
  22. package/prompts/dist/3d-app.md +1712 -0
  23. package/prompts/dist/ar-app.md +1585 -0
  24. package/prompts/dist/concept.md +49 -0
  25. package/prompts/dist/design.md +521 -0
  26. package/prompts/dist/ui-app.md +1175 -0
  27. package/prompts/index.md +3 -1
  28. package/src/animate/tween/Timeline.ts +175 -175
  29. package/src/bridges/device.d.ts +20 -6
  30. package/src/bridges/gl.d.ts +33 -19
  31. package/src/compile/header.ts +11 -4
  32. package/src/gl/Camera.ts +5 -2
  33. package/src/gl/CameraPlace.ts +52 -52
  34. package/src/gl/Light.ts +257 -257
  35. package/src/gl/Mesh.ts +120 -120
  36. package/src/gl/Node.ts +0 -8
  37. package/src/gl/Scene.ts +14 -81
  38. package/src/gl/animation/DynamicBone.ts +482 -482
  39. package/src/gl/scenarios.ts +291 -291
  40. package/src/inject.ts +13 -1
  41. package/src/plugin.ts +45 -2
  42. package/src/plugins/gen/ar/contract.d.ts +118 -0
  43. package/src/plugins/gen/ar/sdk/ar.gen.ts +112 -0
  44. package/src/plugins/gen/ar/sdk/ar.ts +699 -0
  45. package/src/plugins/gen/ar/sdk/controls.ts +148 -0
  46. package/src/plugins/gen/audio-recorder/contract.d.ts +64 -0
  47. package/src/plugins/gen/audio-recorder/sdk/audio-recorder.gen.ts +76 -0
  48. package/src/plugins/gen/audio-recorder/sdk/audio-recorder.ts +113 -0
  49. package/src/plugins/gen/geolocation/contract.d.ts +7 -5
  50. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +6 -2
  51. package/src/plugins/gen/speech/contract.d.ts +67 -0
  52. package/src/plugins/gen/speech/sdk/speech.gen.ts +64 -0
  53. package/src/plugins/gen/speech/sdk/speech.ts +104 -0
  54. package/src/plugins/gen/webrtc/sdk/webrtc.ts +19 -14
  55. package/src/runtime/permissions.ts +40 -0
  56. package/src/server/auth/host.ts +1 -1
  57. package/src/ui/presentable.ts +5 -0
  58. package/src/ui/router.ts +4 -3
  59. package/src/version.ts +1 -1
  60. package/dist/types/gl/controls.d.ts +0 -19
  61. package/dist/types/plugins/permission.d.ts +0 -1
  62. package/src/gl/controls.ts +0 -95
  63. package/src/plugins/permission.ts +0 -8
@@ -0,0 +1,104 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/speech/sdk/speech.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // SpeechRecognizer — speech recognition: the microphone to text, live (dictation, voice commands).
4
+ //
5
+ // Hand-written over the generated wire because the app's object is not the wire's session: a
6
+ // `SpeechRecognizer` is reusable — every `start()` opens a fresh host session (one listening),
7
+ // `end` closes it — and its events are the SDK's `addEventListener` pattern.
8
+ //
9
+ // The permissions (speech recognition, the microphone) are asked by the host on `start()` (the
10
+ // contract's `@permission`); to ask earlier, or to act on a denial, `permissions`.
11
+
12
+ import { Emitter } from "../../../../plugin"
13
+ import { SpeechBase, NAME } from "./speech.gen"
14
+ import type { SpeechErrorCode, SpeechOptions, SpeechResult } from "../contract"
15
+
16
+ export type { SpeechErrorCode, SpeechOptions, SpeechResult }
17
+
18
+ export type SpeechRecognizerEvents = {
19
+ /** A result: partials while the user speaks (`interimResults`), then the final one. */
20
+ result: (result: SpeechResult) => void
21
+ /** The user started speaking. */
22
+ speechStart: () => void
23
+ /** The user stopped speaking: the final result follows. */
24
+ speechEnd: () => void
25
+ /** The listening is over — after the final result, after `stop()` / `abort()`, after an error. */
26
+ end: () => void
27
+ /** The listening failed; `end` follows. */
28
+ error: (code: SpeechErrorCode, message: string) => void
29
+ }
30
+
31
+ /**
32
+ * Recognizes speech from the microphone. `SpeechRecognizer.isSupported` reports whether this host
33
+ * registered the service — check it before showing a dictation button.
34
+ *
35
+ * ```ts
36
+ * const speech = new SpeechRecognizer()
37
+ * speech.addEventListener('result', r => input.value = r.transcript)
38
+ * speech.addEventListener('end', () => micButton.style({ opacity: 1 }))
39
+ * await speech.start({ language: 'en-US' })
40
+ * ```
41
+ */
42
+ export class SpeechRecognizer extends Emitter<SpeechRecognizerEvents> {
43
+ /** Whether this host registered a "speech" service. */
44
+ static get isSupported(): boolean { return SpeechBase.supports(NAME) }
45
+
46
+ private _session: SpeechBase | null = null
47
+
48
+ /** Whether a listening is under way. */
49
+ get listening(): boolean { return this._session !== null }
50
+
51
+ /** Start listening. The host asks for the permissions first; rejects `"denied"` when refused,
52
+ * `"unavailable"` when there is no recognizer for the language or on the device, and before
53
+ * touching the host when the plugin is not there or a listening is under way. */
54
+ async start(options?: SpeechOptions): Promise<void> {
55
+ if (!SpeechRecognizer.isSupported) {
56
+ throw new Error('SpeechRecognizer isn\'t available on this host — it needs a registered "speech" service.')
57
+ }
58
+ if (this._session) throw new Error("SpeechRecognizer: a listening is under way — stop() or abort() it first")
59
+ const session = new SpeechBase()
60
+ session.on("result", result => { if (result && this._session === session) this.dispatch("result", result) })
61
+ session.on("speechStart", () => { if (this._session === session) this.dispatch("speechStart") })
62
+ session.on("speechEnd", () => { if (this._session === session) this.dispatch("speechEnd") })
63
+ session.on("error", payload => { if (payload && this._session === session) this.dispatch("error", payload.code, payload.message) })
64
+ session.on("end", () => {
65
+ if (this._session !== session) return
66
+ this._drop(session)
67
+ this.dispatch("end")
68
+ })
69
+ this._session = session
70
+ try {
71
+ await session.start(options ?? {})
72
+ } catch (e) {
73
+ this._drop(session)
74
+ throw e
75
+ }
76
+ }
77
+
78
+ /** Stop listening and finish: the final result of what was heard, then `end`. */
79
+ stop(): void {
80
+ this._session?.stop().catch(() => {})
81
+ }
82
+
83
+ /** Stop and drop what was heard: `end`, no result. */
84
+ abort(): void {
85
+ this._session?.abort().catch(() => {})
86
+ }
87
+
88
+ /** The languages this host recognizes (BCP-47), sorted; empty when it cannot say (the web). */
89
+ static async supportedLanguages(): Promise<string[]> {
90
+ if (!SpeechRecognizer.isSupported) return []
91
+ const session = new SpeechBase()
92
+ try {
93
+ return await session.supportedLanguages()
94
+ } finally {
95
+ session.close()
96
+ }
97
+ }
98
+
99
+ private _drop(session: SpeechBase): void {
100
+ if (this._session !== session) return
101
+ session.close()
102
+ this._session = null
103
+ }
104
+ }
@@ -509,6 +509,9 @@ export class RTCPeerConnection extends RTCEventTarget {
509
509
  * after the promise, and the app's signaling expects the description first. */
510
510
  private _settingLocal = 0
511
511
  private _heldCandidates: (RTCIceCandidate | null)[] = []
512
+ /** After the app's close(): what the host still says of the dying session is not an event (the
513
+ * browser fires none for close()). */
514
+ private _closedByApp = false
512
515
 
513
516
  onnegotiationneeded: RTCEventListener<RTCEvent<RTCPeerConnection>> | null = null
514
517
  onicecandidate: RTCEventListener<RTCPeerConnectionIceEvent> | null = null
@@ -524,33 +527,34 @@ export class RTCPeerConnection extends RTCEventTarget {
524
527
  if (!RtcPeerConnectionBase.supports(PC_NAME)) throw unsupported("RTCPeerConnection", PC_NAME, "service")
525
528
  this._configuration = configuration
526
529
  const s = this._s = new RtcPeerConnectionBase(configurationWire(configuration))
527
- s.on("negotiationNeeded", () => this._dispatch({ type: "negotiationneeded" }))
528
- s.on("iceCandidate", ({ candidate }) => {
530
+ const on: typeof s.on = (event, callback) => s.on(event, ((payload?: any) => { if (!this._closedByApp) (callback as any)(payload) }) as any)
531
+ on("negotiationNeeded", () => this._dispatch({ type: "negotiationneeded" }))
532
+ on("iceCandidate", ({ candidate }) => {
529
533
  const ice = candidate ? new RTCIceCandidate(candidate) : null
530
534
  if (this._settingLocal > 0) this._heldCandidates.push(ice)
531
535
  else this._dispatch({ type: "icecandidate", candidate: ice })
532
536
  })
533
- s.on("signalingStateChange", ({ state }) => { this._signaling = state; this._dispatch({ type: "signalingstatechange" }) })
534
- s.on("iceGatheringStateChange", ({ state }) => { this._gathering = state; this._dispatch({ type: "icegatheringstatechange" }) })
535
- s.on("iceConnectionStateChange", ({ state }) => { this._ice = state; this._dispatch({ type: "iceconnectionstatechange" }) })
536
- s.on("connectionStateChange", ({ state }) => { this._connection = state; this._dispatch({ type: "connectionstatechange" }) })
537
- s.on("track", ({ track, streamIds, transceiverId }) => { this._onTrack(track, streamIds, transceiverId).catch(() => {}) })
538
- s.on("trackMute", ({ trackId, muted }) => tracks.get(trackId)?._setMuted(muted))
539
- s.on("trackEnded", ({ trackId }) => tracks.get(trackId)?._ended())
540
- s.on("dataChannel", info => {
537
+ on("signalingStateChange", ({ state }) => { this._signaling = state; this._dispatch({ type: "signalingstatechange" }) })
538
+ on("iceGatheringStateChange", ({ state }) => { this._gathering = state; this._dispatch({ type: "icegatheringstatechange" }) })
539
+ on("iceConnectionStateChange", ({ state }) => { this._ice = state; this._dispatch({ type: "iceconnectionstatechange" }) })
540
+ on("connectionStateChange", ({ state }) => { this._connection = state; this._dispatch({ type: "connectionstatechange" }) })
541
+ on("track", ({ track, streamIds, transceiverId }) => { this._onTrack(track, streamIds, transceiverId).catch(() => {}) })
542
+ on("trackMute", ({ trackId, muted }) => tracks.get(trackId)?._setMuted(muted))
543
+ on("trackEnded", ({ trackId }) => tracks.get(trackId)?._ended())
544
+ on("dataChannel", info => {
541
545
  const channel = new RTCDataChannel(this, info.label, {}, info)
542
546
  this._channels.set(info.id, channel)
543
547
  this._dispatch({ type: "datachannel", channel })
544
548
  })
545
- s.on("dataChannelStateChange", ({ id, state }) => {
549
+ on("dataChannelStateChange", ({ id, state }) => {
546
550
  const channel = this._channels.get(id)
547
551
  if (!channel) return
548
552
  channel._setState(state)
549
553
  if (state === "closed") this._channels.delete(id)
550
554
  })
551
- s.on("dataChannelMessage", ({ id, text, binary }) => this._channels.get(id)?._message(text, binary))
552
- s.on("dataChannelError", ({ id, message }) => this._channels.get(id)?._dispatch({ type: "error", message }))
553
- s.on("dataChannelBufferedAmountLow", ({ id }) => this._channels.get(id)?._dispatch({ type: "bufferedamountlow" }))
555
+ on("dataChannelMessage", ({ id, text, binary }) => this._channels.get(id)?._message(text, binary))
556
+ on("dataChannelError", ({ id, message }) => this._channels.get(id)?._dispatch({ type: "error", message }))
557
+ on("dataChannelBufferedAmountLow", ({ id }) => this._channels.get(id)?._dispatch({ type: "bufferedamountlow" }))
554
558
  }
555
559
 
556
560
  get localDescription(): RTCSessionDescription | null { return this._local }
@@ -654,6 +658,7 @@ export class RTCPeerConnection extends RTCEventTarget {
654
658
  * channel closes. */
655
659
  close(): void {
656
660
  if (this._connection === "closed") return
661
+ this._closedByApp = true
657
662
  this._connection = "closed"
658
663
  this._signaling = "closed"
659
664
  this._ice = "closed"
@@ -0,0 +1,40 @@
1
+ // The OS permissions: what the app may ask the platform for, and when. ONE door, host-gated
2
+ // (`_creatorDevice.queryPermission / requestPermission / openSettings`): the SDK's own camera
3
+ // users (ARScene.prepare, QRScanner, CameraView) go through `_ensurePermission`, apps through
4
+ // `permissions`, a plugin's native half through the host's same service (a method tagged
5
+ // `@permission` in its contract is gated by the generated glue, never by the plugin's code).
6
+ //
7
+ // Hosts without a dialog (headless, desktop, the web for `query` where the browser has no answer)
8
+ // leave the slots out, and both answers are "granted": nothing there stands between an app and a
9
+ // feature — the feature itself says whether it exists (`isSupported`, a rejection of its own).
10
+
11
+ export type PermissionKind = "camera" | "microphone" | "location" | "speech"
12
+ /** "prompt" = never asked: `request` shows the dialog. "denied" = refused, or restricted by the
13
+ * device's policy; the OS decides whether a `request` asks again (iOS never does: `openSettings`). */
14
+ export type PermissionState = "granted" | "denied" | "prompt"
15
+
16
+ const slot = (name: "queryPermission" | "requestPermission", kind: PermissionKind): Promise<PermissionState> => {
17
+ const fn = _creatorDevice[name]
18
+ if (typeof fn !== "function") return Promise.resolve("granted")
19
+ return new Promise((resolve, reject) => fn(kind, s => resolve(s as PermissionState), err => reject(new Error(err))))
20
+ }
21
+
22
+ export const permissions = {
23
+ /** The state of `kind` without a prompt. On the web the browser may not know before a request
24
+ * (Safari before 16, Firefox for the camera): "prompt" then. */
25
+ query(kind: PermissionKind): Promise<PermissionState> { return slot("queryPermission", kind) },
26
+ /** Ask for `kind`: the dialog when it was never asked, the state after it (or the state as it
27
+ * stands when the OS will not ask again). Ask where the user expects it — the first tap on the
28
+ * record button, not the start of the call. */
29
+ request(kind: PermissionKind): Promise<PermissionState> { return slot("requestPermission", kind) },
30
+ /** Open the app's own page of the system settings: where a denied permission is turned back on
31
+ * (iOS asks exactly once). A no-op where there is no such page (web). */
32
+ openSettings(): void { _creatorDevice.openSettings?.() },
33
+ }
34
+
35
+ /** Request `kind` and reject when it was not granted — the SDK's own gate before a camera view
36
+ * opens. @internal */
37
+ export const _ensurePermission = async (kind: PermissionKind): Promise<void> => {
38
+ const state = await permissions.request(kind)
39
+ if (state !== "granted") throw new Error(`The ${kind} permission was ${state === "denied" ? "denied" : "not granted"}`)
40
+ }
@@ -40,7 +40,7 @@ export type AuthHostConfig = {
40
40
  appName?: string
41
41
  /** Sends the sign-in code (the platform mailer). Absent = codes are only echoed to the console. */
42
42
  mail?: (msg: AuthMail) => Promise<void>
43
- /** Echo every code to the server console (the draft slot: the developer is the only user). */
43
+ /** Echo every code to the server console (a developer's own backend — `lecodes dev`, a check: the developer is the only user). */
44
44
  echoCodes?: boolean
45
45
  /** Server console sink (default console). */
46
46
  log?: (level: "warn" | "info", text: string) => void
@@ -113,6 +113,11 @@ export interface Presentable {
113
113
  * await it and skip the swap entirely if it rejects (permission denied, or superseded by
114
114
  * another navigation that happened while preparing). */
115
115
  _prepare?(): Promise<void>
116
+ /** @internal What a destination that prepared waits for once it is presented — ARScene: its first
117
+ * frame drawn (the view mounted and sized, the session's frame shown). `open()` and
118
+ * `Router.push/replace` settle with it: they resolve then, or reject when another navigation came
119
+ * first. */
120
+ _presented?(): Promise<void>
116
121
  /** @internal The destination tuple (see DestTuple). */
117
122
  _dest(): DestTuple
118
123
  }
package/src/ui/router.ts CHANGED
@@ -97,9 +97,10 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
97
97
  _setCurrent(page) // optimistic — the runtime's change event confirms
98
98
  }
99
99
  // A preparing destination (ARScene) keeps the current page visible until it's ready; a
100
- // rejection (camera denied / superseded) propagates to the caller and nothing is pushed.
100
+ // rejection (camera denied / superseded) propagates to the caller and nothing is pushed. Pushed,
101
+ // it settles the push once it is on screen (`_presented`: ARScene's first frame), as its open().
101
102
  const prep = page._prepare?.()
102
- return prep ? prep.then(go) : go()
103
+ return prep ? prep.then(() => { go(); return page._presented?.() }) : go()
103
104
  },
104
105
  replace(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
105
106
  const go = () => {
@@ -110,7 +111,7 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
110
111
  _setCurrent(page) // optimistic — the runtime's change event confirms
111
112
  }
112
113
  const prep = page._prepare?.()
113
- return prep ? prep.then(go) : go()
114
+ return prep ? prep.then(() => { go(); return page._presented?.() }) : go()
114
115
  },
115
116
  pop(to?: number, opts?: PopOptions): void {
116
117
  _bumpNavEpoch()
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.2.8"
7
+ export const SDK_VERSION = "2.2.10"
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 => {
@@ -1,19 +0,0 @@
1
- import type { Node } from "./Node";
2
- import type { Scene } from "./Scene";
3
- export type ControlsOptions = {
4
- /** Smallest uniform scale the pinch gesture allows (default 0.08). */
5
- minScale?: number;
6
- /** Largest uniform scale the pinch gesture allows (default 2). */
7
- maxScale?: number;
8
- /** One-finger drag moves the target across the ground plane (default true). */
9
- pan?: boolean;
10
- /** Two-finger pinch scales the target (default true). */
11
- pinch?: boolean;
12
- /** Two-finger twist rotates the target around its Y axis (default true). */
13
- twist?: boolean;
14
- };
15
- /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
16
- export type ControlsHandle = {
17
- remove(): void;
18
- };
19
- export declare const attachControls: (scene: Scene, target: Node, options?: ControlsOptions) => ControlsHandle;
@@ -1 +0,0 @@
1
- export declare const _requestCameraPermission: () => Promise<void>;
@@ -1,95 +0,0 @@
1
- // AR object-placement controls — the classic "drop a model in the room and manipulate it" gesture
2
- // recipe: ONE finger drags the target across the ground plane (camera-relative), TWO fingers pinch to
3
- // scale and twist to rotate (yaw). Lives on ARScene (see Scene.ts) because it orchestrates the scene's
4
- // camera + touch input against a target node; it's built entirely on the public API (no native calls
5
- // the user couldn't make), so it's a convenience, not a primitive.
6
-
7
- import { Vec2, Vec3 } from "../math/vec"
8
- import { Quat } from "../math/quat"
9
- import type { Node } from "./Node"
10
- import type { Scene } from "./Scene"
11
- import type { TouchStartEvent } from "../runtime/touch"
12
-
13
- export type ControlsOptions = {
14
- /** Smallest uniform scale the pinch gesture allows (default 0.08). */
15
- minScale?: number
16
- /** Largest uniform scale the pinch gesture allows (default 2). */
17
- maxScale?: number
18
- /** One-finger drag moves the target across the ground plane (default true). */
19
- pan?: boolean
20
- /** Two-finger pinch scales the target (default true). */
21
- pinch?: boolean
22
- /** Two-finger twist rotates the target around its Y axis (default true). */
23
- twist?: boolean
24
- }
25
-
26
- /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
27
- export type ControlsHandle = { remove(): void }
28
-
29
- type ActiveTouch = { pointerId: number, x: number, y: number }
30
-
31
- const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
32
-
33
- export const attachControls = (scene: Scene, target: Node, options: ControlsOptions = {}): ControlsHandle => {
34
- const minScale = options.minScale ?? 0.08
35
- const maxScale = options.maxScale ?? 2
36
- const pan = options.pan ?? true
37
- const pinch = options.pinch ?? true
38
- const twist = options.twist ?? true
39
-
40
- const active: ActiveTouch[] = []
41
-
42
- const onTouchStart = (e: TouchStartEvent<Node | null>): void => {
43
- const touch: ActiveTouch = { pointerId: e.pointerId, x: e.clientX, y: e.clientY }
44
- active.push(touch)
45
- e.track({
46
- onMove(m) {
47
- if (active.length === 2) {
48
- // --- two-finger: pinch-scale + twist-rotate about the gesture axis ---
49
- const other = active.find((t) => t !== touch)
50
- if (other) {
51
- const axis = new Vec2(other.x - touch.x, other.y - touch.y)
52
- const span = axis.length()
53
- if (span > 1e-3) {
54
- const dir = axis.scale(1 / span)
55
- const delta = new Vec2(m.deltaX, m.deltaY)
56
- if (pinch) {
57
- const along = -dir.dot(delta) // movement along the pinch axis
58
- target.scale = clamp(target.scale.x * (span + along) / span, minScale * 0.4, maxScale * 1.5)
59
- }
60
- if (twist) {
61
- const tangent = dir.perp().dot(delta) // movement across the axis
62
- target.quaternion = target.quaternion.mul(Quat.fromAxisAngle(Vec3.up, Math.atan2(tangent, span)))
63
- }
64
- }
65
- }
66
- } else if (active.length === 1 && pan) {
67
- // --- one-finger: drag across the ground plane, relative to where the camera looks ---
68
- let dir = scene.camera.forward
69
- const parent = target.parent
70
- if (parent) dir = dir.rotate(parent.quaternion.invert()) // into the parent's local frame
71
- dir = dir.withY(0)
72
- const k = Math.hypot(dir.x, dir.z)
73
- const screen = Math.max(...scene.camera.displaySize)
74
- if (k > 1e-4 && screen > 0) {
75
- const dist = target.worldPosition.distanceTo(scene.camera.worldPosition)
76
- const right = new Vec3(-dir.z, 0, dir.x).scale(m.deltaX / k / screen * dist)
77
- const fwd = dir.scale(-m.deltaY / k / screen * dist)
78
- const move = right.add(fwd)
79
- const p = target.position
80
- target.position = new Vec3(p.x + move.x, p.y, p.z + move.z) // keep height fixed
81
- }
82
- }
83
- touch.x = m.clientX
84
- touch.y = m.clientY
85
- },
86
- onEnd() {
87
- const i = active.indexOf(touch)
88
- if (i >= 0) active.splice(i, 1)
89
- },
90
- })
91
- }
92
-
93
- scene.addEventListener("touchstart", onTouchStart)
94
- return { remove() { scene.removeEventListener("touchstart", onTouchStart); active.length = 0 } }
95
- }
@@ -1,8 +0,0 @@
1
- // Camera permission, host-gated: hosts expose it as `_creatorDevice.requestCamera`. Hosts without
2
- // that slot resolve immediately — their view factory prompts on its own (e.g. getUserMedia on
3
- // the web). Shared by ARScene.prepare, QRScanner and CameraView.
4
-
5
- export const _requestCameraPermission = (): Promise<void> =>
6
- typeof _creatorDevice.requestCamera === "function"
7
- ? new Promise<void>((res, rej) => _creatorDevice.requestCamera!(res, rej))
8
- : Promise.resolve()