@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.
- package/lib/browser-CANgtOiM.js +1277 -0
- package/lib/browser-CANgtOiM.js.map +1 -0
- package/lib/components/properties/audio/audioV2Properties.d.ts +93 -0
- package/lib/components/properties/audio/audioV2SpatialProperties.d.ts +12 -0
- package/lib/components/properties/materials/materialTextureDebugPropertyLine.d.ts +18 -0
- package/lib/{extensionsListService-DTrjNf_v.js → extensionsListService-j6viqje8.js} +13 -2
- package/lib/{extensionsListService-DTrjNf_v.js.map → extensionsListService-j6viqje8.js.map} +1 -1
- package/lib/{index-PYblOaAV.js → index--oJsOVVX.js} +2775 -384
- package/lib/index--oJsOVVX.js.map +1 -0
- package/lib/index.js +12 -1
- package/lib/index.js.map +1 -1
- package/lib/projects/overrideEntry.d.ts +36 -0
- package/lib/projects/overrideManager.d.ts +176 -0
- package/lib/projects/projectFile.d.ts +143 -0
- package/lib/{quickCreateToolsService-8d6rBO-A.js → quickCreateToolsService-eZ4MCuJ2.js} +13 -2
- package/lib/{quickCreateToolsService-8d6rBO-A.js.map → quickCreateToolsService-eZ4MCuJ2.js.map} +1 -1
- package/lib/{reflectorService-B7LcD1Sn.js → reflectorService-2dP-GJrK.js} +13 -2
- package/lib/{reflectorService-B7LcD1Sn.js.map → reflectorService-2dP-GJrK.js.map} +1 -1
- package/lib/services/gizmoService.d.ts +9 -0
- package/lib/services/overrideCaptureService.d.ts +16 -0
- package/lib/services/panes/{smartAssetsService.d.ts → babylonProjectAuthoringService.d.ts} +4 -2
- package/lib/services/panes/properties/audioPropertiesService.d.ts +2 -1
- package/lib/services/panes/scene/audioV2ExplorerService.d.ts +6 -0
- package/lib/services/panes/scene/defaultSectionsMetadata.d.ts +2 -1
- package/package.json +1 -1
- package/lib/index-PYblOaAV.js.map +0 -1
- 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,
|
|
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,
|
|
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,
|
|
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
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2686
|
-
|
|
2687
|
-
const
|
|
2688
|
-
if (
|
|
2689
|
-
|
|
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
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
2695
|
-
|
|
2745
|
+
catch {
|
|
2746
|
+
// Storage may be full / disabled — bounds simply won't persist.
|
|
2696
2747
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
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
|
-
|
|
2701
|
-
|
|
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 [
|
|
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
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
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
|
-
}, [
|
|
2917
|
+
}, [id]);
|
|
2771
2918
|
useImperativeHandle(imperativeRef, () => {
|
|
2772
2919
|
return {
|
|
2773
2920
|
open: createWindow,
|
|
2774
|
-
close: () =>
|
|
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
|
|
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
|
-
|
|
2781
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
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
|
-
//
|
|
6157
|
-
//
|
|
6158
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
};
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
}
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
|
|
21527
|
-
|
|
21528
|
-
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
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
|
-
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
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
|
|
22893
|
-
|
|
22894
|
-
|
|
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
|
-
*
|
|
22906
|
-
*
|
|
22907
|
-
* @
|
|
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
|
-
|
|
22910
|
-
const
|
|
22911
|
-
|
|
22912
|
-
|
|
22913
|
-
|
|
22914
|
-
const
|
|
22915
|
-
|
|
22916
|
-
|
|
22917
|
-
|
|
22918
|
-
|
|
22919
|
-
|
|
22920
|
-
|
|
22921
|
-
|
|
22922
|
-
|
|
22923
|
-
|
|
22924
|
-
|
|
22925
|
-
|
|
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
|
-
|
|
22928
|
-
|
|
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
|
-
|
|
22931
|
-
|
|
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
|
-
}
|
|
22934
|
-
|
|
22935
|
-
|
|
22936
|
-
|
|
22937
|
-
|
|
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
|
-
|
|
22940
|
-
|
|
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
|
-
|
|
22943
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
|
|
22947
|
-
|
|
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
|
-
|
|
22950
|
-
|
|
24168
|
+
if (value.startsWith("samTexture:")) {
|
|
24169
|
+
return ResolveSamTextureReference(scene, value.substring(11));
|
|
22951
24170
|
}
|
|
22952
|
-
|
|
22953
|
-
|
|
24171
|
+
if (value.startsWith("texture:")) {
|
|
24172
|
+
return ResolveTextureReference(scene, value.substring(8));
|
|
22954
24173
|
}
|
|
22955
|
-
}
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
22959
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
22971
|
-
|
|
22972
|
-
|
|
22973
|
-
|
|
22974
|
-
|
|
22975
|
-
|
|
22976
|
-
|
|
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(
|
|
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
|
-
// ──
|
|
23041
|
-
const
|
|
25336
|
+
// ── Project Authoring Pane ──
|
|
25337
|
+
const BabylonProjectAuthoringPane = (props) => {
|
|
23042
25338
|
const { scene, selectionService } = props;
|
|
23043
|
-
return (jsxs(Accordion, { uniqueId: "
|
|
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",
|
|
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,
|
|
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
|
|
27352
|
+
//# sourceMappingURL=index--oJsOVVX.js.map
|