Skip to content

Commit a834b39

Browse files
committed
Tools pane
1 parent 9802c18 commit a834b39

20 files changed

+927
-30
lines changed

package-lock.json

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dev/inspector-v2/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
"devDependencies": {
1919
"@dev/core": "1.0.0",
2020
"@dev/loaders": "1.0.0",
21+
"@dev/serializers": "1.0.0",
2122
"@fluentui/react-components": "^9.62.0",
2223
"@fluentui/react-icons": "^2.0.271",
2324
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
25+
"gif.js.optimized": "^1.0.1",
2426
"html-webpack-plugin": "^5.5.0",
2527
"react": "^18.2.0",
2628
"react-dom": "^18.2.0",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
2+
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";
3+
import { useState, useRef } from "react";
4+
import GIF from "gif.js.optimized";
5+
import { Tools } from "core/Misc/tools";
6+
import type { Scene } from "core/scene";
7+
8+
interface IGifPaneProps {
9+
scene: Scene;
10+
}
11+
12+
export const GifPane = ({ scene }: IGifPaneProps) => {
13+
const [gifOptions, setGifOptions] = useState({ width: 512, frequency: 200 });
14+
const [crunchingGIF, setCrunchingGIF] = useState(false);
15+
const gifRecorder = useRef<any>(null);
16+
const gifWorkerBlob = useRef<Blob | null>(null);
17+
const previousRenderingScale = useRef<number>(1);
18+
19+
const recordGIFInternal = () => {
20+
const workerUrl = URL.createObjectURL(gifWorkerBlob.current!);
21+
gifRecorder.current = new GIF({
22+
workers: 2,
23+
quality: 10,
24+
workerScript: workerUrl,
25+
});
26+
const engine = scene.getEngine();
27+
28+
previousRenderingScale.current = engine.getHardwareScalingLevel();
29+
engine.setHardwareScalingLevel(engine.getRenderWidth() / gifOptions.width || 1);
30+
31+
const intervalId = setInterval(() => {
32+
if (!gifRecorder.current) {
33+
clearInterval(intervalId);
34+
return;
35+
}
36+
gifRecorder.current.addFrame(engine.getRenderingCanvas(), { delay: 0, copy: true });
37+
}, gifOptions.frequency);
38+
39+
gifRecorder.current.on("finished", (blob: Blob) => {
40+
setCrunchingGIF(false);
41+
Tools.Download(blob, "record.gif");
42+
43+
URL.revokeObjectURL(workerUrl);
44+
engine.setHardwareScalingLevel(previousRenderingScale.current);
45+
});
46+
};
47+
48+
const recordGIFAsync = async () => {
49+
if (gifRecorder.current) {
50+
setCrunchingGIF(true);
51+
gifRecorder.current.render();
52+
gifRecorder.current = null;
53+
return;
54+
}
55+
56+
if (gifWorkerBlob.current) {
57+
recordGIFInternal();
58+
return;
59+
}
60+
61+
const workerJs = await Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/[email protected]/dist/gif.worker.js");
62+
63+
// Ensure assignment is always based on the latest ref value
64+
if (!gifWorkerBlob.current) {
65+
gifWorkerBlob.current = new Blob([workerJs], {
66+
type: "application/javascript",
67+
});
68+
}
69+
recordGIFInternal();
70+
};
71+
72+
return (
73+
<>
74+
{crunchingGIF && <div>Creating the GIF file...</div>}
75+
{!crunchingGIF && <ButtonLine label={gifRecorder.current ? "Stop" : "Record"} onClick={recordGIFAsync} />}
76+
{!crunchingGIF && !gifRecorder.current && (
77+
<>
78+
<SyncedSliderLine label="Resolution" value={gifOptions.width} onChange={(value) => setGifOptions({ ...gifOptions, width: value })} min={1} step={1} />
79+
<SyncedSliderLine
80+
label="Frequency (ms)"
81+
value={gifOptions.frequency}
82+
onChange={(value) => setGifOptions({ ...gifOptions, frequency: value })}
83+
min={1}
84+
step={1}
85+
/>
86+
</>
87+
)}
88+
</>
89+
);
90+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
2+
import { IndentedTextLineComponent } from "shared-ui-components/lines/indentedTextLineComponent";
3+
import { FileButtonLine } from "shared-ui-components/lines/fileButtonLineComponent";
4+
import { useState, useRef, useCallback } from "react";
5+
import { Tools } from "core/Misc/tools";
6+
import type { Scene } from "core/scene";
7+
import { SceneRecorder } from "core/Misc/sceneRecorder";
8+
9+
interface ICaptureReplayPropertiesProps {
10+
scene: Scene;
11+
}
12+
13+
export const CaptureReplayProperties = ({ scene }: ICaptureReplayPropertiesProps) => {
14+
const [isRecording, setIsRecording] = useState(false);
15+
const sceneRecorder = useRef(new SceneRecorder());
16+
17+
const startRecording = useCallback(async () => {
18+
sceneRecorder.current.track(scene);
19+
setIsRecording(true);
20+
}, [scene]);
21+
22+
const exportReplay = useCallback(async () => {
23+
const content = JSON.stringify(sceneRecorder.current.getDelta());
24+
Tools.Download(new Blob([content]), "diff.json");
25+
setIsRecording(false);
26+
}, [sceneRecorder]);
27+
28+
const applyDelta = useCallback(
29+
(file: File) => {
30+
Tools.ReadFile(file, (data) => {
31+
const json = JSON.parse(data as string);
32+
SceneRecorder.ApplyDelta(json, scene);
33+
setIsRecording(false);
34+
});
35+
},
36+
[scene]
37+
);
38+
39+
return (
40+
<>
41+
{!isRecording && <ButtonLine label="Start recording" onClick={startRecording} />}
42+
{isRecording && <IndentedTextLineComponent value={"Record in progress"} />}
43+
{isRecording && <ButtonLine label="Generate delta file" onClick={exportReplay} />}
44+
<FileButtonLine label={`Apply delta file`} onClick={(file) => applyDelta(file)} accept=".json" />
45+
</>
46+
);
47+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
2+
import type { Scene } from "core/scene";
3+
import { Tools } from "core/Misc/tools";
4+
import { useCallback, useState } from "react";
5+
import type { IScreenshotSize } from "core/Misc/interfaces/screenshotSize";
6+
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/switchPropertyLine";
7+
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";
8+
9+
interface ICaptureRttPropertiesProps {
10+
scene: Scene;
11+
}
12+
13+
export const CaptureRttProperties = ({ scene }: ICaptureRttPropertiesProps) => {
14+
const [useWidthHeight, setUseWidthHeight] = useState(false);
15+
const [screenshotSize, setScreenshotSize] = useState<IScreenshotSize>({ precision: 1 });
16+
17+
const captureRender = useCallback(async () => {
18+
const sizeToUse: IScreenshotSize = { ...screenshotSize };
19+
if (!useWidthHeight) {
20+
sizeToUse.width = undefined;
21+
sizeToUse.height = undefined;
22+
}
23+
24+
if (scene.activeCamera) {
25+
Tools.CreateScreenshotUsingRenderTarget(scene.getEngine(), scene.activeCamera, sizeToUse, undefined, undefined, 4);
26+
}
27+
}, [scene, screenshotSize, useWidthHeight]);
28+
29+
return (
30+
<>
31+
<ButtonLine label="Capture" onClick={captureRender} />
32+
<SyncedSliderLine
33+
label="Precision"
34+
value={screenshotSize.precision ?? 1}
35+
onChange={(value) => setScreenshotSize({ ...screenshotSize, precision: value ?? 1 })}
36+
min={0.1}
37+
max={10}
38+
step={0.1}
39+
/>
40+
<SwitchPropertyLine label="Use Width/Height" value={useWidthHeight} onChange={(value) => setUseWidthHeight(value)} />
41+
{useWidthHeight && (
42+
<>
43+
<SyncedSliderLine
44+
label="Width"
45+
value={screenshotSize.width ?? 512}
46+
onChange={(data) => setScreenshotSize({ ...screenshotSize, width: data ?? 512 })}
47+
min={1}
48+
step={1}
49+
/>
50+
<SyncedSliderLine label="Height" value={screenshotSize.height ?? 512} onChange={(data) => setScreenshotSize({ ...screenshotSize, height: data ?? 512 })} />
51+
</>
52+
)}
53+
</>
54+
);
55+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
2+
import type { Scene } from "core/scene";
3+
import { Tools } from "core/Misc/tools";
4+
import { VideoRecorder } from "core/Misc/videoRecorder";
5+
import { captureEquirectangularFromScene } from "core/Misc/equirectangularCapture";
6+
import { useCallback, useRef, useState } from "react";
7+
8+
interface ICaptureScreenshotPropertiesProps {
9+
scene: Scene;
10+
}
11+
12+
export const CaptureScreenshotProperties = ({ scene }: ICaptureScreenshotPropertiesProps) => {
13+
const [recordVideoText, setRecordVideoText] = useState("Record video");
14+
const videoRecorder = useRef<VideoRecorder | null>(null);
15+
16+
const captureScreenshot = useCallback(() => {
17+
if (scene.activeCamera) {
18+
Tools.CreateScreenshot(scene.getEngine(), scene.activeCamera, { precision: 1 });
19+
}
20+
}, [scene]);
21+
22+
const captureEquirectangularAsync = useCallback(async () => {
23+
if (scene.activeCamera) {
24+
await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
25+
}
26+
}, [scene]);
27+
28+
const recordVideoAsync = useCallback(async () => {
29+
if (videoRecorder.current && videoRecorder.current.isRecording) {
30+
void videoRecorder.current.stopRecording();
31+
return;
32+
}
33+
34+
if (!videoRecorder.current) {
35+
videoRecorder.current = new VideoRecorder(scene.getEngine());
36+
}
37+
38+
await videoRecorder.current.startRecording();
39+
setRecordVideoText("Stop recording");
40+
}, [scene]);
41+
42+
return (
43+
<>
44+
<ButtonLine label="Screenshot" onClick={captureScreenshot} />
45+
<ButtonLine label="Generate equirectangular capture" onClick={captureEquirectangularAsync} />
46+
<ButtonLine label={recordVideoText} onClick={recordVideoAsync} />
47+
</>
48+
);
49+
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* eslint-disable import/no-internal-modules */
2+
import * as React from "react";
3+
import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/switchPropertyLine";
4+
import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent";
5+
import { SceneSerializer } from "core/Misc/sceneSerializer";
6+
import { Tools } from "core/Misc/tools";
7+
import { EnvironmentTextureTools } from "core/Misc/environmentTextureTools";
8+
import type { CubeTexture } from "core/Materials/Textures/cubeTexture";
9+
import { Logger } from "core/Misc/logger";
10+
import { useCallback } from "react";
11+
import type { Scene } from "core/scene";
12+
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
13+
import { SyncedSliderLine } from "shared-ui-components/fluent/hoc/syncedSliderLine";
14+
15+
const EnvExportImageTypes = [
16+
{ label: "PNG", value: 0, imageType: "image/png" },
17+
{ label: "WebP", value: 1, imageType: "image/webp" },
18+
];
19+
20+
interface IBabylonExportOptionsState {
21+
imageTypeIndex: number;
22+
imageQuality: number;
23+
iblDiffuse: boolean;
24+
}
25+
26+
interface IExportBabylonPropertiesProps {
27+
scene: Scene;
28+
}
29+
30+
export const ExportBabylonProperties = ({ scene }: IExportBabylonPropertiesProps) => {
31+
const [babylonExportOptions, setBabylonExportOptions] = React.useState<IBabylonExportOptionsState>({
32+
imageTypeIndex: 0,
33+
imageQuality: 0.8,
34+
iblDiffuse: false,
35+
});
36+
37+
const exportBabylon = useCallback(async () => {
38+
const strScene = JSON.stringify(SceneSerializer.Serialize(scene));
39+
const blob = new Blob([strScene], { type: "octet/stream" });
40+
Tools.Download(blob, "scene.babylon");
41+
}, [scene]);
42+
43+
const createEnvTexture = useCallback(async () => {
44+
if (!scene.environmentTexture) {
45+
return;
46+
}
47+
48+
try {
49+
const buffer = await EnvironmentTextureTools.CreateEnvTextureAsync(scene.environmentTexture as CubeTexture, {
50+
imageType: EnvExportImageTypes[babylonExportOptions.imageTypeIndex].imageType,
51+
imageQuality: babylonExportOptions.imageQuality,
52+
disableIrradianceTexture: !babylonExportOptions.iblDiffuse,
53+
});
54+
const blob = new Blob([buffer], { type: "octet/stream" });
55+
Tools.Download(blob, "environment.env");
56+
} catch (error: any) {
57+
Logger.Error(error);
58+
alert(error);
59+
}
60+
}, [scene, babylonExportOptions]);
61+
62+
return (
63+
<>
64+
<ButtonLine label="Export to Babylon" onClick={exportBabylon} />
65+
{!scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera && (
66+
<>
67+
<ButtonLine label="Generate .env texture" onClick={createEnvTexture} />
68+
{scene.environmentTexture.irradianceTexture && (
69+
<SwitchPropertyLine
70+
key="iblDiffuse"
71+
label="Diffuse Texture"
72+
description="Export diffuse texture for IBL"
73+
value={babylonExportOptions.iblDiffuse}
74+
onChange={(value) => {
75+
setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value }));
76+
}}
77+
/>
78+
)}
79+
{/* <OptionsLine
80+
label="Image type"
81+
options={EnvExportImageTypes}
82+
target={babylonExportOptions}
83+
propertyName="imageTypeIndex"
84+
onSelect={(val) => {
85+
setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val as number }));
86+
}}
87+
/> */}
88+
{babylonExportOptions.imageTypeIndex > 0 && (
89+
<SyncedSliderLine
90+
label="Quality"
91+
value={babylonExportOptions.imageQuality}
92+
onChange={(value) => setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value }))}
93+
min={0}
94+
max={1}
95+
/>
96+
)}
97+
</>
98+
)}
99+
</>
100+
);
101+
};

0 commit comments

Comments
 (0)