Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6f41637
Add Inspector debug panel to extras with an examples toolbar toggle
Sep 12, 2026
6cb6908
Add per-body draw checkboxes to the physics tab and a clearer master …
Sep 12, 2026
841dca2
Persist inspector settings across restarts and draw joint anchor frames
Sep 12, 2026
2df6eda
Keep the inspector's internal views and typedefs out of the public API
Sep 14, 2026
3455bad
Reduce the Inspector public surface to its essential members
Sep 14, 2026
db78341
Trim the Inspector API to constructor, destroy, visible, paused and step
Sep 15, 2026
fbe235a
Merge remote-tracking branch 'origin/main' into mv-inspector
Sep 15, 2026
a370209
Preview the selected render target with TextureRenderer
Sep 15, 2026
9659816
Draw texture previews blended so layers with only a transparent sub-l…
Sep 15, 2026
bb59830
Merge remote-tracking branch 'origin/main' into mv-inspector
Sep 15, 2026
df04022
changes
Sep 15, 2026
9931133
Add mesh instance and material sections and a Textures tab to the Ins…
Sep 19, 2026
2f50cd3
Merge remote-tracking branch 'origin/main' into mv-inspector
Sep 20, 2026
a4acf8b
Merge remote-tracking branch 'origin/main' into mv-inspector
Sep 20, 2026
96f6ea6
Add a Shaders tab with source and bind group viewers to the Inspector
Sep 20, 2026
136b5ef
Add an Assets tab to the Inspector, cross-linked with components and …
Sep 22, 2026
ec76443
Open mesh instances in place in the Inspector, down to vertex elements
Sep 22, 2026
9c7d2a5
Draw indent guides for the expanded rows of the Inspector
Sep 22, 2026
479fc80
Return an Inspector subject to where it was last read
Sep 22, 2026
43652f5
Add a Memory tab to the Inspector, attributing buffers to their owners
Sep 23, 2026
901e009
Show what each forward layer step drew in the Inspector frame graph
Sep 23, 2026
dfc3335
feat: Step through the draws of a render pass in the inspector
Sep 23, 2026
063dad9
Merge branch 'main' into mv-inspector
Sep 23, 2026
0b5cb10
feat: Add a Meshes tab to the inspector
Sep 24, 2026
a13fec3
feat: Add a Materials tab to the inspector
Sep 24, 2026
d8f10ab
feat: Preview a texture in the inspector while its link is hovered
Sep 24, 2026
1b6e3a4
feat: Bracket the passes each pass owns in the inspector frame graph
Sep 24, 2026
47c6b9f
feat: Preview a pass's render target in the inspector while it is hov…
Sep 24, 2026
468942f
feat: Add a Scripts tab to the inspector
Sep 25, 2026
7531855
feat: Filter the inspector hierarchy by component or script, and coun…
Sep 25, 2026
062a336
feat: Group the sub-assets of a container under it in the inspector a…
Sep 25, 2026
3b980fe
feat: Show what uses an asset's resource directly in the inspector
Sep 25, 2026
6cc718c
feat: Pick and fly in the view, and add a Cameras tab to the inspector
Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const booleanFlags = new Set([
'HIDDEN',
'NO_DEVICE_SELECTOR',
'NO_MINISTATS',
'NO_INSPECTOR',
'WEBGPU_DISABLED',
'WEBGPU_BARE_DISABLED',
'WEBGL_DISABLED',
Expand Down
83 changes: 83 additions & 0 deletions examples/iframe/inspector.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { fire, getQueryParams } from './runtime.mjs';

/** @import { AppBase, Inspector as PcInspector } from 'playcanvas' */

const params = getQueryParams(window.location.href);

// keeps the docked panel clear of the toolbar buttons the app floats over the top-left corner
const TOOLBAR_INSET = 52;

export default class Inspector {
/**
* The instance driven by the UI toggle. Either created here on demand, or handed over by an
* example which needs custom options, see {@link Inspector.adopt}.
*
* @type {PcInspector | null}
*/
static instance = null;

/**
* Starts driving an instance: the UI learns about every change of its visibility, whether from
* the toolbar button, the panel's close button or its hotkey.
*
* @param {PcInspector} instance - The instance.
*/
static _bind(instance) {
Inspector.instance = instance;
instance.onVisibleChange = state => fire('inspector', { state });
}

/**
* Takes over an instance created by the example itself, so the UI toggle drives that one
* instead of adding a second panel on top of it.
*
* @param {PcInspector | null | undefined} instance - The instance exported by the example.
*/
static adopt(instance) {
if (instance) {
Inspector._bind(instance);
// thumbnail capture loads the example without the surrounding UI, so there is no toggle
// to fold this into - suppress the panel here instead
if (params.inspector === 'false') {
instance.visible = false;
}
}
}

/**
* @param {AppBase} app - The app instance.
* @param {any} state - The requested visibility.
* @returns {boolean} The resolved visibility.
*/
static enable(app, state) {
if (typeof window.pc === 'undefined' || !window.pc.Inspector) {
return false;
}
if (!app) {
return false;
}

// thumbnail capture suppresses the panel
const suppressed = params.inspector === 'false';
const visible = !suppressed && !!state;

if (visible && !Inspector.instance) {
Inspector._bind(new window.pc.Inspector(app, {
dock: 'left',
top: TOOLBAR_INSET,
visible: false
}));
}
if (!Inspector.instance) {
return false;
}

Inspector.instance.visible = visible;
return Inspector.instance.visible;
}

static destroy() {
Inspector.instance?.destroy();
Inspector.instance = null;
}
}
29 changes: 28 additions & 1 deletion examples/iframe/loader.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import files from './files.mjs';
import Inspector from './inspector.mjs';
import MiniStats from './ministats.mjs';
import { fetchFile, importModule, clearImports, parseConfig, fire, win } from './runtime.mjs';
import { data, deviceType as selectedDeviceType, refreshContext, updateDeviceType } from './state.mjs';
Expand Down Expand Up @@ -57,6 +58,13 @@ class ExampleLoader {
*/
_miniStatsEnabled = false;

/**
* The last requested Inspector visibility, re-applied on hot reload.
*
* @type {boolean}
*/
_inspectorEnabled = false;

/**
* @type {Function[]}
* @private
Expand Down Expand Up @@ -97,6 +105,7 @@ class ExampleLoader {
// here after a hot reload - otherwise the panel stays hidden until the button is toggled twice.
if (!firstStart) {
this.setMiniStats(this._miniStatsEnabled);
this.setInspector(this._inspectorEnabled);
}

// Updates controls UI
Expand Down Expand Up @@ -216,7 +225,8 @@ class ExampleLoader {
// just notify to clean UI, but not during hot-reload
fire('exampleLoading', {
showDeviceSelector: !this._config.NO_DEVICE_SELECTOR,
showMiniStats: !this._config.NO_MINISTATS
showMiniStats: !this._config.NO_MINISTATS,
showInspector: !this._config.NO_INSPECTOR
});
}

Expand All @@ -231,6 +241,9 @@ class ExampleLoader {
// so that the UI toggle drives that instance
MiniStats.adopt(module.miniStats);

// likewise for an example that creates its own Inspector
Inspector.adopt(module.inspector);

// additional destroy handler for non-app resources
if (typeof module.destroy === 'function') {
this.destroyHandlers.push(module.destroy);
Expand Down Expand Up @@ -295,6 +308,19 @@ class ExampleLoader {
fire('miniStats', { state: MiniStats.enable(this._app, enabled) });
}

/**
* @param {boolean} enabled - The requested visibility of the inspector panel
*/
setInspector(enabled = false) {
this._inspectorEnabled = enabled;
if (this._config.NO_INSPECTOR) {
// no button, but the example may own a panel whose state the UI still wants to know
fire('inspector', { state: Inspector.instance?.visible ?? false });
return;
}
fire('inspector', { state: Inspector.enable(this._app, enabled) });
}

hotReload() {
if (!this._allowRestart) {
console.warn('Dropping restart while still restarting');
Expand All @@ -320,6 +346,7 @@ class ExampleLoader {

destroy() {
MiniStats.destroy();
Inspector.destroy();
this._destroyApps();
const handlers = this.destroyHandlers;
this.destroyHandlers = [];
Expand Down
3 changes: 2 additions & 1 deletion examples/src/app/components/Example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ const createState = () => {
* @property {null|'examples'|'code'|'controls'|'description'} [mobilePanel] - Active mobile panel.
* @property {(mobilePanel: null|'examples'|'code'|'controls'|'description') => void} [setMobilePanel] - Set active mobile panel.
* @property {boolean} [showCredits] - Whether the desktop credits overlay is visible.
* @property {boolean} [hideDescription] - Hide the desktop description, e.g. while the inspector panel is shown.
* @property {(event: PointerEvent | import('react').PointerEvent<HTMLElement>) => void} [onMobilePanelDragStart] - Start mobile panel drag.
*/

Expand Down Expand Up @@ -710,7 +711,7 @@ class Example extends TypedComponent {

renderDescription() {
const { exampleLoaded, description } = this.state;
if (!exampleLoaded || !description || !iframe.ready) {
if (!exampleLoaded || !description || !iframe.ready || this.props.hideDescription) {
return null;
}
return jsx(
Expand Down
26 changes: 24 additions & 2 deletions examples/src/app/components/MainLayout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function getDefaultMobilePanelWidth() {
* @property {number} mobilePanelHeight - Active mobile panel height.
* @property {number} mobilePanelWidth - Active mobile panel width.
* @property {boolean} showCredits - Whether the desktop credits overlay is visible.
* @property {boolean} showInspector - Whether the inspector panel is shown, which hides the desktop description.
*/

/** @type {typeof Component<Props, State>} */
Expand All @@ -101,7 +102,8 @@ class MainLayout extends TypedComponent {
mobilePanel: getInitialMobilePanel(layout, panel),
mobilePanelHeight: getMobilePanelHeight(height),
mobilePanelWidth: getMobilePanelWidth(width),
showCredits: localStorage.getItem('showCredits') !== 'false'
showCredits: localStorage.getItem('showCredits') !== 'false',
showInspector: false
};
})();

Expand Down Expand Up @@ -242,12 +244,14 @@ class MainLayout extends TypedComponent {
componentDidMount() {
window.addEventListener('resize', this._onLayoutChange);
window.addEventListener('orientationchange', this._onLayoutChange);
window.addEventListener('inspector', this._handleInspector);
}

componentWillUnmount() {
this.stopMobilePanelDrag();
window.removeEventListener('resize', this._onLayoutChange);
window.removeEventListener('orientationchange', this._onLayoutChange);
window.removeEventListener('inspector', this._handleInspector);
}

/**
Expand All @@ -257,6 +261,22 @@ class MainLayout extends TypedComponent {
iframe.fire('stats', { state: value });
};

/**
* @param {boolean} value - Show Inspector state.
*/
updateShowInspector = (value) => {
this.setState({ showInspector: value });
iframe.fire('inspector', { state: value });
};

/**
* @param {Event} event - Inspector state event from the iframe, carrying the resolved state.
*/
_handleInspector = (event) => {
const customEvent = /** @type {CustomEvent<{ state: boolean }>} */ (event);
this.setState({ showInspector: !!customEvent.detail.state });
};

/**
* @param {boolean} value - Show credits state.
*/
Expand All @@ -266,7 +286,7 @@ class MainLayout extends TypedComponent {
};

render() {
const { layout, mobileOrientation, mobilePanel, mobilePanelHeight, mobilePanelWidth, showCredits } = this.state;
const { layout, mobileOrientation, mobilePanel, mobilePanelHeight, mobilePanelWidth, showCredits, showInspector } = this.state;
return jsx(
'div',
{
Expand Down Expand Up @@ -304,6 +324,7 @@ class MainLayout extends TypedComponent {
jsx(Menu, {
layout,
setShowMiniStats: this.updateShowMiniStats.bind(this),
setShowInspector: this.updateShowInspector,
showCredits,
setShowCredits: this.setShowCredits
}),
Expand All @@ -316,6 +337,7 @@ class MainLayout extends TypedComponent {
mobilePanel,
setMobilePanel: this.setMobilePanel,
showCredits,
hideDescription: showInspector,
onMobilePanelDragStart: this.startMobilePanelDrag
})
)
Expand Down
45 changes: 42 additions & 3 deletions examples/src/app/components/Menu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getLayout } from '../utils.mjs';
/**
* @typedef {object} Props
* @property {(value: boolean) => void} setShowMiniStats - The state set function .
* @property {(value: boolean) => void} setShowInspector - Sets whether the inspector panel is shown.
* @property {'mobile'|'desktop'} [layout] - Current layout.
* @property {boolean} showCredits - Whether the desktop credits overlay is visible.
* @property {(value: boolean) => void} setShowCredits - Set credits overlay visibility.
Expand All @@ -20,6 +21,8 @@ import { getLayout } from '../utils.mjs';
* @typedef {object} State
* @property {boolean} showMiniStats - Show MiniStats state.
* @property {boolean} hasMiniStats - Whether the loaded example allows the MiniStats overlay.
* @property {boolean} showInspector - Show Inspector state.
* @property {boolean} hasInspector - Whether the loaded example allows the Inspector toggle.
* @property {boolean} fullscreen - Fullscreen state.
* @property {boolean} hasCredits - Whether the loaded example has any credits.
* @property {boolean} shareDialogOpen - Whether the share dialog is visible.
Expand All @@ -38,6 +41,8 @@ class Menu extends TypedComponent {
showMiniStats: typeof ui.miniStats === 'boolean' ? ui.miniStats : getLayout() === 'desktop',
fullscreen: typeof ui.fullscreen === 'boolean' ? ui.fullscreen : false,
hasMiniStats: true,
showInspector: typeof ui.inspector === 'boolean' ? ui.inspector : false,
hasInspector: true,
hasCredits: false,
shareDialogOpen: false,
shareUrl: '',
Expand All @@ -57,6 +62,8 @@ class Menu extends TypedComponent {
this._handleExampleLoad = this._handleExampleLoad.bind(this);
this._handleMiniStats = this._handleMiniStats.bind(this);
this.toggleMiniStats = this.toggleMiniStats.bind(this);
this._handleInspector = this._handleInspector.bind(this);
this.toggleInspector = this.toggleInspector.bind(this);
this.toggleCredits = this.toggleCredits.bind(this);
this.openShareDialog = this.openShareDialog.bind(this);
this.closeShareDialog = this.closeShareDialog.bind(this);
Expand Down Expand Up @@ -134,6 +141,7 @@ class Menu extends TypedComponent {
window.addEventListener('exampleLoading', this._handleExampleLoading);
window.addEventListener('exampleLoad', this._handleExampleLoad);
window.addEventListener('miniStats', this._handleMiniStats);
window.addEventListener('inspector', this._handleInspector);
}

componentWillUnmount() {
Expand All @@ -148,6 +156,7 @@ class Menu extends TypedComponent {
window.removeEventListener('exampleLoading', this._handleExampleLoading);
window.removeEventListener('exampleLoad', this._handleExampleLoad);
window.removeEventListener('miniStats', this._handleMiniStats);
window.removeEventListener('inspector', this._handleInspector);
}

openShareDialog() {
Expand Down Expand Up @@ -183,15 +192,19 @@ class Menu extends TypedComponent {
* @param {Event} event - exampleLoading event.
*/
_handleExampleLoading(event) {
const detail = /** @type {CustomEvent<{ showMiniStats?: boolean }>} */ (event).detail;
this.setState({ hasMiniStats: detail?.showMiniStats !== false });
const detail = /** @type {CustomEvent<{ showMiniStats?: boolean, showInspector?: boolean }>} */ (event).detail;
this.setState({
hasMiniStats: detail?.showMiniStats !== false,
hasInspector: detail?.showInspector !== false
});
}

/**
* @param {Event} event - exampleLoad event.
*/
_handleExampleLoad(event) {
this.props.setShowMiniStats(this.state.showMiniStats);
this.props.setShowInspector(this.state.showInspector);
this.setFullscreen(this.state.fullscreen, true);
const detail = /** @type {CustomEvent<{ credits?: unknown[] }>} */ (event).detail;
this.setState({ hasCredits: (detail?.credits?.length ?? 0) > 0 });
Expand All @@ -213,8 +226,27 @@ class Menu extends TypedComponent {
}, () => patchState({ ui: { miniStats: this.state.showMiniStats } }));
}

toggleInspector() {
const value = !this.state.showInspector;
this.setState({ showInspector: value }, () => patchState({ ui: { inspector: this.state.showInspector } }));
this.props.setShowInspector(value);
}

/**
* @param {Event} event - Inspector state event.
*/
_handleInspector(event) {
const customEvent = /** @type {CustomEvent<{ state: boolean }>} */ (event);
// an example that owns its panel reports its state too, but that is not the toggle's state:
// it must neither persist nor carry over to the next example
if (!this.state.hasInspector) return;
this.setState({
showInspector: !!customEvent.detail.state
}, () => patchState({ ui: { inspector: this.state.showInspector } }));
}

render() {
const { showMiniStats, hasMiniStats, hasCredits, shareDialogOpen, shareUrl, shareTitle } = this.state;
const { showMiniStats, hasMiniStats, showInspector, hasInspector, hasCredits, shareDialogOpen, shareUrl, shareTitle } = this.state;
const { layout, showCredits } = this.props;
return jsx(
Container,
Expand Down Expand Up @@ -263,6 +295,13 @@ class Menu extends TypedComponent {
text: '',
onClick: this.toggleMiniStats
}),
hasInspector && jsx(Button, {
icon: 'E147',
id: 'showInspectorButton',
class: showInspector ? 'selected' : undefined,
text: '',
onClick: this.toggleInspector
}),
hasCredits && layout === 'desktop' && jsx(Button, {
id: 'showCreditsButton',
class: showCredits ? 'selected' : undefined,
Expand Down
1 change: 1 addition & 0 deletions examples/src/app/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @typedef {object} LoadingEventDetail
* @property {boolean} showDeviceSelector - Show device selector
* @property {boolean} showMiniStats - Show the MiniStats toggle
* @property {boolean} showInspector - Show the Inspector toggle
*
* @typedef {CustomEvent<LoadingEventDetail>} LoadingEvent.
*/
Expand Down
Loading
Loading