Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const AnimationGroupLoadingModes = [
{ label: "NoSync", value: SceneLoaderAnimationGroupLoadingMode.NoSync },
] as const satisfies DropdownOption<number>[];

export const ImportAnimationsTools: FunctionComponent<{ scene: Scene }> = ({ scene }) => {
/**
* Component for importing animations into an existing scene.
* Allows configuration of animation merge behavior and provides file upload interface.
* @param props - Component props
* @returns The animation import UI
*/
export const GLTFAnimationImport: FunctionComponent<{ scene: Scene }> = ({ scene }) => {
const [importDefaults, setImportDefaults] = useState({
overwriteAnimations: true,
animationGroupLoadingMode: SceneLoaderAnimationGroupLoadingMode.Clean,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import type { FunctionComponent } from "react";
import { ButtonLine } from "shared-ui-components/fluent/hoc/buttonLine";
import type { IGLTFLoaderService } from "../../../services/panes/tools/gltfLoaderService";
import { useObservableState } from "../../../hooks/observableHooks";
import { StringifiedPropertyLine } from "shared-ui-components/fluent/hoc/propertyLines/stringifiedPropertyLine";
import { MessageBar } from "shared-ui-components/fluent/primitives/messageBar";

/**
* Component that displays glTF validation results.
* Shows validation status (errors, warnings, hints, info) and allows viewing detailed report.
* @param props - Component props
* @returns The validation results UI
*/
export const GLTFValidationTools: FunctionComponent<{ gltfLoaderService: IGLTFLoaderService }> = ({ gltfLoaderService }) => {
const validationResults = useObservableState(
useCallback(() => gltfLoaderService.getValidationResults(), [gltfLoaderService]),
gltfLoaderService.onValidationResultsObservable
);

const openValidationDetails = useCallback(() => {
if (!validationResults) {
return;
}

const win = window.open("", "_blank");
if (win) {
win.document.title = `${validationResults.uri} - glTF Validation Results`;
win.document.body.style.backgroundColor = "#322e2eff";
win.document.body.style.color = "#fff";
win.document.body.style.padding = "1rem";
const pre = win.document.createElement("pre");
const code = win.document.createElement("code");
const textNode = win.document.createTextNode(JSON.stringify(validationResults, null, 2));
code.append(textNode);
pre.append(code);
win.document.body.append(pre);
win.focus();
}
}, [validationResults]);

if (!validationResults) {
return <MessageBar intent="info" title="" message="Reload the file to see validation results" />;
}

const issues = validationResults.issues;
const hasErrors = issues.numErrors > 0;

return (
<>
<MessageBar intent={hasErrors ? "error" : "success"} message={hasErrors ? "Your file has validation issues" : "Your file is a valid glTF file"} />
<StringifiedPropertyLine key="NumErrors" label="Errors" value={issues.numErrors} />
<StringifiedPropertyLine key="NumWarnings" label="Warnings" value={issues.numWarnings} />
<StringifiedPropertyLine key="NumInfos" label="Infos" value={issues.numInfos} />
<StringifiedPropertyLine key="NumHints" label="Hints" value={issues.numHints} />
<ButtonLine label="View Report Details" onClick={openValidationDetails} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspecto
description: "Adds new features related to importing Babylon assets.",
keywords: ["import", "tools"],
...BabylonWebResources,
author: { name: "Alex Chuber", forumUserName: "alexchuber" },
author: { name: "Alex Huber", forumUserName: "alexchuber" }, // Q: Any reason to not put Babylon.js here?
getExtensionModuleAsync: async () => await import("../services/panes/tools/importService"),
},
{
Expand Down
2 changes: 2 additions & 0 deletions packages/dev/inspector-v2/src/inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { SceneContextIdentity } from "./services/sceneContext";
import { SelectionServiceDefinition } from "./services/selectionService";
import { ShellServiceIdentity } from "./services/shellService";
import { UserFeedbackServiceDefinition } from "./services/userFeedbackService";
import { GLTFLoaderServiceDefinition } from "./services/panes/tools/gltfLoaderService";

export type InspectorOptions = Omit<ModularToolOptions, "toolbarMode"> & { autoResizeEngine?: boolean };

Expand Down Expand Up @@ -264,6 +265,7 @@ export function ShowInspector(scene: Scene, options: Partial<InspectorOptions> =

// Tools pane tab and related services.
ToolsServiceDefinition,
GLTFLoaderServiceDefinition,

// Settings pane tab and related services.
SettingsServiceDefinition,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { IService } from "../../../modularity/serviceDefinition";
import type { ISceneContext } from "../../sceneContext";
import { SceneContextIdentity } from "../../sceneContext";
import type { Observable } from "core/Misc/observable";
import { Observable as BabylonObservable } from "core/Misc/observable";
import { SceneLoader } from "core/Loading/sceneLoader";
import type { ISceneLoaderPlugin, ISceneLoaderPluginAsync } from "core/Loading/sceneLoader";
import type { GLTFFileLoader, IGLTFLoaderExtension } from "loaders/glTF/glTFFileLoader";
import { GLTFLoaderAnimationStartMode, GLTFLoaderCoordinateSystemMode } from "loaders/glTF/glTFFileLoader";
import type { IGLTFValidationResults } from "babylonjs-gltf2interface";
import type { Nullable } from "core/types";

export const GLTFLoaderServiceIdentity = Symbol("GLTFLoaderService");

/**
* Configuration state for the glTF loader.
* Uses the actual property types from GLTFFileLoader for type safety.
*/
export type IGLTFLoaderState = Pick<
GLTFFileLoader,
| "alwaysComputeBoundingBox"
| "alwaysComputeSkeletonRootNode"
| "animationStartMode"
| "capturePerformanceCounters"
| "compileMaterials"
| "compileShadowGenerators"
| "coordinateSystemMode"
| "createInstances"
| "loggingEnabled"
| "loadAllMaterials"
| "targetFps"
| "transparencyAsCoverage"
| "useClipPlane"
| "useSRGBBuffers"
>;

/**
* State for a single glTF extension.
* Uses the actual enabled property from IGLTFLoaderExtension with additional extension-specific properties.
*/
export type IGLTFExtensionState = Pick<IGLTFLoaderExtension, "enabled"> & {
/** Additional extension-specific properties */
[key: string]: any;
};

/**
* Collection of extension states keyed by extension name
*/
export interface IGLTFExtensionStates {
[key: string]: IGLTFExtensionState;
}

/**
* Service for managing all aspects of the glTF loader including configuration,
* extensions, validation, and plugin lifecycle
*/
export interface IGLTFLoaderService extends IService<typeof GLTFLoaderServiceIdentity> {
// Plugin lifecycle
/**
* Observable that fires when a glTF loader plugin is activated
*/
readonly onLoaderActivatedObservable: Observable<GLTFFileLoader>;

// Loader configuration
/**
* Observable that fires when loader configuration changes
*/
readonly onLoaderConfigChangedObservable: Observable<IGLTFLoaderState>;

/**
* Get the current loader configuration
*/
getLoaderConfig(): IGLTFLoaderState;

/**
* Update a specific loader configuration property
*/
updateLoaderConfig<K extends keyof IGLTFLoaderState>(key: K, value: IGLTFLoaderState[K]): void;

// Extensions configuration
/**
* Observable that fires when extension configuration changes
*/
readonly onExtensionConfigChangedObservable: Observable<IGLTFExtensionStates>;

/**
* Get the current extension configurations
*/
getExtensionStates(): IGLTFExtensionStates;

/**
* Update a specific extension's enabled state
*/
updateExtensionState(extensionName: string, enabled: boolean): void;

/**
* Update a specific property on an extension
*/
updateExtensionProperty<K extends string>(extensionName: string, property: K, value: any): void;

// Validation
/**
* Observable that fires when new validation results are received
*/
readonly onValidationResultsObservable: Observable<Nullable<IGLTFValidationResults>>;

/**
* Get the most recent validation results
*/
getValidationResults(): Nullable<IGLTFValidationResults>;
}

/**
* Creates default glTF loader state
* @returns Default loader configuration
*/
const CreateDefaultLoaderState = (): IGLTFLoaderState => ({
alwaysComputeBoundingBox: false,
alwaysComputeSkeletonRootNode: false,
animationStartMode: GLTFLoaderAnimationStartMode.FIRST,
capturePerformanceCounters: false,
compileMaterials: false,
compileShadowGenerators: false,
coordinateSystemMode: GLTFLoaderCoordinateSystemMode.AUTO,
createInstances: true,
loggingEnabled: false,
loadAllMaterials: false,
targetFps: 60,
transparencyAsCoverage: false,
useClipPlane: false,
useSRGBBuffers: false,
});

/**
* Creates default extension states
* @returns Default extension configuration
*/
const CreateDefaultExtensionStates = (): IGLTFExtensionStates => {
/* eslint-disable @typescript-eslint/naming-convention */
return {
EXT_lights_image_based: { enabled: true },
EXT_mesh_gpu_instancing: { enabled: true },
EXT_texture_webp: { enabled: true },
EXT_texture_avif: { enabled: true },
KHR_draco_mesh_compression: { enabled: true },
KHR_materials_pbrSpecularGlossiness: { enabled: true },
KHR_materials_clearcoat: { enabled: true },
KHR_materials_iridescence: { enabled: true },
KHR_materials_anisotropy: { enabled: true },
KHR_materials_emissive_strength: { enabled: true },
KHR_materials_ior: { enabled: true },
KHR_materials_sheen: { enabled: true },
KHR_materials_specular: { enabled: true },
KHR_materials_unlit: { enabled: true },
KHR_materials_variants: { enabled: true },
KHR_materials_transmission: { enabled: true },
KHR_materials_diffuse_transmission: { enabled: true },
KHR_materials_volume: { enabled: true },
KHR_materials_dispersion: { enabled: true },
KHR_materials_diffuse_roughness: { enabled: true },
KHR_mesh_quantization: { enabled: true },
KHR_lights_punctual: { enabled: true },
EXT_lights_area: { enabled: true },
KHR_texture_basisu: { enabled: true },
KHR_texture_transform: { enabled: true },
KHR_xmp_json_ld: { enabled: true },
MSFT_lod: { enabled: true, maxLODsToLoad: Number.MAX_VALUE },
MSFT_minecraftMesh: { enabled: true },
MSFT_sRGBFactors: { enabled: true },
MSFT_audio_emitter: { enabled: true },
};
};

/**
* Unified service for managing all aspects of the glTF loader
*/
export const GLTFLoaderServiceDefinition: ServiceDefinition<[IGLTFLoaderService], [ISceneContext]> = {
friendlyName: "glTF Loader",
produces: [GLTFLoaderServiceIdentity],
consumes: [SceneContextIdentity],
factory: (_sceneContext) => {
// Plugin lifecycle
const onLoaderActivatedObservable = new BabylonObservable<GLTFFileLoader>();

// Loader configuration
const onLoaderConfigChangedObservable = new BabylonObservable<IGLTFLoaderState>();
let loaderState = CreateDefaultLoaderState();

// Extensions configuration
const onExtensionConfigChangedObservable = new BabylonObservable<IGLTFExtensionStates>();
let extensionStates = CreateDefaultExtensionStates();

// Validation
const onValidationResultsObservable = new BabylonObservable<Nullable<IGLTFValidationResults>>();
let validationResults: Nullable<IGLTFValidationResults> = null;

// Subscribe to plugin activation
const pluginObserver = SceneLoader.OnPluginActivatedObservable.add((plugin: ISceneLoaderPlugin | ISceneLoaderPluginAsync) => {
if (plugin.name === "gltf") {
const loader = plugin as GLTFFileLoader;

// Subscribe to loader
loader.onValidatedObservable.add((results: IGLTFValidationResults) => {
validationResults = results;
onValidationResultsObservable.notifyObservers(results);
});

// Subscribe to extension loading
loader.onExtensionLoadedObservable.add((extension: IGLTFLoaderExtension) => {
const extensionState = extensionStates[extension.name];
if (extensionState) {
// Apply all extension properties
Object.keys(extensionState).forEach((key) => {
(extension as any)[key] = extensionState[key];
});
}
});

// Apply loader configuration
Object.keys(loaderState).forEach((key) => {
(loader as any)[key] = loaderState[key as keyof IGLTFLoaderState];
});

// Always enable validation to provide feedback in the UI
// This ensures validation results are available regardless of user settings
loader.validate = true;

// Notify observers after everything is set up
onLoaderActivatedObservable.notifyObservers(loader);
}
});

return {
// Plugin lifecycle
onLoaderActivatedObservable,

// Loader configuration
onLoaderConfigChangedObservable,

getLoaderConfig: () => ({ ...loaderState }),

updateLoaderConfig: <K extends keyof IGLTFLoaderState>(key: K, value: IGLTFLoaderState[K]) => {
loaderState = { ...loaderState, [key]: value };
onLoaderConfigChangedObservable.notifyObservers(loaderState);
},

// Extensions configuration
onExtensionConfigChangedObservable,

getExtensionStates: () => {
// Return a deep copy to prevent external mutation
return JSON.parse(JSON.stringify(extensionStates));
},

updateExtensionState: (extensionName: string, enabled: boolean) => {
// Ensure extension state exists before updating
const currentState = extensionStates[extensionName] ?? { enabled: true };
extensionStates = {
...extensionStates,
[extensionName]: { ...currentState, enabled },
};
onExtensionConfigChangedObservable.notifyObservers(extensionStates);
},

updateExtensionProperty: <K extends string>(extensionName: string, property: K, value: any) => {
// Ensure extension state exists before updating
const currentState = extensionStates[extensionName] ?? { enabled: true };
extensionStates = {
...extensionStates,
[extensionName]: { ...currentState, [property]: value },
};
onExtensionConfigChangedObservable.notifyObservers(extensionStates);
},

// Validation
onValidationResultsObservable,

getValidationResults: () => validationResults,

dispose: () => {
pluginObserver.remove();
onLoaderActivatedObservable.clear();
onLoaderConfigChangedObservable.clear();
onExtensionConfigChangedObservable.clear();
onValidationResultsObservable.clear();
},
};
},
};
Loading
Loading