@babylonjs/inspector 9.9.0 → 9.9.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.
Files changed (27) hide show
  1. package/lib/browser-CANgtOiM.js +1277 -0
  2. package/lib/browser-CANgtOiM.js.map +1 -0
  3. package/lib/components/properties/audio/audioV2Properties.d.ts +93 -0
  4. package/lib/components/properties/audio/audioV2SpatialProperties.d.ts +12 -0
  5. package/lib/components/properties/materials/materialTextureDebugPropertyLine.d.ts +18 -0
  6. package/lib/{extensionsListService-DTrjNf_v.js → extensionsListService-j6viqje8.js} +13 -2
  7. package/lib/{extensionsListService-DTrjNf_v.js.map → extensionsListService-j6viqje8.js.map} +1 -1
  8. package/lib/{index-PYblOaAV.js → index--oJsOVVX.js} +2775 -384
  9. package/lib/index--oJsOVVX.js.map +1 -0
  10. package/lib/index.js +12 -1
  11. package/lib/index.js.map +1 -1
  12. package/lib/projects/overrideEntry.d.ts +36 -0
  13. package/lib/projects/overrideManager.d.ts +176 -0
  14. package/lib/projects/projectFile.d.ts +143 -0
  15. package/lib/{quickCreateToolsService-8d6rBO-A.js → quickCreateToolsService-eZ4MCuJ2.js} +13 -2
  16. package/lib/{quickCreateToolsService-8d6rBO-A.js.map → quickCreateToolsService-eZ4MCuJ2.js.map} +1 -1
  17. package/lib/{reflectorService-B7LcD1Sn.js → reflectorService-2dP-GJrK.js} +13 -2
  18. package/lib/{reflectorService-B7LcD1Sn.js.map → reflectorService-2dP-GJrK.js.map} +1 -1
  19. package/lib/services/gizmoService.d.ts +9 -0
  20. package/lib/services/overrideCaptureService.d.ts +16 -0
  21. package/lib/services/panes/{smartAssetsService.d.ts → babylonProjectAuthoringService.d.ts} +4 -2
  22. package/lib/services/panes/properties/audioPropertiesService.d.ts +2 -1
  23. package/lib/services/panes/scene/audioV2ExplorerService.d.ts +6 -0
  24. package/lib/services/panes/scene/defaultSectionsMetadata.d.ts +2 -1
  25. package/package.json +1 -1
  26. package/lib/index-PYblOaAV.js.map +0 -1
  27. package/lib/services/panes/tools/smartAssetToolsService.d.ts +0 -10
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { createContext, forwardRef, useContext, useState, useCallback, Component, useMemo, useEffect, useRef, useReducer, Children, isValidElement, useLayoutEffect, useImperativeHandle, cloneElement, createElement, Suspense, memo, Fragment as Fragment$1, lazy } from 'react';
3
- import { tokens, makeStyles, Tooltip as Tooltip$1, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, useFluent, Accordion as Accordion$1, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, MessageBar as MessageBar$1, MessageBarBody, AccordionItem, SearchBox as SearchBox$1, Portal, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, Body1Strong, mergeClasses, useId, useToastController, Toast, ToastTitle, FluentProvider, Toaster, Checkbox as Checkbox$1, createLightTheme, createDarkTheme, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, createDOMRenderer, RendererProvider, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, Switch as Switch$1, treeItemLevelToken, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, MenuItemCheckbox, useMergedRefs, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, PresenceBadge, Slider as Slider$1, MenuItemRadio, Dialog as Dialog$1, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, MessageBarTitle, useComboboxFilter, Combobox, Subtitle2, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, DialogTrigger, Field } from '@fluentui/react-components';
4
- import { ErrorCircleRegular, EyeFilled, EyeOffRegular, CheckmarkFilled, EditRegular, FilterRegular, PinFilled, PinRegular, ArrowCircleUpRegular, ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, CopyRegular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, SettingsRegular, DocumentTextRegular, createFluentIcon, TextSortAscendingRegular, GlobeRegular, WarningRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, ArrowUploadRegular, ArrowBidirectionalUpDownFilled, ArrowDownloadRegular, StopRegular, RecordRegular, DataBarHorizontalRegular, WrenchRegular, ArrowClockwiseRegular, WeatherSunnyRegular, WeatherMoonRegular, PlugDisconnectedRegular, PlugConnectedRegular, PlugConnectedCheckmarkRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, CameraRegular, AddRegular, DeleteRegular, FullScreenMaximizeRegular, ChevronDownRegular, ChevronRightRegular, CircleSmallFilled, SaveRegular, PreviousRegular, ArrowPreviousRegular, TriangleLeftRegular, RecordStopRegular, PlayRegular, ArrowNextRegular, NextRegular, PauseRegular, LinkDismissRegular, LinkEditRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, EyeRegular, CloudArrowUpRegular, CloudArrowDownRegular, EyeOffFilled, ArrowMoveFilled, StopFilled, PlayFilled, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, AppGenericRegular, RectangleLandscapeRegular, BorderOutsideRegular, BorderNoneRegular, MyLocationRegular, BubbleMultipleRegular, LightbulbRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, SoundWaveCircleRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, LinkRegular, ArrowSyncRegular, TargetRegular, PersonFeedbackRegular, DismissRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
3
+ import { tokens, makeStyles, Tooltip as Tooltip$1, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, useFluent, Accordion as Accordion$1, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, MessageBar as MessageBar$1, MessageBarBody, AccordionItem, SearchBox as SearchBox$1, Portal, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, Body1Strong, mergeClasses, useId, useToastController, Toast, ToastTitle, FluentProvider, Toaster, Checkbox as Checkbox$1, createLightTheme, createDarkTheme, createDOMRenderer, RendererProvider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, Switch as Switch$1, treeItemLevelToken, typographyStyles, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, MenuItemCheckbox, useMergedRefs, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, PresenceBadge, Slider as Slider$1, MenuItemRadio, Dialog as Dialog$1, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, MessageBarTitle, useComboboxFilter, Combobox, Subtitle2, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, DialogTrigger, Field } from '@fluentui/react-components';
4
+ import { ErrorCircleRegular, EyeFilled, EyeOffRegular, CheckmarkFilled, EditRegular, FilterRegular, PinFilled, PinRegular, ArrowCircleUpRegular, ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, CopyRegular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, SettingsRegular, DocumentTextRegular, createFluentIcon, TextSortAscendingRegular, GlobeRegular, WarningRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, ArrowUploadRegular, ArrowBidirectionalUpDownFilled, ArrowDownloadRegular, StopRegular, RecordRegular, DataBarHorizontalRegular, WrenchRegular, ArrowClockwiseRegular, WeatherSunnyRegular, WeatherMoonRegular, InfoRegular, CheckmarkCircleRegular, PlugDisconnectedRegular, PlugConnectedRegular, PlugConnectedCheckmarkRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, CameraRegular, AddRegular, DeleteRegular, FullScreenMaximizeRegular, ChevronDownRegular, ChevronRightRegular, CircleSmallFilled, SaveRegular, PreviousRegular, ArrowPreviousRegular, TriangleLeftRegular, RecordStopRegular, PlayRegular, ArrowNextRegular, NextRegular, PauseRegular, LinkDismissRegular, LinkEditRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, EyeRegular, CloudArrowUpRegular, CloudArrowDownRegular, EyeOffFilled, ArrowMoveFilled, StopFilled, PlayFilled, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, HeadphonesSoundWaveRegular, ArrowEnterUpRegular, SoundWaveCircleRegular, SoundWaveCircleFilled, CatchUpRegular, LayerRegular, FrameRegular, AppGenericRegular, RectangleLandscapeRegular, BorderOutsideRegular, BorderNoneRegular, MyLocationRegular, BubbleMultipleRegular, LightbulbRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, LinkRegular, ArrowSyncRegular, TargetRegular, PersonFeedbackRegular, DismissRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
5
5
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
6
6
  import { Vector3, Quaternion, Matrix, Vector2, Vector4, TmpVectors } from '@babylonjs/core/Maths/math.vector.js';
7
7
  import { Observable } from '@babylonjs/core/Misc/observable.js';
@@ -42,6 +42,7 @@ import { FrameGraphUtils, FindMainCamera } from '@babylonjs/core/FrameGraph/fram
42
42
  import { CameraGizmo } from '@babylonjs/core/Gizmos/cameraGizmo.js';
43
43
  import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager.js';
44
44
  import { LightGizmo } from '@babylonjs/core/Gizmos/lightGizmo.js';
45
+ import { SpatialAudioGizmo } from '@babylonjs/core/Gizmos/spatialAudioGizmo.js';
45
46
  import { Light } from '@babylonjs/core/Lights/light.js';
46
47
  import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js';
47
48
  import { Node as Node$1 } from '@babylonjs/core/node.js';
@@ -53,6 +54,13 @@ import { AnimationGroup, TargetedAnimation } from '@babylonjs/core/Animations/an
53
54
  import { Animation } from '@babylonjs/core/Animations/animation.js';
54
55
  import { AnimationPropertiesOverride } from '@babylonjs/core/Animations/animationPropertiesOverride.js';
55
56
  import { Sound } from '@babylonjs/core/Audio/sound.js';
57
+ import { AbstractAudioBus } from '@babylonjs/core/AudioV2/abstractAudio/abstractAudioBus.js';
58
+ import { AbstractSound } from '@babylonjs/core/AudioV2/abstractAudio/abstractSound.js';
59
+ import { AbstractSoundSource } from '@babylonjs/core/AudioV2/abstractAudio/abstractSoundSource.js';
60
+ import { AudioBus } from '@babylonjs/core/AudioV2/abstractAudio/audioBus.js';
61
+ import { AudioEngineV2, OnAudioEngineV2CreatedObservable, LastCreatedAudioEngine } from '@babylonjs/core/AudioV2/abstractAudio/audioEngineV2.js';
62
+ import { StaticSound } from '@babylonjs/core/AudioV2/abstractAudio/staticSound.js';
63
+ import { StreamingSound } from '@babylonjs/core/AudioV2/abstractAudio/streamingSound.js';
56
64
  import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera.js';
57
65
  import { FollowCamera } from '@babylonjs/core/Cameras/followCamera.js';
58
66
  import { FreeCamera } from '@babylonjs/core/Cameras/freeCamera.js';
@@ -135,6 +143,7 @@ import { EnvCubeTexture } from '@babylonjs/core/Materials/Textures/envCubeTextur
135
143
  import { MultiRenderTarget } from '@babylonjs/core/Materials/Textures/multiRenderTarget.js';
136
144
  import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture.js';
137
145
  import { ThinTexture } from '@babylonjs/core/Materials/Textures/thinTexture.js';
146
+ import { MainAudioBus } from '@babylonjs/core/AudioV2/abstractAudio/mainAudioBus.js';
138
147
  import '@babylonjs/core/Rendering/boundingBoxRenderer.js';
139
148
  import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent.js';
140
149
  import '@babylonjs/core/Sprites/spriteSceneComponent.js';
@@ -144,7 +153,9 @@ import { SceneRecorder } from '@babylonjs/core/Misc/sceneRecorder.js';
144
153
  import { VideoRecorder } from '@babylonjs/core/Misc/videoRecorder.js';
145
154
  import { SceneSerializer } from '@babylonjs/core/Misc/sceneSerializer.js';
146
155
  import { EnvironmentTextureTools } from '@babylonjs/core/Misc/environmentTextureTools.js';
147
- import { SerializeSmartAssetManagerMap, GetAllSmartAssets, RemoveSmartAssetAsync, LoadSmartAssetMapAsync, GetSmartAssetTextureExtensions, GetSmartAssetManager, LoadSmartAssetTextureAsync, LoadSmartAssetAsync, ReloadSmartAssetAsync, FindSmartAssetKeyForObject, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.js';
156
+ import { FindSmartAssetKeyForObject, SerializeSmartAssetManagerMap, GetSmartAssetTextureExtensions, GetAllSmartAssets, RemoveSmartAssetAsync, RegisterSmartAsset, LoadAllSmartAssetsAsync, LoadSmartAssetAsync, GetSmartAssetManager, LoadSmartAssetTextureAsync, ReloadSmartAssetAsync, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.js';
157
+ import '@babylonjs/core/Loading/Plugins/babylonFileLoader.js';
158
+ import { ReadJsonSourceAsync, ResolveAssetUrl, DeserializeSmartAssetMap } from '@babylonjs/core/SmartAssets/smartAssetSerializer.js';
148
159
  import { ImportAnimationsAsync, SceneLoader } from '@babylonjs/core/Loading/sceneLoader.js';
149
160
  import { FilesInput } from '@babylonjs/core/Misc/filesInput.js';
150
161
  import { registeredGLTFExtensions } from '@babylonjs/loaders/glTF/2.0/glTFLoaderExtensionRegistry.js';
@@ -246,11 +257,11 @@ function ValidateColorHex(val) {
246
257
  // forwardRef wrapper to avoid "function components cannot be given refs" warning
247
258
  // FluentTooltip handles ref forwarding to children internally via applyTriggerPropsToChildren
248
259
  const Tooltip = forwardRef((props, _ref) => {
249
- const { content, children } = props;
260
+ const { content, positioning, children } = props;
250
261
  if (!content) {
251
262
  return children;
252
263
  }
253
- return (jsx(Tooltip$1, { relationship: "description", content: content, children: children }));
264
+ return (jsx(Tooltip$1, { relationship: "description", content: content, positioning: positioning, children: children }));
254
265
  });
255
266
  Tooltip.displayName = "Tooltip";
256
267
 
@@ -266,7 +277,7 @@ const Button = forwardRef((props, ref) => {
266
277
  const { size } = useContext(ToolContext);
267
278
  const classes = useButtonStyles();
268
279
  // eslint-disable-next-line @typescript-eslint/naming-convention
269
- const { icon: Icon, label, onClick, disabled, className, title, ...buttonProps } = props;
280
+ const { icon: Icon, label, onClick, disabled, className, title, ariaLabel, ...buttonProps } = props;
270
281
  const [isOnClickBusy, setIsOnClickBusy] = useState(false);
271
282
  const handleOnClick = useCallback(async (e) => {
272
283
  const result = onClick?.(e);
@@ -281,7 +292,7 @@ const Button = forwardRef((props, ref) => {
281
292
  }
282
293
  }, [onClick]);
283
294
  const iconClass = size === "small" ? classes.smallIcon : classes.mediumIcon;
284
- return (jsx(Tooltip, { content: title ?? "", children: jsx(Button$1, { ref: ref, iconPosition: "after", ...buttonProps, className: className, size: size, icon: isOnClickBusy ? jsx(Spinner, { size: "extra-tiny" }) : Icon && jsx(Icon, { className: iconClass }), onClick: handleOnClick, disabled: disabled || isOnClickBusy, children: label && props.label }) }));
295
+ return (jsx(Tooltip, { content: title ?? "", children: jsx(Button$1, { ref: ref, iconPosition: "after", ...buttonProps, className: className, size: size, "aria-label": ariaLabel ?? (!label ? title : undefined), icon: isOnClickBusy ? jsx(Spinner, { size: "extra-tiny" }) : Icon && jsx(Icon, { className: iconClass }), onClick: handleOnClick, disabled: disabled || isOnClickBusy, children: label && props.label }) }));
285
296
  });
286
297
  Button.displayName = "Button";
287
298
 
@@ -294,6 +305,9 @@ const useStyles$11 = makeStyles({
294
305
  padding: TokenMap.px20,
295
306
  backgroundColor: tokens.colorNeutralBackground1,
296
307
  color: tokens.colorNeutralForeground1,
308
+ // Claim the full row of the flex parent (e.g. shellService's central-content row)
309
+ // so the centered children sit in the middle of the available area, not at the left edge.
310
+ flex: 1,
297
311
  height: "100%",
298
312
  minHeight: "100px",
299
313
  },
@@ -464,9 +478,13 @@ function InterceptProperty(target, propertyKey, hooks) {
464
478
  // Replace the property with a new one that calls the hooks in addition to the original getter and setter.
465
479
  !Reflect.defineProperty(target, propertyKey, {
466
480
  configurable: true,
467
- get: getValue ? () => getValue.call(target) : undefined,
468
- set: (newValue) => {
469
- setValue.call(target, newValue);
481
+ get: getValue
482
+ ? function () {
483
+ return getValue.call(this);
484
+ }
485
+ : undefined,
486
+ set: function (newValue) {
487
+ setValue.call(this, newValue);
470
488
  for (const { afterSet } of hooksForKey) {
471
489
  afterSet?.(newValue);
472
490
  }
@@ -1691,7 +1709,7 @@ const useStyles$$ = makeStyles({
1691
1709
  */
1692
1710
  const ToggleButton = (props) => {
1693
1711
  ToggleButton.displayName = "ToggleButton";
1694
- const { value, onChange, title, appearance = "subtle" } = props;
1712
+ const { value, onChange, title, appearance = "subtle", ariaLabel } = props;
1695
1713
  const { size } = useContext(ToolContext);
1696
1714
  const classes = useStyles$$();
1697
1715
  const [checked, setChecked] = useState(value);
@@ -1705,7 +1723,7 @@ const ToggleButton = (props) => {
1705
1723
  useEffect(() => {
1706
1724
  setChecked(props.value);
1707
1725
  }, [props.value]);
1708
- return (jsx(Tooltip, { content: title ?? "", children: jsx(ToggleButton$1, { className: classes.button, size: size, icon: checked ? jsx(props.checkedIcon, {}) : props.uncheckedIcon ? jsx(props.uncheckedIcon, {}) : jsx(props.checkedIcon, {}), appearance: appearance, checked: checked, onClick: toggle }) }));
1726
+ return (jsx(Tooltip, { content: title ?? "", positioning: props.titlePositioning, children: jsx(ToggleButton$1, { className: classes.button, size: size, "aria-label": ariaLabel ?? title, icon: checked ? jsx(props.checkedIcon, {}) : props.uncheckedIcon ? jsx(props.uncheckedIcon, {}) : jsx(props.checkedIcon, {}), appearance: appearance, checked: checked, onClick: toggle }) }));
1709
1727
  };
1710
1728
 
1711
1729
  const useInfoLabelStyles = makeStyles({
@@ -2434,16 +2452,39 @@ const useStyles$Y = makeStyles({
2434
2452
  /**
2435
2453
  * A themed Fluent UI provider that applies the current theme mode (light or dark).
2436
2454
  * @param props Fluent provider props, plus an optional `invert` flag to swap the theme.
2455
+ * When `targetDocument` is provided and differs from the inherited Fluent
2456
+ * document (e.g. when rendering into a popup window), a Griffel renderer
2457
+ * scoped to that document is created so styles are injected into it.
2458
+ * When omitted, `targetDocument` is inherited from the ambient Fluent
2459
+ * context so nested Theme components do not lose cross-window targeting.
2437
2460
  * @returns The themed Fluent UI provider component.
2438
2461
  */
2439
2462
  const Theme = (props) => {
2440
2463
  // NOTE: We do not want to applyStylesToPortals by default. It makes classes flow into portals
2441
2464
  // (like popovers), and if those styles do things like disable overflow, they can completely
2442
2465
  // break any UI within the portal. Therefore, default to false.
2443
- const { invert = false, applyStylesToPortals = false, className, ...rest } = props;
2466
+ const { invert = false, applyStylesToPortals = false, className, targetDocument: explicitTargetDocument, ...rest } = props;
2444
2467
  const theme = useTheme(invert);
2445
2468
  const classes = useStyles$Y();
2446
- return (jsx(FluentProvider, { theme: theme, className: mergeClasses(classes.root, className), applyStylesToPortals: applyStylesToPortals, ...rest, children: props.children }));
2469
+ // Resolve the target document from the explicit prop or fall back to the ambient Fluent context.
2470
+ // This makes nested <Theme> components automatically inherit cross-window targeting from a
2471
+ // top-level <Theme targetDocument={popupDocument}> wrapper.
2472
+ const inheritedTargetDocument = useFluent().targetDocument;
2473
+ const resolvedTargetDocument = explicitTargetDocument ?? inheritedTargetDocument;
2474
+ // Only create a new Griffel renderer when the resolved document differs from the inherited
2475
+ // one. In the common (main-window, no nesting) case, this leaves Fluent's default renderer
2476
+ // and renderer provider in place — matching the original behaviour exactly.
2477
+ const renderer = useMemo(() => {
2478
+ if (resolvedTargetDocument && resolvedTargetDocument !== inheritedTargetDocument) {
2479
+ return createDOMRenderer(resolvedTargetDocument);
2480
+ }
2481
+ return undefined;
2482
+ }, [resolvedTargetDocument, inheritedTargetDocument]);
2483
+ const fluent = (jsx(FluentProvider, { theme: theme, className: mergeClasses(classes.root, className), applyStylesToPortals: applyStylesToPortals, targetDocument: resolvedTargetDocument, ...rest, children: props.children }));
2484
+ if (renderer && resolvedTargetDocument) {
2485
+ return (jsx(RendererProvider, { renderer: renderer, targetDocument: resolvedTargetDocument, children: fluent }));
2486
+ }
2487
+ return fluent;
2447
2488
  };
2448
2489
 
2449
2490
  const useStyles$X = makeStyles({
@@ -2682,24 +2723,161 @@ function ConstructorFactory(constructor) {
2682
2723
  return (...args) => new constructor(...args);
2683
2724
  }
2684
2725
 
2685
- function ToFeaturesString(options) {
2686
- const { defaultWidth, defaultHeight, defaultLeft, defaultTop } = options;
2687
- const features = [];
2688
- if (defaultWidth !== undefined) {
2689
- features.push({ key: "width", value: defaultWidth.toString() });
2726
+ const StorageKeyPrefix = "Babylon/Settings/PopupWindow";
2727
+ function LoadSavedBounds(id) {
2728
+ const stored = localStorage.getItem(`${StorageKeyPrefix}/${id}/Bounds`);
2729
+ if (!stored) {
2730
+ return null;
2731
+ }
2732
+ try {
2733
+ const parsed = JSON.parse(stored);
2734
+ return parsed;
2690
2735
  }
2691
- if (defaultHeight !== undefined) {
2692
- features.push({ key: "height", value: defaultHeight.toString() });
2736
+ catch {
2737
+ Logger.Warn(`Could not parse saved bounds for popup window with id ${id}`);
2738
+ return null;
2739
+ }
2740
+ }
2741
+ function SaveBounds(id, bounds) {
2742
+ try {
2743
+ localStorage.setItem(`${StorageKeyPrefix}/${id}/Bounds`, JSON.stringify(bounds));
2693
2744
  }
2694
- if (defaultLeft !== undefined) {
2695
- features.push({ key: "left", value: defaultLeft.toString() });
2745
+ catch {
2746
+ // Storage may be full / disabled — bounds simply won't persist.
2696
2747
  }
2697
- if (defaultTop !== undefined) {
2698
- features.push({ key: "top", value: defaultTop.toString() });
2748
+ }
2749
+ function ResolveBounds(options) {
2750
+ const saved = options.id ? LoadSavedBounds(options.id) : null;
2751
+ const width = options.defaultWidth ?? saved?.width ?? Math.floor(window.innerWidth * (2 / 3));
2752
+ const height = options.defaultHeight ?? saved?.height ?? Math.floor(window.innerHeight * (2 / 3));
2753
+ const left = options.defaultLeft ?? saved?.left ?? Math.floor(window.screenX + (window.innerWidth - width) / 2);
2754
+ const top = options.defaultTop ?? saved?.top ?? Math.floor(window.screenY + (window.innerHeight - height) / 2);
2755
+ // When the caller passes explicit width/height/left/top, always honour them; otherwise fall
2756
+ // back to the (saved or computed) values above. The order above already gives explicit
2757
+ // options precedence, so just build the resulting bounds now.
2758
+ return { left, top, width, height };
2759
+ }
2760
+ function ToFeaturesString(bounds) {
2761
+ return `width=${bounds.width},height=${bounds.height},left=${bounds.left},top=${bounds.top},location=no`;
2762
+ }
2763
+ /**
2764
+ * Opens a new browser popup window suitable for hosting a Fluent-based modular tool.
2765
+ *
2766
+ * The popup body is configured for full-bleed flex layout and a host `<div>` is appended
2767
+ * for the tool to render into. Fluent style targeting (Griffel `RendererProvider`,
2768
+ * `FluentProvider` with `targetDocument`) is the caller's responsibility — typically wired
2769
+ * up by `MakeModularTool`, which derives `targetDocument` from `containerElement.ownerDocument`.
2770
+ *
2771
+ * **Must be called synchronously in response to a user interaction** (e.g. button click) —
2772
+ * otherwise the browser will block the popup as a scripted popup.
2773
+ *
2774
+ * @param options Window options. See {@link PopupWindowOptions}.
2775
+ * @returns A handle to the popup window and its host element, plus a `dispose` to close it.
2776
+ * `null` if the popup was blocked by the browser.
2777
+ */
2778
+ function OpenPopupWindow(options = {}) {
2779
+ const bounds = ResolveBounds(options);
2780
+ const popupWindow = window.open("", "", ToFeaturesString(bounds));
2781
+ if (!popupWindow) {
2782
+ return null;
2699
2783
  }
2700
- features.push({ key: "location", value: "no" });
2701
- return features.map((feature) => `${feature.key}=${feature.value}`).join(",");
2784
+ if (options.title) {
2785
+ popupWindow.document.title = options.title;
2786
+ }
2787
+ const popupBody = popupWindow.document.body;
2788
+ popupBody.style.width = "100%";
2789
+ popupBody.style.height = "100%";
2790
+ popupBody.style.margin = "0";
2791
+ popupBody.style.padding = "0";
2792
+ popupBody.style.display = "flex";
2793
+ popupBody.style.overflow = "hidden";
2794
+ const hostElement = popupWindow.document.createElement("div");
2795
+ hostElement.style.display = "flex";
2796
+ hostElement.style.flexDirection = "column";
2797
+ hostElement.style.flexGrow = "1";
2798
+ hostElement.style.width = "100%";
2799
+ hostElement.style.height = "100%";
2800
+ hostElement.style.margin = "0";
2801
+ hostElement.style.padding = "0";
2802
+ hostElement.style.overflow = "hidden";
2803
+ popupBody.appendChild(hostElement);
2804
+ // Track the most recently observed window bounds. In some browsers (e.g. Firefox), accessing
2805
+ // properties like screenX on a closed window throws, so we cache the last known good values
2806
+ // to use as a fallback when saving after the window has already been closed.
2807
+ const getBounds = () => ({
2808
+ left: popupWindow.screenX,
2809
+ top: popupWindow.screenY,
2810
+ width: popupWindow.innerWidth,
2811
+ height: popupWindow.innerHeight,
2812
+ });
2813
+ let lastBounds = bounds;
2814
+ const onPopupBeforeUnload = () => {
2815
+ try {
2816
+ lastBounds = getBounds();
2817
+ }
2818
+ catch {
2819
+ // Use the cached lastBounds.
2820
+ }
2821
+ };
2822
+ popupWindow.addEventListener("beforeunload", onPopupBeforeUnload);
2823
+ let disposed = false;
2824
+ // Internal cleanup: tears down listeners, persists bounds, closes the popup.
2825
+ // Does NOT invoke `options.onClose` — that's reserved for popup unload events
2826
+ // that originate from outside our own teardown (e.g. user dismissed the popup).
2827
+ const cleanup = () => {
2828
+ if (disposed) {
2829
+ return;
2830
+ }
2831
+ disposed = true;
2832
+ if (options.id) {
2833
+ try {
2834
+ if (!popupWindow.closed) {
2835
+ lastBounds = getBounds();
2836
+ }
2837
+ }
2838
+ catch {
2839
+ // Use the cached lastBounds.
2840
+ }
2841
+ SaveBounds(options.id, lastBounds);
2842
+ }
2843
+ popupWindow.removeEventListener("beforeunload", onPopupBeforeUnload);
2844
+ // Remove the unload listener so that any pending unload event triggered by our
2845
+ // own popupWindow.close() below cannot reach back into onPopupUnload after we've
2846
+ // already torn down. This avoids a race where, during a programmatic open-while-open
2847
+ // swap, the old popup's unload would otherwise fire onClose and clear React state
2848
+ // pointing at the new popup.
2849
+ popupWindow.removeEventListener("unload", onPopupUnload);
2850
+ window.removeEventListener("unload", onParentUnload);
2851
+ if (!popupWindow.closed) {
2852
+ popupWindow.close();
2853
+ }
2854
+ };
2855
+ const onPopupUnload = () => {
2856
+ if (disposed) {
2857
+ // Already torn down programmatically; the popup is just finishing its unload.
2858
+ return;
2859
+ }
2860
+ cleanup();
2861
+ // Notify the consumer only for externally-triggered closures (user dismissed the popup,
2862
+ // tab/browser closed). Programmatic disposal calls cleanup() directly and intentionally
2863
+ // skips this so callers don't get a re-entrant onClose for the close they themselves issued.
2864
+ options.onClose?.();
2865
+ };
2866
+ popupWindow.addEventListener("unload", onPopupUnload, { once: true });
2867
+ // If the parent window is unloaded (page refresh / navigation), don't leave the popup orphaned.
2868
+ const onParentUnload = () => {
2869
+ if (!popupWindow.closed) {
2870
+ popupWindow.close();
2871
+ }
2872
+ };
2873
+ window.addEventListener("unload", onParentUnload, { once: true });
2874
+ return {
2875
+ popupWindow,
2876
+ hostElement,
2877
+ dispose: cleanup,
2878
+ };
2702
2879
  }
2880
+
2703
2881
  /**
2704
2882
  * Allows displaying a child window that can contain child components.
2705
2883
  * @param props Props for the child window.
@@ -2708,133 +2886,66 @@ function ToFeaturesString(options) {
2708
2886
  const ChildWindow = (props) => {
2709
2887
  const { id, children, onOpenChange, imperativeRef: imperativeRef } = props;
2710
2888
  const [windowState, setWindowState] = useState();
2711
- const [childWindow, setChildWindow] = useState();
2712
- const storageKey = id ? `Babylon/Settings/ChildWindow/${id}/Bounds` : null;
2889
+ const [popupHandle, setPopupHandle] = useState();
2713
2890
  // This function is just for creating the child window itself. It is a function because
2714
2891
  // it must be called synchronously in response to a user interaction (e.g. button click),
2715
2892
  // otherwise the browser will block it as a scripted popup.
2716
2893
  const createWindow = useCallback((options = {}) => {
2717
- if (storageKey) {
2718
- // If we are persisting window bounds, but the window is already open, just use the existing bounds.
2719
- // Otherwise, try to load bounds from storage.
2720
- if (childWindow) {
2721
- options.defaultLeft = childWindow.screenX;
2722
- options.defaultTop = childWindow.screenY;
2723
- options.defaultWidth = childWindow.innerWidth;
2724
- options.defaultHeight = childWindow.innerHeight;
2725
- }
2726
- else {
2727
- const savedBounds = localStorage.getItem(storageKey);
2728
- if (savedBounds) {
2729
- try {
2730
- const bounds = JSON.parse(savedBounds);
2731
- options.defaultLeft = bounds.left;
2732
- options.defaultTop = bounds.top;
2733
- options.defaultWidth = bounds.width;
2734
- options.defaultHeight = bounds.height;
2735
- }
2736
- catch {
2737
- Logger.Warn(`Could not parse saved bounds for child window with key ${storageKey}`);
2738
- }
2739
- }
2740
- }
2741
- }
2742
- // Half width by default.
2743
- if (!options.defaultWidth) {
2744
- options.defaultWidth = window.innerWidth * (2 / 3);
2745
- }
2746
- // Half height by default.
2747
- if (!options.defaultHeight) {
2748
- options.defaultHeight = window.innerHeight * (2 / 3);
2749
- }
2750
- // Horizontally centered by default.
2751
- if (!options.defaultLeft) {
2752
- options.defaultLeft = window.screenX + (window.innerWidth - options.defaultWidth) * (2 / 3);
2753
- }
2754
- // Vertically centered by default.
2755
- if (!options.defaultTop) {
2756
- options.defaultTop = window.screenY + (window.innerHeight - options.defaultHeight) * (2 / 3);
2757
- }
2758
- // Try to create the child window (can be null if popups are blocked).
2759
- const newChildWindow = window.open("", "", ToFeaturesString(options));
2760
- if (newChildWindow) {
2761
- // Set the title if provided.
2762
- newChildWindow.document.title = options.title ?? id ?? "";
2763
- // Set the child window state.
2764
- setChildWindow((current) => {
2765
- // But first close any existing child window.
2766
- current?.close();
2767
- return newChildWindow;
2894
+ const handle = OpenPopupWindow({
2895
+ id,
2896
+ title: options.title ?? id,
2897
+ defaultWidth: options.defaultWidth,
2898
+ defaultHeight: options.defaultHeight,
2899
+ defaultLeft: options.defaultLeft,
2900
+ defaultTop: options.defaultTop,
2901
+ onClose: () => {
2902
+ // Triggered when the popup is closed for any reason (user dismissal, parent unload,
2903
+ // or programmatic dispose). Clear the popup handle so the effect cleanup runs and
2904
+ // `onOpenChange(false)` is propagated up the tree (which lets a parent shell re-dock
2905
+ // a previously-undocked pane). teardown() is idempotent so calling dispose() again
2906
+ // from the effect cleanup is a safe no-op.
2907
+ setPopupHandle(undefined);
2908
+ },
2909
+ });
2910
+ if (handle) {
2911
+ setPopupHandle((current) => {
2912
+ // Close any existing child window before adopting the new one.
2913
+ current?.dispose();
2914
+ return handle;
2768
2915
  });
2769
2916
  }
2770
- }, [childWindow, storageKey]);
2917
+ }, [id]);
2771
2918
  useImperativeHandle(imperativeRef, () => {
2772
2919
  return {
2773
2920
  open: createWindow,
2774
- close: () => setChildWindow(undefined),
2921
+ close: () => {
2922
+ setPopupHandle((current) => {
2923
+ current?.dispose();
2924
+ return undefined;
2925
+ });
2926
+ },
2775
2927
  };
2776
2928
  }, [createWindow]);
2777
- // This side effect runs any time the child window instance changes. It does the rest of the child window
2929
+ // This side effect runs any time the popup handle changes. It does the rest of the child window
2778
2930
  // setup work, including creating resources and state needed to properly render the content of the child window.
2779
2931
  useEffect(() => {
2780
- const disposeActions = [];
2781
- if (childWindow) {
2782
- const body = childWindow.document.body;
2783
- body.style.width = "100%";
2784
- body.style.height = "100%";
2785
- body.style.margin = "0";
2786
- body.style.padding = "0";
2787
- body.style.display = "flex";
2788
- body.style.overflow = "hidden";
2789
- // Setup the window state, including creating a Fluent/Griffel "renderer" for managing runtime styles/classes in the child window.
2790
- setWindowState({ mountNode: body, renderer: createDOMRenderer(childWindow.document) });
2791
- onOpenChange?.(true);
2792
- // Track the most recently observed window bounds. In some browsers (e.g. Firefox), accessing
2793
- // properties like screenX on a closed window throws, so we cache the last known good values
2794
- // to use as a fallback when the dispose runs after the window has already been closed.
2795
- const getBounds = () => ({
2796
- left: childWindow.screenX,
2797
- top: childWindow.screenY,
2798
- width: childWindow.innerWidth,
2799
- height: childWindow.innerHeight,
2800
- });
2801
- let lastBounds = getBounds();
2802
- // When the child window is closed for any reason, transition back to a closed state.
2803
- const onChildWindowUnload = () => {
2804
- setWindowState(undefined);
2805
- setChildWindow(undefined);
2806
- onOpenChange?.(false);
2807
- };
2808
- childWindow.addEventListener("unload", onChildWindowUnload, { once: true });
2809
- disposeActions.push(() => childWindow.removeEventListener("unload", onChildWindowUnload));
2810
- // Capture bounds before the window is unloaded, while its properties are still safe to read.
2811
- const onChildWindowBeforeUnload = () => {
2812
- lastBounds = getBounds();
2813
- };
2814
- childWindow.addEventListener("beforeunload", onChildWindowBeforeUnload);
2815
- disposeActions.push(() => childWindow.removeEventListener("beforeunload", onChildWindowBeforeUnload));
2816
- // If the main window closes, close any open child windows as well (don't leave them orphaned).
2817
- const onParentWindowUnload = () => {
2818
- childWindow.close();
2819
- };
2820
- window.addEventListener("unload", onParentWindowUnload, { once: true });
2821
- disposeActions.push(() => window.removeEventListener("unload", onParentWindowUnload));
2822
- // On dispose, close the child window.
2823
- disposeActions.push(() => childWindow.close());
2824
- // On dispose, save the window bounds.
2825
- disposeActions.push(() => {
2826
- if (storageKey) {
2827
- if (!childWindow.closed) {
2828
- lastBounds = getBounds();
2829
- }
2830
- localStorage.setItem(storageKey, JSON.stringify(lastBounds));
2831
- }
2832
- });
2932
+ if (!popupHandle) {
2933
+ return undefined;
2833
2934
  }
2935
+ const popupDocument = popupHandle.popupWindow.document;
2936
+ // Use the popup's hostElement as the React mount point. OpenPopupWindow configures it as a
2937
+ // flex column that fills the popup body, so the FluentProvider (also a flex column with
2938
+ // flex-grow: 1) inside it can fill the available space.
2939
+ setWindowState({ mountNode: popupHandle.hostElement, renderer: createDOMRenderer(popupDocument) });
2940
+ onOpenChange?.(true);
2834
2941
  return () => {
2835
- disposeActions.reverse().forEach((dispose) => dispose());
2942
+ // Tear down the popup. The cached handle's dispose() handles bounds saving and
2943
+ // listener cleanup; React state is reset so the Portal/Provider tree unmounts.
2944
+ popupHandle.dispose();
2945
+ setWindowState(undefined);
2946
+ onOpenChange?.(false);
2836
2947
  };
2837
- }, [childWindow]);
2948
+ }, [popupHandle]);
2838
2949
  if (!windowState) {
2839
2950
  return null;
2840
2951
  }
@@ -2957,6 +3068,7 @@ const useStyles$W = makeStyles({
2957
3068
  paneCollapseButton: {
2958
3069
  padding: `0 0 0 ${tokens.spacingHorizontalXS}`,
2959
3070
  borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
3071
+ backgroundColor: tokens.colorNeutralBackground2,
2960
3072
  },
2961
3073
  paneCollapseButtonWithBorder: {
2962
3074
  borderLeft: `1px solid ${tokens.colorNeutralStroke2}`,
@@ -2980,6 +3092,11 @@ const useStyles$W = makeStyles({
2980
3092
  paneContainer: {
2981
3093
  display: "flex",
2982
3094
  flexDirection: "column",
3095
+ // Side panes hold the width requested by their `style.width` (or saved/dragged value)
3096
+ // and never give it up to a neighboring pane growing. Without this, dragging the left
3097
+ // pane wider would also squeeze the right pane (proportional flex-shrink). The central
3098
+ // content (`flex-grow: 1`) is the only flex item that absorbs the change.
3099
+ flexShrink: 0,
2983
3100
  overflowX: "hidden",
2984
3101
  overflowY: "hidden",
2985
3102
  zIndex: 1,
@@ -3291,16 +3408,25 @@ function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane,
3291
3408
  // registered). The Fluent hook's setValue will silently fail when the element doesn't exist (in relative mode,
3292
3409
  // it measures the element before/after and reverts if unchanged, which always happens when the element is null).
3293
3410
  // By composing the ref callback, we ensure the stored value is applied immediately after the element mounts.
3411
+ //
3412
+ // We also set the CSS variable directly as a fallback. The hook preserves its internal currentValue across
3413
+ // re-mounts, so when (for example) the user undocks a resized pane and then re-docks it, currentValue already
3414
+ // equals the persisted setting and the hook's setValue short-circuits — but the freshly-mounted DOM node has no
3415
+ // inline CSS variable, so the pane visually reverts to the default width until the user drags. Setting the
3416
+ // variable directly closes that gap. The order is important: setValue first (handles the first-mount case where
3417
+ // currentValue starts at 0), then the direct setProperty as a redundant fallback for the no-op case.
3294
3418
  const composedHorizontalElementRef = useCallback((node) => {
3295
3419
  paneHorizontalResizeElementRef(node);
3296
3420
  if (node) {
3297
3421
  setPaneWidthAdjust(paneWidthSettingRef.current);
3422
+ node.style.setProperty(paneWidthAdjustCSSVar, `${paneWidthSettingRef.current}px`);
3298
3423
  }
3299
3424
  }, [paneHorizontalResizeElementRef, setPaneWidthAdjust]);
3300
3425
  const composedVerticalElementRef = useCallback((node) => {
3301
3426
  paneVerticalResizeElementRef(node);
3302
3427
  if (node) {
3303
3428
  setPaneHeightAdjust(paneHeightSettingRef.current);
3429
+ node.style.setProperty(paneHeightAdjustCSSVar, `${paneHeightSettingRef.current}px`);
3304
3430
  }
3305
3431
  }, [paneVerticalResizeElementRef, setPaneHeightAdjust]);
3306
3432
  // Handle external setting changes (e.g. settings reset) after elements are already mounted.
@@ -4279,6 +4405,37 @@ const useStyles$U = makeStyles({
4279
4405
  function GetCommandDescription(command) {
4280
4406
  return command.hotKey ? `${command.displayName} (${GetCommandHotKeyDescription(command)})` : command.displayName;
4281
4407
  }
4408
+ const useTruncatingBody1Styles = makeStyles({
4409
+ container: {
4410
+ display: "block",
4411
+ overflow: "hidden",
4412
+ textOverflow: "ellipsis",
4413
+ whiteSpace: "nowrap",
4414
+ minWidth: 0,
4415
+ ...typographyStyles.body1,
4416
+ },
4417
+ });
4418
+ const TruncatingBody1 = (props) => {
4419
+ const { text } = props;
4420
+ const classes = useTruncatingBody1Styles();
4421
+ const ref = useRef(null);
4422
+ const [isTruncated, setIsTruncated] = useState(false);
4423
+ const [isHovered, setIsHovered] = useState(false);
4424
+ useEffect(() => {
4425
+ const element = ref.current;
4426
+ if (!element) {
4427
+ return undefined;
4428
+ }
4429
+ const update = () => {
4430
+ setIsTruncated(element.scrollWidth > element.clientWidth);
4431
+ };
4432
+ update();
4433
+ const observer = new ResizeObserver(update);
4434
+ observer.observe(element);
4435
+ return () => observer.disconnect();
4436
+ }, [text]);
4437
+ return (jsx(Tooltip$1, { content: text, positioning: "after", relationship: "description", visible: isTruncated && isHovered, onVisibleChange: (_, data) => setIsHovered(data.visible), children: jsx("span", { ref: ref, className: classes.container, children: text }) }));
4438
+ };
4282
4439
  const ActionCommand = (props) => {
4283
4440
  const { command } = props;
4284
4441
  // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -4290,7 +4447,7 @@ const ToggleCommand = (props) => {
4290
4447
  // eslint-disable-next-line @typescript-eslint/naming-convention
4291
4448
  const [Icon, isEnabled] = useObservableState(useCallback(() => [command.icon, command.isEnabled], [command]), command.onChange);
4292
4449
  // TODO-iv2: Consolidate icon prop passing approach for inspector and shared components
4293
- return (jsx(ToggleButton, { appearance: "transparent", title: GetCommandDescription(command), checkedIcon: Icon, value: isEnabled, onChange: (val) => (command.isEnabled = val) }));
4450
+ return (jsx(ToggleButton, { appearance: "transparent", title: GetCommandDescription(command), titlePositioning: "after", checkedIcon: Icon, value: isEnabled, onChange: (val) => (command.isEnabled = val) }));
4294
4451
  };
4295
4452
  // This "placeholder" command has a blank icon and is a no-op. It is used for aside
4296
4453
  // alignment when some toggle commands are enabled. See more details on the commands
@@ -4450,7 +4607,7 @@ const EntityTreeItem = (props) => {
4450
4607
  }, main: {
4451
4608
  // Prevent the "main" content (the Body1 below) from growing too large and pushing the actions/aside out of view.
4452
4609
  className: classes.treeItemLayoutMain,
4453
- }, children: jsx(Tooltip$1, { content: name, relationship: "description", children: jsx(Body1, { wrap: false, truncate: true, children: name }) }) }) }, GetEntityId$1(entityItem.entity)) }), jsx(MenuPopover, { hidden: !hasChildren && contextMenuCommands.length === 0, children: jsxs(MenuList, { children: [hasChildren && (jsxs(Fragment, { children: [jsx(MenuItem, { icon: jsx(ArrowExpandAllRegular, {}), onClick: expandAll, children: jsx(Body1, { children: "Expand All" }) }), jsx(MenuItem, { icon: jsx(ArrowCollapseAllRegular, {}), onClick: collapseAll, children: jsx(Body1, { children: "Collapse All" }) })] })), hasChildren && contextMenuCommands.length > 0 && jsx(MenuDivider, {}), contextMenuItems] }) })] }));
4610
+ }, children: jsx(TruncatingBody1, { text: name }) }) }, GetEntityId$1(entityItem.entity)) }), jsx(MenuPopover, { hidden: !hasChildren && contextMenuCommands.length === 0, children: jsxs(MenuList, { children: [hasChildren && (jsxs(Fragment, { children: [jsx(MenuItem, { icon: jsx(ArrowExpandAllRegular, {}), onClick: expandAll, children: jsx(Body1, { children: "Expand All" }) }), jsx(MenuItem, { icon: jsx(ArrowCollapseAllRegular, {}), onClick: collapseAll, children: jsx(Body1, { children: "Collapse All" }) })] })), hasChildren && contextMenuCommands.length > 0 && jsx(MenuDivider, {}), contextMenuItems] }) })] }));
4454
4611
  };
4455
4612
  const SceneExplorer = (props) => {
4456
4613
  const classes = useStyles$U();
@@ -6153,13 +6310,25 @@ function CoerceStepValue(step, isFineKeyPressed, isCourseKeyPressed) {
6153
6310
  }
6154
6311
  return step;
6155
6312
  }
6156
- // Allow arbitrary expressions, primarily for math operations (e.g. 10*60 for 10 minutes in seconds).
6157
- // Use Function constructor to safely evaluate the expression without allowing access to scope.
6158
- // If the expression is invalid, fallback to NaN which will be caught by validateValue and prevent committing.
6313
+ // Parse the raw input into a number, supporting plain numeric values and arbitrary math expressions
6314
+ // (e.g. "10*60" for 10 minutes in seconds).
6315
+ // First, try Number() so plain numeric input works even under a strict Content-Security-Policy that
6316
+ // disallows eval/Function. Only fall back to the Function constructor for non-numeric inputs that may
6317
+ // be expressions. Empty/whitespace input returns NaN so validateValue rejects it rather than committing
6318
+ // 0 (which is what Number("") would otherwise return).
6319
+ // Non-finite results (NaN, +/-Infinity) are rejected from both paths so callers don't have to handle them.
6159
6320
  function EvaluateExpression(rawValue) {
6160
- const val = rawValue.trim();
6321
+ rawValue = rawValue.trim();
6322
+ if (rawValue.length === 0) {
6323
+ return NaN;
6324
+ }
6325
+ const value = Number(rawValue);
6326
+ if (Number.isFinite(value)) {
6327
+ return value;
6328
+ }
6161
6329
  try {
6162
- return Number(Function(`"use strict";return (${val})`)());
6330
+ const result = Number(Function(`"use strict";return (${rawValue})`)());
6331
+ return Number.isFinite(result) ? result : NaN;
6163
6332
  }
6164
6333
  catch {
6165
6334
  return NaN;
@@ -7633,6 +7802,48 @@ const GizmoServiceDefinition = {
7633
7802
  const getCameraGizmo = (camera) => getGizmo(camera, camera.getScene(), CameraGizmo, cameraGizmos, (camera, gizmo) => (gizmo.camera = camera));
7634
7803
  const lightGizmos = new WeakMap();
7635
7804
  const getLightGizmo = (light) => getGizmo(light, light.getScene(), LightGizmo, lightGizmos, (light, gizmo) => (gizmo.light = light));
7805
+ // Ref-counted spatial audio gizmos. Sound sources are not Babylon Nodes, so we can't reuse the helper above directly.
7806
+ // Unlike `getGizmo`, this doesn't override `gizmo.dispose` — it routes shared cleanup through a lambda
7807
+ // (avoiding both Function.bind and Function.call, which are prohibited by the repo's performance/style rules).
7808
+ const spatialAudioGizmos = new WeakMap();
7809
+ const getSpatialAudioGizmo = (soundSource, scene) => {
7810
+ let refCounted = spatialAudioGizmos.get(soundSource);
7811
+ if (!refCounted) {
7812
+ const utilityLayerRef = getUtilityLayer(scene);
7813
+ const gizmo = new SpatialAudioGizmo(utilityLayerRef.value);
7814
+ gizmo.soundSource = soundSource;
7815
+ let isCleanedUp = false;
7816
+ const cleanup = () => {
7817
+ if (isCleanedUp) {
7818
+ return;
7819
+ }
7820
+ isCleanedUp = true;
7821
+ sourceDisposedObserver.remove();
7822
+ gizmo.dispose();
7823
+ utilityLayerRef.dispose();
7824
+ spatialAudioGizmos.delete(soundSource);
7825
+ };
7826
+ // If the underlying sound source is disposed externally, tear the gizmo down too.
7827
+ const sourceDisposedObserver = soundSource.onDisposeObservable.addOnce(cleanup);
7828
+ refCounted = { gizmo, cleanup, refCount: 0 };
7829
+ spatialAudioGizmos.set(soundSource, refCounted);
7830
+ }
7831
+ refCounted.refCount++;
7832
+ const refCountedCapture = refCounted;
7833
+ let disposed = false;
7834
+ return {
7835
+ value: refCounted.gizmo,
7836
+ dispose: () => {
7837
+ if (!disposed) {
7838
+ disposed = true;
7839
+ refCountedCapture.refCount--;
7840
+ if (refCountedCapture.refCount === 0) {
7841
+ refCountedCapture.cleanup();
7842
+ }
7843
+ }
7844
+ },
7845
+ };
7846
+ };
7636
7847
  // Gizmo mode/coordinates state and GizmoManager lifecycle.
7637
7848
  let gizmoModeState = undefined;
7638
7849
  const gizmoModeObservable = new Observable();
@@ -7768,6 +7979,7 @@ const GizmoServiceDefinition = {
7768
7979
  getUtilityLayer,
7769
7980
  getCameraGizmo,
7770
7981
  getLightGizmo,
7982
+ getSpatialAudioGizmo,
7771
7983
  getCameraGizmos: (scene) => scene.cameras.map((camera) => cameraGizmos.get(camera)?.gizmo).filter(Boolean),
7772
7984
  getLightGizmos: (scene) => scene.lights.map((light) => lightGizmos.get(light)?.gizmo).filter(Boolean),
7773
7985
  get gizmoMode() {
@@ -8340,11 +8552,18 @@ class ServiceContainer {
8340
8552
  }
8341
8553
  }
8342
8554
 
8555
+ /**
8556
+ * The unique identity symbol for the dialog service.
8557
+ */
8558
+ const DialogServiceIdentity = Symbol("DialogService");
8559
+
8343
8560
  /**
8344
8561
  * The unique identity symbol for the toast service.
8345
8562
  */
8346
8563
  const ToastServiceIdentity = Symbol("ToastService");
8347
8564
 
8565
+ const DialogContext = createContext({ showDialog: (options) => alert(options.title) });
8566
+
8348
8567
  const ExtensionManagerContext = createContext(undefined);
8349
8568
  function useExtensionManager() {
8350
8569
  return useContext(ExtensionManagerContext)?.extensionManager;
@@ -8411,6 +8630,24 @@ const useStyles$K = makeStyles({
8411
8630
  extensionErrorIcon: {
8412
8631
  color: tokens.colorPaletteRedForeground1,
8413
8632
  },
8633
+ dialogTitle: {
8634
+ display: "flex",
8635
+ flexDirection: "row",
8636
+ alignItems: "center",
8637
+ gap: tokens.spacingHorizontalS,
8638
+ },
8639
+ dialogIconSuccess: {
8640
+ color: tokens.colorStatusSuccessForeground1,
8641
+ },
8642
+ dialogIconError: {
8643
+ color: tokens.colorStatusDangerForeground1,
8644
+ },
8645
+ dialogIconWarning: {
8646
+ color: tokens.colorStatusWarningForeground1,
8647
+ },
8648
+ dialogIconInfo: {
8649
+ color: tokens.colorBrandForeground1,
8650
+ },
8414
8651
  });
8415
8652
  const ReactContextsWrapper = ({ contexts, children }) => {
8416
8653
  return jsx(Fragment, { children: contexts.reduceRight((acc, entry) => createElement(entry.provider, { value: entry.value }, acc), children) });
@@ -8446,6 +8683,21 @@ function MakeModularTool(options) {
8446
8683
  setToastQueue([]);
8447
8684
  }
8448
8685
  }, [toastHandle, toastQueue]);
8686
+ // Queue of dialogs to display. We show one at a time (the one at the head of the queue).
8687
+ const [dialogQueue, dispatchDialogQueue] = useReducer((state, action) => {
8688
+ switch (action.type) {
8689
+ case "enqueue":
8690
+ return [...state, action.options];
8691
+ case "dequeue":
8692
+ return state.slice(1);
8693
+ }
8694
+ }, []);
8695
+ const showDialog = useCallback((dialogOptions) => {
8696
+ dispatchDialogQueue({ type: "enqueue", options: dialogOptions });
8697
+ }, []);
8698
+ const onDismissDialog = useCallback(() => {
8699
+ dispatchDialogQueue({ type: "dequeue" });
8700
+ }, []);
8449
8701
  const [rootComponentService, setRootComponentService] = useState();
8450
8702
  const [contexts, updateContexts] = useReducer((state, action) => {
8451
8703
  switch (action.type) {
@@ -8496,6 +8748,12 @@ function MakeModularTool(options) {
8496
8748
  },
8497
8749
  }),
8498
8750
  });
8751
+ // Expose the dialog service so non-React code (e.g. Observable callbacks) can show dialogs.
8752
+ serviceContainer.addService({
8753
+ friendlyName: "Dialog Service",
8754
+ produces: [DialogServiceIdentity],
8755
+ factory: () => ({ showDialog }),
8756
+ });
8499
8757
  // Register the shell service (top level toolbar/side pane UI layout).
8500
8758
  serviceContainer.addService(MakeShellServiceDefinition(options));
8501
8759
  // Register a service that simply consumes the services we need before first render.
@@ -8518,7 +8776,7 @@ function MakeModularTool(options) {
8518
8776
  }
8519
8777
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
8520
8778
  if (extensionFeeds.length > 0) {
8521
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-DTrjNf_v.js');
8779
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-j6viqje8.js');
8522
8780
  serviceContainer.addService(ExtensionListServiceDefinition);
8523
8781
  }
8524
8782
  // Register all external services (that make up a unique tool).
@@ -8585,16 +8843,36 @@ function MakeModularTool(options) {
8585
8843
  const onAcknowledgedExtensionInstallError = useCallback(() => {
8586
8844
  setExtensionInstallError(undefined);
8587
8845
  }, [setExtensionInstallError]);
8846
+ // The dialog at the head of the queue, if any, is the one currently being displayed.
8847
+ const currentDialog = dialogQueue[0];
8848
+ const currentDialogIcon = (() => {
8849
+ switch (currentDialog?.intent) {
8850
+ case "success":
8851
+ return jsx(CheckmarkCircleRegular, { className: classes.dialogIconSuccess });
8852
+ case "error":
8853
+ return jsx(ErrorCircleRegular, { className: classes.dialogIconError });
8854
+ case "warning":
8855
+ return jsx(WarningRegular, { className: classes.dialogIconWarning });
8856
+ case "info":
8857
+ case undefined:
8858
+ return jsx(InfoRegular, { className: classes.dialogIconInfo });
8859
+ }
8860
+ })();
8588
8861
  // Show a spinner until a main view has been set.
8589
8862
  if (!rootComponentService) {
8590
- return (jsx(ReactContextsWrapper, { contexts: contexts, children: jsx(SettingsStoreContext.Provider, { value: settingsStore, children: jsx(Theme, { className: classes.app, children: jsx(Spinner, { className: classes.spinner }) }) }) }));
8863
+ return (jsx(ReactContextsWrapper, { contexts: contexts, children: jsx(SettingsStoreContext.Provider, { value: settingsStore, children: jsx(Theme, { className: classes.app, targetDocument: targetDocument, children: jsx(Spinner, { className: classes.spinner }) }) }) }));
8591
8864
  }
8592
8865
  else {
8593
8866
  // eslint-disable-next-line @typescript-eslint/naming-convention
8594
8867
  const Content = rootComponentService.rootComponent;
8595
- return (jsx(ReactContextsWrapper, { contexts: contexts, children: jsx(SettingsStoreContext.Provider, { value: settingsStore, children: jsx(ExtensionManagerContext.Provider, { value: extensionManagerContext, children: jsx(Theme, { className: classes.app, children: jsxs(ToastProvider, { imperativeRef: setToastHandle, children: [jsx(Dialog$1, { open: !!requiredExtensions, modalType: "alert", children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { children: "Required Extensions" }), jsxs(DialogContent, { children: ["Opening this URL requires the following extensions to be installed and enabled:", jsx("ul", { children: requiredExtensions?.map((name) => (jsx("li", { children: name }, name))) })] }), jsxs(DialogActions, { children: [jsx(Button$1, { appearance: "primary", onClick: onAcceptRequiredExtensions, children: "Install & Enable" }), jsx(Button$1, { appearance: "secondary", onClick: onRejectRequiredExtensions, children: "No Thanks" })] })] }) }) }), jsx(Dialog$1, { open: !!extensionInstallError, modalType: "alert", children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { children: jsxs("div", { className: classes.extensionErrorTitleDiv, children: ["Extension Install Error", jsx(ErrorCircleRegular, { className: classes.extensionErrorIcon })] }) }), jsx(DialogContent, { children: jsxs(List$1, { children: [jsx(ListItem, { children: jsx(Body1, { children: `Extension "${extensionInstallError?.extension.name}" failed to install and was removed.` }) }), jsx(ListItem, { children: jsx(Body1, { children: `${extensionInstallError?.error}` }) })] }) }), jsx(DialogActions, { children: jsx(Button$1, { appearance: "primary", onClick: onAcknowledgedExtensionInstallError, children: "Close" }) })] }) }) }), jsx(Suspense, { fallback: jsx(Spinner, { className: classes.spinner }), children: jsx(Content, {}) })] }) }) }) }) }));
8868
+ return (jsx(ReactContextsWrapper, { contexts: contexts, children: jsx(SettingsStoreContext.Provider, { value: settingsStore, children: jsx(ExtensionManagerContext.Provider, { value: extensionManagerContext, children: jsx(Theme, { className: classes.app, targetDocument: targetDocument, children: jsxs(ToastProvider, { imperativeRef: setToastHandle, children: [jsx(Dialog$1, { open: !!requiredExtensions, modalType: "alert", children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { children: "Required Extensions" }), jsxs(DialogContent, { children: ["Opening this URL requires the following extensions to be installed and enabled:", jsx("ul", { children: requiredExtensions?.map((name) => (jsx("li", { children: name }, name))) })] }), jsxs(DialogActions, { children: [jsx(Button$1, { appearance: "primary", onClick: onAcceptRequiredExtensions, children: "Install & Enable" }), jsx(Button$1, { appearance: "secondary", onClick: onRejectRequiredExtensions, children: "No Thanks" })] })] }) }) }), jsx(Dialog$1, { open: !!extensionInstallError, modalType: "alert", children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { children: jsxs("div", { className: classes.extensionErrorTitleDiv, children: ["Extension Install Error", jsx(ErrorCircleRegular, { className: classes.extensionErrorIcon })] }) }), jsx(DialogContent, { children: jsxs(List$1, { children: [jsx(ListItem, { children: jsx(Body1, { children: `Extension "${extensionInstallError?.extension.name}" failed to install and was removed.` }) }), jsx(ListItem, { children: jsx(Body1, { children: `${extensionInstallError?.error}` }) })] }) }), jsx(DialogActions, { children: jsx(Button$1, { appearance: "primary", onClick: onAcknowledgedExtensionInstallError, children: "Close" }) })] }) }) }), jsx(Dialog$1, { open: !!currentDialog, modalType: "alert", children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { children: jsxs("div", { className: classes.dialogTitle, children: [currentDialogIcon, currentDialog?.title] }) }), currentDialog?.content && jsx(DialogContent, { children: currentDialog.content }), jsx(DialogActions, { children: jsx(Button$1, { appearance: "primary", onClick: onDismissDialog, children: "OK" }) })] }) }) }), jsx(Suspense, { fallback: jsx(Spinner, { className: classes.spinner }), children: jsx(DialogContext.Provider, { value: { showDialog }, children: jsx(Content, {}) }) })] }) }) }) }) }));
8596
8869
  }
8597
8870
  };
8871
+ // Derive the target document from the container element. When the container is in a popup
8872
+ // window (or other document distinct from the main one), this is what makes Fluent inject
8873
+ // styles and render portals into the correct document.
8874
+ const containerOwnerDocument = containerElement.ownerDocument;
8875
+ const targetDocument = containerOwnerDocument && containerOwnerDocument !== document ? containerOwnerDocument : undefined;
8598
8876
  // Set the container element to be a flex container so that the tool can be displayed properly.
8599
8877
  const originalContainerElementDisplay = containerElement.style.display;
8600
8878
  containerElement.style.display = "flex";
@@ -8639,14 +8917,14 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
8639
8917
  description: "Adds a new panel for easy creation of various Babylon assets. This is a WIP extension...expect changes!",
8640
8918
  keywords: ["creation", "tools"],
8641
8919
  ...BabylonWebResources,
8642
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-8d6rBO-A.js'),
8920
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-eZ4MCuJ2.js'),
8643
8921
  },
8644
8922
  {
8645
8923
  name: "Reflector",
8646
8924
  description: "Connects to the Reflector Bridge for real-time scene synchronization with the Babylon.js Sandbox.",
8647
8925
  keywords: ["reflector", "bridge", "sync", "sandbox", "tools"],
8648
8926
  ...BabylonWebResources,
8649
- getExtensionModuleAsync: async () => await import('./reflectorService-B7LcD1Sn.js'),
8927
+ getExtensionModuleAsync: async () => await import('./reflectorService-2dP-GJrK.js'),
8650
8928
  },
8651
8929
  ]);
8652
8930
 
@@ -14241,7 +14519,7 @@ const AtmospherePropertiesServiceDefinition = {
14241
14519
  },
14242
14520
  };
14243
14521
 
14244
- function useSoundState(sound) {
14522
+ function useSoundState$1(sound) {
14245
14523
  const stateChangedObservables = [
14246
14524
  useInterceptObservable("function", sound, "play"),
14247
14525
  useInterceptObservable("function", sound, "pause"),
@@ -14253,12 +14531,12 @@ function useSoundState(sound) {
14253
14531
  }
14254
14532
  const SoundGeneralProperties = (props) => {
14255
14533
  const { sound } = props;
14256
- const soundState = useSoundState(sound);
14534
+ const soundState = useSoundState$1(sound);
14257
14535
  return (jsx(Fragment, { children: jsx(TextPropertyLine, { label: "Status", value: soundState }) }));
14258
14536
  };
14259
14537
  const SoundCommandProperties = (props) => {
14260
14538
  const { sound } = props;
14261
- const soundState = useSoundState(sound);
14539
+ const soundState = useSoundState$1(sound);
14262
14540
  const volume = useObservableState(useCallback(() => sound.getVolume(), [sound]), useInterceptObservable("function", sound, "setVolume"));
14263
14541
  return (jsxs(Fragment, { children: [jsx(ButtonLine, { uniqueId: "Start/Stop", label: soundState === "Playing" ? "Pause" : "Play", icon: soundState === "Playing" ? PauseRegular : PlayRegular, onClick: () => {
14264
14542
  if (soundState === "Playing") {
@@ -14272,11 +14550,196 @@ const SoundCommandProperties = (props) => {
14272
14550
  } }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Loop", target: sound, propertyKey: "loop" })] }));
14273
14551
  };
14274
14552
 
14553
+ /**
14554
+ * Spatial-audio property line that shows the scene node a v2 sound or sound source is attached to,
14555
+ * with a clickable link to navigate to that node in the inspector.
14556
+ * @returns The rendered property line.
14557
+ */
14558
+ const AudioV2SpatialAttachmentProperties = ({ source, selectionService }) => {
14559
+ // Intercept attach/detach on this instance's spatial sub-property so the link refreshes when attachment changes.
14560
+ const onAttach = useInterceptObservable("function", source.spatial, "attach");
14561
+ const onDetach = useInterceptObservable("function", source.spatial, "detach");
14562
+ const attachedNode = useObservableState(() => source.spatial.attachedNode, onAttach, onDetach);
14563
+ return (jsx(LinkToEntityPropertyLine, { label: "Attached Node", description: "The scene node this sound is attached to via spatial audio.", entity: attachedNode, selectionService: selectionService }));
14564
+ };
14565
+
14566
+ /**
14567
+ * Renders a clickable "Output Bus" property line for any audio entity that routes to a downstream bus.
14568
+ * @returns The rendered property line.
14569
+ */
14570
+ const AudioV2OutputBusLink = ({ target, description, selectionService }) => {
14571
+ const outBus = useObservableState(useCallback(() => target.outBus, [target]), useInterceptObservable("property", target, "outBus"));
14572
+ return (jsx(LinkToEntityPropertyLine, { label: "Output Bus", description: description ?? "The bus this entity routes its output to.", entity: outBus, selectionService: selectionService }));
14573
+ };
14574
+ // -----------------------------------------------------------------------------
14575
+ // Engine
14576
+ // -----------------------------------------------------------------------------
14577
+ function useEngineState(engine) {
14578
+ const stateChangedObservables = [
14579
+ useInterceptObservable("function", engine, "pauseAsync"),
14580
+ useInterceptObservable("function", engine, "resumeAsync"),
14581
+ useInterceptObservable("function", engine, "unlockAsync"),
14582
+ ];
14583
+ return useObservableState(useCallback(() => engine.state, [engine]), ...stateChangedObservables);
14584
+ }
14585
+ /**
14586
+ * Setup / playback properties for an {@link AudioEngineV2}.
14587
+ * @returns The rendered component.
14588
+ */
14589
+ const AudioV2EngineGeneralProperties = ({ engine }) => {
14590
+ const state = useEngineState(engine);
14591
+ const volume = useObservableState(useCallback(() => engine.volume, [engine]), useInterceptObservable("function", engine, "setVolume"));
14592
+ return (jsxs(Fragment, { children: [jsx(TextPropertyLine, { label: "State", value: state }), jsx(Property, { component: SyncedSliderPropertyLine, label: "Volume", functionPath: "setVolume", value: volume, min: 0, max: 1, step: 0.01, onChange: (value) => engine.setVolume(value) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Parameter Ramp Duration", target: engine, propertyKey: "parameterRampDuration", min: 0, step: 0.01, unit: "s" })] }));
14593
+ };
14594
+ /**
14595
+ * Resume / pause controls for an {@link AudioEngineV2}.
14596
+ *
14597
+ * No separate Unlock button — `engine.unlockAsync()` is just a thin wrapper around
14598
+ * `engine.resumeAsync()` on the base class, so Resume already covers the autoplay-blocked path.
14599
+ * @returns The rendered component.
14600
+ */
14601
+ const AudioV2EngineCommandsProperties = ({ engine }) => {
14602
+ const state = useEngineState(engine);
14603
+ return (jsx(Fragment, { children: state === "running" ? (jsx(ButtonLine, { uniqueId: "audiov2-engine-pause", label: "Pause", icon: PauseRegular, onClick: () => void engine.pauseAsync() })) : (jsx(ButtonLine, { uniqueId: "audiov2-engine-resume", label: "Resume", icon: PlayRegular, onClick: () => void engine.resumeAsync() })) }));
14604
+ };
14605
+ /**
14606
+ * Listener spatial-attachment property line for an {@link AudioEngineV2}.
14607
+ * @returns The rendered component.
14608
+ */
14609
+ const AudioV2EngineListenerProperties = ({ engine, selectionService }) => {
14610
+ const listener = engine.listener;
14611
+ const onAttach = useInterceptObservable("function", listener, "attach");
14612
+ const onDetach = useInterceptObservable("function", listener, "detach");
14613
+ const attachedNode = useObservableState(() => listener.attachedNode, onAttach, onDetach);
14614
+ return (jsx(LinkToEntityPropertyLine, { label: "Attached Node", description: "The scene node the audio listener is attached to via spatial audio.", entity: attachedNode, selectionService: selectionService }));
14615
+ };
14616
+ // -----------------------------------------------------------------------------
14617
+ // Buses (Main + Audio)
14618
+ // -----------------------------------------------------------------------------
14619
+ /**
14620
+ * General properties shared by all v2 audio buses (main and regular).
14621
+ * @returns The rendered component.
14622
+ */
14623
+ const AudioV2BusGeneralProperties = ({ bus }) => {
14624
+ const volume = useObservableState(useCallback(() => bus.volume, [bus]), useInterceptObservable("function", bus, "setVolume"));
14625
+ return (jsx(Property, { component: SyncedSliderPropertyLine, label: "Volume", functionPath: "setVolume", value: volume, min: 0, max: 1, step: 0.01, onChange: (value) => bus.setVolume(value) }));
14626
+ };
14627
+ /**
14628
+ * Additional properties specific to {@link AudioBus} (a non-main bus that can be routed to another bus).
14629
+ * @returns The rendered component.
14630
+ */
14631
+ const AudioV2AudioBusGeneralProperties = ({ bus, selectionService }) => {
14632
+ return jsx(AudioV2OutputBusLink, { target: bus, description: "The bus this bus routes its output to.", selectionService: selectionService });
14633
+ };
14634
+ // -----------------------------------------------------------------------------
14635
+ // Sounds (StaticSound + StreamingSound)
14636
+ // -----------------------------------------------------------------------------
14637
+ function GetSoundStateLabel(state) {
14638
+ switch (state) {
14639
+ case 3 /* SoundState.Started */:
14640
+ return "Playing";
14641
+ case 5 /* SoundState.Paused */:
14642
+ return "Paused";
14643
+ case 2 /* SoundState.Starting */:
14644
+ return "Starting";
14645
+ case 0 /* SoundState.Stopping */:
14646
+ return "Stopping";
14647
+ case 4 /* SoundState.FailedToStart */:
14648
+ return "Failed to start";
14649
+ case 1 /* SoundState.Stopped */:
14650
+ default:
14651
+ return "Stopped";
14652
+ }
14653
+ }
14654
+ function useSoundState(sound) {
14655
+ const stateChangedObservables = [
14656
+ useInterceptObservable("function", sound, "play"),
14657
+ useInterceptObservable("function", sound, "pause"),
14658
+ useInterceptObservable("function", sound, "resume"),
14659
+ useInterceptObservable("function", sound, "stop"),
14660
+ // Engine-level hook fires for every state transition on every sound (including async
14661
+ // Starting → Started, FailedToStart, and natural Stopped after stop()), covering changes
14662
+ // that the per-method intercepts above miss.
14663
+ useInterceptObservable("function", sound.engine, "_onSoundPlaybackStateChanged"),
14664
+ ];
14665
+ return useObservableState(useCallback(() => sound.state, [sound]), ...stateChangedObservables,
14666
+ // Fires when the sound ends naturally (full duration, non-looping).
14667
+ sound.onEndedObservable);
14668
+ }
14669
+ /**
14670
+ * General properties for any v2 sound.
14671
+ * @returns The rendered component.
14672
+ */
14673
+ const AudioV2SoundGeneralProperties = ({ sound, selectionService }) => {
14674
+ const state = useSoundState(sound);
14675
+ const volume = useObservableState(useCallback(() => sound.volume, [sound]), useInterceptObservable("function", sound, "setVolume"));
14676
+ return (jsxs(Fragment, { children: [jsx(TextPropertyLine, { label: "State", value: GetSoundStateLabel(state) }), jsx(Property, { component: SyncedSliderPropertyLine, label: "Volume", functionPath: "setVolume", value: volume, min: 0, max: 1, step: 0.01, onChange: (value) => sound.setVolume(value) }), jsx(AudioV2OutputBusLink, { target: sound, description: "The bus this sound routes its output to.", selectionService: selectionService })] }));
14677
+ };
14678
+ /**
14679
+ * Playback properties shared by all v2 sounds (loop, start offset, current time).
14680
+ * @returns The rendered component.
14681
+ */
14682
+ const AudioV2SoundPlaybackProperties = ({ sound }) => {
14683
+ // currentTime advances implicitly as the sound plays — poll to keep the display in sync.
14684
+ // Note: this is total elapsed playback time, not position-within-buffer — it keeps growing
14685
+ // past buffer.duration when looping. That's why it's rendered as a number input rather
14686
+ // than a scrub-style slider.
14687
+ const tickObservable = usePollingObservable(100);
14688
+ const currentTime = useObservableState(useCallback(() => sound.currentTime, [sound]), tickObservable, useInterceptObservable("property", sound, "currentTime"));
14689
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Loop", target: sound, propertyKey: "loop" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Start Offset", target: sound, propertyKey: "startOffset", min: 0, step: 0.1, unit: "s" }), jsx(Property, { component: NumberInputPropertyLine, label: "Current Time", propertyPath: "currentTime", value: currentTime, min: 0, step: 0.1, unit: "s", onChange: (value) => (sound.currentTime = value) })] }));
14690
+ };
14691
+ /**
14692
+ * Additional playback properties specific to {@link StaticSound} (duration, loop range, pitch, playback rate).
14693
+ * @returns The rendered component.
14694
+ */
14695
+ const AudioV2StaticSoundPlaybackProperties = ({ sound }) => {
14696
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Duration", target: sound, propertyKey: "duration", min: 0, step: 0.1, unit: "s" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Loop Start", target: sound, propertyKey: "loopStart", min: 0, step: 0.1, unit: "s" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Loop End", target: sound, propertyKey: "loopEnd", min: 0, step: 0.1, unit: "s" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Pitch", target: sound, propertyKey: "pitch", step: 1, unit: "\u00A2" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Playback Rate", target: sound, propertyKey: "playbackRate", min: 0, step: 0.1 })] }));
14697
+ };
14698
+ /**
14699
+ * Preload status display for {@link StreamingSound}.
14700
+ * @returns The rendered component.
14701
+ */
14702
+ const AudioV2StreamingSoundPreloadProperties = ({ sound }) => {
14703
+ return (jsxs(Fragment, { children: [jsx(TextPropertyLine, { label: "Preload Count", value: String(sound.preloadCount) }), jsx(TextPropertyLine, { label: "Preload Completed", value: String(sound.preloadCompletedCount) })] }));
14704
+ };
14705
+ /**
14706
+ * Play / pause / stop controls for any v2 sound.
14707
+ * @returns The rendered component.
14708
+ */
14709
+ const AudioV2SoundCommandsProperties = ({ sound }) => {
14710
+ const state = useSoundState(sound);
14711
+ const isPlaying = state === 3 /* SoundState.Started */ || state === 2 /* SoundState.Starting */;
14712
+ const isPaused = state === 5 /* SoundState.Paused */;
14713
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { uniqueId: "audiov2-sound-play-pause", label: isPlaying ? "Pause" : "Play", icon: isPlaying ? PauseRegular : PlayRegular, onClick: () => {
14714
+ if (isPlaying) {
14715
+ sound.pause();
14716
+ }
14717
+ else if (isPaused) {
14718
+ sound.resume();
14719
+ }
14720
+ else {
14721
+ sound.play();
14722
+ }
14723
+ } }), jsx(ButtonLine, { uniqueId: "audiov2-sound-stop", label: "Stop", icon: StopRegular, onClick: () => sound.stop() })] }));
14724
+ };
14725
+ // -----------------------------------------------------------------------------
14726
+ // Standalone sound sources (e.g. microphone)
14727
+ // -----------------------------------------------------------------------------
14728
+ /**
14729
+ * General properties for a v2 sound source that is not a sound (e.g. microphone).
14730
+ * @returns The rendered component.
14731
+ */
14732
+ const AudioV2SoundSourceGeneralProperties = ({ source, selectionService }) => {
14733
+ const volume = useObservableState(useCallback(() => source.volume, [source]), useInterceptObservable("function", source, "setVolume"));
14734
+ return (jsxs(Fragment, { children: [jsx(Property, { component: SyncedSliderPropertyLine, label: "Volume", functionPath: "setVolume", value: volume, min: 0, max: 1, step: 0.01, onChange: (value) => source.setVolume(value) }), jsx(AudioV2OutputBusLink, { target: source, description: "The bus this source routes its output to.", selectionService: selectionService })] }));
14735
+ };
14736
+
14275
14737
  const AudioPropertiesServiceDefinition = {
14276
14738
  friendlyName: "Audio Properties",
14277
- consumes: [PropertiesServiceIdentity],
14278
- factory: (propertiesService) => {
14279
- const soundContentRegistration = propertiesService.addSectionContent({
14739
+ consumes: [PropertiesServiceIdentity, SelectionServiceIdentity],
14740
+ factory: (propertiesService, selectionService) => {
14741
+ // --- v1 Sound ---
14742
+ const soundV1ContentRegistration = propertiesService.addSectionContent({
14280
14743
  key: "Sound General Properties",
14281
14744
  predicate: (entity) => entity instanceof Sound,
14282
14745
  content: [
@@ -14290,55 +14753,167 @@ const AudioPropertiesServiceDefinition = {
14290
14753
  },
14291
14754
  ],
14292
14755
  });
14293
- return {
14294
- dispose: () => {
14295
- soundContentRegistration.dispose();
14296
- },
14297
- };
14298
- },
14299
- };
14300
-
14301
- const ArcRotateCameraTransformProperties = (props) => {
14302
- const { camera } = props;
14303
- const [toDisplayAngle, fromDisplayAngle, useDegrees] = useAngleConverters();
14304
- const lowerAlphaLimit = useProperty(camera, "lowerAlphaLimit") ?? 0;
14305
- const upperAlphaLimit = useProperty(camera, "upperAlphaLimit") ?? Math.PI * 2;
14306
- const lowerBetaLimit = useProperty(camera, "lowerBetaLimit") ?? -Math.PI;
14307
- const upperBetaLimit = useProperty(camera, "upperBetaLimit") ?? Math.PI;
14308
- const lowerRadiusLimit = useProperty(camera, "lowerRadiusLimit");
14309
- const upperRadiusLimit = useProperty(camera, "upperRadiusLimit");
14310
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Alpha", description: `Horizontal angle in ${useDegrees ? "degrees" : "radians"}`, target: camera, propertyKey: "alpha", min: toDisplayAngle(lowerAlphaLimit), max: toDisplayAngle(upperAlphaLimit), step: toDisplayAngle(0.01), unit: useDegrees ? "°" : "rad", convertTo: (value) => toDisplayAngle(value, true), convertFrom: fromDisplayAngle }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Beta", description: `Vertical angle in ${useDegrees ? "degrees" : "radians"}`, target: camera, propertyKey: "beta", min: toDisplayAngle(lowerBetaLimit), max: toDisplayAngle(upperBetaLimit), step: toDisplayAngle(0.01), unit: useDegrees ? "°" : "rad", convertTo: (value) => toDisplayAngle(value, true), convertFrom: fromDisplayAngle }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", description: "Distance from the target point.", target: camera, propertyKey: "radius", min: lowerRadiusLimit ?? undefined, max: upperRadiusLimit ?? undefined, step: 0.01 })] }));
14311
- };
14312
- const ArcRotateCameraControlProperties = (props) => {
14313
- const { camera } = props;
14314
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Angular Sensitivity X", target: camera, propertyKey: "angularSensibilityX" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Angular Sensitivity Y", target: camera, propertyKey: "angularSensibilityY" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Panning Sensitivity", target: camera, propertyKey: "panningSensibility" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Pinch Delta Percentage", target: camera, propertyKey: "pinchDeltaPercentage" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Wheel Delta Percentage", target: camera, propertyKey: "wheelDeltaPercentage" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Natural Pinch Zoom", target: camera, propertyKey: "useNaturalPinchZoom" })] }));
14315
- };
14316
- const ArcRotateCameraCollisionProperties = (props) => {
14317
- const { camera } = props;
14318
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Check Collisions", target: camera, propertyKey: "checkCollisions" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Collision Radius", target: camera, propertyKey: "collisionRadius" })] }));
14319
- };
14320
- const ArcRotateCameraLimitsProperties = (props) => {
14321
- const { camera } = props;
14322
- const [toDisplayAngle, fromDisplayAngle, useDegrees] = useAngleConverters();
14323
- const minAlphaLimit = 0;
14324
- const maxAlphaLimit = Math.PI * 2;
14325
- const minBetaLimit = -Math.PI;
14326
- const maxBetaLimit = Math.PI;
14327
- const lowerAlphaLimit = useProperty(camera, "lowerAlphaLimit") ?? minAlphaLimit;
14328
- const upperAlphaLimit = useProperty(camera, "upperAlphaLimit") ?? maxAlphaLimit;
14329
- const lowerBetaLimit = useProperty(camera, "lowerBetaLimit") ?? minBetaLimit;
14330
- const upperBetaLimit = useProperty(camera, "upperBetaLimit") ?? maxBetaLimit;
14331
- const lowerRadiusLimit = useProperty(camera, "lowerRadiusLimit");
14332
- const upperRadiusLimit = useProperty(camera, "upperRadiusLimit");
14333
- // TODO-Iv2: Update defaultValues
14334
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Alpha Limit", target: camera, propertyKey: "lowerAlphaLimit", nullable: true, defaultValue: toDisplayAngle(minAlphaLimit), min: toDisplayAngle(minAlphaLimit), max: toDisplayAngle(upperAlphaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Alpha Limit", target: camera, propertyKey: "upperAlphaLimit", nullable: true, defaultValue: toDisplayAngle(maxAlphaLimit), min: toDisplayAngle(lowerAlphaLimit), max: toDisplayAngle(maxAlphaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Beta Limit", target: camera, propertyKey: "lowerBetaLimit", nullable: true, defaultValue: toDisplayAngle(minBetaLimit), min: toDisplayAngle(minBetaLimit), max: toDisplayAngle(upperBetaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Beta Limit", target: camera, propertyKey: "upperBetaLimit", nullable: true, defaultValue: toDisplayAngle(maxBetaLimit), min: toDisplayAngle(lowerBetaLimit), max: toDisplayAngle(maxBetaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Radius Limit", target: camera, propertyKey: "lowerRadiusLimit", nullable: true, defaultValue: 0, min: 0, max: upperRadiusLimit ?? undefined }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Radius Limit", target: camera, propertyKey: "upperRadiusLimit", nullable: true, defaultValue: 100, min: lowerRadiusLimit ?? undefined }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Target Y Limit", target: camera, propertyKey: "lowerTargetYLimit" })] }));
14335
- };
14336
- const ArcRotateCameraBehaviorsProperties = (props) => {
14337
- const { camera } = props;
14338
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Auto Rotation", target: camera, propertyKey: "useAutoRotationBehavior" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Bouncing", target: camera, propertyKey: "useBouncingBehavior" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Framing", target: camera, propertyKey: "useFramingBehavior" })] }));
14339
- };
14340
-
14341
- const GeospatialCameraTransformProperties = (props) => {
14756
+ // --- v2 AudioEngineV2 ---
14757
+ const engineV2ContentRegistration = propertiesService.addSectionContent({
14758
+ key: "Audio V2 Engine Properties",
14759
+ predicate: (entity) => entity instanceof AudioEngineV2,
14760
+ content: [
14761
+ {
14762
+ section: "General",
14763
+ component: ({ context }) => jsx(AudioV2EngineGeneralProperties, { engine: context }),
14764
+ },
14765
+ {
14766
+ section: "Listener",
14767
+ component: ({ context }) => jsx(AudioV2EngineListenerProperties, { engine: context, selectionService: selectionService }),
14768
+ },
14769
+ {
14770
+ section: "Commands",
14771
+ component: ({ context }) => jsx(AudioV2EngineCommandsProperties, { engine: context }),
14772
+ },
14773
+ ],
14774
+ });
14775
+ // --- v2 Buses (any AbstractAudioBus — covers Main + Audio) ---
14776
+ const busV2ContentRegistration = propertiesService.addSectionContent({
14777
+ key: "Audio V2 Bus Properties",
14778
+ predicate: (entity) => entity instanceof AbstractAudioBus,
14779
+ content: [
14780
+ {
14781
+ section: "General",
14782
+ component: ({ context }) => jsx(AudioV2BusGeneralProperties, { bus: context }),
14783
+ },
14784
+ ],
14785
+ });
14786
+ // --- v2 AudioBus (non-main bus; adds an Output Bus link) ---
14787
+ const audioBusV2ContentRegistration = propertiesService.addSectionContent({
14788
+ key: "Audio V2 AudioBus Properties",
14789
+ predicate: (entity) => entity instanceof AudioBus,
14790
+ content: [
14791
+ {
14792
+ section: "General",
14793
+ component: ({ context }) => jsx(AudioV2AudioBusGeneralProperties, { bus: context, selectionService: selectionService }),
14794
+ },
14795
+ ],
14796
+ });
14797
+ // --- v2 Sounds (Static + Streaming) ---
14798
+ const soundV2ContentRegistration = propertiesService.addSectionContent({
14799
+ key: "Audio V2 Sound Properties",
14800
+ predicate: (entity) => entity instanceof AbstractSound,
14801
+ content: [
14802
+ {
14803
+ section: "General",
14804
+ component: ({ context }) => jsx(AudioV2SoundGeneralProperties, { sound: context, selectionService: selectionService }),
14805
+ },
14806
+ {
14807
+ section: "Playback",
14808
+ component: ({ context }) => jsx(AudioV2SoundPlaybackProperties, { sound: context }),
14809
+ },
14810
+ {
14811
+ section: "Commands",
14812
+ component: ({ context }) => jsx(AudioV2SoundCommandsProperties, { sound: context }),
14813
+ },
14814
+ ],
14815
+ });
14816
+ // --- v2 StaticSound (extra playback properties) ---
14817
+ const staticSoundV2ContentRegistration = propertiesService.addSectionContent({
14818
+ key: "Audio V2 Static Sound Properties",
14819
+ predicate: (entity) => entity instanceof StaticSound,
14820
+ content: [
14821
+ {
14822
+ section: "Playback",
14823
+ component: ({ context }) => jsx(AudioV2StaticSoundPlaybackProperties, { sound: context }),
14824
+ },
14825
+ ],
14826
+ });
14827
+ // --- v2 StreamingSound (preload status) ---
14828
+ const streamingSoundV2ContentRegistration = propertiesService.addSectionContent({
14829
+ key: "Audio V2 Streaming Sound Properties",
14830
+ predicate: (entity) => entity instanceof StreamingSound,
14831
+ content: [
14832
+ {
14833
+ section: "Streaming",
14834
+ component: ({ context }) => jsx(AudioV2StreamingSoundPreloadProperties, { sound: context }),
14835
+ },
14836
+ ],
14837
+ });
14838
+ // --- v2 Sound Sources (microphone, audio-node) — non-Sound only ---
14839
+ const soundSourceV2ContentRegistration = propertiesService.addSectionContent({
14840
+ key: "Audio V2 Sound Source Properties",
14841
+ predicate: (entity) => entity instanceof AbstractSoundSource && !(entity instanceof AbstractSound),
14842
+ content: [
14843
+ {
14844
+ section: "General",
14845
+ component: ({ context }) => jsx(AudioV2SoundSourceGeneralProperties, { source: context, selectionService: selectionService }),
14846
+ },
14847
+ ],
14848
+ });
14849
+ // --- v2 Spatial attachment (any AbstractSoundSource currently spatial) ---
14850
+ const spatialV2ContentRegistration = propertiesService.addSectionContent({
14851
+ key: "Audio V2 Spatial Properties",
14852
+ predicate: (entity) => entity instanceof AbstractSoundSource && entity._isSpatial,
14853
+ content: [
14854
+ {
14855
+ section: "Spatial",
14856
+ component: ({ context }) => jsx(AudioV2SpatialAttachmentProperties, { source: context, selectionService: selectionService }),
14857
+ },
14858
+ ],
14859
+ });
14860
+ return {
14861
+ dispose: () => {
14862
+ soundV1ContentRegistration.dispose();
14863
+ engineV2ContentRegistration.dispose();
14864
+ busV2ContentRegistration.dispose();
14865
+ audioBusV2ContentRegistration.dispose();
14866
+ soundV2ContentRegistration.dispose();
14867
+ staticSoundV2ContentRegistration.dispose();
14868
+ streamingSoundV2ContentRegistration.dispose();
14869
+ soundSourceV2ContentRegistration.dispose();
14870
+ spatialV2ContentRegistration.dispose();
14871
+ },
14872
+ };
14873
+ },
14874
+ };
14875
+
14876
+ const ArcRotateCameraTransformProperties = (props) => {
14877
+ const { camera } = props;
14878
+ const [toDisplayAngle, fromDisplayAngle, useDegrees] = useAngleConverters();
14879
+ const lowerAlphaLimit = useProperty(camera, "lowerAlphaLimit") ?? 0;
14880
+ const upperAlphaLimit = useProperty(camera, "upperAlphaLimit") ?? Math.PI * 2;
14881
+ const lowerBetaLimit = useProperty(camera, "lowerBetaLimit") ?? -Math.PI;
14882
+ const upperBetaLimit = useProperty(camera, "upperBetaLimit") ?? Math.PI;
14883
+ const lowerRadiusLimit = useProperty(camera, "lowerRadiusLimit");
14884
+ const upperRadiusLimit = useProperty(camera, "upperRadiusLimit");
14885
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Alpha", description: `Horizontal angle in ${useDegrees ? "degrees" : "radians"}`, target: camera, propertyKey: "alpha", min: toDisplayAngle(lowerAlphaLimit), max: toDisplayAngle(upperAlphaLimit), step: toDisplayAngle(0.01), unit: useDegrees ? "°" : "rad", convertTo: (value) => toDisplayAngle(value, true), convertFrom: fromDisplayAngle }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Beta", description: `Vertical angle in ${useDegrees ? "degrees" : "radians"}`, target: camera, propertyKey: "beta", min: toDisplayAngle(lowerBetaLimit), max: toDisplayAngle(upperBetaLimit), step: toDisplayAngle(0.01), unit: useDegrees ? "°" : "rad", convertTo: (value) => toDisplayAngle(value, true), convertFrom: fromDisplayAngle }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", description: "Distance from the target point.", target: camera, propertyKey: "radius", min: lowerRadiusLimit ?? undefined, max: upperRadiusLimit ?? undefined, step: 0.01 })] }));
14886
+ };
14887
+ const ArcRotateCameraControlProperties = (props) => {
14888
+ const { camera } = props;
14889
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Angular Sensitivity X", target: camera, propertyKey: "angularSensibilityX" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Angular Sensitivity Y", target: camera, propertyKey: "angularSensibilityY" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Panning Sensitivity", target: camera, propertyKey: "panningSensibility" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Pinch Delta Percentage", target: camera, propertyKey: "pinchDeltaPercentage" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Wheel Delta Percentage", target: camera, propertyKey: "wheelDeltaPercentage" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Natural Pinch Zoom", target: camera, propertyKey: "useNaturalPinchZoom" })] }));
14890
+ };
14891
+ const ArcRotateCameraCollisionProperties = (props) => {
14892
+ const { camera } = props;
14893
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Check Collisions", target: camera, propertyKey: "checkCollisions" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Collision Radius", target: camera, propertyKey: "collisionRadius" })] }));
14894
+ };
14895
+ const ArcRotateCameraLimitsProperties = (props) => {
14896
+ const { camera } = props;
14897
+ const [toDisplayAngle, fromDisplayAngle, useDegrees] = useAngleConverters();
14898
+ const minAlphaLimit = 0;
14899
+ const maxAlphaLimit = Math.PI * 2;
14900
+ const minBetaLimit = -Math.PI;
14901
+ const maxBetaLimit = Math.PI;
14902
+ const lowerAlphaLimit = useProperty(camera, "lowerAlphaLimit") ?? minAlphaLimit;
14903
+ const upperAlphaLimit = useProperty(camera, "upperAlphaLimit") ?? maxAlphaLimit;
14904
+ const lowerBetaLimit = useProperty(camera, "lowerBetaLimit") ?? minBetaLimit;
14905
+ const upperBetaLimit = useProperty(camera, "upperBetaLimit") ?? maxBetaLimit;
14906
+ const lowerRadiusLimit = useProperty(camera, "lowerRadiusLimit");
14907
+ const upperRadiusLimit = useProperty(camera, "upperRadiusLimit");
14908
+ // TODO-Iv2: Update defaultValues
14909
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Alpha Limit", target: camera, propertyKey: "lowerAlphaLimit", nullable: true, defaultValue: toDisplayAngle(minAlphaLimit), min: toDisplayAngle(minAlphaLimit), max: toDisplayAngle(upperAlphaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Alpha Limit", target: camera, propertyKey: "upperAlphaLimit", nullable: true, defaultValue: toDisplayAngle(maxAlphaLimit), min: toDisplayAngle(lowerAlphaLimit), max: toDisplayAngle(maxAlphaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Beta Limit", target: camera, propertyKey: "lowerBetaLimit", nullable: true, defaultValue: toDisplayAngle(minBetaLimit), min: toDisplayAngle(minBetaLimit), max: toDisplayAngle(upperBetaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Beta Limit", target: camera, propertyKey: "upperBetaLimit", nullable: true, defaultValue: toDisplayAngle(maxBetaLimit), min: toDisplayAngle(lowerBetaLimit), max: toDisplayAngle(maxBetaLimit), unit: useDegrees ? "°" : "rad", convertTo: (value) => (value === null ? value : toDisplayAngle(value, true)), convertFrom: (value) => (value === null ? value : fromDisplayAngle(value)) }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Radius Limit", target: camera, propertyKey: "lowerRadiusLimit", nullable: true, defaultValue: 0, min: 0, max: upperRadiusLimit ?? undefined }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Upper Radius Limit", target: camera, propertyKey: "upperRadiusLimit", nullable: true, defaultValue: 100, min: lowerRadiusLimit ?? undefined }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Lower Target Y Limit", target: camera, propertyKey: "lowerTargetYLimit" })] }));
14910
+ };
14911
+ const ArcRotateCameraBehaviorsProperties = (props) => {
14912
+ const { camera } = props;
14913
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Auto Rotation", target: camera, propertyKey: "useAutoRotationBehavior" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Bouncing", target: camera, propertyKey: "useBouncingBehavior" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Framing", target: camera, propertyKey: "useFramingBehavior" })] }));
14914
+ };
14915
+
14916
+ const GeospatialCameraTransformProperties = (props) => {
14342
14917
  const { camera } = props;
14343
14918
  const [toDisplayAngle, fromDisplayAngle, useDegrees] = useAngleConverters();
14344
14919
  const limits = useProperty(camera, "limits");
@@ -15776,6 +16351,115 @@ const CheckboxPropertyLine = (props) => {
15776
16351
  return (jsx(PropertyLine, { ...props, children: jsx(Checkbox, { ...props }) }));
15777
16352
  };
15778
16353
 
16354
+ const useStyles$l = makeStyles({
16355
+ expandedDebugContainer: {
16356
+ display: "flex",
16357
+ alignItems: "center",
16358
+ marginTop: tokens.spacingVerticalXS,
16359
+ },
16360
+ debugLabel: {
16361
+ flex: 1,
16362
+ },
16363
+ });
16364
+ /**
16365
+ * Toggles debug mode for a texture on a material.
16366
+ * When enabled, creates a debug `StandardMaterial` with the texture wired as `emissiveTexture` and swaps it onto every
16367
+ * mesh that currently uses the original material. Enabling debug for one texture on a material disables it for any
16368
+ * other texture on that material (mutually exclusive).
16369
+ *
16370
+ * Notes:
16371
+ * - The texture is rendered using its current `level`. Textures whose level is not 1 will appear modulated; this is
16372
+ * intentional so that no global state on the source texture is mutated (a level mutation would affect every other
16373
+ * material that references the same texture instance).
16374
+ * - Only meshes referencing the original material at toggle time receive the debug material. Meshes added or
16375
+ * reassigned to the original material afterwards are not affected until the next toggle cycle.
16376
+ * @param material - The material to debug
16377
+ * @param texture - The texture to debug
16378
+ * @param enable - Whether to enable or disable debug mode
16379
+ */
16380
+ function ToggleTextureDebug(material, texture, enable) {
16381
+ if (!material || !texture) {
16382
+ return;
16383
+ }
16384
+ const scene = material.getScene();
16385
+ material.reservedDataStore ?? (material.reservedDataStore = {});
16386
+ const store = material.reservedDataStore;
16387
+ store.debugTexture ?? (store.debugTexture = null);
16388
+ store.debugMaterial ?? (store.debugMaterial = null);
16389
+ const isCurrentlyDebugging = store.debugTexture === texture;
16390
+ if (enable && !isCurrentlyDebugging) {
16391
+ // If we were debugging a different texture, clean it up first.
16392
+ if (store.debugTexture) {
16393
+ ToggleTextureDebug(material, store.debugTexture, false);
16394
+ }
16395
+ const debugMaterial = new StandardMaterial("debugMaterial", scene);
16396
+ debugMaterial.disableLighting = true;
16397
+ debugMaterial.sideOrientation = material.sideOrientation;
16398
+ debugMaterial.emissiveTexture = texture;
16399
+ debugMaterial.forceDepthWrite = true;
16400
+ debugMaterial.reservedDataStore = { hidden: true };
16401
+ for (const mesh of scene.meshes) {
16402
+ if (mesh.material === material) {
16403
+ mesh.material = debugMaterial;
16404
+ }
16405
+ }
16406
+ store.debugMaterial = debugMaterial;
16407
+ // Assign debugTexture LAST so observers see a fully-populated store.
16408
+ store.debugTexture = texture;
16409
+ }
16410
+ else if (!enable && isCurrentlyDebugging) {
16411
+ const debugMaterial = store.debugMaterial;
16412
+ if (debugMaterial) {
16413
+ for (const mesh of scene.meshes) {
16414
+ if (mesh.material === debugMaterial) {
16415
+ mesh.material = material;
16416
+ }
16417
+ }
16418
+ debugMaterial.dispose();
16419
+ store.debugMaterial = null;
16420
+ }
16421
+ store.debugTexture = null;
16422
+ }
16423
+ }
16424
+ /**
16425
+ * A texture selector property line with material texture debug capability.
16426
+ * Displays a debug toggle in expanded content to render the selected texture as emissive on the material.
16427
+ * Enabling debug on one texture for a given material disables it for any other texture on that material.
16428
+ * @param props - The texture selector property line props plus material reference
16429
+ * @returns The property line element with debug toggle
16430
+ */
16431
+ const MaterialTextureDebugPropertyLine = (props) => {
16432
+ const classes = useStyles$l();
16433
+ const { material, ...textureProps } = props;
16434
+ const texture = props.value;
16435
+ const reservedDataStore = useProperty(material, "reservedDataStore");
16436
+ const debugTexture = useProperty(reservedDataStore, "debugTexture");
16437
+ const isDebugEnabled = !!texture && debugTexture === texture;
16438
+ // Track the texture this slot last rendered with so we can detect when its value changes away
16439
+ // from the texture currently being debugged. Without this, reassigning the slot's texture (or
16440
+ // setting it to null) while debug is active would leave the scene stuck in debug view with no
16441
+ // visible toggle to turn it off (the toggle for that slot is keyed off texture-instance
16442
+ // equality and would just appear off).
16443
+ const prevTextureRef = useRef(texture);
16444
+ useEffect(() => {
16445
+ const prevTexture = prevTextureRef.current;
16446
+ prevTextureRef.current = texture;
16447
+ if (prevTexture && prevTexture !== texture) {
16448
+ const store = material.reservedDataStore;
16449
+ if (store?.debugTexture === prevTexture) {
16450
+ ToggleTextureDebug(material, prevTexture, false);
16451
+ }
16452
+ }
16453
+ }, [texture]);
16454
+ const handleDebugToggle = useCallback((checked) => {
16455
+ if (texture) {
16456
+ ToggleTextureDebug(material, texture, checked);
16457
+ }
16458
+ }, [material, texture]);
16459
+ const expandedContent = texture ? (jsxs("div", { className: classes.expandedDebugContainer, children: [jsx("span", { className: classes.debugLabel, children: "Display Texture for Debug" }), jsx(Switch, { value: isDebugEnabled, onChange: handleDebugToggle })] })) : undefined;
16460
+ return (jsx(PropertyLine, { ...textureProps, expandedContent: expandedContent, children: jsx(TextureSelector, { ...textureProps }) }));
16461
+ };
16462
+
15779
16463
  const LightFalloffOptions = [
15780
16464
  { label: "Physical", value: PBRBaseMaterial.LIGHTFALLOFF_PHYSICAL },
15781
16465
  { label: "glTF", value: PBRBaseMaterial.LIGHTFALLOFF_GLTF },
@@ -15880,7 +16564,7 @@ const PBRBaseMaterialChannelsProperties = (props) => {
15880
16564
  const { material, selectionService } = props;
15881
16565
  const scene = material.getScene();
15882
16566
  const selectEntity = (entity) => (selectionService.selectedEntity = entity);
15883
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Albedo", target: material, propertyKey: "_albedoTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Weight", target: material, propertyKey: "_baseWeightTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "_baseDiffuseRoughnessTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Metallic Roughness", target: material, propertyKey: "_metallicTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, uniqueId: "PBRBaseMaterialChannels_Reflection", label: "Reflection", target: material, propertyKey: "_reflectionTexture", scene: scene, cubeOnly: true, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Refraction", target: material.subSurface, propertyKey: "refractionTexture", propertyPath: "subSurface.refractionTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Reflectivity", target: material, propertyKey: "_reflectivityTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Micro Surface", target: material, propertyKey: "_microSurfaceTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Bump", target: material, propertyKey: "_bumpTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Emissive", target: material, propertyKey: "_emissiveTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Opacity", target: material, propertyKey: "_opacityTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Ambient", target: material, propertyKey: "_ambientTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Lightmap", target: material, propertyKey: "_lightmapTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Detailmap", target: material.detailMap, propertyKey: "texture", propertyPath: "detailMap.texture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Lightmap as Shadowmap", target: material, propertyKey: "_useLightmapAsShadowmap" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Detailmap", target: material.detailMap, propertyKey: "isEnabled", propertyPath: "detailMap.isEnabled" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Decalmap", target: material.decalMap, propertyKey: "isEnabled", propertyPath: "decalMap.isEnabled" })] }));
16567
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Albedo", target: material, propertyKey: "_albedoTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Weight", target: material, propertyKey: "_baseWeightTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "_baseDiffuseRoughnessTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Metallic Roughness", target: material, propertyKey: "_metallicTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, uniqueId: "PBRBaseMaterialChannels_Reflection", label: "Reflection", target: material, propertyKey: "_reflectionTexture", scene: scene, cubeOnly: true, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Refraction", target: material.subSurface, propertyKey: "refractionTexture", propertyPath: "subSurface.refractionTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Reflectivity", target: material, propertyKey: "_reflectivityTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Micro Surface", target: material, propertyKey: "_microSurfaceTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Bump", target: material, propertyKey: "_bumpTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Emissive", target: material, propertyKey: "_emissiveTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Opacity", target: material, propertyKey: "_opacityTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Ambient", target: material, propertyKey: "_ambientTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Lightmap", target: material, propertyKey: "_lightmapTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Detailmap", target: material.detailMap, propertyKey: "texture", propertyPath: "detailMap.texture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Lightmap as Shadowmap", target: material, propertyKey: "_useLightmapAsShadowmap" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Detailmap", target: material.detailMap, propertyKey: "isEnabled", propertyPath: "detailMap.isEnabled" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Decalmap", target: material.decalMap, propertyKey: "isEnabled", propertyPath: "decalMap.isEnabled" })] }));
15884
16568
  };
15885
16569
  const PBRBaseMaterialLightingAndColorProperties = (props) => {
15886
16570
  const { material } = props;
@@ -15890,7 +16574,7 @@ const PBRBaseMaterialMetallicWorkflowProperties = (props) => {
15890
16574
  const { material, selectionService } = props;
15891
16575
  const scene = material.getScene();
15892
16576
  const selectEntity = (entity) => (selectionService.selectedEntity = entity);
15893
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Metallic", target: material, propertyKey: "_metallic", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, uniqueId: "PBRBaseMaterialMetallicWorkflow_Roughness", label: "Roughness", target: material, propertyKey: "_roughness", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "_baseDiffuseRoughness", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Index of Refraction", target: material.subSurface, propertyKey: "indexOfRefraction", propertyPath: "subSurface.indexOfRefraction", min: 1, max: 3, step: 0.01 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "F0 Factor", target: material, propertyKey: "_metallicF0Factor", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Reflectance Color", target: material, propertyKey: "_metallicReflectanceColor", isLinearMode: true }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Metallic Only", description: "Use only metallic from MetallicReflectance texture", target: material, propertyKey: "_useOnlyMetallicFromMetallicReflectanceTexture" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Metallic Reflectance", target: material, propertyKey: "_metallicReflectanceTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Reflectance", target: material, propertyKey: "_reflectanceTexture", scene: scene, onLink: selectEntity, defaultValue: null })] }));
16577
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Metallic", target: material, propertyKey: "_metallic", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, uniqueId: "PBRBaseMaterialMetallicWorkflow_Roughness", label: "Roughness", target: material, propertyKey: "_roughness", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "_baseDiffuseRoughness", min: 0, max: 1, step: 0.01, nullable: true, defaultValue: 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Index of Refraction", target: material.subSurface, propertyKey: "indexOfRefraction", propertyPath: "subSurface.indexOfRefraction", min: 1, max: 3, step: 0.01 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "F0 Factor", target: material, propertyKey: "_metallicF0Factor", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Reflectance Color", target: material, propertyKey: "_metallicReflectanceColor", isLinearMode: true }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Metallic Only", description: "Use only metallic from MetallicReflectance texture", target: material, propertyKey: "_useOnlyMetallicFromMetallicReflectanceTexture" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Metallic Reflectance", target: material, propertyKey: "_metallicReflectanceTexture", material: material, scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Reflectance", target: material, propertyKey: "_reflectanceTexture", material: material, scene: scene, onLink: selectEntity, defaultValue: null })] }));
15894
16578
  };
15895
16579
  const PBRBaseMaterialClearCoatProperties = (props) => {
15896
16580
  const { material, selectionService } = props;
@@ -15957,7 +16641,7 @@ const PBRBaseMaterialDebugProperties = (props) => {
15957
16641
  */
15958
16642
  const OpenPBRMaterialBaseProperties = (props) => {
15959
16643
  const { material } = props;
15960
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Weight", target: material, propertyKey: "baseWeight", min: 0, max: 1, step: 0.01, description: "Controls how strong or visible the base aspect appears.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Weight", target: material, propertyKey: "baseWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Base Color", target: material, propertyKey: "baseColor", isLinearMode: true, description: "Sets the primary surface color of the material.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Color", target: material, propertyKey: "baseColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Metalness", target: material, propertyKey: "baseMetalness", min: 0, max: 1, step: 0.01, description: "Controls whether the material behaves as metal or non-metal. The parameter supersedes transmission_weight and subsurface_weight.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Metalness", target: material, propertyKey: "baseMetalnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughness", min: 0, max: 1, step: 0.01, description: "Softens the surface's base appearance. Higher values create matte or porous looks. Lower values are smoother.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Ambient Occlusion", target: material, propertyKey: "ambientOcclusionTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 })] }));
16644
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Weight", target: material, propertyKey: "baseWeight", min: 0, max: 1, step: 0.01, description: "Controls how strong or visible the base aspect appears.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Weight", target: material, propertyKey: "baseWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Base Color", target: material, propertyKey: "baseColor", isLinearMode: true, description: "Sets the primary surface color of the material.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Color", target: material, propertyKey: "baseColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Metalness", target: material, propertyKey: "baseMetalness", min: 0, max: 1, step: 0.01, description: "Controls whether the material behaves as metal or non-metal. The parameter supersedes transmission_weight and subsurface_weight.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Metalness", target: material, propertyKey: "baseMetalnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughness", min: 0, max: 1, step: 0.01, description: "Softens the surface's base appearance. Higher values create matte or porous looks. Lower values are smoother.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Ambient Occlusion", target: material, propertyKey: "ambientOcclusionTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material })] }));
15961
16645
  };
15962
16646
  /**
15963
16647
  * Displays the specular layer properties of an OpenPBR material.
@@ -15966,15 +16650,15 @@ const OpenPBRMaterialBaseProperties = (props) => {
15966
16650
  */
15967
16651
  const OpenPBRMaterialSpecularProperties = (props) => {
15968
16652
  const { material } = props;
15969
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Weight", target: material, propertyKey: "specularWeight", min: 0, max: 1, step: 0.01, description: "Controls how strong the reflections appear.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Specular Weight", target: material, propertyKey: "specularWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Specular Color", target: material, propertyKey: "specularColor", isLinearMode: true, description: "Tints the color of reflections.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Specular Color", target: material, propertyKey: "specularColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Roughness", target: material, propertyKey: "specularRoughness", min: 0, max: 1, step: 0.01, description: "Controls how sharp or blurry reflections are.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Specular Roughness", target: material, propertyKey: "specularRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Roughness Anisotropy", target: material, propertyKey: "specularRoughnessAnisotropy", min: 0, max: 1, step: 0.01, description: "Stretches reflections in one direction for brushed or streaked looks. Requires specular_roughness > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/microfacetmodel" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Specular Roughness Anisotropy", target: material, propertyKey: "specularRoughnessAnisotropyTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular IOR", target: material, propertyKey: "specularIor", min: 1, max: 30, step: 0.01, description: "Index of refraction is a physical value controlling the reflective intensity and refraction. The parameter has no effect on metals.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" })] }));
16653
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Weight", target: material, propertyKey: "specularWeight", min: 0, max: 1, step: 0.01, description: "Controls how strong the reflections appear.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Specular Weight", target: material, propertyKey: "specularWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Specular Color", target: material, propertyKey: "specularColor", isLinearMode: true, description: "Tints the color of reflections.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Specular Color", target: material, propertyKey: "specularColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Roughness", target: material, propertyKey: "specularRoughness", min: 0, max: 1, step: 0.01, description: "Controls how sharp or blurry reflections are.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Specular Roughness", target: material, propertyKey: "specularRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular Roughness Anisotropy", target: material, propertyKey: "specularRoughnessAnisotropy", min: 0, max: 1, step: 0.01, description: "Stretches reflections in one direction for brushed or streaked looks. Requires specular_roughness > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/microfacetmodel" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Specular Roughness Anisotropy", target: material, propertyKey: "specularRoughnessAnisotropyTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular IOR", target: material, propertyKey: "specularIor", min: 1, max: 30, step: 0.01, description: "Index of refraction is a physical value controlling the reflective intensity and refraction. The parameter has no effect on metals.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate" })] }));
15970
16654
  };
15971
16655
  const OpenPBRMaterialTransmissionProperties = (props) => {
15972
16656
  const { material } = props;
15973
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Weight", target: material, propertyKey: "transmissionWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the transparency effect. The parameter is superseded by base_metalness.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Transmission Weight", target: material, propertyKey: "transmissionWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Color", target: material, propertyKey: "transmissionColor", isLinearMode: true, description: "Tints light passing through the material. Works with transmission_depth for realistic thickness-based coloring.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Transmission Color", target: material, propertyKey: "transmissionColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Depth (cm)", target: material, propertyKey: "transmissionDepth", min: 0, max: 50, step: 0.001, convertTo: (value) => value * 100, convertFrom: (value) => value / 100, description: "Controls how quickly light is absorbed with thickness. Distance is in scene units.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Transmission Depth", target: material, propertyKey: "transmissionDepthTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Scatter", target: material, propertyKey: "transmissionScatter", isLinearMode: true, description: "Adds internal cloudiness to create materials like juice, honey, etc. Requires transmission_depth > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Transmission Scatter", target: material, propertyKey: "transmissionScatterTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Scatter Anisotropy", target: material, propertyKey: "transmissionScatterAnisotropy", min: -1, max: 1, step: 0.01, description: "Shifts scattering forward/backward for clearer or hazier appearance depending on viewing angle.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Abbe Number", target: material, propertyKey: "transmissionDispersionAbbeNumber", min: 1, max: 100, step: 1, description: "Physical value for the rainbow color separation in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Scale", target: material, propertyKey: "transmissionDispersionScale", min: 0, max: 5, step: 0.01, description: "Strength of rainbow color separation in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Transmission Dispersion Scale", target: material, propertyKey: "transmissionDispersionScaleTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 })] }));
16657
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Weight", target: material, propertyKey: "transmissionWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the transparency effect. The parameter is superseded by base_metalness.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Transmission Weight", target: material, propertyKey: "transmissionWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Color", target: material, propertyKey: "transmissionColor", isLinearMode: true, description: "Tints light passing through the material. Works with transmission_depth for realistic thickness-based coloring.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Transmission Color", target: material, propertyKey: "transmissionColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Depth (cm)", target: material, propertyKey: "transmissionDepth", min: 0, max: 50, step: 0.001, convertTo: (value) => value * 100, convertFrom: (value) => value / 100, description: "Controls how quickly light is absorbed with thickness. Distance is in scene units.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Transmission Depth", target: material, propertyKey: "transmissionDepthTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Scatter", target: material, propertyKey: "transmissionScatter", isLinearMode: true, description: "Adds internal cloudiness to create materials like juice, honey, etc. Requires transmission_depth > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Transmission Scatter", target: material, propertyKey: "transmissionScatterTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Scatter Anisotropy", target: material, propertyKey: "transmissionScatterAnisotropy", min: -1, max: 1, step: 0.01, description: "Shifts scattering forward/backward for clearer or hazier appearance depending on viewing angle.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Abbe Number", target: material, propertyKey: "transmissionDispersionAbbeNumber", min: 1, max: 100, step: 1, description: "Physical value for the rainbow color separation in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Scale", target: material, propertyKey: "transmissionDispersionScale", min: 0, max: 5, step: 0.01, description: "Strength of rainbow color separation in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/translucentbase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Transmission Dispersion Scale", target: material, propertyKey: "transmissionDispersionScaleTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material })] }));
15974
16658
  };
15975
16659
  const OpenPBRMaterialSubsurfaceProperties = (props) => {
15976
16660
  const { material } = props;
15977
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Weight", target: material, propertyKey: "subsurfaceWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the subsurface effect. The parameter is superseded by base_metalness and transmission_weight.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Subsurface Weight", target: material, propertyKey: "subsurfaceWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Subsurface Color", target: material, propertyKey: "subsurfaceColor", isLinearMode: true, description: "Colors the light that scatters under the surface.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Subsurface Color", target: material, propertyKey: "subsurfaceColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Radius (cm)", target: material, propertyKey: "subsurfaceRadius", min: 0, max: 50, step: 0.001, convertTo: (value) => value * 100, convertFrom: (value) => value / 100, description: "Controls how soft and spread-out the subsurface look appears.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Subsurface Radius Scale", target: material, propertyKey: "subsurfaceRadiusScale", isLinearMode: true, description: "Tints thin areas with light shining through, like warm glow on ears or leaves.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Subsurface Radius Scale", target: material, propertyKey: "subsurfaceRadiusScaleTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Scatter Anisotropy", target: material, propertyKey: "subsurfaceScatterAnisotropy", min: -1, max: 1, step: 0.01, description: "Shifts scattering forward/backward for a softer glow or a sharper one.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" })] }));
16661
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Weight", target: material, propertyKey: "subsurfaceWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the subsurface effect. The parameter is superseded by base_metalness and transmission_weight.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Subsurface Weight", target: material, propertyKey: "subsurfaceWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Subsurface Color", target: material, propertyKey: "subsurfaceColor", isLinearMode: true, description: "Colors the light that scatters under the surface.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Subsurface Color", target: material, propertyKey: "subsurfaceColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Radius (cm)", target: material, propertyKey: "subsurfaceRadius", min: 0, max: 50, step: 0.001, convertTo: (value) => value * 100, convertFrom: (value) => value / 100, description: "Controls how soft and spread-out the subsurface look appears.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Subsurface Radius Scale", target: material, propertyKey: "subsurfaceRadiusScale", isLinearMode: true, description: "Tints thin areas with light shining through, like warm glow on ears or leaves.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Subsurface Radius Scale", target: material, propertyKey: "subsurfaceRadiusScaleTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Subsurface Scatter Anisotropy", target: material, propertyKey: "subsurfaceScatterAnisotropy", min: -1, max: 1, step: 0.01, description: "Shifts scattering forward/backward for a softer glow or a sharper one.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/basesubstrate/subsurface" })] }));
15978
16662
  };
15979
16663
  /**
15980
16664
  * Displays the coat layer properties of an OpenPBR material.
@@ -15983,7 +16667,7 @@ const OpenPBRMaterialSubsurfaceProperties = (props) => {
15983
16667
  */
15984
16668
  const OpenPBRMaterialCoatProperties = (props) => {
15985
16669
  const { material } = props;
15986
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Weight", target: material, propertyKey: "coatWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the coat.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Coat Weight", target: material, propertyKey: "coatWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Coat Color", target: material, propertyKey: "coatColor", isLinearMode: true, description: "Tints the coat, for tinted varnish or paint.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Coat Color", target: material, propertyKey: "coatColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Roughness", target: material, propertyKey: "coatRoughness", min: 0, max: 1, step: 0.01, description: "Controls how sharp or blurry the coat reflections appear.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat/roughening" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Coat Roughness", target: material, propertyKey: "coatRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Roughness Anisotropy", target: material, propertyKey: "coatRoughnessAnisotropy", min: 0, max: 1, step: 0.01, description: "Stretches coat reflections in one direction for brushed or streaked looks. Requires coat_roughness > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Coat Roughness Anisotropy", target: material, propertyKey: "coatRoughnessAnisotropyTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat IOR", target: material, propertyKey: "coatIor", min: 1, max: 3, step: 0.01, description: "Index of refraction is a physical value controlling the reflective intensity of the coat.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Darkening", target: material, propertyKey: "coatDarkening", min: 0, max: 1, step: 0.01, description: "Darkens the base under the coat, similar to how real varnish deepens color.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat/darkening" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Coat Darkening", target: material, propertyKey: "coatDarkeningTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 })] }));
16670
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Weight", target: material, propertyKey: "coatWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the coat.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Coat Weight", target: material, propertyKey: "coatWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Coat Color", target: material, propertyKey: "coatColor", isLinearMode: true, description: "Tints the coat, for tinted varnish or paint.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Coat Color", target: material, propertyKey: "coatColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Roughness", target: material, propertyKey: "coatRoughness", min: 0, max: 1, step: 0.01, description: "Controls how sharp or blurry the coat reflections appear.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat/roughening" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Coat Roughness", target: material, propertyKey: "coatRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Roughness Anisotropy", target: material, propertyKey: "coatRoughnessAnisotropy", min: 0, max: 1, step: 0.01, description: "Stretches coat reflections in one direction for brushed or streaked looks. Requires coat_roughness > 0.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Coat Roughness Anisotropy", target: material, propertyKey: "coatRoughnessAnisotropyTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat IOR", target: material, propertyKey: "coatIor", min: 1, max: 3, step: 0.01, description: "Index of refraction is a physical value controlling the reflective intensity of the coat.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Darkening", target: material, propertyKey: "coatDarkening", min: 0, max: 1, step: 0.01, description: "Darkens the base under the coat, similar to how real varnish deepens color.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/coat/darkening" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Coat Darkening", target: material, propertyKey: "coatDarkeningTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material })] }));
15987
16671
  };
15988
16672
  /**
15989
16673
  * Displays the fuzz layer properties of an OpenPBR material.
@@ -15992,7 +16676,7 @@ const OpenPBRMaterialCoatProperties = (props) => {
15992
16676
  */
15993
16677
  const OpenPBRMaterialFuzzProperties = (props) => {
15994
16678
  const { material } = props;
15995
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Fuzz Weight", target: material, propertyKey: "fuzzWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Fuzz Weight", target: material, propertyKey: "fuzzWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Fuzz Color", target: material, propertyKey: "fuzzColor", isLinearMode: true, description: "Controls the color of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Fuzz Color", target: material, propertyKey: "fuzzColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Fuzz Roughness", target: material, propertyKey: "fuzzRoughness", min: 0, max: 1, step: 0.01, description: "Controls the roughness of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Fuzz Roughness", target: material, propertyKey: "fuzzRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 })] }));
16679
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Fuzz Weight", target: material, propertyKey: "fuzzWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Fuzz Weight", target: material, propertyKey: "fuzzWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Fuzz Color", target: material, propertyKey: "fuzzColor", isLinearMode: true, description: "Controls the color of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Fuzz Color", target: material, propertyKey: "fuzzColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Fuzz Roughness", target: material, propertyKey: "fuzzRoughness", min: 0, max: 1, step: 0.01, description: "Controls the roughness of the fuzz.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/fuzz" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Fuzz Roughness", target: material, propertyKey: "fuzzRoughnessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material })] }));
15996
16680
  };
15997
16681
  /**
15998
16682
  * Displays the emission properties of an OpenPBR material.
@@ -16001,7 +16685,7 @@ const OpenPBRMaterialFuzzProperties = (props) => {
16001
16685
  */
16002
16686
  const OpenPBRMaterialEmissionProperties = (props) => {
16003
16687
  const { material } = props;
16004
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Color3PropertyLine, label: "Emission Color", target: material, propertyKey: "emissionColor", isLinearMode: true, description: "Controls the color of the glow.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/emission" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Emission Color", target: material, propertyKey: "emissionColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Emission Luminance", target: material, propertyKey: "emissionLuminance", min: 0, max: 10, step: 0.01, description: "Controls how bright the glow is.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/emission" })] }));
16688
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Color3PropertyLine, label: "Emission Color", target: material, propertyKey: "emissionColor", isLinearMode: true, description: "Controls the color of the glow.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/emission" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Emission Color", target: material, propertyKey: "emissionColorTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Emission Luminance", target: material, propertyKey: "emissionLuminance", min: 0, max: 10, step: 0.01, description: "Controls how bright the glow is.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/emission" })] }));
16005
16689
  };
16006
16690
  /**
16007
16691
  * Displays the thin film properties of an OpenPBR material.
@@ -16010,7 +16694,7 @@ const OpenPBRMaterialEmissionProperties = (props) => {
16010
16694
  */
16011
16695
  const OpenPBRMaterialThinFilmProperties = (props) => {
16012
16696
  const { material } = props;
16013
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film Weight", target: material, propertyKey: "thinFilmWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the thin-film.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Thin Film Weight", target: material, propertyKey: "thinFilmWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film Thickness", target: material, propertyKey: "thinFilmThickness", min: 0, max: 1, step: 0.01, description: "Changes the color pattern of the iridescence.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Thin Film Thickness", target: material, propertyKey: "thinFilmThicknessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film IOR", target: material, propertyKey: "thinFilmIor", min: 1, max: 3, step: 0.01, description: "Alters the strength and contrast of the color shift based in the index of refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" })] }));
16697
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film Weight", target: material, propertyKey: "thinFilmWeight", min: 0, max: 1, step: 0.01, description: "Controls the presence of the thin-film.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Thin Film Weight", target: material, propertyKey: "thinFilmWeightTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film Thickness", target: material, propertyKey: "thinFilmThickness", min: 0, max: 1, step: 0.01, description: "Changes the color pattern of the iridescence.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Thin Film Thickness", target: material, propertyKey: "thinFilmThicknessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Thin Film IOR", target: material, propertyKey: "thinFilmIor", min: 1, max: 3, step: 0.01, description: "Alters the strength and contrast of the color shift based in the index of refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-filmiridescence" })] }));
16014
16698
  };
16015
16699
  /**
16016
16700
  * Displays the geometry properties of an OpenPBR material.
@@ -16019,7 +16703,7 @@ const OpenPBRMaterialThinFilmProperties = (props) => {
16019
16703
  */
16020
16704
  const OpenPBRMaterialGeometryProperties = (props) => {
16021
16705
  const { material } = props;
16022
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Geometry Opacity", target: material, propertyKey: "geometryOpacity", min: 0, max: 1, step: 0.01, description: "Controls material presence and transparency cutout.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/opacity/transparency" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Opacity", target: material, propertyKey: "geometryOpacityTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: CheckboxPropertyLine, label: "Thin-Walled", target: material, propertyKey: "geometryThinWalled", description: "When enabled, treats material as a thin shell (like leaves, paper sheets or windows). Disables ray bending in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-walledcase" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Normal", target: material, propertyKey: "geometryNormalTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Tangent Angle", target: material, propertyKey: "geometryTangentAngle", min: 0, max: Math.PI, step: 0.01, description: "Tangent vector controlling anisotropic reflection direction for the base (metal and non-metal). Works with specular_roughness_anisotropy.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/geometry/tangent" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Tangent", target: material, propertyKey: "geometryTangentTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Tangent Angle", target: material, propertyKey: "geometryCoatTangentAngle", min: 0, max: Math.PI, step: 0.01, description: "Tangent vector controlling anisotropic reflection direction for the coat. Works with coat_roughness_anisotropy.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/geometry/coat-tangent" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Coat Normal", target: material, propertyKey: "geometryCoatNormalTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Coat Tangent", target: material, propertyKey: "geometryCoatTangentTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Geometry Thickness", target: material, propertyKey: "geometryThickness", min: 0, step: 0.001, description: "Controls the thickness of the geometry for volume approximations.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thickness" }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Geometry Thickness", target: material, propertyKey: "geometryThicknessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0 })] }));
16706
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Geometry Opacity", target: material, propertyKey: "geometryOpacity", min: 0, max: 1, step: 0.01, description: "Controls material presence and transparency cutout.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/opacity/transparency" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Opacity", target: material, propertyKey: "geometryOpacityTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: CheckboxPropertyLine, label: "Thin-Walled", target: material, propertyKey: "geometryThinWalled", description: "When enabled, treats material as a thin shell (like leaves, paper sheets or windows). Disables ray bending in refraction.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thin-walledcase" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Normal", target: material, propertyKey: "geometryNormalTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Tangent Angle", target: material, propertyKey: "geometryTangentAngle", min: 0, max: Math.PI, step: 0.01, description: "Tangent vector controlling anisotropic reflection direction for the base (metal and non-metal). Works with specular_roughness_anisotropy.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/geometry/tangent" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Tangent", target: material, propertyKey: "geometryTangentTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Coat Tangent Angle", target: material, propertyKey: "geometryCoatTangentAngle", min: 0, max: Math.PI, step: 0.01, description: "Tangent vector controlling anisotropic reflection direction for the coat. Works with coat_roughness_anisotropy.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/geometry/coat-tangent" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Coat Normal", target: material, propertyKey: "geometryCoatNormalTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Coat Tangent", target: material, propertyKey: "geometryCoatTangentTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Geometry Thickness", target: material, propertyKey: "geometryThickness", min: 0, step: 0.001, description: "Controls the thickness of the geometry for volume approximations.", docLink: "https://academysoftwarefoundation.github.io/OpenPBR/index.html#model/thickness" }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Geometry Thickness", target: material, propertyKey: "geometryThicknessTexture", scene: material.getScene(), defaultValue: null, onLink: (texture) => void 0, material: material })] }));
16023
16707
  };
16024
16708
  const SssQualityOptions = [
16025
16709
  { label: "Low (8 samples)", value: 0 },
@@ -16054,7 +16738,7 @@ const StandardMaterialTexturesProperties = (props) => {
16054
16738
  const { material, selectionService } = props;
16055
16739
  const scene = material.getScene();
16056
16740
  const selectEntity = (entity) => (selectionService.selectedEntity = entity);
16057
- return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Diffuse", target: material, propertyKey: "diffuseTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Specular", target: material, propertyKey: "specularTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, uniqueId: "StandardMaterialTextures_Reflection", label: "Reflection", target: material, propertyKey: "reflectionTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Refraction", target: material, propertyKey: "refractionTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Emissive", target: material, propertyKey: "emissiveTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Bump", target: material, propertyKey: "bumpTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Opacity", target: material, propertyKey: "opacityTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Ambient", target: material, propertyKey: "ambientTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Lightmap", target: material, propertyKey: "lightmapTexture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Detailmap", target: material.detailMap, propertyKey: "texture", propertyPath: "detailMap.texture", scene: scene, onLink: selectEntity, defaultValue: null }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Lightmap as Shadowmap", target: material, propertyKey: "useLightmapAsShadowmap" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Detailmap", target: material.detailMap, propertyKey: "isEnabled", propertyPath: "detailMap.isEnabled" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Decalmap", target: material.decalMap, propertyKey: "isEnabled", propertyPath: "decalMap.isEnabled" })] }));
16741
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Diffuse", target: material, propertyKey: "diffuseTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Specular", target: material, propertyKey: "specularTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, uniqueId: "StandardMaterialTextures_Reflection", label: "Reflection", target: material, propertyKey: "reflectionTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Refraction", target: material, propertyKey: "refractionTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Emissive", target: material, propertyKey: "emissiveTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Bump", target: material, propertyKey: "bumpTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Opacity", target: material, propertyKey: "opacityTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Ambient", target: material, propertyKey: "ambientTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Lightmap", target: material, propertyKey: "lightmapTexture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: MaterialTextureDebugPropertyLine, label: "Detailmap", target: material.detailMap, propertyKey: "texture", propertyPath: "detailMap.texture", scene: scene, onLink: selectEntity, defaultValue: null, material: material }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Lightmap as Shadowmap", target: material, propertyKey: "useLightmapAsShadowmap" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Detailmap", target: material.detailMap, propertyKey: "isEnabled", propertyPath: "detailMap.isEnabled" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Decalmap", target: material.decalMap, propertyKey: "isEnabled", propertyPath: "decalMap.isEnabled" })] }));
16058
16742
  };
16059
16743
  /**
16060
16744
  * Displays the levels properties of a standard material.
@@ -16440,7 +17124,7 @@ function SaveMetadata(entity, metadata) {
16440
17124
  entity.metadata = metadata;
16441
17125
  }
16442
17126
  }
16443
- const useStyles$l = makeStyles({
17127
+ const useStyles$k = makeStyles({
16444
17128
  mainDiv: {
16445
17129
  display: "flex",
16446
17130
  flexDirection: "column",
@@ -16460,7 +17144,7 @@ const useStyles$l = makeStyles({
16460
17144
  */
16461
17145
  const MetadataProperties = (props) => {
16462
17146
  const { entity } = props;
16463
- const classes = useStyles$l();
17147
+ const classes = useStyles$k();
16464
17148
  const { size } = useContext(ToolContext);
16465
17149
  const metadata = useProperty(entity, "metadata");
16466
17150
  const stringifiedMetadata = useMemo(() => StringifyMetadata(metadata, false) ?? "", [metadata]);
@@ -17395,7 +18079,7 @@ const ParticleSystemEmitterProperties = (props) => {
17395
18079
  } })) : (jsx(Property, { component: TextPropertyLine, propertyPath: "source", label: "Source", value: "No meshes in scene." })), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use normals for direction", target: particleEmitterType, propertyKey: "useMeshNormalsForDirection" }), !useMeshNormalsForDirection && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" })] }))] })), particleEmitterType instanceof BoxParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Min emit box", target: particleEmitterType, propertyKey: "minEmitBox" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Max emit box", target: particleEmitterType, propertyKey: "maxEmitBox" })] })), particleEmitterType instanceof ConeParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Height range", target: particleEmitterType, propertyKey: "heightRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Emit from spawn point only", target: particleEmitterType, propertyKey: "emitFromSpawnPointOnly" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof SphereParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof CylinderParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Height", target: particleEmitterType, propertyKey: "height", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof HemisphericParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof PointParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" })] }))] })), !scene && jsx(TextPropertyLine, { label: "Emitter", value: "No scene available." })] }));
17396
18080
  };
17397
18081
 
17398
- const useStyles$k = makeStyles({
18082
+ const useStyles$j = makeStyles({
17399
18083
  subsection: {
17400
18084
  marginTop: tokens.spacingVerticalM,
17401
18085
  },
@@ -17415,7 +18099,7 @@ const ParticleSystemSizeProperties = (props) => {
17415
18099
  const sizeGradientGetter = useCallback(() => system.getSizeGradients(), [system]);
17416
18100
  const sizeGradient = useObservableArray(system, sizeGradientGetter, "addSizeGradient", "removeSizeGradient", "forceRefreshGradients");
17417
18101
  const useSizeGradients = (sizeGradient?.length ?? 0) > 0;
17418
- const classes = useStyles$k();
18102
+ const classes = useStyles$j();
17419
18103
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min size", target: system, propertyKey: "minSize", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max size", target: system, propertyKey: "maxSize", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min scale x", target: system, propertyKey: "minScaleX", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max scale x", target: system, propertyKey: "maxScaleX", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min scale y", target: system, propertyKey: "minScaleY", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max scale y", target: system, propertyKey: "maxScaleY", min: 0, step: 0.1 }), isCpuParticleSystem && !useStartSizeGradients && (jsx(ButtonLine, { label: "Use Start Size gradients", onClick: () => {
17420
18104
  system.addStartSizeGradient(0, system.minSize, system.maxSize);
17421
18105
  system.forceRefreshGradients();
@@ -17451,7 +18135,7 @@ const ParticleSystemSizeProperties = (props) => {
17451
18135
  } })] }))] }));
17452
18136
  };
17453
18137
 
17454
- const useStyles$j = makeStyles({
18138
+ const useStyles$i = makeStyles({
17455
18139
  subsection: {
17456
18140
  marginTop: tokens.spacingVerticalM,
17457
18141
  },
@@ -17477,7 +18161,7 @@ const ParticleSystemEmissionProperties = (props) => {
17477
18161
  const useVelocityGradients = (velocityGradients?.length ?? 0) > 0;
17478
18162
  const useLimitVelocityGradients = (limitVelocityGradients?.length ?? 0) > 0;
17479
18163
  const useDragGradients = (dragGradients?.length ?? 0) > 0;
17480
- const classes = useStyles$j();
18164
+ const classes = useStyles$i();
17481
18165
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Emit rate", target: system, propertyKey: "emitRate", min: 0, step: 1 }), isCpuParticleSystem && !useEmitRateGradients && (jsx(ButtonLine, { label: "Use Emit rate gradients", onClick: () => {
17482
18166
  system.addEmitRateGradient(0, system.emitRate, system.emitRate);
17483
18167
  system.forceRefreshGradients();
@@ -17545,7 +18229,7 @@ const ParticleSystemEmissionProperties = (props) => {
17545
18229
  } })] }))] }));
17546
18230
  };
17547
18231
 
17548
- const useStyles$i = makeStyles({
18232
+ const useStyles$h = makeStyles({
17549
18233
  subsection: {
17550
18234
  marginTop: tokens.spacingVerticalM,
17551
18235
  },
@@ -17562,7 +18246,7 @@ const ParticleSystemLifetimeProperties = (props) => {
17562
18246
  const lifeTimeGradientsGetter = useCallback(() => system.getLifeTimeGradients(), [system]);
17563
18247
  const lifeTimeGradients = useObservableArray(system, lifeTimeGradientsGetter, "addLifeTimeGradient", "removeLifeTimeGradient", "forceRefreshGradients");
17564
18248
  const useLifeTimeGradients = (lifeTimeGradients?.length ?? 0) > 0;
17565
- const classes = useStyles$i();
18249
+ const classes = useStyles$h();
17566
18250
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min lifetime", target: system, propertyKey: "minLifeTime", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max lifetime", target: system, propertyKey: "maxLifeTime", min: 0, step: 0.1 }), isCpuParticleSystem && !useLifeTimeGradients && (jsx(ButtonLine, { label: "Use Lifetime gradients", onClick: () => {
17567
18251
  system.addLifeTimeGradient(0, system.minLifeTime, system.maxLifeTime);
17568
18252
  system.forceRefreshGradients();
@@ -17582,7 +18266,7 @@ const ParticleSystemLifetimeProperties = (props) => {
17582
18266
  } })] })), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Target stop duration", target: system, propertyKey: "targetStopDuration", min: 0, step: 0.1 })] }));
17583
18267
  };
17584
18268
 
17585
- const useStyles$h = makeStyles({
18269
+ const useStyles$g = makeStyles({
17586
18270
  subsection: {
17587
18271
  marginTop: tokens.spacingVerticalM,
17588
18272
  },
@@ -17609,7 +18293,7 @@ const ParticleSystemColorProperties = (props) => {
17609
18293
  const hasRampGradients = (rampGradients?.length ?? 0) > 0;
17610
18294
  const hasColorRemapGradients = (colorRemapGradients?.length ?? 0) > 0;
17611
18295
  const hasAlphaRemapGradients = (alphaRemapGradients?.length ?? 0) > 0;
17612
- const classes = useStyles$h();
18296
+ const classes = useStyles$g();
17613
18297
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Color4PropertyLine, label: "Color 1", target: system, propertyKey: "color1" }), jsx(BoundProperty, { component: Color4PropertyLine, label: "Color 2", target: system, propertyKey: "color2" }), jsx(BoundProperty, { component: Color4PropertyLine, label: "Color dead", target: system, propertyKey: "colorDead" }), !hasColorGradients && (jsx(ButtonLine, { label: "Use Color gradients", onClick: () => {
17614
18298
  system.addColorGradient(0, system.color1, system.color1);
17615
18299
  system.addColorGradient(1, system.color2, system.color2);
@@ -17681,7 +18365,7 @@ const ParticleSystemColorProperties = (props) => {
17681
18365
  } })] }))] }))] }));
17682
18366
  };
17683
18367
 
17684
- const useStyles$g = makeStyles({
18368
+ const useStyles$f = makeStyles({
17685
18369
  subsection: {
17686
18370
  marginTop: tokens.spacingVerticalM,
17687
18371
  },
@@ -17696,7 +18380,7 @@ const ParticleSystemRotationProperties = (props) => {
17696
18380
  const angularSpeedGradientsGetter = useCallback(() => system.getAngularSpeedGradients(), [system]);
17697
18381
  const angularSpeedGradients = useObservableArray(system, angularSpeedGradientsGetter, "addAngularSpeedGradient", "removeAngularSpeedGradient", "forceRefreshGradients");
17698
18382
  const useAngularSpeedGradients = (angularSpeedGradients?.length ?? 0) > 0;
17699
- const classes = useStyles$g();
18383
+ const classes = useStyles$f();
17700
18384
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min Angular speed", target: system, propertyKey: "minAngularSpeed", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max Angular speed", target: system, propertyKey: "maxAngularSpeed", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min initial rotation", target: system, propertyKey: "minInitialRotation", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max initial rotation", target: system, propertyKey: "maxInitialRotation", step: 0.01 }), !useAngularSpeedGradients && (jsx(ButtonLine, { label: "Use Angular speed gradients", onClick: () => {
17701
18385
  system.addAngularSpeedGradient(0, system.minAngularSpeed, system.maxAngularSpeed);
17702
18386
  system.forceRefreshGradients();
@@ -17802,7 +18486,7 @@ const AttractorComponent = (props) => {
17802
18486
  } }), !attractorData.isReadOnly && (jsx(ToggleButton, { title: "Add / remove position gizmo from particle attractor", checkedIcon: ArrowMoveFilled, value: isControlled(impostor), onChange: (control) => onControl(control ? impostor : undefined) }))] }));
17803
18487
  };
17804
18488
 
17805
- const useStyles$f = makeStyles({
18489
+ const useStyles$e = makeStyles({
17806
18490
  subsection: {
17807
18491
  marginTop: tokens.spacingVerticalM,
17808
18492
  },
@@ -17859,7 +18543,7 @@ const AttractorList = (props) => {
17859
18543
  gizmoManager.attachToMesh(attached);
17860
18544
  setControlledImpostor(attached);
17861
18545
  };
17862
- const classes = useStyles$f();
18546
+ const classes = useStyles$e();
17863
18547
  return (jsxs(Fragment, { children: [items.length > 0 && (jsxs(Fragment, { children: [jsx(Color3PropertyLine, { label: "Attractor Debug Color", value: impostorColor, onChange: setImpostorColor }), jsx(SyncedSliderPropertyLine, { label: "Attractor Debug Size", value: impostorScale, onChange: setImpostorScale, min: 0, max: 10, step: 0.1 }), jsx(Subtitle2, { className: classes.subsection, children: "Attractors list" })] })), jsx(List, { addButtonLabel: `Add New Attractor`, items: items, onDelete: attractorSource.removeAttractor
17864
18548
  ? (item, _index) => {
17865
18549
  // Only CPU attractors (Attractor instances) can be removed
@@ -17969,7 +18653,7 @@ const ParticleSystemAttractorProperties = (props) => {
17969
18653
  return (jsx(Fragment, { children: scene ? (jsx(AttractorList, { attractorSource: attractorSource, scene: scene })) : (jsx(MessageBar, { intent: "info", title: "No Scene Available", message: "Cannot display attractors without a scene" })) }));
17970
18654
  };
17971
18655
 
17972
- const useStyles$e = makeStyles({
18656
+ const useStyles$d = makeStyles({
17973
18657
  subsection: {
17974
18658
  marginTop: tokens.spacingVerticalM,
17975
18659
  },
@@ -18023,7 +18707,7 @@ const InputBlockPropertyLine = (props) => {
18023
18707
  */
18024
18708
  const ParticleSystemNodeEditorProperties = (props) => {
18025
18709
  const { particleSystem: system } = props;
18026
- const classes = useStyles$e();
18710
+ const classes = useStyles$d();
18027
18711
  const source = system.source;
18028
18712
  const inputBlocks = useObservableState(useCallback(() => {
18029
18713
  if (!source) {
@@ -18884,14 +19568,14 @@ const SkeletonPropertiesServiceDefinition = {
18884
19568
  },
18885
19569
  };
18886
19570
 
18887
- const useStyles$d = makeStyles({
19571
+ const useStyles$c = makeStyles({
18888
19572
  uniformWidth: {
18889
19573
  ...UniformWidthStyling,
18890
19574
  },
18891
19575
  });
18892
19576
  const SpinButtonPropertyLine = (props) => {
18893
19577
  SpinButtonPropertyLine.displayName = "SpinButtonPropertyLine";
18894
- const classes = useStyles$d();
19578
+ const classes = useStyles$c();
18895
19579
  return (jsx(PropertyLine, { ...props, children: jsx(SpinButton, { ...props, className: mergeClasses(classes.uniformWidth, props.className) }) }));
18896
19580
  };
18897
19581
 
@@ -19139,7 +19823,7 @@ function _TextureFormatHasNoAlpha(format) {
19139
19823
  }
19140
19824
  }
19141
19825
 
19142
- const useStyles$c = makeStyles({
19826
+ const useStyles$b = makeStyles({
19143
19827
  root: {
19144
19828
  display: "flex",
19145
19829
  flexDirection: "column",
@@ -19191,7 +19875,7 @@ const TextureChannelStates = {
19191
19875
  */
19192
19876
  const TexturePreview = (props) => {
19193
19877
  const { texture, disableToolbar = false, maxWidth = "100%", maxHeight = "384px", offsetX = 0, offsetY = 0, width, height, imperativeRef } = props;
19194
- const classes = useStyles$c();
19878
+ const classes = useStyles$b();
19195
19879
  const canvasRef = useRef(null);
19196
19880
  const [channels, setChannels] = useState(TextureChannelStates.ALL);
19197
19881
  const [face, setFace] = useState(0);
@@ -20101,7 +20785,7 @@ class TextureCanvasManager {
20101
20785
  }
20102
20786
  }
20103
20787
 
20104
- const useStyles$b = makeStyles({
20788
+ const useStyles$a = makeStyles({
20105
20789
  channelsBar: {
20106
20790
  display: "flex",
20107
20791
  flexDirection: "column",
@@ -20146,7 +20830,7 @@ const useStyles$b = makeStyles({
20146
20830
  */
20147
20831
  const ChannelsBar = (props) => {
20148
20832
  const { channels, setChannels } = props;
20149
- const classes = useStyles$b();
20833
+ const classes = useStyles$a();
20150
20834
  const toggleVisibility = useCallback((index) => {
20151
20835
  const newChannels = [...channels];
20152
20836
  newChannels[index] = { ...newChannels[index], visible: !newChannels[index].visible };
@@ -20176,7 +20860,7 @@ const ChannelsBar = (props) => {
20176
20860
  }) }));
20177
20861
  };
20178
20862
 
20179
- const useStyles$a = makeStyles({
20863
+ const useStyles$9 = makeStyles({
20180
20864
  propertiesBar: {
20181
20865
  display: "flex",
20182
20866
  backgroundColor: tokens.colorNeutralBackground1,
@@ -20226,7 +20910,7 @@ const useStyles$a = makeStyles({
20226
20910
  },
20227
20911
  });
20228
20912
  const PixelDataDisplay = ({ label, value }) => {
20229
- const classes = useStyles$a();
20913
+ const classes = useStyles$9();
20230
20914
  return (jsxs("span", { className: classes.pixelData, children: [jsxs(Label, { className: classes.pixelDataLabel, children: [label, ":"] }), jsx(Label, { className: classes.pixelDataValue, children: value !== undefined ? value : "-" })] }));
20231
20915
  };
20232
20916
  const CubeFaces = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
@@ -20237,7 +20921,7 @@ const CubeFaces = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
20237
20921
  */
20238
20922
  const PropertiesBar = (props) => {
20239
20923
  const { texture, size, saveTexture, pixelData, face, setFace, resetTexture, resizeTexture, uploadTexture, mipLevel, setMipLevel } = props;
20240
- const classes = useStyles$a();
20924
+ const classes = useStyles$9();
20241
20925
  const uploadInputRef = useRef(null);
20242
20926
  const [width, setWidth] = useState(size.width);
20243
20927
  const [height, setHeight] = useState(size.height);
@@ -20272,7 +20956,7 @@ const PropertiesBar = (props) => {
20272
20956
  return (jsxs("div", { className: classes.propertiesBar, children: [jsxs("div", { className: classes.section, children: [jsx(Label, { children: "W:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: width.toString(), readOnly: texture.isCube, onChange: (_, data) => setWidth(getNewDimension(width, data.value)) }), jsx(Label, { children: "H:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: height.toString(), readOnly: texture.isCube, onChange: (_, data) => setHeight(getNewDimension(height, data.value)) }), !texture.isCube && (jsx(Tooltip$1, { content: "Resize", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ResizeRegular, {}), onClick: handleResize }) }))] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "X", value: pixelData.x }), jsx(PixelDataDisplay, { label: "Y", value: pixelData.y })] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "R", value: pixelData.r }), jsx(PixelDataDisplay, { label: "G", value: pixelData.g }), jsx(PixelDataDisplay, { label: "B", value: pixelData.b }), jsx(PixelDataDisplay, { label: "A", value: pixelData.a })] }), texture.isCube && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsx(Toolbar$1, { size: "small", children: CubeFaces.map((label, index) => (jsx(ToolbarButton, { className: classes.faceButton, appearance: face === index ? "primary" : "subtle", onClick: () => setFace(index), children: label }, label))) })] })), mipsEnabled && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(Label, { children: "MIP:" }), jsx(Tooltip$1, { content: "Mip Preview Up", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronUpRegular, {}), disabled: mipLevel <= 0, onClick: () => setMipLevel(mipLevel - 1) }) }), jsx(Label, { children: mipLevel }), jsx(Tooltip$1, { content: "Mip Preview Down", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronDownRegular, {}), disabled: mipLevel >= maxLevels, onClick: () => setMipLevel(mipLevel + 1) }) })] })] })), jsx("div", { className: classes.spacer }), jsxs(Toolbar$1, { size: "small", children: [jsx(Tooltip$1, { content: "Reset", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowResetRegular, {}), onClick: resetTexture }) }), jsx(Tooltip$1, { content: "Upload", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowUploadRegular, {}), onClick: handleUploadClick }) }), jsx("input", { ref: uploadInputRef, className: classes.uploadInput, type: "file", accept: ".jpg, .png, .tga, .dds, .env, .exr", onChange: handleFileChange }), jsx(Tooltip$1, { content: "Save", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(SaveRegular, {}), onClick: saveTexture }) })] })] }));
20273
20957
  };
20274
20958
 
20275
- const useStyles$9 = makeStyles({
20959
+ const useStyles$8 = makeStyles({
20276
20960
  statusBar: {
20277
20961
  display: "flex",
20278
20962
  backgroundColor: tokens.colorNeutralBackground1,
@@ -20300,14 +20984,14 @@ const useStyles$9 = makeStyles({
20300
20984
  */
20301
20985
  const StatusBar = (props) => {
20302
20986
  const { texture, mipLevel } = props;
20303
- const classes = useStyles$9();
20987
+ const classes = useStyles$8();
20304
20988
  const factor = Math.pow(2, mipLevel);
20305
20989
  const width = Math.ceil(texture.getSize().width / factor);
20306
20990
  const height = Math.ceil(texture.getSize().height / factor);
20307
20991
  return (jsxs("div", { className: classes.statusBar, children: [jsx("span", { className: classes.fileName, children: texture.name }), !texture.noMipmap && (jsxs("span", { className: classes.mipInfo, children: ["MIP Preview: ", mipLevel, " (", width, "\u00D7", height, ")"] }))] }));
20308
20992
  };
20309
20993
 
20310
- const useStyles$8 = makeStyles({
20994
+ const useStyles$7 = makeStyles({
20311
20995
  toolbar: {
20312
20996
  display: "flex",
20313
20997
  flexDirection: "column",
@@ -20344,7 +21028,7 @@ const useStyles$8 = makeStyles({
20344
21028
  */
20345
21029
  const ToolBar = (props) => {
20346
21030
  const { tools, changeTool, activeToolIndex, metadata, setMetadata, hasAlpha } = props;
20347
- const classes = useStyles$8();
21031
+ const classes = useStyles$7();
20348
21032
  const computeRGBAColor = useCallback(() => {
20349
21033
  const opacityInt = Math.floor(metadata.alpha * 255);
20350
21034
  const opacityHex = opacityInt.toString(16).padStart(2, "0");
@@ -20373,7 +21057,7 @@ const ToolBar = (props) => {
20373
21057
  }) })] }));
20374
21058
  };
20375
21059
 
20376
- const useStyles$7 = makeStyles({
21060
+ const useStyles$6 = makeStyles({
20377
21061
  textureEditor: {
20378
21062
  display: "flex",
20379
21063
  flexDirection: "column",
@@ -20437,7 +21121,7 @@ const PREVIEW_UPDATE_DELAY_MS = 160;
20437
21121
  */
20438
21122
  const TextureEditor = (props) => {
20439
21123
  const { texture, toolProviders = [], window: editorWindow, onUpdate } = props;
20440
- const classes = useStyles$7();
21124
+ const classes = useStyles$6();
20441
21125
  // Canvas refs
20442
21126
  const uiCanvasRef = useRef(null);
20443
21127
  const canvas2DRef = useRef(null);
@@ -20585,7 +21269,7 @@ const TextureEditor = (props) => {
20585
21269
  return (jsxs("div", { className: classes.textureEditor, children: [jsx(PropertiesBar, { texture: texture, saveTexture: saveTexture, pixelData: pixelData, face: face, setFace: setFace, resetTexture: resetTexture, resizeTexture: resizeTexture, uploadTexture: uploadTexture, mipLevel: mipLevel, setMipLevel: setMipLevel, size: canvasManagerRef.current?.size || size }), jsxs("div", { className: classes.mainContent, children: [jsxs("div", { className: classes.canvasContainer, style: { cursor }, children: [jsx("canvas", { ref: uiCanvasRef, className: classes.canvasUI, tabIndex: 1 }), jsx("canvas", { ref: canvas2DRef, className: classes.canvas2D }), jsx("canvas", { ref: canvas3DRef, className: classes.canvas3D })] }), CurrentToolSettings && (jsx("div", { className: classes.toolSettingsContainer, children: jsx(CurrentToolSettings, {}) })), !texture.isCube && (jsx("div", { className: classes.sidebarLeft, children: jsx(ToolBar, { tools: toolProviders, activeToolIndex: activeToolIndex, changeTool: changeTool, metadata: metadata, setMetadata: setMetadata, hasAlpha: hasAlpha }) })), jsx("div", { className: classes.sidebarRight, children: jsx(ChannelsBar, { channels: channels, setChannels: setChannels }) })] }), jsx(StatusBar, { texture: texture, mipLevel: mipLevel })] }));
20586
21270
  };
20587
21271
 
20588
- const useStyles$6 = makeStyles({
21272
+ const useStyles$5 = makeStyles({
20589
21273
  settingsContainer: {
20590
21274
  display: "flex",
20591
21275
  flexDirection: "column",
@@ -20605,7 +21289,7 @@ const Contrast = {
20605
21289
  name: "Contrast/Exposure",
20606
21290
  order: 500,
20607
21291
  icon: () => {
20608
- const classes = useStyles$6();
21292
+ const classes = useStyles$5();
20609
21293
  return jsx(CircleHalfFillRegular, { className: classes.icon });
20610
21294
  },
20611
21295
  is3D: true,
@@ -20670,7 +21354,7 @@ const Contrast = {
20670
21354
  setContrast(0);
20671
21355
  },
20672
21356
  settingsComponent: () => {
20673
- const classes = useStyles$6();
21357
+ const classes = useStyles$5();
20674
21358
  const [contrast, exposure] = useObservableState(useCallback(() => [_contrast, _exposure], []), stateChangedObservable);
20675
21359
  const handleContrastChange = (_, data) => {
20676
21360
  setContrast(data.value);
@@ -20771,7 +21455,7 @@ const Floodfill = {
20771
21455
  },
20772
21456
  };
20773
21457
 
20774
- const useStyles$5 = makeStyles({
21458
+ const useStyles$4 = makeStyles({
20775
21459
  settingsContainer: {
20776
21460
  display: "flex",
20777
21461
  flexDirection: "column",
@@ -20895,7 +21579,7 @@ const Paintbrush = {
20895
21579
  pointerObserver?.remove();
20896
21580
  },
20897
21581
  settingsComponent: () => {
20898
- const classes = useStyles$5();
21582
+ const classes = useStyles$4();
20899
21583
  const width = useObservableState(useCallback(() => _width, []), stateChangedObservable);
20900
21584
  const handleWidthChange = (_, data) => {
20901
21585
  setWidth(data.value);
@@ -21511,57 +22195,294 @@ const AtmosphereExplorerServiceDefinition = {
21511
22195
  },
21512
22196
  };
21513
22197
 
21514
- const DisposableCommandServiceDefinition = {
21515
- friendlyName: "Disposable Command Service",
21516
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity],
21517
- factory: (sceneExplorerService, sceneContext) => {
21518
- const scene = sceneContext.currentScene;
21519
- if (!scene) {
21520
- return undefined;
21521
- }
21522
- const disposeCommandRegistration = sceneExplorerService.addEntityCommand({
21523
- predicate: (entity) => typeof entity.dispose === "function",
21524
- order: 10000 /* DefaultCommandsOrder.Dispose */,
21525
- getCommand: (disposable) => {
21526
- return {
21527
- type: "action",
21528
- mode: "contextMenu",
21529
- displayName: "Dispose",
21530
- icon: () => jsx(DeleteRegular, {}),
21531
- hotKey: {
21532
- keyCode: "Delete",
21533
- },
21534
- execute: () => disposable.dispose(),
21535
- };
21536
- },
21537
- });
21538
- return {
21539
- dispose() {
21540
- disposeCommandRegistration.dispose();
21541
- },
22198
+ function IsRoutable(node) {
22199
+ return node instanceof AbstractSoundSource || node instanceof AudioBus;
22200
+ }
22201
+ function GetEngineDisplayName(engine) {
22202
+ return LastCreatedAudioEngine() === engine ? "Last Created Audio Engine" : "Other Audio Engine";
22203
+ }
22204
+ const AudioV2ExplorerServiceDefinition = {
22205
+ friendlyName: "Audio V2 Explorer",
22206
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, GizmoServiceIdentity, SelectionServiceIdentity],
22207
+ factory: (sceneExplorerService, sceneContext, gizmoService, selectionService) => {
22208
+ // Section-level observables driven by per-engine subscriptions below.
22209
+ const entityAddedObservable = new Observable();
22210
+ const entityRemovedObservable = new Observable();
22211
+ const entityMovedObservable = new Observable();
22212
+ // Notified whenever any engine's "Last Created" status may have changed (i.e. an engine
22213
+ // was added or removed). Display info hooks subscribe to this to refresh the engine label.
22214
+ const engineDisplayNameChangedObservable = new Observable();
22215
+ // Per-engine subscription tokens (disposed when the engine is disposed).
22216
+ const engineSubscriptions = new Map();
22217
+ // Per-instance outBus interception tokens for sounds / sound sources / audio buses.
22218
+ const outBusInterceptors = new Map();
22219
+ const subscribeOutBus = (entity) => {
22220
+ if (outBusInterceptors.has(entity)) {
22221
+ return;
22222
+ }
22223
+ outBusInterceptors.set(entity, InterceptProperty(entity, "outBus", {
22224
+ afterSet: () => entityMovedObservable.notifyObservers(entity),
22225
+ }));
21542
22226
  };
21543
- },
21544
- };
21545
-
21546
- const EffectLayerExplorerServiceDefinition = {
21547
- friendlyName: "Effect Layer Explorer",
21548
- consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
21549
- factory: (sceneExplorerService, sceneContext, watcherService) => {
21550
- const scene = sceneContext.currentScene;
21551
- if (!scene) {
21552
- return undefined;
21553
- }
21554
- const sectionRegistration = sceneExplorerService.addSection({
21555
- displayName: "Effect Layers",
21556
- order: 700 /* DefaultSectionsOrder.EffectLayers */,
21557
- getRootEntities: () => scene.effectLayers,
21558
- getEntityDisplayInfo: (effectLayer) => {
21559
- const onChangeObservable = new Observable();
21560
- const nameHookToken = watcherService.watchProperty(effectLayer, "name", () => onChangeObservable.notifyObservers());
21561
- return {
21562
- get name() {
21563
- return effectLayer.name || `Unnamed ${effectLayer.getClassName()}`;
21564
- },
22227
+ const unsubscribeOutBus = (entity) => {
22228
+ outBusInterceptors.get(entity)?.dispose();
22229
+ outBusInterceptors.delete(entity);
22230
+ };
22231
+ const subscribeEngine = (engine) => {
22232
+ if (engineSubscriptions.has(engine)) {
22233
+ return;
22234
+ }
22235
+ const nodeAddedObserver = engine.onNodeAddedObservable.add((node) => {
22236
+ if (IsRoutable(node)) {
22237
+ subscribeOutBus(node);
22238
+ }
22239
+ entityAddedObservable.notifyObservers(node);
22240
+ });
22241
+ const nodeRemovedObserver = engine.onNodeRemovedObservable.add((node) => {
22242
+ if (IsRoutable(node)) {
22243
+ unsubscribeOutBus(node);
22244
+ }
22245
+ entityRemovedObservable.notifyObservers(node);
22246
+ });
22247
+ const disposeObserver = engine.onDisposeObservable.add(() => {
22248
+ unsubscribeEngine(engine);
22249
+ entityRemovedObservable.notifyObservers(engine);
22250
+ engineDisplayNameChangedObservable.notifyObservers();
22251
+ });
22252
+ // Seed outBus interception for nodes that already exist on this engine.
22253
+ for (const node of engine.nodes) {
22254
+ if (IsRoutable(node)) {
22255
+ subscribeOutBus(node);
22256
+ }
22257
+ }
22258
+ engineSubscriptions.set(engine, {
22259
+ dispose: () => {
22260
+ nodeAddedObserver.remove();
22261
+ nodeRemovedObserver.remove();
22262
+ disposeObserver.remove();
22263
+ },
22264
+ });
22265
+ };
22266
+ const unsubscribeEngine = (engine) => {
22267
+ engineSubscriptions.get(engine)?.dispose();
22268
+ engineSubscriptions.delete(engine);
22269
+ };
22270
+ // Seed with engines that already exist.
22271
+ for (const engine of AudioEngineV2.Instances) {
22272
+ subscribeEngine(engine);
22273
+ }
22274
+ // React to new engines.
22275
+ const engineCreatedObserver = OnAudioEngineV2CreatedObservable.add((engine) => {
22276
+ subscribeEngine(engine);
22277
+ entityAddedObservable.notifyObservers(engine);
22278
+ engineDisplayNameChangedObservable.notifyObservers();
22279
+ });
22280
+ const sectionRegistration = sceneExplorerService.addSection({
22281
+ displayName: "Audio V2",
22282
+ order: 1500 /* DefaultSectionsOrder.AudioV2 */,
22283
+ getRootEntities: () => [...AudioEngineV2.Instances],
22284
+ getEntityChildren: (entity) => {
22285
+ if (entity instanceof AudioEngineV2) {
22286
+ const children = [];
22287
+ for (const node of entity.nodes) {
22288
+ if (node instanceof MainAudioBus) {
22289
+ children.push(node);
22290
+ }
22291
+ else if (IsRoutable(node) && !node.outBus) {
22292
+ // Surface routable nodes that don't route through a bus (e.g. microphone
22293
+ // sound sources, or sounds with outBus explicitly cleared) directly under
22294
+ // the engine so they remain visible in the tree.
22295
+ children.push(node);
22296
+ }
22297
+ }
22298
+ return children;
22299
+ }
22300
+ if (entity instanceof MainAudioBus || entity instanceof AudioBus) {
22301
+ const children = [];
22302
+ for (const node of entity.engine.nodes) {
22303
+ if (IsRoutable(node) && node.outBus === entity) {
22304
+ children.push(node);
22305
+ }
22306
+ }
22307
+ return children;
22308
+ }
22309
+ return [];
22310
+ },
22311
+ getEntityDisplayInfo: (entity) => {
22312
+ const onChangeObservable = new Observable();
22313
+ let nodeNameObserver = null;
22314
+ let engineDisplayObserver = null;
22315
+ if (entity instanceof AudioEngineV2) {
22316
+ engineDisplayObserver = engineDisplayNameChangedObservable.add(() => onChangeObservable.notifyObservers());
22317
+ }
22318
+ else {
22319
+ nodeNameObserver = entity.onNameChangedObservable.add(() => onChangeObservable.notifyObservers());
22320
+ }
22321
+ return {
22322
+ get name() {
22323
+ if (entity instanceof AudioEngineV2) {
22324
+ return GetEngineDisplayName(entity);
22325
+ }
22326
+ return entity.name || `Unnamed ${entity.getClassName()}`;
22327
+ },
22328
+ onChange: onChangeObservable,
22329
+ dispose: () => {
22330
+ nodeNameObserver?.remove();
22331
+ engineDisplayObserver?.remove();
22332
+ onChangeObservable.clear();
22333
+ },
22334
+ };
22335
+ },
22336
+ entityIcon: ({ entity }) => {
22337
+ const color = tokens.colorPaletteForestForeground2;
22338
+ if (entity instanceof AudioEngineV2) {
22339
+ return jsx(HeadphonesSoundWaveRegular, { color: color });
22340
+ }
22341
+ if (entity instanceof AbstractAudioBus) {
22342
+ return jsx(ArrowEnterUpRegular, { color: color });
22343
+ }
22344
+ if (entity instanceof StaticSound) {
22345
+ return jsx(SoundWaveCircleRegular, { color: color });
22346
+ }
22347
+ if (entity instanceof StreamingSound) {
22348
+ return jsx(SoundWaveCircleFilled, { color: color });
22349
+ }
22350
+ if (entity instanceof AbstractSoundSource) {
22351
+ return jsx(CatchUpRegular, { color: color });
22352
+ }
22353
+ return jsx(Fragment, {});
22354
+ },
22355
+ getEntityAddedObservables: () => [entityAddedObservable],
22356
+ getEntityRemovedObservables: () => [entityRemovedObservable],
22357
+ getEntityMovedObservables: () => [entityMovedObservable],
22358
+ });
22359
+ // Spatial-audio visualization gizmo toggle.
22360
+ const spatialGizmoCommandRegistration = sceneExplorerService.addEntityCommand({
22361
+ predicate: (entity) => entity instanceof AbstractSoundSource && entity._isSpatial,
22362
+ order: 800 /* DefaultCommandsOrder.GizmoActive */,
22363
+ getCommand: (source) => {
22364
+ const onChangeObservable = new Observable();
22365
+ let gizmoRef = null;
22366
+ let onClickedObserver = null;
22367
+ const releaseGizmo = () => {
22368
+ onClickedObserver?.dispose();
22369
+ onClickedObserver = null;
22370
+ gizmoRef?.dispose();
22371
+ gizmoRef = null;
22372
+ };
22373
+ return {
22374
+ type: "toggle",
22375
+ get displayName() {
22376
+ return `Turn ${gizmoRef ? "Off" : "On"} Gizmo`;
22377
+ },
22378
+ icon: () => (gizmoRef ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {})),
22379
+ get isEnabled() {
22380
+ return !!gizmoRef;
22381
+ },
22382
+ set isEnabled(enabled) {
22383
+ if (enabled) {
22384
+ if (!gizmoRef) {
22385
+ const scene = sceneContext.currentScene;
22386
+ if (scene) {
22387
+ const ref = gizmoService.getSpatialAudioGizmo(source, scene);
22388
+ gizmoRef = ref;
22389
+ // Clicking the gizmo in the viewport selects the underlying sound source
22390
+ // so the inspector navigates to it (mirrors how camera/light gizmos behave
22391
+ // via the scene picking path).
22392
+ const observer = ref.value.onClickedObservable.add((clickedSource) => {
22393
+ selectionService.selectedEntity = clickedSource;
22394
+ });
22395
+ onClickedObserver = { dispose: () => observer.remove() };
22396
+ onChangeObservable.notifyObservers();
22397
+ }
22398
+ }
22399
+ }
22400
+ else if (gizmoRef) {
22401
+ releaseGizmo();
22402
+ onChangeObservable.notifyObservers();
22403
+ }
22404
+ },
22405
+ onChange: onChangeObservable,
22406
+ dispose: () => {
22407
+ releaseGizmo();
22408
+ onChangeObservable.clear();
22409
+ },
22410
+ };
22411
+ },
22412
+ });
22413
+ return {
22414
+ dispose: () => {
22415
+ engineCreatedObserver.remove();
22416
+ for (const subscription of engineSubscriptions.values()) {
22417
+ subscription.dispose();
22418
+ }
22419
+ engineSubscriptions.clear();
22420
+ for (const token of outBusInterceptors.values()) {
22421
+ token.dispose();
22422
+ }
22423
+ outBusInterceptors.clear();
22424
+ entityAddedObservable.clear();
22425
+ entityRemovedObservable.clear();
22426
+ entityMovedObservable.clear();
22427
+ engineDisplayNameChangedObservable.clear();
22428
+ spatialGizmoCommandRegistration.dispose();
22429
+ sectionRegistration.dispose();
22430
+ },
22431
+ };
22432
+ },
22433
+ };
22434
+
22435
+ const DisposableCommandServiceDefinition = {
22436
+ friendlyName: "Disposable Command Service",
22437
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity],
22438
+ factory: (sceneExplorerService, sceneContext) => {
22439
+ const scene = sceneContext.currentScene;
22440
+ if (!scene) {
22441
+ return undefined;
22442
+ }
22443
+ const disposeCommandRegistration = sceneExplorerService.addEntityCommand({
22444
+ predicate: (entity) => typeof entity.dispose === "function",
22445
+ order: 10000 /* DefaultCommandsOrder.Dispose */,
22446
+ getCommand: (disposable) => {
22447
+ return {
22448
+ type: "action",
22449
+ mode: "contextMenu",
22450
+ displayName: "Dispose",
22451
+ icon: () => jsx(DeleteRegular, {}),
22452
+ hotKey: {
22453
+ keyCode: "Delete",
22454
+ },
22455
+ execute: () => disposable.dispose(),
22456
+ };
22457
+ },
22458
+ });
22459
+ return {
22460
+ dispose() {
22461
+ disposeCommandRegistration.dispose();
22462
+ },
22463
+ };
22464
+ },
22465
+ };
22466
+
22467
+ const EffectLayerExplorerServiceDefinition = {
22468
+ friendlyName: "Effect Layer Explorer",
22469
+ consumes: [SceneExplorerServiceIdentity, SceneContextIdentity, WatcherServiceIdentity],
22470
+ factory: (sceneExplorerService, sceneContext, watcherService) => {
22471
+ const scene = sceneContext.currentScene;
22472
+ if (!scene) {
22473
+ return undefined;
22474
+ }
22475
+ const sectionRegistration = sceneExplorerService.addSection({
22476
+ displayName: "Effect Layers",
22477
+ order: 700 /* DefaultSectionsOrder.EffectLayers */,
22478
+ getRootEntities: () => scene.effectLayers,
22479
+ getEntityDisplayInfo: (effectLayer) => {
22480
+ const onChangeObservable = new Observable();
22481
+ const nameHookToken = watcherService.watchProperty(effectLayer, "name", () => onChangeObservable.notifyObservers());
22482
+ return {
22483
+ get name() {
22484
+ return effectLayer.name || `Unnamed ${effectLayer.getClassName()}`;
22485
+ },
21565
22486
  onChange: onChangeObservable,
21566
22487
  dispose: () => {
21567
22488
  nameHookToken.dispose();
@@ -22889,91 +23810,1447 @@ const ExportServiceDefinition = {
22889
23810
  },
22890
23811
  };
22891
23812
 
22892
- const useStyles$4 = makeStyles({
22893
- statusMessage: {
22894
- padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
22895
- opacity: 0.7,
22896
- },
22897
- busyMessage: {
22898
- display: "flex",
22899
- alignItems: "center",
22900
- gap: tokens.spacingHorizontalXS,
22901
- padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
22902
- },
22903
- });
23813
+ const OverrideManagerKey = Symbol("babylonjs:overrideManager");
23814
+ const OverrideManagerInternals = new WeakMap();
23815
+ const OnOverrideManagerCreatedObservable = new Observable();
22904
23816
  /**
22905
- * Save/load controls for a scene's Smart Asset map.
22906
- * @param props - Component props.
22907
- * @returns The Smart Asset map controls.
23817
+ * Creates a new OverrideManager state object and attaches it to the scene.
23818
+ *
23819
+ * Internal: callers should use {@link GetOverrideManager} which returns the
23820
+ * existing manager when one is already attached.
23821
+ * @param scene - The scene this manager operates on.
23822
+ * @returns The created override manager state.
22908
23823
  */
22909
- const SmartAssetProjectTools = (props) => {
22910
- const { scene } = props;
22911
- const [statusMessage, setStatusMessage] = useState("");
22912
- const [busyMessage, setBusyMessage] = useState("");
22913
- const styles = useStyles$4();
22914
- const isBusy = busyMessage !== "";
22915
- const onSaveAssetMap = useCallback(async () => {
22916
- if (isBusy) {
22917
- return;
22918
- }
22919
- setBusyMessage("Saving assets...");
22920
- setStatusMessage("");
22921
- try {
22922
- const assetMap = SerializeSmartAssetManagerMap(scene);
22923
- const jsonBlob = new Blob([JSON.stringify(assetMap, null, 2)], { type: "application/json" });
22924
- Tools.Download(jsonBlob, "smart-assets.json");
22925
- setStatusMessage(`Saved: ${Object.keys(assetMap.assets).length} assets`);
23824
+ function CreateOverrideManager(scene) {
23825
+ const manager = {
23826
+ scene,
23827
+ onChangedObservable: new Observable(),
23828
+ };
23829
+ const internal = {
23830
+ overrides: [],
23831
+ originalValues: new Map(),
23832
+ sceneDisposeObserver: null,
23833
+ };
23834
+ OverrideManagerInternals.set(manager, internal);
23835
+ if (!scene.metadata) {
23836
+ scene.metadata = {};
23837
+ }
23838
+ scene.metadata[OverrideManagerKey] = manager;
23839
+ // Auto-dispose when the scene is disposed so the manager doesn't outlive it.
23840
+ internal.sceneDisposeObserver = scene.onDisposeObservable.add(() => DisposeOverrideManager(manager));
23841
+ OnOverrideManagerCreatedObservable.notifyObservers(manager);
23842
+ return manager;
23843
+ }
23844
+ /**
23845
+ * Returns the OverrideManager attached to the given scene, creating and
23846
+ * attaching one if none exists.
23847
+ * @param scene - The scene to look up or attach a manager to.
23848
+ * @returns The existing or newly created OverrideManager.
23849
+ */
23850
+ function GetOverrideManager(scene) {
23851
+ const existing = scene.metadata?.[OverrideManagerKey];
23852
+ if (existing) {
23853
+ return existing;
23854
+ }
23855
+ return CreateOverrideManager(scene);
23856
+ }
23857
+ /**
23858
+ * Adds an override entry and immediately applies it.
23859
+ * If an override with the same target coordinates already exists, it is replaced.
23860
+ *
23861
+ * When the caller has already mutated the target (e.g. an Inspector edit),
23862
+ * pass `{ originalValue }` containing the property's prior value — this seeds
23863
+ * the original-value map (so {@link RemoveOverride} can restore it) and skips
23864
+ * the redundant apply step.
23865
+ * @param scene - The scene whose override registry to update.
23866
+ * @param entry - The override to add.
23867
+ * @param options - Optional behavior modifiers; see {@link AddOverrideOptions}.
23868
+ */
23869
+ function AddOverride(scene, entry, options) {
23870
+ const manager = GetOverrideManager(scene);
23871
+ const internal = GetOverrideInternals(manager);
23872
+ RemoveMatchingOverride(internal, entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23873
+ internal.overrides.push(entry);
23874
+ if (options && "originalValue" in options) {
23875
+ // Caller already applied the new value. Seed the captured original from
23876
+ // their pre-edit snapshot — otherwise ApplyOverrideEntry would capture
23877
+ // the post-edit value and RemoveOverride would have nothing to restore.
23878
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23879
+ if (!internal.originalValues.has(origKey)) {
23880
+ internal.originalValues.set(origKey, CloneValue(options.originalValue));
22926
23881
  }
22927
- catch (err) {
22928
- setStatusMessage(`Save error: ${err}`);
23882
+ }
23883
+ else {
23884
+ ApplyOverrideEntry(manager, internal, entry);
23885
+ }
23886
+ manager.onChangedObservable.notifyObservers();
23887
+ }
23888
+ /**
23889
+ * Returns all overrides currently registered with the scene.
23890
+ * @param scene - The scene whose override registry to read.
23891
+ * @returns A read-only array of override entries.
23892
+ */
23893
+ function GetOverrides(scene) {
23894
+ return GetOverrideInternals(GetOverrideManager(scene)).overrides;
23895
+ }
23896
+ /**
23897
+ * Removes all overrides, optionally restoring original values.
23898
+ * @param scene - The scene whose override registry to clear.
23899
+ * @param restoreOriginals - If true, restores all captured original values.
23900
+ */
23901
+ function ClearOverrides(scene, restoreOriginals = false) {
23902
+ const manager = GetOverrideManager(scene);
23903
+ const internal = GetOverrideInternals(manager);
23904
+ if (restoreOriginals) {
23905
+ // Snapshot the entries so we can restore each original without firing
23906
+ // an onChangedObservable notification per entry; consumers only need
23907
+ // one signal that the registry was emptied.
23908
+ const entries = [...internal.overrides];
23909
+ internal.overrides.length = 0;
23910
+ for (const entry of entries) {
23911
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23912
+ const original = internal.originalValues.get(origKey);
23913
+ if (original !== undefined) {
23914
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
23915
+ if (target) {
23916
+ SetNestedProperty(target, entry.propertyPath, original);
23917
+ }
23918
+ }
22929
23919
  }
22930
- finally {
22931
- setBusyMessage("");
23920
+ internal.originalValues.clear();
23921
+ manager.onChangedObservable.notifyObservers();
23922
+ return;
23923
+ }
23924
+ internal.overrides.length = 0;
23925
+ internal.originalValues.clear();
23926
+ manager.onChangedObservable.notifyObservers();
23927
+ }
23928
+ /**
23929
+ * Updates the target coordinates on the override matching a specific (type,
23930
+ * old-name, old-index) so it follows an entity rename. Used by capture services
23931
+ * to keep overrides attached to a specific object after the user renames it.
23932
+ *
23933
+ * Only the override at the exact `(targetType, oldName, oldIndex)` slot is
23934
+ * updated, so other same-named siblings keep their own overrides untouched.
23935
+ *
23936
+ * @param scene - The scene whose override registry to update.
23937
+ * @param targetType - The target type.
23938
+ * @param oldName - The previous name of the renamed entity.
23939
+ * @param oldIndex - The previous index of the renamed entity among same-named siblings.
23940
+ * @param newName - The new name of the renamed entity.
23941
+ * @param newIndex - The new index of the renamed entity among same-named siblings.
23942
+ */
23943
+ function RenameOverrideTarget(scene, targetType, oldName, oldIndex, newName, newIndex) {
23944
+ const manager = GetOverrideManager(scene);
23945
+ const internal = GetOverrideInternals(manager);
23946
+ let changed = false;
23947
+ for (let i = 0; i < internal.overrides.length; i++) {
23948
+ const entry = internal.overrides[i];
23949
+ if (entry.targetType === targetType && entry.targetName === oldName && entry.targetIndex === oldIndex) {
23950
+ internal.overrides[i] = { ...entry, targetName: newName, targetIndex: newIndex };
23951
+ changed = true;
23952
+ }
23953
+ }
23954
+ // Update original-value keys to match the new identity
23955
+ const oldPrefix = `${targetType}::${oldName}::${oldIndex}::`;
23956
+ const newPrefix = `${targetType}::${newName}::${newIndex}::`;
23957
+ for (const [origKey, value] of Array.from(internal.originalValues.entries())) {
23958
+ if (origKey.startsWith(oldPrefix)) {
23959
+ const propertyPath = origKey.substring(oldPrefix.length);
23960
+ internal.originalValues.set(newPrefix + propertyPath, value);
23961
+ internal.originalValues.delete(origKey);
23962
+ }
23963
+ }
23964
+ if (changed) {
23965
+ manager.onChangedObservable.notifyObservers();
23966
+ }
23967
+ }
23968
+ /**
23969
+ * Rewrites override *values* that reference an entity by name when that entity
23970
+ * has been renamed. Mirrors {@link RenameOverrideTarget} but operates on the
23971
+ * `value` field rather than the `targetName` field, so overrides whose value
23972
+ * is `"ref:oldName"` (material/light/camera references) or `"texture:oldName"`
23973
+ * (non-SmartAsset texture references) follow the rename instead of silently
23974
+ * pointing at a non-existent entity.
23975
+ *
23976
+ * SmartAsset texture references (`"samTexture:<key>"`) are unaffected because
23977
+ * the SmartAsset key is decoupled from the texture's runtime `name` field.
23978
+ *
23979
+ * @param scene - The scene whose override registry to update.
23980
+ * @param valueScheme - Which encoded-reference prefix to rewrite: `"ref"` for
23981
+ * material/light/camera references, `"texture"` for non-SAM textures.
23982
+ * @param oldName - The previous name embedded in the reference.
23983
+ * @param newName - The new name embedded in the reference.
23984
+ */
23985
+ function RenameOverrideValueReferences(scene, valueScheme, oldName, newName) {
23986
+ if (oldName === newName) {
23987
+ return;
23988
+ }
23989
+ const manager = GetOverrideManager(scene);
23990
+ const internal = GetOverrideInternals(manager);
23991
+ const oldValue = `${valueScheme}:${oldName}`;
23992
+ const newValue = `${valueScheme}:${newName}`;
23993
+ let changed = false;
23994
+ for (let i = 0; i < internal.overrides.length; i++) {
23995
+ const entry = internal.overrides[i];
23996
+ if (entry.value === oldValue) {
23997
+ internal.overrides[i] = { ...entry, value: newValue };
23998
+ changed = true;
22932
23999
  }
22933
- }, [scene, isBusy]);
22934
- const onLoadAssetMap = useCallback(async (files) => {
22935
- const file = files[0];
22936
- if (!file) {
22937
- return;
24000
+ }
24001
+ if (changed) {
24002
+ manager.onChangedObservable.notifyObservers();
24003
+ }
24004
+ }
24005
+ // ── Application ──
24006
+ /**
24007
+ * Applies all overrides to their current targets in the scene.
24008
+ *
24009
+ * Call this after any scene mutation that might have invalidated previously
24010
+ * applied state (asset reload, object recreation, project load). The override
24011
+ * manager does not auto-subscribe to other scene subsystems — coordination is
24012
+ * the caller's responsibility, which keeps the override system independent.
24013
+ * @param scene - The scene whose overrides to apply.
24014
+ */
24015
+ function ApplyAllOverrides(scene) {
24016
+ const manager = GetOverrideManager(scene);
24017
+ const internal = GetOverrideInternals(manager);
24018
+ for (const entry of internal.overrides) {
24019
+ ApplyOverrideEntry(manager, internal, entry);
24020
+ }
24021
+ }
24022
+ // ── Serialization ──
24023
+ /**
24024
+ * Serializes all overrides to a JSON-compatible array.
24025
+ * The on-disk shape is identical to the in-memory `IOverrideEntry`.
24026
+ * @param scene - The scene whose overrides to serialize.
24027
+ * @returns An array of override entries (shallow copies).
24028
+ */
24029
+ function SerializeOverrides(scene) {
24030
+ const internal = GetOverrideInternals(GetOverrideManager(scene));
24031
+ return internal.overrides.map((o) => ({ ...o }));
24032
+ }
24033
+ /**
24034
+ * Loads overrides from a serialized array and applies them.
24035
+ * @param scene - The scene whose override registry to populate.
24036
+ * @param data - Array of override entries.
24037
+ */
24038
+ function DeserializeAndApplyOverrides(scene, data) {
24039
+ if (!Array.isArray(data)) {
24040
+ throw new Error("OverrideManager: Expected an array of override entries.");
24041
+ }
24042
+ for (const entry of data) {
24043
+ if (!entry.targetType || entry.targetName === undefined || typeof entry.targetIndex !== "number" || !entry.propertyPath || entry.value === undefined) {
24044
+ Logger.Warn("OverrideManager: Skipping invalid override entry.");
24045
+ continue;
22938
24046
  }
22939
- if (isBusy) {
22940
- return;
24047
+ AddOverride(scene, entry);
24048
+ }
24049
+ }
24050
+ // ── Lifecycle ──
24051
+ /**
24052
+ * Disposes the manager, clearing all overrides and detaching it from its scene.
24053
+ * Safe to call multiple times; subsequent calls are no-ops. Automatically invoked when the
24054
+ * owning scene is disposed.
24055
+ * @param manager - The override manager state.
24056
+ */
24057
+ function DisposeOverrideManager(manager) {
24058
+ const internal = OverrideManagerInternals.get(manager);
24059
+ if (!internal) {
24060
+ return;
24061
+ }
24062
+ OverrideManagerInternals.delete(manager);
24063
+ if (internal.sceneDisposeObserver) {
24064
+ manager.scene.onDisposeObservable.remove(internal.sceneDisposeObserver);
24065
+ internal.sceneDisposeObserver = null;
24066
+ }
24067
+ internal.overrides.length = 0;
24068
+ internal.originalValues.clear();
24069
+ manager.onChangedObservable.clear();
24070
+ if (manager.scene.metadata) {
24071
+ delete manager.scene.metadata[OverrideManagerKey];
24072
+ }
24073
+ }
24074
+ // ── Private ──
24075
+ function GetOverrideInternals(manager) {
24076
+ const internal = OverrideManagerInternals.get(manager);
24077
+ if (!internal) {
24078
+ throw new Error("OverrideManager: Unknown manager state.");
24079
+ }
24080
+ return internal;
24081
+ }
24082
+ /**
24083
+ * Applies a single override entry to its target, capturing the original value
24084
+ * on the first application so {@link RemoveOverride} can restore it later.
24085
+ * @param manager - The override manager owning the entry.
24086
+ * @param internal - The manager's internal state.
24087
+ * @param entry - The override to apply.
24088
+ */
24089
+ function ApplyOverrideEntry(manager, internal, entry) {
24090
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
24091
+ if (!target) {
24092
+ Logger.Warn(`OverrideManager: target not found for type="${entry.targetType}" name="${entry.targetName}" index=${entry.targetIndex} prop="${entry.propertyPath}"`);
24093
+ return; // Target not loaded yet — override will be applied on next ApplyAllOverrides
24094
+ }
24095
+ // Capture original value before first override
24096
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
24097
+ if (!internal.originalValues.has(origKey)) {
24098
+ const currentValue = GetNestedProperty(target, entry.propertyPath);
24099
+ if (currentValue !== undefined) {
24100
+ internal.originalValues.set(origKey, CloneValue(currentValue));
22941
24101
  }
22942
- setBusyMessage("Loading assets...");
22943
- setStatusMessage("");
22944
- try {
22945
- await Promise.all(Array.from(GetAllSmartAssets(scene).keys()).map(async (key) => await RemoveSmartAssetAsync(scene, key)));
22946
- await LoadSmartAssetMapAsync(scene, file);
22947
- setStatusMessage(`Loaded: ${GetAllSmartAssets(scene).size} assets`);
24102
+ }
24103
+ const resolvedValue = ResolveOverrideValue(manager.scene, entry.value);
24104
+ SetNestedProperty(target, entry.propertyPath, resolvedValue);
24105
+ }
24106
+ /**
24107
+ * Locates a scene object by (targetType, targetName, targetIndex). The scene
24108
+ * collection is filtered to objects matching `targetName`; the N-th survivor
24109
+ * (per `targetIndex`) is returned. Falls back to the first match if the index
24110
+ * is out of range — useful when the scene shape has changed since capture.
24111
+ * @param scene - The scene to search.
24112
+ * @param targetType - The override target type.
24113
+ * @param targetName - The target object name (or "" for scene-level).
24114
+ * @param targetIndex - The target's position among same-named siblings.
24115
+ * @returns The matching scene object, or null if not found.
24116
+ */
24117
+ function ResolveTarget(scene, targetType, targetName, targetIndex) {
24118
+ // Scene-level overrides target the scene itself
24119
+ if (targetType === "scene") {
24120
+ return scene;
24121
+ }
24122
+ const collection = GetCollection$1(scene, targetType);
24123
+ if (!collection) {
24124
+ return null;
24125
+ }
24126
+ const matches = collection.filter((obj) => obj.name === targetName);
24127
+ if (matches.length === 0) {
24128
+ return null;
24129
+ }
24130
+ return (matches[targetIndex] ?? matches[0]);
24131
+ }
24132
+ /**
24133
+ * Returns the scene collection corresponding to an override target type.
24134
+ * @param scene - The scene to inspect.
24135
+ * @param targetType - The target type.
24136
+ * @returns The collection, or null if the type has no collection.
24137
+ */
24138
+ function GetCollection$1(scene, targetType) {
24139
+ switch (targetType) {
24140
+ case "meshes":
24141
+ return scene.meshes;
24142
+ case "materials":
24143
+ return scene.materials;
24144
+ case "textures":
24145
+ return scene.textures;
24146
+ case "lights":
24147
+ return scene.lights;
24148
+ case "cameras":
24149
+ return scene.cameras;
24150
+ case "animationGroups":
24151
+ return scene.animationGroups;
24152
+ default:
24153
+ return null;
24154
+ }
24155
+ }
24156
+ /**
24157
+ * Resolves an override value, expanding string references like "ref:name",
24158
+ * "samTexture:key", or "texture:name" into the actual scene object they refer to.
24159
+ * @param scene - The scene used to look up references.
24160
+ * @param value - The serialized override value.
24161
+ * @returns The runtime value to assign to the target property.
24162
+ */
24163
+ function ResolveOverrideValue(scene, value) {
24164
+ if (typeof value === "string") {
24165
+ if (value.startsWith("ref:")) {
24166
+ return ResolveObjectReference(scene, value.substring(4));
22948
24167
  }
22949
- catch (err) {
22950
- setStatusMessage(`Load error: ${err}`);
24168
+ if (value.startsWith("samTexture:")) {
24169
+ return ResolveSamTextureReference(scene, value.substring(11));
22951
24170
  }
22952
- finally {
22953
- setBusyMessage("");
24171
+ if (value.startsWith("texture:")) {
24172
+ return ResolveTextureReference(scene, value.substring(8));
22954
24173
  }
22955
- }, [scene, isBusy]);
22956
- return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Save Smart Assets", icon: ArrowDownloadRegular, onClick: onSaveAssetMap, disabled: isBusy }), jsx(FileUploadLine, { label: "Load Smart Assets", accept: ".json", onClick: onLoadAssetMap, disabled: isBusy }), jsx(Collapse, { visible: isBusy, children: jsxs("div", { className: styles.busyMessage, children: [jsx(Spinner, { size: "extra-small" }), jsx(Caption1, { children: busyMessage })] }) }), jsx(Collapse, { visible: statusMessage !== "", children: jsx(Caption1, { className: styles.statusMessage, children: statusMessage }) })] }));
22957
- };
22958
-
22959
- const SmartAssetsPaneKey$1 = "Smart Assets";
22960
- const SceneFileAccept = [".glb", ".gltf", ".babylon", ".obj"];
22961
- const TextureFileAccept = Array.from(GetSmartAssetTextureExtensions());
22962
- const AllAcceptString = [...SceneFileAccept, ...TextureFileAccept].join(",");
22963
- // eslint-disable-next-line @typescript-eslint/naming-convention
22964
- function _isTextureExtension(ext) {
22965
- return GetSmartAssetTextureExtensions().has(ext);
24174
+ }
24175
+ // Number arrays are passed through as-is. SetNestedProperty will use
24176
+ // the live target's `fromArray` method (Vector3, Color3, etc.) to push
24177
+ // values in-place, preserving the math instance identity.
24178
+ return value;
22966
24179
  }
22967
24180
  /**
22968
- * Inspector pane service that hosts the Smart Assets pane.
24181
+ * Resolves a "ref:name" value by looking up a material, light, or camera
24182
+ * in the scene by name.
24183
+ * @param scene - The scene to search.
24184
+ * @param name - The object name to resolve.
24185
+ * @returns The matching material, light, or camera, or undefined if not found.
22969
24186
  */
22970
- const SmartAssetsServiceDefinition = {
22971
- friendlyName: "Smart Assets",
22972
- consumes: [ShellServiceIdentity, SceneContextIdentity, SelectionServiceIdentity],
22973
- factory: (shellService, sceneContext, selectionService) => {
22974
- const paneRegistration = shellService.addSidePane({
22975
- key: SmartAssetsPaneKey$1,
22976
- title: SmartAssetsPaneKey$1,
24187
+ function ResolveObjectReference(scene, name) {
24188
+ const mat = scene.materials.find((m) => m.name === name);
24189
+ if (mat) {
24190
+ return mat;
24191
+ }
24192
+ const light = scene.lights.find((l) => l.name === name);
24193
+ if (light) {
24194
+ return light;
24195
+ }
24196
+ const camera = scene.cameras.find((c) => c.name === name);
24197
+ if (camera) {
24198
+ return camera;
24199
+ }
24200
+ Logger.Warn(`OverrideManager: Object reference "${name}" not found in scene.`);
24201
+ return undefined;
24202
+ }
24203
+ /**
24204
+ * Resolves a "texture:name" value by looking up a texture in the scene by name.
24205
+ * @param scene - The scene to search.
24206
+ * @param name - The texture name to resolve.
24207
+ * @returns The matching texture, or undefined if not found.
24208
+ */
24209
+ function ResolveTextureReference(scene, name) {
24210
+ const tex = scene.textures.find((t) => t.name === name);
24211
+ if (tex) {
24212
+ return tex;
24213
+ }
24214
+ Logger.Warn(`OverrideManager: Texture reference "${name}" not found.`);
24215
+ return undefined;
24216
+ }
24217
+ /**
24218
+ * Resolves a "samTexture:key" value by looking up a SmartAsset-tracked texture
24219
+ * by its registry key. The SAM key is stable across save/load whereas the
24220
+ * texture's `name` (for SAM textures, the blob URL) changes on every reload,
24221
+ * so this is the only reliable way to round-trip texture references on
24222
+ * user-uploaded SmartAsset textures.
24223
+ * @param scene - The scene to search.
24224
+ * @param key - The SmartAsset key to resolve.
24225
+ * @returns The matching texture, or undefined if not found.
24226
+ */
24227
+ function ResolveSamTextureReference(scene, key) {
24228
+ const tex = scene.textures.find((t) => FindSmartAssetKeyForObject(scene, t) === key);
24229
+ if (tex) {
24230
+ return tex;
24231
+ }
24232
+ Logger.Warn(`OverrideManager: SmartAsset texture "${key}" not found.`);
24233
+ return undefined;
24234
+ }
24235
+ /**
24236
+ * Finds the index of an override matching the given coordinates.
24237
+ * @param internal - The manager's internal state.
24238
+ * @param targetType - The target type.
24239
+ * @param targetName - The target object name.
24240
+ * @param targetIndex - The target index among same-named siblings.
24241
+ * @param propertyPath - The property path.
24242
+ * @returns The matching index, or -1 if none found.
24243
+ */
24244
+ function FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath) {
24245
+ return internal.overrides.findIndex((o) => o.targetType === targetType && o.targetName === targetName && o.targetIndex === targetIndex && o.propertyPath === propertyPath);
24246
+ }
24247
+ /**
24248
+ * Removes any existing override that matches the given coordinates. Used by
24249
+ * {@link AddOverride} to enforce one entry per (type, name, index, property).
24250
+ * @param internal - The manager's internal state.
24251
+ * @param targetType - The target type.
24252
+ * @param targetName - The target object name.
24253
+ * @param targetIndex - The target index among same-named siblings.
24254
+ * @param propertyPath - The property path.
24255
+ */
24256
+ function RemoveMatchingOverride(internal, targetType, targetName, targetIndex, propertyPath) {
24257
+ const idx = FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath);
24258
+ if (idx >= 0) {
24259
+ internal.overrides.splice(idx, 1);
24260
+ }
24261
+ }
24262
+ /**
24263
+ * Creates a unique key for storing original values.
24264
+ * @param targetType - The override target type.
24265
+ * @param targetName - The target object name.
24266
+ * @param targetIndex - The target index among same-named siblings.
24267
+ * @param propertyPath - The property path.
24268
+ * @returns A composite string key uniquely identifying the original value slot.
24269
+ */
24270
+ function MakeOriginalValueKey(targetType, targetName, targetIndex, propertyPath) {
24271
+ return `${targetType}::${targetName}::${targetIndex}::${propertyPath}`;
24272
+ }
24273
+ /**
24274
+ * Gets a nested property from an object using a dot-separated path.
24275
+ * @param obj - The root object to traverse.
24276
+ * @param path - The dot-separated property path.
24277
+ * @returns The value at the path, or undefined if any segment is missing.
24278
+ */
24279
+ function GetNestedProperty(obj, path) {
24280
+ const parts = path.split(".");
24281
+ let current = obj;
24282
+ for (const part of parts) {
24283
+ if (current === null || current === undefined || typeof current !== "object") {
24284
+ return undefined;
24285
+ }
24286
+ current = current[part];
24287
+ }
24288
+ return current;
24289
+ }
24290
+ /**
24291
+ * Sets a nested property on an object using a dot-separated path.
24292
+ *
24293
+ * When the value is a number array and the existing property is a Babylon
24294
+ * math type (Vector*, Quaternion, Color3/4, Matrix), uses the math type's
24295
+ * `fromArray` method to mutate it in place — preserving the live instance
24296
+ * identity that consumers may already hold references to. Otherwise falls
24297
+ * back to direct property replacement.
24298
+ * @param obj - The root object to mutate.
24299
+ * @param path - The dot-separated property path.
24300
+ * @param value - The new value to assign.
24301
+ */
24302
+ function SetNestedProperty(obj, path, value) {
24303
+ const parts = path.split(".");
24304
+ let current = obj;
24305
+ for (let i = 0; i < parts.length - 1; i++) {
24306
+ if (current === null || current === undefined || typeof current !== "object") {
24307
+ return;
24308
+ }
24309
+ current = current[parts[i]];
24310
+ }
24311
+ if (current === null || current === undefined || typeof current !== "object") {
24312
+ return;
24313
+ }
24314
+ const lastPart = parts[parts.length - 1];
24315
+ const existing = current[lastPart];
24316
+ if (Array.isArray(value) && existing && typeof existing === "object" && typeof existing.fromArray === "function") {
24317
+ existing.fromArray(value);
24318
+ return;
24319
+ }
24320
+ current[lastPart] = value;
24321
+ }
24322
+ /**
24323
+ * Snapshots a value for original-value tracking.
24324
+ *
24325
+ * Scene entities (textures, materials, meshes, etc.) are stored by reference
24326
+ * because cloning them would register unwanted duplicates in the scene.
24327
+ * Plain math types (Vector3, Color3, etc.) are cloned so mutations to the
24328
+ * live object don't corrupt the saved original.
24329
+ * @param value - The value to snapshot.
24330
+ * @returns The snapshot value (cloned for plain math types, by reference for entities).
24331
+ */
24332
+ function CloneValue(value) {
24333
+ if (value === null || value === undefined) {
24334
+ return value;
24335
+ }
24336
+ if (typeof value !== "object") {
24337
+ return value;
24338
+ }
24339
+ if (typeof value.getScene === "function") {
24340
+ return value;
24341
+ }
24342
+ if ("clone" in value && typeof value.clone === "function") {
24343
+ return value.clone();
24344
+ }
24345
+ return { ...value };
24346
+ }
24347
+
24348
+ /**
24349
+ * Inspector service that captures property edits made through Inspector and
24350
+ * feeds them to the OverrideManager as persistent overrides.
24351
+ *
24352
+ * Works on any scene object — overrides have no concept of "which asset"
24353
+ * owns an object. When multiple objects share a name, an entity's position
24354
+ * among same-named siblings (`targetIndex`) is captured so the override
24355
+ * re-applies to the same object after reload.
24356
+ *
24357
+ * The service re-attaches to the current scene whenever it changes, so
24358
+ * overrides are captured against the active scene even after loads/swaps.
24359
+ */
24360
+ const OverrideCaptureServiceDefinition = {
24361
+ friendlyName: "Override Capture",
24362
+ consumes: [SceneContextIdentity, PropertiesServiceIdentity],
24363
+ factory: (sceneContext, propertiesService) => {
24364
+ // Track each entity's name + index at first contact so we can update
24365
+ // existing overrides when the user renames the entity in Inspector.
24366
+ // Re-created on each scene attach so identities don't leak across scenes.
24367
+ let previousIdentity = new WeakMap();
24368
+ let changeObserver = null;
24369
+ function attachToScene(scene) {
24370
+ if (changeObserver) {
24371
+ changeObserver.remove();
24372
+ changeObserver = null;
24373
+ }
24374
+ previousIdentity = new WeakMap();
24375
+ if (!scene) {
24376
+ return;
24377
+ }
24378
+ changeObserver = propertiesService.onPropertyChanged.add((changeInfo) => {
24379
+ const { entity, propertyKey, oldValue, newValue } = changeInfo;
24380
+ // When "name" changes, update the matching override so it follows the rename
24381
+ // instead of creating a new (orphaned) one. Also rewrite any overrides
24382
+ // whose *value* referenced this entity by name so cross-references
24383
+ // (e.g. `mesh.material = ref:redMat`) survive the rename too.
24384
+ if (propertyKey === "name" && typeof newValue === "string") {
24385
+ const targetType = ClassifyEntity(entity, scene);
24386
+ if (targetType !== null && targetType !== "scene") {
24387
+ const previous = previousIdentity.get(entity);
24388
+ if (previous && previous.name !== newValue) {
24389
+ // The entity already has the new name at this point, so compute its new index among same-named siblings.
24390
+ const newIndex = ComputeTargetIndex(scene, targetType, entity, newValue);
24391
+ RenameOverrideTarget(scene, targetType, previous.name, previous.index, newValue, newIndex);
24392
+ // Mirror the rename into override values that reference
24393
+ // this entity by name. SmartAsset textures use the
24394
+ // `samTexture:<key>` form and stay decoupled from the
24395
+ // texture's runtime name, so they don't need rewriting.
24396
+ const valueScheme = targetType === "textures" ? "texture" : "ref";
24397
+ RenameOverrideValueReferences(scene, valueScheme, previous.name, newValue);
24398
+ }
24399
+ previousIdentity.set(entity, { name: newValue, index: ComputeTargetIndex(scene, targetType, entity, newValue) });
24400
+ }
24401
+ return;
24402
+ }
24403
+ if (propertyKey === "id") {
24404
+ return;
24405
+ }
24406
+ let targetType = ClassifyEntity(entity, scene);
24407
+ let targetName;
24408
+ let targetIndex;
24409
+ let propertyPath = String(propertyKey);
24410
+ let targetEntity;
24411
+ if (targetType !== null) {
24412
+ if (targetType === "scene") {
24413
+ targetName = "";
24414
+ targetIndex = 0;
24415
+ targetEntity = scene;
24416
+ }
24417
+ else {
24418
+ targetName = GetEntityName(entity);
24419
+ targetIndex = ComputeTargetIndex(scene, targetType, entity, targetName);
24420
+ targetEntity = entity;
24421
+ }
24422
+ // Seed identity on first contact so rename tracking works
24423
+ if (!previousIdentity.has(targetEntity) && targetName) {
24424
+ previousIdentity.set(targetEntity, { name: targetName, index: targetIndex });
24425
+ }
24426
+ }
24427
+ else {
24428
+ // Sub-object: check if this is a property of a known parent
24429
+ const parentInfo = FindParentEntity(entity, scene);
24430
+ if (!parentInfo) {
24431
+ return;
24432
+ }
24433
+ targetType = parentInfo.targetType;
24434
+ targetName = parentInfo.targetName;
24435
+ targetIndex = parentInfo.targetIndex;
24436
+ propertyPath = `${parentInfo.parentProperty}.${propertyPath}`;
24437
+ }
24438
+ const serializedValue = SerializeOverrideValueForCapture(newValue, scene);
24439
+ if (serializedValue === undefined) {
24440
+ return;
24441
+ }
24442
+ // The Inspector binding has already written `newValue` to the
24443
+ // entity, so pass `oldValue` so the manager can record the
24444
+ // true pre-edit value (without this, RemoveOverride would have
24445
+ // no record of the original and could not restore it).
24446
+ AddOverride(scene, {
24447
+ targetType,
24448
+ targetName,
24449
+ targetIndex,
24450
+ propertyPath,
24451
+ value: serializedValue,
24452
+ }, { originalValue: oldValue });
24453
+ });
24454
+ }
24455
+ attachToScene(sceneContext.currentScene);
24456
+ const sceneSubObserver = sceneContext.currentSceneObservable.add((scene) => attachToScene(scene));
24457
+ return {
24458
+ dispose: () => {
24459
+ sceneSubObserver.remove();
24460
+ if (changeObserver) {
24461
+ changeObserver.remove();
24462
+ changeObserver = null;
24463
+ }
24464
+ },
24465
+ };
24466
+ },
24467
+ };
24468
+ /**
24469
+ * Classifies an entity into an OverrideTargetType by membership in the
24470
+ * scene's standard collections (or by being the scene itself).
24471
+ * @param entity - The entity to classify.
24472
+ * @param scene - The scene to check collections against.
24473
+ * @returns The target type, or null if unrecognized.
24474
+ */
24475
+ function ClassifyEntity(entity, scene) {
24476
+ if (entity === scene) {
24477
+ return "scene";
24478
+ }
24479
+ const obj = entity;
24480
+ if (!obj || typeof obj !== "object") {
24481
+ return null;
24482
+ }
24483
+ if (scene.materials.includes(obj)) {
24484
+ return "materials";
24485
+ }
24486
+ if (scene.meshes.includes(obj)) {
24487
+ return "meshes";
24488
+ }
24489
+ if (scene.lights.includes(obj)) {
24490
+ return "lights";
24491
+ }
24492
+ if (scene.cameras.includes(obj)) {
24493
+ return "cameras";
24494
+ }
24495
+ if (scene.textures.includes(obj)) {
24496
+ return "textures";
24497
+ }
24498
+ if (scene.animationGroups.includes(obj)) {
24499
+ return "animationGroups";
24500
+ }
24501
+ return null;
24502
+ }
24503
+ /**
24504
+ * Gets the name of a scene entity.
24505
+ * @param entity - The entity to get the name from.
24506
+ * @returns The entity name, or an empty string if unavailable.
24507
+ */
24508
+ function GetEntityName(entity) {
24509
+ const obj = entity;
24510
+ return obj?.name ?? "";
24511
+ }
24512
+ /**
24513
+ * Returns the position of `entity` among scene[targetType] objects with the
24514
+ * same name. Used so overrides can disambiguate same-named siblings.
24515
+ * @param scene - The scene to inspect.
24516
+ * @param targetType - The target type / collection name.
24517
+ * @param entity - The entity to locate.
24518
+ * @param name - The name to filter by.
24519
+ * @returns The index within the same-name filter, or 0 if not found.
24520
+ */
24521
+ function ComputeTargetIndex(scene, targetType, entity, name) {
24522
+ const collection = GetCollection(scene, targetType);
24523
+ if (!collection) {
24524
+ return 0;
24525
+ }
24526
+ const sameName = collection.filter((obj) => obj.name === name);
24527
+ const idx = sameName.indexOf(entity);
24528
+ return idx >= 0 ? idx : 0;
24529
+ }
24530
+ /**
24531
+ * Returns the scene collection matching a target type.
24532
+ * @param scene - The scene to inspect.
24533
+ * @param targetType - The target type.
24534
+ * @returns The collection, or null if `targetType` doesn't map to one.
24535
+ */
24536
+ function GetCollection(scene, targetType) {
24537
+ switch (targetType) {
24538
+ case "meshes":
24539
+ return scene.meshes;
24540
+ case "materials":
24541
+ return scene.materials;
24542
+ case "textures":
24543
+ return scene.textures;
24544
+ case "lights":
24545
+ return scene.lights;
24546
+ case "cameras":
24547
+ return scene.cameras;
24548
+ case "animationGroups":
24549
+ return scene.animationGroups;
24550
+ default:
24551
+ return null;
24552
+ }
24553
+ }
24554
+ /**
24555
+ * Serializes a property value into an OverrideValue.
24556
+ * Returns undefined for unsupported types.
24557
+ * @param value - The value to serialize.
24558
+ * @param scene - Optional scene for resolving object references.
24559
+ * @returns The serialized value, or undefined if unsupported.
24560
+ */
24561
+ function SerializeOverrideValueForCapture(value, scene) {
24562
+ // null is a legitimate override value (e.g. clearing a material slot) and
24563
+ // must round-trip as null — substituting "" here would silently corrupt
24564
+ // object-typed slots with an empty string on reload.
24565
+ if (value === null) {
24566
+ return null;
24567
+ }
24568
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
24569
+ return value;
24570
+ }
24571
+ // Material reference → "ref:materialName"
24572
+ if (value && typeof value === "object" && "getClassName" in value && typeof value.getClassName === "function") {
24573
+ const className = value.getClassName();
24574
+ if (className.includes("Material") || className.includes("material")) {
24575
+ return `ref:${value.name}`;
24576
+ }
24577
+ }
24578
+ // Texture reference → "samTexture:<key>" if SmartAsset-tracked, else "texture:<name>".
24579
+ // The SAM key is stable across save/load; `texture.name` for a SAM-tracked
24580
+ // texture is the blob URL, which dies on page reload — using it as the
24581
+ // override identifier would break the override after every reload.
24582
+ if (value && typeof value === "object" && "getClassName" in value && scene) {
24583
+ const className = value.getClassName();
24584
+ if (className.includes("Texture") || className.includes("texture")) {
24585
+ const samKey = FindSmartAssetKeyForObject(scene, value);
24586
+ if (samKey !== undefined) {
24587
+ return `samTexture:${samKey}`;
24588
+ }
24589
+ return `texture:${value.name}`;
24590
+ }
24591
+ }
24592
+ // Color3 / Color4
24593
+ if (value && typeof value === "object" && "r" in value && "g" in value && "b" in value) {
24594
+ const color = value;
24595
+ if ("a" in color && color.a !== undefined) {
24596
+ return [color.r, color.g, color.b, color.a];
24597
+ }
24598
+ return [color.r, color.g, color.b];
24599
+ }
24600
+ // Vector3 / Vector4
24601
+ if (value && typeof value === "object" && "x" in value && "y" in value && "z" in value) {
24602
+ const vec = value;
24603
+ if ("w" in vec && vec.w !== undefined) {
24604
+ return [vec.x, vec.y, vec.z, vec.w];
24605
+ }
24606
+ return [vec.x, vec.y, vec.z];
24607
+ }
24608
+ // Vector2
24609
+ if (value && typeof value === "object" && "x" in value && "y" in value && !("z" in value)) {
24610
+ const vec2 = value;
24611
+ return [vec2.x, vec2.y];
24612
+ }
24613
+ return undefined;
24614
+ }
24615
+ /**
24616
+ * Checks if an entity is a sub-object of a known scene entity by scanning
24617
+ * well-known sub-object properties on the scene and its collections.
24618
+ * Returns the parent entity info with the property path prefix.
24619
+ * @param entity - The entity to search for.
24620
+ * @param scene - The scene to search in.
24621
+ * @returns The parent entity info, or null if not found.
24622
+ */
24623
+ function FindParentEntity(entity, scene) {
24624
+ // Check scene sub-objects (imageProcessingConfiguration, fogSettings, etc.)
24625
+ const sceneSubProps = ["imageProcessingConfiguration", "postProcessRenderPipelineManager", "ambientColor", "gravity"];
24626
+ for (const prop of sceneSubProps) {
24627
+ if (scene[prop] === entity) {
24628
+ return { targetType: "scene", targetName: "", targetIndex: 0, parentProperty: prop };
24629
+ }
24630
+ }
24631
+ const collections = [
24632
+ { type: "materials", items: scene.materials },
24633
+ { type: "cameras", items: scene.cameras },
24634
+ { type: "meshes", items: scene.meshes },
24635
+ { type: "lights", items: scene.lights },
24636
+ ];
24637
+ for (const { type, items } of collections) {
24638
+ for (const parent of items) {
24639
+ for (const prop of Object.keys(parent)) {
24640
+ if (prop.startsWith("_")) {
24641
+ continue;
24642
+ }
24643
+ try {
24644
+ if (parent[prop] === entity) {
24645
+ const targetIndex = items.filter((p) => p.name === parent.name).indexOf(parent);
24646
+ return { targetType: type, targetName: parent.name, targetIndex: Math.max(targetIndex, 0), parentProperty: prop };
24647
+ }
24648
+ }
24649
+ catch {
24650
+ // Skip properties that throw on access
24651
+ }
24652
+ }
24653
+ }
24654
+ }
24655
+ return null;
24656
+ }
24657
+
24658
+ // Side-effect import: registers the `.babylon` SceneLoader plugin so the
24659
+ // companion `.babylon` file produced by SerializeProject can be loaded back.
24660
+ // Without this, LoadAssetContainerAsync logs "Unable to find a plugin to
24661
+ // load .babylon files" and the companion load fails.
24662
+ /**
24663
+ * ## `.babylonproj` project file format
24664
+ *
24665
+ * The `.babylonproj` zip on disk packages three layers:
24666
+ * 1. **SmartAsset registry** — URL references to external assets (glb/gltf/textures
24667
+ * loaded via SAM). Local blob/data assets are bundled inside the zip and
24668
+ * extracted to fresh blob URLs on load.
24669
+ * 2. **OverrideManager state** — declarative property overrides applied after load.
24670
+ * 3. **Companion `.babylon`** — meshes, lights, cameras, transform nodes, and
24671
+ * materials that are *not* tracked by SAM (i.e. user-created scene content).
24672
+ * Plus a `companionBindings` side table mapping material texture slots back
24673
+ * to SAM-tracked textures so re-attachment works without embedding texture
24674
+ * bytes in the companion.
24675
+ *
24676
+ * ### What round-trips cleanly
24677
+ * - SAM-tracked assets (re-fetched from their URLs or extracted from the zip)
24678
+ * - User-created `Mesh` geometry, `Material`s (Standard/PBR/Multi/Node), and
24679
+ * `*Texture` slot bindings to SAM textures
24680
+ * - `Light`s, `Camera`s, `TransformNode`s, scene/material image processing,
24681
+ * clear color, fog, environment intensity
24682
+ * - Property overrides on any of the above
24683
+ *
24684
+ * ### Known gaps (not preserved on save/load)
24685
+ * - `PostProcess` attachments to cameras (a post-process attaches to a *specific*
24686
+ * camera instance; we dispose+recreate cameras, leaving post-processes orphaned).
24687
+ * - `AdvancedDynamicTexture` GUI controls — not in `.babylon` format.
24688
+ * - Audio (`Sound` / `AudioEngine` state).
24689
+ * - Particle systems with runtime state, baked vertex animations.
24690
+ * - Complex shader-driven content like GaussianSplatting: the mesh round-trips
24691
+ * but its companion utility materials (`gaussianSplattingDepth`, `ProxyMaterial`)
24692
+ * get duplicated on each load cycle.
24693
+ * - Skeleton animation playback state.
24694
+ *
24695
+ * If you hit a "the scene looks different after load" issue, it's almost
24696
+ * certainly one of the gaps above rather than camera or mesh state drift.
24697
+ */
24698
+ /**
24699
+ * Reserved smart asset key for user-created objects (materials, lights, cameras)
24700
+ * that are persisted as a companion `.babylon` file alongside the project JSON.
24701
+ */
24702
+ const ProjectLocalsKey = "__project_locals__";
24703
+ // ── JSON layer (scene ↔ ISerializedProject) ──
24704
+ /**
24705
+ * Serializes a scene's smart asset map and override registry into a project
24706
+ * bundle. User-created objects (materials, lights, cameras not owned by any
24707
+ * smart asset) are serialized into a companion `.babylon` file rather than
24708
+ * embedded in the project JSON.
24709
+ *
24710
+ * Both managers are looked up (and created if missing) via their respective
24711
+ * `Get…Manager(scene)` accessors, so this function can be called on any scene.
24712
+ *
24713
+ * @param scene - The scene to serialize.
24714
+ * @param baseUrl - Optional base URL for making asset paths relative.
24715
+ * @returns A project bundle containing the JSON document and optional companion file.
24716
+ */
24717
+ function SerializeProject(scene, baseUrl) {
24718
+ const assetMap = SerializeSmartAssetManagerMap(scene, baseUrl);
24719
+ const overrides = SerializeOverrides(scene);
24720
+ // Build a minimal .babylon JSON with only user-created objects, plus the
24721
+ // texture-binding side table that records which SmartAsset textures should
24722
+ // be re-attached to which material slots after load.
24723
+ const companionResult = SerializeCompanionBabylon(scene);
24724
+ let companionBabylon;
24725
+ const assets = { ...assetMap.assets };
24726
+ if (companionResult) {
24727
+ companionBabylon = new Blob([JSON.stringify(companionResult.companion)], { type: "application/json" });
24728
+ assets[ProjectLocalsKey] = { url: ProjectLocalsKey + ".babylon" };
24729
+ }
24730
+ else {
24731
+ // Remove stale companion entry if no locals exist
24732
+ delete assets[ProjectLocalsKey];
24733
+ }
24734
+ const hasBindings = companionResult && Object.keys(companionResult.bindings).length > 0;
24735
+ const project = {
24736
+ version: 2,
24737
+ assets,
24738
+ overrides,
24739
+ ...(hasBindings ? { companionBindings: companionResult.bindings } : {}),
24740
+ };
24741
+ return { project, companionBabylon };
24742
+ }
24743
+ /**
24744
+ * Loads a project file from a URL, File, or pre-parsed object.
24745
+ * Registers all asset entries on the scene's SmartAssetManager, loads all
24746
+ * assets (including the companion `.babylon` for user-created objects), then
24747
+ * applies all overrides via the OverrideManager.
24748
+ *
24749
+ * For loading the `.babylonproj` zip on-disk format, use {@link LoadProjectFileAsync}
24750
+ * instead — it extracts the zip and then calls this function with the embedded
24751
+ * JSON document.
24752
+ *
24753
+ * @param scene - The scene to populate.
24754
+ * @param source - A URL string, File object, or pre-parsed ISerializedProject.
24755
+ * @param rootUrl - Optional root URL for resolving relative asset paths.
24756
+ */
24757
+ async function LoadProjectAsync(scene, source, rootUrl) {
24758
+ let resolvedRootUrl = "";
24759
+ if (typeof source === "string" && true) {
24760
+ const { Tools } = await import('@babylonjs/core/Misc/tools.js');
24761
+ resolvedRootUrl = Tools.GetFolderPath(source);
24762
+ }
24763
+ const raw = await ReadJsonSourceAsync(source);
24764
+ const doc = DeserializeProject(raw);
24765
+ // Pause the engine's render loops for the duration of the swap. Disposing
24766
+ // cameras mid-frame would throw "No camera defined" out of `scene.render`,
24767
+ // which kills the render loop entirely (it is not re-queued after an
24768
+ // uncaught exception). Snapshot the active loops first so we can restore
24769
+ // exactly what was running, even if multiple callbacks were registered.
24770
+ const engine = scene.getEngine();
24771
+ const savedRenderLoops = [...engine.activeRenderLoops];
24772
+ engine.stopRenderLoop();
24773
+ try {
24774
+ // Clear existing state so we load fresh from the project file.
24775
+ // The companion `.babylon` (when present) is the source of truth for all
24776
+ // user-created scene content, so dispose user-owned meshes, lights,
24777
+ // cameras, materials, and animation groups before reloading.
24778
+ await Promise.all(Array.from(GetAllSmartAssets(scene).keys()).map(async (existingKey) => await RemoveSmartAssetAsync(scene, existingKey)));
24779
+ ClearOverrides(scene);
24780
+ for (const mesh of [...scene.meshes]) {
24781
+ mesh.dispose();
24782
+ }
24783
+ for (const tn of [...scene.transformNodes]) {
24784
+ tn.dispose();
24785
+ }
24786
+ for (const ag of [...scene.animationGroups]) {
24787
+ ag.dispose();
24788
+ }
24789
+ for (const mat of [...scene.materials]) {
24790
+ mat.dispose();
24791
+ }
24792
+ for (const light of [...scene.lights]) {
24793
+ light.dispose();
24794
+ }
24795
+ for (const camera of [...scene.cameras]) {
24796
+ camera.dispose();
24797
+ }
24798
+ // Register all assets. Defer the companion .babylon — it must load after
24799
+ // textures are available so binding re-attachment can find them.
24800
+ let hasCompanion = false;
24801
+ for (const [key, entry] of Object.entries(doc.assets)) {
24802
+ if (key === ProjectLocalsKey) {
24803
+ hasCompanion = true;
24804
+ continue;
24805
+ }
24806
+ const resolved = resolvedRootUrl ? ResolveAssetUrl(entry.url, resolvedRootUrl) : entry.url;
24807
+ RegisterSmartAsset(scene, key, resolved, { type: entry.type, extension: entry.extension, metadata: entry.metadata });
24808
+ }
24809
+ await LoadAllSmartAssetsAsync(scene);
24810
+ // Now load the companion .babylon. Its materials were saved with texture
24811
+ // slots stripped (the binding side table records which SmartAsset texture
24812
+ // each slot should be re-attached to), so the loader never sees a broken
24813
+ // texture URL. Pass the .babylon extension hint because blob URLs have
24814
+ // no file extension.
24815
+ if (hasCompanion) {
24816
+ const companionEntry = doc.assets[ProjectLocalsKey];
24817
+ const companionUrl = resolvedRootUrl ? ResolveAssetUrl(companionEntry.url, resolvedRootUrl) : companionEntry.url;
24818
+ await LoadSmartAssetAsync(scene, ProjectLocalsKey, companionUrl, { extension: ".babylon" });
24819
+ if (doc.companionBindings) {
24820
+ ApplyCompanionBindings(scene, doc.companionBindings);
24821
+ }
24822
+ }
24823
+ // Apply overrides
24824
+ if (doc.overrides.length > 0) {
24825
+ DeserializeAndApplyOverrides(scene, doc.overrides);
24826
+ }
24827
+ // Re-assign the active camera if the companion brought in fresh cameras.
24828
+ // The .babylon scene loader populates scene.cameras but does not set
24829
+ // scene.activeCamera, so render would otherwise throw "No camera defined".
24830
+ if (!scene.activeCamera && scene.cameras.length > 0) {
24831
+ scene.activeCamera = scene.cameras[0];
24832
+ }
24833
+ // Attach controls so the user can rotate/zoom/pan after load. New
24834
+ // camera instances from the companion .babylon are not attached to
24835
+ // the canvas by the loader — without this, the camera renders but
24836
+ // ignores mouse/touch input.
24837
+ const canvas = engine.getRenderingCanvas();
24838
+ if (scene.activeCamera && canvas) {
24839
+ scene.activeCamera.attachControl(canvas, true);
24840
+ }
24841
+ }
24842
+ finally {
24843
+ // Always restore the render loops, even if loading threw — otherwise
24844
+ // the canvas stays frozen forever and the user has no way to recover.
24845
+ for (const loop of savedRenderLoops) {
24846
+ engine.runRenderLoop(loop);
24847
+ }
24848
+ }
24849
+ }
24850
+ /**
24851
+ * Validates and parses a serialized project document.
24852
+ * @param data - The raw data to validate (typically parsed JSON).
24853
+ * @returns The validated project document.
24854
+ * @throws If the data does not conform to the expected schema.
24855
+ */
24856
+ function DeserializeProject(data) {
24857
+ if (!data || typeof data !== "object") {
24858
+ throw new Error("ProjectFile: Invalid project file — expected an object.");
24859
+ }
24860
+ const doc = data;
24861
+ if (doc.version !== 2) {
24862
+ throw new Error(`ProjectFile: Unsupported project version "${doc.version}". Expected version 2.`);
24863
+ }
24864
+ // Validate the asset map portion
24865
+ DeserializeSmartAssetMap({ version: 1, assets: doc.assets });
24866
+ // Validate overrides array
24867
+ if (!Array.isArray(doc.overrides)) {
24868
+ throw new Error("ProjectFile: Invalid project file — 'overrides' must be an array.");
24869
+ }
24870
+ // Validate optional companion bindings (shape-only check)
24871
+ if (doc.companionBindings !== undefined) {
24872
+ if (typeof doc.companionBindings !== "object" || doc.companionBindings === null || Array.isArray(doc.companionBindings)) {
24873
+ throw new Error("ProjectFile: Invalid project file — 'companionBindings' must be an object.");
24874
+ }
24875
+ }
24876
+ return data;
24877
+ }
24878
+ // ── Zip layer (.babylonproj on disk) ──
24879
+ /**
24880
+ * Serializes a scene's project (smart assets + overrides) into a `.babylonproj`
24881
+ * zip bundle.
24882
+ *
24883
+ * The zip contains:
24884
+ * - `project.json` — the project document (assets + overrides)
24885
+ * - `__project_locals__.babylon` — companion file for user-created objects (if any)
24886
+ * - Bundled local asset files (blobs the user dragged in from disk)
24887
+ *
24888
+ * Remote URLs (http/https) are left as references and not bundled.
24889
+ *
24890
+ * @param scene - The scene to serialize.
24891
+ * @returns A Blob containing the zip bundle.
24892
+ */
24893
+ async function SaveProjectFileAsync(scene) {
24894
+ const bundle = SerializeProject(scene);
24895
+ const files = {};
24896
+ // Collect local (blob/data URI) assets to bundle inside the zip.
24897
+ // Rewrite their URLs in the project JSON to relative paths.
24898
+ const projectAssets = { ...bundle.project.assets };
24899
+ // Fetch all blob/data URIs in parallel (avoid serial awaits in a loop).
24900
+ const blobEntries = Object.entries(projectAssets).filter(([key, entry]) => key !== ProjectLocalsKey && (entry.url.startsWith("blob:") || entry.url.startsWith("data:")));
24901
+ const fetched = await Promise.all(blobEntries.map(async ([key, entry]) => {
24902
+ try {
24903
+ const response = await fetch(entry.url);
24904
+ const arrayBuffer = await response.arrayBuffer();
24905
+ return { key, entry, arrayBuffer };
24906
+ }
24907
+ catch {
24908
+ // Can't fetch blob — leave the URL as-is (will break on reload,
24909
+ // but at least the project structure is preserved).
24910
+ return null;
24911
+ }
24912
+ }));
24913
+ for (const result of fetched) {
24914
+ if (!result) {
24915
+ continue;
24916
+ }
24917
+ const { key, entry, arrayBuffer } = result;
24918
+ const ext = GuessExtension(entry.url, IsTextureEntry(entry.url, entry.extension, entry.type));
24919
+ const filename = `assets/${key}${ext}`;
24920
+ files[filename] = new Uint8Array(arrayBuffer);
24921
+ projectAssets[key] = { ...entry, url: filename };
24922
+ }
24923
+ // Add companion .babylon if it exists
24924
+ if (bundle.companionBabylon) {
24925
+ const companionBuffer = await bundle.companionBabylon.arrayBuffer();
24926
+ const companionFilename = ProjectLocalsKey + ".babylon";
24927
+ files[companionFilename] = new Uint8Array(companionBuffer);
24928
+ projectAssets[ProjectLocalsKey] = { url: companionFilename };
24929
+ }
24930
+ // Write the project JSON with updated asset paths
24931
+ const projectWithBundledPaths = {
24932
+ ...bundle.project,
24933
+ assets: projectAssets,
24934
+ };
24935
+ const { zip, strToU8 } = await import('./browser-CANgtOiM.js');
24936
+ files["project.json"] = strToU8(JSON.stringify(projectWithBundledPaths, null, 2));
24937
+ // Create the zip (async — runs in a Web Worker to avoid blocking the UI thread)
24938
+ const zipped = await new Promise((resolve, reject) => {
24939
+ zip(files, { level: 6 }, (err, data) => (err ? reject(err) : resolve(data)));
24940
+ });
24941
+ return new Blob([zipped], { type: "application/zip" });
24942
+ }
24943
+ /**
24944
+ * Loads a `.babylonproj` zip bundle into a scene. Extracts all files, creates
24945
+ * blob URLs for bundled assets, and loads the project through SAM.
24946
+ *
24947
+ * @param scene - The scene to load the project into.
24948
+ * @param zipFile - The `.babylonproj` zip file to load.
24949
+ */
24950
+ async function LoadProjectFileAsync(scene, zipFile) {
24951
+ const arrayBuffer = await zipFile.arrayBuffer();
24952
+ const { unzip, strFromU8 } = await import('./browser-CANgtOiM.js');
24953
+ const extracted = await new Promise((resolve, reject) => {
24954
+ unzip(new Uint8Array(arrayBuffer), (err, data) => (err ? reject(err) : resolve(data)));
24955
+ });
24956
+ // Parse project.json
24957
+ const projectJsonBytes = extracted["project.json"];
24958
+ if (!projectJsonBytes) {
24959
+ throw new Error("ProjectFile: Invalid project bundle — missing project.json");
24960
+ }
24961
+ const projectJson = JSON.parse(strFromU8(projectJsonBytes));
24962
+ // Create blob URLs for all bundled files and rewrite asset URLs
24963
+ for (const [, entry] of Object.entries(projectJson.assets)) {
24964
+ const filename = entry.url;
24965
+ const fileBytes = extracted[filename];
24966
+ if (fileBytes) {
24967
+ const mimeType = GuessMimeType(filename);
24968
+ // Use a named File so LoadAssetContainerAsync can detect the
24969
+ // format from the filename (blob URLs alone have no extension).
24970
+ const file = new File([fileBytes], filename, { type: mimeType });
24971
+ const blobUrl = URL.createObjectURL(file);
24972
+ entry.url = blobUrl;
24973
+ }
24974
+ // If no file found in zip, assume the URL is a remote reference — leave it as-is
24975
+ }
24976
+ // Load through the standard JSON path
24977
+ await LoadProjectAsync(scene, projectJson);
24978
+ // Note: textures and scene files may still reference the blob URLs created
24979
+ // above, so we do NOT revoke them here. They'll be cleaned up when SAM disposes.
24980
+ }
24981
+ // ── Private ──
24982
+ /**
24983
+ * Returns true if a scene object is a "local" — not owned by any external
24984
+ * smart asset, or owned by the reserved `__project_locals__` key.
24985
+ * @param scene - The scene that owns the object.
24986
+ * @param obj - The scene object to check.
24987
+ * @returns True if the object should be included in the companion file.
24988
+ */
24989
+ function IsLocalObject(scene, obj) {
24990
+ const key = FindSmartAssetKeyForObject(scene, obj);
24991
+ return key === undefined || key === ProjectLocalsKey;
24992
+ }
24993
+ /**
24994
+ * Builds a `.babylon`-compatible JSON containing all user-created scene
24995
+ * content (meshes, lights, cameras, transform nodes, and materials not owned
24996
+ * by any external smart asset), plus a side table recording which `*Texture`
24997
+ * slots on each material should be re-attached to which SmartAsset textures
24998
+ * after load.
24999
+ *
25000
+ * Mesh, light, camera, and standalone material serialization is delegated to
25001
+ * `SceneSerializer.SerializeMesh`, which auto-handles geometries, sub-materials,
25002
+ * and skeletons. Texture slots that map to a SmartAsset-tracked texture are
25003
+ * stripped from the serialized material so the `.babylon` loader never sees a
25004
+ * broken URL; the binding table is the sole source of truth for re-attachment.
25005
+ *
25006
+ * @param scene - The scene to extract locals from.
25007
+ * @returns The companion document and binding table, or null if there are no local objects.
25008
+ */
25009
+ function SerializeCompanionBabylon(scene) {
25010
+ const meshes = scene.meshes.filter((m) => m instanceof Mesh && m.name !== "__root__" && IsLocalObject(scene, m));
25011
+ const lights = scene.lights.filter((l) => IsLocalObject(scene, l));
25012
+ const cameras = scene.cameras.filter((c) => IsLocalObject(scene, c));
25013
+ const transformNodes = scene.transformNodes.filter((t) => IsLocalObject(scene, t));
25014
+ // Standalone materials (not attached to any included mesh) need to be
25015
+ // added explicitly — SerializeMesh only picks up materials reachable from
25016
+ // the supplied meshes.
25017
+ const meshMaterialIds = new Set(meshes.map((m) => m.material?.uniqueId).filter((id) => id !== undefined));
25018
+ const standaloneMaterials = scene.materials.filter((mat) => mat.name !== "default material" && IsLocalObject(scene, mat) && !meshMaterialIds.has(mat.uniqueId));
25019
+ if (meshes.length === 0 && lights.length === 0 && cameras.length === 0 && transformNodes.length === 0 && standaloneMaterials.length === 0) {
25020
+ return null;
25021
+ }
25022
+ const companion = SceneSerializer.SerializeMesh([...meshes, ...lights, ...cameras, ...transformNodes], false, false);
25023
+ const allMaterials = companion.materials ?? [];
25024
+ companion.materials = allMaterials;
25025
+ for (const mat of standaloneMaterials) {
25026
+ const serialized = mat.serialize();
25027
+ if (serialized && !allMaterials.some((m) => m.uniqueId === serialized.uniqueId)) {
25028
+ allMaterials.push(serialized);
25029
+ }
25030
+ }
25031
+ // Strip non-JSON-serializable metadata (e.g. metadata that references
25032
+ // another scene object) to avoid `Converting circular structure to JSON`
25033
+ // when the companion is stringified. Simple JSON metadata is preserved.
25034
+ SanitizeMetadataInPlace(companion);
25035
+ // Walk every serialized material (mesh-attached + standalone + multi-material
25036
+ // children) and extract SmartAsset texture bindings, stripping those slots
25037
+ // from the serialized data.
25038
+ const bindings = {};
25039
+ for (const serializedMat of allMaterials) {
25040
+ const matBindings = ExtractTextureBindings(scene, serializedMat);
25041
+ if (Object.keys(matBindings).length > 0 && typeof serializedMat.name === "string") {
25042
+ bindings[serializedMat.name] = matBindings;
25043
+ }
25044
+ }
25045
+ const multiMaterials = companion.multiMaterials ?? [];
25046
+ for (const serializedMat of multiMaterials) {
25047
+ const matBindings = ExtractTextureBindings(scene, serializedMat);
25048
+ if (Object.keys(matBindings).length > 0 && typeof serializedMat.name === "string") {
25049
+ bindings[serializedMat.name] = matBindings;
25050
+ }
25051
+ }
25052
+ return { companion, bindings };
25053
+ }
25054
+ /**
25055
+ * Walks the top-level entity arrays in a serialized companion document and
25056
+ * strips `metadata` fields that cannot be JSON-stringified (typically because
25057
+ * the user put a reference to another scene object in metadata). Simple
25058
+ * JSON-serializable metadata is preserved.
25059
+ * @param companion - The serialized companion document to mutate in-place.
25060
+ */
25061
+ function SanitizeMetadataInPlace(companion) {
25062
+ const arrays = ["meshes", "transformNodes", "lights", "cameras", "materials", "multiMaterials"];
25063
+ for (const arrayKey of arrays) {
25064
+ const arr = companion[arrayKey];
25065
+ if (!Array.isArray(arr)) {
25066
+ continue;
25067
+ }
25068
+ for (const item of arr) {
25069
+ if (item && typeof item === "object" && "metadata" in item && item.metadata !== undefined) {
25070
+ try {
25071
+ item.metadata = JSON.parse(JSON.stringify(item.metadata));
25072
+ }
25073
+ catch {
25074
+ delete item.metadata;
25075
+ }
25076
+ }
25077
+ }
25078
+ }
25079
+ }
25080
+ /**
25081
+ * Walks a serialized material's `*Texture` slots and, for any that reference a
25082
+ * SmartAsset-tracked texture, records a `{slot: samKey}` binding and removes
25083
+ * the slot from the serialized data so the `.babylon` loader does not try to
25084
+ * fetch the (now-dead) original URL.
25085
+ * @param scene - The scene that owns the textures.
25086
+ * @param serializedMaterial - The serialized material data to rewrite in-place.
25087
+ * @returns A map of stripped slot names to their SmartAsset keys.
25088
+ */
25089
+ function ExtractTextureBindings(scene, serializedMaterial) {
25090
+ const bindings = {};
25091
+ for (const [propName, propValue] of Object.entries(serializedMaterial)) {
25092
+ if (!propName.endsWith("Texture") || typeof propValue !== "object" || propValue === null) {
25093
+ continue;
25094
+ }
25095
+ const texData = propValue;
25096
+ const texName = typeof texData.name === "string" ? texData.name : undefined;
25097
+ const texUrl = typeof texData.url === "string" ? texData.url : undefined;
25098
+ if (!texName && !texUrl) {
25099
+ continue;
25100
+ }
25101
+ for (const tex of scene.textures) {
25102
+ const key = FindSmartAssetKeyForObject(scene, tex);
25103
+ if (key && (tex.name === texName || tex.url === texName || tex.name === texUrl || tex.url === texUrl)) {
25104
+ bindings[propName] = key;
25105
+ delete serializedMaterial[propName];
25106
+ break;
25107
+ }
25108
+ }
25109
+ }
25110
+ return bindings;
25111
+ }
25112
+ /**
25113
+ * Re-attaches SmartAsset-tracked textures to user-created material slots
25114
+ * after the companion `.babylon` has loaded. Silently skips bindings whose
25115
+ * material or texture is no longer present (e.g. the underlying SmartAsset
25116
+ * was removed before reload).
25117
+ * @param scene - The scene that owns the materials and textures.
25118
+ * @param bindings - The binding table from the project document.
25119
+ */
25120
+ function ApplyCompanionBindings(scene, bindings) {
25121
+ for (const [materialName, slots] of Object.entries(bindings)) {
25122
+ const mat = scene.materials.find((m) => m.name === materialName);
25123
+ if (!mat) {
25124
+ continue;
25125
+ }
25126
+ for (const [slotName, samKey] of Object.entries(slots)) {
25127
+ const texture = scene.textures.find((tex) => FindSmartAssetKeyForObject(scene, tex) === samKey);
25128
+ if (texture) {
25129
+ mat[slotName] = texture;
25130
+ }
25131
+ }
25132
+ }
25133
+ }
25134
+ /**
25135
+ * Returns true if a serialized asset entry refers to a standalone texture,
25136
+ * based on the registered options or the URL extension.
25137
+ * @param url - The asset URL.
25138
+ * @param extension - Optional explicit extension hint from the registration options.
25139
+ * @param type - Optional explicit type from the registration options.
25140
+ * @returns True if the entry should be treated as a texture.
25141
+ */
25142
+ function IsTextureEntry(url, extension, type) {
25143
+ if (type === "texture") {
25144
+ return true;
25145
+ }
25146
+ const textureExts = GetSmartAssetTextureExtensions();
25147
+ if (extension && textureExts.has(extension.toLowerCase())) {
25148
+ return true;
25149
+ }
25150
+ const ext = ExtractExtension(url);
25151
+ return ext !== "" && textureExts.has(ext);
25152
+ }
25153
+ /**
25154
+ * Extracts the file extension (with leading dot, lowercased) from a URL,
25155
+ * stripping query/hash and ignoring blob/data prefixes.
25156
+ * @param url - The URL to inspect.
25157
+ * @returns The extension including the leading dot, or "" if none found.
25158
+ */
25159
+ function ExtractExtension(url) {
25160
+ if (url.startsWith("blob:") || url.startsWith("data:")) {
25161
+ return "";
25162
+ }
25163
+ const clean = url.split("?")[0].split("#")[0];
25164
+ const lastDot = clean.lastIndexOf(".");
25165
+ const lastSlash = Math.max(clean.lastIndexOf("/"), clean.lastIndexOf("\\"));
25166
+ if (lastDot > lastSlash && lastDot >= 0) {
25167
+ return clean.substring(lastDot).toLowerCase();
25168
+ }
25169
+ return "";
25170
+ }
25171
+ /**
25172
+ * Guesses a file extension for a blob/data URL when bundling into the zip.
25173
+ * @param url - The original URL.
25174
+ * @param isTexture - Whether the key is known to be a texture.
25175
+ * @returns A file extension including the dot (e.g. ".glb", ".png").
25176
+ */
25177
+ function GuessExtension(url, isTexture) {
25178
+ // Try to extract from data URI mime type
25179
+ if (url.startsWith("data:")) {
25180
+ const mimeMatch = url.match(/^data:([^;,]+)/);
25181
+ if (mimeMatch) {
25182
+ const ext = MimeToExtension(mimeMatch[1]);
25183
+ if (ext) {
25184
+ return ext;
25185
+ }
25186
+ }
25187
+ }
25188
+ return isTexture ? ".png" : ".glb";
25189
+ }
25190
+ // Tuple-array `Map`s rather than object literals so the MIME and extension
25191
+ // keys (e.g. "model/gltf-binary", ".glb") don't trigger the naming-convention
25192
+ // rule that runs on object-literal property names.
25193
+ const MimeToExtensionMap = new Map([
25194
+ ["model/gltf-binary", ".glb"],
25195
+ ["model/gltf+json", ".gltf"],
25196
+ ["image/png", ".png"],
25197
+ ["image/jpeg", ".jpg"],
25198
+ ["image/webp", ".webp"],
25199
+ ["application/octet-stream", ".glb"],
25200
+ ["application/json", ".babylon"],
25201
+ ]);
25202
+ const ExtensionToMimeMap = new Map([
25203
+ [".glb", "model/gltf-binary"],
25204
+ [".gltf", "model/gltf+json"],
25205
+ [".babylon", "application/json"],
25206
+ [".png", "image/png"],
25207
+ [".jpg", "image/jpeg"],
25208
+ [".jpeg", "image/jpeg"],
25209
+ [".env", "application/octet-stream"],
25210
+ [".hdr", "application/octet-stream"],
25211
+ [".dds", "application/octet-stream"],
25212
+ [".ktx", "application/octet-stream"],
25213
+ [".ktx2", "application/octet-stream"],
25214
+ [".json", "application/json"],
25215
+ ]);
25216
+ /**
25217
+ * Maps a MIME type to a file extension.
25218
+ * @param mime - The MIME type string.
25219
+ * @returns The file extension including the dot, or empty string if unknown.
25220
+ */
25221
+ function MimeToExtension(mime) {
25222
+ return MimeToExtensionMap.get(mime) ?? "";
25223
+ }
25224
+ /**
25225
+ * Guesses a MIME type from a filename.
25226
+ * @param filename - The filename to check.
25227
+ * @returns The guessed MIME type string.
25228
+ */
25229
+ function GuessMimeType(filename) {
25230
+ const ext = filename.substring(filename.lastIndexOf(".")).toLowerCase();
25231
+ return ExtensionToMimeMap.get(ext) ?? "application/octet-stream";
25232
+ }
25233
+
25234
+ const ProjectAuthoringPaneKey = "Project Authoring";
25235
+ const SceneFileAccept = [".glb", ".gltf", ".babylon", ".obj"];
25236
+ const TextureFileAccept = Array.from(GetSmartAssetTextureExtensions());
25237
+ const AllAcceptString = [...SceneFileAccept, ...TextureFileAccept].join(",");
25238
+ // eslint-disable-next-line @typescript-eslint/naming-convention
25239
+ function _isTextureExtension(ext) {
25240
+ return GetSmartAssetTextureExtensions().has(ext);
25241
+ }
25242
+ /**
25243
+ * Inspector pane service that hosts the Project Authoring pane. Combines
25244
+ * smart-asset management (assets list, asset map I/O) with override-driven
25245
+ * scene authoring (material assignment, override summary).
25246
+ */
25247
+ const BabylonProjectAuthoringServiceDefinition = {
25248
+ friendlyName: "Project Authoring",
25249
+ consumes: [ShellServiceIdentity, SceneContextIdentity, SelectionServiceIdentity],
25250
+ factory: (shellService, sceneContext, selectionService) => {
25251
+ const paneRegistration = shellService.addSidePane({
25252
+ key: ProjectAuthoringPaneKey,
25253
+ title: ProjectAuthoringPaneKey,
22977
25254
  icon: CubeRegular,
22978
25255
  horizontalLocation: "right",
22979
25256
  verticalLocation: "top",
@@ -22981,7 +25258,7 @@ const SmartAssetsServiceDefinition = {
22981
25258
  teachingMoment: false,
22982
25259
  content: () => {
22983
25260
  const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
22984
- return scene ? jsx(SmartAssetsPane, { scene: scene, selectionService: selectionService }) : null;
25261
+ return scene ? jsx(BabylonProjectAuthoringPane, { scene: scene, selectionService: selectionService }) : null;
22985
25262
  },
22986
25263
  });
22987
25264
  return {
@@ -23036,11 +25313,81 @@ const useStyles$3 = makeStyles({
23036
25313
  hiddenInput: {
23037
25314
  display: "none",
23038
25315
  },
25316
+ overrideRow: {
25317
+ display: "flex",
25318
+ gap: tokens.spacingHorizontalXS,
25319
+ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
25320
+ fontSize: "10px",
25321
+ fontFamily: "monospace",
25322
+ },
25323
+ dimSeparator: {
25324
+ opacity: 0.5,
25325
+ },
25326
+ overrideValue: {
25327
+ color: tokens.colorPaletteGreenForeground1,
25328
+ },
25329
+ busyMessage: {
25330
+ display: "flex",
25331
+ alignItems: "center",
25332
+ gap: tokens.spacingHorizontalXS,
25333
+ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
25334
+ },
23039
25335
  });
23040
- // ── Smart Assets Pane ──
23041
- const SmartAssetsPane = (props) => {
25336
+ // ── Project Authoring Pane ──
25337
+ const BabylonProjectAuthoringPane = (props) => {
23042
25338
  const { scene, selectionService } = props;
23043
- return (jsxs(Accordion, { uniqueId: "SmartAssets", enablePinnedItems: true, enableHiddenItems: true, enableSearchItems: true, children: [jsx(AccordionSection, { title: "Assets", children: jsx(SmartAssetList, { scene: scene, selectionService: selectionService }) }), jsx(AccordionSection, { title: "Asset Map", children: jsx(SmartAssetProjectTools, { scene: scene }) })] }));
25339
+ return (jsxs(Accordion, { uniqueId: "ProjectAuthoring", enablePinnedItems: true, enableHiddenItems: true, enableSearchItems: true, children: [jsx(AccordionSection, { title: "Project File", children: jsx(ProjectFileTools, { scene: scene }) }), jsx(AccordionSection, { title: "Assets", children: jsx(SmartAssetList, { scene: scene, selectionService: selectionService }) }), jsx(AccordionSection, { title: "Override Summary", children: jsx(OverrideSummary, { scene: scene }) })] }));
25340
+ };
25341
+ // ── Project File ──
25342
+ /**
25343
+ * Save/load controls for the `.babylonproj` zip bundle that captures the
25344
+ * scene's smart assets and overrides as a single project file.
25345
+ * @param props - Component props.
25346
+ * @returns The project file controls.
25347
+ */
25348
+ const ProjectFileTools = (props) => {
25349
+ const { scene } = props;
25350
+ const styles = useStyles$3();
25351
+ const [status, setStatus] = useState("");
25352
+ const [busy, setBusy] = useState("");
25353
+ const isBusy = busy !== "";
25354
+ const onSaveProject = useCallback(async () => {
25355
+ if (isBusy) {
25356
+ return;
25357
+ }
25358
+ setBusy("Saving project...");
25359
+ setStatus("");
25360
+ try {
25361
+ const blob = await SaveProjectFileAsync(scene);
25362
+ Tools.Download(blob, "scene.babylonproj");
25363
+ setStatus("Saved scene.babylonproj");
25364
+ }
25365
+ catch (err) {
25366
+ setStatus(`Save error: ${err}`);
25367
+ }
25368
+ finally {
25369
+ setBusy("");
25370
+ }
25371
+ }, [scene, isBusy]);
25372
+ const onLoadProject = useCallback(async (files) => {
25373
+ const file = files[0];
25374
+ if (!file || isBusy) {
25375
+ return;
25376
+ }
25377
+ setBusy("Loading project...");
25378
+ setStatus("");
25379
+ try {
25380
+ await LoadProjectFileAsync(scene, file);
25381
+ setStatus(`Loaded ${file.name}`);
25382
+ }
25383
+ catch (err) {
25384
+ setStatus(`Load error: ${err}`);
25385
+ }
25386
+ finally {
25387
+ setBusy("");
25388
+ }
25389
+ }, [scene, isBusy]);
25390
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Save Project (.babylonproj)", icon: SaveRegular, onClick: onSaveProject, disabled: isBusy }), jsx(FileUploadLine, { label: "Load Project (.babylonproj)", accept: ".babylonproj", onClick: onLoadProject, disabled: isBusy }), isBusy && (jsxs("div", { className: styles.busyMessage, children: [jsx(Spinner, { size: "extra-small" }), jsx(Caption1, { children: busy })] })), status && jsx("div", { className: styles.statusMessage, children: status })] }));
23044
25391
  };
23045
25392
  // ── Smart Asset List ──
23046
25393
  const SmartAssetList = (props) => {
@@ -23055,8 +25402,10 @@ const SmartAssetList = (props) => {
23055
25402
  const compactToolContext = useMemo(() => ({ ...toolContext, size: "small" }), [toolContext]);
23056
25403
  // Subscribe reactively to changes — re-renders the asset list whenever
23057
25404
  // RegisterSmartAsset / Load / Remove / Reload fire onChangedObservable.
25405
+ // Filter out the reserved companion key so it does not appear as a user
25406
+ // asset alongside dragged-in textures/models.
23058
25407
  const sam = GetSmartAssetManager(scene);
23059
- const assets = useObservableState(useCallback(() => Array.from(GetAllSmartAssets(scene), ([key, url]) => ({ key, url })), [scene]), sam.onChangedObservable);
25408
+ const assets = useObservableState(useCallback(() => Array.from(GetAllSmartAssets(scene), ([key, url]) => ({ key, url })).filter((a) => a.key !== ProjectLocalsKey), [scene]), sam.onChangedObservable);
23060
25409
  const onAddAsset = useCallback(() => {
23061
25410
  fileInputRef.current?.click();
23062
25411
  }, []);
@@ -23107,6 +25456,12 @@ const SmartAssetList = (props) => {
23107
25456
  }, [scene]);
23108
25457
  const onReloadAsset = useCallback(async (key) => {
23109
25458
  await ReloadSmartAssetAsync(scene, key);
25459
+ // ReloadSmartAssetAsync disposes the old asset and loads fresh
25460
+ // instances — those new objects have no knowledge of previously
25461
+ // applied overrides, so we must re-apply them. (The swap path
25462
+ // does the same.) Without this, overrides on a smart asset
25463
+ // appear to silently revert whenever the user hits Reload.
25464
+ ApplyAllOverrides(scene);
23110
25465
  setStatus(`Reloaded: ${key}`);
23111
25466
  }, [scene]);
23112
25467
  const onSwapAsset = useCallback((key) => {
@@ -23234,6 +25589,34 @@ const SmartAssetList = (props) => {
23234
25589
  }), jsx(ButtonLine, { label: "Add Asset", icon: AddRegular, onClick: onAddAsset }), jsx("input", { ref: fileInputRef, type: "file", accept: AllAcceptString, multiple: true, className: styles.hiddenInput, onChange: onFileSelected }), status && jsx(Caption1, { className: styles.statusMessage, children: status })] }));
23235
25590
  };
23236
25591
  // ── Utilities ──
25592
+ // ── Override Summary ──
25593
+ /**
25594
+ * Pane content that lists all registered overrides for the scene. Subscribes
25595
+ * directly to the manager's change observable so loads, deletes, and
25596
+ * Inspector-driven edits update the view instantly.
25597
+ * @param props - Component props.
25598
+ * @returns The override list view.
25599
+ */
25600
+ const OverrideSummary = (props) => {
25601
+ const { scene } = props;
25602
+ const styles = useStyles$3();
25603
+ const overrideManager = GetOverrideManager(scene);
25604
+ const overrideList = useObservableState(useCallback(() => {
25605
+ return GetOverrides(scene).map((o) => {
25606
+ const nameLabel = o.targetName === "" ? "(scene)" : o.targetIndex > 0 ? `${o.targetName}[${o.targetIndex}]` : o.targetName;
25607
+ return {
25608
+ target: `${o.targetType}.${nameLabel}`,
25609
+ prop: o.propertyPath,
25610
+ value: String(o.value),
25611
+ };
25612
+ });
25613
+ }, [scene]), overrideManager.onChangedObservable);
25614
+ if (overrideList.length === 0) {
25615
+ return jsx("div", { className: styles.emptyMessage, children: "No overrides tracked. Edit properties in Inspector to create overrides." });
25616
+ }
25617
+ return (jsx(Fragment, { children: overrideList.map((o, i) => (jsxs("div", { className: styles.overrideRow, children: [jsx("span", { children: o.target }), jsx("span", { className: styles.dimSeparator, children: "." }), jsx("span", { children: o.prop }), jsx("span", { className: styles.dimSeparator, children: "=" }), jsx("span", { className: styles.overrideValue, children: ShortenValue(o.value) })] }, i))) }));
25618
+ };
25619
+ // ── Utilities ──
23237
25620
  /**
23238
25621
  * Finds the first scene entity produced by a smart asset key, for click-to-select.
23239
25622
  * Prefers non-root meshes, then materials, then textures.
@@ -23293,6 +25676,14 @@ function _getExtension(url) {
23293
25676
  }
23294
25677
  return "";
23295
25678
  }
25679
+ /**
25680
+ * Truncates a value string to a maximum display length.
25681
+ * @param value - The value string to shorten.
25682
+ * @returns The truncated string, with an ellipsis if it was shortened.
25683
+ */
25684
+ function ShortenValue(value) {
25685
+ return value.length > 30 ? value.substring(0, 27) + "…" : value;
25686
+ }
23296
25687
 
23297
25688
  const AnimationGroupLoadingModes = [
23298
25689
  { label: "Clean", value: 0 /* SceneLoaderAnimationGroupLoadingMode.Clean */ },
@@ -23804,7 +26195,7 @@ const Dialog = (props) => {
23804
26195
  if (!data.open && onDismiss) {
23805
26196
  onDismiss();
23806
26197
  }
23807
- }, children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { action: onDismiss ? (jsx(DialogTrigger, { action: "close", children: jsx(Button, { appearance: "subtle", "aria-label": "close", icon: DismissRegular }) })) : undefined, children: title }), jsx(DialogContent, { children: children }), actions && actions.length > 0 && (jsx(DialogActions, { children: actions.map((action, index) => (jsx(Button, { appearance: action.appearance ?? "secondary", onClick: action.onClick, label: action.label }, index))) }))] }) }) }));
26198
+ }, children: jsx(DialogSurface, { children: jsxs(DialogBody, { children: [jsx(DialogTitle, { action: onDismiss ? (jsx(DialogTrigger, { action: "close", children: jsx(Button, { appearance: "subtle", ariaLabel: "close", icon: DismissRegular }) })) : undefined, children: title }), jsx(DialogContent, { children: children }), actions && actions.length > 0 && (jsx(DialogActions, { children: actions.map((action, index) => (jsx(Button, { appearance: action.appearance ?? "secondary", onClick: action.onClick, label: action.label }, index))) }))] }) }) }));
23808
26199
  };
23809
26200
 
23810
26201
  const SmartAssetsPaneKey = "Smart Assets";
@@ -24086,7 +26477,7 @@ function ShowInspector(scene, options = {}) {
24086
26477
  // Helps with managing gizmos and a shared utility layer.
24087
26478
  GizmoServiceDefinition,
24088
26479
  // Scene explorer tab and related services.
24089
- SceneExplorerServiceDefinition, NodeExplorerServiceDefinition, SkeletonExplorerServiceDefinition, MaterialExplorerServiceDefinition, TextureExplorerServiceDefinition, PostProcessExplorerServiceDefinition, RenderingPipelineExplorerServiceDefinition, EffectLayerExplorerServiceDefinition, ParticleSystemExplorerServiceDefinition, SpriteManagerExplorerServiceDefinition, AnimationGroupExplorerServiceDefinition, GuiExplorerServiceDefinition, FrameGraphExplorerServiceDefinition, AtmosphereExplorerServiceDefinition, SoundExplorerServiceDefinition, DisposableCommandServiceDefinition,
26480
+ SceneExplorerServiceDefinition, NodeExplorerServiceDefinition, SkeletonExplorerServiceDefinition, MaterialExplorerServiceDefinition, TextureExplorerServiceDefinition, PostProcessExplorerServiceDefinition, RenderingPipelineExplorerServiceDefinition, EffectLayerExplorerServiceDefinition, ParticleSystemExplorerServiceDefinition, SpriteManagerExplorerServiceDefinition, AnimationGroupExplorerServiceDefinition, GuiExplorerServiceDefinition, FrameGraphExplorerServiceDefinition, AtmosphereExplorerServiceDefinition, SoundExplorerServiceDefinition, AudioV2ExplorerServiceDefinition, DisposableCommandServiceDefinition,
24090
26481
  // Properties pane tab and related services.
24091
26482
  ScenePropertiesServiceDefinition, PropertiesServiceDefinition, TexturePropertiesServiceDefinition, CommonPropertiesServiceDefinition, TransformPropertiesServiceDefinition, AnimationPropertiesServiceDefinition, NodePropertiesServiceDefinition, PhysicsPropertiesServiceDefinition, SkeletonPropertiesServiceDefinition, MaterialPropertiesServiceDefinition, LightPropertiesServiceDefinition, SpritePropertiesServiceDefinition, ParticleSystemPropertiesServiceDefinition, CameraPropertiesServiceDefinition, PostProcessPropertiesServiceDefinition, RenderingPipelinePropertiesServiceDefinition, EffectLayerPropertiesServiceDefinition, FrameGraphPropertiesServiceDefinition, AnimationGroupPropertiesServiceDefinition, MetadataPropertiesServiceDefinition, AtmospherePropertiesServiceDefinition, AudioPropertiesServiceDefinition,
24092
26483
  // Texture editor and related services.
@@ -24096,7 +26487,7 @@ function ShowInspector(scene, options = {}) {
24096
26487
  // Stats pane tab and related services.
24097
26488
  StatsServiceDefinition,
24098
26489
  // Tools pane tab and related services.
24099
- ToolsServiceDefinition, ExportServiceDefinition, SmartAssetPromptServiceDefinition, SmartAssetsServiceDefinition, GLTFAnimationImportServiceDefinition, GLTFLoaderOptionsServiceDefinition, GLTFValidationServiceDefinition, CaptureToolsDefinition,
26490
+ ToolsServiceDefinition, ExportServiceDefinition, SmartAssetPromptServiceDefinition, BabylonProjectAuthoringServiceDefinition, OverrideCaptureServiceDefinition, GLTFAnimationImportServiceDefinition, GLTFLoaderOptionsServiceDefinition, GLTFValidationServiceDefinition, CaptureToolsDefinition,
24100
26491
  // Settings pane tab and related services.
24101
26492
  SettingsServiceDefinition, InspectorSettingsServiceDefinition, WatcherSettingsServiceDefinition, ShellSettingsServiceDefinition,
24102
26493
  // Adds a button to refresh all properties manually (when watcher is in "manual" mode).
@@ -24958,4 +27349,4 @@ const TextAreaPropertyLine = (props) => {
24958
27349
  AttachDebugLayer();
24959
27350
 
24960
27351
  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 };
24961
- //# sourceMappingURL=index-PYblOaAV.js.map
27352
+ //# sourceMappingURL=index--oJsOVVX.js.map