@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.
- package/lib/{extensionsListService-C6UitRwb.js → extensionsListService-PdjX95Wt.js} +3 -3
- package/lib/{extensionsListService-C6UitRwb.js.map → extensionsListService-PdjX95Wt.js.map} +1 -1
- package/lib/{index-DG9gOJk5.js → index-DhgT03HX.js} +2001 -1732
- package/lib/index-DhgT03HX.js.map +1 -0
- package/lib/index.js +2 -2
- package/lib/{quickCreateToolsService-xvSzPXeJ.js → quickCreateToolsService-D5JrCb-u.js} +3 -3
- package/lib/{quickCreateToolsService-xvSzPXeJ.js.map → quickCreateToolsService-D5JrCb-u.js.map} +1 -1
- package/lib/{reflectorService-CVK7MlrU.js → reflectorService-Ce2pbz5D.js} +3 -3
- package/lib/{reflectorService-CVK7MlrU.js.map → reflectorService-Ce2pbz5D.js.map} +1 -1
- package/lib/services/overrideCapture.d.ts +72 -0
- package/lib/services/overrideCaptureService.d.ts +4 -0
- package/package.json +1 -1
- package/lib/index-DG9gOJk5.js.map +0 -1
|
@@ -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-
|
|
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-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
22831
|
-
|
|
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
|
-
|
|
22834
|
-
|
|
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
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
|
|
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
|
-
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
22854
|
-
|
|
22855
|
-
|
|
22856
|
-
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22864
|
-
|
|
22865
|
-
|
|
22866
|
-
|
|
22867
|
-
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
|
|
22871
|
-
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
|
|
22878
|
-
|
|
22879
|
-
|
|
22880
|
-
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
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
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22892
|
-
|
|
22893
|
-
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22902
|
-
|
|
22903
|
-
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22919
|
-
|
|
22920
|
-
|
|
22921
|
-
|
|
22922
|
-
|
|
22923
|
-
|
|
22924
|
-
|
|
22925
|
-
|
|
22926
|
-
|
|
22927
|
-
|
|
22928
|
-
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
|
|
22938
|
-
|
|
22939
|
-
|
|
22940
|
-
|
|
22941
|
-
|
|
22942
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
22948
|
-
|
|
22949
|
-
|
|
22950
|
-
|
|
22951
|
-
|
|
22952
|
-
|
|
22953
|
-
|
|
22954
|
-
|
|
22955
|
-
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
22965
|
-
|
|
22966
|
-
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
22980
|
-
|
|
22981
|
-
|
|
22982
|
-
|
|
22983
|
-
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
22987
|
-
|
|
22988
|
-
|
|
22989
|
-
|
|
22990
|
-
|
|
22991
|
-
|
|
22992
|
-
|
|
22993
|
-
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
22997
|
-
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23014
|
-
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23025
|
-
|
|
23026
|
-
|
|
23027
|
-
|
|
23028
|
-
|
|
23029
|
-
|
|
23030
|
-
|
|
23031
|
-
|
|
23032
|
-
|
|
23033
|
-
|
|
23034
|
-
|
|
23035
|
-
|
|
23036
|
-
|
|
23037
|
-
|
|
23038
|
-
|
|
23039
|
-
|
|
23040
|
-
|
|
23041
|
-
|
|
23042
|
-
|
|
23043
|
-
|
|
23044
|
-
|
|
23045
|
-
|
|
23046
|
-
|
|
23047
|
-
|
|
23048
|
-
|
|
23049
|
-
|
|
23050
|
-
|
|
23051
|
-
|
|
23052
|
-
|
|
23053
|
-
|
|
23054
|
-
|
|
23055
|
-
|
|
23056
|
-
|
|
23057
|
-
|
|
23058
|
-
|
|
23059
|
-
|
|
23060
|
-
|
|
23061
|
-
|
|
23062
|
-
|
|
23063
|
-
|
|
23064
|
-
|
|
23065
|
-
|
|
23066
|
-
|
|
23067
|
-
|
|
23068
|
-
|
|
23069
|
-
|
|
23070
|
-
|
|
23071
|
-
|
|
23072
|
-
|
|
23073
|
-
|
|
23074
|
-
|
|
23075
|
-
|
|
23076
|
-
|
|
23077
|
-
|
|
23078
|
-
|
|
23079
|
-
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
|
|
23083
|
-
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23091
|
-
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
|
|
23095
|
-
|
|
23096
|
-
|
|
23097
|
-
|
|
23098
|
-
|
|
23099
|
-
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23103
|
-
|
|
23104
|
-
|
|
23105
|
-
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
|
|
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
|
-
|
|
23116
|
-
|
|
23117
|
-
|
|
23118
|
-
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23125
|
-
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
23129
|
-
|
|
23130
|
-
|
|
23131
|
-
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
|
|
23137
|
-
|
|
23138
|
-
|
|
23139
|
-
|
|
23140
|
-
|
|
23141
|
-
|
|
23142
|
-
|
|
23143
|
-
|
|
23144
|
-
|
|
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
|
-
|
|
23171
|
-
|
|
23172
|
-
|
|
23173
|
-
|
|
23174
|
-
|
|
23175
|
-
|
|
23176
|
-
|
|
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
|
-
|
|
23179
|
-
|
|
23180
|
-
|
|
23181
|
-
|
|
23182
|
-
|
|
23183
|
-
|
|
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
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
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
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
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
|
-
|
|
23292
|
-
|
|
23293
|
-
|
|
23294
|
-
|
|
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
|
|
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: "
|
|
23302
|
-
order:
|
|
23303
|
-
getRootEntities: () =>
|
|
23304
|
-
|
|
23305
|
-
|
|
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(
|
|
23308
|
-
const parentHookToken =
|
|
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
|
|
23947
|
+
return node.name || `Unnamed ${node.getClassName()}`;
|
|
23312
23948
|
},
|
|
23949
|
+
validationError,
|
|
23313
23950
|
onChange: onChangeObservable,
|
|
23314
23951
|
dispose: () => {
|
|
23315
23952
|
nameHookToken.dispose();
|
|
23316
|
-
parentHookToken
|
|
23953
|
+
parentHookToken.dispose();
|
|
23317
23954
|
onChangeObservable.clear();
|
|
23318
23955
|
},
|
|
23319
23956
|
};
|
|
23320
23957
|
},
|
|
23321
|
-
entityIcon: ({ entity:
|
|
23322
|
-
getEntityAddedObservables: () => [
|
|
23323
|
-
|
|
23324
|
-
|
|
23325
|
-
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
|
|
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
|
-
|
|
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
|
|
24027
|
+
const showBoundingBoxHook = watcherService.watchProperty(mesh, "showBoundingBox", () => onChangeObservable.notifyObservers());
|
|
23371
24028
|
return {
|
|
23372
|
-
|
|
23373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
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
|
|
24053
|
+
const isVisibleHook = watcherService.watchProperty(mesh, "isVisible", () => onChangeObservable.notifyObservers());
|
|
23415
24054
|
return {
|
|
23416
|
-
|
|
23417
|
-
|
|
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
|
-
|
|
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
|
|
23431
|
-
|
|
23432
|
-
|
|
23433
|
-
|
|
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
|
|
23436
|
-
|
|
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: "
|
|
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
|
|
24186
|
+
return `Turn Light ${light.isEnabled() ? "Off" : "On"}`;
|
|
23448
24187
|
},
|
|
23449
|
-
icon: () => (
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
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
|
-
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
|
|
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
|
-
|
|
23488
|
-
|
|
23489
|
-
},
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
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
|
|
23653
|
-
|
|
23654
|
-
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
|
|
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
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
|
|
23669
|
-
const
|
|
23670
|
-
|
|
23671
|
-
|
|
23672
|
-
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
|
|
23677
|
-
|
|
23678
|
-
|
|
23679
|
-
|
|
23680
|
-
|
|
23681
|
-
|
|
23682
|
-
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
|
|
23695
|
-
|
|
23696
|
-
|
|
23697
|
-
|
|
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
|
-
|
|
24278
|
+
sectionRegistration.dispose();
|
|
24279
|
+
editParticleSystemCommandRegistration.dispose();
|
|
23712
24280
|
},
|
|
23713
24281
|
};
|
|
23714
24282
|
},
|
|
23715
24283
|
};
|
|
23716
24284
|
|
|
23717
|
-
const
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
|
|
23721
|
-
const
|
|
23722
|
-
|
|
23723
|
-
|
|
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
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
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
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
|
|
23775
|
-
|
|
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
|
|
23820
|
-
friendlyName: "
|
|
23821
|
-
consumes: [
|
|
23822
|
-
factory: (
|
|
23823
|
-
const
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
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
|
-
|
|
24348
|
+
sectionRegistration.dispose();
|
|
23839
24349
|
},
|
|
23840
24350
|
};
|
|
23841
24351
|
},
|
|
23842
24352
|
};
|
|
23843
24353
|
|
|
23844
|
-
const
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
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
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
|
|
24084
|
-
|
|
24085
|
-
|
|
24086
|
-
|
|
24087
|
-
|
|
24088
|
-
|
|
24089
|
-
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
|
|
24093
|
-
|
|
24094
|
-
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
|
|
24100
|
-
|
|
24101
|
-
|
|
24102
|
-
|
|
24103
|
-
|
|
24104
|
-
}
|
|
24105
|
-
|
|
24106
|
-
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
|
|
24110
|
-
}
|
|
24111
|
-
|
|
24112
|
-
|
|
24113
|
-
|
|
24114
|
-
|
|
24115
|
-
|
|
24116
|
-
|
|
24117
|
-
|
|
24118
|
-
|
|
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
|
-
|
|
24135
|
-
|
|
24136
|
-
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
|
|
24143
|
-
|
|
24144
|
-
|
|
24145
|
-
|
|
24146
|
-
|
|
24147
|
-
|
|
24148
|
-
|
|
24149
|
-
|
|
24150
|
-
|
|
24151
|
-
|
|
24152
|
-
|
|
24153
|
-
|
|
24154
|
-
|
|
24155
|
-
|
|
24156
|
-
|
|
24157
|
-
|
|
24158
|
-
|
|
24159
|
-
|
|
24160
|
-
|
|
24161
|
-
|
|
24162
|
-
|
|
24163
|
-
|
|
24164
|
-
|
|
24165
|
-
|
|
24166
|
-
|
|
24167
|
-
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
|
|
24181
|
-
|
|
24182
|
-
|
|
24183
|
-
|
|
24184
|
-
|
|
24185
|
-
|
|
24186
|
-
}
|
|
24187
|
-
|
|
24188
|
-
|
|
24189
|
-
|
|
24190
|
-
|
|
24191
|
-
|
|
24192
|
-
|
|
24193
|
-
|
|
24194
|
-
|
|
24195
|
-
|
|
24196
|
-
|
|
24197
|
-
|
|
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
|
-
|
|
24200
|
-
|
|
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 (
|
|
24203
|
-
|
|
24579
|
+
if (scene.activeCamera) {
|
|
24580
|
+
await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
|
|
24204
24581
|
}
|
|
24205
|
-
|
|
24206
|
-
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
|
|
24214
|
-
|
|
24215
|
-
|
|
24216
|
-
|
|
24217
|
-
|
|
24218
|
-
|
|
24219
|
-
|
|
24220
|
-
|
|
24221
|
-
|
|
24222
|
-
|
|
24223
|
-
|
|
24224
|
-
|
|
24225
|
-
|
|
24226
|
-
|
|
24227
|
-
|
|
24228
|
-
|
|
24229
|
-
|
|
24230
|
-
|
|
24231
|
-
|
|
24232
|
-
|
|
24233
|
-
|
|
24234
|
-
|
|
24235
|
-
|
|
24236
|
-
|
|
24237
|
-
|
|
24238
|
-
|
|
24239
|
-
|
|
24240
|
-
|
|
24241
|
-
|
|
24242
|
-
|
|
24243
|
-
|
|
24244
|
-
|
|
24245
|
-
|
|
24246
|
-
|
|
24247
|
-
|
|
24248
|
-
|
|
24249
|
-
|
|
24250
|
-
|
|
24251
|
-
|
|
24252
|
-
|
|
24253
|
-
|
|
24254
|
-
|
|
24255
|
-
|
|
24256
|
-
|
|
24257
|
-
|
|
24258
|
-
|
|
24259
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24263
|
-
|
|
24264
|
-
|
|
24265
|
-
}
|
|
24266
|
-
|
|
24267
|
-
|
|
24268
|
-
|
|
24269
|
-
|
|
24270
|
-
|
|
24271
|
-
|
|
24272
|
-
|
|
24273
|
-
|
|
24274
|
-
|
|
24275
|
-
|
|
24276
|
-
|
|
24277
|
-
|
|
24278
|
-
|
|
24279
|
-
|
|
24280
|
-
|
|
24281
|
-
|
|
24282
|
-
|
|
24283
|
-
|
|
24284
|
-
|
|
24285
|
-
|
|
24286
|
-
|
|
24287
|
-
|
|
24288
|
-
|
|
24289
|
-
|
|
24290
|
-
|
|
24291
|
-
|
|
24292
|
-
|
|
24293
|
-
|
|
24294
|
-
|
|
24295
|
-
|
|
24296
|
-
|
|
24297
|
-
|
|
24298
|
-
|
|
24299
|
-
|
|
24300
|
-
|
|
24301
|
-
|
|
24302
|
-
|
|
24303
|
-
}
|
|
24304
|
-
|
|
24305
|
-
|
|
24306
|
-
|
|
24307
|
-
|
|
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
|
-
|
|
24318
|
-
|
|
24319
|
-
|
|
24320
|
-
|
|
24321
|
-
|
|
24322
|
-
|
|
24323
|
-
|
|
24324
|
-
|
|
24325
|
-
|
|
24326
|
-
|
|
24327
|
-
|
|
24328
|
-
|
|
24329
|
-
|
|
24330
|
-
|
|
24331
|
-
|
|
24332
|
-
|
|
24333
|
-
|
|
24334
|
-
const
|
|
24335
|
-
|
|
24336
|
-
|
|
24337
|
-
|
|
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
|
-
|
|
24341
|
-
|
|
24342
|
-
|
|
24343
|
-
|
|
24344
|
-
|
|
24345
|
-
|
|
24346
|
-
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
|
|
24350
|
-
|
|
24351
|
-
|
|
24352
|
-
|
|
24353
|
-
|
|
24354
|
-
|
|
24355
|
-
|
|
24356
|
-
|
|
24357
|
-
|
|
24358
|
-
|
|
24359
|
-
|
|
24360
|
-
|
|
24361
|
-
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
24365
|
-
|
|
24366
|
-
|
|
24367
|
-
|
|
24368
|
-
|
|
24369
|
-
|
|
24370
|
-
|
|
24371
|
-
|
|
24372
|
-
|
|
24373
|
-
|
|
24374
|
-
|
|
24375
|
-
|
|
24376
|
-
|
|
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
|
-
//
|
|
24396
|
-
//
|
|
24397
|
-
|
|
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
|
-
|
|
24936
|
+
captureState = CreateCaptureState();
|
|
24406
24937
|
if (!scene) {
|
|
24407
24938
|
return;
|
|
24408
24939
|
}
|
|
24409
24940
|
changeObserver = propertiesService.onPropertyChanged.add((changeInfo) => {
|
|
24410
|
-
|
|
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-
|
|
27653
|
+
//# sourceMappingURL=index-DhgT03HX.js.map
|