create-playcanvas 0.9.0 → 0.10.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.
Files changed (33) hide show
  1. package/package.json +2 -2
  2. package/skills/apply-conventions/SKILL.md +3 -3
  3. package/skills/assemble-scene/SKILL.md +1 -1
  4. package/skills/build-app/SKILL.md +2 -0
  5. package/skills/configure-animation/SKILL.md +1 -1
  6. package/skills/inspect-runtime/SKILL.md +41 -0
  7. package/skills/inspect-runtime/scripts/devtools-hook.js +23 -0
  8. package/skills/override-shader-chunks/SKILL.md +1 -1
  9. package/skills/reduce-draw-calls/SKILL.md +4 -4
  10. package/skills/verify-pixels/SKILL.md +1 -1
  11. package/templates/engine/base/README.md +4 -0
  12. package/templates/engine/base/package.json +3 -2
  13. package/templates/engine/base/src/inspector.css +33 -0
  14. package/templates/engine/base/src/inspector.ts +38 -0
  15. package/templates/engine/base/tsconfig.json +1 -1
  16. package/templates/engine/base/vite.config.ts +16 -0
  17. package/templates/engine/interactive-sphere/src/App.ts +2 -2
  18. package/templates/engine/model-viewer/src/main.ts +1 -2
  19. package/templates/engine/product-configurator/src/main.ts +6 -7
  20. package/templates/react/base/README.md +4 -0
  21. package/templates/react/base/package.json +3 -2
  22. package/templates/react/base/src/inspector.css +33 -0
  23. package/templates/react/base/src/inspector.ts +38 -0
  24. package/templates/react/base/vite.config.ts +12 -1
  25. package/templates/react/product-configurator/src/App.tsx +6 -6
  26. package/templates/web-components/base/README.md +4 -0
  27. package/templates/web-components/base/package.json +4 -3
  28. package/templates/web-components/base/src/inspector.css +33 -0
  29. package/templates/web-components/base/src/inspector.ts +38 -0
  30. package/templates/web-components/base/tsconfig.json +1 -1
  31. package/templates/web-components/base/vite.config.ts +16 -0
  32. package/templates/web-components/interactive-sphere/src/main.ts +1 -2
  33. package/templates/web-components/product-configurator/src/main.ts +5 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-playcanvas",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Scaffold a PlayCanvas project",
5
5
  "keywords": [
6
6
  "playcanvas",
@@ -44,7 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@clack/prompts": "^1.8.1",
46
46
  "@playcanvas/eslint-config": "3.0.0-beta.8",
47
- "@types/node": "22.20.4",
47
+ "@types/node": "22.20.5",
48
48
  "eslint": "^9.39.5",
49
49
  "mri": "^1.2.0",
50
50
  "picocolors": "^1.1.1",
@@ -44,6 +44,6 @@ Check version-sensitive APIs against installed `playcanvas` declarations.
44
44
 
45
45
  ## Verify in the running app
46
46
 
47
- Offline values cannot prove facing, poses, or placement. Log values from a `Script` or update
48
- callback in the running app, and judge visuals only from screenshots you have inspected. A saved
49
- file path is not evidence.
47
+ Offline values cannot prove facing, poses, or placement. Read values from the running app with
48
+ `inspect-runtime`, and judge visuals only from screenshots you have inspected. A saved file path is
49
+ not evidence.
@@ -20,7 +20,7 @@ once; if the tuning record is missing, calibrate first.
20
20
  ## Check placement
21
21
 
22
22
  After a rendered frame, measure each support or attachment at the object's actual position, using
23
- a mount point, raycast, or geometry sample. A positive gap floats; a negative gap penetrates.
23
+ a mount point, raycast, or geometry sample read through `inspect-runtime`. A positive gap floats; a negative gap penetrates.
24
24
  Overlapping world AABBs between unrelated roots are a failure unless two different views prove the
25
25
  overlap is empty space.
26
26
 
@@ -19,6 +19,8 @@ reference: [direct Engine](references/direct-engine.md), [React](references/reac
19
19
  `canvas.width / canvas.clientWidth`.
20
20
  - Rigid bodies stay inert until Ammo loads through `WasmModule`. Copy the loader from the
21
21
  `physics/falling-shapes` example with `find-examples`.
22
+ - `devtools` defaults to `true`, so debugging tools can find the app (`inspect-runtime`). Set it to
23
+ `false` only to hide a production build.
22
24
 
23
25
  ## Update loop
24
26
 
@@ -20,7 +20,7 @@ Get clip and joint names from `inspect-glb`; never guess them.
20
20
  ## Prove playback
21
21
 
22
22
  An active state or advancing state time does not prove motion. Sample two non-root joints over
23
- several rendered frames at idle and at both ends of each blend tree. Zero joint motion means a
23
+ several rendered frames (`inspect-runtime`) at idle and at both ends of each blend tree. Zero joint motion means a
24
24
  binding, playback, or blend-weight failure.
25
25
 
26
26
  ## Diagnose
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: inspect-runtime
3
+ description: Use when reading state from a running PlayCanvas app, to reach its live Application through the Engine devtools hook without changing app code.
4
+ ---
5
+
6
+ # Inspect a running app
7
+
8
+ From Engine 2.23, every app announces itself to a hook at
9
+ `globalThis[Symbol.for('playcanvas.inspector')]`. Read live state through it instead of adding
10
+ `window.app` or debug code to the app.
11
+
12
+ ## Attach
13
+
14
+ - Resolve `scripts/devtools-hook.js` relative to this skill and inject it as an init script that
15
+ runs before any page script. A hook defined later misses existing apps; reload instead.
16
+ - Page code then reads `globalThis.__playcanvasApps`, a `Map` from each live app to
17
+ `{ version, revision, protocol }`. `app.init()` adds the entry and `app.destroy()` removes it.
18
+ - The script chains a hook defined before it, such as a devtools extension's.
19
+ - No entry means the Engine predates 2.23, the hook loaded late, or the app set
20
+ `devtools: false`. The Engine swallows errors thrown by the hook.
21
+ - In 2.23.0 only the debug build (`playcanvas/debug`, `build/playcanvas.dbg.*`) announces apps; the
22
+ default import never calls the hook. Later 2.23 patches fix every build.
23
+
24
+ ## Read state
25
+
26
+ - Read after `frameend`, so values reflect a rendered frame.
27
+ - Return plain numbers and strings to the caller, not Engine objects.
28
+ - Useful reads: `app.root.findByName()` and `findByTag()`, world positions and
29
+ `meshInstance.aabb`, `app.stats.drawCallCount` and `frameTime`, and
30
+ `entity.anim.baseLayer.activeState`.
31
+ - Pause with `app.timeScale = 0`; `update` still runs with a zero `dt`. Step one frame by restoring
32
+ the scale, then zeroing it again on the next `frameend`.
33
+ - Revert anything you change to take a measurement.
34
+
35
+ ## Inspector panel
36
+
37
+ `@playcanvas/inspector` adds a panel for people: `new Inspector(app)` shows the hierarchy, assets,
38
+ materials, shaders, textures, render targets, frame graph, and physics. It must import the app's
39
+ own `playcanvas` package, so add it only to development builds. It draws over the canvas and
40
+ applies view overrides, so destroy it before `verify-pixels` captures. Stepping through draws
41
+ needs the debug build.
@@ -0,0 +1,23 @@
1
+ // Records every PlayCanvas app (Engine 2.23+) in globalThis.__playcanvasApps, a Map from app to
2
+ // { version, revision, protocol }. Run it before any page script; apps created earlier are missed.
3
+ // Plain script with no imports or exports, so it runs as an init script, a <script>, or an import.
4
+ {
5
+ const key = Symbol.for('playcanvas.inspector');
6
+ const prev = globalThis[key];
7
+ const apps = (globalThis.__playcanvasApps ??= new Map());
8
+
9
+ // chain a hook defined earlier, such as a devtools extension's, and install only once
10
+ if (!prev?.playcanvasApps) {
11
+ globalThis[key] = {
12
+ playcanvasApps: apps,
13
+ register(app, info) {
14
+ apps.set(app, info);
15
+ prev?.register?.(app, info);
16
+ },
17
+ unregister(app) {
18
+ apps.delete(app);
19
+ prev?.unregister?.(app);
20
+ }
21
+ };
22
+ }
23
+ }
@@ -8,7 +8,7 @@ description: Use when a PlayCanvas StandardMaterial needs custom shading its pro
8
8
  Overriding a `StandardMaterial` chunk keeps fog, tone mapping, shadows, skinning, and instancing
9
9
  intact. Use a `ShaderMaterial` only when the look needs its own vertex and fragment pair.
10
10
 
11
- - Set `material.shaderChunksVersion` to the installed major.minor, such as `'2.22'`. Chunk names
11
+ - Set `material.shaderChunksVersion` to the installed major.minor, such as `'2.23'`. Chunk names
12
12
  and contracts change between versions, so read the installed chunk source, not memory.
13
13
  - Set chunks with `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)` and the WGSL
14
14
  equivalent, then call `material.update()`. For every material on a device, use
@@ -5,10 +5,10 @@ description: Use when a PlayCanvas app has too many draw calls or is CPU-bound,
5
5
 
6
6
  # Cut draw calls
7
7
 
8
- Measure first with `app.stats.drawCalls.total` or `MiniStats`; the per-pass breakdown fills only in
9
- profiler builds. Also halve the pixel ratio and call `app.resizeCanvas()`: if frame time drops, the
10
- cost is fill rate, not draw calls. Stop at the first rung that meets the budget and report
11
- before-and-after counts.
8
+ Measure first with `app.stats.drawCallCount` or `MiniStats`; the per-pass `app.stats.drawCalls`
9
+ breakdown fills only in profiler builds. Also halve the pixel ratio and call `app.resizeCanvas()`:
10
+ if frame time drops, the cost is fill rate, not draw calls. Stop at the first rung that meets the
11
+ budget and report before-and-after counts.
12
12
 
13
13
  ## 1. Stop drawing hidden things
14
14
 
@@ -33,4 +33,4 @@ description: Use when a PlayCanvas rendering change is meant to leave the image
33
33
  - Report the count, for example "0 of 65536 pixels differ", or the count and where the differences
34
34
  are. "Looks the same" is not a result.
35
35
 
36
- Keep harness code out of the shipped bundle.
36
+ Keep harness code out of the shipped bundle; attach it through `inspect-runtime`.
@@ -17,6 +17,10 @@ npm run dev
17
17
 
18
18
  Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
19
19
 
20
+ ## Inspector
21
+
22
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
23
+
20
24
  ## Scripts
21
25
 
22
26
  | Command | Description |
@@ -15,14 +15,15 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "playcanvas": "^2.22.6"
18
+ "playcanvas": "^2.23.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@playcanvas/eslint-config": "3.0.0-beta.8",
22
+ "@playcanvas/inspector": "^0.2.0",
22
23
  "eslint": "^9.39.5",
23
24
  "prettier": "^3.9.9",
24
25
  "typescript": "~6.0.3",
25
- "vite": "^8.3.1"
26
+ "vite": "^8.3.2"
26
27
  },
27
28
  "engines": {
28
29
  "node": ">=22.23.2"
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -21,5 +21,5 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
  "noUncheckedSideEffectImports": true
23
23
  },
24
- "include": ["src"]
24
+ "include": ["src", "vite.config.ts"]
25
25
  }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // https://vite.dev/config/
4
+ export default defineConfig({
5
+ plugins: [
6
+ {
7
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
8
+ name: 'playcanvas-inspector',
9
+ apply: 'serve',
10
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
11
+ transformIndexHtml: () => [
12
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
13
+ ]
14
+ }
15
+ ]
16
+ });
@@ -1,4 +1,4 @@
1
- import type { Layer, Texture } from 'playcanvas';
1
+ import type { Layer } from 'playcanvas';
2
2
  import {
3
3
  createGraphicsDevice,
4
4
  AppBase,
@@ -89,7 +89,7 @@ const setupApp = async (canvas: HTMLCanvasElement, onClick: () => void) => {
89
89
  app.start();
90
90
 
91
91
  // Set up environment lighting (no skybox, just IBL)
92
- app.scene.envAtlas = assets.envAtlas.resource as Texture;
92
+ app.scene.envAtlas = assets.envAtlas.resource!;
93
93
  const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
94
94
  if (skyboxLayer) {
95
95
  skyboxLayer.enabled = false;
@@ -1,4 +1,3 @@
1
- import type { ContainerResource } from 'playcanvas';
2
1
  import {
3
2
  AppBase,
4
3
  AppOptions,
@@ -57,7 +56,7 @@ await new Promise<void>((resolve) => {
57
56
  app.start();
58
57
 
59
58
  // A container asset holds the whole glTF scene, so instantiate it to get an entity hierarchy
60
- app.root.addChild((model.resource as ContainerResource).instantiateRenderEntity());
59
+ app.root.addChild(model.resource!.instantiateRenderEntity());
61
60
 
62
61
  // The procedural sky is both the background and the image-based lighting, so no light or
63
62
  // environment map asset is needed. Its default luminance is scene-scale bright, so turn it down to
@@ -1,4 +1,3 @@
1
- import type { ContainerResource, RenderComponent } from 'playcanvas';
2
1
  import {
3
2
  AppBase,
4
3
  AppOptions,
@@ -62,12 +61,12 @@ await new Promise<void>((resolve) => new AssetListLoader([product], app.assets).
62
61
 
63
62
  app.start();
64
63
 
65
- const car = (product.resource as ContainerResource).instantiateRenderEntity();
64
+ const car = product.resource!.instantiateRenderEntity();
66
65
  car.setLocalEulerAngles(-90, -25, 0);
67
66
  app.root.addChild(car);
68
67
  app.root.syncHierarchy();
69
68
 
70
- const meshes = (car.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
69
+ const meshes = car.findComponents('render').flatMap((render) => render.meshInstances);
71
70
  const bounds = meshes[0].aabb.clone();
72
71
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
73
72
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -77,10 +76,10 @@ bounds.copy(meshes[0].aabb);
77
76
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
78
77
  car.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
79
78
 
80
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
81
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
82
- const paint = body[0].material.clone() as StandardMaterial;
83
- const shade = panels[0].material.clone() as StandardMaterial;
79
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
80
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
81
+ const paint = body[0].material!.clone() as StandardMaterial;
82
+ const shade = panels[0].material!.clone() as StandardMaterial;
84
83
  body.forEach((mesh) => (mesh.material = paint));
85
84
  panels.forEach((mesh) => (mesh.material = shade));
86
85
  paint.diffuse.fromString(PAINTS[0].color);
@@ -17,6 +17,10 @@ npm run dev
17
17
 
18
18
  Open <http://localhost:5173>. Edit the files under `src/` and save to see the scene update.
19
19
 
20
+ ## Inspector
21
+
22
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
23
+
20
24
  ## Scripts
21
25
 
22
26
  | Command | Description |
@@ -16,19 +16,20 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/react": "^0.11.7",
19
- "playcanvas": "^2.22.6",
19
+ "playcanvas": "^2.23.0",
20
20
  "react": "^19.3.0",
21
21
  "react-dom": "^19.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playcanvas/eslint-config": "3.0.0-beta.8",
25
+ "@playcanvas/inspector": "^0.2.0",
25
26
  "@types/react": "^19.3.0",
26
27
  "@types/react-dom": "^19.3.0",
27
28
  "@vitejs/plugin-react": "^6.1.1",
28
29
  "eslint": "^9.39.5",
29
30
  "prettier": "^3.9.9",
30
31
  "typescript": "~6.0.3",
31
- "vite": "^8.3.1"
32
+ "vite": "^8.3.2"
32
33
  },
33
34
  "engines": {
34
35
  "node": ">=22.23.2"
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -3,5 +3,16 @@ import { defineConfig } from 'vite';
3
3
 
4
4
  // https://vite.dev/config/
5
5
  export default defineConfig({
6
- plugins: [react()]
6
+ plugins: [
7
+ react(),
8
+ {
9
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
10
+ name: 'playcanvas-inspector',
11
+ apply: 'serve',
12
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
13
+ transformIndexHtml: () => [
14
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
15
+ ]
16
+ }
17
+ ]
7
18
  });
@@ -1,7 +1,7 @@
1
1
  import { Application, Entity } from '@playcanvas/react';
2
2
  import { Camera, Light, Script } from '@playcanvas/react/components';
3
3
  import { useApp, useModel } from '@playcanvas/react/hooks';
4
- import type { ContainerResource, Entity as PcEntity, RenderComponent } from 'playcanvas';
4
+ import type { ContainerResource, Entity as PcEntity } from 'playcanvas';
5
5
  import { Color, CylinderGeometry, Mesh, MeshInstance, StandardMaterial, TONEMAP_ACES2, Vec2, Vec3 } from 'playcanvas';
6
6
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
7
7
  import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
@@ -32,7 +32,7 @@ function Product({ color, shade }: { color: string; shade: string }) {
32
32
  entity.setLocalEulerAngles(-90, -25, 0);
33
33
  root.current.addChild(entity);
34
34
 
35
- const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
35
+ const meshes = entity.findComponents('render').flatMap((render) => render.meshInstances);
36
36
  const bounds = meshes[0].aabb.clone();
37
37
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
38
38
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -42,10 +42,10 @@ function Product({ color, shade }: { color: string; shade: string }) {
42
42
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
43
43
  entity.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
44
44
 
45
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
46
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
47
- paint.current = body[0].material.clone() as StandardMaterial;
48
- tone.current = panels[0].material.clone() as StandardMaterial;
45
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
46
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
47
+ paint.current = body[0].material!.clone() as StandardMaterial;
48
+ tone.current = panels[0].material!.clone() as StandardMaterial;
49
49
  body.forEach((mesh) => (mesh.material = paint.current!));
50
50
  panels.forEach((mesh) => (mesh.material = tone.current!));
51
51
  return () => {
@@ -18,6 +18,10 @@ npm run dev
18
18
 
19
19
  Open <http://localhost:5173>. Edit `index.html` to change the scene.
20
20
 
21
+ ## Inspector
22
+
23
+ `npm run dev` loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the hierarchy, assets, materials, textures, frame graph and physics. Press the backquote key (<kbd>&#96;</kbd>) to show or hide it, <kbd>F9</kbd> to pause and <kbd>F10</kbd> to step a frame. It is wired up in `vite.config.ts` and `src/inspector.ts` and is left out of production builds.
24
+
21
25
  ## Scripts
22
26
 
23
27
  | Command | Description |
@@ -15,15 +15,16 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/web-components": "^0.26.0",
19
- "playcanvas": "^2.22.6"
18
+ "@playcanvas/web-components": "^0.28.0",
19
+ "playcanvas": "^2.23.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0-beta.8",
23
+ "@playcanvas/inspector": "^0.2.0",
23
24
  "eslint": "^9.39.5",
24
25
  "prettier": "^3.9.9",
25
26
  "typescript": "~6.0.3",
26
- "vite": "^8.3.1"
27
+ "vite": "^8.3.2"
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=22.23.2"
@@ -0,0 +1,33 @@
1
+ /* Inspector key hint, added on the dev server only by src/inspector.ts. Styled like the starter panels */
2
+ .inspector-hint {
3
+ position: fixed;
4
+ bottom: 20px;
5
+ left: 20px;
6
+ z-index: 10;
7
+ margin: 0;
8
+ padding: 8px 12px;
9
+ border: 1px solid #ffffff24;
10
+ border-radius: 10px;
11
+ background: #0b1019d9;
12
+ box-shadow: 0 12px 40px #0008;
13
+ color: #aeb8c8;
14
+ font:
15
+ 12px/1.6 Inter,
16
+ ui-sans-serif,
17
+ system-ui,
18
+ sans-serif;
19
+ backdrop-filter: blur(12px);
20
+ pointer-events: none;
21
+ }
22
+
23
+ .inspector-hint kbd {
24
+ padding: 1px 5px;
25
+ border-radius: 4px;
26
+ background: #263247;
27
+ color: #f8fafc;
28
+ font:
29
+ 11px ui-monospace,
30
+ SFMono-Regular,
31
+ Menlo,
32
+ monospace;
33
+ }
@@ -0,0 +1,38 @@
1
+ // Loaded by the dev server only (see vite.config.ts), so production builds never include it
2
+ import { Inspector } from '@playcanvas/inspector';
3
+ import type { AppBase } from 'playcanvas';
4
+
5
+ import './inspector.css';
6
+
7
+ type Hook = {
8
+ register?(app: AppBase, info: object): void;
9
+ unregister?(app: AppBase): void;
10
+ };
11
+
12
+ // Every app announces itself to this hook as it starts, whichever format created it
13
+ const KEY = Symbol.for('playcanvas.inspector');
14
+ const scope = globalThis as unknown as Record<symbol, Hook | undefined>;
15
+
16
+ // Keep a hook installed earlier, such as a devtools extension's
17
+ const prev = scope[KEY];
18
+
19
+ scope[KEY] = {
20
+ register(app, info) {
21
+ prev?.register?.(app, info);
22
+
23
+ // Skip headless apps, such as the null apps @playcanvas/react creates for validation. Each
24
+ // panel destroys itself with its app
25
+ if (!app.graphicsDevice.isNull) new Inspector(app, { visible: false });
26
+ },
27
+ unregister(app) {
28
+ prev?.unregister?.(app);
29
+ }
30
+ };
31
+
32
+ console.info('PlayCanvas Inspector: press ` to toggle');
33
+
34
+ // Key hint, a separate corner chip so the starter's own UI stays untouched
35
+ const hint = document.createElement('p');
36
+ hint.className = 'inspector-hint';
37
+ hint.innerHTML = '<kbd>`</kbd> inspector · <kbd>F9</kbd> pause · <kbd>F10</kbd> step';
38
+ document.body.append(hint);
@@ -21,5 +21,5 @@
21
21
  "noFallthroughCasesInSwitch": true,
22
22
  "noUncheckedSideEffectImports": true
23
23
  },
24
- "include": ["src"]
24
+ "include": ["src", "vite.config.ts"]
25
25
  }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ // https://vite.dev/config/
4
+ export default defineConfig({
5
+ plugins: [
6
+ {
7
+ // Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
8
+ name: 'playcanvas-inspector',
9
+ apply: 'serve',
10
+ config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
11
+ transformIndexHtml: () => [
12
+ { tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
13
+ ]
14
+ }
15
+ ]
16
+ });
@@ -1,6 +1,5 @@
1
1
  import type { EntityElement } from '@playcanvas/web-components';
2
2
  import { whenReady } from '@playcanvas/web-components';
3
- import type { Texture } from 'playcanvas';
4
3
  import { Asset, TEXTURETYPE_RGBP } from 'playcanvas';
5
4
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
6
5
  import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
@@ -40,7 +39,7 @@ await new Promise<void>((resolve) => {
40
39
  });
41
40
 
42
41
  // Light the scene from the environment, with the skybox itself left hidden
43
- app.scene.envAtlas = envAtlas.resource as Texture;
42
+ app.scene.envAtlas = envAtlas.resource!;
44
43
  const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
45
44
  if (skyboxLayer) {
46
45
  skyboxLayer.enabled = false;
@@ -1,6 +1,5 @@
1
1
  import type { AssetElement, EntityElement, ModelElement } from '@playcanvas/web-components';
2
2
  import { whenReady } from '@playcanvas/web-components';
3
- import type { RenderComponent } from 'playcanvas';
4
3
  import { Color, CylinderGeometry, Mesh, MeshInstance, StandardMaterial, Vec2, Vec3 } from 'playcanvas';
5
4
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
6
5
  import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
@@ -63,7 +62,7 @@ ground.addComponent('render', {
63
62
  const entity = model.entity!;
64
63
  entity.setLocalEulerAngles(0, -25, 0);
65
64
  entity.root.syncHierarchy();
66
- const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
65
+ const meshes = entity.findComponents('render').flatMap((render) => render.meshInstances);
67
66
  const bounds = meshes[0].aabb.clone();
68
67
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
69
68
  const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
@@ -73,10 +72,10 @@ bounds.copy(meshes[0].aabb);
73
72
  meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
74
73
  entity.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
75
74
 
76
- const body = meshes.filter((mesh) => mesh.material.name === 'material');
77
- const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
78
- const paint = body[0].material.clone() as StandardMaterial;
79
- const shade = panels[0].material.clone() as StandardMaterial;
75
+ const body = meshes.filter((mesh) => mesh.material?.name === 'material');
76
+ const panels = meshes.filter((mesh) => mesh.material?.name === 'material_1');
77
+ const paint = body[0].material!.clone() as StandardMaterial;
78
+ const shade = panels[0].material!.clone() as StandardMaterial;
80
79
  body.forEach((mesh) => (mesh.material = paint));
81
80
  panels.forEach((mesh) => (mesh.material = shade));
82
81
  paint.diffuse.fromString('#d6293e');