lecodes-cli 0.20.2 → 2.0.0
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/README.md +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
package/src/commands/navmesh.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
3
|
import { basename, join, relative, resolve } from "node:path"
|
|
4
|
-
import { findProjectRoot } from "../manifest"
|
|
5
4
|
import { buildBundle } from "./compile"
|
|
6
|
-
import { readAppConfigAt, resolveDesktopRenderer } from "./
|
|
7
|
-
import { runDesktopRender } from "../desktopRenderer"
|
|
8
|
-
import { CliError,
|
|
5
|
+
import { readAppConfigAt, resolveDesktopRenderer } from "./app/shared"
|
|
6
|
+
import { runDesktopRender } from "../hosts/desktopRenderer"
|
|
7
|
+
import { CliError, bool, c, defineCommand, logErr, note, num, str, warnErr } from "../cli"
|
|
8
|
+
import { bundleFlags, bundleOptions, localProject } from "./shared"
|
|
9
9
|
import { loadCreatorNav, NavBuilder, readNgeo, navMeshTriangles, type BuildParams } from "lecodes-creator-nav"
|
|
10
10
|
|
|
11
11
|
/*
|
|
@@ -22,15 +22,18 @@ import { loadCreatorNav, NavBuilder, readNgeo, navMeshTriangles, type BuildParam
|
|
|
22
22
|
*
|
|
23
23
|
* Running the build in the CLI keeps the host generic (a geometry dump, useful on its own),
|
|
24
24
|
* lets the bake parameters iterate without a host rebuild, and is the same code a future headless
|
|
25
|
-
* bake reuses. The desktop host must know the dump (built from
|
|
25
|
+
* bake reuses. The desktop host must know the dump (built from hosts/desktop, or a release that
|
|
26
26
|
* ships it: LECODES_DESKTOP_EXE for a local build until then).
|
|
27
|
+
*
|
|
28
|
+
* The agent size (--radius/--height/--climb/--slope) defaults to what the scene declares — the
|
|
29
|
+
* host's `[navmesh] config` line carries it — so those flags have no static default.
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const n =
|
|
33
|
-
if (
|
|
32
|
+
/** A numeric flag Recast gets: the framework parses it, the range is checked here. `fallback` is
|
|
33
|
+
* the scene's value for the agent-size flags. */
|
|
34
|
+
const range = (name: string, value: number | undefined, fallback: number, min: number, max: number): number => {
|
|
35
|
+
const n = value ?? fallback
|
|
36
|
+
if (n < min || n > max) throw new CliError(`--${name} wants a number in ${min}..${max}, got "${n}".`)
|
|
34
37
|
return n
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -85,141 +88,168 @@ const writeObj = (path: string, tris: Float32Array): void => {
|
|
|
85
88
|
writeFileSync(path, lines.join("\n") + "\n")
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
const bake =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
const bake = defineCommand({
|
|
92
|
+
name: "bake",
|
|
93
|
+
summary: "Build the level's navigation mesh for NavAgent / NavMesh",
|
|
94
|
+
description: "The desktop host dumps every STATIC body's collision triangles at the scene's env.navmesh (or NavMesh.load) point, then Recast builds the tiled mesh here → <out>/<name>.navmesh. The agent size defaults to what the scene declares. Needs a desktop host with the geometry dump (LECODES_DESKTOP_EXE for a local build).",
|
|
95
|
+
flags: {
|
|
96
|
+
out: str("where it lands", { value: "<dir>", default: "assets/nav" }),
|
|
97
|
+
name: str("file name (default: the scene file that declares navmesh:, else scene)", { value: "<n>" }),
|
|
98
|
+
...bundleFlags,
|
|
99
|
+
entry: str("the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load", { value: "<file>", default: "main.ts" }),
|
|
100
|
+
radius: num("agent radius (m; default: the scene's)"),
|
|
101
|
+
height: num("agent height (m; default: the scene's)"),
|
|
102
|
+
climb: num("agent max step height (m; default: the scene's)"),
|
|
103
|
+
slope: num("agent max walkable slope (deg; default: the scene's)"),
|
|
104
|
+
cell: num("voxel size (m)", { default: 0.25 }),
|
|
105
|
+
"cell-height": num("voxel height (m)", { default: 0.1 }),
|
|
106
|
+
tile: num("tile size (cells; 0 = one tile)", { default: 48 }),
|
|
107
|
+
"region-min": num("min region area (cells²)", { default: 8 }),
|
|
108
|
+
"region-merge": num("regions smaller than this merge (cells²)", { default: 20 }),
|
|
109
|
+
"edge-max": num("max polygon edge length (voxels)", { default: 12 }),
|
|
110
|
+
"edge-error": num("max edge simplification error (voxels)", { default: 1.3 }),
|
|
111
|
+
detail: num("detail mesh sample distance (voxels)", { default: 6 }),
|
|
112
|
+
"detail-error": num("detail mesh max error (voxels)", { default: 1 }),
|
|
113
|
+
obj: bool("also write <name>.navmesh.obj to inspect the polygons"),
|
|
114
|
+
"keep-geometry": bool("keep the host's <name>.navgeo dump"),
|
|
115
|
+
frames: num("host frames to run before giving up — load headroom", { default: 900 }),
|
|
116
|
+
timeout: num("host time cap (ms)", { default: 600000 }),
|
|
117
|
+
width: num("host window width (default: app.json desktop.window, else 1280)"),
|
|
118
|
+
renderer: str("desktop host + shader variant (default: app.json desktop.renderer, else gl)", { value: "<gl|vulkan>" }),
|
|
119
|
+
logs: bool("forward the host's output"),
|
|
120
|
+
},
|
|
121
|
+
examples: ["lecodes navmesh bake", "lecodes navmesh bake --name yard --cell 0.2 --obj --logs"],
|
|
122
|
+
run: async ({ flags }) => {
|
|
123
|
+
const { root } = localProject()
|
|
124
|
+
const outDir = resolve(root, flags.out)
|
|
125
|
+
const entryFile = flags.entry
|
|
126
|
+
if (!existsSync(resolve(root, entryFile))) {
|
|
127
|
+
throw new CliError(`No ${entryFile} in ${root} — pass --entry <file> (the file that builds the level: a scene file with env.navmesh, or code calling NavMesh.load).`)
|
|
128
|
+
}
|
|
129
|
+
const name = flags.name ?? guessName(root) ?? "scene"
|
|
130
|
+
const frames = range("frames", flags.frames, 900, 30, 100000)
|
|
131
|
+
const win = readAppConfigAt(root)?.desktop?.window
|
|
132
|
+
const width = flags.width || win?.width || 1280
|
|
133
|
+
const height = win?.height || 720
|
|
134
|
+
const logs = flags.logs
|
|
111
135
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
// The navmesh file must exist BEFORE the compile: the scene references it through asset(), and a
|
|
137
|
+
// missing asset is a broken bundle. The first bake starts from a placeholder the real file overwrites.
|
|
138
|
+
mkdirSync(outDir, { recursive: true })
|
|
139
|
+
const navmeshPath = join(outDir, `${name}.navmesh`)
|
|
140
|
+
if (!existsSync(navmeshPath)) writeFileSync(navmeshPath, placeholderLnav())
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
entryOverride: entryPath,
|
|
122
|
-
})
|
|
142
|
+
const importPath = "./" + entryFile.replace(/\\/g, "/").replace(/\.tsx?$/, "")
|
|
143
|
+
const entryPath = "/.navmesh-entry.ts"
|
|
144
|
+
const cfg = { outDir: outDir.replace(/\\/g, "/"), name }
|
|
145
|
+
const entryText = `globalThis.__lecodesNavmesh = ${JSON.stringify(cfg)}\nimport ${JSON.stringify(importPath)}\n`
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 600000
|
|
130
|
-
const res = await runDesktopRender(js, resources, {
|
|
131
|
-
renderer: desktopRenderer, width, height, frames, outPng, logs, timeoutMs,
|
|
132
|
-
stopOnLine: (chunk) => chunk.includes("[navmesh] done") || chunk.includes("[navmesh] error"),
|
|
147
|
+
const desktopRenderer = resolveDesktopRenderer(root, flags.renderer)
|
|
148
|
+
const { js, resources } = await buildBundle(bundleOptions(flags), {
|
|
149
|
+
localAssets: true, desktopRenderer,
|
|
150
|
+
extraEntries: [{ path: entryPath, type: "text", text: entryText }],
|
|
151
|
+
entryOverride: entryPath,
|
|
133
152
|
})
|
|
134
|
-
output = res.output
|
|
135
|
-
} catch (e) {
|
|
136
|
-
const msg = e instanceof Error ? e.message : String(e)
|
|
137
|
-
if (!/\[navmesh\] done/.test(msg)) throw e
|
|
138
|
-
output = msg
|
|
139
|
-
} finally {
|
|
140
|
-
rmSync(scratch, { recursive: true, force: true })
|
|
141
|
-
}
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
const scratch = mkdtempSync(join(tmpdir(), "lecodes-navmesh-"))
|
|
155
|
+
const outPng = join(scratch, "frame.png")
|
|
156
|
+
note(`Dumping the level's static collision geometry through the desktop host → ${relative(root, outDir) || "."}/${name}.navgeo…`)
|
|
157
|
+
let output = ""
|
|
158
|
+
try {
|
|
159
|
+
const res = await runDesktopRender(js, resources, {
|
|
160
|
+
renderer: desktopRenderer, width, height, frames, outPng, logs, timeoutMs: flags.timeout || 600000,
|
|
161
|
+
stopOnLine: (chunk) => chunk.includes("[navmesh] done") || chunk.includes("[navmesh] error"),
|
|
162
|
+
})
|
|
163
|
+
output = res.output
|
|
164
|
+
} catch (e) {
|
|
165
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
166
|
+
if (!/\[navmesh\] done/.test(msg)) throw e
|
|
167
|
+
output = msg
|
|
168
|
+
} finally {
|
|
169
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
158
170
|
}
|
|
159
|
-
if (lines.some((l) => /error/.test(l))) throw new CliError("The bake failed — see the lines above.")
|
|
160
|
-
warnErr("The host ran to its frame cap without dumping geometry. Make sure the level declares `env.navmesh` (a scene file) or calls NavMesh.load(...) after its static bodies exist, and skips menus when NavMesh.baking is true. More load headroom: --frames <n>. Host output: --logs.")
|
|
161
|
-
throw new CliError("Nothing was baked.")
|
|
162
|
-
}
|
|
163
|
-
if (!config) throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?")
|
|
164
|
-
const geoPath = join(outDir, `${name}.navgeo`)
|
|
165
|
-
if (!existsSync(geoPath)) throw new CliError(`The host reported a dump but ${geoPath} is missing.`)
|
|
166
171
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
regionMergeSize: num(args, "region-merge", 20, 0, 1000),
|
|
179
|
-
edgeMaxLen: num(args, "edge-max", 12, 0, 1000),
|
|
180
|
-
edgeMaxError: num(args, "edge-error", 1.3, 0.1, 10),
|
|
181
|
-
detailSampleDist: num(args, "detail", 6, 0, 100),
|
|
182
|
-
detailMaxError: num(args, "detail-error", 1, 0, 100),
|
|
183
|
-
}
|
|
184
|
-
note(`Building the navmesh: ${geo.triangleCount} triangles from ${geo.bodyCount} static bodies, agent r ${params.agentRadius} / h ${params.agentHeight} / climb ${params.agentMaxClimb} / slope ${params.agentMaxSlope}°, cell ${params.cellSize} m…`)
|
|
185
|
-
const mod = await loadCreatorNav()
|
|
186
|
-
const builder = new NavBuilder(mod, params)
|
|
187
|
-
let result
|
|
188
|
-
try {
|
|
189
|
-
builder.addSoup(geo.triangles, geo.areas)
|
|
190
|
-
const meta = {
|
|
191
|
-
scene: name, areas: config.areas, costs: config.costs, bakedAt: new Date().toISOString(),
|
|
192
|
-
params, triangles: geo.triangleCount, bodies: geo.bodyCount,
|
|
172
|
+
// the markers are console.log lines from the SDK — the host prefixes them ("[DEBUG] [navmesh] …")
|
|
173
|
+
const lines = output.split(/\r?\n/).filter((l) => l.includes("[navmesh]")).map((l) => l.slice(l.indexOf("[navmesh]")))
|
|
174
|
+
let config: BakeConfigLine | null = null
|
|
175
|
+
let triangles = 0
|
|
176
|
+
for (const l of lines) {
|
|
177
|
+
const text = l.slice("[navmesh]".length).trim()
|
|
178
|
+
if (text === "done") continue
|
|
179
|
+
if (text.startsWith("config ")) { try { config = JSON.parse(text.slice(7)) as BakeConfigLine } catch { /* reported below */ } continue }
|
|
180
|
+
const geo = /^geometry (\d+) triangles/.exec(text)
|
|
181
|
+
if (geo) triangles = Number(geo[1])
|
|
182
|
+
logErr(text.startsWith("error") ? c.red(`✗ ${text}`) : c.dim(` ${text}`))
|
|
193
183
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
writeFileSync(navmeshPath, result.data)
|
|
206
|
-
if (obj) {
|
|
207
|
-
const objPath = join(outDir, `${name}.navmesh.obj`)
|
|
208
|
-
writeObj(objPath, navMeshTriangles(mod, result.data))
|
|
209
|
-
logErr(c.dim(` wrote ${relative(root, objPath)} (the polygon mesh, for inspection)`))
|
|
210
|
-
}
|
|
211
|
-
if (!keepGeometry) unlinkSync(geoPath)
|
|
212
|
-
const kb = Math.round(statSync(navmeshPath).size / 1024)
|
|
213
|
-
const b = result.bounds
|
|
214
|
-
logErr(c.dim(` ${result.tiles} tiles, ${result.polys} polygons, ${(b.max[0] - b.min[0]).toFixed(0)} × ${(b.max[2] - b.min[2]).toFixed(0)} m, ${result.time.toFixed(1)} s`))
|
|
215
|
-
logErr(c.green(`✓ baked → ${relative(root, navmeshPath).replace(/\\/g, "/")} (${kb} KB)`))
|
|
216
|
-
if (!config.data) {
|
|
217
|
-
logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`))
|
|
218
|
-
}
|
|
219
|
-
}
|
|
184
|
+
if (!/\[navmesh\] done/.test(output)) {
|
|
185
|
+
if (lines.some((l) => /cannot dump collision geometry/.test(l))) {
|
|
186
|
+
throw new CliError("This desktop host has no collision-geometry dump. Build hosts/desktop and point LECODES_DESKTOP_EXE at build/lecodes-desktop.exe, or update: lecodes desktop update.")
|
|
187
|
+
}
|
|
188
|
+
if (lines.some((l) => /error/.test(l))) throw new CliError("The bake failed — see the lines above.")
|
|
189
|
+
warnErr("The host ran to its frame cap without dumping geometry. Make sure the level declares `env.navmesh` (a scene file) or calls NavMesh.load(...) after its static bodies exist, and skips menus when NavMesh.baking is true. More load headroom: --frames <n>. Host output: --logs.")
|
|
190
|
+
throw new CliError("Nothing was baked.")
|
|
191
|
+
}
|
|
192
|
+
if (!config) throw new CliError("The host reported a dump but no `[navmesh] config` line — SDK/host mismatch?")
|
|
193
|
+
const geoPath = join(outDir, `${name}.navgeo`)
|
|
194
|
+
if (!existsSync(geoPath)) throw new CliError(`The host reported a dump but ${geoPath} is missing.`)
|
|
220
195
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
196
|
+
// ---- Recast (wasm) ----
|
|
197
|
+
const geo = readNgeo(new Uint8Array(readFileSync(geoPath)))
|
|
198
|
+
const params: BuildParams = {
|
|
199
|
+
agentRadius: range("radius", flags.radius, config.agent.radius, 0.05, 10),
|
|
200
|
+
agentHeight: range("height", flags.height, config.agent.height, 0.1, 20),
|
|
201
|
+
agentMaxClimb: range("climb", flags.climb, config.agent.climb, 0, 10),
|
|
202
|
+
agentMaxSlope: range("slope", flags.slope, config.agent.slope, 0, 89),
|
|
203
|
+
cellSize: range("cell", flags.cell, 0.25, 0.01, 5),
|
|
204
|
+
cellHeight: range("cell-height", flags["cell-height"], 0.1, 0.01, 5),
|
|
205
|
+
tileSize: range("tile", flags.tile, 48, 0, 1024),
|
|
206
|
+
regionMinSize: range("region-min", flags["region-min"], 8, 0, 1000),
|
|
207
|
+
regionMergeSize: range("region-merge", flags["region-merge"], 20, 0, 1000),
|
|
208
|
+
edgeMaxLen: range("edge-max", flags["edge-max"], 12, 0, 1000),
|
|
209
|
+
edgeMaxError: range("edge-error", flags["edge-error"], 1.3, 0.1, 10),
|
|
210
|
+
detailSampleDist: range("detail", flags.detail, 6, 0, 100),
|
|
211
|
+
detailMaxError: range("detail-error", flags["detail-error"], 1, 0, 100),
|
|
212
|
+
}
|
|
213
|
+
note(`Building the navmesh: ${geo.triangleCount} triangles from ${geo.bodyCount} static bodies, agent r ${params.agentRadius} / h ${params.agentHeight} / climb ${params.agentMaxClimb} / slope ${params.agentMaxSlope}°, cell ${params.cellSize} m…`)
|
|
214
|
+
const mod = await loadCreatorNav()
|
|
215
|
+
const builder = new NavBuilder(mod, params)
|
|
216
|
+
let result
|
|
217
|
+
try {
|
|
218
|
+
builder.addSoup(geo.triangles, geo.areas)
|
|
219
|
+
const meta = {
|
|
220
|
+
scene: name, areas: config.areas, costs: config.costs, bakedAt: new Date().toISOString(),
|
|
221
|
+
params, triangles: geo.triangleCount, bodies: geo.bodyCount,
|
|
222
|
+
}
|
|
223
|
+
let lastTile = -1
|
|
224
|
+
result = builder.run(meta, (tile, count, message) => {
|
|
225
|
+
if (message) logErr(c.dim(` recast: ${message}`))
|
|
226
|
+
else if (count > 8 && tile !== lastTile && (tile + 1) % Math.max(1, Math.floor(count / 8)) === 0) { lastTile = tile; logErr(c.dim(` tile ${tile + 1}/${count}`)) }
|
|
227
|
+
})
|
|
228
|
+
} finally {
|
|
229
|
+
builder.destroy()
|
|
230
|
+
}
|
|
231
|
+
if (result.polys === 0) {
|
|
232
|
+
throw new CliError("The build produced no polygons — nothing walkable for this agent. Check the cell size against the level's scale, the slope, and that the floor is a STATIC body (Shape + Physics { motion: 'static' }).")
|
|
233
|
+
}
|
|
234
|
+
writeFileSync(navmeshPath, result.data)
|
|
235
|
+
if (flags.obj) {
|
|
236
|
+
const objPath = join(outDir, `${name}.navmesh.obj`)
|
|
237
|
+
writeObj(objPath, navMeshTriangles(mod, result.data))
|
|
238
|
+
logErr(c.dim(` wrote ${relative(root, objPath)} (the polygon mesh, for inspection)`))
|
|
239
|
+
}
|
|
240
|
+
if (!flags["keep-geometry"]) unlinkSync(geoPath)
|
|
241
|
+
const kb = Math.round(statSync(navmeshPath).size / 1024)
|
|
242
|
+
const b = result.bounds
|
|
243
|
+
logErr(c.dim(` ${result.tiles} tiles, ${result.polys} polygons, ${(b.max[0] - b.min[0]).toFixed(0)} × ${(b.max[2] - b.min[2]).toFixed(0)} m, ${result.time.toFixed(1)} s`))
|
|
244
|
+
logErr(c.green(`✓ baked → ${relative(root, navmeshPath).replace(/\\/g, "/")} (${kb} KB)`))
|
|
245
|
+
if (!config.data) {
|
|
246
|
+
logErr(c.dim(` load it with env.navmesh: { data: asset('<rel>/${basename(navmeshPath)}') } in the scene file, or NavMesh.load(scene, { data: asset(...) })`))
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
export default defineCommand({
|
|
252
|
+
name: "navmesh",
|
|
253
|
+
summary: "Build the level's navigation mesh (bake)",
|
|
254
|
+
commands: [bake],
|
|
255
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CliError, bool, c, defineCommand, info, log, success } from "../cli"
|
|
2
|
+
import { generate } from "../plugin/generate"
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* `lecodes plugin` — for the AUTHOR of a native plugin (the app's side is `lecodes app`, which
|
|
6
|
+
* installs plugins into a shell).
|
|
7
|
+
*
|
|
8
|
+
* plugin gen a plugin's contract.d.ts → the generated files beside its hand-written ones.
|
|
9
|
+
* The contract is the one description of the channel; what is generated from it is
|
|
10
|
+
* committed, and `--check` is what a CI runs to refuse a stale file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const gen = defineCommand({
|
|
14
|
+
name: "gen",
|
|
15
|
+
summary: "Generate a plugin's files from its contract.d.ts (--check: fail on a stale one)",
|
|
16
|
+
usage: "[path]",
|
|
17
|
+
description: "The path is one plugin (a directory with a contract.d.ts) or a repo that keeps its plugins in plugins/<id>/; the current directory when omitted.\n\nWrites sdk/<id>.gen.ts — the wrapper an app calls: the wire call by call, the class with the wire's methods as its own, and the public export when the plugin has no hand-written sdk/<id>.ts. Where the plugin has an ios/ folder, writes ios/<Name>.gen.swift — the contract's types, the protocol the plugin implements and the glue that reads a call off the wire. Where it has an android/ folder, writes the same as <Name>.gen.kt into the folder of the plugin's Kotlin package (android/ is laid out as a Gradle module: the package is the folder the path under android/src/main/java narrows down to). Where it has a web/ folder, writes web/<id>.gen.ts — the same for the web half, in a file that imports nothing; a web half that imports a LeCodes package is refused, because it would run on that host only. A contract the generator does not understand is an error with its line, and every problem of a file is reported at once.\n\nGenerated files are committed. Reading a contract needs TypeScript in the plugin's repo.",
|
|
18
|
+
flags: {
|
|
19
|
+
check: bool("write nothing; exit 1 when a generated file is missing or stale"),
|
|
20
|
+
},
|
|
21
|
+
examples: ["lecodes plugin gen", "lecodes plugin gen plugins/camera", "lecodes plugin gen --check"],
|
|
22
|
+
run: ({ args, flags }) => {
|
|
23
|
+
const report = generate(args[0] ?? ".", { check: flags.check })
|
|
24
|
+
|
|
25
|
+
for (const p of report.plugins) {
|
|
26
|
+
const channels = p.contract.channels.map(ch => `${ch.kind} "${ch.name}" v${ch.version}`).join(", ")
|
|
27
|
+
info(`${c.bold(p.id)} ${c.dim(channels)}`)
|
|
28
|
+
for (const f of p.written) log(c.green(` written ${f}`))
|
|
29
|
+
for (const f of p.stale) log(c.red(` stale ${f}`))
|
|
30
|
+
for (const f of p.fresh) log(c.dim(` fresh ${f}`))
|
|
31
|
+
}
|
|
32
|
+
for (const failure of report.failures) log(c.red(failure))
|
|
33
|
+
|
|
34
|
+
const stale = report.plugins.reduce((n, p) => n + p.stale.length, 0)
|
|
35
|
+
if (report.failures.length) throw new CliError(`${report.failures.length} contract${report.failures.length === 1 ? "" : "s"} did not generate.`)
|
|
36
|
+
if (stale) throw new CliError(`${stale} generated file${stale === 1 ? " is" : "s are"} stale — run \`lecodes plugin gen\` and commit.`)
|
|
37
|
+
success(flags.check ? "Every generated file is fresh." : "Generated.")
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export default defineCommand({
|
|
42
|
+
name: "plugin",
|
|
43
|
+
summary: "Write a native plugin: gen",
|
|
44
|
+
description: "For the author of a plugin — a capability an app reaches the same way on iOS, Android and the web. `lecodes app` is the other side: it installs plugins into an app's shell.",
|
|
45
|
+
commands: [gen],
|
|
46
|
+
})
|