lecodes-cli 2.4.0 → 2.4.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 (44) hide show
  1. package/dist/index.js +18930 -1829
  2. package/package.json +10 -9
  3. package/runtime/sdk-types.json +1 -1
  4. package/runtime/web/VERSION.json +3 -3
  5. package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
  6. package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
  7. package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
  8. package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
  9. package/runtime/web/index.html +1 -0
  10. package/runtime/web/player.js +12 -12
  11. package/runtime/web-lite/assets/{index-By07xWGA.js → index-oPsDD47E.js} +1 -1
  12. package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
  13. package/runtime/web-lite/features.json +2 -2
  14. package/runtime/web-lite/index.html +2 -1
  15. package/runtime/web-lite/lite.js +9 -9
  16. package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
  17. package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
  18. package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
  19. package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
  20. package/runtime/web-lite/plugins.json +1 -1
  21. package/src/commands/app/index.ts +26 -9
  22. package/src/commands/app/shared.ts +72 -22
  23. package/src/commands/plugin.ts +3 -3
  24. package/src/commands/push.ts +2 -2
  25. package/src/commands/web.ts +12 -5
  26. package/src/compile/collect.ts +2 -3
  27. package/src/compile/collectLocal.ts +13 -2
  28. package/src/compile/designCompile.ts +2 -3
  29. package/src/compile/projectCompile.ts +3 -0
  30. package/src/dev/devServer.ts +63 -7
  31. package/src/dev/https.ts +50 -0
  32. package/src/dev/serve.ts +15 -9
  33. package/src/dev/webRunner.ts +11 -3
  34. package/src/platform/serverDiff.ts +2 -3
  35. package/src/plugin/emitWeb.ts +17 -8
  36. package/src/plugin/firstParty.ts +30 -0
  37. package/src/plugin/generate.ts +41 -21
  38. package/src/plugin/manifest.ts +33 -3
  39. package/src/project/localFiles.ts +44 -7
  40. package/src/web/build.ts +57 -11
  41. package/src/web/halves.ts +76 -0
  42. package/runtime/web/assets/ar-CHvaraG4.js +0 -1
  43. package/runtime/web-lite/assets/scene3d-D5R7Vq3f.js +0 -888
  44. package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs"
2
- import { join, relative } from "node:path"
2
+ import { basename, dirname, join, relative } from "node:path"
3
3
  import { sanitizeAssetName } from "lecodes-sdk/compile/assetName"
4
4
  import { sha256, toKey } from "./paths"
5
5
  import { loadIgnore, type IgnoreMatcher } from "./ignore"
@@ -25,17 +25,49 @@ const ext = (name: string) => {
25
25
  return i < 0 ? "" : name.slice(i + 1).toLowerCase()
26
26
  }
27
27
 
28
- /** Best-effort kind for a file the user added locally (known files keep their manifest type). */
29
- export const classifyKind = (name: string): FileKind => {
28
+ /** Best-effort kind of a file by its name (a scanned file's is `LocalFile.kind`). */
29
+ const classifyKind = (name: string): FileKind => {
30
30
  const e = ext(name)
31
31
  if (e === "mat") return "shader"
32
32
  if (e === "scene") return "scene"
33
33
  return TEXT_EXTS.has(e) ? "text" : "resource"
34
34
  }
35
35
 
36
+ // A plugin's `web.static` folder (its lecodes-plugin.json, plugin/manifest.ts) is a runtime its web
37
+ // half loads beside itself AS IT IS — 8th Wall's xr.js of 1 MB, xr-slam.js of 5.5 MB, its models:
38
+ // files a page is served, never code of the app, whatever their extensions. Typed by the extension
39
+ // they went up as TEXT assets, megabyte rows the builder was then sent with every compile; every
40
+ // file under one is a resource. (Only a project's half has one: a first-party half asks its bundler
41
+ // for `resources`. The manifest is read as collectLocal reads a half folder's, tolerantly: a broken
42
+ // one names no folder.)
43
+ const PLUGIN_MANIFEST = "lecodes-plugin.json"
44
+
45
+ /** The keys of the static folders the scanned plugin manifests name ("/plugins/ar-8thwall/xr-runtime/"). */
46
+ const staticFolders = (root: string, files: { path: string, absPath: string }[]): string[] => {
47
+ const folders: string[] = []
48
+ for (const file of files) {
49
+ if (basename(file.path) !== PLUGIN_MANIFEST) continue
50
+ let folder: unknown
51
+ try {
52
+ folder = (JSON.parse(readFileSync(file.absPath, "utf8").replace(/^\uFEFF/, "")) as { web?: { static?: unknown } | null }).web?.static
53
+ } catch { continue }
54
+ if (typeof folder !== "string" || folder.trim() === "") continue
55
+ try { folders.push(toKey(relative(root, join(dirname(file.absPath), folder))) + "/") } catch { /* outside the project: none of its files is scanned */ }
56
+ }
57
+ return folders
58
+ }
59
+
36
60
  export const isTextKind = (kind: FileKind) => kind !== "resource"
37
61
 
38
- export type LocalFile = { path: string, absPath: string, sha: string }
62
+ export type LocalFile = {
63
+ path: string
64
+ absPath: string
65
+ sha: string
66
+ /** What the platform stores the file as when the manifest has no type of it (a known file keeps
67
+ * its recorded one): a resource under a plugin's static folder, else by its extension. Every
68
+ * caller that types a local file takes this — the rule lives here alone. */
69
+ kind: FileKind
70
+ }
39
71
 
40
72
  export type LocalScan = { files: LocalFile[], dirs: string[], ignored: string[] }
41
73
 
@@ -46,10 +78,10 @@ export type ScanOptions = {
46
78
  tracked?: Set<string>
47
79
  }
48
80
 
49
- /** Recursively scan the working tree, skipping ignored dirs/files; hashes each file. */
81
+ /** Recursively scan the working tree, skipping ignored dirs/files; hashes and types each file. */
50
82
  export const scanLocal = (root: string, opts: ScanOptions = {}): LocalScan => {
51
83
  const { ignore, tracked } = opts
52
- const files: LocalFile[] = []
84
+ const found: Omit<LocalFile, "kind">[] = []
53
85
  const dirs: string[] = []
54
86
  const ignored: string[] = []
55
87
  const isTracked = (key: string) => tracked?.has(key) ?? false
@@ -77,11 +109,16 @@ export const scanLocal = (root: string, opts: ScanOptions = {}): LocalScan => {
77
109
  if (SKIP_FILES.has(entry.name)) continue
78
110
  const fileIgnored = parentIgnored || (!!ignore && ignore(key, false))
79
111
  if (fileIgnored && !isTracked(key)) { ignored.push(key); continue }
80
- files.push({ path: key, absPath: abs, sha: sha256(readFileSync(abs)) })
112
+ found.push({ path: key, absPath: abs, sha: sha256(readFileSync(abs)) })
81
113
  }
82
114
  }
83
115
  }
84
116
  walk(root, false)
117
+ const statics = staticFolders(root, found)
118
+ const files: LocalFile[] = found.map(f => ({
119
+ ...f,
120
+ kind: statics.some(dir => f.path.startsWith(dir)) ? "resource" : classifyKind(basename(f.path)),
121
+ }))
85
122
  return { files, dirs, ignored }
86
123
  }
87
124
 
package/src/web/build.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto"
2
2
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
3
3
  import { dirname, extname, join, resolve } from "node:path"
4
+ import { buildLiteMaterialArtifact } from "lecodes-sdk/compile"
4
5
  import { collectLocalEntries } from "../compile/collectLocal"
5
6
  import { applyProjectFonts } from "../compile/fonts"
6
7
  import { applyAppAssetIcons } from "../compile/assetIcons"
@@ -8,6 +9,7 @@ import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile
8
9
  import { readAppConfigAt } from "../commands/app/shared"
9
10
  import type { Manifest } from "../project/manifest"
10
11
  import { CliError } from "../cli"
12
+ import { buildHalf, copyStatic, halfModule, projectHalves, type ProjectHalf } from "./halves"
11
13
 
12
14
  /*
13
15
  * `lecodes web build`: the project as a FOLDER a static hosting serves.
@@ -21,9 +23,18 @@ import { CliError } from "../cli"
21
23
  * tweens, the canvas, the 3D engine, its twins, AR — a chunk each with
22
24
  * the files it fetches; the page's features.json says what
23
25
  * a bundle has to name for one to be needed, and whose files they are)
24
- * plugins/<id>/… the web halves of the plugins the app lists, a folder each, and
25
- * plugins.json where the half of each is
26
+ * plugins/<id>/… the web halves of the plugins the app lists, a folder each — a plugin
27
+ * app.json names a folder of the project for on the web (`"ar": { "web":
28
+ * "./plugins/ar-8thwall" }`) has the project's half too, built here
29
+ * (plugins/<folder>/index.js + its `web.static` folder, web/halves.ts), tried
30
+ * before the first-party one — and
31
+ * plugins.json where the half of each is, by the plugin's id: its module, or the modules
32
+ * to try in order (the project's half, then the first-party one)
26
33
  * assets/<path> the project's assets — only the ones the bundle names
34
+ * shaders/<hash>_webgl-lite.filamat
35
+ * the project's .mat shaders the bundle names, in the one form Web Lite
36
+ * reads (`_creator.backend` is "webgl-lite"): the parsed material as JSON,
37
+ * built from the source alone — no matc
27
38
  * 404.html in `path` mode: the page again, for the hostings that answer an unknown
28
39
  * path with it
29
40
  *
@@ -100,13 +111,24 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
100
111
 
101
112
  // ---- the bundle: every asset a url relative to the page ------------------------------------------------
102
113
  const assetUrl = (path: string) => "assets" + encodeURI(path)
103
- const collected = collectLocalEntries(root, manifest, "assets")
114
+ const collected = collectLocalEntries(root, manifest, "assets", { localShaders: true })
104
115
  const entries = collected.entries.filter((e) => {
105
116
  // what a build made is no part of the project
106
117
  const abs = e.absPath ? resolve(e.absPath) : null
107
118
  return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")))
108
119
  })
109
- for (const w of collected.warnings) if (!/shaders compile on the server/.test(w)) warnings.push(w)
120
+ warnings.push(...collected.warnings)
121
+ // A shader's artifact is named by a hash of its text (a hosting keeps a file of one name for
122
+ // good). The bundle builds the name at run time from the `_opengl` template (compileProject):
123
+ // on the page that is `<hash>_webgl-lite.filamat`, the only artifact written.
124
+ const shaders = new Map<string, string>() // hash → the lite artifact's JSON
125
+ for (const entry of entries) {
126
+ if (entry.type !== "shader" || !entry.absPath) continue
127
+ const text = readFileSync(entry.absPath, "utf8")
128
+ const hash = createHash("sha256").update(text).digest("hex").slice(0, 16)
129
+ shaders.set(hash, JSON.stringify(buildLiteMaterialArtifact(text)))
130
+ entry.shaders = [{ platform: "opengl", src: `shaders/${hash}_opengl.filamat` }]
131
+ }
110
132
  const fontsHeader = await applyProjectFonts(entries, {
111
133
  mode: "vendor", root, vendorDir: root, vendorPrefix: "", publicUrl: "", relative: true,
112
134
  makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: assetUrl(path), absPath }),
@@ -130,6 +152,12 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
130
152
  warnings.push("This project uses 2D scenes, which Web Lite does not show yet: the page runs its UI and its 3D, a 2D scene's calls fail. (`lecodes dev --web` runs the whole app on the web player.)")
131
153
  }
132
154
 
155
+ // the project's own web halves of the plugins it lists, built before the folder is cleaned: one
156
+ // that does not build leaves the last build as it was
157
+ const listed = new Set((/^\/\/ plugins: (.+)$/m.exec(header)?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean))
158
+ const halves: { half: ProjectHalf, js: string }[] = []
159
+ for (const half of projectHalves(root)) if (listed.has(half.id)) halves.push({ half, js: await buildHalf(half, { minify: options.minify }) })
160
+
133
161
  // ---- the folder ----------------------------------------------------------------------------------------------
134
162
  rmSync(out, { recursive: true, force: true })
135
163
  mkdirSync(out, { recursive: true })
@@ -145,19 +173,31 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
145
173
  }
146
174
  rmSync(table)
147
175
  }
148
- // …and the plugins the app does not list (`// plugins:` of the bundle's header)
176
+ // …and the plugins the app does not list (`// plugins:` of the bundle's header). A plugin app.json
177
+ // names a folder of the project for on the web takes the project's half (web/halves.ts) FIRST and
178
+ // keeps its first-party one as the fallback: if the project's half declines the page (`register`
179
+ // returns false), the first-party half takes it — plugins.json names both, in that order
180
+ // (`"ar": ["plugins/ar-8thwall/index.js", "plugins/ar/index-….js"]`); a plain module otherwise.
149
181
  const pluginsTable = join(out, "plugins.json")
150
- if (existsSync(pluginsTable)) {
151
- const listed = new Set((/^\/\/ plugins: (.+)$/m.exec(header)?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean))
152
- const { plugins } = JSON.parse(readFileSync(pluginsTable, "utf8")) as { plugins: Record<string, string> }
153
- const kept: Record<string, string> = {}
182
+ if (existsSync(pluginsTable) || halves.length > 0) {
183
+ const { plugins } = existsSync(pluginsTable) ? JSON.parse(readFileSync(pluginsTable, "utf8")) as { plugins: Record<string, string> } : { plugins: {} }
184
+ const kept: Record<string, string | string[]> = {}
154
185
  for (const id of Object.keys(plugins)) {
155
186
  if (listed.has(id)) kept[id] = plugins[id]!
156
187
  else rmSync(join(out, "plugins", id), { recursive: true, force: true })
157
188
  }
158
- for (const id of listed) if (!(id in plugins)) warnings.push(`The "${id}" plugin has no web half in this CLI: it is unsupported on the page (\`isSupported\` answers false).`)
189
+ for (const { half, js } of halves) {
190
+ if (half.folder in plugins && half.folder in kept) throw new CliError(`Plugin "${half.id}": its half's folder is named like the "${half.folder}" plugin the page carries — rename ${half.dir}.`)
191
+ const dir = join(out, "plugins", half.folder)
192
+ mkdirSync(dir, { recursive: true })
193
+ writeFileSync(join(dir, "index.js"), js)
194
+ copyStatic(half, join(out, "plugins"))
195
+ const firstParty = plugins[half.id]
196
+ kept[half.id] = firstParty !== undefined && listed.has(half.id) ? [halfModule(half), firstParty] : halfModule(half)
197
+ }
198
+ for (const id of listed) if (!(id in kept)) warnings.push(`The "${id}" plugin has no web half in this CLI: it is unsupported on the page (\`isSupported\` answers false).`)
159
199
  if (Object.keys(kept).length > 0) writeFileSync(pluginsTable, JSON.stringify({ plugins: kept }, null, 2))
160
- else { rmSync(pluginsTable); rmSync(join(out, "plugins"), { recursive: true, force: true }) }
200
+ else { rmSync(pluginsTable, { force: true }); rmSync(join(out, "plugins"), { recursive: true, force: true }) }
161
201
  }
162
202
 
163
203
  const bundle = `app.${createHash("sha256").update(js).digest("hex").slice(0, 10)}.js`
@@ -172,6 +212,12 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
172
212
  mkdirSync(dirname(to), { recursive: true })
173
213
  copyFileSync(entry.absPath, to)
174
214
  }
215
+ // …and its shaders: the ones the bundle names
216
+ for (const [hash, artifact] of shaders) {
217
+ if (!js.includes(`shaders/${hash}_`)) continue
218
+ mkdirSync(join(out, "shaders"), { recursive: true })
219
+ writeFileSync(join(out, "shaders", `${hash}_webgl-lite.filamat`), artifact)
220
+ }
175
221
 
176
222
  // ---- the page ----------------------------------------------------------------------------------------------
177
223
  const app = readAppConfigAt(root)
@@ -0,0 +1,76 @@
1
+ // A project's OWN web halves on Web Lite: the plugins app.json names a folder of the project for on
2
+ // the web — a half folder over a first-party plugin (`"ar": { "web": "./plugins/ar-8thwall" }`, its
3
+ // manifest `implements: "ar"`), or a plugin of the project with a web half (`"dial": "./plugins/dial"`).
4
+ // The CLI builds each — `lecodes web build` into the folder, `lecodes web dev` in memory. A project's
5
+ // own half of an id comes first; if it DECLINES the page (`register` returns `false`, sync or through
6
+ // a promise — renderers/web-lite registry.ts), the first-party half of that id takes it: the page
7
+ // carries both, the first-party one as the fallback.
8
+ //
9
+ // plugins/<folder>/index.js the half: ONE module (Bun.build, ES) that exports `register(host)`
10
+ // plugins/<folder>/<static>/… the manifest's `web.static` folder, as it is, names unhashed (a
11
+ // runtime the half loads by `new URL("<static>/x.js", import.meta.url)`
12
+ // fetches its own files relative to itself)
13
+ // plugins.json `"<id>": ["plugins/<folder>/index.js", "plugins/<id>/index-….js"]`
14
+ // — keyed by the plugin's id, the project's half then the
15
+ // first-party one; the plain module when the page has no
16
+ // first-party half of the id
17
+ //
18
+ // What a half is built into — plain TypeScript of its own folder, ONE module, no package, no file
19
+ // outside the folder — is the SDK's (lecodes-sdk/compile `bundleWebHalf`, `webHalfManifestOf`): the
20
+ // build service builds the halves of a project the platform deploys by the same rules. Here: the
21
+ // halves found on disk, and the SDK's errors as the CLI's.
22
+
23
+ import { cpSync, existsSync, readFileSync } from "node:fs"
24
+ import { join, sep } from "node:path"
25
+ import { bundleWebHalf, webHalfManifestOf, webHalfSpecsOf, type WebHalfManifest } from "lecodes-sdk/compile"
26
+ import { CliError } from "../cli"
27
+ import { localPlugin, readAppConfigAt, type PluginManifest } from "../commands/app/shared"
28
+ import { MANIFEST_FILE } from "../plugin/manifest"
29
+
30
+ export type ProjectHalf = {
31
+ /** The plugin's id: app.json's key, what the page asks the half for. */
32
+ id: string
33
+ /** The folder's name: where the half lands, `plugins/<folder>/`. */
34
+ folder: string
35
+ dir: string
36
+ manifest: PluginManifest
37
+ /** The module that exports `register(host)` (the manifest's `web.register`). */
38
+ entry: string
39
+ /** `web.static`, relative to the folder; null when none. */
40
+ static: string | null
41
+ }
42
+
43
+ /** Where a half's module is, relative to the page. */
44
+ export const halfModule = (half: ProjectHalf): string => `plugins/${half.folder}/index.js`
45
+
46
+ /** The SDK's error as the CLI's. */
47
+ const asCliError = (e: unknown): CliError => new CliError(e instanceof Error ? e.message : String(e))
48
+
49
+ /** The project's web halves, by app.json's web specs that are folders. Throws on one that cannot be
50
+ * built: no web half, packages, a missing generated file. */
51
+ export const projectHalves = (root: string): ProjectHalf[] => {
52
+ const halves: ProjectHalf[] = []
53
+ for (const { id, spec } of webHalfSpecsOf(readAppConfigAt(root))) {
54
+ // the whole manifest checked first (every problem of the file at once), then what a build takes of it
55
+ const { dir, manifest } = localPlugin(root, id, spec)
56
+ const folder = dir.split(sep).pop()!
57
+ let web: WebHalfManifest
58
+ try { web = webHalfManifestOf(readFileSync(join(dir, MANIFEST_FILE), "utf8"), folder, id) } catch (e) { throw asCliError(e) }
59
+ const genId = web.implements ?? id
60
+ if (!existsSync(join(dir, "web", `${genId}.gen.ts`))) {
61
+ throw new CliError(`Plugin "${id}": ${spec}/web/${genId}.gen.ts is not there — run \`lecodes plugin gen ${spec}\`.`)
62
+ }
63
+ if (halves.some((h) => h.folder === folder)) throw new CliError(`Plugin "${id}": ${spec} is the half of another plugin already`)
64
+ halves.push({ id, folder, dir, manifest, entry: join(dir, web.register), static: web.static })
65
+ }
66
+ return halves
67
+ }
68
+
69
+ /** The half as ONE ES module's text. `dev`: not minified, its source map inline. */
70
+ export const buildHalf = (half: ProjectHalf, opts: { dev?: boolean, minify?: boolean } = {}): Promise<string> =>
71
+ bundleWebHalf({ id: half.id, dir: half.dir, entry: half.entry, dev: opts.dev, minify: opts.minify }).catch((e: unknown) => { throw asCliError(e) })
72
+
73
+ /** The half's `web.static` folder, copied beside its module (`<plugins>/<folder>/<static>/`). */
74
+ export const copyStatic = (half: ProjectHalf, pluginsDir: string): void => {
75
+ if (half.static) cpSync(join(half.dir, half.static), join(pluginsDir, half.folder, half.static), { recursive: true })
76
+ }
@@ -1 +0,0 @@
1
- var nt=Object.defineProperty;var ot=(e,t,n)=>t in e?nt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var P=(e,t,n)=>ot(e,typeof t!="symbol"?t+"":t,n);const it=(e,t)=>{const n=t.data instanceof Uint8Array?t.data.slice().buffer:t.data;return{systemId:e.putBuffer(n),name:t.name,size:n.byteLength}},l={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,t)=>Array.isArray(e)&&(t===void 0||e.length===t)?e:void 0,list:(e,t)=>{if(!Array.isArray(e))return;const n=[];for(const o of e){const i=t(o);if(i===void 0)return;n.push(i)}return n},map:(e,t)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const n={};for(const[o,i]of Object.entries(e)){const r=t(i);if(r===void 0)return;n[o]=r}return n}};let $;const rt=e=>{const t=l.object(e),n=t?l.i32(t.systemId):void 0,o=n===void 0||$==null?void 0:$(n);return t&&o?{data:o,name:l.string(t.name)??""}:void 0},S=e=>typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=4294967295?e:void 0,st=e=>{const t=l.object(e);if(!t)return;let n;if(l.present(t.scene)){const r=S(t.scene);if(r===void 0)return;n=r}let o;if(l.present(t.camera)){const r=S(t.camera);if(r===void 0)return;o=r}const i={};return n!==void 0&&(i.scene=n),o!==void 0&&(i.camera=o),i},at=e=>e==="world"||e==="images"?e:void 0,ct=e=>e==="horizontal"||e==="vertical"||e==="both"||e==="none"?e:void 0,Y=e=>{const t=l.object(e);if(!t)return;const n=rt(t.image);if(n===void 0)return;const o=l.f64(t.physicalWidth);if(o===void 0)return;const i=S(t.entity);return i===void 0?void 0:{image:n,physicalWidth:o,entity:i}},dt=e=>e==="gravity"||e==="gravityAndHeading"?e:void 0,ut=e=>{const t=l.object(e);if(!t)return;let n;if(l.present(t.mode)){const s=at(t.mode);if(s===void 0)return;n=s}let o;if(l.present(t.planes)){const s=ct(t.planes);if(s===void 0)return;o=s}let i;if(l.present(t.images)){const s=l.list(t.images,u=>Y(u));if(s===void 0)return;i=s}let r;if(l.present(t.worldAlignment)){const s=dt(t.worldAlignment);if(s===void 0)return;r=s}let h;if(l.present(t.lightEstimation)){const s=l.bool(t.lightEstimation);if(s===void 0)return;h=s}let c;if(l.present(t.autoFocus)){const s=l.bool(t.autoFocus);if(s===void 0)return;c=s}const d={};return n!==void 0&&(d.mode=n),o!==void 0&&(d.planes=o),i!==void 0&&(d.images=i),r!==void 0&&(d.worldAlignment=r),h!==void 0&&(d.lightEstimation=h),c!==void 0&&(d.autoFocus=c),d},lt=e=>e==="planes"||e==="estimated"||e==="any"?e:void 0,ft=e=>{const t=l.object(e);if(!t)return;const n=l.f64(t.x);if(n===void 0)return;const o=l.f64(t.y);if(o===void 0)return;const i=l.f64(t.z);return i===void 0?void 0:{x:n,y:o,z:i}},ht=e=>{const t=l.object(e);if(!t)return;const n=l.f64(t.x);if(n===void 0)return;const o=l.f64(t.y);if(o===void 0)return;const i=l.f64(t.z);if(i===void 0)return;const r=l.f64(t.w);return r===void 0?void 0:{x:n,y:o,z:i,w:r}},mt=e=>{const t=l.object(e);if(!t)return;const n=ft(t.position);if(n===void 0)return;const o=ht(t.rotation);return o===void 0?void 0:{position:n,rotation:o}};class A extends Error{constructor(t){super(t),this.code=t}}const q={name:"ar",version:1},vt=(e,t)=>{$=n=>e.getBuffer(n),e.registerService(q.name,(n,o)=>{const i={tracking:c=>o.emit("tracking",c),plane:c=>o.emit("plane",c),image:c=>o.emit("image",c),anchor:c=>o.emit("anchor",c),light:c=>o.emit("light",c),error:c=>o.emit("error",c)},r=t(st(n)??{},i);return{call:async(c,d)=>{switch(c){case"start":{let s;if(l.present(d[0])){const u=ut(d[0]);if(u===void 0)throw new Error("bad arguments — AR.start: options");s=u}await r.start(s);return}case"stop":{await r.stop();return}case"setImages":{const s=l.list(d[0],u=>Y(u));if(s===void 0)throw new Error("bad arguments — AR.setImages: images");await r.setImages(s);return}case"hitTest":{const s=l.f64(d[0]);if(s===void 0)throw new Error("bad arguments — AR.hitTest: x");const u=l.f64(d[1]);if(u===void 0)throw new Error("bad arguments — AR.hitTest: y");let p;if(l.present(d[2])){const E=lt(d[2]);if(E===void 0)throw new Error("bad arguments — AR.hitTest: target");p=E}return await r.hitTest(s,u,p)}case"anchor":{const s=S(d[0]);if(s===void 0)throw new Error("bad arguments — AR.anchor: entity");const u=mt(d[1]);if(u===void 0)throw new Error("bad arguments — AR.anchor: pose");await r.anchor(s,u);return}case"release":{const s=S(d[0]);if(s===void 0)throw new Error("bad arguments — AR.release: entity");await r.release(s);return}case"captureFrame":{const s=await r.captureFrame();return it(e,s)}default:throw new Error(`Unknown method: ${c}`)}},close:()=>{var c;return(c=r.close)==null?void 0:c.call(r)}}},q.version)},F=Math.PI/180,D=(e,t)=>[e[3]*t[0]+e[0]*t[3]+e[1]*t[2]-e[2]*t[1],e[3]*t[1]-e[0]*t[2]+e[1]*t[3]+e[2]*t[0],e[3]*t[2]+e[0]*t[1]-e[1]*t[0]+e[2]*t[3],e[3]*t[3]-e[0]*t[0]-e[1]*t[1]-e[2]*t[2]],U=e=>{const t=Math.hypot(e[0],e[1],e[2],e[3])||1;return[e[0]/t,e[1]/t,e[2]/t,e[3]/t]},O=(e,t)=>{const n=Math.sin(t/2);return[e[0]*n,e[1]*n,e[2]*n,Math.cos(t/2)]},G=(e,t)=>{const[n,o,i,r]=e,h=2*(o*t[2]-i*t[1]),c=2*(i*t[0]-n*t[2]),d=2*(n*t[1]-o*t[0]);return[t[0]+r*h+(o*d-i*c),t[1]+r*c+(i*h-n*d),t[2]+r*d+(n*c-o*h)]},Q=(e,t,n)=>D(D(O([0,0,1],e*F),O([1,0,0],t*F)),O([0,1,0],n*F)),tt=e=>{const[t,n,o,i]=e;return[2*(t*o-i*n),2*(i*t+n*o),i*i-t*t-n*n+o*o]},wt=(e,t)=>{const n=e[0]*t[0]+e[1]*t[1]+e[2]*t[2];if(n<-.999999)return[1,0,0,0];const o=[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]];return U([o[0],o[1],o[2],1+n])};class Z{constructor(t=1,n=.05){P(this,"q",[0,0,0,1]);P(this,"seeded",!1);P(this,"ix",0);P(this,"iy",0);P(this,"iz",0);this.kp=t,this.ki=n}get ready(){return this.seeded}seed(t){this.q=U(t),this.seeded=!0}update(t,n,o){const i=Math.hypot(n[0],n[1],n[2]);if(!this.seeded){if(i===0)return;this.seed(wt([n[0]/i,n[1]/i,n[2]/i],[0,0,1]));return}let r=t[0],h=t[1],c=t[2];if(i>0){const z=n[0]/i,j=n[1]/i,I=n[2]/i,b=tt(this.q),M=j*b[2]-I*b[1],T=I*b[0]-z*b[2],k=z*b[1]-j*b[0];this.ki>0&&(this.ix+=this.ki*M*o,this.iy+=this.ki*T*o,this.iz+=this.ki*k*o,r+=this.ix,h+=this.iy,c+=this.iz),r+=this.kp*M,h+=this.kp*T,c+=this.kp*k}const d=.5*o*r,s=.5*o*h,u=.5*o*c,[p,E,x,R]=this.q;this.q=U([p+R*d+E*u-x*s,E+R*s-p*u+x*d,x+R*u+p*s-E*d,R-p*d-E*s-x*u])}}const gt=O([1,0,0],-Math.PI/2),yt=(e,t)=>D(D(gt,e),O([0,0,1],-t*F)),pt=e=>{let[t,,n]=G(e,[0,0,-1]);return Math.hypot(t,n)<.3&&([t,,n]=G(e,[0,1,0])),O([0,1,0],-Math.atan2(-t,-n))},At=e=>{const[t,n,o,i]=e;return[1-2*(n*n+o*o),2*(t*n+i*o),2*(t*o-i*n),0,2*(t*n-i*o),1-2*(t*t+o*o),2*(n*o+i*t),0,2*(t*o+i*n),2*(n*o-i*t),1-2*(t*t+n*n),0,0,0,0,1]},Et=(e,t,n,o,i)=>{const r=Math.max(e,t)/2/Math.tan(i*F/2),h=Math.max(n/e,o/t);return 2*Math.atan(o/h/2/r)/F},bt=(e,t,n,o)=>{const i=1/Math.tan(e*F/2);return[i/t,0,0,0,0,i,0,0,0,0,(o+n)/(n-o),-1,0,0,2*o*n/(n-o),0]},xt=(e,t,n,o)=>{const i=Math.max(n/e,o/t),r=n/(e*i),h=o/(t*i);return[r,0,(1-r)/2,0,h,(1-h)/2]},Mt=68,J=.01,K=1e3,N=Math.PI/180,X=30,kt=e=>{var n;if(typeof navigator>"u"||!((n=navigator.mediaDevices)!=null&&n.getUserMedia))return;const t=e.gl;vt(e,(o,i)=>{const r=o.scene;let h=0,c=!1,d=!1,s=null,u=null,p=null,E="",x=new Z,R=!1,z=null,j=null,I=0,b=0,M=0,T=null,k=null;const W=a=>{a.alpha===null||a.beta===null||a.gamma===null||a.alpha===0&&a.beta===0&&a.gamma===0||(j=[a.beta,a.gamma],z=Q(a.alpha,a.beta,a.gamma))},B=a=>{const m=a.rotationRate,f=a.accelerationIncludingGravity;if(!m||!f||m.alpha===null||m.beta===null||m.gamma===null||f.x===null||f.y===null||f.z===null)return;const g=a.timeStamp;if(b||(b=g),M===0)if(j){const y=tt(Q(0,j[0],j[1])),v=Math.hypot(f.x,f.y,f.z)||1;M=(y[0]*f.x+y[1]*f.y+y[2]*f.z)/v<0?-1:1}else if(g-b>500)M=1;else return;const w=I?Math.min(.1,Math.max(0,(g-I)/1e3)):0;I=g,R||(R=!0,z&&x.seed(z)),x.update([m.beta*N,m.gamma*N,m.alpha*N],[f.x*M,f.y*M,f.z*M],w)},C=()=>{if(!u||r===void 0)return;const a=t.viewport(r),{width:m,height:f}=a,g=`${u.videoWidth}x${u.videoHeight}:${m}x${f}`;if(g!==E&&u.videoWidth>0&&m>0&&f>0){E=g;const v=Et(u.videoWidth,u.videoHeight,m,f,Mt);t.setProjection(r,bt(v,m/f,J,K),J,K),t.setBackground(r,u,xt(u.videoWidth,u.videoHeight,m,f))}const w=R?x.ready?x.q:null:z;if(!w)return;const y=yt(w,a.rotation);T??(T=pt(y)),t.setCameraMatrix(r,At(D(T,y)))},H=async()=>{const a=[window.DeviceMotionEvent,window.DeviceOrientationEvent];if(!a[1])return!1;let m=!0;for(const f of a)if(typeof(f==null?void 0:f.requestPermission)=="function")try{m=await f.requestPermission()==="granted"&&m}catch{m=!1}return m},V=()=>{k&&window.removeEventListener("pointerup",k,!0),k=null},et=()=>{k||(k=()=>{V(),H().then(a=>{a||console.warn("[ar] the page was not given the device's sensors: the camera of the scene stays as the app turned it")})},window.addEventListener("pointerup",k,!0))},_=()=>{h++,c=!1,d=!1,p==null||p(),p=null;for(const a of(s==null?void 0:s.getTracks())??[])a.stop();s=null,u&&(u.srcObject=null,r!==void 0&&(t.setBackground(r,null),t.clearProjection(r)),u.remove()),u=null,E="",V(),window.removeEventListener("deviceorientation",W),window.removeEventListener("devicemotion",B),x=new Z,R=!1,z=null,j=null,I=0,b=0,M=0,T=null};return{async start(a){var y;if(c||d)throw new A("busy");if(r===void 0||o.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new A("failed");((a==null?void 0:a.mode)==="images"||(y=a==null?void 0:a.images)!=null&&y.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),d=!0;const m=++h,f=H();let g;try{g=await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:{ideal:"environment"},width:{ideal:1280},height:{ideal:720}}})}catch(v){m===h&&(d=!1);const L=v instanceof DOMException?v.name:"";throw console.warn(`[ar] no camera: ${v instanceof Error?v.message:v}`),new A(L==="NotAllowedError"||L==="SecurityError"?"denied":L==="NotFoundError"||L==="OverconstrainedError"?"unavailable":"failed")}if(m!==h){for(const v of g.getTracks())v.stop();throw new A("failed")}s=g;const w=document.createElement("video");if(w.muted=!0,w.playsInline=!0,w.autoplay=!0,w.srcObject=g,u=w,await new Promise(v=>{if(w.readyState>=2)return v();w.addEventListener("loadeddata",()=>v(),{once:!0})}),await w.play().catch(()=>{}),m!==h)throw new A("failed");window.addEventListener("deviceorientation",W),window.addEventListener("devicemotion",B),p=t.onFrame(C),C(),d=!1,c=!0,i.tracking({state:"normal"}),f.then(v=>{!v&&m===h&&et()})},stop(){_()},setImages(a){if(!c)throw new A("notRunning");a.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},hitTest(){if(!c)throw new A("notRunning");return[]},anchor(){if(!c)throw new A("notRunning")},release(){},async captureFrame(){const a=u;if(!c||!a)throw new A("notRunning");const m=h;for(let y=0;a.readyState<2||a.videoWidth===0;y++){if(y>=X)throw console.warn(`[ar] captureFrame: the camera gave no picture in ${X} frames`),new A("failed");if(await new Promise(v=>requestAnimationFrame(v)),m!==h)throw new A("notRunning")}const f=document.createElement("canvas");f.width=a.videoWidth,f.height=a.videoHeight;const g=f.getContext("2d");if(!g)throw new A("failed");g.drawImage(a,0,0);const w=await new Promise(y=>f.toBlob(y,"image/jpeg",.9));if(!w)throw new A("failed");return{data:await w.arrayBuffer(),name:"frame.jpg"}},close(){_()}}})};export{kt as register};