Skip to content

Inspector V2: Tools Pane Init #16798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
5 changes: 4 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/dev/inspector-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
"devDependencies": {
"@dev/core": "1.0.0",
"@dev/loaders": "1.0.0",
"@dev/serializers": "1.0.0",
"@fluentui/react-components": "^9.62.0",
"@fluentui/react-icons": "^2.0.271",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"gif.js.optimized": "^1.0.1",
"html-webpack-plugin": "^5.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";
import { useState, useRef } from "react";
import GIF from "gif.js.optimized";
import { Tools } from "core/Misc/tools";
import type { Scene } from "core/scene";

interface IGifPaneProps {
scene: Scene;
}

export const GifPane = ({ scene }: IGifPaneProps) => {
const [gifOptions, setGifOptions] = useState({ width: 512, frequency: 200 });
const [crunchingGIF, setCrunchingGIF] = useState(false);
const gifRecorder = useRef<any>(null);
const gifWorkerBlob = useRef<Blob | null>(null);
const previousRenderingScale = useRef<number>(1);

const recordGIFInternal = () => {
const workerUrl = URL.createObjectURL(gifWorkerBlob.current!);
gifRecorder.current = new GIF({
workers: 2,
quality: 10,
workerScript: workerUrl,
});
const engine = scene.getEngine();

previousRenderingScale.current = engine.getHardwareScalingLevel();
engine.setHardwareScalingLevel(engine.getRenderWidth() / gifOptions.width || 1);

const intervalId = setInterval(() => {
if (!gifRecorder.current) {
clearInterval(intervalId);
return;
}
gifRecorder.current.addFrame(engine.getRenderingCanvas(), { delay: 0, copy: true });
}, gifOptions.frequency);

gifRecorder.current.on("finished", (blob: Blob) => {
setCrunchingGIF(false);
Tools.Download(blob, "record.gif");

URL.revokeObjectURL(workerUrl);
engine.setHardwareScalingLevel(previousRenderingScale.current);
});
};

const recordGIFAsync = async () => {
if (gifRecorder.current) {
setCrunchingGIF(true);
gifRecorder.current.render();
gifRecorder.current = null;
return;
}

if (gifWorkerBlob.current) {
recordGIFInternal();
return;
}

const workerJs = await Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/[email protected]/dist/gif.worker.js");

// Ensure assignment is always based on the latest ref value
if (!gifWorkerBlob.current) {
gifWorkerBlob.current = new Blob([workerJs], {
type: "application/javascript",
});
}
recordGIFInternal();
};

return (
<>
{crunchingGIF && <div>Creating the GIF file...</div>}
{!crunchingGIF && <ButtonLine label={gifRecorder.current ? "Stop" : "Record"} onClick={recordGIFAsync} />}
{!crunchingGIF && !gifRecorder.current && (
<>
<SyncedSliderLine label="Resolution" value={gifOptions.width} onChange={(value) => setGifOptions({ ...gifOptions, width: value })} min={1} step={1} />
<SyncedSliderLine
label="Frequency (ms)"
value={gifOptions.frequency}
onChange={(value) => setGifOptions({ ...gifOptions, frequency: value })}
min={1}
step={1}
/>
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import { IndentedTextLineComponent } from "shared-ui-components/lines/indentedTextLineComponent";
import { FileButtonLine } from "shared-ui-components/lines/fileButtonLineComponent";
import { useState, useRef, useCallback } from "react";
import { Tools } from "core/Misc/tools";
import type { Scene } from "core/scene";
import { SceneRecorder } from "core/Misc/sceneRecorder";

interface ICaptureReplayPropertiesProps {
scene: Scene;
}

export const CaptureReplayProperties = ({ scene }: ICaptureReplayPropertiesProps) => {
const [isRecording, setIsRecording] = useState(false);
const sceneRecorder = useRef(new SceneRecorder());

const startRecording = useCallback(async () => {
sceneRecorder.current.track(scene);
setIsRecording(true);
}, [scene]);

const exportReplay = useCallback(async () => {
const content = JSON.stringify(sceneRecorder.current.getDelta());
Tools.Download(new Blob([content]), "diff.json");
setIsRecording(false);
}, [sceneRecorder]);

const applyDelta = useCallback(
(file: File) => {
Tools.ReadFile(file, (data) => {
const json = JSON.parse(data as string);
SceneRecorder.ApplyDelta(json, scene);
setIsRecording(false);
});
},
[scene]
);

return (
<>
{!isRecording && <ButtonLine label="Start recording" onClick={startRecording} />}
{isRecording && <IndentedTextLineComponent value={"Record in progress"} />}
{isRecording && <ButtonLine label="Generate delta file" onClick={exportReplay} />}
<FileButtonLine label={`Apply delta file`} onClick={(file) => applyDelta(file)} accept=".json" />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import type { Scene } from "core/scene";
import { Tools } from "core/Misc/tools";
import { useCallback, useState } from "react";
import type { IScreenshotSize } from "core/Misc/interfaces/screenshotSize";
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/switchPropertyLine";
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";

interface ICaptureRttPropertiesProps {
scene: Scene;
}

export const CaptureRttProperties = ({ scene }: ICaptureRttPropertiesProps) => {
const [useWidthHeight, setUseWidthHeight] = useState(false);
const [screenshotSize, setScreenshotSize] = useState<IScreenshotSize>({ precision: 1 });

const captureRender = useCallback(async () => {
const sizeToUse: IScreenshotSize = { ...screenshotSize };
if (!useWidthHeight) {
sizeToUse.width = undefined;
sizeToUse.height = undefined;
}

if (scene.activeCamera) {
Tools.CreateScreenshotUsingRenderTarget(scene.getEngine(), scene.activeCamera, sizeToUse, undefined, undefined, 4);
}
}, [scene, screenshotSize, useWidthHeight]);

return (
<>
<ButtonLine label="Capture" onClick={captureRender} />
<SyncedSliderLine
label="Precision"
value={screenshotSize.precision ?? 1}
onChange={(value) => setScreenshotSize({ ...screenshotSize, precision: value ?? 1 })}
min={0.1}
max={10}
step={0.1}
/>
<SwitchPropertyLine label="Use Width/Height" value={useWidthHeight} onChange={(value) => setUseWidthHeight(value)} />
{useWidthHeight && (
<>
<SyncedSliderLine
label="Width"
value={screenshotSize.width ?? 512}
onChange={(data) => setScreenshotSize({ ...screenshotSize, width: data ?? 512 })}
min={1}
step={1}
/>
<SyncedSliderLine label="Height" value={screenshotSize.height ?? 512} onChange={(data) => setScreenshotSize({ ...screenshotSize, height: data ?? 512 })} />
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import type { Scene } from "core/scene";
import { Tools } from "core/Misc/tools";
import { VideoRecorder } from "core/Misc/videoRecorder";
import { captureEquirectangularFromScene } from "core/Misc/equirectangularCapture";
import { useCallback, useRef, useState } from "react";

interface ICaptureScreenshotPropertiesProps {
scene: Scene;
}

export const CaptureScreenshotProperties = ({ scene }: ICaptureScreenshotPropertiesProps) => {
const [recordVideoText, setRecordVideoText] = useState("Record video");
const videoRecorder = useRef<VideoRecorder | null>(null);

const captureScreenshot = useCallback(() => {
if (scene.activeCamera) {
Tools.CreateScreenshot(scene.getEngine(), scene.activeCamera, { precision: 1 });
}
}, [scene]);

const captureEquirectangularAsync = useCallback(async () => {
if (scene.activeCamera) {
await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
}
}, [scene]);

const recordVideoAsync = useCallback(async () => {
if (videoRecorder.current && videoRecorder.current.isRecording) {
void videoRecorder.current.stopRecording();
return;
}

if (!videoRecorder.current) {
videoRecorder.current = new VideoRecorder(scene.getEngine());
}

await videoRecorder.current.startRecording();
setRecordVideoText("Stop recording");
}, [scene]);

return (
<>
<ButtonLine label="Screenshot" onClick={captureScreenshot} />
<ButtonLine label="Generate equirectangular capture" onClick={captureEquirectangularAsync} />
<ButtonLine label={recordVideoText} onClick={recordVideoAsync} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* eslint-disable import/no-internal-modules */
import * as React from "react";
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/switchPropertyLine";
import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent";
import { SceneSerializer } from "core/Misc/sceneSerializer";
import { Tools } from "core/Misc/tools";
import { EnvironmentTextureTools } from "core/Misc/environmentTextureTools";
import type { CubeTexture } from "core/Materials/Textures/cubeTexture";
import { Logger } from "core/Misc/logger";
import { useCallback } from "react";
import type { Scene } from "core/scene";
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";

const EnvExportImageTypes = [
{ label: "PNG", value: 0, imageType: "image/png" },
{ label: "WebP", value: 1, imageType: "image/webp" },
];

interface IBabylonExportOptionsState {
imageTypeIndex: number;
imageQuality: number;
iblDiffuse: boolean;
}

interface IExportBabylonPropertiesProps {
scene: Scene;
}

export const ExportBabylonProperties = ({ scene }: IExportBabylonPropertiesProps) => {
const [babylonExportOptions, setBabylonExportOptions] = React.useState<IBabylonExportOptionsState>({
imageTypeIndex: 0,
imageQuality: 0.8,
iblDiffuse: false,
});

const exportBabylon = useCallback(async () => {
const strScene = JSON.stringify(SceneSerializer.Serialize(scene));
const blob = new Blob([strScene], { type: "octet/stream" });
Tools.Download(blob, "scene.babylon");
}, [scene]);

const createEnvTexture = useCallback(async () => {
if (!scene.environmentTexture) {
return;
}

try {
const buffer = await EnvironmentTextureTools.CreateEnvTextureAsync(scene.environmentTexture as CubeTexture, {
imageType: EnvExportImageTypes[babylonExportOptions.imageTypeIndex].imageType,
imageQuality: babylonExportOptions.imageQuality,
disableIrradianceTexture: !babylonExportOptions.iblDiffuse,
});
const blob = new Blob([buffer], { type: "octet/stream" });
Tools.Download(blob, "environment.env");
} catch (error: any) {
Logger.Error(error);
alert(error);
}
}, [scene, babylonExportOptions]);

return (
<>
<ButtonLine label="Export to Babylon" onClick={exportBabylon} />
{!scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera && (
<>
<ButtonLine label="Generate .env texture" onClick={createEnvTexture} />
{scene.environmentTexture.irradianceTexture && (
<SwitchPropertyLine
key="iblDiffuse"
label="Diffuse Texture"
description="Export diffuse texture for IBL"
value={babylonExportOptions.iblDiffuse}
onChange={(value) => {
setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value }));
}}
/>
)}
{/* <OptionsLine
label="Image type"
options={EnvExportImageTypes}
target={babylonExportOptions}
propertyName="imageTypeIndex"
onSelect={(val) => {
setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val as number }));
}}
/> */}
{babylonExportOptions.imageTypeIndex > 0 && (
<SyncedSliderLine
label="Quality"
value={babylonExportOptions.imageQuality}
onChange={(value) => setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value }))}
min={0}
max={1}
/>
)}
</>
)}
</>
);
};
Loading
Loading