lecodes-cli 0.6.3 → 0.6.4
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 +6 -0
- package/dist/index.js +205 -3
- package/package.json +2 -2
- package/runtime/sdk/compile/index.ts +6 -0
- package/runtime/sdk/compile/sceneEditor.ts +66 -0
- package/runtime/sdk/core/fields.ts +108 -0
- package/runtime/sdk/gl/Material.ts +11 -1
- package/runtime/sdk/gl/Scene.ts +6 -1
- package/runtime/sdk/inject.ts +6 -0
- package/runtime/sdk/runtime/datetime.ts +331 -0
- package/runtime/sdk/scene/defineScene.ts +269 -0
- package/runtime/sdk-types.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,11 @@ The `render` command additionally needs the optional peer **[`lecodes-renderer`]
|
|
|
19
19
|
npm i -D lecodes-renderer @napi-rs/canvas
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
**Standalone build** (the release archive, no Node/Bun needed): unpack it and run `./lecodes install`
|
|
23
|
+
to copy the bundle to a library folder and put `lecodes` on your PATH — per-user by default, or
|
|
24
|
+
`--system` for all users on Linux/macOS. (This archive ships the `lecodes` binary alongside the
|
|
25
|
+
`chisel` bundler and a `runtime/` folder; keep them together, or let `install` place them for you.)
|
|
26
|
+
|
|
22
27
|
## Quick start
|
|
23
28
|
|
|
24
29
|
Start a project **locally** — no account, no login:
|
|
@@ -47,6 +52,7 @@ lecodes push -m "Tweak the menu" # push as a new checkpoint
|
|
|
47
52
|
| Command | What it does |
|
|
48
53
|
| --- | --- |
|
|
49
54
|
| `init [dir]` | Scaffold a new **local** project (no login, no server): a starter `main.ts`, the SDK types + a `tsconfig.json` for IntelliSense, and a `.lecodesignore`. Run it with `dev`, or compile it with `preview`. `dir` defaults to the current folder. Idempotent — existing files are kept. |
|
|
55
|
+
| `install` | **Standalone build only.** Install the unpacked release bundle so `lecodes` is on your PATH: copies the `lecodes`+`chisel`+`runtime/` bundle to a library folder, then (Linux/macOS) symlinks it into a `bin` dir. PATH itself is edited only with `--modify-path` (appends `export PATH=…` to your shell rc on Linux/macOS, or adds the folder to your user PATH on Windows); otherwise it prints how to finish. Per-user by default; `--system` installs to `/usr/local` (needs sudo, and `/usr/local/bin` is already on PATH). `--dir <path>` overrides the library folder, `--bin <path>` the symlink folder. Refuses when run from an npm/source install (there, install globally with your package manager). |
|
|
50
56
|
| `login` | Authorize in the browser; stores an access token in `~/.lecodes/config.json`. `--token <pat>` to use a token created in the web UI instead, `--api <url>` for the API origin (default `https://le.codes`), `--web <url>` for the app origin. |
|
|
51
57
|
| `clone <uuid\|url> [dir]` | Download a project's file tree + binary resources, plus `.d.ts` types and a `tsconfig.json` for IntelliSense. `--no-types` to skip types. |
|
|
52
58
|
| `status` | Show added / modified / moved / deleted files vs the last sync. |
|
package/dist/index.js
CHANGED
|
@@ -1052,7 +1052,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
1052
1052
|
// package.json
|
|
1053
1053
|
var package_default = {
|
|
1054
1054
|
name: "lecodes-cli",
|
|
1055
|
-
version: "0.6.
|
|
1055
|
+
version: "0.6.4",
|
|
1056
1056
|
description: "Clone, edit and push LeCodes projects from your machine.",
|
|
1057
1057
|
type: "module",
|
|
1058
1058
|
license: "MIT",
|
|
@@ -1082,7 +1082,7 @@ var package_default = {
|
|
|
1082
1082
|
prepublishOnly: "bun run sync-peers && bun run build"
|
|
1083
1083
|
},
|
|
1084
1084
|
dependencies: {
|
|
1085
|
-
"@letary/chisel": "^0.
|
|
1085
|
+
"@letary/chisel": "^0.3.0"
|
|
1086
1086
|
},
|
|
1087
1087
|
peerDependencies: {
|
|
1088
1088
|
"lecodes-design": "^0.3.1",
|
|
@@ -4519,6 +4519,206 @@ var status = async (_args) => {
|
|
|
4519
4519
|
};
|
|
4520
4520
|
|
|
4521
4521
|
// src/index.ts
|
|
4522
|
+
import { existsSync as existsSync16, realpathSync as realpathSync2 } from "node:fs";
|
|
4523
|
+
import { delimiter as delimiter2, dirname as dirname11, join as join18 } from "node:path";
|
|
4524
|
+
|
|
4525
|
+
// src/commands/install.ts
|
|
4526
|
+
import { execFileSync } from "node:child_process";
|
|
4527
|
+
import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync15, realpathSync, rmSync as rmSync2, symlinkSync } from "node:fs";
|
|
4528
|
+
import { homedir as homedir2 } from "node:os";
|
|
4529
|
+
import { basename as basename10, delimiter, dirname as dirname10, join as join17, resolve as resolve6, sep } from "node:path";
|
|
4530
|
+
var isWin = process.platform === "win32";
|
|
4531
|
+
var HOME = homedir2();
|
|
4532
|
+
var safeRealpath = (p) => {
|
|
4533
|
+
try {
|
|
4534
|
+
return realpathSync(p);
|
|
4535
|
+
} catch {
|
|
4536
|
+
return resolve6(p);
|
|
4537
|
+
}
|
|
4538
|
+
};
|
|
4539
|
+
var onPath = (dir) => {
|
|
4540
|
+
const want = safeRealpath(dir);
|
|
4541
|
+
return (process.env.PATH ?? "").split(delimiter).some((p) => {
|
|
4542
|
+
if (!p)
|
|
4543
|
+
return false;
|
|
4544
|
+
try {
|
|
4545
|
+
return safeRealpath(p) === want;
|
|
4546
|
+
} catch {
|
|
4547
|
+
return false;
|
|
4548
|
+
}
|
|
4549
|
+
});
|
|
4550
|
+
};
|
|
4551
|
+
var install = async (args) => {
|
|
4552
|
+
const bundle = dirname10(process.execPath);
|
|
4553
|
+
const exeName = basename10(process.execPath);
|
|
4554
|
+
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
4555
|
+
if (!existsSync15(join17(bundle, "runtime")) || !existsSync15(join17(bundle, chiselName))) {
|
|
4556
|
+
throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
|
|
4557
|
+
}
|
|
4558
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f) => existsSync15(join17(bundle, f)));
|
|
4559
|
+
if (isWin)
|
|
4560
|
+
return installWindows(args, bundle, companions);
|
|
4561
|
+
return installUnix(args, bundle, companions);
|
|
4562
|
+
};
|
|
4563
|
+
var copyBundle = (bundle, target, companions) => {
|
|
4564
|
+
mkdirSync12(target, { recursive: true });
|
|
4565
|
+
if (safeRealpath(target) === safeRealpath(bundle))
|
|
4566
|
+
return;
|
|
4567
|
+
for (const f of companions) {
|
|
4568
|
+
const dest = join17(target, f);
|
|
4569
|
+
rmSync2(dest, { recursive: true, force: true });
|
|
4570
|
+
cpSync(join17(bundle, f), dest, { recursive: true });
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
var permError = (e, dir, system) => {
|
|
4574
|
+
const code = e?.code;
|
|
4575
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
4576
|
+
return new CliError(`Permission denied writing to ${dir}.` + (system ? " Re-run with sudo: sudo lecodes install --system" : " Try without --system, or pick a writable --dir."));
|
|
4577
|
+
}
|
|
4578
|
+
return new CliError(`Install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
4579
|
+
};
|
|
4580
|
+
var installUnix = (args, bundle, companions) => {
|
|
4581
|
+
const system = flagBool(args, "system");
|
|
4582
|
+
const libDir = resolve6(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join17(HOME, ".local", "lib", "lecodes-cli")));
|
|
4583
|
+
const binDir = resolve6(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join17(HOME, ".local", "bin")));
|
|
4584
|
+
const link = join17(binDir, "lecodes");
|
|
4585
|
+
try {
|
|
4586
|
+
copyBundle(bundle, libDir, companions);
|
|
4587
|
+
chmodSync(join17(libDir, "lecodes"), 493);
|
|
4588
|
+
chmodSync(join17(libDir, "chisel"), 493);
|
|
4589
|
+
} catch (e) {
|
|
4590
|
+
throw permError(e, libDir, system);
|
|
4591
|
+
}
|
|
4592
|
+
try {
|
|
4593
|
+
mkdirSync12(binDir, { recursive: true });
|
|
4594
|
+
rmSync2(link, { force: true });
|
|
4595
|
+
symlinkSync(join17(libDir, "lecodes"), link);
|
|
4596
|
+
} catch (e) {
|
|
4597
|
+
throw permError(e, binDir, system);
|
|
4598
|
+
}
|
|
4599
|
+
success(`Installed to ${c.bold(libDir)}`);
|
|
4600
|
+
log(` ${c.dim(link + " → " + join17(libDir, "lecodes"))}`);
|
|
4601
|
+
finishPathUnix(args, binDir);
|
|
4602
|
+
};
|
|
4603
|
+
var shellRc = () => {
|
|
4604
|
+
const shell = basename10(process.env.SHELL ?? "");
|
|
4605
|
+
if (shell === "zsh")
|
|
4606
|
+
return { file: join17(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
4607
|
+
if (shell === "bash")
|
|
4608
|
+
return { file: join17(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
4609
|
+
return { file: join17(HOME, ".profile"), name: "~/.profile" };
|
|
4610
|
+
};
|
|
4611
|
+
var PATH_MARKER = "# Added by `lecodes install`";
|
|
4612
|
+
var appendPathToShellRc = (binDir) => {
|
|
4613
|
+
const { file, name } = shellRc();
|
|
4614
|
+
const line = `export PATH="${binDir}:$PATH"`;
|
|
4615
|
+
try {
|
|
4616
|
+
const existing = existsSync15(file) ? readFileSync15(file, "utf8") : "";
|
|
4617
|
+
if (existing.includes(line))
|
|
4618
|
+
return name;
|
|
4619
|
+
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
4620
|
+
`) ? `
|
|
4621
|
+
` : ""}
|
|
4622
|
+
${PATH_MARKER}
|
|
4623
|
+
${line}
|
|
4624
|
+
`);
|
|
4625
|
+
return name;
|
|
4626
|
+
} catch {
|
|
4627
|
+
return null;
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
var finishPathUnix = (args, binDir) => {
|
|
4631
|
+
if (onPath(binDir)) {
|
|
4632
|
+
log(` Run ${c.bold("lecodes --version")} in a new shell to verify.`);
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
if (flagBool(args, "modify-path")) {
|
|
4636
|
+
const rc = appendPathToShellRc(binDir);
|
|
4637
|
+
if (rc) {
|
|
4638
|
+
success(`Added ${c.bold(binDir)} to PATH in ${rc}.`);
|
|
4639
|
+
log(` Run ${c.bold(`source ${rc}`)} (or open a new terminal), then ${c.bold("lecodes --version")}.`);
|
|
4640
|
+
} else {
|
|
4641
|
+
warn("Couldn't edit your shell startup file. Add this line to it manually:");
|
|
4642
|
+
log(` ${c.bold(`export PATH="${binDir}:$PATH"`)}`);
|
|
4643
|
+
}
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
const { name } = shellRc();
|
|
4647
|
+
warn(`${binDir} is not on your PATH yet. To finish, either:`);
|
|
4648
|
+
log(` • this shell now: ${c.bold(`export PATH="${binDir}:$PATH"`)}`);
|
|
4649
|
+
log(` • all new shells: ${c.bold(`echo 'export PATH="${binDir}:$PATH"' >> ${name}`)} ${c.dim("(then reopen it)")}`);
|
|
4650
|
+
log(` • let install do it: re-run with ${c.bold("--modify-path")}`);
|
|
4651
|
+
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
4652
|
+
};
|
|
4653
|
+
var installWindows = (args, bundle, companions) => {
|
|
4654
|
+
const target = resolve6(flagStr(args, "dir") ?? join17(process.env.LOCALAPPDATA ?? join17(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
4655
|
+
try {
|
|
4656
|
+
copyBundle(bundle, target, companions);
|
|
4657
|
+
} catch (e) {
|
|
4658
|
+
throw new CliError(`Install failed writing to ${target}: ${e instanceof Error ? e.message : String(e)}`);
|
|
4659
|
+
}
|
|
4660
|
+
success(`Installed to ${c.bold(target)}`);
|
|
4661
|
+
if (onPath(target)) {
|
|
4662
|
+
log(` Already on PATH — run ${c.bold("lecodes --version")} in a new terminal.`);
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
if (flagBool(args, "modify-path")) {
|
|
4666
|
+
if (addToUserPathWindows(target)) {
|
|
4667
|
+
success(`Added ${c.bold(target)} to your user PATH.`);
|
|
4668
|
+
log(` ${c.yellow("Open a NEW terminal")} for it to take effect, then run ${c.bold("lecodes --version")}.`);
|
|
4669
|
+
} else {
|
|
4670
|
+
warn("Couldn't edit PATH automatically. Add this folder to your PATH manually:");
|
|
4671
|
+
log(` ${c.bold(target)}`);
|
|
4672
|
+
}
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
warn(`${target} is not on your PATH yet. To finish, either:`);
|
|
4676
|
+
log(` • let install do it: re-run with ${c.bold("--modify-path")} (adds it to your user PATH)`);
|
|
4677
|
+
log(` • or add the folder manually via System → Environment Variables.`);
|
|
4678
|
+
};
|
|
4679
|
+
var addToUserPathWindows = (dir) => {
|
|
4680
|
+
const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
|
|
4681
|
+
try {
|
|
4682
|
+
execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
|
|
4683
|
+
return true;
|
|
4684
|
+
} catch {
|
|
4685
|
+
return false;
|
|
4686
|
+
}
|
|
4687
|
+
};
|
|
4688
|
+
|
|
4689
|
+
// src/index.ts
|
|
4690
|
+
var STANDALONE = (() => {
|
|
4691
|
+
const dir = dirname11(process.execPath);
|
|
4692
|
+
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
4693
|
+
return existsSync16(join18(dir, "runtime")) && existsSync16(join18(dir, chisel));
|
|
4694
|
+
})();
|
|
4695
|
+
var INSTALLED = STANDALONE && (() => {
|
|
4696
|
+
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
4697
|
+
const self = (() => {
|
|
4698
|
+
try {
|
|
4699
|
+
return realpathSync2(process.execPath);
|
|
4700
|
+
} catch {
|
|
4701
|
+
return process.execPath;
|
|
4702
|
+
}
|
|
4703
|
+
})();
|
|
4704
|
+
return (process.env.PATH ?? "").split(delimiter2).some((d) => {
|
|
4705
|
+
if (!d)
|
|
4706
|
+
return false;
|
|
4707
|
+
try {
|
|
4708
|
+
const cand = join18(d, exe);
|
|
4709
|
+
return existsSync16(cand) && realpathSync2(cand) === self;
|
|
4710
|
+
} catch {
|
|
4711
|
+
return false;
|
|
4712
|
+
}
|
|
4713
|
+
});
|
|
4714
|
+
})();
|
|
4715
|
+
var INSTALL_HELP = STANDALONE && !INSTALLED ? `
|
|
4716
|
+
install Install this standalone build so 'lecodes' is on your PATH
|
|
4717
|
+
(copies the bundle to a library folder + a symlink/PATH entry).
|
|
4718
|
+
--modify-path also add it to PATH (edits your shell rc / user PATH)
|
|
4719
|
+
--system install for all users (Linux/macOS; needs sudo)
|
|
4720
|
+
--dir <path> override the install/library folder
|
|
4721
|
+
--bin <path> override the symlink folder (Linux/macOS)` : "";
|
|
4522
4722
|
var HELP = `${c.bold("lecodes")} — clone, edit and push LeCodes projects from your machine.
|
|
4523
4723
|
|
|
4524
4724
|
${c.bold("Usage:")} lecodes <command> [options]
|
|
@@ -4526,7 +4726,7 @@ ${c.bold("Usage:")} lecodes <command> [options]
|
|
|
4526
4726
|
${c.bold("Commands:")}
|
|
4527
4727
|
init [dir] Scaffold a new LOCAL project (no login/server): a starter
|
|
4528
4728
|
main.ts, IDE types + tsconfig, and a .lecodesignore.
|
|
4529
|
-
Run it with 'lecodes dev' or 'lecodes preview'
|
|
4729
|
+
Run it with 'lecodes dev' or 'lecodes preview'.${INSTALL_HELP}
|
|
4530
4730
|
login Authorize in the browser and store an access token
|
|
4531
4731
|
--token <pat> store a token from the web UI instead
|
|
4532
4732
|
--api <url> API origin (default https://le.codes)
|
|
@@ -4587,6 +4787,8 @@ ${c.bold("Commands:")}
|
|
|
4587
4787
|
|
|
4588
4788
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
4589
4789
|
var commands = { init: init2, login, create, clone, status, diff, pull, push, preview, render, design, dev };
|
|
4790
|
+
if (STANDALONE)
|
|
4791
|
+
commands.install = install;
|
|
4590
4792
|
var readVersion = () => package_default.version ?? "unknown";
|
|
4591
4793
|
var main = async () => {
|
|
4592
4794
|
const argv = process.argv.slice(2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"prepublishOnly": "bun run sync-peers && bun run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@letary/chisel": "^0.
|
|
33
|
+
"@letary/chisel": "^0.3.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"lecodes-design": "^0.3.1",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The scene-editor compile composition, shared by every host that compiles an edit-mode bundle
|
|
2
|
+
// for a .scene.ts file: the platform backend (src/scene/compileSceneEditor.ts), the standalone
|
|
3
|
+
// scene-editor dev server, and (later) the lecodes CLI. One bundle on purpose: chisel's
|
|
4
|
+
// method-granular DCE trims each bundle's SDK copy to its own calls, so a separately-compiled
|
|
5
|
+
// harness could not call scene methods on objects the scene bundle created.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import type { CompileEntry } from "./compileProject"
|
|
10
|
+
|
|
11
|
+
/** Where the harness sources land in the compile's file map. The dotfolder can't collide with
|
|
12
|
+
* project assets (the tree forbids leading-dot names) and reads clearly in compile errors. */
|
|
13
|
+
export const SCENE_HARNESS_DIR = "./.scene-editor"
|
|
14
|
+
|
|
15
|
+
/** The virtual entrypoint path (`entryOverride`). */
|
|
16
|
+
export const SCENE_ENTRY_PATH = "./.scene-entry.ts"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the harness sources (packages/projects/src/scene-editor) from a directory:
|
|
20
|
+
* `LECODES_SCENE_HARNESS_DIR` env override first (deployments vendor the sources, like
|
|
21
|
+
* LECODES_SDK_INJECT does for the SDK), else the given fallback dir. Returns name → source.
|
|
22
|
+
*/
|
|
23
|
+
export const loadSceneHarness = (fallbackDir?: string): Record<string, string> => {
|
|
24
|
+
const override = process.env.LECODES_SCENE_HARNESS_DIR
|
|
25
|
+
const dir = override && existsSync(override) ? override
|
|
26
|
+
: fallbackDir && existsSync(fallbackDir) ? fallbackDir
|
|
27
|
+
: null
|
|
28
|
+
if (!dir) throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)")
|
|
29
|
+
const out: Record<string, string> = {}
|
|
30
|
+
for (const f of readdirSync(dir)) {
|
|
31
|
+
if (f.endsWith(".ts")) out[f] = readFileSync(join(dir, f), "utf8")
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The extra entries that turn a project compile into a scene-editor compile: the harness modules
|
|
38
|
+
* plus a virtual entry that (1) sets the edit-mode flag in its own module — imports hoist, so the
|
|
39
|
+
* flag must run as a module, first — (2) imports the scene, which registers its handle on
|
|
40
|
+
* `__lecodesScenes`, and (3) imports the harness main, which installs `__lecodesSceneHarness`.
|
|
41
|
+
* FULL extensions everywhere — exact-match specifiers need no resolver guesswork on dotted names,
|
|
42
|
+
* so even the unpatched published chisel resolves the `.scene.ts` import.
|
|
43
|
+
*
|
|
44
|
+
* `scenePath` is the project path of the scene file ("./scenes/city.scene.ts"). Compile with
|
|
45
|
+
* `entryOverride: SCENE_ENTRY_PATH`, `header: false`, `format: "iife"`.
|
|
46
|
+
*/
|
|
47
|
+
export const sceneEditorEntries = (scenePath: string, harness: Record<string, string>): CompileEntry[] => {
|
|
48
|
+
const entries: CompileEntry[] = Object.entries(harness).map(([ name, text ]) => ({
|
|
49
|
+
path: `${SCENE_HARNESS_DIR}/${name}`,
|
|
50
|
+
type: "text" as const,
|
|
51
|
+
text,
|
|
52
|
+
}))
|
|
53
|
+
entries.push({
|
|
54
|
+
path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
|
|
55
|
+
type: "text",
|
|
56
|
+
text: `;(globalThis as any).__lecodesSceneEdit = true\nexport {}\n`,
|
|
57
|
+
})
|
|
58
|
+
entries.push({
|
|
59
|
+
path: SCENE_ENTRY_PATH,
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"\n`
|
|
62
|
+
+ `import "${scenePath}"\n`
|
|
63
|
+
+ `import "${SCENE_HARNESS_DIR}/main.ts"\n`,
|
|
64
|
+
})
|
|
65
|
+
return entries
|
|
66
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Field introspection for the scene-editor inspector: what's editable on an aspect, and with which
|
|
2
|
+
// editor widget. Engine-agnostic (works for 2D and 3D aspects alike).
|
|
3
|
+
//
|
|
4
|
+
// The editable surface of an aspect is its public class fields with defaults — the same convention
|
|
5
|
+
// aspects already follow for `node.aspect(Ctor, opts)` configuration. Editors are layered:
|
|
6
|
+
// 1. inference from the default value's shape (number → number field, '#rrggbb' → color, …), free
|
|
7
|
+
// for every user aspect with zero ceremony;
|
|
8
|
+
// 2. optional `static fields: FieldMeta<T>` on the class for ranges/labels/options/overrides
|
|
9
|
+
// (mirrors how `static aspect` declares the accessor name).
|
|
10
|
+
|
|
11
|
+
/** Editor widget kinds the inspector knows how to render. */
|
|
12
|
+
export type FieldEditor =
|
|
13
|
+
| "number" | "text" | "color" | "switch" | "select"
|
|
14
|
+
| "vec2" | "vec3" | "vec4"
|
|
15
|
+
|
|
16
|
+
export type FieldMetaEntry = {
|
|
17
|
+
/** Inspector label (default: the field name). */
|
|
18
|
+
label?: string
|
|
19
|
+
min?: number
|
|
20
|
+
max?: number
|
|
21
|
+
step?: number
|
|
22
|
+
/** Allowed values — renders a dropdown (editor "select"). */
|
|
23
|
+
options?: readonly (string | number)[]
|
|
24
|
+
/** Override the editor inferred from the default value. */
|
|
25
|
+
editor?: FieldEditor
|
|
26
|
+
/** Exclude the field from the inspector. */
|
|
27
|
+
hidden?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Declared on an aspect class as `static fields: FieldMeta<MyAspect> = { … }`. All optional. */
|
|
31
|
+
export type FieldMeta<T = unknown> = { [K in Extract<keyof T, string>]?: FieldMetaEntry }
|
|
32
|
+
|
|
33
|
+
/** One editable field: key + default value + resolved editor + merged metadata. */
|
|
34
|
+
export type FieldDescriptor = {
|
|
35
|
+
key: string
|
|
36
|
+
label: string
|
|
37
|
+
/** The default value (from a freshly constructed instance; undefined if only declared in meta). */
|
|
38
|
+
value: unknown
|
|
39
|
+
/** Resolved editor kind; undefined = no editor known (inspector shows it read-only). */
|
|
40
|
+
editor: FieldEditor | undefined
|
|
41
|
+
min?: number
|
|
42
|
+
max?: number
|
|
43
|
+
step?: number
|
|
44
|
+
options?: readonly (string | number)[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type AspectClassInfo = {
|
|
48
|
+
/** The accessor name (`static aspect`, injected by chisel for user aspects). */
|
|
49
|
+
name: string
|
|
50
|
+
className: string
|
|
51
|
+
fields: FieldDescriptor[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Instance fields every aspect inherits from the Aspect base — configuration plumbing, not content.
|
|
55
|
+
const BASE_FIELDS = new Set([ "node", "updateBeforePhysics", "order", "updateWhenVisible" ])
|
|
56
|
+
|
|
57
|
+
/** Infer the editor widget from a default value's shape. */
|
|
58
|
+
export const inferFieldEditor = (value: unknown): FieldEditor | undefined => {
|
|
59
|
+
if (typeof value === "boolean") return "switch"
|
|
60
|
+
if (typeof value === "number") return "number"
|
|
61
|
+
if (typeof value === "string") return value.startsWith("#") ? "color" : "text"
|
|
62
|
+
if (Array.isArray(value) && value.length >= 2 && value.length <= 4 && value.every((n) => typeof n === "number")) {
|
|
63
|
+
return value.length === 2 ? "vec2" : value.length === 3 ? "vec3" : "vec4"
|
|
64
|
+
}
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cloneValue = (v: unknown): unknown => (Array.isArray(v) ? [ ...v ] : v)
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enumerate the editable fields of a class: construct a default instance, take its own enumerable
|
|
72
|
+
* non-underscore fields (minus the Aspect base plumbing), infer editors from the default values, and
|
|
73
|
+
* merge the class's optional `static fields` metadata (which can also add keys that have no runtime
|
|
74
|
+
* default, e.g. fields declared without an initializer).
|
|
75
|
+
*/
|
|
76
|
+
export const describeFields = (ctor: abstract new () => unknown): FieldDescriptor[] => {
|
|
77
|
+
let defaults: Record<string, unknown> = {}
|
|
78
|
+
// Aspects have no-arg constructors by convention (the engine creates them via node.aspect()); a
|
|
79
|
+
// class that throws here just loses default-value inference, it doesn't break the inspector.
|
|
80
|
+
try { defaults = new (ctor as new () => Record<string, unknown>)() } catch { /* meta-only */ }
|
|
81
|
+
|
|
82
|
+
const meta = ((ctor as { fields?: FieldMeta }).fields ?? {}) as Record<string, FieldMetaEntry>
|
|
83
|
+
|
|
84
|
+
const keys = new Set<string>()
|
|
85
|
+
for (const k of Object.keys(defaults)) {
|
|
86
|
+
if (BASE_FIELDS.has(k) || k.startsWith("_")) continue
|
|
87
|
+
if (typeof defaults[k] === "function") continue
|
|
88
|
+
keys.add(k)
|
|
89
|
+
}
|
|
90
|
+
for (const k of Object.keys(meta)) keys.add(k)
|
|
91
|
+
|
|
92
|
+
const out: FieldDescriptor[] = []
|
|
93
|
+
for (const key of keys) {
|
|
94
|
+
const m = meta[key] ?? {}
|
|
95
|
+
if (m.hidden) continue
|
|
96
|
+
const value = defaults[key]
|
|
97
|
+
const editor = m.editor ?? (m.options ? "select" : inferFieldEditor(value))
|
|
98
|
+
out.push({ key, label: m.label ?? key, value: cloneValue(value), editor, min: m.min, max: m.max, step: m.step, options: m.options })
|
|
99
|
+
}
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Describe an aspect class for the inspector: accessor name + class name + editable fields. */
|
|
104
|
+
export const describeAspect = (ctor: abstract new () => unknown): AspectClassInfo => ({
|
|
105
|
+
name: (ctor as { aspect?: string }).aspect ?? "",
|
|
106
|
+
className: (ctor as { name?: string }).name ?? "",
|
|
107
|
+
fields: describeFields(ctor),
|
|
108
|
+
})
|
|
@@ -36,6 +36,14 @@ const applyUniform = (id: number, key: string, value: UniformValue): void => {
|
|
|
36
36
|
|
|
37
37
|
export type MaterialColorOptions = { color?: ColorInput, map?: Texture | Canvas | null }
|
|
38
38
|
|
|
39
|
+
/** `Material.lit` options — PBR scalars on top of the color/map pair. */
|
|
40
|
+
export type LitMaterialOptions = MaterialColorOptions & {
|
|
41
|
+
/** Perceptual roughness, 0 (mirror) … 1 (matte). Unset = the shader's default. */
|
|
42
|
+
roughness?: number
|
|
43
|
+
/** Metallic factor, 0 (dielectric) … 1 (metal). Unset = the shader's default. */
|
|
44
|
+
metallic?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
export class Material {
|
|
40
48
|
/** @internal native material-instance handle. */
|
|
41
49
|
_id: number
|
|
@@ -84,12 +92,14 @@ export class Material {
|
|
|
84
92
|
// --- factories ---
|
|
85
93
|
|
|
86
94
|
/** PBR lit material. */
|
|
87
|
-
static lit(options:
|
|
95
|
+
static lit(options: LitMaterialOptions = {}): Material {
|
|
88
96
|
// The fetchLocal literal must be inline so the compiler's preload header captures the shader name.
|
|
89
97
|
const m = new Material({ _id: _creatorUtils.fetchLocal("lit.filamat") } as any)
|
|
90
98
|
m._colorKey = "baseColor"
|
|
91
99
|
if (options.color !== undefined) m.color = options.color
|
|
92
100
|
if (options.map) m.map = options.map
|
|
101
|
+
if (options.roughness !== undefined) m.uniforms.roughness = options.roughness
|
|
102
|
+
if (options.metallic !== undefined) m.uniforms.metallic = options.metallic
|
|
93
103
|
return m
|
|
94
104
|
}
|
|
95
105
|
|
package/runtime/sdk/gl/Scene.ts
CHANGED
|
@@ -66,11 +66,16 @@ export class Scene {
|
|
|
66
66
|
if (parent) {
|
|
67
67
|
this.camera = parent.camera
|
|
68
68
|
this._id = _creator.createOverlayScene(parent._id)
|
|
69
|
+
// No camera re-attach: the native createOverlayScene wires the parent's camera into the
|
|
70
|
+
// overlay view. Re-attaching would (a) point camera._sceneId — the source for getRay /
|
|
71
|
+
// getViewDirection / fov — at the overlay's view, whose viewport/projection aren't
|
|
72
|
+
// maintained like the render view's, and (b) create a second native camera component on
|
|
73
|
+
// the shared entity. (Found via the scene editor: gizmo pick rays came out distorted.)
|
|
69
74
|
} else {
|
|
70
75
|
this.camera = new Camera()
|
|
71
76
|
this._id = _creator.createScene()
|
|
77
|
+
this.camera._attach(this._id)
|
|
72
78
|
}
|
|
73
|
-
this.camera._attach(this._id)
|
|
74
79
|
|
|
75
80
|
if (options.ibl !== false) _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000)
|
|
76
81
|
if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
|
package/runtime/sdk/inject.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { Canvas, Bitmap } from "./canvas/Canvas"
|
|
|
25
25
|
|
|
26
26
|
// ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
|
|
27
27
|
export { toast, SvgSource } from "./runtime/misc"
|
|
28
|
+
export { date, type DateValue, type DateInput, type Unit, type Locale } from "./runtime/datetime"
|
|
28
29
|
export { fetch, fetchLocal, FormData, File } from "./runtime/fetch"
|
|
29
30
|
export { localStorage } from "./runtime/storage"
|
|
30
31
|
export { device } from "./runtime/device"
|
|
@@ -78,6 +79,11 @@ export { easeIn, easeOut, easeInOut } from "./animate/easings"
|
|
|
78
79
|
// ---- plugins (optional, host-provided capabilities — see src/plugins/) ----
|
|
79
80
|
export { QRScanner } from "./plugins/qr"
|
|
80
81
|
|
|
82
|
+
// ---- scenes as data (.scene.ts files — see src/scene/defineScene.ts) ----
|
|
83
|
+
export { defineScene, use, SceneHandle } from "./scene/defineScene"
|
|
84
|
+
export type { SceneDef, SceneNodeDef, MeshDef, LightDef, MaterialDef, AspectEntry, LoadedScene } from "./scene/defineScene"
|
|
85
|
+
export type { FieldMeta } from "./core/fields"
|
|
86
|
+
|
|
81
87
|
// ---- 3D engine (creator-gl) ----
|
|
82
88
|
export { Scene, ARScene, VRScene } from "./gl/Scene"
|
|
83
89
|
export { Node } from "./gl/Node"
|