pelican-engine 0.0.0-stage → 0.3.1
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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
package/lib/tools/cli.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile, cp, access, lstat, } from "node:fs/promises";
|
|
3
|
+
import { build as viteBuild, createServer } from "vite";
|
|
4
|
+
import { compileProject } from "../compiler/index.js";
|
|
5
|
+
import { SDK_VERSION } from "../sdk/index.js";
|
|
6
|
+
import { generateApiIndex, queryApiSymbol } from "./api-index.js";
|
|
7
|
+
import { optimizeModel } from "../assets/optimize.js";
|
|
8
|
+
import { compressDist } from "./compress-dist.js";
|
|
9
|
+
import { doctor } from "./doctor.js";
|
|
10
|
+
import { setup } from "./setup.js";
|
|
11
|
+
import { attachControl } from "./control-server.js";
|
|
12
|
+
import { createControlClient } from "./control-client.js";
|
|
13
|
+
import { diagnosticCode } from "./diagnostic-code.js";
|
|
14
|
+
import { cliLayout, createMinimalProject, prepareInstalledDevProject, installedDoctor, writeAuthorEntry, } from "./npm/layout.js";
|
|
15
|
+
import { automationLaunchOptions, automationURL, waitForEngine, withAutomationDeadline, } from "./browser-session.js";
|
|
16
|
+
const layout = cliLayout(import.meta.url);
|
|
17
|
+
const engineRoot = layout.root;
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const activeManifest = JSON.parse(await readFile(path.join(engineRoot, "engine.manifest.json"), "utf8"));
|
|
20
|
+
const defaultProject = layout.installed
|
|
21
|
+
? process.cwd()
|
|
22
|
+
: path.join(engineRoot, activeManifest.milestone === "M3" ? "examples/m3" : "examples/m2");
|
|
23
|
+
const command = args[0] ?? "help";
|
|
24
|
+
const option = (name, fallback) => {
|
|
25
|
+
const index = args.indexOf(`--${name}`);
|
|
26
|
+
return index >= 0 ? args[index + 1] : fallback;
|
|
27
|
+
};
|
|
28
|
+
const projectRoot = path.resolve(command === "api" ? defaultProject : option("project", defaultProject));
|
|
29
|
+
const taskId = `${command}-${Date.now()}`;
|
|
30
|
+
const envelope = (status, data, diagnostics = [], artifacts = []) => ({
|
|
31
|
+
protocolVersion: "0.1.0",
|
|
32
|
+
taskId,
|
|
33
|
+
status,
|
|
34
|
+
diagnostics,
|
|
35
|
+
artifacts: artifacts.map((path) => ({
|
|
36
|
+
type: "file",
|
|
37
|
+
path: path.replaceAll("\\", "/"),
|
|
38
|
+
})),
|
|
39
|
+
data,
|
|
40
|
+
});
|
|
41
|
+
const emit = (result) => process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
42
|
+
const target = path.join(projectRoot, ".pelican/public/generated");
|
|
43
|
+
async function compile() {
|
|
44
|
+
await mkdir(target, { recursive: true });
|
|
45
|
+
return compileProject({ projectRoot, outDir: target, engineRoot });
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
if (command === "help") {
|
|
49
|
+
emit(envelope("succeeded", {
|
|
50
|
+
commands: [
|
|
51
|
+
"doctor",
|
|
52
|
+
"setup [--install-browser] [--offline]",
|
|
53
|
+
"create --project <directory> [--template minimal|m2]",
|
|
54
|
+
"check --project <directory>",
|
|
55
|
+
"compile --project <directory>",
|
|
56
|
+
"dev --project <directory> [--port 4173]",
|
|
57
|
+
"build --project <directory>",
|
|
58
|
+
"optimize --project <directory> --asset <id> [--output <new-project-relative.glb>]",
|
|
59
|
+
"capture [--mode raster|hybrid|path] [--output file.png]",
|
|
60
|
+
"inspect | step --count <n> | pause | resume [--port <dev-port>]",
|
|
61
|
+
"input --key ArrowRight [--pressed true] [--port <dev-port>]",
|
|
62
|
+
"snapshot --output <file> | restore --file <file>",
|
|
63
|
+
"record --action start|stop|cancel | replay --file <trace.json>",
|
|
64
|
+
"status | cancel --task <id> | reset-session | api [--symbol <name>]",
|
|
65
|
+
],
|
|
66
|
+
stage: activeManifest.stage,
|
|
67
|
+
}));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (command === "doctor") {
|
|
71
|
+
const report = layout.installed
|
|
72
|
+
? await installedDoctor(engineRoot, taskId)
|
|
73
|
+
: await doctor({ projectRoot: engineRoot, taskId });
|
|
74
|
+
emit(report);
|
|
75
|
+
if (report.status !== "succeeded")
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (command === "api") {
|
|
80
|
+
const index = layout.installed
|
|
81
|
+
? JSON.parse(await readFile(path.join(engineRoot, "docs/api/index.json"), "utf8"))
|
|
82
|
+
: await generateApiIndex(engineRoot);
|
|
83
|
+
const matches = queryApiSymbol(index.symbols, args.slice(1));
|
|
84
|
+
emit(envelope("succeeded", matches === undefined
|
|
85
|
+
? { engineVersion: SDK_VERSION, symbols: index.symbols.length }
|
|
86
|
+
: { engineVersion: SDK_VERSION, symbols: matches }, [], ["docs/api/index.json", "docs/api/REFERENCE.md"].map((file) => layout.installed ? path.join(engineRoot, file) : file)));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if ([
|
|
90
|
+
"inspect",
|
|
91
|
+
"step",
|
|
92
|
+
"pause",
|
|
93
|
+
"resume",
|
|
94
|
+
"input",
|
|
95
|
+
"snapshot",
|
|
96
|
+
"restore",
|
|
97
|
+
"record",
|
|
98
|
+
"replay",
|
|
99
|
+
"status",
|
|
100
|
+
"cancel",
|
|
101
|
+
"reset-session",
|
|
102
|
+
].includes(command) ||
|
|
103
|
+
(command === "capture" && args.includes("--live"))) {
|
|
104
|
+
const client = createControlClient({
|
|
105
|
+
url: `http://127.0.0.1:${Number(option("port", "4173"))}/`,
|
|
106
|
+
});
|
|
107
|
+
let response;
|
|
108
|
+
if (command === "status")
|
|
109
|
+
response = await client.status();
|
|
110
|
+
else if (command === "cancel") {
|
|
111
|
+
const task = option("task");
|
|
112
|
+
if (!task)
|
|
113
|
+
throw new Error("TC_CLI_TASK: --task is required.");
|
|
114
|
+
response = await client.cancel(task);
|
|
115
|
+
}
|
|
116
|
+
else if (command === "reset-session")
|
|
117
|
+
response = await client.reset();
|
|
118
|
+
else {
|
|
119
|
+
let operation;
|
|
120
|
+
if (command === "capture")
|
|
121
|
+
operation = { type: "capture" };
|
|
122
|
+
else if (command === "inspect")
|
|
123
|
+
operation = {
|
|
124
|
+
type: "inspect",
|
|
125
|
+
...(option("entity") ? { entity: option("entity") } : {}),
|
|
126
|
+
...(option("offset") ? { offset: Number(option("offset")) } : {}),
|
|
127
|
+
...(option("limit") ? { limit: Number(option("limit")) } : {}),
|
|
128
|
+
};
|
|
129
|
+
else if (command === "step")
|
|
130
|
+
operation = { type: "step", count: Number(option("count", "1")) };
|
|
131
|
+
else if (command === "input") {
|
|
132
|
+
const pressed = option("pressed", "true");
|
|
133
|
+
if (!["true", "false"].includes(pressed))
|
|
134
|
+
throw new Error("TC_CLI_INPUT: --pressed must be true or false.");
|
|
135
|
+
operation = {
|
|
136
|
+
type: "input",
|
|
137
|
+
key: option("key", ""),
|
|
138
|
+
pressed: pressed === "true",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
else if (command === "restore" || command === "replay") {
|
|
142
|
+
const file = option("file");
|
|
143
|
+
if (!file)
|
|
144
|
+
throw new Error("TC_CLI_FILE: --file is required.");
|
|
145
|
+
const data = JSON.parse(await readFile(path.resolve(file), "utf8"));
|
|
146
|
+
operation =
|
|
147
|
+
command === "restore"
|
|
148
|
+
? { type: "restore", snapshot: data.data ?? data }
|
|
149
|
+
: { type: "replay", trace: data.data ?? data };
|
|
150
|
+
}
|
|
151
|
+
else if (command === "record") {
|
|
152
|
+
const action = option("action", "start");
|
|
153
|
+
if (!["start", "stop", "cancel"].includes(action))
|
|
154
|
+
throw new Error("TC_CLI_RECORD: action must be start, stop or cancel.");
|
|
155
|
+
operation = {
|
|
156
|
+
type: action === "start"
|
|
157
|
+
? "recordStart"
|
|
158
|
+
: action === "stop"
|
|
159
|
+
? "recordStop"
|
|
160
|
+
: "recordCancel",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
operation = {
|
|
165
|
+
type: command,
|
|
166
|
+
};
|
|
167
|
+
response = await client.send(operation, option("task") ? { taskId: option("task") } : {});
|
|
168
|
+
}
|
|
169
|
+
if (command === "capture" && response.status === "succeeded") {
|
|
170
|
+
const data = response.data;
|
|
171
|
+
const buffer = Buffer.from(data.base64, "base64");
|
|
172
|
+
if (data.format !== "png" ||
|
|
173
|
+
buffer.length !== data.bytes ||
|
|
174
|
+
buffer.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a")
|
|
175
|
+
throw new Error("TC_CAPTURE_FORMAT: Invalid captured image.");
|
|
176
|
+
const output = path.resolve(option("output", path.join(projectRoot, ".pelican/live-capture.png")));
|
|
177
|
+
if (path.extname(output).toLowerCase() !== ".png")
|
|
178
|
+
throw new Error("TC_CAPTURE_EXTENSION: Live capture output must be a PNG file.");
|
|
179
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
180
|
+
await writeFile(output, buffer);
|
|
181
|
+
response.data = {
|
|
182
|
+
format: data.format,
|
|
183
|
+
width: data.width,
|
|
184
|
+
height: data.height,
|
|
185
|
+
bytes: data.bytes,
|
|
186
|
+
};
|
|
187
|
+
response.artifacts.push({ type: "image", path: output });
|
|
188
|
+
}
|
|
189
|
+
else if (option("output") && response.status === "succeeded") {
|
|
190
|
+
const output = path.resolve(option("output"));
|
|
191
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
192
|
+
await writeFile(output, JSON.stringify(response.data, null, 2) + "\n");
|
|
193
|
+
response.artifacts.push({ type: "file", path: output });
|
|
194
|
+
}
|
|
195
|
+
emit(response);
|
|
196
|
+
if (!["succeeded", "running", "queued"].includes(response.status))
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (command === "setup") {
|
|
201
|
+
if (layout.installed) {
|
|
202
|
+
if (args.includes("--install-browser")) {
|
|
203
|
+
const { chromium } = await import("playwright");
|
|
204
|
+
try {
|
|
205
|
+
await access(chromium.executablePath());
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
if (args.includes("--offline"))
|
|
209
|
+
throw new Error("TCSETUP_BROWSER_OFFLINE: Install Chromium while online before offline capture.");
|
|
210
|
+
const { runCommand } = await import("./doctor.js");
|
|
211
|
+
const { playwrightCli } = await import("./npm/browser-cli.js");
|
|
212
|
+
const result = await runCommand(process.execPath, [playwrightCli(engineRoot), "install", "chromium"], { cwd: projectRoot, timeoutMs: 120_000 });
|
|
213
|
+
if (result.exitCode !== 0)
|
|
214
|
+
throw new Error("TCSETUP_BROWSER_INSTALL_FAILED: Chromium installation failed; retry setup --install-browser while online.");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const report = await installedDoctor(engineRoot, taskId);
|
|
218
|
+
emit(report);
|
|
219
|
+
if (report.status !== "succeeded")
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const result = await setup({
|
|
224
|
+
projectRoot: engineRoot,
|
|
225
|
+
taskId,
|
|
226
|
+
offline: args.includes("--offline"),
|
|
227
|
+
installBrowser: args.includes("--install-browser"),
|
|
228
|
+
});
|
|
229
|
+
emit(result);
|
|
230
|
+
if (result.status !== "succeeded")
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (command === "create") {
|
|
235
|
+
const template = option("template", layout.installed ? "minimal" : "m2");
|
|
236
|
+
if (!["minimal", "m2"].includes(template) ||
|
|
237
|
+
(layout.installed && template !== "minimal"))
|
|
238
|
+
throw new Error("TC_CREATE_TEMPLATE: Use minimal in either distribution; m2 sample assets are available in source checkouts only.");
|
|
239
|
+
if (!layout.installed && !option("project"))
|
|
240
|
+
throw new Error("TC_CREATE_PATH: --project is required.");
|
|
241
|
+
if (template === "minimal") {
|
|
242
|
+
for (const filename of ["game.ts", "pelican.project.json", "AGENTS.md"])
|
|
243
|
+
try {
|
|
244
|
+
await access(path.join(projectRoot, filename));
|
|
245
|
+
throw new Error("TC_CREATE_EXISTS: Project files already exist; no files were overwritten.");
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error.code !== "ENOENT")
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
await createMinimalProject(projectRoot, engineRoot, layout.installed);
|
|
252
|
+
emit(envelope("succeeded", {
|
|
253
|
+
project: path.basename(projectRoot),
|
|
254
|
+
engineVersion: SDK_VERSION,
|
|
255
|
+
template,
|
|
256
|
+
}, [], ["game.ts", "pelican.project.json", "AGENTS.md"]));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await access(projectRoot);
|
|
261
|
+
throw new Error("TC_CREATE_EXISTS: Choose a new directory; existing files are never overwritten.");
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (error.code !== "ENOENT")
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
await cp(path.join(engineRoot, "examples/m2"), projectRoot, {
|
|
268
|
+
recursive: true,
|
|
269
|
+
filter: (source) => !source.split(path.sep).includes(".pelican") &&
|
|
270
|
+
!source.split(path.sep).includes("dist"),
|
|
271
|
+
});
|
|
272
|
+
const manifestPath = path.join(projectRoot, "pelican.project.json");
|
|
273
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{}"));
|
|
274
|
+
manifest.engine = {
|
|
275
|
+
path: path.relative(projectRoot, engineRoot).split(path.sep).join("/"),
|
|
276
|
+
version: SDK_VERSION,
|
|
277
|
+
};
|
|
278
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
279
|
+
await writeAuthorEntry(projectRoot, engineRoot);
|
|
280
|
+
emit(envelope("succeeded", {
|
|
281
|
+
project: path.basename(projectRoot),
|
|
282
|
+
engineVersion: manifest.engine.version,
|
|
283
|
+
template,
|
|
284
|
+
}, [], ["game.ts", "pelican.project.json", "AGENTS.md"]));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (["check", "compile", "build", "dev", "capture", "optimize"].includes(command)) {
|
|
288
|
+
const scaffolded = command === "dev" && layout.installed
|
|
289
|
+
? await prepareInstalledDevProject(projectRoot, engineRoot)
|
|
290
|
+
: false;
|
|
291
|
+
const result = await compile();
|
|
292
|
+
if (!result.success) {
|
|
293
|
+
emit(envelope("failed", result.report, result.diagnostics, [
|
|
294
|
+
path.relative(projectRoot, path.join(target, "build-report.json")),
|
|
295
|
+
]));
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (command === "optimize") {
|
|
300
|
+
const asset = result.ir?.assets.find((item) => item.id === option("asset"));
|
|
301
|
+
if (!asset || asset.type !== "model")
|
|
302
|
+
throw new Error("TC_ASSET_OPTIMIZE_ID: --asset must reference a model asset.");
|
|
303
|
+
const output = path.resolve(projectRoot, option("output", path.join(".pelican/optimized", asset.id + ".glb")));
|
|
304
|
+
const relative = path.relative(projectRoot, output);
|
|
305
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
306
|
+
throw new Error("TC_ASSET_OPTIMIZE_PATH: Output must stay inside the game project.");
|
|
307
|
+
const report = await optimizeModel(path.join(projectRoot, asset.sourcePath), output);
|
|
308
|
+
emit(envelope("succeeded", report, [], [output]));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (command === "compile" || command === "check") {
|
|
312
|
+
emit(envelope("succeeded", result.report, [], [path.relative(projectRoot, target)]));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (command === "build") {
|
|
316
|
+
const output = !layout.installed && projectRoot === defaultProject
|
|
317
|
+
? path.join(engineRoot, "dist")
|
|
318
|
+
: path.join(projectRoot, "dist");
|
|
319
|
+
try {
|
|
320
|
+
if ((await lstat(output)).isSymbolicLink())
|
|
321
|
+
throw new Error("TC_BUILD_SYMLINK: dist must be a real output directory.");
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (error.code !== "ENOENT")
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
328
|
+
process.env.NODE_ENV = "production";
|
|
329
|
+
try {
|
|
330
|
+
await viteBuild({
|
|
331
|
+
root: layout.webRoot,
|
|
332
|
+
configFile: layout.installed
|
|
333
|
+
? false
|
|
334
|
+
: path.join(engineRoot, "vite.config.ts"),
|
|
335
|
+
envFile: false,
|
|
336
|
+
publicDir: path.dirname(target),
|
|
337
|
+
base: option("base", "./"),
|
|
338
|
+
logLevel: "silent",
|
|
339
|
+
build: {
|
|
340
|
+
outDir: output,
|
|
341
|
+
emptyOutDir: true,
|
|
342
|
+
target: "es2023",
|
|
343
|
+
sourcemap: false,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
if (previousNodeEnv === undefined)
|
|
349
|
+
delete process.env.NODE_ENV;
|
|
350
|
+
else
|
|
351
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
352
|
+
}
|
|
353
|
+
await cp(path.join(engineRoot, "LICENSE"), path.join(output, "LICENSE"));
|
|
354
|
+
await writeFile(path.join(output, "THIRDPARTY-NOTICES.txt"), "pelican runtime uses Three.js 0.187.0, fast-png 8.0.0, fflate 0.8.3, iobuffer 6.0.1 and ngraph.path 1.6.1 (MIT), and @dimforge/rapier3d-compat 0.21.0 (Apache-2.0). Full dependency licenses are distributed beside this notice. User asset declarations follow in generated/asset-notices.json.\n" +
|
|
355
|
+
JSON.stringify(result.ir?.assetNotices ?? [], null, 2) +
|
|
356
|
+
"\n");
|
|
357
|
+
await cp(path.join(engineRoot, layout.installed
|
|
358
|
+
? "licenses/three/LICENSE"
|
|
359
|
+
: "node_modules/three/LICENSE"), path.join(output, "THREE-LICENSE.txt"));
|
|
360
|
+
await cp(path.join(engineRoot, layout.installed
|
|
361
|
+
? "licenses/@dimforge/rapier3d-compat/LICENSE"
|
|
362
|
+
: "node_modules/@dimforge/rapier3d-compat/LICENSE"), path.join(output, "RAPIER-LICENSE.txt"));
|
|
363
|
+
for (const dependency of ["fast-png", "fflate", "iobuffer", "ngraph.path"]) {
|
|
364
|
+
await cp(path.join(engineRoot, layout.installed ? "licenses" : "node_modules", dependency, "LICENSE"), path.join(output, `${dependency.toUpperCase()}-LICENSE.txt`));
|
|
365
|
+
}
|
|
366
|
+
await writeFile(path.join(output, "pelican-build.json"), JSON.stringify({
|
|
367
|
+
engineVersion: SDK_VERSION,
|
|
368
|
+
stage: activeManifest.stage,
|
|
369
|
+
game: result.ir?.gameId,
|
|
370
|
+
entry: "index.html",
|
|
371
|
+
renderer: "webgpu-with-webgl2-fallback",
|
|
372
|
+
limitations: [
|
|
373
|
+
"M3 rendering is in development; not the complete engine or an accepted M3 release",
|
|
374
|
+
"Opaque textured surface transport is integrated; unsupported combinations report explicit diagnostics",
|
|
375
|
+
"Physics snapshots replay operation history and are bounded to 100000 operations",
|
|
376
|
+
"Compute BVH path tracer, no hardware ray tracing",
|
|
377
|
+
"Spatial denoising is integrated; temporal motion reconstruction remains work in progress",
|
|
378
|
+
],
|
|
379
|
+
}, null, 2) + "\n");
|
|
380
|
+
await compressDist(output);
|
|
381
|
+
emit(envelope("succeeded", { output: "dist", game: result.ir?.gameId }, [], [
|
|
382
|
+
"dist/index.html",
|
|
383
|
+
"dist/generated/scene.ir.json",
|
|
384
|
+
"dist/pelican-build.json",
|
|
385
|
+
]));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const port = Number(option("port", "4173"));
|
|
389
|
+
const server = await createServer({
|
|
390
|
+
root: layout.webRoot,
|
|
391
|
+
configFile: layout.installed
|
|
392
|
+
? false
|
|
393
|
+
: path.join(engineRoot, "vite.config.ts"),
|
|
394
|
+
envFile: false,
|
|
395
|
+
plugins: layout.installed
|
|
396
|
+
? [
|
|
397
|
+
{
|
|
398
|
+
name: "pelican-installed-browser-entry",
|
|
399
|
+
transformIndexHtml: {
|
|
400
|
+
order: "pre",
|
|
401
|
+
handler(html) {
|
|
402
|
+
return html.replace("../lib/renderer/browser.js", "/@fs/" +
|
|
403
|
+
path
|
|
404
|
+
.join(engineRoot, "lib/renderer/browser.js")
|
|
405
|
+
.replaceAll("\\", "/"));
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
]
|
|
410
|
+
: [],
|
|
411
|
+
publicDir: path.dirname(target),
|
|
412
|
+
server: {
|
|
413
|
+
port,
|
|
414
|
+
host: "127.0.0.1",
|
|
415
|
+
strictPort: true,
|
|
416
|
+
fs: { allow: [engineRoot, projectRoot] },
|
|
417
|
+
},
|
|
418
|
+
logLevel: "silent",
|
|
419
|
+
});
|
|
420
|
+
const controls = command === "dev" ? attachControl(server) : undefined;
|
|
421
|
+
await server.listen();
|
|
422
|
+
if (command === "capture") {
|
|
423
|
+
let browser;
|
|
424
|
+
try {
|
|
425
|
+
const { chromium } = await import("playwright");
|
|
426
|
+
browser = await chromium.launch(automationLaunchOptions());
|
|
427
|
+
const page = await browser.newPage({
|
|
428
|
+
viewport: { width: 960, height: 540 },
|
|
429
|
+
});
|
|
430
|
+
await withAutomationDeadline(() => page.goto(automationURL(`http://127.0.0.1:${port}/`), {
|
|
431
|
+
waitUntil: "domcontentloaded",
|
|
432
|
+
timeout: 30_000,
|
|
433
|
+
}));
|
|
434
|
+
await waitForEngine(page);
|
|
435
|
+
await withAutomationDeadline(() => page.evaluate(async (mode) => {
|
|
436
|
+
await window.__pelican.mode(mode);
|
|
437
|
+
if (mode !== "raster")
|
|
438
|
+
await window.__pelican.accumulate(8);
|
|
439
|
+
}, option("mode", "raster")));
|
|
440
|
+
const output = path.resolve(option("output", path.join(projectRoot, ".pelican/capture.png")));
|
|
441
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
442
|
+
await withAutomationDeadline(() => page.screenshot({ path: output, timeout: 30_000 }));
|
|
443
|
+
emit(envelope("succeeded", { mode: option("mode", "raster") }, [], [output]));
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
try {
|
|
447
|
+
await browser?.close();
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
await server.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
server.watcher.add(projectRoot);
|
|
456
|
+
let pending = false, dirty = false;
|
|
457
|
+
server.watcher.on("all", async (event, filename) => {
|
|
458
|
+
const relative = path.relative(projectRoot, filename);
|
|
459
|
+
if (relative.startsWith("..") ||
|
|
460
|
+
path.isAbsolute(relative) ||
|
|
461
|
+
filename.includes(`${path.sep}.pelican${path.sep}`) ||
|
|
462
|
+
filename.includes(`${path.sep}dist${path.sep}`))
|
|
463
|
+
return;
|
|
464
|
+
dirty = true;
|
|
465
|
+
if (pending)
|
|
466
|
+
return;
|
|
467
|
+
pending = true;
|
|
468
|
+
try {
|
|
469
|
+
while (dirty) {
|
|
470
|
+
dirty = false;
|
|
471
|
+
const next = await compile();
|
|
472
|
+
if (next.success && !dirty && !next.report.incremental.cacheHit)
|
|
473
|
+
server.ws.send({ type: "full-reload" });
|
|
474
|
+
else if (!next.success)
|
|
475
|
+
emit(envelope("failed", next.report, next.diagnostics));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
emit(envelope("failed", null, [
|
|
480
|
+
{ code: "TC_DEV_REBUILD", message: String(error) },
|
|
481
|
+
]));
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
pending = false;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
emit(envelope("running", {
|
|
488
|
+
url: `http://127.0.0.1:${port}/`,
|
|
489
|
+
project: path.basename(projectRoot),
|
|
490
|
+
...(scaffolded ? { scaffolded: true, template: "minimal" } : {}),
|
|
491
|
+
hotUpdate: "full-reload; compatibility policy is reported but live patch is not implemented",
|
|
492
|
+
}));
|
|
493
|
+
for (const signal of ["SIGINT", "SIGTERM"])
|
|
494
|
+
process.on(signal, () => {
|
|
495
|
+
void (controls?.close() ?? Promise.resolve())
|
|
496
|
+
.then(() => server.close())
|
|
497
|
+
.then(() => process.exit(0));
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
throw new Error(`TC_COMMAND_UNKNOWN: ${command}`);
|
|
502
|
+
}
|
|
503
|
+
main().catch((error) => {
|
|
504
|
+
emit(envelope("failed", null, [
|
|
505
|
+
{
|
|
506
|
+
code: diagnosticCode(error),
|
|
507
|
+
severity: "error",
|
|
508
|
+
message: String(error),
|
|
509
|
+
remediation: "Inspect the referenced source or run doctor; preserve failure output before retrying.",
|
|
510
|
+
},
|
|
511
|
+
]));
|
|
512
|
+
process.exitCode = 1;
|
|
513
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { brotliCompressSync, gzipSync, constants } from "node:zlib";
|
|
2
|
+
import { readdir, readFile, writeFile, lstat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
export async function compressDist(root) {
|
|
6
|
+
const entries = [];
|
|
7
|
+
const digest = (data) => createHash("sha256").update(data).digest("hex");
|
|
8
|
+
async function walk(directory) {
|
|
9
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
10
|
+
const file = path.join(directory, name), info = await lstat(file);
|
|
11
|
+
if (info.isSymbolicLink())
|
|
12
|
+
throw new Error("TC_BUILD_SYMLINK: Compression input must be owned regular files.");
|
|
13
|
+
if (info.isDirectory()) {
|
|
14
|
+
await walk(file);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!/\.(js|json|wasm|glb|wav|css|html)$/.test(name) || info.size < 256)
|
|
18
|
+
continue;
|
|
19
|
+
const original = await readFile(file), relative = path.relative(root, file).split(path.sep).join("/"), entry = {
|
|
20
|
+
path: relative,
|
|
21
|
+
bytes: original.length,
|
|
22
|
+
sha256: digest(original),
|
|
23
|
+
encodings: {},
|
|
24
|
+
};
|
|
25
|
+
for (const [extension, compressed] of [
|
|
26
|
+
[
|
|
27
|
+
"br",
|
|
28
|
+
brotliCompressSync(original, {
|
|
29
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: 6 },
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
["gz", gzipSync(original, { level: 9 })],
|
|
33
|
+
]) {
|
|
34
|
+
if (compressed.length >= original.length)
|
|
35
|
+
continue;
|
|
36
|
+
await writeFile(file + "." + extension, compressed);
|
|
37
|
+
entry.encodings[extension] = {
|
|
38
|
+
path: relative + "." + extension,
|
|
39
|
+
bytes: compressed.length,
|
|
40
|
+
sha256: digest(compressed),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
entries.push(entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await walk(root);
|
|
47
|
+
const report = {
|
|
48
|
+
format: "pelican.dist-compression",
|
|
49
|
+
version: 1,
|
|
50
|
+
mode: "lossless-http-content-encoding",
|
|
51
|
+
entries,
|
|
52
|
+
notes: [
|
|
53
|
+
"Identity originals remain available. Configure the Web server to negotiate .br/.gz with Content-Encoding and Vary: Accept-Encoding.",
|
|
54
|
+
"Compressed files are never a required custom loader or engine service.",
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
await writeFile(path.join(root, "compression-manifest.json"), JSON.stringify(report, null, 2) + "\n");
|
|
58
|
+
return report;
|
|
59
|
+
}
|