Scene Inspector (#11)

This PR adds the bones of a user-facing scene inspector.

### Work Done

To facilitate this work, the Bevy/frontend interface has been expanded to
accommodate two-way communications:

- The Bevy app can now dispatch JavaScript `CustomEvent`s via
  `wasm::dispatch_to_js(...)`
- The frontend application can receive those events by listening for the given
  event type on the `window` object

In its current state, the inspection system works by attaching the `Inspectable`
marker component to any entities which should be exposed to the user. A Bevy
system runs on `Update` to check for newly spawned or despawned entities with
that component, and emits events to notify the frontend. The frontend listens
for those events and updates its model of the scene accordingly, which is then
displayed with a simple tree view.

### Future Enhancements

There are many improvements that must be made to make this system actually
useful, but this PR is being expedited to focus on migrating to Bevy 0.12. A
shortlist of planned updates:

- Identify and expose the "type" of entity (e.g. lights, cameras, meshes,
  skeletons, etc.) via a field on the `Inspectable` component
- Highlight the currently selected entity in the frontend hierarchy
- Ask the Bevy app to update the selection when the user selects an entry in the
  hierarchy
- Add a separate UI panel to inspect and modify the selected entity's components
  via the frontend
This commit is contained in:
Danny McGee
2023-11-10 21:07:39 -05:00
committed by GitHub
parent 5fed2b6f59
commit 8bf4abc3a4
30 changed files with 1178 additions and 271 deletions
+71
View File
@@ -0,0 +1,71 @@
import fs from "node:fs";
import url from "node:url";
import path from "node:path";
import { rimraf } from "rimraf";
import which from "which";
import copyAssets from "./copy-assets.mjs";
import handleError from "./handle-error.mjs";
import run from "./run.mjs";
async function main() {
try {
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const workspaceRoot = path.resolve(__dirname, "../..");
/** @type import("node:child_process").StdioOptions */
const stdio = ["ignore", "inherit", "inherit"];
const outDir = path.join(workspaceRoot, "dist/studio");
if (fs.existsSync(outDir))
await rimraf(outDir);
await run("cargo", [
"build",
"-p", "studio",
"--lib",
"--release",
"--features", "wasm",
"--target", "wasm32-unknown-unknown",
], {
shell: true,
cwd: workspaceRoot,
stdio,
});
const wasmBindgenInstalled = Boolean(await which("wasm-bindgen", { nothrow: true }));
if (!wasmBindgenInstalled) {
await run("cargo", [
"install", "-f", "wasm-bindgen-cli",
"--version", "0.2.88",
], {
shell: true,
cwd: workspaceRoot,
stdio,
});
}
await run("wasm-bindgen", [
"--out-dir", "./dist/studio",
"--target", "bundler",
"./target/wasm32-unknown-unknown/release/studio.wasm",
], {
shell: true,
cwd: workspaceRoot,
stdio,
});
await copyAssets();
await fs.promises.copyFile(
path.join(workspaceRoot, "studio/package.json"),
path.join(outDir, "package.json"),
);
}
catch (err) {
handleError(err);
}
}
main();
+3 -4
View File
@@ -4,12 +4,13 @@ import fs from "node:fs";
import path from "node:path";
import url from "node:url";
async function main() {
export default async function main() {
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const assetsSrc = path.resolve(__dirname, "../assets");
const assetsDest = path.resolve(__dirname, "../../dist/studio/public/assets");
console.log(`\nCopying assets from ${assetsSrc} to ${assetsDest}`);
console.log("");
console.log(`Copying assets from "${assetsSrc}" to "${assetsDest}"`);
let count = 0;
const start = performance.now();
@@ -76,5 +77,3 @@ function handleError(err) {
process.exit(1);
}
}
main();
+33
View File
@@ -0,0 +1,33 @@
import chalk from "chalk";
import util from "node:util";
/**
* @param {any} err
* @returns {never}
*/
export default function (err) {
if (typeof err === "number") {
console.log(`${
chalk.bold.redBright.inverse(" ERROR ")
} Child process exited with code ${err}`);
process.exit(err);
}
if (err != null && err instanceof Error) {
console.log(`${
chalk.bold.redBright.inverse(" ERROR ")
} ${err.message}`);
if (err.stack)
console.log(chalk.dim(err.stack));
process.exit(1);
}
console.log(`${
chalk.bold.redBright.inverse(" ERROR ")
} Child process threw an exception: ${util.inspect(err)}`);
process.exit(1);
}
+27
View File
@@ -0,0 +1,27 @@
import chalk from "chalk";
import cp from "node:child_process";
/**
* @param {string} command
* @param {readonly string[]} args
* @param {import("node:child_process").SpawnOptions} options
*
* @returns {Promise<void>}
*/
export default function run(command, args, options) {
console.log("");
console.log(chalk.dim(`> ${command} ${args.join(" ")}`));
return new Promise((resolve, reject) => {
cp.spawn(command, args, options)
.on("exit", code => {
if (code) reject(code)
else resolve();
})
.on("close", code => {
if (code) reject(code)
else resolve();
})
.on("error", reject);
});
}