lecodes-cli 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.
- package/dist/index.js +36134 -11203
- package/package.json +10 -9
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
- package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
- package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
- package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web/index.html +1 -0
- package/runtime/web/player.js +12 -12
- package/runtime/web-lite/assets/{index-B2zEJlWS.js → index-oPsDD47E.js} +1 -1
- package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/index.html +2 -1
- package/runtime/web-lite/lite.js +9 -9
- package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
- package/runtime/web-lite/plugins.json +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +84 -17
- package/src/commands/app/shared.ts +157 -24
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/plugin.ts +3 -3
- package/src/commands/push.ts +2 -2
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/commands/web.ts +12 -5
- package/src/compile/collect.ts +2 -3
- package/src/compile/collectLocal.ts +13 -2
- package/src/compile/designCompile.ts +2 -3
- package/src/compile/projectCompile.ts +3 -0
- package/src/compile/screenEntry.ts +123 -123
- package/src/dev/devServer.ts +63 -7
- package/src/dev/https.ts +50 -0
- package/src/dev/serve.ts +15 -9
- package/src/dev/webRunner.ts +11 -3
- package/src/platform/serverDiff.ts +2 -3
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/emitWeb.ts +17 -8
- package/src/plugin/firstParty.ts +30 -0
- package/src/plugin/generate.ts +52 -24
- package/src/plugin/manifest.ts +66 -7
- package/src/project/ignore.ts +164 -164
- package/src/project/localFiles.ts +44 -7
- package/src/web/build.ts +33 -9
- package/src/web/halves.ts +76 -0
- package/runtime/web/assets/ar-CHvaraG4.js +0 -1
- package/runtime/web-lite/assets/scene3d-Bew8g7Ws.js +0 -1081
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
package/src/project/ignore.ts
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
* `.lecodesignore` support — keeps scratch files (temp probes, render screenshots, logs) out of
|
|
6
|
-
* `status` / `push` so they never land in a checkpoint. Syntax is a practical subset of .gitignore:
|
|
7
|
-
* - blank lines and `# comments` are skipped
|
|
8
|
-
* - `!pattern` re-includes something a previous pattern ignored
|
|
9
|
-
* - a trailing `/` matches directories only
|
|
10
|
-
* - a leading `/` (or any embedded `/`) anchors the pattern to the project root
|
|
11
|
-
* - `*` matches within a path segment, `**` across segments, `?` a single char
|
|
12
|
-
* - a slash-free pattern (e.g. `_*`, `*.png`) matches a basename at any depth
|
|
13
|
-
*
|
|
14
|
-
* IMPORTANT: ignores apply to UNTRACKED files only. A file already in the project (recorded in the
|
|
15
|
-
* manifest) is always tracked, even if it matches a pattern — so ignoring `*.png` can never make an
|
|
16
|
-
* existing image asset silently disappear from the project. Pruning happens in `scanLocal`.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export type IgnoreMatcher = (key: string, isDir: boolean) => boolean
|
|
20
|
-
|
|
21
|
-
type Rule = { re: RegExp, negate: boolean, dirOnly: boolean }
|
|
22
|
-
|
|
23
|
-
// Always applied, even with no .lecodesignore — only patterns that are never real project assets, so
|
|
24
|
-
// they're safe as invisible defaults. Everything asset-shaped (*.png, *.json, …) is opt-in via the
|
|
25
|
-
// file written at clone time (see DEFAULT_IGNORE_FILE). `.artifacts/` is `lecodes test`'s
|
|
26
|
-
// failure-frame dump dir (tests/.artifacts/) — regenerated scratch, never a project asset.
|
|
27
|
-
// `dist/` is `lecodes app build desktop`'s default output (<root>/dist/<slug>, appDesktop.ts) —
|
|
28
|
-
// same class, but it must be ignored for a second, load-bearing reason: that folder holds a COPY of
|
|
29
|
-
// the host's runtime assets (lit.filamat, uberarchive.bin, …). Collected as project assets they get
|
|
30
|
-
// staged next to the bundle, and onFetchLocal searches the bundle's dir BEFORE the host's assets dir
|
|
31
|
-
// (main.cpp) — so the copy shadows the host's own. Harmless while every host was OpenGL and the
|
|
32
|
-
// bytes matched; fatal once a build output can come from the Vulkan host, where the shadowing
|
|
33
|
-
// lit.filamat is SPIR-V and Filament aborts with "not built for any of the OpenGL backend's
|
|
34
|
-
// supported shader languages".
|
|
35
|
-
// `.env` / `.env.*`: the project's LECODES_* config (projectEnv.ts) — it holds tokens and keystore
|
|
36
|
-
// passwords, so it never leaves the machine whatever .lecodesignore says.
|
|
37
|
-
// `.lightmap/` = a bake's float debug layers (`lecodes lightmap bake --debug-dir`): gigabytes the compile scan must never read
|
|
38
|
-
const BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/", ".env", ".env.*", ".lightmap/"]
|
|
39
|
-
|
|
40
|
-
/** Template `.lecodesignore` written on clone — the explicit, editable "what we don't push" list. */
|
|
41
|
-
export const DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
42
|
-
# Only untracked files are affected — assets already in the project stay tracked
|
|
43
|
-
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log, .artifacts)
|
|
44
|
-
# always apply.
|
|
45
|
-
|
|
46
|
-
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
47
|
-
screenshot.png
|
|
48
|
-
shots/
|
|
49
|
-
|
|
50
|
-
# Design-board comments live on the platform; design/comments/ is the local mirror of the
|
|
51
|
-
# open ones (\`lecodes design comments pull\`) — read by Claude, but pushing it back as
|
|
52
|
-
# project files would only duplicate the database.
|
|
53
|
-
design/comments/
|
|
54
|
-
|
|
55
|
-
# Image & other binary assets ARE pushed by default — they're real project resources.
|
|
56
|
-
# Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
|
|
57
|
-
# *.png
|
|
58
|
-
|
|
59
|
-
# Local notes & dumps (uncomment to ignore)
|
|
60
|
-
# *.json
|
|
61
|
-
# notes.md
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
/** Translate a glob (one path segment's worth of `*`/`?`/`**`) into a regex fragment. */
|
|
65
|
-
const translate = (glob: string): string => {
|
|
66
|
-
let re = ""
|
|
67
|
-
for (let i = 0; i < glob.length; i++) {
|
|
68
|
-
const ch = glob[i]
|
|
69
|
-
if (ch === "*") {
|
|
70
|
-
if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++ } // `**/` spans dirs
|
|
71
|
-
else re += "[^/]*"
|
|
72
|
-
} else if (ch === "?") re += "[^/]"
|
|
73
|
-
else if (ch === "/") re += "/"
|
|
74
|
-
else if ("\\^$.|+()[]{}".includes(ch)) re += "\\" + ch
|
|
75
|
-
else re += ch
|
|
76
|
-
}
|
|
77
|
-
return re
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const compile = (raw: string): Rule | null => {
|
|
81
|
-
let p = raw.trim()
|
|
82
|
-
if (!p || p.startsWith("#")) return null
|
|
83
|
-
let negate = false
|
|
84
|
-
if (p.startsWith("!")) { negate = true; p = p.slice(1) }
|
|
85
|
-
let dirOnly = false
|
|
86
|
-
if (p.endsWith("/")) { dirOnly = true; p = p.slice(0, -1) }
|
|
87
|
-
const anchored = p.startsWith("/") || p.includes("/")
|
|
88
|
-
if (p.startsWith("/")) p = p.slice(1)
|
|
89
|
-
if (!p) return null
|
|
90
|
-
const body = translate(p)
|
|
91
|
-
// `(/.*)?$` lets a directory pattern also match files nested under it, matching gitignore behavior
|
|
92
|
-
// even without relying on directory pruning.
|
|
93
|
-
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$")
|
|
94
|
-
return { re, negate, dirOnly }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const buildMatcher = (rules: Rule[]): IgnoreMatcher => (key, isDir) => {
|
|
98
|
-
let ignored = false
|
|
99
|
-
for (const r of rules) {
|
|
100
|
-
if (r.dirOnly && !isDir) continue // a `foo/` rule only matches dirs directly (files via prune)
|
|
101
|
-
if (r.re.test(key)) ignored = !r.negate
|
|
102
|
-
}
|
|
103
|
-
return ignored
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Compile a standalone glob list (same syntax as .lecodesignore lines, including `!` re-includes)
|
|
107
|
-
* into a file-path matcher — used for app.json's `resources.remote` patterns. */
|
|
108
|
-
export const buildGlobMatcher = (patterns: string[]): ((path: string) => boolean) => {
|
|
109
|
-
// Matching is over FILE paths — normalize `videos/` (dir-only, would match nothing here) to
|
|
110
|
-
// `videos`, whose `(/.*)?$` tail matches everything under the directory.
|
|
111
|
-
const rules = patterns.map((p) => compile(p.trim().replace(/\/+$/, ""))).filter((r): r is Rule => r !== null)
|
|
112
|
-
const match = buildMatcher(rules)
|
|
113
|
-
// Anchored rules compile to `^/…` — they expect the canonical leading-slash key form.
|
|
114
|
-
return (path) => match(path.startsWith("/") ? path : "/" + path, false)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Read `<root>/.lecodesignore` (if any) and combine with the built-in defaults into a matcher. */
|
|
118
|
-
export const loadIgnore = (root: string): IgnoreMatcher => {
|
|
119
|
-
const lines = [...BUILTIN_PATTERNS]
|
|
120
|
-
const file = join(root, ".lecodesignore")
|
|
121
|
-
if (existsSync(file)) {
|
|
122
|
-
try { lines.push(...readFileSync(file, "utf8").split(/\r?\n/)) } catch { /* unreadable → defaults only */ }
|
|
123
|
-
}
|
|
124
|
-
const rules = lines.map(compile).filter((r): r is Rule => r !== null)
|
|
125
|
-
return buildMatcher(rules)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Write the template `.lecodesignore` if the project doesn't have one yet (used on clone). */
|
|
129
|
-
export const writeDefaultIgnore = (root: string) => {
|
|
130
|
-
const file = join(root, ".lecodesignore")
|
|
131
|
-
if (!existsSync(file)) writeFileSync(file, DEFAULT_IGNORE_FILE)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* `.gitignore` for the project's OWN git repo. `.lecodes/` is per-checkout state — regenerable IDE
|
|
135
|
-
* types (`lecodes update`) + `manifest.json`, this machine's push cursor (baseHeadId + per-file shas).
|
|
136
|
-
* Committing it makes collaborators fight over the cursor and ship stale types, so every scaffold
|
|
137
|
-
* path (init / clone / link) makes sure it's ignored. Render outputs mirror DEFAULT_IGNORE_FILE. */
|
|
138
|
-
export const DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
|
|
139
|
-
# (.lecodes/manifest.json). Regenerate with \`lecodes update\` / re-attach with \`lecodes link\`.
|
|
140
|
-
.lecodes/
|
|
141
|
-
|
|
142
|
-
# Render / preview outputs and test failure frames
|
|
143
|
-
screenshot.png
|
|
144
|
-
shots/
|
|
145
|
-
tests/.artifacts/
|
|
146
|
-
tests/.shots/
|
|
147
|
-
|
|
148
|
-
node_modules/
|
|
149
|
-
|
|
150
|
-
dist/
|
|
151
|
-
`
|
|
152
|
-
|
|
153
|
-
/** Create `.gitignore` if missing; if it exists but doesn't ignore `.lecodes/`, append that rule.
|
|
154
|
-
* Returns what happened so callers can log it ("created" | "updated" | null). */
|
|
155
|
-
export const ensureGitignore = (root: string): "created" | "updated" | null => {
|
|
156
|
-
const file = join(root, ".gitignore")
|
|
157
|
-
if (!existsSync(file)) { writeFileSync(file, DEFAULT_GITIGNORE); return "created" }
|
|
158
|
-
const text = readFileSync(file, "utf8")
|
|
159
|
-
const covered = text.split(/\r?\n/).some(l => /^\/?\.lecodes\/?\s*$/.test(l.trim()))
|
|
160
|
-
if (covered) return null
|
|
161
|
-
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n"
|
|
162
|
-
writeFileSync(file, `${text}${sep}\n# LeCodes per-checkout state (generated types + sync cursor) — not source\n.lecodes/\n`)
|
|
163
|
-
return "updated"
|
|
164
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* `.lecodesignore` support — keeps scratch files (temp probes, render screenshots, logs) out of
|
|
6
|
+
* `status` / `push` so they never land in a checkpoint. Syntax is a practical subset of .gitignore:
|
|
7
|
+
* - blank lines and `# comments` are skipped
|
|
8
|
+
* - `!pattern` re-includes something a previous pattern ignored
|
|
9
|
+
* - a trailing `/` matches directories only
|
|
10
|
+
* - a leading `/` (or any embedded `/`) anchors the pattern to the project root
|
|
11
|
+
* - `*` matches within a path segment, `**` across segments, `?` a single char
|
|
12
|
+
* - a slash-free pattern (e.g. `_*`, `*.png`) matches a basename at any depth
|
|
13
|
+
*
|
|
14
|
+
* IMPORTANT: ignores apply to UNTRACKED files only. A file already in the project (recorded in the
|
|
15
|
+
* manifest) is always tracked, even if it matches a pattern — so ignoring `*.png` can never make an
|
|
16
|
+
* existing image asset silently disappear from the project. Pruning happens in `scanLocal`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type IgnoreMatcher = (key: string, isDir: boolean) => boolean
|
|
20
|
+
|
|
21
|
+
type Rule = { re: RegExp, negate: boolean, dirOnly: boolean }
|
|
22
|
+
|
|
23
|
+
// Always applied, even with no .lecodesignore — only patterns that are never real project assets, so
|
|
24
|
+
// they're safe as invisible defaults. Everything asset-shaped (*.png, *.json, …) is opt-in via the
|
|
25
|
+
// file written at clone time (see DEFAULT_IGNORE_FILE). `.artifacts/` is `lecodes test`'s
|
|
26
|
+
// failure-frame dump dir (tests/.artifacts/) — regenerated scratch, never a project asset.
|
|
27
|
+
// `dist/` is `lecodes app build desktop`'s default output (<root>/dist/<slug>, appDesktop.ts) —
|
|
28
|
+
// same class, but it must be ignored for a second, load-bearing reason: that folder holds a COPY of
|
|
29
|
+
// the host's runtime assets (lit.filamat, uberarchive.bin, …). Collected as project assets they get
|
|
30
|
+
// staged next to the bundle, and onFetchLocal searches the bundle's dir BEFORE the host's assets dir
|
|
31
|
+
// (main.cpp) — so the copy shadows the host's own. Harmless while every host was OpenGL and the
|
|
32
|
+
// bytes matched; fatal once a build output can come from the Vulkan host, where the shadowing
|
|
33
|
+
// lit.filamat is SPIR-V and Filament aborts with "not built for any of the OpenGL backend's
|
|
34
|
+
// supported shader languages".
|
|
35
|
+
// `.env` / `.env.*`: the project's LECODES_* config (projectEnv.ts) — it holds tokens and keystore
|
|
36
|
+
// passwords, so it never leaves the machine whatever .lecodesignore says.
|
|
37
|
+
// `.lightmap/` = a bake's float debug layers (`lecodes lightmap bake --debug-dir`): gigabytes the compile scan must never read
|
|
38
|
+
const BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/", "dist/", ".env", ".env.*", ".lightmap/"]
|
|
39
|
+
|
|
40
|
+
/** Template `.lecodesignore` written on clone — the explicit, editable "what we don't push" list. */
|
|
41
|
+
export const DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
42
|
+
# Only untracked files are affected — assets already in the project stay tracked
|
|
43
|
+
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log, .artifacts)
|
|
44
|
+
# always apply.
|
|
45
|
+
|
|
46
|
+
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
47
|
+
screenshot.png
|
|
48
|
+
shots/
|
|
49
|
+
|
|
50
|
+
# Design-board comments live on the platform; design/comments/ is the local mirror of the
|
|
51
|
+
# open ones (\`lecodes design comments pull\`) — read by Claude, but pushing it back as
|
|
52
|
+
# project files would only duplicate the database.
|
|
53
|
+
design/comments/
|
|
54
|
+
|
|
55
|
+
# Image & other binary assets ARE pushed by default — they're real project resources.
|
|
56
|
+
# Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
|
|
57
|
+
# *.png
|
|
58
|
+
|
|
59
|
+
# Local notes & dumps (uncomment to ignore)
|
|
60
|
+
# *.json
|
|
61
|
+
# notes.md
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
/** Translate a glob (one path segment's worth of `*`/`?`/`**`) into a regex fragment. */
|
|
65
|
+
const translate = (glob: string): string => {
|
|
66
|
+
let re = ""
|
|
67
|
+
for (let i = 0; i < glob.length; i++) {
|
|
68
|
+
const ch = glob[i]
|
|
69
|
+
if (ch === "*") {
|
|
70
|
+
if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++ } // `**/` spans dirs
|
|
71
|
+
else re += "[^/]*"
|
|
72
|
+
} else if (ch === "?") re += "[^/]"
|
|
73
|
+
else if (ch === "/") re += "/"
|
|
74
|
+
else if ("\\^$.|+()[]{}".includes(ch)) re += "\\" + ch
|
|
75
|
+
else re += ch
|
|
76
|
+
}
|
|
77
|
+
return re
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const compile = (raw: string): Rule | null => {
|
|
81
|
+
let p = raw.trim()
|
|
82
|
+
if (!p || p.startsWith("#")) return null
|
|
83
|
+
let negate = false
|
|
84
|
+
if (p.startsWith("!")) { negate = true; p = p.slice(1) }
|
|
85
|
+
let dirOnly = false
|
|
86
|
+
if (p.endsWith("/")) { dirOnly = true; p = p.slice(0, -1) }
|
|
87
|
+
const anchored = p.startsWith("/") || p.includes("/")
|
|
88
|
+
if (p.startsWith("/")) p = p.slice(1)
|
|
89
|
+
if (!p) return null
|
|
90
|
+
const body = translate(p)
|
|
91
|
+
// `(/.*)?$` lets a directory pattern also match files nested under it, matching gitignore behavior
|
|
92
|
+
// even without relying on directory pruning.
|
|
93
|
+
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$")
|
|
94
|
+
return { re, negate, dirOnly }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const buildMatcher = (rules: Rule[]): IgnoreMatcher => (key, isDir) => {
|
|
98
|
+
let ignored = false
|
|
99
|
+
for (const r of rules) {
|
|
100
|
+
if (r.dirOnly && !isDir) continue // a `foo/` rule only matches dirs directly (files via prune)
|
|
101
|
+
if (r.re.test(key)) ignored = !r.negate
|
|
102
|
+
}
|
|
103
|
+
return ignored
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Compile a standalone glob list (same syntax as .lecodesignore lines, including `!` re-includes)
|
|
107
|
+
* into a file-path matcher — used for app.json's `resources.remote` patterns. */
|
|
108
|
+
export const buildGlobMatcher = (patterns: string[]): ((path: string) => boolean) => {
|
|
109
|
+
// Matching is over FILE paths — normalize `videos/` (dir-only, would match nothing here) to
|
|
110
|
+
// `videos`, whose `(/.*)?$` tail matches everything under the directory.
|
|
111
|
+
const rules = patterns.map((p) => compile(p.trim().replace(/\/+$/, ""))).filter((r): r is Rule => r !== null)
|
|
112
|
+
const match = buildMatcher(rules)
|
|
113
|
+
// Anchored rules compile to `^/…` — they expect the canonical leading-slash key form.
|
|
114
|
+
return (path) => match(path.startsWith("/") ? path : "/" + path, false)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read `<root>/.lecodesignore` (if any) and combine with the built-in defaults into a matcher. */
|
|
118
|
+
export const loadIgnore = (root: string): IgnoreMatcher => {
|
|
119
|
+
const lines = [...BUILTIN_PATTERNS]
|
|
120
|
+
const file = join(root, ".lecodesignore")
|
|
121
|
+
if (existsSync(file)) {
|
|
122
|
+
try { lines.push(...readFileSync(file, "utf8").split(/\r?\n/)) } catch { /* unreadable → defaults only */ }
|
|
123
|
+
}
|
|
124
|
+
const rules = lines.map(compile).filter((r): r is Rule => r !== null)
|
|
125
|
+
return buildMatcher(rules)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Write the template `.lecodesignore` if the project doesn't have one yet (used on clone). */
|
|
129
|
+
export const writeDefaultIgnore = (root: string) => {
|
|
130
|
+
const file = join(root, ".lecodesignore")
|
|
131
|
+
if (!existsSync(file)) writeFileSync(file, DEFAULT_IGNORE_FILE)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* `.gitignore` for the project's OWN git repo. `.lecodes/` is per-checkout state — regenerable IDE
|
|
135
|
+
* types (`lecodes update`) + `manifest.json`, this machine's push cursor (baseHeadId + per-file shas).
|
|
136
|
+
* Committing it makes collaborators fight over the cursor and ship stale types, so every scaffold
|
|
137
|
+
* path (init / clone / link) makes sure it's ignored. Render outputs mirror DEFAULT_IGNORE_FILE. */
|
|
138
|
+
export const DEFAULT_GITIGNORE = `# LeCodes per-checkout state: generated IDE types + this machine's sync cursor
|
|
139
|
+
# (.lecodes/manifest.json). Regenerate with \`lecodes update\` / re-attach with \`lecodes link\`.
|
|
140
|
+
.lecodes/
|
|
141
|
+
|
|
142
|
+
# Render / preview outputs and test failure frames
|
|
143
|
+
screenshot.png
|
|
144
|
+
shots/
|
|
145
|
+
tests/.artifacts/
|
|
146
|
+
tests/.shots/
|
|
147
|
+
|
|
148
|
+
node_modules/
|
|
149
|
+
|
|
150
|
+
dist/
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
/** Create `.gitignore` if missing; if it exists but doesn't ignore `.lecodes/`, append that rule.
|
|
154
|
+
* Returns what happened so callers can log it ("created" | "updated" | null). */
|
|
155
|
+
export const ensureGitignore = (root: string): "created" | "updated" | null => {
|
|
156
|
+
const file = join(root, ".gitignore")
|
|
157
|
+
if (!existsSync(file)) { writeFileSync(file, DEFAULT_GITIGNORE); return "created" }
|
|
158
|
+
const text = readFileSync(file, "utf8")
|
|
159
|
+
const covered = text.split(/\r?\n/).some(l => /^\/?\.lecodes\/?\s*$/.test(l.trim()))
|
|
160
|
+
if (covered) return null
|
|
161
|
+
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n"
|
|
162
|
+
writeFileSync(file, `${text}${sep}\n# LeCodes per-checkout state (generated types + sync cursor) — not source\n.lecodes/\n`)
|
|
163
|
+
return "updated"
|
|
164
|
+
}
|
|
@@ -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
|
|
29
|
-
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
@@ -9,6 +9,7 @@ import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile
|
|
|
9
9
|
import { readAppConfigAt } from "../commands/app/shared"
|
|
10
10
|
import type { Manifest } from "../project/manifest"
|
|
11
11
|
import { CliError } from "../cli"
|
|
12
|
+
import { buildHalf, copyStatic, halfModule, projectHalves, type ProjectHalf } from "./halves"
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
* `lecodes web build`: the project as a FOLDER a static hosting serves.
|
|
@@ -22,8 +23,13 @@ import { CliError } from "../cli"
|
|
|
22
23
|
* tweens, the canvas, the 3D engine, its twins, AR — a chunk each with
|
|
23
24
|
* the files it fetches; the page's features.json says what
|
|
24
25
|
* a bundle has to name for one to be needed, and whose files they are)
|
|
25
|
-
* plugins/<id>/… the web halves of the plugins the app lists, a folder each
|
|
26
|
-
*
|
|
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)
|
|
27
33
|
* assets/<path> the project's assets — only the ones the bundle names
|
|
28
34
|
* shaders/<hash>_webgl-lite.filamat
|
|
29
35
|
* the project's .mat shaders the bundle names, in the one form Web Lite
|
|
@@ -146,6 +152,12 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
|
|
|
146
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.)")
|
|
147
153
|
}
|
|
148
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
|
+
|
|
149
161
|
// ---- the folder ----------------------------------------------------------------------------------------------
|
|
150
162
|
rmSync(out, { recursive: true, force: true })
|
|
151
163
|
mkdirSync(out, { recursive: true })
|
|
@@ -161,19 +173,31 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
|
|
|
161
173
|
}
|
|
162
174
|
rmSync(table)
|
|
163
175
|
}
|
|
164
|
-
// …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.
|
|
165
181
|
const pluginsTable = join(out, "plugins.json")
|
|
166
|
-
if (existsSync(pluginsTable)) {
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
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[]> = {}
|
|
170
185
|
for (const id of Object.keys(plugins)) {
|
|
171
186
|
if (listed.has(id)) kept[id] = plugins[id]!
|
|
172
187
|
else rmSync(join(out, "plugins", id), { recursive: true, force: true })
|
|
173
188
|
}
|
|
174
|
-
for (const
|
|
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).`)
|
|
175
199
|
if (Object.keys(kept).length > 0) writeFileSync(pluginsTable, JSON.stringify({ plugins: kept }, null, 2))
|
|
176
|
-
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 }) }
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
const bundle = `app.${createHash("sha256").update(js).digest("hex").slice(0, 10)}.js`
|
|
@@ -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};
|