Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions packages/tools/sandbox/src/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import babylonIdentity from "../img/babylon-identity.svg";
import iconEdit from "../img/icon-edit.svg";
import iconOpen from "../img/icon-open.svg";
import iconIBL from "../img/icon-ibl.svg";
import iconOpenPBR from "../img/icon-openpbr.png";
import iconCameras from "../img/icon-cameras.svg";
import iconVariants from "../img/icon-variants.svg";
import { LocalStorageHelper } from "../tools/localStorageHelper";

interface IFooterProps {
globalState: GlobalState;
Expand Down Expand Up @@ -74,6 +76,17 @@ export class Footer extends React.Component<IFooterProps, IFooterState> {
return this.props.globalState?.glTFLoaderExtensions["KHR_materials_variants"] as KHR_materials_variants;
}

private _toggleUseOpenPBR(): void {
this.props.globalState.useOpenPBR = !this.props.globalState.useOpenPBR;
LocalStorageHelper.SetUseOpenPBR(this.props.globalState.useOpenPBR);

if (this.props.globalState.currentScene) {
this.props.globalState.onRequestSceneReload.notifyObservers();
}

this.forceUpdate();
}

override render() {
let variantNames: string[] = [];
let hasVariants = false;
Expand Down Expand Up @@ -151,6 +164,14 @@ export class Footer extends React.Component<IFooterProps, IFooterState> {
enabled={!!this.props.globalState.currentScene}
searchPlaceholder="Search environment"
/>
<FooterButton
globalState={this.props.globalState}
icon={iconOpenPBR}
label="Toggle OpenPBR for glTF loading"
onClick={() => this._toggleUseOpenPBR()}
enabled={true}
active={this.props.globalState.useOpenPBR}
/>
<FooterButton
globalState={this.props.globalState}
icon={iconEdit}
Expand Down
3 changes: 2 additions & 1 deletion packages/tools/sandbox/src/components/footerButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface IFooterButtonProps {
onClick: () => void;
icon: any;
label: string;
active?: boolean;
}

export class FooterButton extends React.Component<IFooterButtonProps> {
Expand All @@ -16,7 +17,7 @@ export class FooterButton extends React.Component<IFooterButtonProps> {
}

return (
<div className="button" onClick={() => this.props.onClick()}>
<div className={"button" + (this.props.active ? " active" : "")} onClick={() => this.props.onClick()}>
<img src={this.props.icon} alt={this.props.label} title={this.props.label} />
</div>
);
Expand Down
40 changes: 31 additions & 9 deletions packages/tools/sandbox/src/components/renderingZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { PBRMaterial } from "core/Materials/PBR/pbrMaterial";
import { type AbstractEngine } from "core/Engines/abstractEngine";
import { setOpenGLOrientationForUV, useOpenGLOrientationForUV } from "core/Compat/compatibilityOptions";
import { ImageProcessingConfiguration } from "core/Materials/imageProcessingConfiguration";
import { type Observer } from "core/Misc/observable";

function GetFileExtension(str: string): string {
return str.split(".").pop() || "";
Expand Down Expand Up @@ -60,6 +61,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
private _scene: Scene;
private _canvas: HTMLCanvasElement;
private _restoreInspector = false;
private _onRequestSceneReloadObserver: Observer<void> | null = null;

public constructor(props: IRenderingZoneProps) {
super(props);
Expand Down Expand Up @@ -120,10 +122,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
}
},
() => {
// Ensure we stop any existing render loop when reloading, because if there was a previous scene loaded from the URL
// the filesInput will not know about it, and so it won't call stopRenderLoop.
this._engine.stopRenderLoop();
filesInput.reload();
this._reloadCurrentAsset();
},
(file, scene, message) => {
this.props.globalState.onError.notifyObservers({ message: message });
Expand Down Expand Up @@ -180,15 +179,30 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
window.addEventListener("keydown", (event) => {
// Press R to reload
if (event.keyCode === 82 && event.target && (event.target as HTMLElement).nodeName !== "INPUT" && this._scene) {
if (this.props.globalState.assetUrl) {
this.loadAssetFromUrl(this.props.globalState.assetUrl);
} else {
filesInput.reload();
}
this._reloadCurrentAsset();
}
});

this._onRequestSceneReloadObserver = this.props.globalState.onRequestSceneReload.add(() => {
if (this._scene) {
this._reloadCurrentAsset();
}
});
}

private _reloadCurrentAsset(): void {
// Ensure we stop any existing render loop when reloading, because if there was a previous scene loaded from the URL
// filesInput will not know about it, and so it won't call stopRenderLoop.
this._engine.stopRenderLoop();

if (this.props.globalState.assetUrl) {
this.loadAssetFromUrl(this.props.globalState.assetUrl);
return;
}

this.props.globalState.filesInput.reload();
}

prepareCamera() {
let camera = this._scene.activeCamera as ArcRotateCamera;
// Attach camera to canvas inputs
Expand Down Expand Up @@ -420,6 +434,7 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
if (this._currentPluginName === "gltf") {
const loader = plugin as GLTFFileLoader;
loader.transparencyAsCoverage = this.props.globalState.commerceMode;
loader.useOpenPBR = this.props.globalState.useOpenPBR;

loader.validate = true;

Expand All @@ -438,6 +453,13 @@ export class RenderingZone extends React.Component<IRenderingZoneProps> {
this.initEngine();
}

public override componentWillUnmount(): void {
if (this._onRequestSceneReloadObserver) {
this.props.globalState.onRequestSceneReload.remove(this._onRequestSceneReloadObserver);
this._onRequestSceneReloadObserver = null;
}
}
Comment on lines +456 to +461
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

componentWillUnmount cleans up the onRequestSceneReload observer, but initEngine() also registers window-level resize and keydown listeners that aren’t removed. If this component is ever unmounted/remounted (e.g., hot reload), those handlers will accumulate and can cause duplicate reload/resize behavior; consider storing the handler functions and removing them here as well.

Copilot uses AI. Check for mistakes.

override shouldComponentUpdate(nextProps: IRenderingZoneProps) {
if (nextProps.expanded !== this.props.expanded) {
setTimeout(() => this._engine.resize());
Expand Down
10 changes: 10 additions & 0 deletions packages/tools/sandbox/src/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@ declare module "*.svg" {
const content: string;
export default content;
}

declare module "*.scss" {
const content: string;
export default content;
}

declare module "*.png" {
const content: string;
export default content;
}
2 changes: 2 additions & 0 deletions packages/tools/sandbox/src/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export class GlobalState {
public onEnvironmentChanged = new Observable<string>();
public onRequestClickInterceptor = new Observable<void>();
public onClickInterceptorClicked = new Observable<void>();
public onRequestSceneReload = new Observable<void>();
public glTFLoaderExtensions: { [key: string]: import("loaders/glTF/index").IGLTFLoaderExtension } = {};
public onFilesInputReady = new Observable<void>();

public filesInput: FilesInput;
public isDebugLayerEnabled = false;

public commerceMode = false;
public useOpenPBR = false;

public assetUrl?: string;
public autoRotate = false;
Expand Down
Binary file added packages/tools/sandbox/src/img/icon-openpbr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/tools/sandbox/src/sandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export class Sandbox extends React.Component<
public constructor(props: ISandboxProps) {
super(props);
this._globalState = new GlobalState({ version: props.version, bundles: props.bundles });
this._globalState.useOpenPBR = LocalStorageHelper.GetUseOpenPBR();
this._logoRef = React.createRef();
this._dropTextRef = React.createRef();
this._clickInterceptorRef = React.createRef();
Expand Down
17 changes: 17 additions & 0 deletions packages/tools/sandbox/src/tools/localStorageHelper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const WelcomeDialogDismissedKey = "WelcomeDialogDismissed";
const UseOpenPBRKey = "UseOpenPBR";

export class LocalStorageHelper {
public static ReadLocalStorageValue(key: string, defaultValue: number) {
Expand Down Expand Up @@ -30,4 +31,20 @@ export class LocalStorageHelper {
public static ClearWelcomeDialogDismissed(): void {
localStorage.removeItem(WelcomeDialogDismissedKey);
}

/**
* Sets whether glTF loading should force OpenPBR materials.
* @param value The value to persist.
*/
public static SetUseOpenPBR(value: boolean): void {
localStorage.setItem(UseOpenPBRKey, value ? "true" : "false");
}

/**
* Gets whether glTF loading should force OpenPBR materials.
* @returns true when OpenPBR mode is enabled.
*/
public static GetUseOpenPBR(): boolean {
return localStorage.getItem(UseOpenPBRKey) === "true";
}
}
Loading