@babylonjs/inspector 9.16.1 → 9.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -145,6 +145,7 @@ import { MultiRenderTarget } from '@babylonjs/core/Materials/Textures/multiRende
145
145
  import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture.js';
146
146
  import { ThinTexture } from '@babylonjs/core/Materials/Textures/thinTexture.js';
147
147
  import { MainAudioBus } from '@babylonjs/core/AudioV2/abstractAudio/mainAudioBus.js';
148
+ import { FindSmartAssetKeyForObject, SerializeSmartAssetManagerMap, GetSmartAssetTextureExtensions, GetAllSmartAssets, RemoveSmartAssetAsync, RegisterSmartAsset, LoadAllSmartAssetsAsync, LoadSmartAssetAsync, GetSmartAssetManager, LoadSmartAssetTextureAsync, ReloadSmartAssetAsync, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.pure.js';
148
149
  import '@babylonjs/core/Rendering/boundingBoxRenderer.js';
149
150
  import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent.js';
150
151
  import '@babylonjs/core/Sprites/spriteSceneComponent.js';
@@ -154,7 +155,6 @@ import { SceneRecorder } from '@babylonjs/core/Misc/sceneRecorder.js';
154
155
  import { VideoRecorder } from '@babylonjs/core/Misc/videoRecorder.js';
155
156
  import { SceneSerializer } from '@babylonjs/core/Misc/sceneSerializer.js';
156
157
  import { EnvironmentTextureTools } from '@babylonjs/core/Misc/environmentTextureTools.js';
157
- import { FindSmartAssetKeyForObject, SerializeSmartAssetManagerMap, GetSmartAssetTextureExtensions, GetAllSmartAssets, RemoveSmartAssetAsync, RegisterSmartAsset, LoadAllSmartAssetsAsync, LoadSmartAssetAsync, GetSmartAssetManager, LoadSmartAssetTextureAsync, ReloadSmartAssetAsync, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.pure.js';
158
158
  import '@babylonjs/core/Loading/Plugins/babylonFileLoader.js';
159
159
  import { ReadJsonSourceAsync, ResolveAssetUrl, DeserializeSmartAssetMap } from '@babylonjs/core/SmartAssets/smartAssetSerializer.js';
160
160
  import { ImportAnimationsAsync, SceneLoader } from '@babylonjs/core/Loading/sceneLoader.js';
@@ -8805,7 +8805,7 @@ function MakeModularTool(options) {
8805
8805
  }
8806
8806
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
8807
8807
  if (extensionFeeds.length > 0) {
8808
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-C6UitRwb.js');
8808
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-PdjX95Wt.js');
8809
8809
  serviceContainer.addService(ExtensionListServiceDefinition);
8810
8810
  }
8811
8811
  // Register all external services (that make up a unique tool).
@@ -8946,14 +8946,14 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
8946
8946
  description: "Adds a new panel for easy creation of various Babylon assets. This is a WIP extension...expect changes!",
8947
8947
  keywords: ["creation", "tools"],
8948
8948
  ...BabylonWebResources,
8949
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-xvSzPXeJ.js'),
8949
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-D5JrCb-u.js'),
8950
8950
  },
8951
8951
  {
8952
8952
  name: "Reflector",
8953
8953
  description: "Connects to the Reflector Bridge for real-time scene synchronization with the Babylon.js Sandbox.",
8954
8954
  keywords: ["reflector", "bridge", "sync", "sandbox", "tools"],
8955
8955
  ...BabylonWebResources,
8956
- getExtensionModuleAsync: async () => await import('./reflectorService-CVK7MlrU.js'),
8956
+ getExtensionModuleAsync: async () => await import('./reflectorService-Ce2pbz5D.js'),
8957
8957
  },
8958
8958
  ]);
8959
8959
 
@@ -21867,7 +21867,7 @@ const BaseTextureCharacteristicProperties = (props) => {
21867
21867
  const displayFormat = FindTextureFormat(format === -1 ? Constants.TEXTUREFORMAT_RGBA : format);
21868
21868
  const displayType = FindTextureType(type === -1 ? Constants.TEXTURETYPE_UNSIGNED_BYTE : type);
21869
21869
  const maxAnisotropy = texture.getScene()?.getEngine().getCaps().maxAnisotropy ?? 1;
21870
- return (jsxs(Fragment, { children: [texture.is2DArray && jsx(TextPropertyLine, { label: "Layers", value: depth?.toString() ?? "?" }), texture.is3D && jsx(TextPropertyLine, { label: "Depth", value: depth?.toString() ?? "?" }), jsx(TextPropertyLine, { label: "Format", value: displayFormat?.label ?? "unknown" }), !displayFormat?.hideType && !displayFormat?.compressed && jsx(TextPropertyLine, { label: "Type", value: displayType?.label ?? "unknown" }), !!displayFormat?.normalizable && !displayFormat?.compressed && displayType?.normalizable != undefined && (jsx(BooleanBadgePropertyLine, { label: "Normalized", value: displayType.normalizable })), jsx(BooleanBadgePropertyLine, { label: "Compressed", value: displayFormat?.compressed ?? false }), jsx(BooleanBadgePropertyLine, { label: "sRGB Buffers", value: useSRGBBuffer ?? false }), jsx(BoundProperty, { component: BooleanBadgePropertyLine, label: "Gamma Space", target: texture, propertyKey: "gammaSpace" }), jsx(BoundProperty, { component: BooleanBadgePropertyLine, label: "Has Alpha", target: texture, propertyKey: "hasAlpha" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Alpha from RGB", target: texture, propertyKey: "getAlphaFromRGB" }), jsx(BooleanBadgePropertyLine, { label: "3D", value: texture.is3D }), jsx(BooleanBadgePropertyLine, { label: "2D Array", value: texture.is2DArray }), jsx(BooleanBadgePropertyLine, { label: "Cube", value: texture.isCube }), jsx(BooleanBadgePropertyLine, { label: "Render Target", value: texture.isRenderTarget }), jsx(BooleanBadgePropertyLine, { label: "Mipmaps", value: !texture.noMipmap }), jsx(TextPropertyLine, { label: "Samples", value: samples.toString() }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "UV Set", target: texture, propertyKey: "coordinatesIndex", min: 0, max: 3, step: 1 }), jsx(BoundProperty, { component: NumberDropdownPropertyLine, label: "Mode", target: texture, propertyKey: "coordinatesMode", options: CoordinatesMode }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Level", target: texture, propertyKey: "level", min: 0, max: 2, step: 0.01 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Anisotropy", target: texture, propertyKey: "anisotropicFilteringLevel", min: 1, max: maxAnisotropy, step: 1 })] }));
21870
+ return (jsxs(Fragment, { children: [texture.is2DArray && jsx(TextPropertyLine, { label: "Layers", value: depth?.toString() ?? "?" }), texture.is3D && jsx(TextPropertyLine, { label: "Depth", value: depth?.toString() ?? "?" }), jsx(TextPropertyLine, { label: "Format", value: displayFormat?.label ?? "unknown" }), !displayFormat?.hideType && !displayFormat?.compressed && jsx(TextPropertyLine, { label: "Type", value: displayType?.label ?? "unknown" }), !!displayFormat?.normalizable && !displayFormat?.compressed && displayType?.normalizable != undefined && (jsx(BooleanBadgePropertyLine, { label: "Normalized", value: displayType.normalizable })), jsx(BooleanBadgePropertyLine, { label: "Compressed", value: displayFormat?.compressed ?? false }), jsx(BooleanBadgePropertyLine, { label: "sRGB Buffers", value: useSRGBBuffer ?? false }), jsx(BoundProperty, { component: BooleanBadgePropertyLine, label: "Gamma Space", target: texture, propertyKey: "gammaSpace" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Has Alpha", target: texture, propertyKey: "hasAlpha" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Alpha from RGB", target: texture, propertyKey: "getAlphaFromRGB" }), jsx(BooleanBadgePropertyLine, { label: "3D", value: texture.is3D }), jsx(BooleanBadgePropertyLine, { label: "2D Array", value: texture.is2DArray }), jsx(BooleanBadgePropertyLine, { label: "Cube", value: texture.isCube }), jsx(BooleanBadgePropertyLine, { label: "Render Target", value: texture.isRenderTarget }), jsx(BooleanBadgePropertyLine, { label: "Mipmaps", value: !texture.noMipmap }), jsx(TextPropertyLine, { label: "Samples", value: samples.toString() }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "UV Set", target: texture, propertyKey: "coordinatesIndex", min: 0, max: 3, step: 1 }), jsx(BoundProperty, { component: NumberDropdownPropertyLine, label: "Mode", target: texture, propertyKey: "coordinatesMode", options: CoordinatesMode }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Level", target: texture, propertyKey: "level", min: 0, max: 2, step: 0.01 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Anisotropy", target: texture, propertyKey: "anisotropicFilteringLevel", min: 1, max: maxAnisotropy, step: 1 })] }));
21871
21871
  };
21872
21872
  const BaseTextureTransformProperties = (props) => {
21873
21873
  const { texture } = props;
@@ -21906,7 +21906,25 @@ const TexturePreviewProperties = (props) => {
21906
21906
  };
21907
21907
  const TextureGeneralProperties = (props) => {
21908
21908
  const { texture } = props;
21909
- return (jsx(Fragment, { children: jsx(BooleanBadgePropertyLine, { label: "Invert Y", description: "If true, the texture is stored as inverted on Y", value: texture.invertY }) }));
21909
+ const invertY = useProperty(texture, "invertY");
21910
+ const url = useProperty(texture, "url");
21911
+ const notifyPropertyChanged = usePropertyChangedNotifier();
21912
+ // invertY is applied at upload time (UNPACK_FLIP_Y), so changing it requires re-uploading the source
21913
+ // image. That is only valid for a plain Texture loaded from a URL. Subclasses that reuse this section
21914
+ // (VideoTexture, DynamicTexture, RenderTargetTexture, etc.) either report a different class name or have
21915
+ // no source URL to reload, so for those invertY is surfaced as read-only.
21916
+ const canToggleInvertY = texture.getClassName() === "Texture" && !!url;
21917
+ return (jsx(Fragment, { children: canToggleInvertY ? (jsx(Property, { component: SwitchPropertyLine, label: "Invert Y", description: "If true, the texture is stored as inverted on Y", propertyPath: "invertY", value: invertY, onChange: (value) => {
21918
+ const oldValue = texture.invertY;
21919
+ // invertY is baked into the uploaded texel data, so re-upload the texture to apply it.
21920
+ // updateURL releases only the underlying GPU texture and reuses this same Texture
21921
+ // instance, so all scene references (materials, nodes) stay intact.
21922
+ texture._invertY = value;
21923
+ texture.updateURL(texture.url, texture._buffer);
21924
+ // Property does not bind onChange, so forward the change to the Inspector's
21925
+ // property-change pipeline (e.g. for override capture on .babylonproj projects).
21926
+ notifyPropertyChanged(texture, "invertY", oldValue, value);
21927
+ } })) : (jsx(BooleanBadgePropertyLine, { label: "Invert Y", description: "If true, the texture is stored as inverted on Y", value: invertY })) }));
21910
21928
  };
21911
21929
  const TextureTransformProperties = (props) => {
21912
21930
  const { texture } = props;
@@ -22078,10 +22096,10 @@ const TexturePropertiesServiceDefinition = {
22078
22096
 
22079
22097
  const TransformProperties = (props) => {
22080
22098
  const { transform } = props;
22081
- const quatRotation = useQuaternionProperty(transform, "rotationQuaternion");
22099
+ const quatRotation = useProperty(transform, "rotationQuaternion");
22082
22100
  const [useDegrees] = useSetting(UseDegreesSettingDescriptor);
22083
22101
  const [useEuler] = useSetting(UseEulerSettingDescriptor);
22084
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Position", target: transform, propertyKey: "position" }), quatRotation ? (jsx(Property, { component: QuaternionPropertyLine, label: "Rotation", propertyPath: "rotationQuaternion", value: quatRotation, onChange: (val) => (transform.rotationQuaternion = val), useDegrees: useDegrees, useEuler: useEuler })) : (jsx(BoundProperty, { component: RotationVectorPropertyLine, label: "Rotation", target: transform, propertyKey: "rotation", useDegrees: useDegrees })), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Scaling", target: transform, propertyKey: "scaling", step: 0.1 })] }));
22102
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Position", target: transform, propertyKey: "position" }), quatRotation ? (jsx(BoundProperty, { component: QuaternionPropertyLine, label: "Rotation", target: transform, propertyKey: "rotationQuaternion", propertyPath: "rotationQuaternion", defaultValue: null, useDegrees: useDegrees, useEuler: useEuler })) : (jsx(BoundProperty, { component: RotationVectorPropertyLine, label: "Rotation", target: transform, propertyKey: "rotation", useDegrees: useDegrees })), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Scaling", target: transform, propertyKey: "scaling", step: 0.1 })] }));
22085
22103
  };
22086
22104
 
22087
22105
  const TransformPropertiesServiceDefinition = {
@@ -22827,1554 +22845,2064 @@ const MaterialExplorerServiceDefinition = {
22827
22845
  },
22828
22846
  };
22829
22847
 
22830
- function IsCameraFrameGraphTask(task) {
22831
- return task.camera instanceof Camera;
22848
+ const OverrideManagerKey = Symbol("babylonjs:overrideManager");
22849
+ const OverrideManagerInternals = new WeakMap();
22850
+ const OnOverrideManagerCreatedObservable = new Observable();
22851
+ /**
22852
+ * Creates a new OverrideManager state object and attaches it to the scene.
22853
+ *
22854
+ * Internal: callers should use {@link GetOverrideManager} which returns the
22855
+ * existing manager when one is already attached.
22856
+ * @param scene - The scene this manager operates on.
22857
+ * @returns The created override manager state.
22858
+ */
22859
+ function CreateOverrideManager(scene) {
22860
+ const manager = {
22861
+ scene,
22862
+ onChangedObservable: new Observable(),
22863
+ };
22864
+ const internal = {
22865
+ overrides: [],
22866
+ originalValues: new Map(),
22867
+ sceneDisposeObserver: null,
22868
+ };
22869
+ OverrideManagerInternals.set(manager, internal);
22870
+ if (!scene.metadata) {
22871
+ scene.metadata = {};
22872
+ }
22873
+ scene.metadata[OverrideManagerKey] = manager;
22874
+ // Auto-dispose when the scene is disposed so the manager doesn't outlive it.
22875
+ internal.sceneDisposeObserver = scene.onDisposeObservable.add(() => DisposeOverrideManager(manager));
22876
+ OnOverrideManagerCreatedObservable.notifyObservers(manager);
22877
+ return manager;
22832
22878
  }
22833
- function IsNodesSectionType(node) {
22834
- return node instanceof TransformNode || node instanceof Camera || node instanceof Light;
22879
+ /**
22880
+ * Returns the OverrideManager attached to the given scene, creating and
22881
+ * attaching one if none exists.
22882
+ * @param scene - The scene to look up or attach a manager to.
22883
+ * @returns The existing or newly created OverrideManager.
22884
+ */
22885
+ function GetOverrideManager(scene) {
22886
+ const existing = scene.metadata?.[OverrideManagerKey];
22887
+ if (existing) {
22888
+ return existing;
22889
+ }
22890
+ return CreateOverrideManager(scene);
22835
22891
  }
22836
- const NodeExplorerServiceDefinition = {
22837
- friendlyName: "Node Explorer",
22838
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, GizmoServiceIdentity, WatcherServiceIdentity],
22839
- factory: (sceneExplorerService, sceneContext, gizmoService, watcherService) => {
22840
- const scene = sceneContext.currentScene;
22841
- if (!scene) {
22842
- return undefined;
22892
+ /**
22893
+ * Adds an override entry and immediately applies it.
22894
+ * If an override with the same target coordinates already exists, it is replaced.
22895
+ *
22896
+ * When the caller has already mutated the target (e.g. an Inspector edit),
22897
+ * pass `{ originalValue }` containing the property's prior value — this seeds
22898
+ * the original-value map (so {@link RemoveOverride} can restore it) and skips
22899
+ * the redundant apply step.
22900
+ * @param scene - The scene whose override registry to update.
22901
+ * @param entry - The override to add.
22902
+ * @param options - Optional behavior modifiers; see {@link AddOverrideOptions}.
22903
+ */
22904
+ function AddOverride(scene, entry, options) {
22905
+ const manager = GetOverrideManager(scene);
22906
+ const internal = GetOverrideInternals(manager);
22907
+ RemoveMatchingOverride(internal, entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
22908
+ internal.overrides.push(entry);
22909
+ if (options && "originalValue" in options) {
22910
+ // Caller already applied the new value. Seed the captured original from
22911
+ // their pre-edit snapshot — otherwise ApplyOverrideEntry would capture
22912
+ // the post-edit value and RemoveOverride would have nothing to restore.
22913
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
22914
+ if (!internal.originalValues.has(origKey)) {
22915
+ internal.originalValues.set(origKey, CloneValue(options.originalValue));
22843
22916
  }
22844
- const nodeMovedObservable = new Observable();
22845
- // Set of all nodes known to be in the scene, rebuilt each time getRootEntities
22846
- // is called. Used by getEntityDisplayInfo to detect orphaned ancestor nodes.
22847
- const knownSceneNodes = new Set();
22848
- const sectionRegistration = sceneExplorerService.addSection({
22849
- displayName: "Nodes",
22850
- order: 100 /* DefaultSectionsOrder.Nodes */,
22851
- getRootEntities: () => {
22852
- const rootNodes = new Set(scene.rootNodes);
22853
- knownSceneNodes.clear();
22854
- // Ensure all nodes in the scene are reachable in the explorer, even if their
22855
- // parent was removed from the scene or is not a type shown in the Nodes section.
22856
- for (const node of [...scene.meshes, ...scene.transformNodes, ...scene.cameras, ...scene.lights]) {
22857
- knownSceneNodes.add(node);
22858
- if (!node.parent) {
22859
- continue;
22860
- }
22861
- if (!IsNodesSectionType(node.parent)) {
22862
- // Parent is not a type shown in the Nodes section (e.g. a Bone).
22863
- // Treat this node as a root so it still appears in the explorer.
22864
- rootNodes.add(node);
22865
- }
22866
- else {
22867
- // Walk up through Nodes-section-type parents to find the topmost ancestor.
22868
- // If that ancestor was removed from the scene (not in rootNodes), add it
22869
- // so the entire subtree remains visible in the explorer.
22870
- let ancestor = node.parent;
22871
- while (ancestor.parent && IsNodesSectionType(ancestor.parent)) {
22872
- ancestor = ancestor.parent;
22873
- }
22874
- rootNodes.add(ancestor);
22875
- }
22876
- }
22877
- // Lights within a clustered light container are not included in Scene.lights or Scene.rootNodes.
22878
- // If they also have no parent, then they won't show up anywhere, so show them as root nodes.
22879
- for (const light of scene.lights) {
22880
- if (light instanceof ClusteredLightContainer) {
22881
- for (const childLight of light.lights) {
22882
- knownSceneNodes.add(childLight);
22883
- if (!childLight.parent) {
22884
- rootNodes.add(childLight);
22885
- }
22886
- }
22887
- }
22917
+ }
22918
+ else {
22919
+ ApplyOverrideEntry(manager, internal, entry);
22920
+ }
22921
+ manager.onChangedObservable.notifyObservers();
22922
+ }
22923
+ /**
22924
+ * Removes a single override matching the given coordinates. Restores the
22925
+ * original value if one was captured.
22926
+ * @param scene - The scene whose override registry to update.
22927
+ * @param targetType - The target type.
22928
+ * @param targetName - The target object name.
22929
+ * @param targetIndex - The target index among same-named siblings.
22930
+ * @param propertyPath - The property path to un-override.
22931
+ * @returns True if an override was removed.
22932
+ */
22933
+ function RemoveOverride(scene, targetType, targetName, targetIndex, propertyPath) {
22934
+ const manager = GetOverrideManager(scene);
22935
+ const internal = GetOverrideInternals(manager);
22936
+ const idx = FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath);
22937
+ if (idx < 0) {
22938
+ return false;
22939
+ }
22940
+ internal.overrides.splice(idx, 1);
22941
+ const origKey = MakeOriginalValueKey(targetType, targetName, targetIndex, propertyPath);
22942
+ const original = internal.originalValues.get(origKey);
22943
+ if (original !== undefined) {
22944
+ const target = ResolveTarget(manager.scene, targetType, targetName, targetIndex);
22945
+ if (target) {
22946
+ SetNestedProperty(target, propertyPath, original);
22947
+ }
22948
+ internal.originalValues.delete(origKey);
22949
+ }
22950
+ manager.onChangedObservable.notifyObservers();
22951
+ return true;
22952
+ }
22953
+ /**
22954
+ * Returns all overrides currently registered with the scene.
22955
+ * @param scene - The scene whose override registry to read.
22956
+ * @returns A read-only array of override entries.
22957
+ */
22958
+ function GetOverrides(scene) {
22959
+ return GetOverrideInternals(GetOverrideManager(scene)).overrides;
22960
+ }
22961
+ /**
22962
+ * Removes all overrides, optionally restoring original values.
22963
+ * @param scene - The scene whose override registry to clear.
22964
+ * @param restoreOriginals - If true, restores all captured original values.
22965
+ */
22966
+ function ClearOverrides(scene, restoreOriginals = false) {
22967
+ const manager = GetOverrideManager(scene);
22968
+ const internal = GetOverrideInternals(manager);
22969
+ if (restoreOriginals) {
22970
+ // Snapshot the entries so we can restore each original without firing
22971
+ // an onChangedObservable notification per entry; consumers only need
22972
+ // one signal that the registry was emptied.
22973
+ const entries = [...internal.overrides];
22974
+ internal.overrides.length = 0;
22975
+ for (const entry of entries) {
22976
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
22977
+ const original = internal.originalValues.get(origKey);
22978
+ if (original !== undefined) {
22979
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
22980
+ if (target) {
22981
+ SetNestedProperty(target, entry.propertyPath, original);
22888
22982
  }
22889
- return [...rootNodes];
22890
- },
22891
- getEntityChildren: (node) => node.getChildren(),
22892
- getEntityDisplayInfo: (node) => {
22893
- const onChangeObservable = new Observable();
22894
- const nameHookToken = watcherService.watchProperty(node, "name", () => onChangeObservable.notifyObservers());
22895
- const parentHookToken = watcherService.watchProperty(node, "parent", () => nodeMovedObservable.notifyObservers(node));
22896
- // A node is "not in the scene" if it is a Nodes-section type but is not
22897
- // a known scene node. This handles nodes that were removed from the scene
22898
- // but still appear because a descendant is in the scene. Nodes from the
22899
- // !IsNodesSectionType(parent) branch are unaffected because they always
22900
- // come from the scene's tracking lists. Clustered light children are also
22901
- // unaffected because they are added to knownSceneNodes explicitly.
22902
- const validationError = IsNodesSectionType(node) && !knownSceneNodes.has(node) ? "This entity is not in the scene but is shown because a descendant is still in the scene." : undefined;
22903
- return {
22904
- get name() {
22905
- return node.name || `Unnamed ${node.getClassName()}`;
22906
- },
22907
- validationError,
22908
- onChange: onChangeObservable,
22909
- dispose: () => {
22910
- nameHookToken.dispose();
22911
- parentHookToken.dispose();
22912
- onChangeObservable.clear();
22913
- },
22914
- };
22915
- },
22916
- entityIcon: ({ entity: node }) => node instanceof AbstractMesh ? (jsx(MeshIcon, { color: tokens.colorPaletteBlueForeground2 })) : node instanceof TransformNode ? (jsx(MyLocationRegular, { color: tokens.colorPaletteBlueForeground2 })) : node instanceof Camera ? (jsx(CameraRegular, { color: tokens.colorPaletteGreenForeground2 })) : node instanceof ClusteredLightContainer ? (jsx(BubbleMultipleRegular, { color: tokens.colorPaletteYellowForeground2 })) : node instanceof Light ? (jsx(LightbulbRegular, { color: tokens.colorPaletteYellowForeground2 })) : (jsx(Fragment, {})),
22917
- getEntityAddedObservables: () => [
22918
- scene.onNewMeshAddedObservable,
22919
- scene.onNewTransformNodeAddedObservable,
22920
- scene.onNewCameraAddedObservable,
22921
- scene.onNewLightAddedObservable,
22922
- ],
22923
- getEntityRemovedObservables: () => [
22924
- scene.onMeshRemovedObservable,
22925
- scene.onTransformNodeRemovedObservable,
22926
- scene.onCameraRemovedObservable,
22927
- scene.onLightRemovedObservable,
22928
- ],
22929
- getEntityMovedObservables: () => [nodeMovedObservable],
22930
- dragDropConfig: {
22931
- canDrag: (node) => node instanceof Node$1,
22932
- canDrop: (draggedNode, targetNode) => {
22933
- // Can't drop on self
22934
- if (targetNode === draggedNode) {
22935
- return false;
22936
- }
22937
- // Can't drop on a descendant
22938
- if (targetNode !== null && targetNode.isDescendantOf(draggedNode)) {
22939
- return false;
22940
- }
22941
- // Can drop onto section root (null) only if node has a parent
22942
- if (targetNode === null) {
22943
- return draggedNode.parent !== null;
22944
- }
22945
- return true;
22946
- },
22947
- onDrop: (draggedNode, targetNode) => {
22948
- if (draggedNode.parent === targetNode) {
22949
- return;
22950
- }
22951
- // Use setParent for TransformNodes to preserve world transform
22952
- if (draggedNode instanceof TransformNode) {
22953
- draggedNode.setParent(targetNode);
22954
- }
22955
- else {
22956
- draggedNode.parent = targetNode;
22957
- }
22958
- },
22959
- },
22960
- });
22961
- const abstractMeshBoundingBoxCommandRegistration = sceneExplorerService.addEntityCommand({
22962
- predicate: (entity) => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
22963
- order: 1000 /* DefaultCommandsOrder.MeshBoundingBox */,
22964
- getCommand: (mesh) => {
22965
- const onChangeObservable = new Observable();
22966
- const showBoundingBoxHook = watcherService.watchProperty(mesh, "showBoundingBox", () => onChangeObservable.notifyObservers());
22967
- return {
22968
- type: "toggle",
22969
- get displayName() {
22970
- return `${mesh.showBoundingBox ? "Hide" : "Show"} Bounding Box`;
22971
- },
22972
- icon: () => (mesh.showBoundingBox ? jsx(BorderOutsideRegular, {}) : jsx(BorderNoneRegular, {})),
22973
- get isEnabled() {
22974
- return mesh.showBoundingBox;
22975
- },
22976
- set isEnabled(enabled) {
22977
- mesh.showBoundingBox = enabled;
22978
- },
22979
- onChange: onChangeObservable,
22980
- dispose: () => {
22981
- showBoundingBoxHook.dispose();
22982
- onChangeObservable.clear();
22983
- },
22984
- };
22985
- },
22986
- });
22987
- const abstractMeshVisibilityCommandRegistration = sceneExplorerService.addEntityCommand({
22988
- predicate: (entity) => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
22989
- order: 1100 /* DefaultCommandsOrder.MeshVisibility */,
22990
- getCommand: (mesh) => {
22991
- const onChangeObservable = new Observable();
22992
- const isVisibleHook = watcherService.watchProperty(mesh, "isVisible", () => onChangeObservable.notifyObservers());
22993
- return {
22994
- type: "toggle",
22995
- get displayName() {
22996
- return `${mesh.isVisible ? "Hide" : "Show"} Mesh`;
22997
- },
22998
- icon: () => (mesh.isVisible ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {})),
22999
- hotKey: {
23000
- keyCode: "Space",
23001
- control: true,
23002
- },
23003
- get isEnabled() {
23004
- return !mesh.isVisible;
23005
- },
23006
- set isEnabled(enabled) {
23007
- mesh.isVisible = !enabled;
23008
- },
23009
- onChange: onChangeObservable,
23010
- dispose: () => {
23011
- isVisibleHook.dispose();
23012
- onChangeObservable.clear();
23013
- },
23014
- };
23015
- },
23016
- });
23017
- const getActiveCamera = () => (scene.frameGraph ? FindMainCamera(scene.frameGraph) : scene.activeCamera);
23018
- const activeCameraCommandRegistration = sceneExplorerService.addEntityCommand({
23019
- predicate: (entity) => entity instanceof Camera,
23020
- order: 700 /* DefaultCommandsOrder.CameraActive */,
23021
- getCommand: (camera) => {
23022
- const scene = camera.getScene();
23023
- const onChangeObservable = new Observable();
23024
- const activeCameraChangedObserver = scene.onActiveCameraChanged.add(() => {
23025
- onChangeObservable.notifyObservers();
23026
- });
23027
- return {
23028
- type: "toggle",
23029
- displayName: "Activate and Attach Controls",
23030
- icon: () => {
23031
- return getActiveCamera() === camera ? jsx(VideoFilled, {}) : jsx(VideoRegular, {});
23032
- },
23033
- get isEnabled() {
23034
- return getActiveCamera() === camera;
23035
- },
23036
- set isEnabled(enabled) {
23037
- const activeCamera = getActiveCamera();
23038
- if (enabled && activeCamera !== camera) {
23039
- if (scene.frameGraph) {
23040
- void (async (frameGraph) => {
23041
- let updated = false;
23042
- const nrg = frameGraph.getLinkedNodeRenderGraph();
23043
- if (nrg) {
23044
- updated = await nrg.replaceCameraAsync(activeCamera, camera);
23045
- }
23046
- else {
23047
- for (const task of frameGraph.tasks) {
23048
- if (IsCameraFrameGraphTask(task)) {
23049
- task.camera = camera;
23050
- updated = true;
23051
- }
23052
- }
23053
- }
23054
- if (updated) {
23055
- activeCamera?.detachControl();
23056
- camera.attachControl(true);
23057
- onChangeObservable.notifyObservers();
23058
- }
23059
- })(scene.frameGraph);
23060
- }
23061
- else {
23062
- activeCamera?.detachControl();
23063
- scene.activeCamera = camera;
23064
- camera.attachControl(true);
23065
- }
23066
- }
23067
- },
23068
- onChange: onChangeObservable,
23069
- dispose: () => {
23070
- activeCameraChangedObserver.remove();
23071
- onChangeObservable.clear();
23072
- },
23073
- };
23074
- },
23075
- });
23076
- function addGizmoCommand(nodeClass, getGizmoRef) {
23077
- return sceneExplorerService.addEntityCommand({
23078
- predicate: (entity) => entity instanceof nodeClass,
23079
- order: 800 /* DefaultCommandsOrder.GizmoActive */,
23080
- getCommand: (node) => {
23081
- const onChangeObservable = new Observable();
23082
- let gizmoRef = null;
23083
- return {
23084
- type: "toggle",
23085
- get displayName() {
23086
- return `Turn ${gizmoRef ? "Off" : "On"} Gizmo`;
23087
- },
23088
- icon: () => (gizmoRef ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {})),
23089
- get isEnabled() {
23090
- return !!gizmoRef;
23091
- },
23092
- set isEnabled(enabled) {
23093
- if (enabled) {
23094
- if (!gizmoRef) {
23095
- gizmoRef = getGizmoRef(node);
23096
- onChangeObservable.notifyObservers();
23097
- }
23098
- }
23099
- else {
23100
- if (gizmoRef) {
23101
- gizmoRef.dispose();
23102
- gizmoRef = null;
23103
- onChangeObservable.notifyObservers();
23104
- }
23105
- }
23106
- },
23107
- onChange: onChangeObservable,
23108
- dispose: () => {
23109
- onChangeObservable.clear();
23110
- },
23111
- };
23112
- },
23113
- });
22983
+ }
22984
+ }
22985
+ internal.originalValues.clear();
22986
+ manager.onChangedObservable.notifyObservers();
22987
+ return;
22988
+ }
22989
+ internal.overrides.length = 0;
22990
+ internal.originalValues.clear();
22991
+ manager.onChangedObservable.notifyObservers();
22992
+ }
22993
+ /**
22994
+ * Updates the target coordinates on the override matching a specific (type,
22995
+ * old-name, old-index) so it follows an entity rename. Used by capture services
22996
+ * to keep overrides attached to a specific object after the user renames it.
22997
+ *
22998
+ * Only the override at the exact `(targetType, oldName, oldIndex)` slot is
22999
+ * updated, so other same-named siblings keep their own overrides untouched.
23000
+ *
23001
+ * @param scene - The scene whose override registry to update.
23002
+ * @param targetType - The target type.
23003
+ * @param oldName - The previous name of the renamed entity.
23004
+ * @param oldIndex - The previous index of the renamed entity among same-named siblings.
23005
+ * @param newName - The new name of the renamed entity.
23006
+ * @param newIndex - The new index of the renamed entity among same-named siblings.
23007
+ */
23008
+ function RenameOverrideTarget(scene, targetType, oldName, oldIndex, newName, newIndex) {
23009
+ const manager = GetOverrideManager(scene);
23010
+ const internal = GetOverrideInternals(manager);
23011
+ let changed = false;
23012
+ for (let i = 0; i < internal.overrides.length; i++) {
23013
+ const entry = internal.overrides[i];
23014
+ if (entry.targetType === targetType && entry.targetName === oldName && entry.targetIndex === oldIndex) {
23015
+ internal.overrides[i] = { ...entry, targetName: newName, targetIndex: newIndex };
23016
+ changed = true;
23017
+ }
23018
+ }
23019
+ // Update original-value keys to match the new identity
23020
+ const oldPrefix = `${targetType}::${oldName}::${oldIndex}::`;
23021
+ const newPrefix = `${targetType}::${newName}::${newIndex}::`;
23022
+ for (const [origKey, value] of Array.from(internal.originalValues.entries())) {
23023
+ if (origKey.startsWith(oldPrefix)) {
23024
+ const propertyPath = origKey.substring(oldPrefix.length);
23025
+ internal.originalValues.set(newPrefix + propertyPath, value);
23026
+ internal.originalValues.delete(origKey);
23027
+ }
23028
+ }
23029
+ if (changed) {
23030
+ manager.onChangedObservable.notifyObservers();
23031
+ }
23032
+ }
23033
+ /**
23034
+ * Rewrites override *values* that reference an entity by name when that entity
23035
+ * has been renamed. Mirrors {@link RenameOverrideTarget} but operates on the
23036
+ * `value` field rather than the `targetName` field, so overrides whose value
23037
+ * is `"ref:oldName"` (material/light/camera references) or `"texture:oldName"`
23038
+ * (non-SmartAsset texture references) follow the rename instead of silently
23039
+ * pointing at a non-existent entity.
23040
+ *
23041
+ * SmartAsset texture references (`"samTexture:<key>"`) are unaffected because
23042
+ * the SmartAsset key is decoupled from the texture's runtime `name` field.
23043
+ *
23044
+ * @param scene - The scene whose override registry to update.
23045
+ * @param valueScheme - Which encoded-reference prefix to rewrite: `"ref"` for
23046
+ * material/light/camera references, `"texture"` for non-SAM textures.
23047
+ * @param oldName - The previous name embedded in the reference.
23048
+ * @param newName - The new name embedded in the reference.
23049
+ */
23050
+ function RenameOverrideValueReferences(scene, valueScheme, oldName, newName) {
23051
+ if (oldName === newName) {
23052
+ return;
23053
+ }
23054
+ const manager = GetOverrideManager(scene);
23055
+ const internal = GetOverrideInternals(manager);
23056
+ const oldValue = `${valueScheme}:${oldName}`;
23057
+ const newValue = `${valueScheme}:${newName}`;
23058
+ let changed = false;
23059
+ for (let i = 0; i < internal.overrides.length; i++) {
23060
+ const entry = internal.overrides[i];
23061
+ if (entry.value === oldValue) {
23062
+ internal.overrides[i] = { ...entry, value: newValue };
23063
+ changed = true;
23064
+ }
23065
+ }
23066
+ if (changed) {
23067
+ manager.onChangedObservable.notifyObservers();
23068
+ }
23069
+ }
23070
+ // ── Application ──
23071
+ /**
23072
+ * Applies all overrides to their current targets in the scene.
23073
+ *
23074
+ * Call this after any scene mutation that might have invalidated previously
23075
+ * applied state (asset reload, object recreation, project load). The override
23076
+ * manager does not auto-subscribe to other scene subsystems — coordination is
23077
+ * the caller's responsibility, which keeps the override system independent.
23078
+ * @param scene - The scene whose overrides to apply.
23079
+ */
23080
+ function ApplyAllOverrides(scene) {
23081
+ const manager = GetOverrideManager(scene);
23082
+ const internal = GetOverrideInternals(manager);
23083
+ for (const entry of SortNameOverridesFirst(internal.overrides)) {
23084
+ // Apply each override in isolation: a single failing entry (e.g. a
23085
+ // stale property path, or a target whose shape changed) must not abort
23086
+ // the remaining overrides.
23087
+ try {
23088
+ ApplyOverrideEntry(manager, internal, entry);
23089
+ }
23090
+ catch (e) {
23091
+ Logger.Warn(`OverrideManager: failed to apply override type="${entry.targetType}" name="${entry.targetName}" prop="${entry.propertyPath}": ${e}`);
23092
+ }
23093
+ }
23094
+ }
23095
+ /**
23096
+ * Returns a copy of the entries ordered so that `name` overrides are applied
23097
+ * first. Renames must run before other overrides because targets (and
23098
+ * reference values like `ref:<name>`) are resolved by name: applying a rename
23099
+ * first ensures subsequent overrides resolve against the new names.
23100
+ * @param entries - The override entries to order.
23101
+ * @returns A new array with `name` overrides first, otherwise stable order.
23102
+ */
23103
+ function SortNameOverridesFirst(entries) {
23104
+ const nameOverrides = [];
23105
+ const rest = [];
23106
+ for (const entry of entries) {
23107
+ if (entry.propertyPath === "name") {
23108
+ nameOverrides.push(entry);
23109
+ }
23110
+ else {
23111
+ rest.push(entry);
23112
+ }
23113
+ }
23114
+ return [...nameOverrides, ...rest];
23115
+ }
23116
+ // ── Serialization ──
23117
+ /**
23118
+ * Serializes all overrides to a JSON-compatible array.
23119
+ * The on-disk shape is identical to the in-memory `IOverrideEntry`.
23120
+ * @param scene - The scene whose overrides to serialize.
23121
+ * @returns An array of override entries (shallow copies).
23122
+ */
23123
+ function SerializeOverrides(scene) {
23124
+ const internal = GetOverrideInternals(GetOverrideManager(scene));
23125
+ return internal.overrides.map((o) => ({ ...o }));
23126
+ }
23127
+ /**
23128
+ * Loads overrides from a serialized array and applies them.
23129
+ * @param scene - The scene whose override registry to populate.
23130
+ * @param data - Array of override entries.
23131
+ */
23132
+ function DeserializeAndApplyOverrides(scene, data) {
23133
+ if (!Array.isArray(data)) {
23134
+ throw new Error("OverrideManager: Expected an array of override entries.");
23135
+ }
23136
+ for (const entry of SortNameOverridesFirst(data)) {
23137
+ if (!entry.targetType || entry.targetName === undefined || typeof entry.targetIndex !== "number" || !entry.propertyPath || entry.value === undefined) {
23138
+ Logger.Warn("OverrideManager: Skipping invalid override entry.");
23139
+ continue;
23140
+ }
23141
+ AddOverride(scene, entry);
23142
+ }
23143
+ }
23144
+ // ── Lifecycle ──
23145
+ /**
23146
+ * Disposes the manager, clearing all overrides and detaching it from its scene.
23147
+ * Safe to call multiple times; subsequent calls are no-ops. Automatically invoked when the
23148
+ * owning scene is disposed.
23149
+ * @param manager - The override manager state.
23150
+ */
23151
+ function DisposeOverrideManager(manager) {
23152
+ const internal = OverrideManagerInternals.get(manager);
23153
+ if (!internal) {
23154
+ return;
23155
+ }
23156
+ OverrideManagerInternals.delete(manager);
23157
+ if (internal.sceneDisposeObserver) {
23158
+ manager.scene.onDisposeObservable.remove(internal.sceneDisposeObserver);
23159
+ internal.sceneDisposeObserver = null;
23160
+ }
23161
+ internal.overrides.length = 0;
23162
+ internal.originalValues.clear();
23163
+ manager.onChangedObservable.clear();
23164
+ if (manager.scene.metadata) {
23165
+ delete manager.scene.metadata[OverrideManagerKey];
23166
+ }
23167
+ }
23168
+ // ── Private ──
23169
+ function GetOverrideInternals(manager) {
23170
+ const internal = OverrideManagerInternals.get(manager);
23171
+ if (!internal) {
23172
+ throw new Error("OverrideManager: Unknown manager state.");
23173
+ }
23174
+ return internal;
23175
+ }
23176
+ /**
23177
+ * Applies a single override entry to its target, capturing the original value
23178
+ * on the first application so {@link RemoveOverride} can restore it later.
23179
+ * @param manager - The override manager owning the entry.
23180
+ * @param internal - The manager's internal state.
23181
+ * @param entry - The override to apply.
23182
+ */
23183
+ function ApplyOverrideEntry(manager, internal, entry) {
23184
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
23185
+ if (!target) {
23186
+ Logger.Warn(`OverrideManager: target not found for type="${entry.targetType}" name="${entry.targetName}" index=${entry.targetIndex} prop="${entry.propertyPath}"`);
23187
+ return; // Target not loaded yet — override will be applied on next ApplyAllOverrides
23188
+ }
23189
+ // Capture original value before first override
23190
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23191
+ if (!internal.originalValues.has(origKey)) {
23192
+ const currentValue = GetNestedProperty(target, entry.propertyPath);
23193
+ if (currentValue !== undefined) {
23194
+ internal.originalValues.set(origKey, CloneValue(currentValue));
23195
+ }
23196
+ }
23197
+ const resolvedValue = ResolveOverrideValue(manager.scene, entry.value);
23198
+ SetNestedProperty(target, entry.propertyPath, resolvedValue);
23199
+ }
23200
+ /**
23201
+ * Locates a scene object by (targetType, targetName, targetIndex). The scene
23202
+ * collection is filtered to objects matching `targetName`; the N-th survivor
23203
+ * (per `targetIndex`) is returned. Falls back to the first match if the index
23204
+ * is out of range — useful when the scene shape has changed since capture.
23205
+ * @param scene - The scene to search.
23206
+ * @param targetType - The override target type.
23207
+ * @param targetName - The target object name (or "" for scene-level).
23208
+ * @param targetIndex - The target's position among same-named siblings.
23209
+ * @returns The matching scene object, or null if not found.
23210
+ */
23211
+ function ResolveTarget(scene, targetType, targetName, targetIndex) {
23212
+ // Scene-level overrides target the scene itself
23213
+ if (targetType === "scene") {
23214
+ return scene;
23215
+ }
23216
+ const collection = GetCollection$1(scene, targetType);
23217
+ if (!collection) {
23218
+ return null;
23219
+ }
23220
+ const matches = collection.filter((obj) => obj.name === targetName);
23221
+ if (matches.length === 0) {
23222
+ return null;
23223
+ }
23224
+ return (matches[targetIndex] ?? matches[0]);
23225
+ }
23226
+ /**
23227
+ * Returns the scene collection corresponding to an override target type.
23228
+ * @param scene - The scene to inspect.
23229
+ * @param targetType - The target type.
23230
+ * @returns The collection, or null if the type has no collection.
23231
+ */
23232
+ function GetCollection$1(scene, targetType) {
23233
+ switch (targetType) {
23234
+ case "meshes":
23235
+ return scene.meshes;
23236
+ case "transformNodes":
23237
+ return scene.transformNodes;
23238
+ case "materials":
23239
+ return scene.materials;
23240
+ case "textures":
23241
+ return scene.textures;
23242
+ case "lights":
23243
+ return scene.lights;
23244
+ case "cameras":
23245
+ return scene.cameras;
23246
+ case "animationGroups":
23247
+ return scene.animationGroups;
23248
+ default:
23249
+ return null;
23250
+ }
23251
+ }
23252
+ /**
23253
+ * Resolves an override value, expanding string references like "ref:name",
23254
+ * "samTexture:key", or "texture:name" into the actual scene object they refer to.
23255
+ * @param scene - The scene used to look up references.
23256
+ * @param value - The serialized override value.
23257
+ * @returns The runtime value to assign to the target property.
23258
+ */
23259
+ function ResolveOverrideValue(scene, value) {
23260
+ if (typeof value === "string") {
23261
+ if (value.startsWith("ref:")) {
23262
+ return ResolveObjectReference(scene, value.substring(4));
23263
+ }
23264
+ if (value.startsWith("samTexture:")) {
23265
+ return ResolveSamTextureReference(scene, value.substring(11));
23266
+ }
23267
+ if (value.startsWith("texture:")) {
23268
+ return ResolveTextureReference(scene, value.substring(8));
23269
+ }
23270
+ }
23271
+ // Number arrays are passed through as-is. SetNestedProperty will use
23272
+ // the live target's `fromArray` method (Vector3, Color3, etc.) to push
23273
+ // values in-place, preserving the math instance identity.
23274
+ return value;
23275
+ }
23276
+ /**
23277
+ * Resolves a "ref:name" value by looking up a scene object by name. Materials,
23278
+ * lights, and cameras are searched first, then transform nodes and meshes
23279
+ * (both valid parent references).
23280
+ * @param scene - The scene to search.
23281
+ * @param name - The object name to resolve.
23282
+ * @returns The matching object, or undefined if not found.
23283
+ */
23284
+ function ResolveObjectReference(scene, name) {
23285
+ const collections = [scene.materials, scene.lights, scene.cameras, scene.transformNodes, scene.meshes];
23286
+ for (const collection of collections) {
23287
+ const match = collection.find((item) => item.name === name);
23288
+ if (match) {
23289
+ return match;
23290
+ }
23291
+ }
23292
+ Logger.Warn(`OverrideManager: Object reference "${name}" not found in scene.`);
23293
+ return undefined;
23294
+ }
23295
+ /**
23296
+ * Resolves a "texture:name" value by looking up a texture in the scene by name.
23297
+ * @param scene - The scene to search.
23298
+ * @param name - The texture name to resolve.
23299
+ * @returns The matching texture, or undefined if not found.
23300
+ */
23301
+ function ResolveTextureReference(scene, name) {
23302
+ const tex = scene.textures.find((t) => t.name === name);
23303
+ if (tex) {
23304
+ return tex;
23305
+ }
23306
+ Logger.Warn(`OverrideManager: Texture reference "${name}" not found.`);
23307
+ return undefined;
23308
+ }
23309
+ /**
23310
+ * Resolves a "samTexture:key" value by looking up a SmartAsset-tracked texture
23311
+ * by its registry key. The SAM key is stable across save/load whereas the
23312
+ * texture's `name` (for SAM textures, the blob URL) changes on every reload,
23313
+ * so this is the only reliable way to round-trip texture references on
23314
+ * user-uploaded SmartAsset textures.
23315
+ * @param scene - The scene to search.
23316
+ * @param key - The SmartAsset key to resolve.
23317
+ * @returns The matching texture, or undefined if not found.
23318
+ */
23319
+ function ResolveSamTextureReference(scene, key) {
23320
+ const tex = scene.textures.find((t) => FindSmartAssetKeyForObject(scene, t) === key);
23321
+ if (tex) {
23322
+ return tex;
23323
+ }
23324
+ Logger.Warn(`OverrideManager: SmartAsset texture "${key}" not found.`);
23325
+ return undefined;
23326
+ }
23327
+ /**
23328
+ * Finds the index of an override matching the given coordinates.
23329
+ * @param internal - The manager's internal state.
23330
+ * @param targetType - The target type.
23331
+ * @param targetName - The target object name.
23332
+ * @param targetIndex - The target index among same-named siblings.
23333
+ * @param propertyPath - The property path.
23334
+ * @returns The matching index, or -1 if none found.
23335
+ */
23336
+ function FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath) {
23337
+ return internal.overrides.findIndex((o) => o.targetType === targetType && o.targetName === targetName && o.targetIndex === targetIndex && o.propertyPath === propertyPath);
23338
+ }
23339
+ /**
23340
+ * Removes any existing override that matches the given coordinates. Used by
23341
+ * {@link AddOverride} to enforce one entry per (type, name, index, property).
23342
+ * @param internal - The manager's internal state.
23343
+ * @param targetType - The target type.
23344
+ * @param targetName - The target object name.
23345
+ * @param targetIndex - The target index among same-named siblings.
23346
+ * @param propertyPath - The property path.
23347
+ */
23348
+ function RemoveMatchingOverride(internal, targetType, targetName, targetIndex, propertyPath) {
23349
+ const idx = FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath);
23350
+ if (idx >= 0) {
23351
+ internal.overrides.splice(idx, 1);
23352
+ }
23353
+ }
23354
+ /**
23355
+ * Creates a unique key for storing original values.
23356
+ * @param targetType - The override target type.
23357
+ * @param targetName - The target object name.
23358
+ * @param targetIndex - The target index among same-named siblings.
23359
+ * @param propertyPath - The property path.
23360
+ * @returns A composite string key uniquely identifying the original value slot.
23361
+ */
23362
+ function MakeOriginalValueKey(targetType, targetName, targetIndex, propertyPath) {
23363
+ return `${targetType}::${targetName}::${targetIndex}::${propertyPath}`;
23364
+ }
23365
+ /**
23366
+ * Gets a nested property from an object using a dot-separated path.
23367
+ * @param obj - The root object to traverse.
23368
+ * @param path - The dot-separated property path.
23369
+ * @returns The value at the path, or undefined if any segment is missing.
23370
+ */
23371
+ function GetNestedProperty(obj, path) {
23372
+ const parts = path.split(".");
23373
+ let current = obj;
23374
+ for (const part of parts) {
23375
+ if (current === null || current === undefined || typeof current !== "object") {
23376
+ return undefined;
23377
+ }
23378
+ current = current[part];
23379
+ }
23380
+ return current;
23381
+ }
23382
+ /**
23383
+ * Sets a nested property on an object using a dot-separated path.
23384
+ *
23385
+ * When the value is a number array and the existing property is a Babylon
23386
+ * math type (Vector*, Quaternion, Color3/4, Matrix), uses the math type's
23387
+ * `fromArray` method to mutate it in place — preserving the live instance
23388
+ * identity that consumers may already hold references to. Otherwise falls
23389
+ * back to direct property replacement.
23390
+ *
23391
+ * Getter-only accessors (e.g. `Texture.invertY`) cannot be assigned directly —
23392
+ * doing so throws in strict mode. For those, the backing field (`_<name>`) is
23393
+ * written instead when present, and load-time texture properties are re-applied
23394
+ * via a re-upload (see {@link ReapplyLoadTimeTextureProperty}). Truly read-only
23395
+ * properties with no backing field are skipped with a warning.
23396
+ * @param obj - The root object to mutate.
23397
+ * @param path - The dot-separated property path.
23398
+ * @param value - The new value to assign.
23399
+ */
23400
+ function SetNestedProperty(obj, path, value) {
23401
+ const parts = path.split(".");
23402
+ let current = obj;
23403
+ for (let i = 0; i < parts.length - 1; i++) {
23404
+ if (current === null || current === undefined || typeof current !== "object") {
23405
+ return;
23406
+ }
23407
+ current = current[parts[i]];
23408
+ }
23409
+ if (current === null || current === undefined || typeof current !== "object") {
23410
+ return;
23411
+ }
23412
+ const target = current;
23413
+ const lastPart = parts[parts.length - 1];
23414
+ const existing = target[lastPart];
23415
+ if (Array.isArray(value) && existing && typeof existing === "object" && typeof existing.fromArray === "function") {
23416
+ existing.fromArray(value);
23417
+ return;
23418
+ }
23419
+ // Guard against getter-only accessors: assigning to one throws in strict
23420
+ // mode (ES modules are strict), which would otherwise abort the whole apply
23421
+ // batch. Write the backing field instead, then re-apply if it is a
23422
+ // load-time texture property.
23423
+ const accessor = FindAccessorDescriptor(target, lastPart);
23424
+ if (accessor && accessor.get && !accessor.set) {
23425
+ const backingField = `_${lastPart}`;
23426
+ if (backingField in target) {
23427
+ target[backingField] = value;
23428
+ ReapplyLoadTimeTextureProperty(target, lastPart);
23429
+ }
23430
+ else {
23431
+ Logger.Warn(`OverrideManager: property "${lastPart}" is read-only and has no backing field; skipping.`);
23432
+ }
23433
+ return;
23434
+ }
23435
+ target[lastPart] = value;
23436
+ }
23437
+ /**
23438
+ * Walks the prototype chain to find the property descriptor for `key`, so
23439
+ * accessor (getter/setter) properties defined on a class prototype can be
23440
+ * detected. Returns undefined if the property is a plain data field.
23441
+ * @param obj - The object to inspect.
23442
+ * @param key - The property name.
23443
+ * @returns The property descriptor, or undefined if none is found.
23444
+ */
23445
+ function FindAccessorDescriptor(obj, key) {
23446
+ let proto = obj;
23447
+ while (proto) {
23448
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
23449
+ if (descriptor) {
23450
+ return descriptor;
23451
+ }
23452
+ proto = Object.getPrototypeOf(proto);
23453
+ }
23454
+ return undefined;
23455
+ }
23456
+ /**
23457
+ * Re-applies a texture property that only takes effect at upload time (such as
23458
+ * `invertY`, which is baked into the GPU texel data via `UNPACK_FLIP_Y`).
23459
+ * Writing the backing field alone does not re-flip an already-loaded texture,
23460
+ * so this re-uploads the source via `updateURL`, which releases only the GPU
23461
+ * texture and reuses the same texture instance (all scene references stay
23462
+ * intact). Duck-typed so this module stays decoupled from the Texture class,
23463
+ * and a no-op for anything that is not a URL-loaded texture.
23464
+ * @param target - The object whose property was written.
23465
+ * @param propertyName - The property that was written.
23466
+ */
23467
+ function ReapplyLoadTimeTextureProperty(target, propertyName) {
23468
+ if (propertyName !== "invertY") {
23469
+ return;
23470
+ }
23471
+ if (typeof target.updateURL === "function" && typeof target.url === "string" && target.url) {
23472
+ try {
23473
+ target.updateURL(target.url, target._buffer ?? null);
23114
23474
  }
23115
- const cameraGizmoCommandRegistration = addGizmoCommand(Camera, gizmoService.getCameraGizmo.bind(gizmoService));
23116
- const lightEnabledCommandRegistration = sceneExplorerService.addEntityCommand({
23117
- predicate: (entity) => entity instanceof Light,
23118
- order: 700 /* DefaultCommandsOrder.LightActive */,
23119
- getCommand: (light) => {
23120
- return {
23121
- type: "toggle",
23122
- get displayName() {
23123
- return `Turn Light ${light.isEnabled() ? "Off" : "On"}`;
23124
- },
23125
- icon: () => (light.isEnabled() ? jsx(FlashlightRegular, {}) : jsx(FlashlightOffRegular, {})),
23126
- get isEnabled() {
23127
- return !light.isEnabled();
23128
- },
23129
- set isEnabled(enabled) {
23130
- light.setEnabled(!enabled);
23131
- },
23132
- onChange: light.onEnabledStateChangedObservable,
23133
- };
23134
- },
23135
- });
23136
- const lightGizmoCommandRegistration = addGizmoCommand(Light, gizmoService.getLightGizmo.bind(gizmoService));
23137
- const editNodeGeometryCommandRegistration = sceneExplorerService.addEntityCommand({
23138
- predicate: (entity) => entity instanceof Mesh && !!GetNodeGeometry(entity),
23139
- order: 1100 /* DefaultCommandsOrder.EditNodeGeometry */,
23140
- getCommand: (mesh) => {
23141
- return {
23142
- type: "action",
23143
- displayName: "Edit in Node Geometry Editor",
23144
- icon: () => jsx(EditRegular, {}),
23145
- // eslint-disable-next-line @typescript-eslint/naming-convention
23146
- execute: async () => {
23147
- const nodeGeometry = GetNodeGeometry(mesh);
23148
- if (nodeGeometry) {
23149
- await EditNodeGeometry(nodeGeometry, mesh.getScene());
23150
- }
23151
- },
23152
- };
23153
- },
23154
- });
23155
- return {
23156
- dispose: () => {
23157
- sectionRegistration.dispose();
23158
- abstractMeshBoundingBoxCommandRegistration.dispose();
23159
- abstractMeshVisibilityCommandRegistration.dispose();
23160
- activeCameraCommandRegistration.dispose();
23161
- cameraGizmoCommandRegistration.dispose();
23162
- lightEnabledCommandRegistration.dispose();
23163
- lightGizmoCommandRegistration.dispose();
23164
- editNodeGeometryCommandRegistration.dispose();
23165
- },
23166
- };
23167
- },
23168
- };
23475
+ catch (e) {
23476
+ Logger.Warn(`OverrideManager: failed to re-upload texture to apply "${propertyName}": ${e}`);
23477
+ }
23478
+ }
23479
+ }
23480
+ /**
23481
+ * Snapshots a value for original-value tracking.
23482
+ *
23483
+ * Scene entities (textures, materials, meshes, etc.) are stored by reference
23484
+ * because cloning them would register unwanted duplicates in the scene.
23485
+ * Plain math types (Vector3, Color3, etc.) are cloned so mutations to the
23486
+ * live object don't corrupt the saved original.
23487
+ * @param value - The value to snapshot.
23488
+ * @returns The snapshot value (cloned for plain math types, by reference for entities).
23489
+ */
23490
+ function CloneValue(value) {
23491
+ if (value === null || value === undefined) {
23492
+ return value;
23493
+ }
23494
+ if (typeof value !== "object") {
23495
+ return value;
23496
+ }
23497
+ if (typeof value.getScene === "function") {
23498
+ return value;
23499
+ }
23500
+ if ("clone" in value && typeof value.clone === "function") {
23501
+ return value.clone();
23502
+ }
23503
+ return { ...value };
23504
+ }
23169
23505
 
23170
- const ParticleSystemExplorerServiceDefinition = {
23171
- friendlyName: "Particle System Explorer",
23172
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23173
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23174
- const scene = sceneContext.currentScene;
23175
- if (!scene) {
23176
- return undefined;
23506
+ /**
23507
+ * Creates a fresh capture state. One is created per attached scene so identities
23508
+ * never leak across scenes.
23509
+ * @returns A new, empty capture state.
23510
+ */
23511
+ function CreateCaptureState() {
23512
+ return { previousIdentity: new WeakMap(), originalIdentity: new WeakMap() };
23513
+ }
23514
+ /**
23515
+ * Handles a single Inspector property change, recording the corresponding
23516
+ * override. Name changes are handled specially so renames persist and other
23517
+ * overrides follow the rename.
23518
+ * @param scene - The active scene.
23519
+ * @param state - The capture state for this scene.
23520
+ * @param change - The captured property change.
23521
+ */
23522
+ function HandleCapturedPropertyChange(scene, state, change) {
23523
+ const { entity, propertyKey, oldValue, newValue } = change;
23524
+ if (propertyKey === "name" && typeof newValue === "string") {
23525
+ HandleRename(scene, state, entity, oldValue, newValue);
23526
+ return;
23527
+ }
23528
+ if (propertyKey === "id") {
23529
+ return;
23530
+ }
23531
+ RecordEntityPropertyOverride(scene, entity, String(propertyKey), newValue, oldValue, state);
23532
+ }
23533
+ /**
23534
+ * Records (or replaces) an override for a single property edit on a scene
23535
+ * entity. Also usable directly by non-Inspector edit paths (e.g. scene-explorer
23536
+ * commands like the visibility eye toggle, or drag-to-reparent) that mutate an
23537
+ * entity without going through the Inspector's property change pipeline.
23538
+ *
23539
+ * The caller is expected to have already applied `newValue` to the entity;
23540
+ * `oldValue` is recorded as the pre-edit original so the override can be
23541
+ * reverted later.
23542
+ * @param scene - The active scene.
23543
+ * @param entity - The mutated entity.
23544
+ * @param propertyPath - The dot-separated property path that changed.
23545
+ * @param newValue - The new (already-applied) value.
23546
+ * @param oldValue - The pre-edit value, recorded as the override's original.
23547
+ * @param state - Optional capture state; when provided, entity identities are
23548
+ * seeded so subsequent renames can be tracked.
23549
+ */
23550
+ function RecordEntityPropertyOverride(scene, entity, propertyPath, newValue, oldValue, state) {
23551
+ let targetType = ClassifyEntity(entity, scene);
23552
+ let targetName;
23553
+ let targetIndex;
23554
+ let finalPath = propertyPath;
23555
+ if (targetType !== null) {
23556
+ if (targetType === "scene") {
23557
+ targetName = "";
23558
+ targetIndex = 0;
23559
+ }
23560
+ else {
23561
+ targetName = GetEntityName(entity);
23562
+ targetIndex = ComputeTargetIndex(scene, targetType, entity, targetName);
23563
+ if (state && targetName) {
23564
+ SeedIdentity(state, entity, targetName, targetIndex);
23565
+ }
23566
+ }
23567
+ }
23568
+ else {
23569
+ // Sub-object: check if this is a property of a known parent
23570
+ const parentInfo = FindParentEntity(entity, scene);
23571
+ if (!parentInfo) {
23572
+ return;
23573
+ }
23574
+ targetType = parentInfo.targetType;
23575
+ targetName = parentInfo.targetName;
23576
+ targetIndex = parentInfo.targetIndex;
23577
+ finalPath = `${parentInfo.parentProperty}.${propertyPath}`;
23578
+ }
23579
+ const serializedValue = SerializeOverrideValueForCapture(newValue, scene);
23580
+ if (serializedValue === undefined) {
23581
+ return;
23582
+ }
23583
+ // The caller has already written `newValue` to the entity, so pass
23584
+ // `oldValue` so the manager records the true pre-edit value.
23585
+ AddOverride(scene, { targetType, targetName, targetIndex, propertyPath: finalPath, value: serializedValue }, { originalValue: oldValue });
23586
+ }
23587
+ /**
23588
+ * Handles an entity rename. Records the rename as a `name` override keyed on the
23589
+ * entity's original (first-seen) name so it round-trips through a project
23590
+ * reload, and moves any other overrides / value references to follow the new
23591
+ * current name.
23592
+ * @param scene - The active scene.
23593
+ * @param state - The capture state for this scene.
23594
+ * @param entity - The renamed entity.
23595
+ * @param oldValue - The name before the rename.
23596
+ * @param newValue - The name after the rename.
23597
+ */
23598
+ function HandleRename(scene, state, entity, oldValue, newValue) {
23599
+ const targetType = ClassifyEntity(entity, scene);
23600
+ if (targetType === null || targetType === "scene") {
23601
+ return;
23602
+ }
23603
+ const entityObject = entity;
23604
+ const previous = state.previousIdentity.get(entityObject);
23605
+ const oldName = typeof oldValue === "string" ? oldValue : previous?.name;
23606
+ if (oldName !== undefined && oldName !== newValue) {
23607
+ // The entity already has the new name, so compute its new index among same-named siblings.
23608
+ const newIndex = ComputeTargetIndex(scene, targetType, entity, newValue);
23609
+ // Move existing overrides (and value references) to follow the current name.
23610
+ if (previous) {
23611
+ RenameOverrideTarget(scene, targetType, previous.name, previous.index, newValue, newIndex);
23612
+ const valueScheme = targetType === "textures" ? "texture" : "ref";
23613
+ RenameOverrideValueReferences(scene, valueScheme, previous.name, newValue);
23614
+ }
23615
+ // Record the rename as an override keyed on the entity's ORIGINAL name.
23616
+ let original = state.originalIdentity.get(entityObject);
23617
+ if (!original) {
23618
+ // First time we've seen this entity, and its first observed edit is a rename, so its
23619
+ // pre-rename index among same-named siblings can't be read directly (it already carries
23620
+ // `newValue`). Fall back to the count of siblings that still hold `oldName`, which places
23621
+ // the entity last in that group — correct for the common case where duplicate names come
23622
+ // from append-ordered imports (e.g. re-importing a glTF) and the renamed one was added
23623
+ // last. When the entity was seen before its rename, `previous.index` is exact.
23624
+ const originalIndex = previous?.index ?? CountEntitiesWithName(scene, targetType, oldName);
23625
+ original = { name: oldName, index: originalIndex };
23626
+ state.originalIdentity.set(entityObject, original);
23627
+ }
23628
+ if (original.name !== newValue) {
23629
+ AddOverride(scene, { targetType, targetName: original.name, targetIndex: original.index, propertyPath: "name", value: newValue }, { originalValue: original.name });
23630
+ }
23631
+ else {
23632
+ // Renamed back to the original name — the name override is now a no-op.
23633
+ RemoveOverride(scene, targetType, original.name, original.index, "name");
23634
+ }
23635
+ }
23636
+ state.previousIdentity.set(entityObject, { name: newValue, index: ComputeTargetIndex(scene, targetType, entity, newValue) });
23637
+ }
23638
+ /**
23639
+ * Seeds both identity maps for an entity on first contact.
23640
+ * @param state - The capture state.
23641
+ * @param entity - The entity to seed.
23642
+ * @param name - The entity's current name.
23643
+ * @param index - The entity's index among same-named siblings.
23644
+ */
23645
+ function SeedIdentity(state, entity, name, index) {
23646
+ if (!state.previousIdentity.has(entity)) {
23647
+ state.previousIdentity.set(entity, { name, index });
23648
+ }
23649
+ if (!state.originalIdentity.has(entity)) {
23650
+ state.originalIdentity.set(entity, { name, index });
23651
+ }
23652
+ }
23653
+ /**
23654
+ * Classifies an entity into an OverrideTargetType by membership in the
23655
+ * scene's standard collections (or by being the scene itself).
23656
+ * @param entity - The entity to classify.
23657
+ * @param scene - The scene to check collections against.
23658
+ * @returns The target type, or null if unrecognized.
23659
+ */
23660
+ function ClassifyEntity(entity, scene) {
23661
+ if (entity === scene) {
23662
+ return "scene";
23663
+ }
23664
+ const obj = entity;
23665
+ if (!obj || typeof obj !== "object") {
23666
+ return null;
23667
+ }
23668
+ if (scene.materials.includes(obj)) {
23669
+ return "materials";
23670
+ }
23671
+ if (scene.meshes.includes(obj)) {
23672
+ return "meshes";
23673
+ }
23674
+ // Pure TransformNodes (not meshes) are a separate scene collection and are common in glTF
23675
+ // imports (pivots, empties, armature roots). Meshes are never added to scene.transformNodes,
23676
+ // so this check is disjoint from the meshes check above.
23677
+ if (scene.transformNodes.includes(obj)) {
23678
+ return "transformNodes";
23679
+ }
23680
+ if (scene.lights.includes(obj)) {
23681
+ return "lights";
23682
+ }
23683
+ if (scene.cameras.includes(obj)) {
23684
+ return "cameras";
23685
+ }
23686
+ if (scene.textures.includes(obj)) {
23687
+ return "textures";
23688
+ }
23689
+ if (scene.animationGroups.includes(obj)) {
23690
+ return "animationGroups";
23691
+ }
23692
+ return null;
23693
+ }
23694
+ /**
23695
+ * Gets the name of a scene entity.
23696
+ * @param entity - The entity to get the name from.
23697
+ * @returns The entity name, or an empty string if unavailable.
23698
+ */
23699
+ function GetEntityName(entity) {
23700
+ const obj = entity;
23701
+ return obj?.name ?? "";
23702
+ }
23703
+ /**
23704
+ * Returns the position of `entity` among scene[targetType] objects with the
23705
+ * same name. Used so overrides can disambiguate same-named siblings.
23706
+ * @param scene - The scene to inspect.
23707
+ * @param targetType - The target type / collection name.
23708
+ * @param entity - The entity to locate.
23709
+ * @param name - The name to filter by.
23710
+ * @returns The index within the same-name filter, or 0 if not found.
23711
+ */
23712
+ function ComputeTargetIndex(scene, targetType, entity, name) {
23713
+ const collection = GetCollection(scene, targetType);
23714
+ if (!collection) {
23715
+ return 0;
23716
+ }
23717
+ const sameName = collection.filter((obj) => obj.name === name);
23718
+ const idx = sameName.indexOf(entity);
23719
+ return idx >= 0 ? idx : 0;
23720
+ }
23721
+ /**
23722
+ * Counts how many entities in scene[targetType] currently have the given name.
23723
+ * Used as a best-effort original index when an entity's first observed edit is
23724
+ * a rename (its pre-rename position can no longer be read directly).
23725
+ * @param scene - The scene to inspect.
23726
+ * @param targetType - The target type / collection name.
23727
+ * @param name - The name to count.
23728
+ * @returns The number of same-named entities, or 0 if the collection is unknown.
23729
+ */
23730
+ function CountEntitiesWithName(scene, targetType, name) {
23731
+ const collection = GetCollection(scene, targetType);
23732
+ if (!collection) {
23733
+ return 0;
23734
+ }
23735
+ return collection.filter((obj) => obj.name === name).length;
23736
+ }
23737
+ /**
23738
+ * Returns the scene collection matching a target type.
23739
+ * @param scene - The scene to inspect.
23740
+ * @param targetType - The target type.
23741
+ * @returns The collection, or null if `targetType` doesn't map to one.
23742
+ */
23743
+ function GetCollection(scene, targetType) {
23744
+ switch (targetType) {
23745
+ case "meshes":
23746
+ return scene.meshes;
23747
+ case "transformNodes":
23748
+ return scene.transformNodes;
23749
+ case "materials":
23750
+ return scene.materials;
23751
+ case "textures":
23752
+ return scene.textures;
23753
+ case "lights":
23754
+ return scene.lights;
23755
+ case "cameras":
23756
+ return scene.cameras;
23757
+ case "animationGroups":
23758
+ return scene.animationGroups;
23759
+ default:
23760
+ return null;
23761
+ }
23762
+ }
23763
+ /**
23764
+ * Serializes a property value into an OverrideValue.
23765
+ * Returns undefined for unsupported types.
23766
+ * @param value - The value to serialize.
23767
+ * @param scene - Optional scene for resolving object references.
23768
+ * @returns The serialized value, or undefined if unsupported.
23769
+ */
23770
+ function SerializeOverrideValueForCapture(value, scene) {
23771
+ // null is a legitimate override value (e.g. clearing a material slot or a
23772
+ // parent) and must round-trip as null — substituting "" here would silently
23773
+ // corrupt object-typed slots with an empty string on reload.
23774
+ if (value === null) {
23775
+ return null;
23776
+ }
23777
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
23778
+ return value;
23779
+ }
23780
+ // Node reference (parent, etc.) → "ref:nodeName". Checked before the math
23781
+ // heuristics below because a Node is an object without top-level x/y/z.
23782
+ if (value instanceof Node$1) {
23783
+ return `ref:${value.name}`;
23784
+ }
23785
+ // Material reference → "ref:materialName"
23786
+ if (value && typeof value === "object" && "getClassName" in value && typeof value.getClassName === "function") {
23787
+ const className = value.getClassName();
23788
+ if (className.includes("Material") || className.includes("material")) {
23789
+ return `ref:${value.name}`;
23790
+ }
23791
+ }
23792
+ // Texture reference → "samTexture:<key>" if SmartAsset-tracked, else "texture:<name>".
23793
+ // The SAM key is stable across save/load; `texture.name` for a SAM-tracked
23794
+ // texture is the blob URL, which dies on page reload — using it as the
23795
+ // override identifier would break the override after every reload.
23796
+ if (value && typeof value === "object" && "getClassName" in value && scene) {
23797
+ const className = value.getClassName();
23798
+ if (className.includes("Texture") || className.includes("texture")) {
23799
+ const samKey = FindSmartAssetKeyForObject(scene, value);
23800
+ if (samKey !== undefined) {
23801
+ return `samTexture:${samKey}`;
23802
+ }
23803
+ return `texture:${value.name}`;
23177
23804
  }
23178
- const sectionRegistration = sceneExplorerService.addSection({
23179
- displayName: "Particle Systems",
23180
- order: 800 /* DefaultSectionsOrder.ParticleSystems */,
23181
- getRootEntities: () => scene.particleSystems,
23182
- getEntityDisplayInfo: (particleSystem) => {
23183
- const onChangeObservable = new Observable();
23184
- const nameHookToken = watcherService.watchProperty(particleSystem, "name", () => onChangeObservable.notifyObservers());
23185
- return {
23186
- get name() {
23187
- return particleSystem.name || `Unnamed ${particleSystem.getClassName()}`;
23188
- },
23189
- onChange: onChangeObservable,
23190
- dispose: () => {
23191
- nameHookToken.dispose();
23192
- onChangeObservable.clear();
23193
- },
23194
- };
23195
- },
23196
- entityIcon: () => jsx(DropRegular, { color: tokens.colorPaletteCranberryForeground2 }),
23197
- getEntityAddedObservables: () => [scene.onNewParticleSystemAddedObservable],
23198
- getEntityRemovedObservables: () => [scene.onParticleSystemRemovedObservable],
23199
- });
23200
- const editParticleSystemCommandRegistration = sceneExplorerService.addEntityCommand({
23201
- predicate: (entity) => entity instanceof ParticleSystem && entity.isNodeGenerated,
23202
- order: 1100 /* DefaultCommandsOrder.EditParticleSystem */,
23203
- getCommand: (particleSystem) => {
23204
- return {
23205
- type: "action",
23206
- displayName: "Edit in Node Particle System Editor",
23207
- icon: () => jsx(EditRegular, {}),
23208
- // eslint-disable-next-line @typescript-eslint/naming-convention
23209
- execute: async () => await EditParticleSystem(particleSystem),
23210
- };
23211
- },
23212
- });
23213
- return {
23214
- dispose: () => {
23215
- sectionRegistration.dispose();
23216
- editParticleSystemCommandRegistration.dispose();
23217
- },
23218
- };
23219
- },
23220
- };
23221
-
23222
- const PostProcessExplorerServiceDefinition = {
23223
- friendlyName: "Post Process Explorer",
23224
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23225
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23226
- const scene = sceneContext.currentScene;
23227
- if (!scene) {
23228
- return undefined;
23805
+ }
23806
+ // Color3 / Color4
23807
+ if (value && typeof value === "object" && "r" in value && "g" in value && "b" in value) {
23808
+ const color = value;
23809
+ if ("a" in color && color.a !== undefined) {
23810
+ return [color.r, color.g, color.b, color.a];
23229
23811
  }
23230
- const sectionRegistration = sceneExplorerService.addSection({
23231
- displayName: "Post Processes",
23232
- order: 500 /* DefaultSectionsOrder.PostProcesses */,
23233
- getRootEntities: () => scene.postProcesses,
23234
- getEntityDisplayInfo: (postProcess) => {
23235
- const onChangeObservable = new Observable();
23236
- const nameHookToken = watcherService.watchProperty(postProcess, "name", () => onChangeObservable.notifyObservers());
23237
- return {
23238
- get name() {
23239
- return postProcess.name || `Unnamed ${postProcess.getClassName()}`;
23240
- },
23241
- onChange: onChangeObservable,
23242
- dispose: () => {
23243
- nameHookToken.dispose();
23244
- onChangeObservable.clear();
23245
- },
23246
- };
23247
- },
23248
- entityIcon: () => jsx(BlurRegular, { color: tokens.colorPaletteRedForeground2 }),
23249
- getEntityAddedObservables: () => [scene.onNewPostProcessAddedObservable],
23250
- getEntityRemovedObservables: () => [scene.onPostProcessRemovedObservable],
23251
- });
23252
- return {
23253
- dispose: () => {
23254
- sectionRegistration.dispose();
23255
- },
23256
- };
23257
- },
23258
- };
23259
-
23260
- const RenderingPipelineExplorerServiceDefinition = {
23261
- friendlyName: "Rendering Pipeline Explorer",
23262
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity],
23263
- factory: (sceneExplorerService, sceneContext) => {
23264
- const scene = sceneContext.currentScene;
23265
- if (!scene) {
23266
- return undefined;
23812
+ return [color.r, color.g, color.b];
23813
+ }
23814
+ // Vector3 / Vector4 / Quaternion
23815
+ if (value && typeof value === "object" && "x" in value && "y" in value && "z" in value) {
23816
+ const vec = value;
23817
+ if ("w" in vec && vec.w !== undefined) {
23818
+ return [vec.x, vec.y, vec.z, vec.w];
23267
23819
  }
23268
- const sectionRegistration = sceneExplorerService.addSection({
23269
- displayName: "Rendering Pipelines",
23270
- order: 600 /* DefaultSectionsOrder.RenderingPipelines */,
23271
- getRootEntities: () => scene.postProcessRenderPipelineManager.supportedPipelines ?? [],
23272
- getEntityDisplayInfo: (pipeline) => {
23273
- return {
23274
- get name() {
23275
- return `${pipeline.name || "Unnamed"} [${pipeline.getClassName()}]`;
23276
- },
23277
- };
23278
- },
23279
- entityIcon: () => jsx(PipelineRegular, { color: tokens.colorPaletteRedForeground2 }),
23280
- getEntityAddedObservables: () => [scene.postProcessRenderPipelineManager.onNewPipelineAddedObservable],
23281
- getEntityRemovedObservables: () => [scene.postProcessRenderPipelineManager.onPipelineRemovedObservable],
23282
- });
23283
- return {
23284
- dispose: () => {
23285
- sectionRegistration.dispose();
23286
- },
23287
- };
23288
- },
23289
- };
23820
+ return [vec.x, vec.y, vec.z];
23821
+ }
23822
+ // Vector2
23823
+ if (value && typeof value === "object" && "x" in value && "y" in value && !("z" in value)) {
23824
+ const vec2 = value;
23825
+ return [vec2.x, vec2.y];
23826
+ }
23827
+ return undefined;
23828
+ }
23829
+ /**
23830
+ * Checks if an entity is a sub-object of a known scene entity by scanning
23831
+ * well-known sub-object properties on the scene and its collections.
23832
+ * Returns the parent entity info with the property path prefix.
23833
+ * @param entity - The entity to search for.
23834
+ * @param scene - The scene to search in.
23835
+ * @returns The parent entity info, or null if not found.
23836
+ */
23837
+ function FindParentEntity(entity, scene) {
23838
+ // Check scene sub-objects (imageProcessingConfiguration, fogSettings, etc.)
23839
+ const sceneSubProps = ["imageProcessingConfiguration", "postProcessRenderPipelineManager", "ambientColor", "gravity"];
23840
+ for (const prop of sceneSubProps) {
23841
+ if (scene[prop] === entity) {
23842
+ return { targetType: "scene", targetName: "", targetIndex: 0, parentProperty: prop };
23843
+ }
23844
+ }
23845
+ const collections = [
23846
+ { type: "materials", items: scene.materials },
23847
+ { type: "cameras", items: scene.cameras },
23848
+ { type: "meshes", items: scene.meshes },
23849
+ { type: "lights", items: scene.lights },
23850
+ ];
23851
+ for (const { type, items } of collections) {
23852
+ for (const parent of items) {
23853
+ for (const prop of Object.keys(parent)) {
23854
+ if (prop.startsWith("_")) {
23855
+ continue;
23856
+ }
23857
+ try {
23858
+ if (parent[prop] === entity) {
23859
+ const targetIndex = items.filter((p) => p.name === parent.name).indexOf(parent);
23860
+ return { targetType: type, targetName: parent.name, targetIndex: Math.max(targetIndex, 0), parentProperty: prop };
23861
+ }
23862
+ }
23863
+ catch {
23864
+ // Skip properties that throw on access
23865
+ }
23866
+ }
23867
+ }
23868
+ }
23869
+ return null;
23870
+ }
23290
23871
 
23291
- const SkeletonExplorerServiceDefinition = {
23292
- friendlyName: "Skeleton Explorer",
23293
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23294
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23872
+ function IsCameraFrameGraphTask(task) {
23873
+ return task.camera instanceof Camera;
23874
+ }
23875
+ function IsNodesSectionType(node) {
23876
+ return node instanceof TransformNode || node instanceof Camera || node instanceof Light;
23877
+ }
23878
+ const NodeExplorerServiceDefinition = {
23879
+ friendlyName: "Node Explorer",
23880
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, GizmoServiceIdentity, WatcherServiceIdentity],
23881
+ factory: (sceneExplorerService, sceneContext, gizmoService, watcherService) => {
23295
23882
  const scene = sceneContext.currentScene;
23296
23883
  if (!scene) {
23297
23884
  return undefined;
23298
23885
  }
23299
- const boneMovedObservable = new Observable();
23886
+ const nodeMovedObservable = new Observable();
23887
+ // Set of all nodes known to be in the scene, rebuilt each time getRootEntities
23888
+ // is called. Used by getEntityDisplayInfo to detect orphaned ancestor nodes.
23889
+ const knownSceneNodes = new Set();
23300
23890
  const sectionRegistration = sceneExplorerService.addSection({
23301
- displayName: "Skeletons",
23302
- order: 200 /* DefaultSectionsOrder.Skeletons */,
23303
- getRootEntities: () => scene.skeletons,
23304
- getEntityChildren: (skeletonOrBone) => skeletonOrBone.getChildren(),
23305
- getEntityDisplayInfo: (skeletonOrBone) => {
23891
+ displayName: "Nodes",
23892
+ order: 100 /* DefaultSectionsOrder.Nodes */,
23893
+ getRootEntities: () => {
23894
+ const rootNodes = new Set(scene.rootNodes);
23895
+ knownSceneNodes.clear();
23896
+ // Ensure all nodes in the scene are reachable in the explorer, even if their
23897
+ // parent was removed from the scene or is not a type shown in the Nodes section.
23898
+ for (const node of [...scene.meshes, ...scene.transformNodes, ...scene.cameras, ...scene.lights]) {
23899
+ knownSceneNodes.add(node);
23900
+ if (!node.parent) {
23901
+ continue;
23902
+ }
23903
+ if (!IsNodesSectionType(node.parent)) {
23904
+ // Parent is not a type shown in the Nodes section (e.g. a Bone).
23905
+ // Treat this node as a root so it still appears in the explorer.
23906
+ rootNodes.add(node);
23907
+ }
23908
+ else {
23909
+ // Walk up through Nodes-section-type parents to find the topmost ancestor.
23910
+ // If that ancestor was removed from the scene (not in rootNodes), add it
23911
+ // so the entire subtree remains visible in the explorer.
23912
+ let ancestor = node.parent;
23913
+ while (ancestor.parent && IsNodesSectionType(ancestor.parent)) {
23914
+ ancestor = ancestor.parent;
23915
+ }
23916
+ rootNodes.add(ancestor);
23917
+ }
23918
+ }
23919
+ // Lights within a clustered light container are not included in Scene.lights or Scene.rootNodes.
23920
+ // If they also have no parent, then they won't show up anywhere, so show them as root nodes.
23921
+ for (const light of scene.lights) {
23922
+ if (light instanceof ClusteredLightContainer) {
23923
+ for (const childLight of light.lights) {
23924
+ knownSceneNodes.add(childLight);
23925
+ if (!childLight.parent) {
23926
+ rootNodes.add(childLight);
23927
+ }
23928
+ }
23929
+ }
23930
+ }
23931
+ return [...rootNodes];
23932
+ },
23933
+ getEntityChildren: (node) => node.getChildren(),
23934
+ getEntityDisplayInfo: (node) => {
23306
23935
  const onChangeObservable = new Observable();
23307
- const nameHookToken = watcherService.watchProperty(skeletonOrBone, "name", () => onChangeObservable.notifyObservers());
23308
- const parentHookToken = skeletonOrBone instanceof Skeleton ? null : watcherService.watchProperty(skeletonOrBone, "parent", () => boneMovedObservable.notifyObservers(skeletonOrBone));
23936
+ const nameHookToken = watcherService.watchProperty(node, "name", () => onChangeObservable.notifyObservers());
23937
+ const parentHookToken = watcherService.watchProperty(node, "parent", () => nodeMovedObservable.notifyObservers(node));
23938
+ // A node is "not in the scene" if it is a Nodes-section type but is not
23939
+ // a known scene node. This handles nodes that were removed from the scene
23940
+ // but still appear because a descendant is in the scene. Nodes from the
23941
+ // !IsNodesSectionType(parent) branch are unaffected because they always
23942
+ // come from the scene's tracking lists. Clustered light children are also
23943
+ // unaffected because they are added to knownSceneNodes explicitly.
23944
+ const validationError = IsNodesSectionType(node) && !knownSceneNodes.has(node) ? "This entity is not in the scene but is shown because a descendant is still in the scene." : undefined;
23309
23945
  return {
23310
23946
  get name() {
23311
- return skeletonOrBone.name || `Unnamed ${skeletonOrBone.getClassName()}`;
23947
+ return node.name || `Unnamed ${node.getClassName()}`;
23312
23948
  },
23949
+ validationError,
23313
23950
  onChange: onChangeObservable,
23314
23951
  dispose: () => {
23315
23952
  nameHookToken.dispose();
23316
- parentHookToken?.dispose();
23953
+ parentHookToken.dispose();
23317
23954
  onChangeObservable.clear();
23318
23955
  },
23319
23956
  };
23320
23957
  },
23321
- entityIcon: ({ entity: skeletonOrBone }) => skeletonOrBone instanceof Skeleton ? (jsx(PersonWalkingRegular, { color: tokens.colorPaletteAnchorForeground2 })) : (jsx(DataLineRegular, { color: tokens.colorPaletteBeigeForeground2 })),
23322
- getEntityAddedObservables: () => [scene.onNewSkeletonAddedObservable],
23323
- getEntityRemovedObservables: () => [scene.onSkeletonRemovedObservable],
23324
- getEntityMovedObservables: () => [boneMovedObservable],
23325
- });
23326
- return {
23327
- dispose: () => {
23328
- sectionRegistration.dispose();
23958
+ entityIcon: ({ entity: node }) => node instanceof AbstractMesh ? (jsx(MeshIcon, { color: tokens.colorPaletteBlueForeground2 })) : node instanceof TransformNode ? (jsx(MyLocationRegular, { color: tokens.colorPaletteBlueForeground2 })) : node instanceof Camera ? (jsx(CameraRegular, { color: tokens.colorPaletteGreenForeground2 })) : node instanceof ClusteredLightContainer ? (jsx(BubbleMultipleRegular, { color: tokens.colorPaletteYellowForeground2 })) : node instanceof Light ? (jsx(LightbulbRegular, { color: tokens.colorPaletteYellowForeground2 })) : (jsx(Fragment, {})),
23959
+ getEntityAddedObservables: () => [
23960
+ scene.onNewMeshAddedObservable,
23961
+ scene.onNewTransformNodeAddedObservable,
23962
+ scene.onNewCameraAddedObservable,
23963
+ scene.onNewLightAddedObservable,
23964
+ ],
23965
+ getEntityRemovedObservables: () => [
23966
+ scene.onMeshRemovedObservable,
23967
+ scene.onTransformNodeRemovedObservable,
23968
+ scene.onCameraRemovedObservable,
23969
+ scene.onLightRemovedObservable,
23970
+ ],
23971
+ getEntityMovedObservables: () => [nodeMovedObservable],
23972
+ dragDropConfig: {
23973
+ canDrag: (node) => node instanceof Node$1,
23974
+ canDrop: (draggedNode, targetNode) => {
23975
+ // Can't drop on self
23976
+ if (targetNode === draggedNode) {
23977
+ return false;
23978
+ }
23979
+ // Can't drop on a descendant
23980
+ if (targetNode !== null && targetNode.isDescendantOf(draggedNode)) {
23981
+ return false;
23982
+ }
23983
+ // Can drop onto section root (null) only if node has a parent
23984
+ if (targetNode === null) {
23985
+ return draggedNode.parent !== null;
23986
+ }
23987
+ return true;
23988
+ },
23989
+ onDrop: (draggedNode, targetNode) => {
23990
+ if (draggedNode.parent === targetNode) {
23991
+ return;
23992
+ }
23993
+ const nodeScene = draggedNode.getScene();
23994
+ const oldParent = draggedNode.parent;
23995
+ // Use setParent for TransformNodes to preserve world transform
23996
+ if (draggedNode instanceof TransformNode) {
23997
+ // setParent recomputes local position/rotation/scaling to
23998
+ // preserve the world transform, so capture those alongside
23999
+ // the parent change to round-trip the reparent on reload.
24000
+ const oldPosition = draggedNode.position.clone();
24001
+ const oldRotation = draggedNode.rotation.clone();
24002
+ const oldRotationQuaternion = draggedNode.rotationQuaternion ? draggedNode.rotationQuaternion.clone() : null;
24003
+ const oldScaling = draggedNode.scaling.clone();
24004
+ draggedNode.setParent(targetNode);
24005
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "parent", targetNode, oldParent);
24006
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "position", draggedNode.position, oldPosition);
24007
+ if (draggedNode.rotationQuaternion) {
24008
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "rotationQuaternion", draggedNode.rotationQuaternion, oldRotationQuaternion);
24009
+ }
24010
+ else {
24011
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "rotation", draggedNode.rotation, oldRotation);
24012
+ }
24013
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "scaling", draggedNode.scaling, oldScaling);
24014
+ }
24015
+ else {
24016
+ draggedNode.parent = targetNode;
24017
+ RecordEntityPropertyOverride(nodeScene, draggedNode, "parent", targetNode, oldParent);
24018
+ }
24019
+ },
23329
24020
  },
23330
- };
23331
- },
23332
- };
23333
-
23334
- const SoundExplorerServiceDefinition = {
23335
- friendlyName: "Sound Explorer",
23336
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23337
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23338
- const scene = sceneContext.currentScene;
23339
- if (!scene) {
23340
- return undefined;
23341
- }
23342
- const soundAddedObservable = new Observable();
23343
- const soundRemovedObservable = new Observable();
23344
- let addSoundHook;
23345
- let removeSoundHook;
23346
- const hookMainSoundTrack = (mainSoundTrack) => {
23347
- addSoundHook?.dispose();
23348
- addSoundHook = undefined;
23349
- removeSoundHook?.dispose();
23350
- removeSoundHook = undefined;
23351
- if (mainSoundTrack) {
23352
- addSoundHook = InterceptFunction(mainSoundTrack, "addSound", {
23353
- afterCall: (sound) => soundAddedObservable.notifyObservers(sound),
23354
- });
23355
- removeSoundHook = InterceptFunction(mainSoundTrack, "removeSound", {
23356
- afterCall: (sound) => soundRemovedObservable.notifyObservers(sound),
23357
- });
23358
- }
23359
- };
23360
- // If _mainSoundTrack is already defined, set up hooks immediately.
23361
- hookMainSoundTrack(scene.mainSoundTrack);
23362
- // Watch for _mainSoundTrack being set (it is lazily created by the mainSoundTrack getter in audioSceneComponent.ts).
23363
- const mainSoundTrackHook = watcherService.watchProperty(scene, "_mainSoundTrack", () => hookMainSoundTrack(scene._mainSoundTrack));
23364
- const sectionRegistration = sceneExplorerService.addSection({
23365
- displayName: "Sounds",
23366
- order: 1400 /* DefaultSectionsOrder.Sounds */,
23367
- getRootEntities: () => scene.mainSoundTrack?.soundCollection ?? [],
23368
- getEntityDisplayInfo: (sound) => {
24021
+ });
24022
+ const abstractMeshBoundingBoxCommandRegistration = sceneExplorerService.addEntityCommand({
24023
+ predicate: (entity) => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
24024
+ order: 1000 /* DefaultCommandsOrder.MeshBoundingBox */,
24025
+ getCommand: (mesh) => {
23369
24026
  const onChangeObservable = new Observable();
23370
- const nameHookToken = watcherService.watchProperty(sound, "name", () => onChangeObservable.notifyObservers());
24027
+ const showBoundingBoxHook = watcherService.watchProperty(mesh, "showBoundingBox", () => onChangeObservable.notifyObservers());
23371
24028
  return {
23372
- get name() {
23373
- return sound.name || `Unnamed ${sound.getClassName()}`;
24029
+ type: "toggle",
24030
+ get displayName() {
24031
+ return `${mesh.showBoundingBox ? "Hide" : "Show"} Bounding Box`;
24032
+ },
24033
+ icon: () => (mesh.showBoundingBox ? jsx(BorderOutsideRegular, {}) : jsx(BorderNoneRegular, {})),
24034
+ get isEnabled() {
24035
+ return mesh.showBoundingBox;
24036
+ },
24037
+ set isEnabled(enabled) {
24038
+ mesh.showBoundingBox = enabled;
23374
24039
  },
23375
24040
  onChange: onChangeObservable,
23376
24041
  dispose: () => {
23377
- nameHookToken.dispose();
24042
+ showBoundingBoxHook.dispose();
23378
24043
  onChangeObservable.clear();
23379
24044
  },
23380
24045
  };
23381
24046
  },
23382
- entityIcon: () => jsx(SoundWaveCircleRegular, { color: tokens.colorPaletteForestForeground2 }),
23383
- getEntityAddedObservables: () => [soundAddedObservable],
23384
- getEntityRemovedObservables: () => [soundRemovedObservable],
23385
24047
  });
23386
- return {
23387
- dispose: () => {
23388
- mainSoundTrackHook.dispose();
23389
- addSoundHook?.dispose();
23390
- removeSoundHook?.dispose();
23391
- soundAddedObservable.clear();
23392
- soundRemovedObservable.clear();
23393
- sectionRegistration.dispose();
23394
- },
23395
- };
23396
- },
23397
- };
23398
-
23399
- const SpriteManagerExplorerServiceDefinition = {
23400
- friendlyName: "Sprite Manager Explorer",
23401
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23402
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23403
- const scene = sceneContext.currentScene;
23404
- if (!scene) {
23405
- return undefined;
23406
- }
23407
- const sectionRegistration = sceneExplorerService.addSection({
23408
- displayName: "Sprite Managers",
23409
- order: 900 /* DefaultSectionsOrder.SpriteManagers */,
23410
- getRootEntities: () => scene.spriteManagers ?? [],
23411
- getEntityChildren: (spriteEntity) => (spriteEntity instanceof Sprite ? [] : spriteEntity.sprites),
23412
- getEntityDisplayInfo: (spriteEntity) => {
24048
+ const abstractMeshVisibilityCommandRegistration = sceneExplorerService.addEntityCommand({
24049
+ predicate: (entity) => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
24050
+ order: 1100 /* DefaultCommandsOrder.MeshVisibility */,
24051
+ getCommand: (mesh) => {
23413
24052
  const onChangeObservable = new Observable();
23414
- const nameHookToken = watcherService.watchProperty(spriteEntity, "name", () => onChangeObservable.notifyObservers());
24053
+ const isVisibleHook = watcherService.watchProperty(mesh, "isVisible", () => onChangeObservable.notifyObservers());
23415
24054
  return {
23416
- get name() {
23417
- return spriteEntity.name || `Unnamed ${spriteEntity instanceof Sprite ? spriteEntity.getClassName() : "SpriteManager"}`;
24055
+ type: "toggle",
24056
+ get displayName() {
24057
+ return `${mesh.isVisible ? "Hide" : "Show"} Mesh`;
24058
+ },
24059
+ icon: () => (mesh.isVisible ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {})),
24060
+ hotKey: {
24061
+ keyCode: "Space",
24062
+ control: true,
24063
+ },
24064
+ get isEnabled() {
24065
+ return !mesh.isVisible;
24066
+ },
24067
+ set isEnabled(enabled) {
24068
+ const oldValue = mesh.isVisible;
24069
+ mesh.isVisible = !enabled;
24070
+ RecordEntityPropertyOverride(mesh.getScene(), mesh, "isVisible", mesh.isVisible, oldValue);
23418
24071
  },
23419
24072
  onChange: onChangeObservable,
23420
24073
  dispose: () => {
23421
- nameHookToken.dispose();
24074
+ isVisibleHook.dispose();
23422
24075
  onChangeObservable.clear();
23423
24076
  },
23424
24077
  };
23425
24078
  },
23426
- entityIcon: ({ entity: spriteEntity }) => spriteEntity instanceof Sprite ? (jsx(PersonSquareRegular, { color: tokens.colorPalettePeachForeground2 })) : (jsx(LayerDiagonalPersonRegular, { color: tokens.colorPalettePeachForeground2 })),
23427
- getEntityAddedObservables: () => [scene.onNewSpriteManagerAddedObservable],
23428
- getEntityRemovedObservables: () => [scene.onSpriteManagerRemovedObservable],
23429
24079
  });
23430
- const spritePlayStopCommandRegistration = sceneExplorerService.addEntityCommand({
23431
- predicate: (entity) => entity instanceof Sprite,
23432
- order: 600 /* DefaultCommandsOrder.SpritePlay */,
23433
- getCommand: (sprite) => {
24080
+ const getActiveCamera = () => (scene.frameGraph ? FindMainCamera(scene.frameGraph) : scene.activeCamera);
24081
+ const activeCameraCommandRegistration = sceneExplorerService.addEntityCommand({
24082
+ predicate: (entity) => entity instanceof Camera,
24083
+ order: 700 /* DefaultCommandsOrder.CameraActive */,
24084
+ getCommand: (camera) => {
24085
+ const scene = camera.getScene();
23434
24086
  const onChangeObservable = new Observable();
23435
- const playHook = InterceptFunction(sprite, "playAnimation", {
23436
- afterCall: () => onChangeObservable.notifyObservers(),
23437
- });
23438
- const stopHook = InterceptFunction(sprite, "stopAnimation", {
23439
- afterCall: () => onChangeObservable.notifyObservers(),
23440
- });
23441
- const animateHook = InterceptFunction(sprite, "_animate", {
23442
- afterCall: () => onChangeObservable.notifyObservers(),
24087
+ const activeCameraChangedObserver = scene.onActiveCameraChanged.add(() => {
24088
+ onChangeObservable.notifyObservers();
23443
24089
  });
23444
24090
  return {
23445
- type: "action",
24091
+ type: "toggle",
24092
+ displayName: "Activate and Attach Controls",
24093
+ icon: () => {
24094
+ return getActiveCamera() === camera ? jsx(VideoFilled, {}) : jsx(VideoRegular, {});
24095
+ },
24096
+ get isEnabled() {
24097
+ return getActiveCamera() === camera;
24098
+ },
24099
+ set isEnabled(enabled) {
24100
+ const activeCamera = getActiveCamera();
24101
+ if (enabled && activeCamera !== camera) {
24102
+ if (scene.frameGraph) {
24103
+ void (async (frameGraph) => {
24104
+ let updated = false;
24105
+ const nrg = frameGraph.getLinkedNodeRenderGraph();
24106
+ if (nrg) {
24107
+ updated = await nrg.replaceCameraAsync(activeCamera, camera);
24108
+ }
24109
+ else {
24110
+ for (const task of frameGraph.tasks) {
24111
+ if (IsCameraFrameGraphTask(task)) {
24112
+ task.camera = camera;
24113
+ updated = true;
24114
+ }
24115
+ }
24116
+ }
24117
+ if (updated) {
24118
+ activeCamera?.detachControl();
24119
+ camera.attachControl(true);
24120
+ onChangeObservable.notifyObservers();
24121
+ }
24122
+ })(scene.frameGraph);
24123
+ }
24124
+ else {
24125
+ activeCamera?.detachControl();
24126
+ scene.activeCamera = camera;
24127
+ camera.attachControl(true);
24128
+ }
24129
+ }
24130
+ },
24131
+ onChange: onChangeObservable,
24132
+ dispose: () => {
24133
+ activeCameraChangedObserver.remove();
24134
+ onChangeObservable.clear();
24135
+ },
24136
+ };
24137
+ },
24138
+ });
24139
+ function addGizmoCommand(nodeClass, getGizmoRef) {
24140
+ return sceneExplorerService.addEntityCommand({
24141
+ predicate: (entity) => entity instanceof nodeClass,
24142
+ order: 800 /* DefaultCommandsOrder.GizmoActive */,
24143
+ getCommand: (node) => {
24144
+ const onChangeObservable = new Observable();
24145
+ let gizmoRef = null;
24146
+ return {
24147
+ type: "toggle",
24148
+ get displayName() {
24149
+ return `Turn ${gizmoRef ? "Off" : "On"} Gizmo`;
24150
+ },
24151
+ icon: () => (gizmoRef ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {})),
24152
+ get isEnabled() {
24153
+ return !!gizmoRef;
24154
+ },
24155
+ set isEnabled(enabled) {
24156
+ if (enabled) {
24157
+ if (!gizmoRef) {
24158
+ gizmoRef = getGizmoRef(node);
24159
+ onChangeObservable.notifyObservers();
24160
+ }
24161
+ }
24162
+ else {
24163
+ if (gizmoRef) {
24164
+ gizmoRef.dispose();
24165
+ gizmoRef = null;
24166
+ onChangeObservable.notifyObservers();
24167
+ }
24168
+ }
24169
+ },
24170
+ onChange: onChangeObservable,
24171
+ dispose: () => {
24172
+ onChangeObservable.clear();
24173
+ },
24174
+ };
24175
+ },
24176
+ });
24177
+ }
24178
+ const cameraGizmoCommandRegistration = addGizmoCommand(Camera, gizmoService.getCameraGizmo.bind(gizmoService));
24179
+ const lightEnabledCommandRegistration = sceneExplorerService.addEntityCommand({
24180
+ predicate: (entity) => entity instanceof Light,
24181
+ order: 700 /* DefaultCommandsOrder.LightActive */,
24182
+ getCommand: (light) => {
24183
+ return {
24184
+ type: "toggle",
23446
24185
  get displayName() {
23447
- return `${sprite.animationStarted ? "Stop" : "Play"} Animation`;
24186
+ return `Turn Light ${light.isEnabled() ? "Off" : "On"}`;
23448
24187
  },
23449
- icon: () => (sprite.animationStarted ? jsx(StopFilled, {}) : jsx(PlayFilled, {})),
23450
- execute: () => (sprite.animationStarted ? sprite.stopAnimation() : sprite.playAnimation(sprite.fromIndex, sprite.toIndex, sprite.loopAnimation, sprite.delay)),
23451
- onChange: onChangeObservable,
23452
- dispose: () => {
23453
- playHook.dispose();
23454
- stopHook.dispose();
23455
- animateHook.dispose();
23456
- onChangeObservable.clear();
24188
+ icon: () => (light.isEnabled() ? jsx(FlashlightRegular, {}) : jsx(FlashlightOffRegular, {})),
24189
+ get isEnabled() {
24190
+ return !light.isEnabled();
24191
+ },
24192
+ set isEnabled(enabled) {
24193
+ light.setEnabled(!enabled);
23457
24194
  },
24195
+ onChange: light.onEnabledStateChangedObservable,
23458
24196
  };
23459
24197
  },
23460
24198
  });
23461
- return {
23462
- dispose: () => {
23463
- sectionRegistration.dispose();
23464
- spritePlayStopCommandRegistration.dispose();
23465
- },
23466
- };
23467
- },
23468
- };
23469
-
23470
- const TextureExplorerServiceDefinition = {
23471
- friendlyName: "Texture Explorer",
23472
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
23473
- factory: (sceneExplorerService, sceneContext, watcherService) => {
23474
- const scene = sceneContext.currentScene;
23475
- if (!scene) {
23476
- return undefined;
23477
- }
23478
- const sectionRegistration = sceneExplorerService.addSection({
23479
- displayName: "Textures",
23480
- order: 400 /* DefaultSectionsOrder.Textures */,
23481
- getRootEntities: () => scene.textures.filter((texture) => texture.getClassName() !== "AdvancedDynamicTexture"),
23482
- getEntityDisplayInfo: (texture) => {
23483
- const onChangeObservable = new Observable();
23484
- const displayNameHookToken = watcherService.watchProperty(texture, "displayName", () => onChangeObservable.notifyObservers());
23485
- const nameHookToken = watcherService.watchProperty(texture, "name", () => onChangeObservable.notifyObservers());
24199
+ const lightGizmoCommandRegistration = addGizmoCommand(Light, gizmoService.getLightGizmo.bind(gizmoService));
24200
+ const editNodeGeometryCommandRegistration = sceneExplorerService.addEntityCommand({
24201
+ predicate: (entity) => entity instanceof Mesh && !!GetNodeGeometry(entity),
24202
+ order: 1100 /* DefaultCommandsOrder.EditNodeGeometry */,
24203
+ getCommand: (mesh) => {
23486
24204
  return {
23487
- get name() {
23488
- return texture.displayName || texture.name || `${texture.getClassName() || "Unnamed Texture"} (${texture.uniqueId})`;
23489
- },
23490
- onChange: onChangeObservable,
23491
- dispose: () => {
23492
- nameHookToken.dispose();
23493
- displayNameHookToken.dispose();
23494
- onChangeObservable.clear();
24205
+ type: "action",
24206
+ displayName: "Edit in Node Geometry Editor",
24207
+ icon: () => jsx(EditRegular, {}),
24208
+ // eslint-disable-next-line @typescript-eslint/naming-convention
24209
+ execute: async () => {
24210
+ const nodeGeometry = GetNodeGeometry(mesh);
24211
+ if (nodeGeometry) {
24212
+ await EditNodeGeometry(nodeGeometry, mesh.getScene());
24213
+ }
23495
24214
  },
23496
24215
  };
23497
24216
  },
23498
- entityIcon: ({ entity: texture }) => texture instanceof DynamicTexture ? jsx(ImageEditRegular, { color: tokens.colorPaletteGrapeForeground2 }) : jsx(ImageRegular, { color: tokens.colorPaletteGrapeForeground2 }),
23499
- getEntityAddedObservables: () => [scene.onNewTextureAddedObservable],
23500
- getEntityRemovedObservables: () => [scene.onTextureRemovedObservable],
23501
24217
  });
23502
24218
  return {
23503
24219
  dispose: () => {
23504
24220
  sectionRegistration.dispose();
24221
+ abstractMeshBoundingBoxCommandRegistration.dispose();
24222
+ abstractMeshVisibilityCommandRegistration.dispose();
24223
+ activeCameraCommandRegistration.dispose();
24224
+ cameraGizmoCommandRegistration.dispose();
24225
+ lightEnabledCommandRegistration.dispose();
24226
+ lightGizmoCommandRegistration.dispose();
24227
+ editNodeGeometryCommandRegistration.dispose();
23505
24228
  },
23506
- };
23507
- },
23508
- };
23509
-
23510
- const EquirectangularCaptureTool = ({ scene }) => {
23511
- const captureEquirectangularAsync = useCallback(async () => {
23512
- const currentActiveCamera = scene.activeCamera;
23513
- if (!currentActiveCamera && scene.frameGraph) {
23514
- scene.activeCamera = FrameGraphUtils.FindMainCamera(scene.frameGraph);
23515
- }
23516
- if (scene.activeCamera) {
23517
- await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
23518
- }
23519
- // eslint-disable-next-line require-atomic-updates
23520
- scene.activeCamera = currentActiveCamera;
23521
- }, [scene]);
23522
- return jsx(ButtonLine, { label: "Capture Equirectangular", icon: CameraRegular, onClick: captureEquirectangularAsync });
23523
- };
23524
-
23525
- const GIFCaptureTool = MakeLazyComponent(async () => {
23526
- const gif = (await import('./gif-CdXfT-By.js').then(function (n) { return n.g; })).default;
23527
- // TODO: Figure out how to grab this from NPM package instead of CDN
23528
- const workerContent = await Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/gif.js.optimized@0.1.6/dist/gif.worker.js");
23529
- const workerBlob = new Blob([workerContent], { type: "application/javascript" });
23530
- const workerUrl = URL.createObjectURL(workerBlob);
23531
- return ({ scene }) => {
23532
- const [recordingSession, setRecordingSession] = useState({ state: "Idle" });
23533
- const [targetWidth, setTargetWidth] = useState(512);
23534
- const [frequency, setFrequency] = useState(200);
23535
- useEffect(() => {
23536
- return () => {
23537
- if (recordingSession.state === "Recording") {
23538
- // Reset session resources if component is unmounted
23539
- scene.onAfterRenderObservable.remove(recordingSession.captureObserver);
23540
- scene.getEngine().setHardwareScalingLevel(recordingSession.previousHardwareScaling);
23541
- }
23542
- };
23543
- }, [recordingSession, scene]);
23544
- // Use functional setState to guard against multiple rapid clicks
23545
- const startRecording = useCallback(() => {
23546
- setRecordingSession((currentSession) => {
23547
- // If already recording/rendering, don't start a new session
23548
- if (currentSession.state !== "Idle") {
23549
- return currentSession;
23550
- }
23551
- const engine = scene.getEngine();
23552
- const canvas = engine.getRenderingCanvas();
23553
- if (!canvas) {
23554
- return currentSession;
23555
- }
23556
- const gifInstance = new gif({
23557
- workers: 2,
23558
- quality: 10,
23559
- workerScript: workerUrl,
23560
- });
23561
- // Adjust hardware scaling to match desired width
23562
- const previousHardwareScaling = engine.getHardwareScalingLevel();
23563
- engine.setHardwareScalingLevel(engine.getRenderWidth() / (targetWidth * globalThis.devicePixelRatio) || 1);
23564
- // Capture frames after each render
23565
- let lastCaptureTime = 0;
23566
- const captureObserver = scene.onAfterRenderObservable.add(() => {
23567
- const now = Date.now();
23568
- if (now - lastCaptureTime >= frequency && gifInstance) {
23569
- lastCaptureTime = now;
23570
- gifInstance.addFrame(canvas, { delay: 1, copy: true });
23571
- }
23572
- });
23573
- return {
23574
- state: "Recording",
23575
- gif: gifInstance,
23576
- captureObserver: captureObserver,
23577
- previousHardwareScaling: previousHardwareScaling,
23578
- };
23579
- });
23580
- }, [scene, targetWidth, frequency]);
23581
- const stopRecording = useCallback(() => {
23582
- setRecordingSession((currentSession) => {
23583
- if (currentSession.state !== "Recording") {
23584
- return currentSession;
23585
- }
23586
- // Remove the frame capture observer
23587
- scene.onAfterRenderObservable.remove(currentSession.captureObserver);
23588
- // Restore previous hardware scaling
23589
- scene.getEngine().setHardwareScalingLevel(currentSession.previousHardwareScaling);
23590
- currentSession.gif.on("finished", (blob) => {
23591
- // Download the rendered GIF
23592
- Tools.Download(blob, "recording.gif");
23593
- // Reset state
23594
- setRecordingSession({ state: "Idle" });
23595
- });
23596
- // Start rendering the GIF
23597
- currentSession.gif.render();
23598
- return { state: "Rendering", gif: currentSession.gif };
23599
- });
23600
- }, [scene]);
23601
- return (jsxs(Fragment, { children: [recordingSession.state === "Idle" && jsx(ButtonLine, { label: "Record GIF", icon: RecordRegular, onClick: startRecording }), recordingSession.state === "Recording" && jsx(ButtonLine, { label: "Stop", icon: RecordStopRegular, onClick: stopRecording }), recordingSession.state === "Rendering" && jsx(Label, { children: "Creating the GIF file..." }), jsxs(Collapse, { visible: recordingSession.state === "Idle", children: [jsx(SyncedSliderPropertyLine, { label: "Resolution", description: "The pixel width of the output. The height will be adjusted accordingly to maintain the aspect ratio.", value: targetWidth, onChange: (value) => setTargetWidth(Math.floor(value)), min: 128, max: 2048, step: 128 }), jsx(SyncedSliderPropertyLine, { label: "Frequency (ms)", description: "The time interval in milliseconds between each capture of the scene.", value: frequency, onChange: (value) => setFrequency(Math.floor(value)), min: 50, max: 1000, step: 50 })] })] }));
23602
- };
23603
- }, { spinnerSize: "extra-tiny", spinnerLabel: "Loading..." });
23604
-
23605
- const SceneReplayTool = ({ scene }) => {
23606
- const [isRecording, setIsRecording] = useState(false);
23607
- const sceneRecorder = useResource(() => new SceneRecorder());
23608
- const startRecording = useCallback(() => {
23609
- sceneRecorder.track(scene);
23610
- setIsRecording(true);
23611
- }, [scene]);
23612
- const exportReplay = useCallback(() => {
23613
- const content = JSON.stringify(sceneRecorder.getDelta());
23614
- const blob = new Blob([content], { type: "application/json" });
23615
- Tools.Download(blob, "replay_delta.json");
23616
- setIsRecording(false);
23617
- }, []);
23618
- const applyDelta = useCallback((files) => {
23619
- const file = files[0];
23620
- if (!file) {
23621
- return;
23622
- }
23623
- Tools.ReadFile(file, (data) => {
23624
- try {
23625
- const json = JSON.parse(data);
23626
- SceneRecorder.ApplyDelta(json, scene);
23627
- }
23628
- catch (error) {
23629
- Logger.Error("Failed to apply replay delta:" + error);
23630
- }
23631
- }, undefined, false);
23632
- }, [scene]);
23633
- return (jsxs(Fragment, { children: [!isRecording && jsx(ButtonLine, { label: "Start Recording", icon: RecordRegular, onClick: startRecording }), isRecording && (jsxs(Fragment, { children: [jsx(Label, { children: "Recording in progress..." }), jsx(ButtonLine, { label: "Generate Delta File", icon: SaveRegular, onClick: exportReplay })] })), jsx(FileUploadLine, { label: "Apply Delta File", icon: ArrowDownloadRegular, onClick: applyDelta, accept: ".json" })] }));
23634
- };
23635
-
23636
- const ScreenshotTool = ({ scene }) => {
23637
- const [precision, setPrecision] = useState(1);
23638
- const [useCustomSize, setUseCustomSize] = useState(false);
23639
- const [width, setWidth] = useState(512);
23640
- const [height, setHeight] = useState(512);
23641
- const captureScreenshot = useCallback(async () => {
23642
- const engine = scene.getEngine();
23643
- const camera = scene.frameGraph ? FrameGraphUtils.FindMainCamera(scene.frameGraph) : scene.activeCamera;
23644
- const screenshotSize = useCustomSize ? { width, height, precision } : { precision };
23645
- if (camera) {
23646
- await CreateScreenshotUsingRenderTargetAsync(engine, camera, screenshotSize, "image/png", undefined, undefined, "screenshot.png");
23647
- }
23648
- }, [useCustomSize, precision, width, height, scene]);
23649
- return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Capture Screenshot", icon: CameraRegular, onClick: captureScreenshot }), jsx(SyncedSliderPropertyLine, { label: "Precision", description: "A multiplier allowing capture at a higher or lower resolution.", value: precision, onChange: setPrecision, min: 0.1, max: 10, step: 0.1 }), jsx(SwitchPropertyLine, { label: "Use Custom Size", value: useCustomSize, onChange: setUseCustomSize }), jsxs(Collapse, { visible: useCustomSize, children: [jsx(SyncedSliderPropertyLine, { label: "Width", description: "The width of the screenshot in pixels. ", value: width, onChange: setWidth, min: 1, step: 1 }), jsx(SyncedSliderPropertyLine, { label: "Height", description: "The height of the screenshot in pixels.", value: height, onChange: setHeight, min: 1, step: 1 })] })] }));
24229
+ };
24230
+ },
23650
24231
  };
23651
24232
 
23652
- const VideoCaptureTool = ({ scene }) => {
23653
- const [isRecording, setIsRecording] = useState(false);
23654
- const videoRecorder = useResource(useCallback(() => {
23655
- return new VideoRecorder(scene.getEngine());
23656
- }, [scene.getEngine()]));
23657
- const recordVideoAsync = useCallback(async () => {
23658
- if (videoRecorder && videoRecorder.isRecording) {
23659
- videoRecorder.stopRecording();
23660
- setIsRecording(false);
23661
- return;
24233
+ const ParticleSystemExplorerServiceDefinition = {
24234
+ friendlyName: "Particle System Explorer",
24235
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24236
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24237
+ const scene = sceneContext.currentScene;
24238
+ if (!scene) {
24239
+ return undefined;
23662
24240
  }
23663
- void videoRecorder.startRecording(undefined, 0); // Use 0 to prevent automatic stop; let the user stop it
23664
- setIsRecording(true);
23665
- }, [scene]);
23666
- return (jsx(Fragment, { children: jsx(ButtonLine, { uniqueId: "Start/Stop", label: isRecording ? "Stop Recording" : "Record Video", icon: isRecording ? RecordStopRegular : RecordRegular, onClick: recordVideoAsync }) }));
23667
- };
23668
-
23669
- const CaptureToolsDefinition = {
23670
- friendlyName: "Capture Tools",
23671
- consumes: [ToolsServiceIdentity],
23672
- factory: (toolsService) => {
23673
- const contentRegistrations = [];
23674
- // Screenshot
23675
- contentRegistrations.push(toolsService.addSectionContent({
23676
- key: "Screenshot",
23677
- section: "Screenshot",
23678
- order: 10,
23679
- component: ({ context }) => jsx(ScreenshotTool, { scene: context }),
23680
- }));
23681
- // Equirectangular capture
23682
- contentRegistrations.push(toolsService.addSectionContent({
23683
- key: "Equirectangular",
23684
- section: "Equirectangular",
23685
- order: 15,
23686
- component: ({ context }) => jsx(EquirectangularCaptureTool, { scene: context }),
23687
- }));
23688
- // Video recorder
23689
- contentRegistrations.push(toolsService.addSectionContent({
23690
- key: "Video",
23691
- section: "Video",
23692
- order: 20,
23693
- component: ({ context }) => jsx(VideoCaptureTool, { scene: context }),
23694
- }));
23695
- // GIF recorder
23696
- contentRegistrations.push(toolsService.addSectionContent({
23697
- key: "GIF",
23698
- section: "GIF",
23699
- order: 25,
23700
- component: ({ context }) => jsx(GIFCaptureTool, { scene: context }),
23701
- }));
23702
- // Scene replay
23703
- contentRegistrations.push(toolsService.addSectionContent({
23704
- key: "Scene Replay",
23705
- section: "Scene Replay",
23706
- order: 30,
23707
- component: ({ context }) => jsx(SceneReplayTool, { scene: context }),
23708
- }));
24241
+ const sectionRegistration = sceneExplorerService.addSection({
24242
+ displayName: "Particle Systems",
24243
+ order: 800 /* DefaultSectionsOrder.ParticleSystems */,
24244
+ getRootEntities: () => scene.particleSystems,
24245
+ getEntityDisplayInfo: (particleSystem) => {
24246
+ const onChangeObservable = new Observable();
24247
+ const nameHookToken = watcherService.watchProperty(particleSystem, "name", () => onChangeObservable.notifyObservers());
24248
+ return {
24249
+ get name() {
24250
+ return particleSystem.name || `Unnamed ${particleSystem.getClassName()}`;
24251
+ },
24252
+ onChange: onChangeObservable,
24253
+ dispose: () => {
24254
+ nameHookToken.dispose();
24255
+ onChangeObservable.clear();
24256
+ },
24257
+ };
24258
+ },
24259
+ entityIcon: () => jsx(DropRegular, { color: tokens.colorPaletteCranberryForeground2 }),
24260
+ getEntityAddedObservables: () => [scene.onNewParticleSystemAddedObservable],
24261
+ getEntityRemovedObservables: () => [scene.onParticleSystemRemovedObservable],
24262
+ });
24263
+ const editParticleSystemCommandRegistration = sceneExplorerService.addEntityCommand({
24264
+ predicate: (entity) => entity instanceof ParticleSystem && entity.isNodeGenerated,
24265
+ order: 1100 /* DefaultCommandsOrder.EditParticleSystem */,
24266
+ getCommand: (particleSystem) => {
24267
+ return {
24268
+ type: "action",
24269
+ displayName: "Edit in Node Particle System Editor",
24270
+ icon: () => jsx(EditRegular, {}),
24271
+ // eslint-disable-next-line @typescript-eslint/naming-convention
24272
+ execute: async () => await EditParticleSystem(particleSystem),
24273
+ };
24274
+ },
24275
+ });
23709
24276
  return {
23710
24277
  dispose: () => {
23711
- contentRegistrations.forEach((registration) => registration.dispose());
24278
+ sectionRegistration.dispose();
24279
+ editParticleSystemCommandRegistration.dispose();
23712
24280
  },
23713
24281
  };
23714
24282
  },
23715
24283
  };
23716
24284
 
23717
- const EnvExportImageTypes = [
23718
- { label: "PNG", value: 0, imageType: "image/png" },
23719
- { label: "WebP", value: 1, imageType: "image/webp" },
23720
- ];
23721
- const ExportBabylonTools = ({ scene }) => {
23722
- // Track environment texture changes to re-render when it's updated (e.g., when a new HDRI is loaded)
23723
- const environmentTexture = useProperty(scene, "environmentTexture");
23724
- const [babylonExportOptions, setBabylonExportOptions] = useState({
23725
- imageTypeIndex: 0,
23726
- imageQuality: 0.8,
23727
- iblDiffuse: false,
23728
- });
23729
- const exportBabylon = useCallback(async () => {
23730
- const strScene = JSON.stringify(SceneSerializer.Serialize(scene));
23731
- const blob = new Blob([strScene], { type: "octet/stream" });
23732
- Tools.Download(blob, "scene.babylon");
23733
- }, [scene]);
23734
- const createEnvTexture = useCallback(async () => {
23735
- if (!environmentTexture) {
23736
- return;
23737
- }
23738
- try {
23739
- const buffer = await EnvironmentTextureTools.CreateEnvTextureAsync(environmentTexture, {
23740
- imageType: EnvExportImageTypes[babylonExportOptions.imageTypeIndex].imageType,
23741
- imageQuality: babylonExportOptions.imageQuality,
23742
- disableIrradianceTexture: !babylonExportOptions.iblDiffuse,
23743
- });
23744
- const blob = new Blob([buffer], { type: "octet/stream" });
23745
- Tools.Download(blob, "environment.env");
23746
- }
23747
- catch (error) {
23748
- Logger.Error(error);
23749
- alert(error);
24285
+ const PostProcessExplorerServiceDefinition = {
24286
+ friendlyName: "Post Process Explorer",
24287
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24288
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24289
+ const scene = sceneContext.currentScene;
24290
+ if (!scene) {
24291
+ return undefined;
23750
24292
  }
23751
- }, [scene, environmentTexture, babylonExportOptions]);
23752
- return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to Babylon", icon: ArrowDownloadRegular, onClick: exportBabylon }), !scene.getEngine().premultipliedAlpha && environmentTexture && environmentTexture._prefiltered && scene.activeCamera && (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Generate .env Texture", icon: ArrowDownloadRegular, onClick: createEnvTexture }), environmentTexture.irradianceTexture && (jsx(SwitchPropertyLine, { label: "Diffuse Texture", description: "Export diffuse texture for IBL", value: babylonExportOptions.iblDiffuse, onChange: (value) => {
23753
- setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value }));
23754
- } }, "iblDiffuse")), jsx(NumberDropdownPropertyLine, { label: "Image type", options: EnvExportImageTypes, value: babylonExportOptions.imageTypeIndex, onChange: (val) => {
23755
- setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val }));
23756
- } }), jsx(Collapse, { visible: babylonExportOptions.imageTypeIndex > 0, children: jsx(SyncedSliderPropertyLine, { label: "Quality", value: babylonExportOptions.imageQuality, onChange: (value) => setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value })), min: 0, max: 1 }) })] }))] }));
23757
- };
23758
- const ExportGltfTools = MakeLazyComponent(async () => {
23759
- // Defer importing anything from the serializers package until this component is actually mounted.
23760
- const { GLTF2Export } = await import('@babylonjs/serializers/glTF/2.0/glTFSerializer.js');
23761
- return (props) => {
23762
- const [isExportingGltf, setIsExportingGltf] = useState(false);
23763
- const [gltfExportOptions, setGltfExportOptions] = useState({
23764
- exportDisabledNodes: false,
23765
- exportSkyboxes: false,
23766
- exportCameras: false,
23767
- exportLights: false,
23768
- dracoCompression: false,
24293
+ const sectionRegistration = sceneExplorerService.addSection({
24294
+ displayName: "Post Processes",
24295
+ order: 500 /* DefaultSectionsOrder.PostProcesses */,
24296
+ getRootEntities: () => scene.postProcesses,
24297
+ getEntityDisplayInfo: (postProcess) => {
24298
+ const onChangeObservable = new Observable();
24299
+ const nameHookToken = watcherService.watchProperty(postProcess, "name", () => onChangeObservable.notifyObservers());
24300
+ return {
24301
+ get name() {
24302
+ return postProcess.name || `Unnamed ${postProcess.getClassName()}`;
24303
+ },
24304
+ onChange: onChangeObservable,
24305
+ dispose: () => {
24306
+ nameHookToken.dispose();
24307
+ onChangeObservable.clear();
24308
+ },
24309
+ };
24310
+ },
24311
+ entityIcon: () => jsx(BlurRegular, { color: tokens.colorPaletteRedForeground2 }),
24312
+ getEntityAddedObservables: () => [scene.onNewPostProcessAddedObservable],
24313
+ getEntityRemovedObservables: () => [scene.onPostProcessRemovedObservable],
23769
24314
  });
23770
- const exportGLTF = useCallback(async () => {
23771
- setIsExportingGltf(true);
23772
- const shouldExport = (node) => {
23773
- if (!gltfExportOptions.exportDisabledNodes) {
23774
- if (!node.isEnabled()) {
23775
- return false;
23776
- }
23777
- }
23778
- if (!gltfExportOptions.exportSkyboxes) {
23779
- if (node instanceof Mesh) {
23780
- if (node.material) {
23781
- const material = node.material;
23782
- const reflectionTexture = material.reflectionTexture;
23783
- if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
23784
- return false;
23785
- }
23786
- }
23787
- }
23788
- }
23789
- if (!gltfExportOptions.exportCameras) {
23790
- if (node instanceof Camera) {
23791
- return false;
23792
- }
23793
- }
23794
- if (!gltfExportOptions.exportLights) {
23795
- if (node instanceof Light) {
23796
- return false;
23797
- }
23798
- }
23799
- return true;
23800
- };
23801
- try {
23802
- const glb = await GLTF2Export.GLBAsync(props.scene, "scene", {
23803
- meshCompressionMethod: gltfExportOptions.dracoCompression ? "Draco" : undefined,
23804
- shouldExportNode: (node) => shouldExport(node),
23805
- });
23806
- glb.downloadFiles();
23807
- }
23808
- catch (reason) {
23809
- Logger.Error(`Failed to export GLB: ${reason}`);
23810
- }
23811
- finally {
23812
- setIsExportingGltf(false);
23813
- }
23814
- }, [gltfExportOptions, props.scene]);
23815
- return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to GLB", icon: ArrowDownloadRegular, onClick: exportGLTF, disabled: isExportingGltf }), jsx(SwitchPropertyLine, { label: "Export Disabled Nodes", description: "Whether to export nodes that are disabled in the scene.", value: gltfExportOptions.exportDisabledNodes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked }) }, "GLTFExportDisabledNodes"), jsx(SwitchPropertyLine, { label: "Export Skyboxes", description: "Whether to export skybox nodes in the scene.", value: gltfExportOptions.exportSkyboxes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked }) }, "GLTFExportSkyboxes"), jsx(SwitchPropertyLine, { label: "Export Cameras", description: "Whether to export cameras in the scene.", value: gltfExportOptions.exportCameras, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked }) }, "GLTFExportCameras"), jsx(SwitchPropertyLine, { label: "Export Lights", description: "Whether to export lights in the scene.", value: gltfExportOptions.exportLights, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportLights: checked }) }, "GLTFExportLights"), jsx(SwitchPropertyLine, { label: "Draco Compression", description: "Whether to apply Draco compression to geometry.", value: gltfExportOptions.dracoCompression, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, dracoCompression: checked }) }, "GLTFDracoCompression")] }));
23816
- };
23817
- });
24315
+ return {
24316
+ dispose: () => {
24317
+ sectionRegistration.dispose();
24318
+ },
24319
+ };
24320
+ },
24321
+ };
23818
24322
 
23819
- const ExportServiceDefinition = {
23820
- friendlyName: "Export Tools",
23821
- consumes: [ToolsServiceIdentity],
23822
- factory: (toolsService) => {
23823
- const contentRegistrations = [];
23824
- // glTF export content
23825
- contentRegistrations.push(toolsService.addSectionContent({
23826
- key: "GLTF Export",
23827
- section: "GLTF Export",
23828
- component: ({ context }) => jsx(ExportGltfTools, { scene: context }),
23829
- }));
23830
- // Babylon export content
23831
- contentRegistrations.push(toolsService.addSectionContent({
23832
- key: "Babylon Export",
23833
- section: "Babylon Export",
23834
- component: ({ context }) => jsx(ExportBabylonTools, { scene: context }),
23835
- }));
24323
+ const RenderingPipelineExplorerServiceDefinition = {
24324
+ friendlyName: "Rendering Pipeline Explorer",
24325
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity],
24326
+ factory: (sceneExplorerService, sceneContext) => {
24327
+ const scene = sceneContext.currentScene;
24328
+ if (!scene) {
24329
+ return undefined;
24330
+ }
24331
+ const sectionRegistration = sceneExplorerService.addSection({
24332
+ displayName: "Rendering Pipelines",
24333
+ order: 600 /* DefaultSectionsOrder.RenderingPipelines */,
24334
+ getRootEntities: () => scene.postProcessRenderPipelineManager.supportedPipelines ?? [],
24335
+ getEntityDisplayInfo: (pipeline) => {
24336
+ return {
24337
+ get name() {
24338
+ return `${pipeline.name || "Unnamed"} [${pipeline.getClassName()}]`;
24339
+ },
24340
+ };
24341
+ },
24342
+ entityIcon: () => jsx(PipelineRegular, { color: tokens.colorPaletteRedForeground2 }),
24343
+ getEntityAddedObservables: () => [scene.postProcessRenderPipelineManager.onNewPipelineAddedObservable],
24344
+ getEntityRemovedObservables: () => [scene.postProcessRenderPipelineManager.onPipelineRemovedObservable],
24345
+ });
23836
24346
  return {
23837
24347
  dispose: () => {
23838
- contentRegistrations.forEach((registration) => registration.dispose());
24348
+ sectionRegistration.dispose();
23839
24349
  },
23840
24350
  };
23841
24351
  },
23842
24352
  };
23843
24353
 
23844
- const OverrideManagerKey = Symbol("babylonjs:overrideManager");
23845
- const OverrideManagerInternals = new WeakMap();
23846
- const OnOverrideManagerCreatedObservable = new Observable();
23847
- /**
23848
- * Creates a new OverrideManager state object and attaches it to the scene.
23849
- *
23850
- * Internal: callers should use {@link GetOverrideManager} which returns the
23851
- * existing manager when one is already attached.
23852
- * @param scene - The scene this manager operates on.
23853
- * @returns The created override manager state.
23854
- */
23855
- function CreateOverrideManager(scene) {
23856
- const manager = {
23857
- scene,
23858
- onChangedObservable: new Observable(),
23859
- };
23860
- const internal = {
23861
- overrides: [],
23862
- originalValues: new Map(),
23863
- sceneDisposeObserver: null,
23864
- };
23865
- OverrideManagerInternals.set(manager, internal);
23866
- if (!scene.metadata) {
23867
- scene.metadata = {};
23868
- }
23869
- scene.metadata[OverrideManagerKey] = manager;
23870
- // Auto-dispose when the scene is disposed so the manager doesn't outlive it.
23871
- internal.sceneDisposeObserver = scene.onDisposeObservable.add(() => DisposeOverrideManager(manager));
23872
- OnOverrideManagerCreatedObservable.notifyObservers(manager);
23873
- return manager;
23874
- }
23875
- /**
23876
- * Returns the OverrideManager attached to the given scene, creating and
23877
- * attaching one if none exists.
23878
- * @param scene - The scene to look up or attach a manager to.
23879
- * @returns The existing or newly created OverrideManager.
23880
- */
23881
- function GetOverrideManager(scene) {
23882
- const existing = scene.metadata?.[OverrideManagerKey];
23883
- if (existing) {
23884
- return existing;
23885
- }
23886
- return CreateOverrideManager(scene);
23887
- }
23888
- /**
23889
- * Adds an override entry and immediately applies it.
23890
- * If an override with the same target coordinates already exists, it is replaced.
23891
- *
23892
- * When the caller has already mutated the target (e.g. an Inspector edit),
23893
- * pass `{ originalValue }` containing the property's prior value — this seeds
23894
- * the original-value map (so {@link RemoveOverride} can restore it) and skips
23895
- * the redundant apply step.
23896
- * @param scene - The scene whose override registry to update.
23897
- * @param entry - The override to add.
23898
- * @param options - Optional behavior modifiers; see {@link AddOverrideOptions}.
23899
- */
23900
- function AddOverride(scene, entry, options) {
23901
- const manager = GetOverrideManager(scene);
23902
- const internal = GetOverrideInternals(manager);
23903
- RemoveMatchingOverride(internal, entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23904
- internal.overrides.push(entry);
23905
- if (options && "originalValue" in options) {
23906
- // Caller already applied the new value. Seed the captured original from
23907
- // their pre-edit snapshot — otherwise ApplyOverrideEntry would capture
23908
- // the post-edit value and RemoveOverride would have nothing to restore.
23909
- const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23910
- if (!internal.originalValues.has(origKey)) {
23911
- internal.originalValues.set(origKey, CloneValue(options.originalValue));
23912
- }
23913
- }
23914
- else {
23915
- ApplyOverrideEntry(manager, internal, entry);
23916
- }
23917
- manager.onChangedObservable.notifyObservers();
23918
- }
23919
- /**
23920
- * Returns all overrides currently registered with the scene.
23921
- * @param scene - The scene whose override registry to read.
23922
- * @returns A read-only array of override entries.
23923
- */
23924
- function GetOverrides(scene) {
23925
- return GetOverrideInternals(GetOverrideManager(scene)).overrides;
23926
- }
23927
- /**
23928
- * Removes all overrides, optionally restoring original values.
23929
- * @param scene - The scene whose override registry to clear.
23930
- * @param restoreOriginals - If true, restores all captured original values.
23931
- */
23932
- function ClearOverrides(scene, restoreOriginals = false) {
23933
- const manager = GetOverrideManager(scene);
23934
- const internal = GetOverrideInternals(manager);
23935
- if (restoreOriginals) {
23936
- // Snapshot the entries so we can restore each original without firing
23937
- // an onChangedObservable notification per entry; consumers only need
23938
- // one signal that the registry was emptied.
23939
- const entries = [...internal.overrides];
23940
- internal.overrides.length = 0;
23941
- for (const entry of entries) {
23942
- const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23943
- const original = internal.originalValues.get(origKey);
23944
- if (original !== undefined) {
23945
- const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
23946
- if (target) {
23947
- SetNestedProperty(target, entry.propertyPath, original);
23948
- }
23949
- }
23950
- }
23951
- internal.originalValues.clear();
23952
- manager.onChangedObservable.notifyObservers();
23953
- return;
23954
- }
23955
- internal.overrides.length = 0;
23956
- internal.originalValues.clear();
23957
- manager.onChangedObservable.notifyObservers();
23958
- }
23959
- /**
23960
- * Updates the target coordinates on the override matching a specific (type,
23961
- * old-name, old-index) so it follows an entity rename. Used by capture services
23962
- * to keep overrides attached to a specific object after the user renames it.
23963
- *
23964
- * Only the override at the exact `(targetType, oldName, oldIndex)` slot is
23965
- * updated, so other same-named siblings keep their own overrides untouched.
23966
- *
23967
- * @param scene - The scene whose override registry to update.
23968
- * @param targetType - The target type.
23969
- * @param oldName - The previous name of the renamed entity.
23970
- * @param oldIndex - The previous index of the renamed entity among same-named siblings.
23971
- * @param newName - The new name of the renamed entity.
23972
- * @param newIndex - The new index of the renamed entity among same-named siblings.
23973
- */
23974
- function RenameOverrideTarget(scene, targetType, oldName, oldIndex, newName, newIndex) {
23975
- const manager = GetOverrideManager(scene);
23976
- const internal = GetOverrideInternals(manager);
23977
- let changed = false;
23978
- for (let i = 0; i < internal.overrides.length; i++) {
23979
- const entry = internal.overrides[i];
23980
- if (entry.targetType === targetType && entry.targetName === oldName && entry.targetIndex === oldIndex) {
23981
- internal.overrides[i] = { ...entry, targetName: newName, targetIndex: newIndex };
23982
- changed = true;
23983
- }
23984
- }
23985
- // Update original-value keys to match the new identity
23986
- const oldPrefix = `${targetType}::${oldName}::${oldIndex}::`;
23987
- const newPrefix = `${targetType}::${newName}::${newIndex}::`;
23988
- for (const [origKey, value] of Array.from(internal.originalValues.entries())) {
23989
- if (origKey.startsWith(oldPrefix)) {
23990
- const propertyPath = origKey.substring(oldPrefix.length);
23991
- internal.originalValues.set(newPrefix + propertyPath, value);
23992
- internal.originalValues.delete(origKey);
23993
- }
23994
- }
23995
- if (changed) {
23996
- manager.onChangedObservable.notifyObservers();
23997
- }
23998
- }
23999
- /**
24000
- * Rewrites override *values* that reference an entity by name when that entity
24001
- * has been renamed. Mirrors {@link RenameOverrideTarget} but operates on the
24002
- * `value` field rather than the `targetName` field, so overrides whose value
24003
- * is `"ref:oldName"` (material/light/camera references) or `"texture:oldName"`
24004
- * (non-SmartAsset texture references) follow the rename instead of silently
24005
- * pointing at a non-existent entity.
24006
- *
24007
- * SmartAsset texture references (`"samTexture:<key>"`) are unaffected because
24008
- * the SmartAsset key is decoupled from the texture's runtime `name` field.
24009
- *
24010
- * @param scene - The scene whose override registry to update.
24011
- * @param valueScheme - Which encoded-reference prefix to rewrite: `"ref"` for
24012
- * material/light/camera references, `"texture"` for non-SAM textures.
24013
- * @param oldName - The previous name embedded in the reference.
24014
- * @param newName - The new name embedded in the reference.
24015
- */
24016
- function RenameOverrideValueReferences(scene, valueScheme, oldName, newName) {
24017
- if (oldName === newName) {
24018
- return;
24019
- }
24020
- const manager = GetOverrideManager(scene);
24021
- const internal = GetOverrideInternals(manager);
24022
- const oldValue = `${valueScheme}:${oldName}`;
24023
- const newValue = `${valueScheme}:${newName}`;
24024
- let changed = false;
24025
- for (let i = 0; i < internal.overrides.length; i++) {
24026
- const entry = internal.overrides[i];
24027
- if (entry.value === oldValue) {
24028
- internal.overrides[i] = { ...entry, value: newValue };
24029
- changed = true;
24030
- }
24031
- }
24032
- if (changed) {
24033
- manager.onChangedObservable.notifyObservers();
24034
- }
24035
- }
24036
- // ── Application ──
24037
- /**
24038
- * Applies all overrides to their current targets in the scene.
24039
- *
24040
- * Call this after any scene mutation that might have invalidated previously
24041
- * applied state (asset reload, object recreation, project load). The override
24042
- * manager does not auto-subscribe to other scene subsystems — coordination is
24043
- * the caller's responsibility, which keeps the override system independent.
24044
- * @param scene - The scene whose overrides to apply.
24045
- */
24046
- function ApplyAllOverrides(scene) {
24047
- const manager = GetOverrideManager(scene);
24048
- const internal = GetOverrideInternals(manager);
24049
- for (const entry of internal.overrides) {
24050
- ApplyOverrideEntry(manager, internal, entry);
24051
- }
24052
- }
24053
- // ── Serialization ──
24054
- /**
24055
- * Serializes all overrides to a JSON-compatible array.
24056
- * The on-disk shape is identical to the in-memory `IOverrideEntry`.
24057
- * @param scene - The scene whose overrides to serialize.
24058
- * @returns An array of override entries (shallow copies).
24059
- */
24060
- function SerializeOverrides(scene) {
24061
- const internal = GetOverrideInternals(GetOverrideManager(scene));
24062
- return internal.overrides.map((o) => ({ ...o }));
24063
- }
24064
- /**
24065
- * Loads overrides from a serialized array and applies them.
24066
- * @param scene - The scene whose override registry to populate.
24067
- * @param data - Array of override entries.
24068
- */
24069
- function DeserializeAndApplyOverrides(scene, data) {
24070
- if (!Array.isArray(data)) {
24071
- throw new Error("OverrideManager: Expected an array of override entries.");
24072
- }
24073
- for (const entry of data) {
24074
- if (!entry.targetType || entry.targetName === undefined || typeof entry.targetIndex !== "number" || !entry.propertyPath || entry.value === undefined) {
24075
- Logger.Warn("OverrideManager: Skipping invalid override entry.");
24076
- continue;
24354
+ const SkeletonExplorerServiceDefinition = {
24355
+ friendlyName: "Skeleton Explorer",
24356
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24357
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24358
+ const scene = sceneContext.currentScene;
24359
+ if (!scene) {
24360
+ return undefined;
24077
24361
  }
24078
- AddOverride(scene, entry);
24079
- }
24080
- }
24081
- // ── Lifecycle ──
24082
- /**
24083
- * Disposes the manager, clearing all overrides and detaching it from its scene.
24084
- * Safe to call multiple times; subsequent calls are no-ops. Automatically invoked when the
24085
- * owning scene is disposed.
24086
- * @param manager - The override manager state.
24087
- */
24088
- function DisposeOverrideManager(manager) {
24089
- const internal = OverrideManagerInternals.get(manager);
24090
- if (!internal) {
24091
- return;
24092
- }
24093
- OverrideManagerInternals.delete(manager);
24094
- if (internal.sceneDisposeObserver) {
24095
- manager.scene.onDisposeObservable.remove(internal.sceneDisposeObserver);
24096
- internal.sceneDisposeObserver = null;
24097
- }
24098
- internal.overrides.length = 0;
24099
- internal.originalValues.clear();
24100
- manager.onChangedObservable.clear();
24101
- if (manager.scene.metadata) {
24102
- delete manager.scene.metadata[OverrideManagerKey];
24103
- }
24104
- }
24105
- // ── Private ──
24106
- function GetOverrideInternals(manager) {
24107
- const internal = OverrideManagerInternals.get(manager);
24108
- if (!internal) {
24109
- throw new Error("OverrideManager: Unknown manager state.");
24110
- }
24111
- return internal;
24112
- }
24113
- /**
24114
- * Applies a single override entry to its target, capturing the original value
24115
- * on the first application so {@link RemoveOverride} can restore it later.
24116
- * @param manager - The override manager owning the entry.
24117
- * @param internal - The manager's internal state.
24118
- * @param entry - The override to apply.
24119
- */
24120
- function ApplyOverrideEntry(manager, internal, entry) {
24121
- const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
24122
- if (!target) {
24123
- Logger.Warn(`OverrideManager: target not found for type="${entry.targetType}" name="${entry.targetName}" index=${entry.targetIndex} prop="${entry.propertyPath}"`);
24124
- return; // Target not loaded yet — override will be applied on next ApplyAllOverrides
24125
- }
24126
- // Capture original value before first override
24127
- const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
24128
- if (!internal.originalValues.has(origKey)) {
24129
- const currentValue = GetNestedProperty(target, entry.propertyPath);
24130
- if (currentValue !== undefined) {
24131
- internal.originalValues.set(origKey, CloneValue(currentValue));
24362
+ const boneMovedObservable = new Observable();
24363
+ const sectionRegistration = sceneExplorerService.addSection({
24364
+ displayName: "Skeletons",
24365
+ order: 200 /* DefaultSectionsOrder.Skeletons */,
24366
+ getRootEntities: () => scene.skeletons,
24367
+ getEntityChildren: (skeletonOrBone) => skeletonOrBone.getChildren(),
24368
+ getEntityDisplayInfo: (skeletonOrBone) => {
24369
+ const onChangeObservable = new Observable();
24370
+ const nameHookToken = watcherService.watchProperty(skeletonOrBone, "name", () => onChangeObservable.notifyObservers());
24371
+ const parentHookToken = skeletonOrBone instanceof Skeleton ? null : watcherService.watchProperty(skeletonOrBone, "parent", () => boneMovedObservable.notifyObservers(skeletonOrBone));
24372
+ return {
24373
+ get name() {
24374
+ return skeletonOrBone.name || `Unnamed ${skeletonOrBone.getClassName()}`;
24375
+ },
24376
+ onChange: onChangeObservable,
24377
+ dispose: () => {
24378
+ nameHookToken.dispose();
24379
+ parentHookToken?.dispose();
24380
+ onChangeObservable.clear();
24381
+ },
24382
+ };
24383
+ },
24384
+ entityIcon: ({ entity: skeletonOrBone }) => skeletonOrBone instanceof Skeleton ? (jsx(PersonWalkingRegular, { color: tokens.colorPaletteAnchorForeground2 })) : (jsx(DataLineRegular, { color: tokens.colorPaletteBeigeForeground2 })),
24385
+ getEntityAddedObservables: () => [scene.onNewSkeletonAddedObservable],
24386
+ getEntityRemovedObservables: () => [scene.onSkeletonRemovedObservable],
24387
+ getEntityMovedObservables: () => [boneMovedObservable],
24388
+ });
24389
+ return {
24390
+ dispose: () => {
24391
+ sectionRegistration.dispose();
24392
+ },
24393
+ };
24394
+ },
24395
+ };
24396
+
24397
+ const SoundExplorerServiceDefinition = {
24398
+ friendlyName: "Sound Explorer",
24399
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24400
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24401
+ const scene = sceneContext.currentScene;
24402
+ if (!scene) {
24403
+ return undefined;
24132
24404
  }
24133
- }
24134
- const resolvedValue = ResolveOverrideValue(manager.scene, entry.value);
24135
- SetNestedProperty(target, entry.propertyPath, resolvedValue);
24136
- }
24137
- /**
24138
- * Locates a scene object by (targetType, targetName, targetIndex). The scene
24139
- * collection is filtered to objects matching `targetName`; the N-th survivor
24140
- * (per `targetIndex`) is returned. Falls back to the first match if the index
24141
- * is out of range — useful when the scene shape has changed since capture.
24142
- * @param scene - The scene to search.
24143
- * @param targetType - The override target type.
24144
- * @param targetName - The target object name (or "" for scene-level).
24145
- * @param targetIndex - The target's position among same-named siblings.
24146
- * @returns The matching scene object, or null if not found.
24147
- */
24148
- function ResolveTarget(scene, targetType, targetName, targetIndex) {
24149
- // Scene-level overrides target the scene itself
24150
- if (targetType === "scene") {
24151
- return scene;
24152
- }
24153
- const collection = GetCollection$1(scene, targetType);
24154
- if (!collection) {
24155
- return null;
24156
- }
24157
- const matches = collection.filter((obj) => obj.name === targetName);
24158
- if (matches.length === 0) {
24159
- return null;
24160
- }
24161
- return (matches[targetIndex] ?? matches[0]);
24162
- }
24163
- /**
24164
- * Returns the scene collection corresponding to an override target type.
24165
- * @param scene - The scene to inspect.
24166
- * @param targetType - The target type.
24167
- * @returns The collection, or null if the type has no collection.
24168
- */
24169
- function GetCollection$1(scene, targetType) {
24170
- switch (targetType) {
24171
- case "meshes":
24172
- return scene.meshes;
24173
- case "materials":
24174
- return scene.materials;
24175
- case "textures":
24176
- return scene.textures;
24177
- case "lights":
24178
- return scene.lights;
24179
- case "cameras":
24180
- return scene.cameras;
24181
- case "animationGroups":
24182
- return scene.animationGroups;
24183
- default:
24184
- return null;
24185
- }
24186
- }
24187
- /**
24188
- * Resolves an override value, expanding string references like "ref:name",
24189
- * "samTexture:key", or "texture:name" into the actual scene object they refer to.
24190
- * @param scene - The scene used to look up references.
24191
- * @param value - The serialized override value.
24192
- * @returns The runtime value to assign to the target property.
24193
- */
24194
- function ResolveOverrideValue(scene, value) {
24195
- if (typeof value === "string") {
24196
- if (value.startsWith("ref:")) {
24197
- return ResolveObjectReference(scene, value.substring(4));
24405
+ const soundAddedObservable = new Observable();
24406
+ const soundRemovedObservable = new Observable();
24407
+ let addSoundHook;
24408
+ let removeSoundHook;
24409
+ const hookMainSoundTrack = (mainSoundTrack) => {
24410
+ addSoundHook?.dispose();
24411
+ addSoundHook = undefined;
24412
+ removeSoundHook?.dispose();
24413
+ removeSoundHook = undefined;
24414
+ if (mainSoundTrack) {
24415
+ addSoundHook = InterceptFunction(mainSoundTrack, "addSound", {
24416
+ afterCall: (sound) => soundAddedObservable.notifyObservers(sound),
24417
+ });
24418
+ removeSoundHook = InterceptFunction(mainSoundTrack, "removeSound", {
24419
+ afterCall: (sound) => soundRemovedObservable.notifyObservers(sound),
24420
+ });
24421
+ }
24422
+ };
24423
+ // If _mainSoundTrack is already defined, set up hooks immediately.
24424
+ hookMainSoundTrack(scene.mainSoundTrack);
24425
+ // Watch for _mainSoundTrack being set (it is lazily created by the mainSoundTrack getter in audioSceneComponent.ts).
24426
+ const mainSoundTrackHook = watcherService.watchProperty(scene, "_mainSoundTrack", () => hookMainSoundTrack(scene._mainSoundTrack));
24427
+ const sectionRegistration = sceneExplorerService.addSection({
24428
+ displayName: "Sounds",
24429
+ order: 1400 /* DefaultSectionsOrder.Sounds */,
24430
+ getRootEntities: () => scene.mainSoundTrack?.soundCollection ?? [],
24431
+ getEntityDisplayInfo: (sound) => {
24432
+ const onChangeObservable = new Observable();
24433
+ const nameHookToken = watcherService.watchProperty(sound, "name", () => onChangeObservable.notifyObservers());
24434
+ return {
24435
+ get name() {
24436
+ return sound.name || `Unnamed ${sound.getClassName()}`;
24437
+ },
24438
+ onChange: onChangeObservable,
24439
+ dispose: () => {
24440
+ nameHookToken.dispose();
24441
+ onChangeObservable.clear();
24442
+ },
24443
+ };
24444
+ },
24445
+ entityIcon: () => jsx(SoundWaveCircleRegular, { color: tokens.colorPaletteForestForeground2 }),
24446
+ getEntityAddedObservables: () => [soundAddedObservable],
24447
+ getEntityRemovedObservables: () => [soundRemovedObservable],
24448
+ });
24449
+ return {
24450
+ dispose: () => {
24451
+ mainSoundTrackHook.dispose();
24452
+ addSoundHook?.dispose();
24453
+ removeSoundHook?.dispose();
24454
+ soundAddedObservable.clear();
24455
+ soundRemovedObservable.clear();
24456
+ sectionRegistration.dispose();
24457
+ },
24458
+ };
24459
+ },
24460
+ };
24461
+
24462
+ const SpriteManagerExplorerServiceDefinition = {
24463
+ friendlyName: "Sprite Manager Explorer",
24464
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24465
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24466
+ const scene = sceneContext.currentScene;
24467
+ if (!scene) {
24468
+ return undefined;
24469
+ }
24470
+ const sectionRegistration = sceneExplorerService.addSection({
24471
+ displayName: "Sprite Managers",
24472
+ order: 900 /* DefaultSectionsOrder.SpriteManagers */,
24473
+ getRootEntities: () => scene.spriteManagers ?? [],
24474
+ getEntityChildren: (spriteEntity) => (spriteEntity instanceof Sprite ? [] : spriteEntity.sprites),
24475
+ getEntityDisplayInfo: (spriteEntity) => {
24476
+ const onChangeObservable = new Observable();
24477
+ const nameHookToken = watcherService.watchProperty(spriteEntity, "name", () => onChangeObservable.notifyObservers());
24478
+ return {
24479
+ get name() {
24480
+ return spriteEntity.name || `Unnamed ${spriteEntity instanceof Sprite ? spriteEntity.getClassName() : "SpriteManager"}`;
24481
+ },
24482
+ onChange: onChangeObservable,
24483
+ dispose: () => {
24484
+ nameHookToken.dispose();
24485
+ onChangeObservable.clear();
24486
+ },
24487
+ };
24488
+ },
24489
+ entityIcon: ({ entity: spriteEntity }) => spriteEntity instanceof Sprite ? (jsx(PersonSquareRegular, { color: tokens.colorPalettePeachForeground2 })) : (jsx(LayerDiagonalPersonRegular, { color: tokens.colorPalettePeachForeground2 })),
24490
+ getEntityAddedObservables: () => [scene.onNewSpriteManagerAddedObservable],
24491
+ getEntityRemovedObservables: () => [scene.onSpriteManagerRemovedObservable],
24492
+ });
24493
+ const spritePlayStopCommandRegistration = sceneExplorerService.addEntityCommand({
24494
+ predicate: (entity) => entity instanceof Sprite,
24495
+ order: 600 /* DefaultCommandsOrder.SpritePlay */,
24496
+ getCommand: (sprite) => {
24497
+ const onChangeObservable = new Observable();
24498
+ const playHook = InterceptFunction(sprite, "playAnimation", {
24499
+ afterCall: () => onChangeObservable.notifyObservers(),
24500
+ });
24501
+ const stopHook = InterceptFunction(sprite, "stopAnimation", {
24502
+ afterCall: () => onChangeObservable.notifyObservers(),
24503
+ });
24504
+ const animateHook = InterceptFunction(sprite, "_animate", {
24505
+ afterCall: () => onChangeObservable.notifyObservers(),
24506
+ });
24507
+ return {
24508
+ type: "action",
24509
+ get displayName() {
24510
+ return `${sprite.animationStarted ? "Stop" : "Play"} Animation`;
24511
+ },
24512
+ icon: () => (sprite.animationStarted ? jsx(StopFilled, {}) : jsx(PlayFilled, {})),
24513
+ execute: () => (sprite.animationStarted ? sprite.stopAnimation() : sprite.playAnimation(sprite.fromIndex, sprite.toIndex, sprite.loopAnimation, sprite.delay)),
24514
+ onChange: onChangeObservable,
24515
+ dispose: () => {
24516
+ playHook.dispose();
24517
+ stopHook.dispose();
24518
+ animateHook.dispose();
24519
+ onChangeObservable.clear();
24520
+ },
24521
+ };
24522
+ },
24523
+ });
24524
+ return {
24525
+ dispose: () => {
24526
+ sectionRegistration.dispose();
24527
+ spritePlayStopCommandRegistration.dispose();
24528
+ },
24529
+ };
24530
+ },
24531
+ };
24532
+
24533
+ const TextureExplorerServiceDefinition = {
24534
+ friendlyName: "Texture Explorer",
24535
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
24536
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
24537
+ const scene = sceneContext.currentScene;
24538
+ if (!scene) {
24539
+ return undefined;
24198
24540
  }
24199
- if (value.startsWith("samTexture:")) {
24200
- return ResolveSamTextureReference(scene, value.substring(11));
24541
+ const sectionRegistration = sceneExplorerService.addSection({
24542
+ displayName: "Textures",
24543
+ order: 400 /* DefaultSectionsOrder.Textures */,
24544
+ getRootEntities: () => scene.textures.filter((texture) => texture.getClassName() !== "AdvancedDynamicTexture"),
24545
+ getEntityDisplayInfo: (texture) => {
24546
+ const onChangeObservable = new Observable();
24547
+ const displayNameHookToken = watcherService.watchProperty(texture, "displayName", () => onChangeObservable.notifyObservers());
24548
+ const nameHookToken = watcherService.watchProperty(texture, "name", () => onChangeObservable.notifyObservers());
24549
+ return {
24550
+ get name() {
24551
+ return texture.displayName || texture.name || `${texture.getClassName() || "Unnamed Texture"} (${texture.uniqueId})`;
24552
+ },
24553
+ onChange: onChangeObservable,
24554
+ dispose: () => {
24555
+ nameHookToken.dispose();
24556
+ displayNameHookToken.dispose();
24557
+ onChangeObservable.clear();
24558
+ },
24559
+ };
24560
+ },
24561
+ entityIcon: ({ entity: texture }) => texture instanceof DynamicTexture ? jsx(ImageEditRegular, { color: tokens.colorPaletteGrapeForeground2 }) : jsx(ImageRegular, { color: tokens.colorPaletteGrapeForeground2 }),
24562
+ getEntityAddedObservables: () => [scene.onNewTextureAddedObservable],
24563
+ getEntityRemovedObservables: () => [scene.onTextureRemovedObservable],
24564
+ });
24565
+ return {
24566
+ dispose: () => {
24567
+ sectionRegistration.dispose();
24568
+ },
24569
+ };
24570
+ },
24571
+ };
24572
+
24573
+ const EquirectangularCaptureTool = ({ scene }) => {
24574
+ const captureEquirectangularAsync = useCallback(async () => {
24575
+ const currentActiveCamera = scene.activeCamera;
24576
+ if (!currentActiveCamera && scene.frameGraph) {
24577
+ scene.activeCamera = FrameGraphUtils.FindMainCamera(scene.frameGraph);
24201
24578
  }
24202
- if (value.startsWith("texture:")) {
24203
- return ResolveTextureReference(scene, value.substring(8));
24579
+ if (scene.activeCamera) {
24580
+ await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
24204
24581
  }
24205
- }
24206
- // Number arrays are passed through as-is. SetNestedProperty will use
24207
- // the live target's `fromArray` method (Vector3, Color3, etc.) to push
24208
- // values in-place, preserving the math instance identity.
24209
- return value;
24210
- }
24211
- /**
24212
- * Resolves a "ref:name" value by looking up a material, light, or camera
24213
- * in the scene by name.
24214
- * @param scene - The scene to search.
24215
- * @param name - The object name to resolve.
24216
- * @returns The matching material, light, or camera, or undefined if not found.
24217
- */
24218
- function ResolveObjectReference(scene, name) {
24219
- const mat = scene.materials.find((m) => m.name === name);
24220
- if (mat) {
24221
- return mat;
24222
- }
24223
- const light = scene.lights.find((l) => l.name === name);
24224
- if (light) {
24225
- return light;
24226
- }
24227
- const camera = scene.cameras.find((c) => c.name === name);
24228
- if (camera) {
24229
- return camera;
24230
- }
24231
- Logger.Warn(`OverrideManager: Object reference "${name}" not found in scene.`);
24232
- return undefined;
24233
- }
24234
- /**
24235
- * Resolves a "texture:name" value by looking up a texture in the scene by name.
24236
- * @param scene - The scene to search.
24237
- * @param name - The texture name to resolve.
24238
- * @returns The matching texture, or undefined if not found.
24239
- */
24240
- function ResolveTextureReference(scene, name) {
24241
- const tex = scene.textures.find((t) => t.name === name);
24242
- if (tex) {
24243
- return tex;
24244
- }
24245
- Logger.Warn(`OverrideManager: Texture reference "${name}" not found.`);
24246
- return undefined;
24247
- }
24248
- /**
24249
- * Resolves a "samTexture:key" value by looking up a SmartAsset-tracked texture
24250
- * by its registry key. The SAM key is stable across save/load whereas the
24251
- * texture's `name` (for SAM textures, the blob URL) changes on every reload,
24252
- * so this is the only reliable way to round-trip texture references on
24253
- * user-uploaded SmartAsset textures.
24254
- * @param scene - The scene to search.
24255
- * @param key - The SmartAsset key to resolve.
24256
- * @returns The matching texture, or undefined if not found.
24257
- */
24258
- function ResolveSamTextureReference(scene, key) {
24259
- const tex = scene.textures.find((t) => FindSmartAssetKeyForObject(scene, t) === key);
24260
- if (tex) {
24261
- return tex;
24262
- }
24263
- Logger.Warn(`OverrideManager: SmartAsset texture "${key}" not found.`);
24264
- return undefined;
24265
- }
24266
- /**
24267
- * Finds the index of an override matching the given coordinates.
24268
- * @param internal - The manager's internal state.
24269
- * @param targetType - The target type.
24270
- * @param targetName - The target object name.
24271
- * @param targetIndex - The target index among same-named siblings.
24272
- * @param propertyPath - The property path.
24273
- * @returns The matching index, or -1 if none found.
24274
- */
24275
- function FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath) {
24276
- return internal.overrides.findIndex((o) => o.targetType === targetType && o.targetName === targetName && o.targetIndex === targetIndex && o.propertyPath === propertyPath);
24277
- }
24278
- /**
24279
- * Removes any existing override that matches the given coordinates. Used by
24280
- * {@link AddOverride} to enforce one entry per (type, name, index, property).
24281
- * @param internal - The manager's internal state.
24282
- * @param targetType - The target type.
24283
- * @param targetName - The target object name.
24284
- * @param targetIndex - The target index among same-named siblings.
24285
- * @param propertyPath - The property path.
24286
- */
24287
- function RemoveMatchingOverride(internal, targetType, targetName, targetIndex, propertyPath) {
24288
- const idx = FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath);
24289
- if (idx >= 0) {
24290
- internal.overrides.splice(idx, 1);
24291
- }
24292
- }
24293
- /**
24294
- * Creates a unique key for storing original values.
24295
- * @param targetType - The override target type.
24296
- * @param targetName - The target object name.
24297
- * @param targetIndex - The target index among same-named siblings.
24298
- * @param propertyPath - The property path.
24299
- * @returns A composite string key uniquely identifying the original value slot.
24300
- */
24301
- function MakeOriginalValueKey(targetType, targetName, targetIndex, propertyPath) {
24302
- return `${targetType}::${targetName}::${targetIndex}::${propertyPath}`;
24303
- }
24304
- /**
24305
- * Gets a nested property from an object using a dot-separated path.
24306
- * @param obj - The root object to traverse.
24307
- * @param path - The dot-separated property path.
24308
- * @returns The value at the path, or undefined if any segment is missing.
24309
- */
24310
- function GetNestedProperty(obj, path) {
24311
- const parts = path.split(".");
24312
- let current = obj;
24313
- for (const part of parts) {
24314
- if (current === null || current === undefined || typeof current !== "object") {
24315
- return undefined;
24582
+ // eslint-disable-next-line require-atomic-updates
24583
+ scene.activeCamera = currentActiveCamera;
24584
+ }, [scene]);
24585
+ return jsx(ButtonLine, { label: "Capture Equirectangular", icon: CameraRegular, onClick: captureEquirectangularAsync });
24586
+ };
24587
+
24588
+ const GIFCaptureTool = MakeLazyComponent(async () => {
24589
+ const gif = (await import('./gif-CdXfT-By.js').then(function (n) { return n.g; })).default;
24590
+ // TODO: Figure out how to grab this from NPM package instead of CDN
24591
+ const workerContent = await Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/gif.js.optimized@0.1.6/dist/gif.worker.js");
24592
+ const workerBlob = new Blob([workerContent], { type: "application/javascript" });
24593
+ const workerUrl = URL.createObjectURL(workerBlob);
24594
+ return ({ scene }) => {
24595
+ const [recordingSession, setRecordingSession] = useState({ state: "Idle" });
24596
+ const [targetWidth, setTargetWidth] = useState(512);
24597
+ const [frequency, setFrequency] = useState(200);
24598
+ useEffect(() => {
24599
+ return () => {
24600
+ if (recordingSession.state === "Recording") {
24601
+ // Reset session resources if component is unmounted
24602
+ scene.onAfterRenderObservable.remove(recordingSession.captureObserver);
24603
+ scene.getEngine().setHardwareScalingLevel(recordingSession.previousHardwareScaling);
24604
+ }
24605
+ };
24606
+ }, [recordingSession, scene]);
24607
+ // Use functional setState to guard against multiple rapid clicks
24608
+ const startRecording = useCallback(() => {
24609
+ setRecordingSession((currentSession) => {
24610
+ // If already recording/rendering, don't start a new session
24611
+ if (currentSession.state !== "Idle") {
24612
+ return currentSession;
24613
+ }
24614
+ const engine = scene.getEngine();
24615
+ const canvas = engine.getRenderingCanvas();
24616
+ if (!canvas) {
24617
+ return currentSession;
24618
+ }
24619
+ const gifInstance = new gif({
24620
+ workers: 2,
24621
+ quality: 10,
24622
+ workerScript: workerUrl,
24623
+ });
24624
+ // Adjust hardware scaling to match desired width
24625
+ const previousHardwareScaling = engine.getHardwareScalingLevel();
24626
+ engine.setHardwareScalingLevel(engine.getRenderWidth() / (targetWidth * globalThis.devicePixelRatio) || 1);
24627
+ // Capture frames after each render
24628
+ let lastCaptureTime = 0;
24629
+ const captureObserver = scene.onAfterRenderObservable.add(() => {
24630
+ const now = Date.now();
24631
+ if (now - lastCaptureTime >= frequency && gifInstance) {
24632
+ lastCaptureTime = now;
24633
+ gifInstance.addFrame(canvas, { delay: 1, copy: true });
24634
+ }
24635
+ });
24636
+ return {
24637
+ state: "Recording",
24638
+ gif: gifInstance,
24639
+ captureObserver: captureObserver,
24640
+ previousHardwareScaling: previousHardwareScaling,
24641
+ };
24642
+ });
24643
+ }, [scene, targetWidth, frequency]);
24644
+ const stopRecording = useCallback(() => {
24645
+ setRecordingSession((currentSession) => {
24646
+ if (currentSession.state !== "Recording") {
24647
+ return currentSession;
24648
+ }
24649
+ // Remove the frame capture observer
24650
+ scene.onAfterRenderObservable.remove(currentSession.captureObserver);
24651
+ // Restore previous hardware scaling
24652
+ scene.getEngine().setHardwareScalingLevel(currentSession.previousHardwareScaling);
24653
+ currentSession.gif.on("finished", (blob) => {
24654
+ // Download the rendered GIF
24655
+ Tools.Download(blob, "recording.gif");
24656
+ // Reset state
24657
+ setRecordingSession({ state: "Idle" });
24658
+ });
24659
+ // Start rendering the GIF
24660
+ currentSession.gif.render();
24661
+ return { state: "Rendering", gif: currentSession.gif };
24662
+ });
24663
+ }, [scene]);
24664
+ return (jsxs(Fragment, { children: [recordingSession.state === "Idle" && jsx(ButtonLine, { label: "Record GIF", icon: RecordRegular, onClick: startRecording }), recordingSession.state === "Recording" && jsx(ButtonLine, { label: "Stop", icon: RecordStopRegular, onClick: stopRecording }), recordingSession.state === "Rendering" && jsx(Label, { children: "Creating the GIF file..." }), jsxs(Collapse, { visible: recordingSession.state === "Idle", children: [jsx(SyncedSliderPropertyLine, { label: "Resolution", description: "The pixel width of the output. The height will be adjusted accordingly to maintain the aspect ratio.", value: targetWidth, onChange: (value) => setTargetWidth(Math.floor(value)), min: 128, max: 2048, step: 128 }), jsx(SyncedSliderPropertyLine, { label: "Frequency (ms)", description: "The time interval in milliseconds between each capture of the scene.", value: frequency, onChange: (value) => setFrequency(Math.floor(value)), min: 50, max: 1000, step: 50 })] })] }));
24665
+ };
24666
+ }, { spinnerSize: "extra-tiny", spinnerLabel: "Loading..." });
24667
+
24668
+ const SceneReplayTool = ({ scene }) => {
24669
+ const [isRecording, setIsRecording] = useState(false);
24670
+ const sceneRecorder = useResource(() => new SceneRecorder());
24671
+ const startRecording = useCallback(() => {
24672
+ sceneRecorder.track(scene);
24673
+ setIsRecording(true);
24674
+ }, [scene]);
24675
+ const exportReplay = useCallback(() => {
24676
+ const content = JSON.stringify(sceneRecorder.getDelta());
24677
+ const blob = new Blob([content], { type: "application/json" });
24678
+ Tools.Download(blob, "replay_delta.json");
24679
+ setIsRecording(false);
24680
+ }, []);
24681
+ const applyDelta = useCallback((files) => {
24682
+ const file = files[0];
24683
+ if (!file) {
24684
+ return;
24316
24685
  }
24317
- current = current[part];
24318
- }
24319
- return current;
24320
- }
24321
- /**
24322
- * Sets a nested property on an object using a dot-separated path.
24323
- *
24324
- * When the value is a number array and the existing property is a Babylon
24325
- * math type (Vector*, Quaternion, Color3/4, Matrix), uses the math type's
24326
- * `fromArray` method to mutate it in place — preserving the live instance
24327
- * identity that consumers may already hold references to. Otherwise falls
24328
- * back to direct property replacement.
24329
- * @param obj - The root object to mutate.
24330
- * @param path - The dot-separated property path.
24331
- * @param value - The new value to assign.
24332
- */
24333
- function SetNestedProperty(obj, path, value) {
24334
- const parts = path.split(".");
24335
- let current = obj;
24336
- for (let i = 0; i < parts.length - 1; i++) {
24337
- if (current === null || current === undefined || typeof current !== "object") {
24686
+ Tools.ReadFile(file, (data) => {
24687
+ try {
24688
+ const json = JSON.parse(data);
24689
+ SceneRecorder.ApplyDelta(json, scene);
24690
+ }
24691
+ catch (error) {
24692
+ Logger.Error("Failed to apply replay delta:" + error);
24693
+ }
24694
+ }, undefined, false);
24695
+ }, [scene]);
24696
+ return (jsxs(Fragment, { children: [!isRecording && jsx(ButtonLine, { label: "Start Recording", icon: RecordRegular, onClick: startRecording }), isRecording && (jsxs(Fragment, { children: [jsx(Label, { children: "Recording in progress..." }), jsx(ButtonLine, { label: "Generate Delta File", icon: SaveRegular, onClick: exportReplay })] })), jsx(FileUploadLine, { label: "Apply Delta File", icon: ArrowDownloadRegular, onClick: applyDelta, accept: ".json" })] }));
24697
+ };
24698
+
24699
+ const ScreenshotTool = ({ scene }) => {
24700
+ const [precision, setPrecision] = useState(1);
24701
+ const [useCustomSize, setUseCustomSize] = useState(false);
24702
+ const [width, setWidth] = useState(512);
24703
+ const [height, setHeight] = useState(512);
24704
+ const captureScreenshot = useCallback(async () => {
24705
+ const engine = scene.getEngine();
24706
+ const camera = scene.frameGraph ? FrameGraphUtils.FindMainCamera(scene.frameGraph) : scene.activeCamera;
24707
+ const screenshotSize = useCustomSize ? { width, height, precision } : { precision };
24708
+ if (camera) {
24709
+ await CreateScreenshotUsingRenderTargetAsync(engine, camera, screenshotSize, "image/png", undefined, undefined, "screenshot.png");
24710
+ }
24711
+ }, [useCustomSize, precision, width, height, scene]);
24712
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Capture Screenshot", icon: CameraRegular, onClick: captureScreenshot }), jsx(SyncedSliderPropertyLine, { label: "Precision", description: "A multiplier allowing capture at a higher or lower resolution.", value: precision, onChange: setPrecision, min: 0.1, max: 10, step: 0.1 }), jsx(SwitchPropertyLine, { label: "Use Custom Size", value: useCustomSize, onChange: setUseCustomSize }), jsxs(Collapse, { visible: useCustomSize, children: [jsx(SyncedSliderPropertyLine, { label: "Width", description: "The width of the screenshot in pixels. ", value: width, onChange: setWidth, min: 1, step: 1 }), jsx(SyncedSliderPropertyLine, { label: "Height", description: "The height of the screenshot in pixels.", value: height, onChange: setHeight, min: 1, step: 1 })] })] }));
24713
+ };
24714
+
24715
+ const VideoCaptureTool = ({ scene }) => {
24716
+ const [isRecording, setIsRecording] = useState(false);
24717
+ const videoRecorder = useResource(useCallback(() => {
24718
+ return new VideoRecorder(scene.getEngine());
24719
+ }, [scene.getEngine()]));
24720
+ const recordVideoAsync = useCallback(async () => {
24721
+ if (videoRecorder && videoRecorder.isRecording) {
24722
+ videoRecorder.stopRecording();
24723
+ setIsRecording(false);
24338
24724
  return;
24339
24725
  }
24340
- current = current[parts[i]];
24341
- }
24342
- if (current === null || current === undefined || typeof current !== "object") {
24343
- return;
24344
- }
24345
- const lastPart = parts[parts.length - 1];
24346
- const existing = current[lastPart];
24347
- if (Array.isArray(value) && existing && typeof existing === "object" && typeof existing.fromArray === "function") {
24348
- existing.fromArray(value);
24349
- return;
24350
- }
24351
- current[lastPart] = value;
24352
- }
24353
- /**
24354
- * Snapshots a value for original-value tracking.
24355
- *
24356
- * Scene entities (textures, materials, meshes, etc.) are stored by reference
24357
- * because cloning them would register unwanted duplicates in the scene.
24358
- * Plain math types (Vector3, Color3, etc.) are cloned so mutations to the
24359
- * live object don't corrupt the saved original.
24360
- * @param value - The value to snapshot.
24361
- * @returns The snapshot value (cloned for plain math types, by reference for entities).
24362
- */
24363
- function CloneValue(value) {
24364
- if (value === null || value === undefined) {
24365
- return value;
24366
- }
24367
- if (typeof value !== "object") {
24368
- return value;
24369
- }
24370
- if (typeof value.getScene === "function") {
24371
- return value;
24372
- }
24373
- if ("clone" in value && typeof value.clone === "function") {
24374
- return value.clone();
24375
- }
24376
- return { ...value };
24377
- }
24726
+ void videoRecorder.startRecording(undefined, 0); // Use 0 to prevent automatic stop; let the user stop it
24727
+ setIsRecording(true);
24728
+ }, [scene]);
24729
+ return (jsx(Fragment, { children: jsx(ButtonLine, { uniqueId: "Start/Stop", label: isRecording ? "Stop Recording" : "Record Video", icon: isRecording ? RecordStopRegular : RecordRegular, onClick: recordVideoAsync }) }));
24730
+ };
24731
+
24732
+ const CaptureToolsDefinition = {
24733
+ friendlyName: "Capture Tools",
24734
+ consumes: [ToolsServiceIdentity],
24735
+ factory: (toolsService) => {
24736
+ const contentRegistrations = [];
24737
+ // Screenshot
24738
+ contentRegistrations.push(toolsService.addSectionContent({
24739
+ key: "Screenshot",
24740
+ section: "Screenshot",
24741
+ order: 10,
24742
+ component: ({ context }) => jsx(ScreenshotTool, { scene: context }),
24743
+ }));
24744
+ // Equirectangular capture
24745
+ contentRegistrations.push(toolsService.addSectionContent({
24746
+ key: "Equirectangular",
24747
+ section: "Equirectangular",
24748
+ order: 15,
24749
+ component: ({ context }) => jsx(EquirectangularCaptureTool, { scene: context }),
24750
+ }));
24751
+ // Video recorder
24752
+ contentRegistrations.push(toolsService.addSectionContent({
24753
+ key: "Video",
24754
+ section: "Video",
24755
+ order: 20,
24756
+ component: ({ context }) => jsx(VideoCaptureTool, { scene: context }),
24757
+ }));
24758
+ // GIF recorder
24759
+ contentRegistrations.push(toolsService.addSectionContent({
24760
+ key: "GIF",
24761
+ section: "GIF",
24762
+ order: 25,
24763
+ component: ({ context }) => jsx(GIFCaptureTool, { scene: context }),
24764
+ }));
24765
+ // Scene replay
24766
+ contentRegistrations.push(toolsService.addSectionContent({
24767
+ key: "Scene Replay",
24768
+ section: "Scene Replay",
24769
+ order: 30,
24770
+ component: ({ context }) => jsx(SceneReplayTool, { scene: context }),
24771
+ }));
24772
+ return {
24773
+ dispose: () => {
24774
+ contentRegistrations.forEach((registration) => registration.dispose());
24775
+ },
24776
+ };
24777
+ },
24778
+ };
24779
+
24780
+ const EnvExportImageTypes = [
24781
+ { label: "PNG", value: 0, imageType: "image/png" },
24782
+ { label: "WebP", value: 1, imageType: "image/webp" },
24783
+ ];
24784
+ const ExportBabylonTools = ({ scene }) => {
24785
+ // Track environment texture changes to re-render when it's updated (e.g., when a new HDRI is loaded)
24786
+ const environmentTexture = useProperty(scene, "environmentTexture");
24787
+ const [babylonExportOptions, setBabylonExportOptions] = useState({
24788
+ imageTypeIndex: 0,
24789
+ imageQuality: 0.8,
24790
+ iblDiffuse: false,
24791
+ });
24792
+ const exportBabylon = useCallback(async () => {
24793
+ const strScene = JSON.stringify(SceneSerializer.Serialize(scene));
24794
+ const blob = new Blob([strScene], { type: "octet/stream" });
24795
+ Tools.Download(blob, "scene.babylon");
24796
+ }, [scene]);
24797
+ const createEnvTexture = useCallback(async () => {
24798
+ if (!environmentTexture) {
24799
+ return;
24800
+ }
24801
+ try {
24802
+ const buffer = await EnvironmentTextureTools.CreateEnvTextureAsync(environmentTexture, {
24803
+ imageType: EnvExportImageTypes[babylonExportOptions.imageTypeIndex].imageType,
24804
+ imageQuality: babylonExportOptions.imageQuality,
24805
+ disableIrradianceTexture: !babylonExportOptions.iblDiffuse,
24806
+ });
24807
+ const blob = new Blob([buffer], { type: "octet/stream" });
24808
+ Tools.Download(blob, "environment.env");
24809
+ }
24810
+ catch (error) {
24811
+ Logger.Error(error);
24812
+ alert(error);
24813
+ }
24814
+ }, [scene, environmentTexture, babylonExportOptions]);
24815
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to Babylon", icon: ArrowDownloadRegular, onClick: exportBabylon }), !scene.getEngine().premultipliedAlpha && environmentTexture && environmentTexture._prefiltered && scene.activeCamera && (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Generate .env Texture", icon: ArrowDownloadRegular, onClick: createEnvTexture }), environmentTexture.irradianceTexture && (jsx(SwitchPropertyLine, { label: "Diffuse Texture", description: "Export diffuse texture for IBL", value: babylonExportOptions.iblDiffuse, onChange: (value) => {
24816
+ setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value }));
24817
+ } }, "iblDiffuse")), jsx(NumberDropdownPropertyLine, { label: "Image type", options: EnvExportImageTypes, value: babylonExportOptions.imageTypeIndex, onChange: (val) => {
24818
+ setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val }));
24819
+ } }), jsx(Collapse, { visible: babylonExportOptions.imageTypeIndex > 0, children: jsx(SyncedSliderPropertyLine, { label: "Quality", value: babylonExportOptions.imageQuality, onChange: (value) => setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value })), min: 0, max: 1 }) })] }))] }));
24820
+ };
24821
+ const ExportGltfTools = MakeLazyComponent(async () => {
24822
+ // Defer importing anything from the serializers package until this component is actually mounted.
24823
+ const { GLTF2Export } = await import('@babylonjs/serializers/glTF/2.0/glTFSerializer.js');
24824
+ return (props) => {
24825
+ const [isExportingGltf, setIsExportingGltf] = useState(false);
24826
+ const [gltfExportOptions, setGltfExportOptions] = useState({
24827
+ exportDisabledNodes: false,
24828
+ exportSkyboxes: false,
24829
+ exportCameras: false,
24830
+ exportLights: false,
24831
+ dracoCompression: false,
24832
+ });
24833
+ const exportGLTF = useCallback(async () => {
24834
+ setIsExportingGltf(true);
24835
+ const shouldExport = (node) => {
24836
+ if (!gltfExportOptions.exportDisabledNodes) {
24837
+ if (!node.isEnabled()) {
24838
+ return false;
24839
+ }
24840
+ }
24841
+ if (!gltfExportOptions.exportSkyboxes) {
24842
+ if (node instanceof Mesh) {
24843
+ if (node.material) {
24844
+ const material = node.material;
24845
+ const reflectionTexture = material.reflectionTexture;
24846
+ if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
24847
+ return false;
24848
+ }
24849
+ }
24850
+ }
24851
+ }
24852
+ if (!gltfExportOptions.exportCameras) {
24853
+ if (node instanceof Camera) {
24854
+ return false;
24855
+ }
24856
+ }
24857
+ if (!gltfExportOptions.exportLights) {
24858
+ if (node instanceof Light) {
24859
+ return false;
24860
+ }
24861
+ }
24862
+ return true;
24863
+ };
24864
+ try {
24865
+ const glb = await GLTF2Export.GLBAsync(props.scene, "scene", {
24866
+ meshCompressionMethod: gltfExportOptions.dracoCompression ? "Draco" : undefined,
24867
+ shouldExportNode: (node) => shouldExport(node),
24868
+ });
24869
+ glb.downloadFiles();
24870
+ }
24871
+ catch (reason) {
24872
+ Logger.Error(`Failed to export GLB: ${reason}`);
24873
+ }
24874
+ finally {
24875
+ setIsExportingGltf(false);
24876
+ }
24877
+ }, [gltfExportOptions, props.scene]);
24878
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to GLB", icon: ArrowDownloadRegular, onClick: exportGLTF, disabled: isExportingGltf }), jsx(SwitchPropertyLine, { label: "Export Disabled Nodes", description: "Whether to export nodes that are disabled in the scene.", value: gltfExportOptions.exportDisabledNodes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked }) }, "GLTFExportDisabledNodes"), jsx(SwitchPropertyLine, { label: "Export Skyboxes", description: "Whether to export skybox nodes in the scene.", value: gltfExportOptions.exportSkyboxes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked }) }, "GLTFExportSkyboxes"), jsx(SwitchPropertyLine, { label: "Export Cameras", description: "Whether to export cameras in the scene.", value: gltfExportOptions.exportCameras, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked }) }, "GLTFExportCameras"), jsx(SwitchPropertyLine, { label: "Export Lights", description: "Whether to export lights in the scene.", value: gltfExportOptions.exportLights, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportLights: checked }) }, "GLTFExportLights"), jsx(SwitchPropertyLine, { label: "Draco Compression", description: "Whether to apply Draco compression to geometry.", value: gltfExportOptions.dracoCompression, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, dracoCompression: checked }) }, "GLTFDracoCompression")] }));
24879
+ };
24880
+ });
24881
+
24882
+ const ExportServiceDefinition = {
24883
+ friendlyName: "Export Tools",
24884
+ consumes: [ToolsServiceIdentity],
24885
+ factory: (toolsService) => {
24886
+ const contentRegistrations = [];
24887
+ // glTF export content
24888
+ contentRegistrations.push(toolsService.addSectionContent({
24889
+ key: "GLTF Export",
24890
+ section: "GLTF Export",
24891
+ component: ({ context }) => jsx(ExportGltfTools, { scene: context }),
24892
+ }));
24893
+ // Babylon export content
24894
+ contentRegistrations.push(toolsService.addSectionContent({
24895
+ key: "Babylon Export",
24896
+ section: "Babylon Export",
24897
+ component: ({ context }) => jsx(ExportBabylonTools, { scene: context }),
24898
+ }));
24899
+ return {
24900
+ dispose: () => {
24901
+ contentRegistrations.forEach((registration) => registration.dispose());
24902
+ },
24903
+ };
24904
+ },
24905
+ };
24378
24906
 
24379
24907
  /**
24380
24908
  * Inspector service that captures property edits made through Inspector and
@@ -24387,100 +24915,30 @@ function CloneValue(value) {
24387
24915
  *
24388
24916
  * The service re-attaches to the current scene whenever it changes, so
24389
24917
  * overrides are captured against the active scene even after loads/swaps.
24918
+ *
24919
+ * The capture logic itself lives in the React-free `overrideCapture`
24920
+ * module so it can be reused by non-Inspector edit paths (e.g. scene-explorer
24921
+ * commands) and unit-tested without a DOM.
24390
24922
  */
24391
24923
  const OverrideCaptureServiceDefinition = {
24392
24924
  friendlyName: "Override Capture",
24393
24925
  consumes: [SceneContextIdentity, PropertiesServiceIdentity],
24394
24926
  factory: (sceneContext, propertiesService) => {
24395
- // Track each entity's name + index at first contact so we can update
24396
- // existing overrides when the user renames the entity in Inspector.
24397
- // Re-created on each scene attach so identities don't leak across scenes.
24398
- let previousIdentity = new WeakMap();
24927
+ // Per-scene identity tracking, re-created on each scene attach so
24928
+ // identities don't leak across scenes.
24929
+ let captureState = CreateCaptureState();
24399
24930
  let changeObserver = null;
24400
24931
  function attachToScene(scene) {
24401
24932
  if (changeObserver) {
24402
24933
  changeObserver.remove();
24403
24934
  changeObserver = null;
24404
24935
  }
24405
- previousIdentity = new WeakMap();
24936
+ captureState = CreateCaptureState();
24406
24937
  if (!scene) {
24407
24938
  return;
24408
24939
  }
24409
24940
  changeObserver = propertiesService.onPropertyChanged.add((changeInfo) => {
24410
- const { entity, propertyKey, oldValue, newValue } = changeInfo;
24411
- // When "name" changes, update the matching override so it follows the rename
24412
- // instead of creating a new (orphaned) one. Also rewrite any overrides
24413
- // whose *value* referenced this entity by name so cross-references
24414
- // (e.g. `mesh.material = ref:redMat`) survive the rename too.
24415
- if (propertyKey === "name" && typeof newValue === "string") {
24416
- const targetType = ClassifyEntity(entity, scene);
24417
- if (targetType !== null && targetType !== "scene") {
24418
- const previous = previousIdentity.get(entity);
24419
- if (previous && previous.name !== newValue) {
24420
- // The entity already has the new name at this point, so compute its new index among same-named siblings.
24421
- const newIndex = ComputeTargetIndex(scene, targetType, entity, newValue);
24422
- RenameOverrideTarget(scene, targetType, previous.name, previous.index, newValue, newIndex);
24423
- // Mirror the rename into override values that reference
24424
- // this entity by name. SmartAsset textures use the
24425
- // `samTexture:<key>` form and stay decoupled from the
24426
- // texture's runtime name, so they don't need rewriting.
24427
- const valueScheme = targetType === "textures" ? "texture" : "ref";
24428
- RenameOverrideValueReferences(scene, valueScheme, previous.name, newValue);
24429
- }
24430
- previousIdentity.set(entity, { name: newValue, index: ComputeTargetIndex(scene, targetType, entity, newValue) });
24431
- }
24432
- return;
24433
- }
24434
- if (propertyKey === "id") {
24435
- return;
24436
- }
24437
- let targetType = ClassifyEntity(entity, scene);
24438
- let targetName;
24439
- let targetIndex;
24440
- let propertyPath = String(propertyKey);
24441
- let targetEntity;
24442
- if (targetType !== null) {
24443
- if (targetType === "scene") {
24444
- targetName = "";
24445
- targetIndex = 0;
24446
- targetEntity = scene;
24447
- }
24448
- else {
24449
- targetName = GetEntityName(entity);
24450
- targetIndex = ComputeTargetIndex(scene, targetType, entity, targetName);
24451
- targetEntity = entity;
24452
- }
24453
- // Seed identity on first contact so rename tracking works
24454
- if (!previousIdentity.has(targetEntity) && targetName) {
24455
- previousIdentity.set(targetEntity, { name: targetName, index: targetIndex });
24456
- }
24457
- }
24458
- else {
24459
- // Sub-object: check if this is a property of a known parent
24460
- const parentInfo = FindParentEntity(entity, scene);
24461
- if (!parentInfo) {
24462
- return;
24463
- }
24464
- targetType = parentInfo.targetType;
24465
- targetName = parentInfo.targetName;
24466
- targetIndex = parentInfo.targetIndex;
24467
- propertyPath = `${parentInfo.parentProperty}.${propertyPath}`;
24468
- }
24469
- const serializedValue = SerializeOverrideValueForCapture(newValue, scene);
24470
- if (serializedValue === undefined) {
24471
- return;
24472
- }
24473
- // The Inspector binding has already written `newValue` to the
24474
- // entity, so pass `oldValue` so the manager can record the
24475
- // true pre-edit value (without this, RemoveOverride would have
24476
- // no record of the original and could not restore it).
24477
- AddOverride(scene, {
24478
- targetType,
24479
- targetName,
24480
- targetIndex,
24481
- propertyPath,
24482
- value: serializedValue,
24483
- }, { originalValue: oldValue });
24941
+ HandleCapturedPropertyChange(scene, captureState, changeInfo);
24484
24942
  });
24485
24943
  }
24486
24944
  attachToScene(sceneContext.currentScene);
@@ -24496,195 +24954,6 @@ const OverrideCaptureServiceDefinition = {
24496
24954
  };
24497
24955
  },
24498
24956
  };
24499
- /**
24500
- * Classifies an entity into an OverrideTargetType by membership in the
24501
- * scene's standard collections (or by being the scene itself).
24502
- * @param entity - The entity to classify.
24503
- * @param scene - The scene to check collections against.
24504
- * @returns The target type, or null if unrecognized.
24505
- */
24506
- function ClassifyEntity(entity, scene) {
24507
- if (entity === scene) {
24508
- return "scene";
24509
- }
24510
- const obj = entity;
24511
- if (!obj || typeof obj !== "object") {
24512
- return null;
24513
- }
24514
- if (scene.materials.includes(obj)) {
24515
- return "materials";
24516
- }
24517
- if (scene.meshes.includes(obj)) {
24518
- return "meshes";
24519
- }
24520
- if (scene.lights.includes(obj)) {
24521
- return "lights";
24522
- }
24523
- if (scene.cameras.includes(obj)) {
24524
- return "cameras";
24525
- }
24526
- if (scene.textures.includes(obj)) {
24527
- return "textures";
24528
- }
24529
- if (scene.animationGroups.includes(obj)) {
24530
- return "animationGroups";
24531
- }
24532
- return null;
24533
- }
24534
- /**
24535
- * Gets the name of a scene entity.
24536
- * @param entity - The entity to get the name from.
24537
- * @returns The entity name, or an empty string if unavailable.
24538
- */
24539
- function GetEntityName(entity) {
24540
- const obj = entity;
24541
- return obj?.name ?? "";
24542
- }
24543
- /**
24544
- * Returns the position of `entity` among scene[targetType] objects with the
24545
- * same name. Used so overrides can disambiguate same-named siblings.
24546
- * @param scene - The scene to inspect.
24547
- * @param targetType - The target type / collection name.
24548
- * @param entity - The entity to locate.
24549
- * @param name - The name to filter by.
24550
- * @returns The index within the same-name filter, or 0 if not found.
24551
- */
24552
- function ComputeTargetIndex(scene, targetType, entity, name) {
24553
- const collection = GetCollection(scene, targetType);
24554
- if (!collection) {
24555
- return 0;
24556
- }
24557
- const sameName = collection.filter((obj) => obj.name === name);
24558
- const idx = sameName.indexOf(entity);
24559
- return idx >= 0 ? idx : 0;
24560
- }
24561
- /**
24562
- * Returns the scene collection matching a target type.
24563
- * @param scene - The scene to inspect.
24564
- * @param targetType - The target type.
24565
- * @returns The collection, or null if `targetType` doesn't map to one.
24566
- */
24567
- function GetCollection(scene, targetType) {
24568
- switch (targetType) {
24569
- case "meshes":
24570
- return scene.meshes;
24571
- case "materials":
24572
- return scene.materials;
24573
- case "textures":
24574
- return scene.textures;
24575
- case "lights":
24576
- return scene.lights;
24577
- case "cameras":
24578
- return scene.cameras;
24579
- case "animationGroups":
24580
- return scene.animationGroups;
24581
- default:
24582
- return null;
24583
- }
24584
- }
24585
- /**
24586
- * Serializes a property value into an OverrideValue.
24587
- * Returns undefined for unsupported types.
24588
- * @param value - The value to serialize.
24589
- * @param scene - Optional scene for resolving object references.
24590
- * @returns The serialized value, or undefined if unsupported.
24591
- */
24592
- function SerializeOverrideValueForCapture(value, scene) {
24593
- // null is a legitimate override value (e.g. clearing a material slot) and
24594
- // must round-trip as null — substituting "" here would silently corrupt
24595
- // object-typed slots with an empty string on reload.
24596
- if (value === null) {
24597
- return null;
24598
- }
24599
- if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
24600
- return value;
24601
- }
24602
- // Material reference → "ref:materialName"
24603
- if (value && typeof value === "object" && "getClassName" in value && typeof value.getClassName === "function") {
24604
- const className = value.getClassName();
24605
- if (className.includes("Material") || className.includes("material")) {
24606
- return `ref:${value.name}`;
24607
- }
24608
- }
24609
- // Texture reference → "samTexture:<key>" if SmartAsset-tracked, else "texture:<name>".
24610
- // The SAM key is stable across save/load; `texture.name` for a SAM-tracked
24611
- // texture is the blob URL, which dies on page reload — using it as the
24612
- // override identifier would break the override after every reload.
24613
- if (value && typeof value === "object" && "getClassName" in value && scene) {
24614
- const className = value.getClassName();
24615
- if (className.includes("Texture") || className.includes("texture")) {
24616
- const samKey = FindSmartAssetKeyForObject(scene, value);
24617
- if (samKey !== undefined) {
24618
- return `samTexture:${samKey}`;
24619
- }
24620
- return `texture:${value.name}`;
24621
- }
24622
- }
24623
- // Color3 / Color4
24624
- if (value && typeof value === "object" && "r" in value && "g" in value && "b" in value) {
24625
- const color = value;
24626
- if ("a" in color && color.a !== undefined) {
24627
- return [color.r, color.g, color.b, color.a];
24628
- }
24629
- return [color.r, color.g, color.b];
24630
- }
24631
- // Vector3 / Vector4
24632
- if (value && typeof value === "object" && "x" in value && "y" in value && "z" in value) {
24633
- const vec = value;
24634
- if ("w" in vec && vec.w !== undefined) {
24635
- return [vec.x, vec.y, vec.z, vec.w];
24636
- }
24637
- return [vec.x, vec.y, vec.z];
24638
- }
24639
- // Vector2
24640
- if (value && typeof value === "object" && "x" in value && "y" in value && !("z" in value)) {
24641
- const vec2 = value;
24642
- return [vec2.x, vec2.y];
24643
- }
24644
- return undefined;
24645
- }
24646
- /**
24647
- * Checks if an entity is a sub-object of a known scene entity by scanning
24648
- * well-known sub-object properties on the scene and its collections.
24649
- * Returns the parent entity info with the property path prefix.
24650
- * @param entity - The entity to search for.
24651
- * @param scene - The scene to search in.
24652
- * @returns The parent entity info, or null if not found.
24653
- */
24654
- function FindParentEntity(entity, scene) {
24655
- // Check scene sub-objects (imageProcessingConfiguration, fogSettings, etc.)
24656
- const sceneSubProps = ["imageProcessingConfiguration", "postProcessRenderPipelineManager", "ambientColor", "gravity"];
24657
- for (const prop of sceneSubProps) {
24658
- if (scene[prop] === entity) {
24659
- return { targetType: "scene", targetName: "", targetIndex: 0, parentProperty: prop };
24660
- }
24661
- }
24662
- const collections = [
24663
- { type: "materials", items: scene.materials },
24664
- { type: "cameras", items: scene.cameras },
24665
- { type: "meshes", items: scene.meshes },
24666
- { type: "lights", items: scene.lights },
24667
- ];
24668
- for (const { type, items } of collections) {
24669
- for (const parent of items) {
24670
- for (const prop of Object.keys(parent)) {
24671
- if (prop.startsWith("_")) {
24672
- continue;
24673
- }
24674
- try {
24675
- if (parent[prop] === entity) {
24676
- const targetIndex = items.filter((p) => p.name === parent.name).indexOf(parent);
24677
- return { targetType: type, targetName: parent.name, targetIndex: Math.max(targetIndex, 0), parentProperty: prop };
24678
- }
24679
- }
24680
- catch {
24681
- // Skip properties that throw on access
24682
- }
24683
- }
24684
- }
24685
- }
24686
- return null;
24687
- }
24688
24957
 
24689
24958
  // Side-effect import: registers the `.babylon` SceneLoader plugin so the
24690
24959
  // companion `.babylon` file produced by SerializeProject can be loaded back.
@@ -27381,4 +27650,4 @@ const TextAreaPropertyLine = (props) => {
27381
27650
  AttachDebugLayer();
27382
27651
 
27383
27652
  export { GetPropertyDescriptor as $, Accordion as A, Button as B, CheckboxPropertyLine as C, Color4PropertyLine as D, ColorPickerPopup as E, ColorStepGradientComponent as F, ComboBox as G, ComboBoxPropertyLine as H, ConstructorFactory as I, ConvertOptions as J, DebugServiceIdentity as K, Link as L, MessageBar as M, NumberInputPropertyLine as N, DetachDebugLayer as O, Popover as P, DraggableLine as Q, Dropdown as R, ShellServiceIdentity as S, TextInputPropertyLine as T, EntitySelector as U, Vector3PropertyLine as V, ErrorBoundary as W, ExtensibleAccordion as X, FactorGradientComponent as Y, FactorGradientList as Z, FileUploadLine as _, useToast as a, ThemeServiceIdentity as a$, GizmoServiceIdentity as a0, HexPropertyLine as a1, InfoLabel as a2, InputHexField as a3, InputHsvField as a4, Inspector as a5, InterceptFunction as a6, InterceptProperty as a7, IsPropertyReadonly as a8, LineContainer as a9, SearchBox as aA, SelectionServiceDefinition as aB, SettingsServiceIdentity as aC, SettingsStore as aD, SettingsStoreIdentity as aE, ShowInspector as aF, SidePaneContainer as aG, SkeletonSelector as aH, Slider as aI, SpinButton as aJ, StartInspectable as aK, StatsServiceIdentity as aL, StringDropdown as aM, StringDropdownPropertyLine as aN, StringifiedPropertyLine as aO, Switch as aP, SwitchPropertyLine as aQ, SyncedSliderInput as aR, SyncedSliderPropertyLine as aS, TeachingMoment as aT, TextAreaPropertyLine as aU, TextInput as aV, TextPropertyLine as aW, Textarea as aX, TextureSelector as aY, TextureUpload as aZ, Theme as a_, LinkPropertyLine as aa, LinkToEntityPropertyLine as ab, List as ac, MakeDialogTeachingMoment as ad, MakeLazyComponent as ae, MakeModularBridge as af, MakeModularTool as ag, MakePopoverTeachingMoment as ah, MakePropertyHook as ai, MakeTeachingMoment as aj, MaterialSelector as ak, NodeSelector as al, NumberDropdown as am, NumberDropdownPropertyLine as an, ObservableCollection as ao, Pane as ap, PlaceholderPropertyLine as aq, PositionedPopover as ar, PropertiesServiceIdentity as as, Property as at, PropertyContext as au, PropertyLine as av, QuaternionPropertyLine as aw, RotationVectorPropertyLine as ax, SceneExplorerServiceIdentity as ay, SearchBar as az, useInterceptObservable as b, ToastProvider as b0, ToggleButton as b1, Tooltip as b2, UploadButton as b3, Vector2PropertyLine as b4, Vector4PropertyLine as b5, WatcherServiceIdentity as b6, inspectorAssetNotFoundHandler as b7, useAngleConverters as b8, useAsyncResource as b9, useColor3Property as ba, useColor4Property as bb, useEventListener as bc, useEventfulState as bd, useKeyListener as be, useKeyState as bf, useObservableCollection as bg, useOrderedObservableCollection as bh, usePollingObservable as bi, usePropertyChangedNotifier as bj, useQuaternionProperty as bk, useResource as bl, useTheme as bm, useThemeMode as bn, useVector3Property as bo, LinkToEntity as c, SpinButtonPropertyLine as d, useProperty as e, SceneContextIdentity as f, SelectionServiceIdentity as g, useObservableState as h, AccordionSection as i, ButtonLine as j, ToolsServiceIdentity as k, AccordionSectionItem as l, AttachDebugLayer as m, BooleanBadgePropertyLine as n, BoundProperty as o, BridgeCommandRegistryIdentity as p, BuiltInsExtensionFeed as q, Checkbox as r, ChildWindow as s, Collapse as t, useExtensionManager as u, Color3GradientComponent as v, Color3GradientList as w, Color3PropertyLine as x, Color4GradientComponent as y, Color4GradientList as z };
27384
- //# sourceMappingURL=index-DG9gOJk5.js.map
27653
+ //# sourceMappingURL=index-DhgT03HX.js.map