From f04118b8660c7b0f50cc1f1068affd116c714ac7 Mon Sep 17 00:00:00 2001 From: "Alex C. Huber" <91097647+alexchuber@users.noreply.github.com> Date: Mon, 4 Aug 2025 19:05:30 -0400 Subject: [PATCH 1/4] Init --- packages/dev/inspector-v2/package.json | 1 + .../src/components/tools/captureTools.tsx | 97 ++++++++ .../src/components/tools/exportTools.tsx | 219 ++++++++++++++++++ .../src/components/tools/importTools.tsx | 80 +++++++ .../src/components/tools/toolsPane.tsx | 27 +++ .../extensibility/builtInsExtensionFeed.ts | 26 ++- .../services/panes/tools/captureService.tsx | 41 ++++ .../services/panes/tools/exportService.tsx | 41 ++++ .../services/panes/tools/importService.tsx | 26 +++ .../src/services/panes/toolsService.tsx | 66 +++++- packages/dev/inspector-v2/tsconfig.build.json | 3 + packages/dev/inspector-v2/webpack.config.js | 1 + 12 files changed, 617 insertions(+), 11 deletions(-) create mode 100644 packages/dev/inspector-v2/src/components/tools/captureTools.tsx create mode 100644 packages/dev/inspector-v2/src/components/tools/exportTools.tsx create mode 100644 packages/dev/inspector-v2/src/components/tools/importTools.tsx create mode 100644 packages/dev/inspector-v2/src/components/tools/toolsPane.tsx create mode 100644 packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx create mode 100644 packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx create mode 100644 packages/dev/inspector-v2/src/services/panes/tools/importService.tsx diff --git a/packages/dev/inspector-v2/package.json b/packages/dev/inspector-v2/package.json index b214e423544..d63b9c1ccaf 100644 --- a/packages/dev/inspector-v2/package.json +++ b/packages/dev/inspector-v2/package.json @@ -21,6 +21,7 @@ "@dev/gui": "^1.0.0", "@dev/loaders": "1.0.0", "@dev/materials": "^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", diff --git a/packages/dev/inspector-v2/src/components/tools/captureTools.tsx b/packages/dev/inspector-v2/src/components/tools/captureTools.tsx new file mode 100644 index 00000000000..00e82894294 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/tools/captureTools.tsx @@ -0,0 +1,97 @@ +import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine"; +import { useState, useRef, useCallback } from "react"; +import type { FunctionComponent } from "react"; +import { Tools } from "core/Misc/tools"; +import type { Scene } from "core/scene"; +import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine"; +import type { IScreenshotSize } from "core/Misc/interfaces/screenshotSize"; +import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; +import { VideoRecorder } from "core/Misc/videoRecorder"; +import { captureEquirectangularFromScene } from "core/Misc/equirectangularCapture"; + +export const CaptureRttProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [useWidthHeight, setUseWidthHeight] = useState(false); + const [screenshotSize, setScreenshotSize] = useState({ 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 ( + <> + + setScreenshotSize({ ...screenshotSize, precision: value ?? 1 })} + min={0.1} + max={10} + step={0.1} + /> + setUseWidthHeight(value)} /> + {useWidthHeight && ( + <> + setScreenshotSize({ ...screenshotSize, width: data ?? 512 })} + min={1} + step={1} + /> + setScreenshotSize({ ...screenshotSize, height: data ?? 512 })} + /> + + )} + + ); +}; + +export const CaptureScreenshotProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [recordVideoText, setRecordVideoText] = useState("Record video"); + const videoRecorder = useRef(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 ( + <> + + + + + ); +}; diff --git a/packages/dev/inspector-v2/src/components/tools/exportTools.tsx b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx new file mode 100644 index 00000000000..7e71defc441 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx @@ -0,0 +1,219 @@ +/* eslint-disable import/no-internal-modules */ +import * as React from "react"; +import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; +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, useState } from "react"; +import type { FunctionComponent } from "react"; +import type { Scene } from "core/scene"; +import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine"; +import { SyncedSliderPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/syncedSliderPropertyLine"; +import type { Node } from "core/node"; +import { Mesh } from "core/Meshes/mesh"; +import type { PBRMaterial } from "core/Materials/PBR/pbrMaterial"; +import type { StandardMaterial } from "core/Materials/standardMaterial"; +import type { BackgroundMaterial } from "core/Materials/Background/backgroundMaterial"; +import { Texture } from "core/Materials/Textures/texture"; +import { Camera } from "core/Cameras/camera"; +import { Light } from "core/Lights/light"; +import { Text } from "@fluentui/react-components"; +import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; + +// NOTE: Only importing types from serializers package, which is a dev dependency +import type { GLTF2Export } from "serializers/glTF/2.0/glTFSerializer"; +import { MakeLazyComponent } from "shared-ui-components/fluent/primitives/lazyComponent"; + +const EnvExportImageTypes = [ + { label: "PNG", value: 0, imageType: "image/png" }, + { label: "WebP", value: 1, imageType: "image/webp" }, +]; + +interface IBabylonExportOptionsState { + imageTypeIndex: number; + imageQuality: number; + iblDiffuse: boolean; +} + +export const ExportBabylonProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [babylonExportOptions, setBabylonExportOptions] = React.useState({ + 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 ( + <> + + {!scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera && ( + <> + + {scene.environmentTexture.irradianceTexture && ( + { + setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value })); + }} + /> + )} + { + setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val as number })); + }} + /> + {babylonExportOptions.imageTypeIndex > 0 && ( + setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value }))} + min={0} + max={1} + /> + )} + + )} + + ); +}; + +interface IGltfExportOptionsState { + exportDisabledNodes: boolean; + exportSkyboxes: boolean; + exportCameras: boolean; + exportLights: boolean; +} + +export const ExportGltfProperties = MakeLazyComponent(async () => { + // Defer importing anything from the gui package until this component is actually mounted. + // eslint-disable-next-line @typescript-eslint/naming-convention + const { GLTF2Export } = await import("serializers/glTF/2.0/glTFSerializer"); + + return (props: { scene: Scene }) => { + const [isExportingGltf, setIsExportingGltf] = useState(false); + const [gltfExportOptions, setGltfExportOptions] = useState({ + exportDisabledNodes: false, + exportSkyboxes: false, + exportCameras: false, + exportLights: false, + }); + + const exportGLTF = useCallback(async () => { + setIsExportingGltf(true); + + const shouldExport = (node: Node): boolean => { + if (!gltfExportOptions.exportDisabledNodes) { + if (!node.isEnabled()) { + return false; + } + } + + if (!gltfExportOptions.exportSkyboxes) { + if (node instanceof Mesh) { + if (node.material) { + const material = node.material as PBRMaterial | StandardMaterial | BackgroundMaterial; + const reflectionTexture = material.reflectionTexture; + if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) { + return false; + } + } + } + } + + if (!gltfExportOptions.exportCameras) { + if (node instanceof Camera) { + return false; + } + } + + if (!gltfExportOptions.exportLights) { + if (node instanceof Light) { + return false; + } + } + + return true; + }; + + try { + const glb = await GLTF2Export.GLBAsync(props.scene, "scene", { shouldExportNode: (node) => shouldExport(node) }); + glb.downloadFiles(); + } catch (reason) { + Logger.Error(`Failed to export GLB: ${reason}`); + } finally { + setIsExportingGltf(false); + } + }, [gltfExportOptions, props.scene]); + + return ( + <> + {isExportingGltf && } + {!isExportingGltf && ( + <> + setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportLights: checked })} + /> + + )} + {!isExportingGltf && } + + ); + }; +}); diff --git a/packages/dev/inspector-v2/src/components/tools/importTools.tsx b/packages/dev/inspector-v2/src/components/tools/importTools.tsx new file mode 100644 index 00000000000..a23f6094f97 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/tools/importTools.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; +import type { FunctionComponent } from "react"; +import type { Scene } from "core/scene"; +import { ImportAnimationsAsync, SceneLoaderAnimationGroupLoadingMode } from "core/Loading/sceneLoader"; +import { FilesInput } from "core/Misc/filesInput"; +import { Logger } from "core/Misc"; +import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; +import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; +import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine"; + +const AnimationGroupLoadingModes = [ + { label: "Clean", value: SceneLoaderAnimationGroupLoadingMode.Clean }, + { label: "Stop", value: SceneLoaderAnimationGroupLoadingMode.Stop }, + { label: "Sync", value: SceneLoaderAnimationGroupLoadingMode.Sync }, + { label: "NoSync", value: SceneLoaderAnimationGroupLoadingMode.NoSync }, +]; + +export const ImportAnimationsProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [importDefaults, setImportDefaults] = useState({ + overwriteAnimations: true, + animationGroupLoadingMode: SceneLoaderAnimationGroupLoadingMode.Clean, + }); + + const importAnimations = (event: any) => { + const reloadAsync = async function (sceneFile: File) { + if (sceneFile) { + try { + await ImportAnimationsAsync(sceneFile, scene, { + overwriteAnimations: importDefaults.overwriteAnimations, + animationGroupLoadingMode: importDefaults.animationGroupLoadingMode, + }); + + if (scene.animationGroups.length > 0) { + const currentGroup = scene.animationGroups[0]; + currentGroup.play(true); + } + } catch (error) { + Logger.Error(`Error importing animations: ${error}`); + } + } + }; + + const filesInputAnimation = new FilesInput( + scene.getEngine() as any, + scene as any, + () => {}, + () => {}, + () => {}, + () => {}, + () => {}, + reloadAsync, + () => {} + ); + + filesInputAnimation.loadFiles(event); + }; + + return ( + <> + importAnimations(evt)} /> + { + setImportDefaults({ ...importDefaults, overwriteAnimations: value }); + }} + /> + {importDefaults.overwriteAnimations === false && ( + { + setImportDefaults({ ...importDefaults, animationGroupLoadingMode: value }); + }} + /> + )} + + ); +}; diff --git a/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx b/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx new file mode 100644 index 00000000000..515e14c2add --- /dev/null +++ b/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx @@ -0,0 +1,27 @@ +// eslint-disable-next-line import/no-internal-modules + +import { Body1Strong, makeStyles, tokens } from "@fluentui/react-components"; + +import { ExtensibleAccordion } from "../extensibleAccordion"; +import type { Scene } from "core/scene"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const useStyles = makeStyles({ + placeholderDiv: { + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`, + }, +}); + +export const ToolsPane: typeof ExtensibleAccordion = (props) => { + const classes = useStyles(); + + const entity = props.context; + + return entity != null ? ( + + ) : ( +
+ No entity selected. +
+ ); +}; diff --git a/packages/dev/inspector-v2/src/extensibility/builtInsExtensionFeed.ts b/packages/dev/inspector-v2/src/extensibility/builtInsExtensionFeed.ts index 0d511f4fa2a..a783a7a1f4d 100644 --- a/packages/dev/inspector-v2/src/extensibility/builtInsExtensionFeed.ts +++ b/packages/dev/inspector-v2/src/extensibility/builtInsExtensionFeed.ts @@ -10,7 +10,25 @@ const CreationToolsExtensionMetadata = { keywords: ["creation"], } as const; -const Extensions: readonly ExtensionMetadata[] = [CreationToolsExtensionMetadata]; +const ExportToolsExtensionMetadata = { + name: "Export Tools", + description: "Adds new features to enable exporting Babylon assets such as .gltf, .glb, .babylon, and more.", + keywords: ["export", "gltf", "glb", "babylon", "exporter", "tools"], +} as const; + +const CaptureToolsExtensionMetadata = { + name: "Capture Tools", + description: "Adds new features to enable capturing screenshots, GIFs, videos, and more.", + keywords: ["capture", "screenshot", "gif", "video", "tools"], +} as const; + +const ImportToolsExtensionMetadata = { + name: "Import Tools", + description: "Adds new features related to importing Babylon assets.", + keywords: ["import", "tools"], +} as const; + +const Extensions: readonly ExtensionMetadata[] = [CreationToolsExtensionMetadata, ExportToolsExtensionMetadata, CaptureToolsExtensionMetadata, ImportToolsExtensionMetadata]; /** * @internal @@ -31,6 +49,12 @@ export class BuiltInsExtensionFeed implements IExtensionFeed { public async getExtensionModuleAsync(name: string): Promise { if (name === CreationToolsExtensionMetadata.name) { return await import("../services/creationToolsService"); + } else if (name === ExportToolsExtensionMetadata.name) { + return await import("../services/panes/tools/exportService"); + } else if (name === CaptureToolsExtensionMetadata.name) { + return await import("../services/panes/tools/captureService"); + } else if (name === ImportToolsExtensionMetadata.name) { + return await import("../services/panes/tools/importService"); } return undefined; } diff --git a/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx new file mode 100644 index 00000000000..84d13656ee4 --- /dev/null +++ b/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx @@ -0,0 +1,41 @@ +import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; +import { ToolsServiceIdentity } from "../toolsService"; +import type { IToolsService } from "../toolsService"; +import type { IDisposable } from "core/scene"; +import { CaptureRttProperties, CaptureScreenshotProperties } from "../../../components/tools/captureTools"; + +export const CaptureServiceDefinition: ServiceDefinition<[], [IToolsService]> = { + friendlyName: "Capture Tools", + consumes: [ToolsServiceIdentity], + factory: (toolsService) => { + const contentRegistrations: IDisposable[] = []; + + // Screenshot capture content + contentRegistrations.push( + toolsService.addSectionContent({ + key: "Screenshot Capture", + section: "Screenshot Capture", + component: ({ context }) => , + }) + ); + + // RTT capture content + contentRegistrations.push( + toolsService.addSectionContent({ + key: "RTT Capture", + section: "RTT Capture", + component: ({ context }) => , + }) + ); + + return { + dispose: () => { + contentRegistrations.forEach((registration) => registration.dispose()); + }, + }; + }, +}; + +export default { + serviceDefinitions: [CaptureServiceDefinition], +} as const; diff --git a/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx new file mode 100644 index 00000000000..a955fd6bcd7 --- /dev/null +++ b/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx @@ -0,0 +1,41 @@ +import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; +import { ToolsServiceIdentity } from "../toolsService"; +import type { IToolsService } from "../toolsService"; +import type { IDisposable } from "core/scene"; +import { ExportBabylonProperties, ExportGltfProperties } from "../../../components/tools/exportTools"; + +export const ExportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { + friendlyName: "Export Tools", + consumes: [ToolsServiceIdentity], + factory: (toolsService) => { + const contentRegistrations: IDisposable[] = []; + + // glTF export content + contentRegistrations.push( + toolsService.addSectionContent({ + key: "glTF Export", + section: "glTF Export", + component: ({ context }) => , + }) + ); + + // Babylon export content + contentRegistrations.push( + toolsService.addSectionContent({ + key: "Babylon Export", + section: "Babylon Export", + component: ({ context }) => , + }) + ); + + return { + dispose: () => { + contentRegistrations.forEach((registration) => registration.dispose()); + }, + }; + }, +}; + +export default { + serviceDefinitions: [ExportServiceDefinition], +} as const; diff --git a/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx new file mode 100644 index 00000000000..8bb8da435f3 --- /dev/null +++ b/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx @@ -0,0 +1,26 @@ +import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; +import { ToolsServiceIdentity } from "../toolsService"; +import type { IToolsService } from "../toolsService"; +import { ImportAnimationsProperties } from "../../../components/tools/importTools"; + +export const SceneImportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { + friendlyName: "Import Tool", + consumes: [ToolsServiceIdentity], + factory: (toolsService) => { + const contentRegistration = toolsService.addSectionContent({ + key: "AnimationImport", + section: "Animation Import", + component: ({ context }) => , + }); + + return { + dispose: () => { + contentRegistration.dispose(); + }, + }; + }, +}; + +export default { + serviceDefinitions: [SceneImportServiceDefinition], +} as const; diff --git a/packages/dev/inspector-v2/src/services/panes/toolsService.tsx b/packages/dev/inspector-v2/src/services/panes/toolsService.tsx index 20b131da2c9..3eff645bad3 100644 --- a/packages/dev/inspector-v2/src/services/panes/toolsService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/toolsService.tsx @@ -1,15 +1,47 @@ -import type { ServiceDefinition } from "../../modularity/serviceDefinition"; +import type { IDisposable, Scene } from "core/scene"; +import type { IService, ServiceDefinition } from "../../modularity/serviceDefinition"; import type { IShellService } from "../shellService"; - +import type { DynamicAccordionSection, DynamicAccordionSectionContent } from "../../components/extensibleAccordion"; import { WrenchRegular } from "@fluentui/react-icons"; - +import { useObservableCollection, useObservableState, useOrderedObservableCollection } from "../../hooks/observableHooks"; +import { ObservableCollection } from "../../misc/observableCollection"; import { ShellServiceIdentity } from "../shellService"; +import { ToolsPane } from "../../components/tools/toolsPane"; +import { SceneContextIdentity } from "../sceneContext"; +import type { ISceneContext } from "../sceneContext"; + +export const ToolsServiceIdentity = Symbol("ToolsService"); + +/** + * A service that provides tools for the user to generate artifacts or perform actions on entities. + */ +export interface IToolsService extends IService { + /** + * Adds a new section (e.g. "Export", "Capture", etc.). + * @param section A description of the section to add. + */ + addSection(section: DynamicAccordionSection): IDisposable; -export const ToolsServiceDefinition: ServiceDefinition<[], [IShellService]> = { - friendlyName: "Tools", - consumes: [ShellServiceIdentity], - factory: (shellService) => { - const registration = shellService.addSidePane({ + /** + * Adds content to one or more sections. + * @param content A description of the content to add. + */ + addSectionContent(content: DynamicAccordionSectionContent): IDisposable; +} + +/** + * A collection of usually optional, dynamic extensions. + * Common examples includes importing/exporting, or other general creation tools. + */ +export const ToolsServiceDefinition: ServiceDefinition<[IToolsService], [IShellService, ISceneContext]> = { + friendlyName: "Tools Editor", + produces: [ToolsServiceIdentity], + consumes: [ShellServiceIdentity, SceneContextIdentity], + factory: (shellService, sceneContext) => { + const sectionsCollection = new ObservableCollection(); + const sectionContentCollection = new ObservableCollection>(); + + const toolsPaneRegistration = shellService.addSidePane({ key: "Tools", title: "Tools", icon: WrenchRegular, @@ -17,12 +49,26 @@ export const ToolsServiceDefinition: ServiceDefinition<[], [IShellService]> = { order: 400, suppressTeachingMoment: true, content: () => { - return <>Not yet implemented.; + const sections = useOrderedObservableCollection(sectionsCollection); + const sectionContent = useObservableCollection(sectionContentCollection); + const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable); + + return scene && ; }, }); + /** + * Left TODO: Implement the following sections from toolsTabComponent.tsx + * - GLTF Validator (see glTFComponent.tsx) (consider putting in Import tools) + * - Reflector + * - GIF (consider putting in Capture Tools) + * - Replay (consider putting in Capture Tools) + */ + return { - dispose: () => registration.dispose(), + addSection: (section) => sectionsCollection.add(section), + addSectionContent: (content) => sectionContentCollection.add(content), + dispose: () => toolsPaneRegistration.dispose(), }; }, }; diff --git a/packages/dev/inspector-v2/tsconfig.build.json b/packages/dev/inspector-v2/tsconfig.build.json index cf829f5ae0f..6ac2b9f3a38 100644 --- a/packages/dev/inspector-v2/tsconfig.build.json +++ b/packages/dev/inspector-v2/tsconfig.build.json @@ -21,6 +21,9 @@ }, { "path": "../materials/tsconfig.build.json" + }, + { + "path": "../serializers/tsconfig.build.json" } ], diff --git a/packages/dev/inspector-v2/webpack.config.js b/packages/dev/inspector-v2/webpack.config.js index ccf08404f19..c9a82559860 100644 --- a/packages/dev/inspector-v2/webpack.config.js +++ b/packages/dev/inspector-v2/webpack.config.js @@ -29,6 +29,7 @@ module.exports = (env) => { materials: path.resolve("../../dev/materials/dist"), "shared-ui-components": path.resolve("../../dev/sharedUiComponents/src"), "inspector-v2": path.resolve("./src"), + serializers: path.resolve("../../dev/serializers/dist"), }, }, From f33445370dd04a4e323a8c7ca469cb5fbd37c853 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 7 Aug 2025 10:23:54 -0700 Subject: [PATCH 2/4] Dynamically register/unregister tools pane, depending on content availability --- .../src/components/tools/exportTools.tsx | 2 - .../src/services/panes/toolsService.tsx | 38 ++++++++++++------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/dev/inspector-v2/src/components/tools/exportTools.tsx b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx index 7e71defc441..ee47818dead 100644 --- a/packages/dev/inspector-v2/src/components/tools/exportTools.tsx +++ b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx @@ -22,8 +22,6 @@ import { Light } from "core/Lights/light"; import { Text } from "@fluentui/react-components"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; -// NOTE: Only importing types from serializers package, which is a dev dependency -import type { GLTF2Export } from "serializers/glTF/2.0/glTFSerializer"; import { MakeLazyComponent } from "shared-ui-components/fluent/primitives/lazyComponent"; const EnvExportImageTypes = [ diff --git a/packages/dev/inspector-v2/src/services/panes/toolsService.tsx b/packages/dev/inspector-v2/src/services/panes/toolsService.tsx index 3eff645bad3..22c10d5fbb4 100644 --- a/packages/dev/inspector-v2/src/services/panes/toolsService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/toolsService.tsx @@ -1,3 +1,4 @@ +import type { Nullable } from "core/types"; import type { IDisposable, Scene } from "core/scene"; import type { IService, ServiceDefinition } from "../../modularity/serviceDefinition"; import type { IShellService } from "../shellService"; @@ -41,20 +42,29 @@ export const ToolsServiceDefinition: ServiceDefinition<[IToolsService], [IShellS const sectionsCollection = new ObservableCollection(); const sectionContentCollection = new ObservableCollection>(); - const toolsPaneRegistration = shellService.addSidePane({ - key: "Tools", - title: "Tools", - icon: WrenchRegular, - horizontalLocation: "right", - order: 400, - suppressTeachingMoment: true, - content: () => { - const sections = useOrderedObservableCollection(sectionsCollection); - const sectionContent = useObservableCollection(sectionContentCollection); - const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable); + // Only show the Tools pane if some tool content has been added. + let toolsPaneRegistration: Nullable = null; + sectionContentCollection.observable.add(() => { + if (sectionContentCollection.items.length === 0) { + toolsPaneRegistration?.dispose(); + toolsPaneRegistration = null; + } else if (!toolsPaneRegistration) { + toolsPaneRegistration = shellService.addSidePane({ + key: "Tools", + title: "Tools", + icon: WrenchRegular, + horizontalLocation: "right", + order: 400, + suppressTeachingMoment: true, + content: () => { + const sections = useOrderedObservableCollection(sectionsCollection); + const sectionContent = useObservableCollection(sectionContentCollection); + const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable); - return scene && ; - }, + return scene && ; + }, + }); + } }); /** @@ -68,7 +78,7 @@ export const ToolsServiceDefinition: ServiceDefinition<[IToolsService], [IShellS return { addSection: (section) => sectionsCollection.add(section), addSectionContent: (content) => sectionContentCollection.add(content), - dispose: () => toolsPaneRegistration.dispose(), + dispose: () => toolsPaneRegistration?.dispose(), }; }, }; From ca32b355099d1ef2a14293e3eb7e01dab3fb726c Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 7 Aug 2025 11:49:33 -0700 Subject: [PATCH 3/4] Various cleanup --- .../src/components/tools/captureTools.tsx | 57 ++++++------ .../src/components/tools/exportTools.tsx | 89 +++++++++---------- .../src/components/tools/importTools.tsx | 28 +++--- .../src/components/tools/toolsPane.tsx | 23 +---- .../services/panes/tools/captureService.tsx | 6 +- .../services/panes/tools/exportService.tsx | 6 +- .../services/panes/tools/importService.tsx | 4 +- 7 files changed, 90 insertions(+), 123 deletions(-) diff --git a/packages/dev/inspector-v2/src/components/tools/captureTools.tsx b/packages/dev/inspector-v2/src/components/tools/captureTools.tsx index 00e82894294..d82336c6225 100644 --- a/packages/dev/inspector-v2/src/components/tools/captureTools.tsx +++ b/packages/dev/inspector-v2/src/components/tools/captureTools.tsx @@ -8,8 +8,10 @@ import type { IScreenshotSize } from "core/Misc/interfaces/screenshotSize"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; import { VideoRecorder } from "core/Misc/videoRecorder"; import { captureEquirectangularFromScene } from "core/Misc/equirectangularCapture"; +import { Collapse } from "shared-ui-components/fluent/primitives/collapse"; +import { CameraRegular, RecordRegular, RecordStopRegular } from "@fluentui/react-icons"; -export const CaptureRttProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { +export const CaptureRttTools: FunctionComponent<{ scene: Scene }> = ({ scene }) => { const [useWidthHeight, setUseWidthHeight] = useState(false); const [screenshotSize, setScreenshotSize] = useState({ precision: 1 }); @@ -27,7 +29,7 @@ export const CaptureRttProperties: FunctionComponent<{ scene: Scene }> = ({ scen return ( <> - + = ({ scen step={0.1} /> setUseWidthHeight(value)} /> - {useWidthHeight && ( - <> - setScreenshotSize({ ...screenshotSize, width: data ?? 512 })} - min={1} - step={1} - /> - setScreenshotSize({ ...screenshotSize, height: data ?? 512 })} - /> - - )} + + setScreenshotSize({ ...screenshotSize, width: data ?? 512 })} + min={1} + step={1} + /> + setScreenshotSize({ ...screenshotSize, height: data ?? 512 })} + min={1} + step={1} + /> + ); }; -export const CaptureScreenshotProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { - const [recordVideoText, setRecordVideoText] = useState("Record video"); - const videoRecorder = useRef(null); +export const CaptureScreenshotTools: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [isRecording, setIsRecording] = useState(false); + const videoRecorder = useRef(); const captureScreenshot = useCallback(() => { if (scene.activeCamera) { @@ -75,7 +77,8 @@ export const CaptureScreenshotProperties: FunctionComponent<{ scene: Scene }> = const recordVideoAsync = useCallback(async () => { if (videoRecorder.current && videoRecorder.current.isRecording) { - void videoRecorder.current.stopRecording(); + videoRecorder.current.stopRecording(); + setIsRecording(false); return; } @@ -83,15 +86,15 @@ export const CaptureScreenshotProperties: FunctionComponent<{ scene: Scene }> = videoRecorder.current = new VideoRecorder(scene.getEngine()); } - await videoRecorder.current.startRecording(); - setRecordVideoText("Stop recording"); + void videoRecorder.current.startRecording(); + setIsRecording(true); }, [scene]); return ( <> - - - + + + ); }; diff --git a/packages/dev/inspector-v2/src/components/tools/exportTools.tsx b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx index ee47818dead..da9ccf5e064 100644 --- a/packages/dev/inspector-v2/src/components/tools/exportTools.tsx +++ b/packages/dev/inspector-v2/src/components/tools/exportTools.tsx @@ -1,5 +1,3 @@ -/* eslint-disable import/no-internal-modules */ -import * as React from "react"; import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; import { SceneSerializer } from "core/Misc/sceneSerializer"; import { Tools } from "core/Misc/tools"; @@ -19,15 +17,16 @@ import type { BackgroundMaterial } from "core/Materials/Background/backgroundMat import { Texture } from "core/Materials/Textures/texture"; import { Camera } from "core/Cameras/camera"; import { Light } from "core/Lights/light"; -import { Text } from "@fluentui/react-components"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; import { MakeLazyComponent } from "shared-ui-components/fluent/primitives/lazyComponent"; +import { Collapse } from "shared-ui-components/fluent/primitives/collapse"; +import { ArrowDownloadRegular } from "@fluentui/react-icons"; const EnvExportImageTypes = [ { label: "PNG", value: 0, imageType: "image/png" }, { label: "WebP", value: 1, imageType: "image/webp" }, -]; +] as const; interface IBabylonExportOptionsState { imageTypeIndex: number; @@ -35,8 +34,8 @@ interface IBabylonExportOptionsState { iblDiffuse: boolean; } -export const ExportBabylonProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { - const [babylonExportOptions, setBabylonExportOptions] = React.useState({ +export const ExportBabylonTools: FunctionComponent<{ scene: Scene }> = ({ scene }) => { + const [babylonExportOptions, setBabylonExportOptions] = useState>({ imageTypeIndex: 0, imageQuality: 0.8, iblDiffuse: false, @@ -69,10 +68,10 @@ export const ExportBabylonProperties: FunctionComponent<{ scene: Scene }> = ({ s return ( <> - + {!scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera && ( <> - + {scene.environmentTexture.irradianceTexture && ( = ({ s setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val as number })); }} /> - {babylonExportOptions.imageTypeIndex > 0 && ( + 0}> = ({ s min={0} max={1} /> - )} + )} @@ -114,14 +113,13 @@ interface IGltfExportOptionsState { exportLights: boolean; } -export const ExportGltfProperties = MakeLazyComponent(async () => { - // Defer importing anything from the gui package until this component is actually mounted. - // eslint-disable-next-line @typescript-eslint/naming-convention +export const ExportGltfTools = MakeLazyComponent(async () => { + // Defer importing anything from the serializers package until this component is actually mounted. const { GLTF2Export } = await import("serializers/glTF/2.0/glTFSerializer"); return (props: { scene: Scene }) => { const [isExportingGltf, setIsExportingGltf] = useState(false); - const [gltfExportOptions, setGltfExportOptions] = useState({ + const [gltfExportOptions, setGltfExportOptions] = useState>({ exportDisabledNodes: false, exportSkyboxes: false, exportCameras: false, @@ -177,40 +175,35 @@ export const ExportGltfProperties = MakeLazyComponent(async () => { return ( <> - {isExportingGltf && } - {!isExportingGltf && ( - <> - setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked })} - /> - setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked })} - /> - setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked })} - /> - setGltfExportOptions({ ...gltfExportOptions, exportLights: checked })} - /> - - )} - {!isExportingGltf && } + setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked })} + /> + setGltfExportOptions({ ...gltfExportOptions, exportLights: checked })} + /> + ); }; diff --git a/packages/dev/inspector-v2/src/components/tools/importTools.tsx b/packages/dev/inspector-v2/src/components/tools/importTools.tsx index a23f6094f97..d37d82be1d5 100644 --- a/packages/dev/inspector-v2/src/components/tools/importTools.tsx +++ b/packages/dev/inspector-v2/src/components/tools/importTools.tsx @@ -1,27 +1,29 @@ import { useState } from "react"; import type { FunctionComponent } from "react"; import type { Scene } from "core/scene"; +import type { DropdownOption } from "shared-ui-components/fluent/primitives/dropdown"; import { ImportAnimationsAsync, SceneLoaderAnimationGroupLoadingMode } from "core/Loading/sceneLoader"; import { FilesInput } from "core/Misc/filesInput"; import { Logger } from "core/Misc"; import { SwitchPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/switchPropertyLine"; import { NumberDropdownPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/dropdownPropertyLine"; import { FileUploadLine } from "shared-ui-components/fluent/hoc/fileUploadLine"; +import { Collapse } from "shared-ui-components/fluent/primitives/collapse"; const AnimationGroupLoadingModes = [ { label: "Clean", value: SceneLoaderAnimationGroupLoadingMode.Clean }, { label: "Stop", value: SceneLoaderAnimationGroupLoadingMode.Stop }, { label: "Sync", value: SceneLoaderAnimationGroupLoadingMode.Sync }, { label: "NoSync", value: SceneLoaderAnimationGroupLoadingMode.NoSync }, -]; +] as const satisfies DropdownOption[]; -export const ImportAnimationsProperties: FunctionComponent<{ scene: Scene }> = ({ scene }) => { +export const ImportAnimationsTools: FunctionComponent<{ scene: Scene }> = ({ scene }) => { const [importDefaults, setImportDefaults] = useState({ overwriteAnimations: true, animationGroupLoadingMode: SceneLoaderAnimationGroupLoadingMode.Clean, }); - const importAnimations = (event: any) => { + const importAnimations = (event: FileList) => { const reloadAsync = async function (sceneFile: File) { if (sceneFile) { try { @@ -40,24 +42,14 @@ export const ImportAnimationsProperties: FunctionComponent<{ scene: Scene }> = ( } }; - const filesInputAnimation = new FilesInput( - scene.getEngine() as any, - scene as any, - () => {}, - () => {}, - () => {}, - () => {}, - () => {}, - reloadAsync, - () => {} - ); - + const filesInputAnimation = new FilesInput(scene.getEngine(), scene, null, null, null, null, null, reloadAsync, null); filesInputAnimation.loadFiles(event); + filesInputAnimation.dispose(); }; return ( <> - importAnimations(evt)} /> + importAnimations(evt)} /> = ( setImportDefaults({ ...importDefaults, overwriteAnimations: value }); }} /> - {importDefaults.overwriteAnimations === false && ( + = ( setImportDefaults({ ...importDefaults, animationGroupLoadingMode: value }); }} /> - )} + ); }; diff --git a/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx b/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx index 515e14c2add..8a49b3176e4 100644 --- a/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx +++ b/packages/dev/inspector-v2/src/components/tools/toolsPane.tsx @@ -1,27 +1,6 @@ -// eslint-disable-next-line import/no-internal-modules - -import { Body1Strong, makeStyles, tokens } from "@fluentui/react-components"; - import { ExtensibleAccordion } from "../extensibleAccordion"; import type { Scene } from "core/scene"; -// eslint-disable-next-line @typescript-eslint/naming-convention -const useStyles = makeStyles({ - placeholderDiv: { - padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`, - }, -}); - export const ToolsPane: typeof ExtensibleAccordion = (props) => { - const classes = useStyles(); - - const entity = props.context; - - return entity != null ? ( - - ) : ( -
- No entity selected. -
- ); + return ; }; diff --git a/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx index 84d13656ee4..e2243d77de9 100644 --- a/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/tools/captureService.tsx @@ -2,7 +2,7 @@ import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; import { ToolsServiceIdentity } from "../toolsService"; import type { IToolsService } from "../toolsService"; import type { IDisposable } from "core/scene"; -import { CaptureRttProperties, CaptureScreenshotProperties } from "../../../components/tools/captureTools"; +import { CaptureRttTools, CaptureScreenshotTools } from "../../../components/tools/captureTools"; export const CaptureServiceDefinition: ServiceDefinition<[], [IToolsService]> = { friendlyName: "Capture Tools", @@ -15,7 +15,7 @@ export const CaptureServiceDefinition: ServiceDefinition<[], [IToolsService]> = toolsService.addSectionContent({ key: "Screenshot Capture", section: "Screenshot Capture", - component: ({ context }) => , + component: ({ context }) => , }) ); @@ -24,7 +24,7 @@ export const CaptureServiceDefinition: ServiceDefinition<[], [IToolsService]> = toolsService.addSectionContent({ key: "RTT Capture", section: "RTT Capture", - component: ({ context }) => , + component: ({ context }) => , }) ); diff --git a/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx index a955fd6bcd7..68b7d79b034 100644 --- a/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/tools/exportService.tsx @@ -2,7 +2,7 @@ import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; import { ToolsServiceIdentity } from "../toolsService"; import type { IToolsService } from "../toolsService"; import type { IDisposable } from "core/scene"; -import { ExportBabylonProperties, ExportGltfProperties } from "../../../components/tools/exportTools"; +import { ExportBabylonTools, ExportGltfTools } from "../../../components/tools/exportTools"; export const ExportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { friendlyName: "Export Tools", @@ -15,7 +15,7 @@ export const ExportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { toolsService.addSectionContent({ key: "glTF Export", section: "glTF Export", - component: ({ context }) => , + component: ({ context }) => , }) ); @@ -24,7 +24,7 @@ export const ExportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { toolsService.addSectionContent({ key: "Babylon Export", section: "Babylon Export", - component: ({ context }) => , + component: ({ context }) => , }) ); diff --git a/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx b/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx index 8bb8da435f3..e474c3440c3 100644 --- a/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/tools/importService.tsx @@ -1,7 +1,7 @@ import type { ServiceDefinition } from "../../../modularity/serviceDefinition"; import { ToolsServiceIdentity } from "../toolsService"; import type { IToolsService } from "../toolsService"; -import { ImportAnimationsProperties } from "../../../components/tools/importTools"; +import { ImportAnimationsTools } from "../../../components/tools/importTools"; export const SceneImportServiceDefinition: ServiceDefinition<[], [IToolsService]> = { friendlyName: "Import Tool", @@ -10,7 +10,7 @@ export const SceneImportServiceDefinition: ServiceDefinition<[], [IToolsService] const contentRegistration = toolsService.addSectionContent({ key: "AnimationImport", section: "Animation Import", - component: ({ context }) => , + component: ({ context }) => , }); return { From 4fb356309eb00cde4837e534c8a59b906a8f0f26 Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Thu, 7 Aug 2025 12:16:21 -0700 Subject: [PATCH 4/4] Add serializers to Playground webpack config for inspector v2 --- packages/tools/playground/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tools/playground/webpack.config.js b/packages/tools/playground/webpack.config.js index 37a96abbb3e..adf6eb75cbc 100644 --- a/packages/tools/playground/webpack.config.js +++ b/packages/tools/playground/webpack.config.js @@ -34,6 +34,7 @@ module.exports = (env) => { core: path.resolve("../../dev/core/dist"), loaders: path.resolve("../../dev/loaders/dist"), gui: path.resolve("../../dev/gui/dist"), + serializers: path.resolve("../../dev/serializers/dist"), }, }, externals: [