@babylonjs/inspector 8.36.1-preview → 8.37.0-preview

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.
@@ -3,8 +3,8 @@ import { useMemo, useEffect, useState, useRef, useCallback, forwardRef, createCo
3
3
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
4
4
  import { Vector3, Quaternion, Matrix, Vector2, Vector4, TmpVectors } from '@babylonjs/core/Maths/math.vector.js';
5
5
  import { Observable } from '@babylonjs/core/Misc/observable.js';
6
- import { makeStyles, Link as Link$1, Body1, ToggleButton as ToggleButton$1, Button as Button$1, tokens, InfoLabel as InfoLabel$1, Body1Strong, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, createLightTheme, createDarkTheme, FluentProvider, Tooltip, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, Portal, RendererProvider, ToolbarRadioButton, createDOMRenderer, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, treeItemLevelToken, Switch as Switch$1, PresenceBadge, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Spinner, Badge, useId, Input, SpinButton as SpinButton$1, Slider, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, Dropdown as Dropdown$1, Option, Popover, PopoverTrigger, ColorSwatch, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, Textarea as Textarea$1, ToolbarButton, useComboboxFilter, Combobox, Field } from '@fluentui/react-components';
7
- import { ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, Copy20Regular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ErrorCircleRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, SaveRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, CopyRegular, DeleteRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, PlayRegular, AppGenericRegular, MyLocationRegular, CameraRegular, LightbulbRegular, BorderOutsideRegular, BorderNoneRegular, EyeRegular, EyeOffRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, DeleteFilled } from '@fluentui/react-icons';
6
+ import { makeStyles, Link as Link$1, Body1, ToggleButton as ToggleButton$1, Button as Button$1, tokens, InfoLabel as InfoLabel$1, Body1Strong, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, createLightTheme, createDarkTheme, FluentProvider, Tooltip, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, Portal, RendererProvider, ToolbarRadioButton, createDOMRenderer, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, treeItemLevelToken, Switch as Switch$1, PresenceBadge, useId, SpinButton as SpinButton$1, Slider, Input, Dropdown as Dropdown$1, Option, Popover, PopoverTrigger, ColorSwatch, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Spinner, Badge, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, Textarea as Textarea$1, ToolbarButton, useComboboxFilter, Combobox, Field } from '@fluentui/react-components';
7
+ import { ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, Copy20Regular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ErrorCircleRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, SaveRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, CopyRegular, DeleteRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, PlayRegular, AppGenericRegular, MyLocationRegular, CameraRegular, LightbulbRegular, BorderOutsideRegular, BorderNoneRegular, EyeRegular, EyeOffRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
8
8
  import { Collapse as Collapse$1, Fade } from '@fluentui/react-motion-components-preview';
9
9
  import '@babylonjs/core/Misc/typeStore.js';
10
10
  import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
@@ -32,7 +32,6 @@ import { PerfCollectionStrategy } from '@babylonjs/core/Misc/PerformanceViewer/p
32
32
  import '@babylonjs/core/Misc/PerformanceViewer/performanceViewerSceneExtension.js';
33
33
  import { PressureObserverWrapper } from '@babylonjs/core/Misc/pressureObserverWrapper.js';
34
34
  import { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js';
35
- import { EngineStore } from '@babylonjs/core/Engines/engineStore.js';
36
35
  import { createRoot } from 'react-dom/client';
37
36
  import { Logger } from '@babylonjs/core/Misc/logger.js';
38
37
  import { FrameGraphUtils } from '@babylonjs/core/FrameGraph/frameGraphUtils.js';
@@ -105,6 +104,8 @@ import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineMa
105
104
  import '@babylonjs/core/Sprites/spriteSceneComponent.js';
106
105
  import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
107
106
  import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js';
107
+ import { EngineStore } from '@babylonjs/core/Engines/engineStore.js';
108
+ import { UniqueIdGenerator } from '@babylonjs/core/Misc/uniqueIdGenerator.js';
108
109
 
109
110
  const InterceptorHooksMaps$1 = new WeakMap();
110
111
  /**
@@ -1631,6 +1632,17 @@ const useStyles$c = makeStyles({
1631
1632
  flexDirection: "column",
1632
1633
  overflowX: "hidden",
1633
1634
  overflowY: "hidden",
1635
+ zIndex: 1,
1636
+ },
1637
+ paneContainerOverlay: {
1638
+ position: "absolute",
1639
+ height: "100%",
1640
+ },
1641
+ paneContainerOverlayLeft: {
1642
+ left: 0,
1643
+ },
1644
+ paneContainerOverlayRight: {
1645
+ right: 0,
1634
1646
  },
1635
1647
  paneContent: {
1636
1648
  display: "flex",
@@ -1759,7 +1771,7 @@ const SidePaneTab = (props) => {
1759
1771
  // This hook provides a side pane container and the tab list.
1760
1772
  // In "compact" mode, the tab list is integrated into the pane itself.
1761
1773
  // In "full" mode, the returned tab list is later injected into the toolbar.
1762
- function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane, dockOperations, toolbarMode, topBarItems, bottomBarItems) {
1774
+ function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSelectSidePane, dockOperations, toolbarMode, topBarItems, bottomBarItems) {
1763
1775
  const classes = useStyles$c();
1764
1776
  const [topSelectedTab, setTopSelectedTab] = useState();
1765
1777
  const [bottomSelectedTab, setBottomSelectedTab] = useState();
@@ -1959,7 +1971,9 @@ function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane,
1959
1971
  const pane = useMemo(() => {
1960
1972
  if (!windowState) {
1961
1973
  // If there is no window state, then we are docked, so render the resizable div and the collapse container.
1962
- return (jsx("div", { ref: paneContainerRef, className: classes.paneContainer, children: (topPanes.length > 0 || bottomPanes.length > 0) && (jsxs("div", { className: `${classes.pane} ${location === "left" ? classes.paneLeft : classes.paneRight}`, children: [jsx(Collapse, { orientation: "horizontal", visible: !collapsed, children: jsx("div", { ref: paneHorizontalResizeElementRef, className: classes.paneContainer, style: { width: `clamp(${minWidth}px, calc(${defaultWidth}px + var(${paneWidthAdjustCSSVar}, 0px)), 1000px)` }, children: corePane }) }), jsx("div", { ref: paneHorizontalResizeHandleRef, className: `${classes.resizer} ${location === "left" ? classes.resizerLeft : classes.resizerRight}`, style: { pointerEvents: `${collapsed ? "none" : "auto"}` } })] })) }));
1974
+ return (jsx("div", { ref: paneContainerRef, className: mergeClasses(classes.paneContainer, layoutMode === "inline"
1975
+ ? undefined
1976
+ : mergeClasses(classes.paneContainerOverlay, location === "left" ? classes.paneContainerOverlayLeft : classes.paneContainerOverlayRight)), children: (topPanes.length > 0 || bottomPanes.length > 0) && (jsxs("div", { className: `${classes.pane} ${location === "left" ? classes.paneLeft : classes.paneRight}`, children: [jsx(Collapse, { orientation: "horizontal", visible: !collapsed, children: jsx("div", { ref: paneHorizontalResizeElementRef, className: classes.paneContainer, style: { width: `clamp(${minWidth}px, calc(${defaultWidth}px + var(${paneWidthAdjustCSSVar}, 0px)), 1000px)` }, children: corePane }) }), jsx("div", { ref: paneHorizontalResizeHandleRef, className: `${classes.resizer} ${location === "left" ? classes.resizerLeft : classes.resizerRight}`, style: { pointerEvents: `${collapsed ? "none" : "auto"}` } })] })) }));
1963
1977
  }
1964
1978
  else {
1965
1979
  // Otherwise we are undocked, so render into the portal that targets the body of the child window.
@@ -1971,7 +1985,7 @@ function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane,
1971
1985
  }, [collapsed, corePane, windowState]);
1972
1986
  return [topPaneTabList, pane, collapsed, setCollapsed];
1973
1987
  }
1974
- function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWidth = 350, rightPaneDefaultWidth = 350, rightPaneMinWidth = 350, toolbarMode = "full", sidePaneRemapper = undefined, } = {}) {
1988
+ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWidth = 350, rightPaneDefaultWidth = 350, rightPaneMinWidth = 350, toolbarMode = "full", sidePaneRemapper = undefined, layoutMode = "inline", } = {}) {
1975
1989
  return {
1976
1990
  friendlyName: "MainView",
1977
1991
  produces: [ShellServiceIdentity, RootComponentServiceIdentity],
@@ -1998,7 +2012,8 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
1998
2012
  const coercedSidePaneCache = useRef(new Map());
1999
2013
  const coercedSidePanes = useMemo(() => {
2000
2014
  // First pass - apply overrides and respect the side pane mode.
2001
- const coercedSidePanes = sidePanes.map((sidePaneDefinition) => {
2015
+ const coercedSidePanes = sidePanes
2016
+ .map((sidePaneDefinition) => {
2002
2017
  let coercedSidePane = coercedSidePaneCache.current.get(sidePaneDefinition.key);
2003
2018
  if (!coercedSidePane) {
2004
2019
  coercedSidePane = { ...sidePaneDefinition };
@@ -2012,9 +2027,14 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
2012
2027
  }
2013
2028
  else if (sidePaneRemapper) {
2014
2029
  // A side pane remapper has the next highest priority.
2015
- const { horizontalLocation, verticalLocation } = sidePaneRemapper(sidePaneDefinition);
2016
- coercedSidePane.horizontalLocation = horizontalLocation;
2017
- coercedSidePane.verticalLocation = verticalLocation;
2030
+ const remapping = sidePaneRemapper(sidePaneDefinition);
2031
+ if (!remapping) {
2032
+ coercedSidePane = undefined;
2033
+ }
2034
+ else {
2035
+ coercedSidePane.horizontalLocation = remapping.horizontalLocation;
2036
+ coercedSidePane.verticalLocation = remapping.verticalLocation;
2037
+ }
2018
2038
  }
2019
2039
  else {
2020
2040
  // Otherwise use the default defined location.
@@ -2022,7 +2042,8 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
2022
2042
  coercedSidePane.verticalLocation = sidePaneDefinition.verticalLocation;
2023
2043
  }
2024
2044
  return coercedSidePane;
2025
- });
2045
+ })
2046
+ .filter((sidePane) => !!sidePane);
2026
2047
  // Second pass - correct any invalid state, specifically if there are only bottom panes, force them to be top panes.
2027
2048
  for (const side of ["left", "right"]) {
2028
2049
  const topPanes = coercedSidePanes.filter((entry) => entry.horizontalLocation === side && entry.verticalLocation === "top");
@@ -2098,8 +2119,8 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
2098
2119
  const bottomBarLeftItems = useMemo(() => bottomToolBarItems.filter((entry) => coerceToolBarItemHorizontalLocation(entry) === "left"), [bottomToolBarItems, coerceToolBarItemHorizontalLocation]);
2099
2120
  const bottomBarRightItems = useMemo(() => bottomToolBarItems.filter((entry) => coerceToolBarItemHorizontalLocation(entry) === "right"), [bottomToolBarItems, coerceToolBarItemHorizontalLocation]);
2100
2121
  const centralContents = useOrderedObservableCollection(centralContentCollection);
2101
- const [leftPaneTabList, leftPane, leftPaneCollapsed, setLeftPaneCollapsed] = usePane("left", leftPaneDefaultWidth, leftPaneMinWidth, coercedSidePanes, onSelectSidePane, sidePaneDockOperations, toolbarMode, topBarLeftItems, bottomBarLeftItems);
2102
- const [rightPaneTabList, rightPane, rightPaneCollapsed, setRightPaneCollapsed] = usePane("right", rightPaneDefaultWidth, rightPaneMinWidth, coercedSidePanes, onSelectSidePane, sidePaneDockOperations, toolbarMode, topBarRightItems, bottomBarRightItems);
2122
+ const [leftPaneTabList, leftPane, leftPaneCollapsed, setLeftPaneCollapsed] = usePane("left", layoutMode, leftPaneDefaultWidth, leftPaneMinWidth, coercedSidePanes, onSelectSidePane, sidePaneDockOperations, toolbarMode, topBarLeftItems, bottomBarLeftItems);
2123
+ const [rightPaneTabList, rightPane, rightPaneCollapsed, setRightPaneCollapsed] = usePane("right", layoutMode, rightPaneDefaultWidth, rightPaneMinWidth, coercedSidePanes, onSelectSidePane, sidePaneDockOperations, toolbarMode, topBarRightItems, bottomBarRightItems);
2103
2124
  return (jsxs("div", { className: classes.mainView, children: [toolbarMode === "full" && (jsx(Fragment, { children: jsxs("div", { className: classes.barDiv, children: [leftPaneTabList, jsx(Toolbar, { location: "top", components: topToolBarItems }), rightPaneTabList] }) })), jsxs("div", { className: classes.verticallyCentralContent, children: [leftPane, jsxs("div", { className: classes.centralContent, children: [centralContents.map((entry) => (jsx(entry.component, {}, entry.key))), toolbarMode === "compact" && (jsxs(Fragment, { children: [jsx(Fade, { visible: leftPaneCollapsed, delay: 50, children: jsx("div", { className: mergeClasses(classes.expandButtonContainer, classes.expandButtonContainerLeft), children: jsx(Tooltip, { content: "Show Side Pane", relationship: "label", children: jsx(Button$1, { className: classes.expandButton, icon: jsx(PanelLeftExpandRegular, {}), onClick: () => setLeftPaneCollapsed(false) }) }) }) }), jsx(Fade, { visible: rightPaneCollapsed, delay: 50, children: jsx("div", { className: mergeClasses(classes.expandButtonContainer, classes.expandButtonContainerRight), children: jsx(Tooltip, { content: "Show Side Pane", relationship: "label", children: jsx(Button$1, { className: classes.expandButton, icon: jsx(PanelRightExpandRegular, {}), onClick: () => setRightPaneCollapsed(false) }) }) }) })] }))] }), rightPane] }), toolbarMode === "full" && (jsx(Fragment, { children: jsx("div", { className: classes.barDiv, children: jsx(Toolbar, { location: "bottom", components: bottomToolBarItems }) }) }))] }));
2104
2125
  };
2105
2126
  rootComponent.displayName = "Shell Service Root";
@@ -3452,7 +3473,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3452
3473
  keywords: ["export", "gltf", "glb", "babylon", "exporter", "tools"],
3453
3474
  ...BabylonWebResources,
3454
3475
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3455
- getExtensionModuleAsync: async () => await import('./exportService-B6ej2VVA.js'),
3476
+ getExtensionModuleAsync: async () => await import('./exportService-BB4L49R4.js'),
3456
3477
  },
3457
3478
  {
3458
3479
  name: "Capture Tools",
@@ -3460,7 +3481,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3460
3481
  keywords: ["capture", "screenshot", "gif", "video", "tools"],
3461
3482
  ...BabylonWebResources,
3462
3483
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3463
- getExtensionModuleAsync: async () => await import('./captureService-Blk4cfMr.js'),
3484
+ getExtensionModuleAsync: async () => await import('./captureService-DF30oxR-.js'),
3464
3485
  },
3465
3486
  {
3466
3487
  name: "Import Tools",
@@ -3468,164 +3489,753 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3468
3489
  keywords: ["import", "tools"],
3469
3490
  ...BabylonWebResources,
3470
3491
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3471
- getExtensionModuleAsync: async () => await import('./importService-ClQU2IkW.js'),
3492
+ getExtensionModuleAsync: async () => await import('./importService-DEe18q7F.js'),
3472
3493
  },
3473
3494
  ]);
3474
3495
 
3475
- const ExtensionManagerContext = createContext(undefined);
3476
- function useExtensionManager() {
3477
- return useContext(ExtensionManagerContext)?.extensionManager;
3478
- }
3479
-
3480
- const InstalledExtensionsKey = "Babylon/Extensions/InstalledExtensions";
3481
- const ExtensionInstalledKeyPrefix = "Babylon/Extensions/IsExtensionInstalled";
3482
- function GetExtensionInstalledKey(name) {
3483
- return `${ExtensionInstalledKeyPrefix}/${name}`;
3484
- }
3485
- function GetExtensionIdentity(feed, name) {
3486
- return `${feed}|${name}`;
3487
- }
3488
3496
  /**
3489
- * Manages the installation, uninstallation, enabling, and disabling of extensions.
3497
+ * Renders a label with an optional popup containing more info
3498
+ * @param props
3499
+ * @returns
3490
3500
  */
3491
- class ExtensionManager {
3492
- constructor(_serviceContainer, _feeds, _onInstallFailed) {
3493
- this._serviceContainer = _serviceContainer;
3494
- this._feeds = _feeds;
3495
- this._onInstallFailed = _onInstallFailed;
3496
- this._installedExtensions = new Map();
3497
- this._stateChangedHandlers = new Map();
3498
- }
3499
- /**
3500
- * Creates a new instance of the ExtensionManager.
3501
- * This will automatically rehydrate previously installed and enabled extensions.
3502
- * @param serviceContainer The service container to use.
3503
- * @param feeds The extension feeds to include.
3504
- * @param onInstallFailed A callback that is called when an extension installation fails.
3505
- * @returns A promise that resolves to the new instance of the ExtensionManager.
3506
- */
3507
- static async CreateAsync(serviceContainer, feeds, onInstallFailed) {
3508
- const extensionManager = new ExtensionManager(serviceContainer, feeds, onInstallFailed);
3509
- // Rehydrate installed extensions.
3510
- const installedExtensionNames = JSON.parse(localStorage.getItem(InstalledExtensionsKey) ?? "[]");
3511
- for (const installedExtensionName of installedExtensionNames) {
3512
- const installedExtensionRaw = localStorage.getItem(GetExtensionInstalledKey(installedExtensionName));
3513
- if (installedExtensionRaw) {
3514
- const installedExtensionData = JSON.parse(installedExtensionRaw);
3515
- const feed = feeds.find((feed) => feed.name === installedExtensionData.feed);
3516
- if (feed) {
3517
- const installedExtension = extensionManager._createInstalledExtension(installedExtensionData.metadata, feed);
3518
- extensionManager._installedExtensions.set(installedExtension.metadata.name, installedExtension);
3519
- }
3520
- }
3501
+ const InfoLabel = (props) => {
3502
+ InfoLabel.displayName = "InfoLabel";
3503
+ return (jsx(InfoLabel$1, { htmlFor: props.htmlFor, info: props.info, children: jsx(Body1, { children: props.label }) }));
3504
+ };
3505
+
3506
+ const SpinButton = (props) => {
3507
+ SpinButton.displayName = "SpinButton";
3508
+ const classes = useInputStyles$1();
3509
+ const { size } = useContext(ToolContext);
3510
+ const { min, max } = props;
3511
+ const [value, setValue] = useState(props.value);
3512
+ const lastCommittedValue = useRef(props.value);
3513
+ // step and forceInt are not mutually exclusive since there could be cases where you want to forceInt but have spinButton jump >1 int per spin
3514
+ const step = props.step != undefined ? props.step : props.forceInt ? 1 : undefined;
3515
+ const precision = Math.min(4, step !== undefined ? Math.max(0, CalculatePrecision(step)) : 2); // If no step, set precision to 2. Regardless, cap precision at 4 to avoid wild numbers
3516
+ useEffect(() => {
3517
+ if (props.value !== lastCommittedValue.current) {
3518
+ lastCommittedValue.current = props.value;
3519
+ setValue(props.value); // Update local state when props.value changes
3521
3520
  }
3522
- // Load installed and enabled extensions.
3523
- const enablePromises = [];
3524
- for (const extension of extensionManager._installedExtensions.values()) {
3525
- enablePromises.push((async () => {
3526
- try {
3527
- await extensionManager._enableAsync(extension.metadata, false, false);
3528
- }
3529
- catch {
3530
- // If enabling the extension fails, uninstall it. The extension install fail callback will still be called,
3531
- // so the owner of the ExtensionManager instance can decide what to do with the error.
3532
- await extensionManager._uninstallAsync(extension.metadata, false);
3533
- }
3534
- })());
3521
+ }, [props.value]);
3522
+ const validateValue = (numericValue) => {
3523
+ const outOfBounds = (min !== undefined && numericValue < min) || (max !== undefined && numericValue > max);
3524
+ const failsValidator = props.validator && !props.validator(numericValue);
3525
+ const failsIntCheck = props.forceInt ? !Number.isInteger(numericValue) : false;
3526
+ const invalid = !!outOfBounds || !!failsValidator || isNaN(numericValue) || !!failsIntCheck;
3527
+ return !invalid;
3528
+ };
3529
+ const tryCommitValue = (currVal) => {
3530
+ // Only commit if valid and different from last committed value
3531
+ if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
3532
+ lastCommittedValue.current = currVal;
3533
+ props.onChange(currVal);
3535
3534
  }
3536
- await Promise.all(enablePromises);
3537
- return extensionManager;
3538
- }
3539
- /**
3540
- * Gets the names of the feeds that are included in the extension manager.
3541
- * @returns The names of the feeds.
3542
- */
3543
- get feedNames() {
3544
- return this._feeds.map((feed) => feed.name);
3545
- }
3546
- /**
3547
- * Queries the extension manager for extensions.
3548
- * @param filter The filter to apply to the query.
3549
- * @param feeds The feeds to include in the query.
3550
- * @param installedOnly Whether to only include installed extensions.
3551
- * @returns A promise that resolves to the extension query.
3552
- */
3553
- async queryExtensionsAsync(filter = "", feeds = this.feedNames, installedOnly = false) {
3554
- if (installedOnly) {
3555
- const installedExtensions = Array.from(this._installedExtensions.values()).filter((installedExtension) => feeds.includes(installedExtension.feed.name));
3556
- return {
3557
- totalCount: installedExtensions.length,
3558
- getExtensionsAsync: async (index, count) => {
3559
- return installedExtensions.slice(index, index + count).map((installedExtension) => this._createExtension(installedExtension.metadata, installedExtension.feed));
3560
- },
3561
- };
3535
+ };
3536
+ const handleChange = (event, data) => {
3537
+ event.stopPropagation(); // Prevent event propagation
3538
+ if (data.value != null && !Number.isNaN(data.value)) {
3539
+ setValue(data.value);
3540
+ tryCommitValue(data.value);
3562
3541
  }
3563
- const queries = await Promise.all(this._feeds.filter((feed) => feeds.includes(feed.name)).map(async (feed) => Object.assign(await feed.queryExtensionsAsync(filter), { feed })));
3564
- const totalCount = queries.reduce((sum, query) => sum + query.totalCount, 0);
3565
- return {
3566
- totalCount,
3567
- getExtensionsAsync: async (index, count) => {
3568
- const extensions = new Array();
3569
- let remaining = count;
3570
- for (const query of queries) {
3571
- if (remaining <= 0) {
3572
- break;
3573
- }
3574
- if (index >= query.totalCount) {
3575
- index -= query.totalCount;
3576
- continue;
3577
- }
3578
- // This is intentionally sequential as we are querying for results until the count of results is met.
3579
- // eslint-disable-next-line no-await-in-loop
3580
- const metadataSlice = await query.getExtensionMetadataAsync(index, remaining);
3581
- extensions.push(...metadataSlice.map((metadata) => this._createExtension(metadata, query.feed)));
3582
- remaining -= metadataSlice.length;
3583
- index = 0;
3584
- }
3585
- return extensions;
3586
- },
3587
- };
3588
- }
3589
- /**
3590
- * Disposes the extension manager.
3591
- */
3592
- dispose() {
3593
- for (const installedExtension of this._installedExtensions.values()) {
3594
- // eslint-disable-next-line github/no-then
3595
- this._disableAsync(installedExtension.metadata, false).catch((error) => {
3596
- Logger.Warn(`Failed to disable extension ${installedExtension.metadata.name}: ${error}`);
3597
- });
3542
+ };
3543
+ const handleKeyUp = (event) => {
3544
+ event.stopPropagation(); // Prevent event propagation
3545
+ if (event.key !== "Enter") {
3546
+ const currVal = parseFloat(event.target.value); // Cannot use currentTarget.value as it won't have the most recently typed value
3547
+ setValue(currVal);
3548
+ tryCommitValue(currVal);
3598
3549
  }
3599
- this._stateChangedHandlers.clear();
3600
- }
3601
- async _installAsync(metadata, feed, isNestedStateChange) {
3602
- let installedExtension = this._installedExtensions.get(metadata.name);
3603
- if (!installedExtension) {
3604
- installedExtension = this._createInstalledExtension(metadata, feed);
3605
- installedExtension.isStateChanging = true;
3606
- this._installedExtensions.set(metadata.name, installedExtension);
3607
- try {
3608
- // Enable the extension.
3609
- await this._enableAsync(metadata, true, true);
3610
- }
3611
- catch (error) {
3612
- this._installedExtensions.delete(metadata.name);
3613
- throw error;
3614
- }
3615
- finally {
3616
- !isNestedStateChange && (installedExtension.isStateChanging = false);
3617
- }
3618
- // Mark the extension as being installed.
3619
- localStorage.setItem(GetExtensionInstalledKey(GetExtensionIdentity(feed.name, metadata.name)), JSON.stringify({
3620
- feed: feed.name,
3621
- metadata,
3622
- }));
3623
- localStorage.setItem(InstalledExtensionsKey, JSON.stringify(Array.from(this._installedExtensions.values()).map((extension) => GetExtensionIdentity(extension.feed.name, extension.metadata.name))));
3550
+ };
3551
+ const id = useId("spin-button");
3552
+ const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
3553
+ return (jsxs("div", { className: classes.container, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(SpinButton$1, { ...props, input: { className: classes.inputSlot }, step: step, id: id, size: size, precision: precision, displayValue: `${value.toFixed(precision)}${props.unit ? " " + props.unit : ""}`, value: value, onChange: handleChange, onKeyUp: handleKeyUp, onKeyDown: HandleKeyDown, onBlur: HandleOnBlur, className: mergedClassName })] }));
3554
+ };
3555
+
3556
+ const useSyncedSliderStyles = makeStyles({
3557
+ container: { display: "flex" },
3558
+ syncedSlider: {
3559
+ flex: "1 1 0",
3560
+ flexDirection: "row",
3561
+ display: "flex",
3562
+ alignItems: "center",
3563
+ },
3564
+ slider: {
3565
+ minWidth: CustomTokens.sliderMinWidth, // Minimum width for slider to remain usable
3566
+ maxWidth: CustomTokens.sliderMaxWidth,
3567
+ },
3568
+ });
3569
+ /**
3570
+ * Component which synchronizes a slider and an input field, allowing the user to change the value using either control
3571
+ * @param props
3572
+ * @returns SyncedSlider component
3573
+ */
3574
+ const SyncedSliderInput = (props) => {
3575
+ SyncedSliderInput.displayName = "SyncedSliderInput";
3576
+ const { infoLabel, ...passthroughProps } = props;
3577
+ const classes = useSyncedSliderStyles();
3578
+ const { size } = useContext(ToolContext);
3579
+ const [value, setValue] = useState(props.value);
3580
+ const pendingValueRef = useRef(undefined);
3581
+ const isDraggingRef = useRef(false);
3582
+ // NOTE: The Fluent slider will add tick marks if the step prop is anything other than undefined.
3583
+ // To avoid this, we scale the min/max based on the step so we can always make step undefined.
3584
+ // The actual step size in the Fluent slider is 1 when it is ste to undefined.
3585
+ const min = props.min ?? 0;
3586
+ const max = props.max ?? 100;
3587
+ const step = props.step ?? 1;
3588
+ useEffect(() => {
3589
+ !isDraggingRef.current && setValue(props.value ?? ""); // Update local state when props.value changes as long as user is not actively dragging
3590
+ }, [props.value]);
3591
+ const handleSliderChange = (_, data) => {
3592
+ const newValue = data.value * step;
3593
+ setValue(newValue);
3594
+ if (props.notifyOnlyOnRelease) {
3595
+ // Store the value but don't notify parent yet
3596
+ pendingValueRef.current = newValue;
3624
3597
  }
3625
- return installedExtension;
3626
- }
3627
- async _uninstallAsync(metadata, isNestedStateChange) {
3628
- const installedExtension = this._installedExtensions.get(metadata.name);
3598
+ else {
3599
+ // Notify parent as slider changes
3600
+ props.onChange(newValue);
3601
+ }
3602
+ };
3603
+ const handleSliderPointerDown = () => {
3604
+ isDraggingRef.current = true;
3605
+ };
3606
+ const handleSliderPointerUp = () => {
3607
+ if (props.notifyOnlyOnRelease && isDraggingRef.current && pendingValueRef.current !== undefined) {
3608
+ props.onChange(pendingValueRef.current);
3609
+ pendingValueRef.current = undefined;
3610
+ }
3611
+ isDraggingRef.current = false;
3612
+ };
3613
+ const handleInputChange = (value) => {
3614
+ setValue(value);
3615
+ props.onChange(value); // Input always updates immediately
3616
+ };
3617
+ return (jsxs("div", { className: classes.container, children: [infoLabel && jsx(InfoLabel, { ...infoLabel, htmlFor: "syncedSlider" }), jsxs("div", { id: "syncedSlider", className: classes.syncedSlider, children: [props.min !== undefined && props.max !== undefined && (jsx(Slider, { ...passthroughProps, className: classes.slider, size: size, min: min / step, max: max / step, step: undefined, value: value / step, onChange: handleSliderChange, onPointerDown: handleSliderPointerDown, onPointerUp: handleSliderPointerUp })), jsx(SpinButton, { ...passthroughProps, value: value, onChange: handleInputChange, step: props.step })] })] }));
3618
+ };
3619
+
3620
+ /**
3621
+ * Renders a simple wrapper around the SyncedSliderInput
3622
+ * @param props
3623
+ * @returns
3624
+ */
3625
+ const SyncedSliderPropertyLine = forwardRef((props, ref) => {
3626
+ SyncedSliderPropertyLine.displayName = "SyncedSliderPropertyLine";
3627
+ const { label, description, ...sliderProps } = props;
3628
+ return (jsx(PropertyLine, { ref: ref, ...props, children: jsx(SyncedSliderInput, { ...sliderProps }) }));
3629
+ });
3630
+
3631
+ const TextInput = (props) => {
3632
+ TextInput.displayName = "TextInput";
3633
+ const classes = useInputStyles$1();
3634
+ const [value, setValue] = useState(props.value);
3635
+ const lastCommittedValue = useRef(props.value);
3636
+ const { size } = useContext(ToolContext);
3637
+ useEffect(() => {
3638
+ if (props.value !== lastCommittedValue.current) {
3639
+ setValue(props.value); // Update local state when props.value changes
3640
+ lastCommittedValue.current = props.value;
3641
+ }
3642
+ }, [props.value]);
3643
+ const validateValue = (val) => {
3644
+ const failsValidator = props.validator && !props.validator(val);
3645
+ return !failsValidator;
3646
+ };
3647
+ const tryCommitValue = (currVal) => {
3648
+ // Only commit if valid and different from last committed value
3649
+ if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
3650
+ lastCommittedValue.current = currVal;
3651
+ props.onChange(currVal);
3652
+ }
3653
+ };
3654
+ const handleChange = (event, data) => {
3655
+ event.stopPropagation();
3656
+ setValue(data.value);
3657
+ tryCommitValue(data.value);
3658
+ };
3659
+ const handleKeyUp = (event) => {
3660
+ event.stopPropagation();
3661
+ setValue(event.currentTarget.value);
3662
+ tryCommitValue(event.currentTarget.value);
3663
+ };
3664
+ const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
3665
+ const id = useId("input-button");
3666
+ return (jsxs("div", { className: classes.container, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(Input, { ...props, input: { className: classes.inputSlot }, id: id, size: size, value: value, onChange: handleChange, onKeyUp: handleKeyUp, onKeyDown: HandleKeyDown, onBlur: HandleOnBlur, className: mergedClassName })] }));
3667
+ };
3668
+
3669
+ const useDropdownStyles = makeStyles({
3670
+ dropdown: {
3671
+ minWidth: 0,
3672
+ width: "100%",
3673
+ },
3674
+ container: {
3675
+ display: "flex",
3676
+ flexDirection: "column",
3677
+ justifyContent: "center", // align items vertically
3678
+ },
3679
+ dropdownText: { textAlign: "end", textOverflow: "ellipsis", whiteSpace: "nowrap", overflowX: "hidden" },
3680
+ });
3681
+ /**
3682
+ * Renders a fluent UI dropdown component for the options passed in, and an additional 'Not Defined' option if null is set to true
3683
+ * This component can handle both null and undefined values
3684
+ * @param props
3685
+ * @returns dropdown component
3686
+ */
3687
+ const Dropdown = (props) => {
3688
+ Dropdown.displayName = "Dropdown";
3689
+ const classes = useDropdownStyles();
3690
+ const { options, value } = props;
3691
+ const [defaultVal, setDefaultVal] = useState(props.value);
3692
+ const { size } = useContext(ToolContext);
3693
+ useEffect(() => {
3694
+ setDefaultVal(value);
3695
+ }, [props.value]);
3696
+ const id = useId("dropdown");
3697
+ const mergedClassName = mergeClasses(classes.container, props.className);
3698
+ const optionLabel = options.find((o) => o.value === defaultVal)?.label;
3699
+ return (jsxs("div", { className: mergedClassName, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(Dropdown$1, { id: id, disabled: props.disabled, size: size, className: classes.dropdown, button: jsx("span", { className: classes.dropdownText, children: optionLabel }), onOptionSelect: (evt, data) => {
3700
+ const value = typeof props.value === "number" ? Number(data.optionValue) : data.optionValue;
3701
+ if (value !== undefined) {
3702
+ setDefaultVal(value);
3703
+ props.onChange(value);
3704
+ }
3705
+ }, selectedOptions: [defaultVal.toString()], value: optionLabel, children: options.map((option) => (jsx(Option, { value: option.value.toString(), disabled: false, children: option.label }, option.label))) })] }));
3706
+ };
3707
+ const NumberDropdown = Dropdown;
3708
+ const StringDropdown = Dropdown;
3709
+
3710
+ const useColorPickerStyles = makeStyles({
3711
+ container: {
3712
+ width: "350px",
3713
+ display: "flex", // becomes a flexbox
3714
+ flexDirection: "column", // with children in a column
3715
+ alignItems: "center", // centers children horizontally
3716
+ justifyContent: "center", // centers children vertically (if height is set)
3717
+ gap: tokens.spacingVerticalM,
3718
+ overflow: "visible",
3719
+ },
3720
+ row: {
3721
+ flex: 1, // is a row in the container's flex column
3722
+ display: "flex", // becomes its own flexbox
3723
+ flexDirection: "row", // with children in a row
3724
+ gap: tokens.spacingHorizontalXL,
3725
+ alignItems: "center", // align items vertically
3726
+ width: "100%",
3727
+ },
3728
+ colorPicker: {
3729
+ flex: 1,
3730
+ width: "350px",
3731
+ height: "350px",
3732
+ },
3733
+ previewColor: {
3734
+ width: "60px",
3735
+ height: "60px",
3736
+ borderRadius: tokens.borderRadiusMedium, // 4px?
3737
+ border: `${tokens.spacingVerticalXXS} solid ${tokens.colorNeutralShadowKeyLighter}`,
3738
+ "@media (forced-colors: active)": {
3739
+ forcedColorAdjust: "none", // ensures elmement maintains color in high constrast mode
3740
+ },
3741
+ },
3742
+ inputRow: {
3743
+ display: "flex",
3744
+ flexDirection: "row",
3745
+ flex: 1, // grow and fill available space
3746
+ justifyContent: "center",
3747
+ gap: "10px",
3748
+ width: "100%",
3749
+ },
3750
+ inputField: {
3751
+ flex: 1, // grow and fill available space
3752
+ width: "auto",
3753
+ minWidth: 0,
3754
+ gap: tokens.spacingVerticalSNudge, // 6px
3755
+ },
3756
+ });
3757
+ const ColorPickerPopup = (props) => {
3758
+ ColorPickerPopup.displayName = "ColorPickerPopup";
3759
+ const classes = useColorPickerStyles();
3760
+ const [color, setColor] = useState(props.value);
3761
+ const [popoverOpen, setPopoverOpen] = useState(false);
3762
+ const [isLinear, setIsLinear] = useState(props.isLinearMode ?? false);
3763
+ const [isFloat, setFloat] = useState(false);
3764
+ const { size } = useContext(ToolContext);
3765
+ useEffect(() => {
3766
+ setColor(props.value); // Ensures the trigger color updates when props.value changes
3767
+ }, [props.value]);
3768
+ const handleColorPickerChange = (_, data) => {
3769
+ let color = Color3.FromHSV(data.color.h, data.color.s, data.color.v);
3770
+ if (props.value instanceof Color4) {
3771
+ color = Color4.FromColor3(color, data.color.a ?? 1);
3772
+ }
3773
+ handleChange(color);
3774
+ };
3775
+ const handleChange = (newColor) => {
3776
+ setColor(newColor);
3777
+ props.onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
3778
+ };
3779
+ return (jsxs(Popover, { positioning: {
3780
+ align: "start",
3781
+ overflowBoundary: document.body,
3782
+ autoSize: true,
3783
+ }, open: popoverOpen, trapFocus: true, onOpenChange: (_, data) => setPopoverOpen(data.open), children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: jsx(ColorSwatch, { borderColor: tokens.colorNeutralShadowKeyDarker, size: size === "small" ? "extra-small" : "small", shape: "rounded", color: color.toHexString(), value: color.toHexString().slice(1) }) }), jsx(PopoverSurface, { children: jsxs("div", { className: classes.container, children: [jsxs(ColorPicker, { className: classes.colorPicker, color: rgbaToHsv(color), onColorChange: handleColorPickerChange, children: [jsx(ColorArea, { inputX: { "aria-label": "Saturation" }, inputY: { "aria-label": "Brightness" } }), jsx(ColorSlider, { "aria-label": "Hue" }), color instanceof Color4 && jsx(AlphaSlider, { "aria-label": "Alpha" })] }), jsxs("div", { className: classes.row, children: [jsx("div", { className: classes.previewColor, style: { backgroundColor: color.toHexString() } }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
3784
+ label: "Color Space",
3785
+ info: jsx(Body1, { children: "Today this is not mutable as the color space is determined by the entity. Soon we will allow swapping" }),
3786
+ }, options: [
3787
+ { label: "Gamma", value: 0 },
3788
+ { label: "Linear", value: 1 },
3789
+ ], disabled: true, value: isLinear ? 1 : 0, onChange: (val) => setIsLinear(val === 1) }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
3790
+ label: "Data Type",
3791
+ info: jsx(Body1, { children: "We will introduce this functionality soon!" }),
3792
+ }, options: [
3793
+ { label: "Int", value: 0 },
3794
+ { label: "Float", value: 1 },
3795
+ ], disabled: true, value: isFloat ? 1 : 0, onChange: (val) => setFloat(val === 1) })] }), jsxs("div", { className: classes.inputRow, children: [jsx(InputRgbField, { title: "Red", value: color, rgbKey: "r", onChange: handleChange }), jsx(InputRgbField, { title: "Green", value: color, rgbKey: "g", onChange: handleChange }), jsx(InputRgbField, { title: "Blue", value: color, rgbKey: "b", onChange: handleChange }), jsx(InputAlphaField, { color: color, onChange: handleChange })] }), jsxs("div", { className: classes.inputRow, children: [jsx(InputHsvField, { title: "Hue", value: color, hsvKey: "h", max: 360, onChange: handleChange }), jsx(InputHsvField, { title: "Saturation", value: color, hsvKey: "s", max: 100, scale: 100, onChange: handleChange }), jsx(InputHsvField, { title: "Value", value: color, hsvKey: "v", max: 100, scale: 100, onChange: handleChange })] }), jsx("div", { className: classes.inputRow, children: jsx(InputHexField, { title: "Hexadecimal", linearHex: isLinear, isLinearMode: isLinear, value: color, onChange: handleChange }) })] }) })] }));
3796
+ };
3797
+ /**
3798
+ * Component which displays the passed in color's HEX value, either in linearSpace (if linearHex is true) or in gamma space
3799
+ * When the hex color is changed by user, component calculates the new Color3/4 value and calls onChange
3800
+ *
3801
+ * Component uses the isLinearMode boolean to display an informative label regarding linear / gamma space
3802
+ * @param props - The properties for the InputHexField component.
3803
+ * @returns
3804
+ */
3805
+ const InputHexField = (props) => {
3806
+ const classes = useColorPickerStyles();
3807
+ const { title, value, onChange, linearHex, isLinearMode } = props;
3808
+ return (jsx(TextInput, { disabled: linearHex ? !isLinearMode : false, className: classes.inputField, value: linearHex ? value.toLinearSpace().toHexString() : value.toHexString(), validator: ValidateColorHex, onChange: (val) => (linearHex ? onChange(Color3.FromHexString(val).toGammaSpace()) : onChange(Color3.FromHexString(val))), infoLabel: title
3809
+ ? {
3810
+ label: title,
3811
+ // If not representing a linearHex, no info is needed.
3812
+ info: !props.linearHex ? undefined : !isLinearMode ? ( // If representing a linear hex but we are in gammaMode, simple message explaining why linearHex is disabled
3813
+ jsx(Fragment, { children: " This color picker is attached to an entity whose color is stored in gamma space, so we are showing linear hex in disabled view " })) : (
3814
+ // If representing a linear hex and we are in linearMode, give information about how to use these hex values
3815
+ jsxs(Fragment, { children: ["This color picker is attached to an entity whose color is stored in linear space (ex: PBR Material), and Babylon converts the color to gamma space before rendering on screen because the human eye is best at processing colors in gamma space. We thus also want to display the color picker in gamma space so that the color chosen here will match the color seen in your entity.", jsx("br", {}), "If you want to copy/paste the HEX into your code, you can either use", jsx(Body1Strong, { children: "Color3.FromHexString(LINEAR_HEX)" }), jsx("br", {}), "or", jsx("br", {}), jsx(Body1Strong, { children: "Color3.FromHexString(GAMMA_HEX).toLinearSpace()" }), jsx("br", {}), jsx("br", {}), jsx(Link, { url: "https://doc.babylonjs.com/preparingArtForBabylon/controllingColorSpace/", value: "Read more in our docs!" })] })),
3816
+ }
3817
+ : undefined }));
3818
+ };
3819
+ const InputRgbField = (props) => {
3820
+ const { value, onChange, title, rgbKey } = props;
3821
+ const classes = useColorPickerStyles();
3822
+ const handleChange = useCallback((val) => {
3823
+ const newColor = value.clone();
3824
+ newColor[rgbKey] = val / 255.0; // Convert to 0-1 range
3825
+ onChange(newColor);
3826
+ }, [value, onChange, rgbKey]);
3827
+ return (jsx(SpinButton, { title: title, infoLabel: title ? { label: title } : undefined, className: classes.inputField, min: 0, max: 255, value: Math.round(value[rgbKey] * 255), forceInt: true, onChange: handleChange }));
3828
+ };
3829
+ function rgbaToHsv(color) {
3830
+ const c = new Color3(color.r, color.g, color.b);
3831
+ const hsv = c.toHSV();
3832
+ return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a };
3833
+ }
3834
+ /**
3835
+ * In the HSV (Hue, Saturation, Value) color model, Hue (H) ranges from 0 to 360 degrees, representing the color's position on the color wheel.
3836
+ * Saturation (S) ranges from 0 to 100%, indicating the intensity or purity of the color, with 0 being shades of gray and 100 being a fully saturated color.
3837
+ * Value (V) ranges from 0 to 100%, representing the brightness of the color, with 0 being black and 100 being the brightest.
3838
+ * @param props - The properties for the InputHsvField component.
3839
+ */
3840
+ const InputHsvField = (props) => {
3841
+ const { value, title, hsvKey, max, onChange, scale = 1 } = props;
3842
+ const classes = useColorPickerStyles();
3843
+ const handleChange = useCallback((val) => {
3844
+ // Convert current color to HSV, update the new hsv value, then call onChange prop
3845
+ const hsv = rgbaToHsv(value);
3846
+ hsv[hsvKey] = val / scale;
3847
+ let newColor = Color3.FromHSV(hsv.h, hsv.s, hsv.v);
3848
+ if (value instanceof Color4) {
3849
+ newColor = Color4.FromColor3(newColor, value.a ?? 1);
3850
+ }
3851
+ props.onChange(newColor);
3852
+ }, [value, onChange, hsvKey, scale]);
3853
+ return (jsx(SpinButton, { infoLabel: title ? { label: title } : undefined, title: title, className: classes.inputField, min: 0, max: max, value: Math.round(rgbaToHsv(value)[hsvKey] * scale), forceInt: true, onChange: handleChange }));
3854
+ };
3855
+ /**
3856
+ * Displays the alpha value of a color, either in the disabled state (if color is Color3) or as a spin button (if color is Color4).
3857
+ * @param props
3858
+ * @returns
3859
+ */
3860
+ const InputAlphaField = (props) => {
3861
+ const classes = useColorPickerStyles();
3862
+ const { color, onChange } = props;
3863
+ const handleChange = useCallback((value) => {
3864
+ if (Number.isNaN(value) || value < 0 || value > 1) {
3865
+ return;
3866
+ }
3867
+ if (color instanceof Color4) {
3868
+ const newColor = color.clone();
3869
+ newColor.a = value;
3870
+ return newColor;
3871
+ }
3872
+ else {
3873
+ return Color4.FromColor3(color, value);
3874
+ }
3875
+ }, [onChange]);
3876
+ return (jsx(SpinButton, { disabled: color instanceof Color3, min: 0, max: 1, className: classes.inputField, value: color instanceof Color3 ? 1 : color.a, step: 0.01, onChange: handleChange, infoLabel: {
3877
+ label: "Alpha",
3878
+ info: color instanceof Color3 ? (jsx(Fragment, { children: "Because this color picker is representing a Color3, we do not permit modifying alpha from the color picker. You can however modify the entity's alpha property directly, either in code via entity.alpha OR via inspector's transparency section." })) : undefined,
3879
+ } }));
3880
+ };
3881
+
3882
+ /**
3883
+ * Reusable component which renders a color property line containing a label, colorPicker popout, and expandable RGBA values
3884
+ * The expandable RGBA values are synced sliders that allow the user to modify the color's RGBA values directly
3885
+ * @param props - PropertyLine props, replacing children with a color object so that we can properly display the color
3886
+ * @returns Component wrapping a colorPicker component with a property line
3887
+ */
3888
+ const ColorPropertyLine = forwardRef((props, ref) => {
3889
+ ColorPropertyLine.displayName = "ColorPropertyLine";
3890
+ const [color, setColor] = useState(props.value);
3891
+ useEffect(() => {
3892
+ setColor(props.value);
3893
+ }, [props.value]);
3894
+ const onSliderChange = (value, key) => {
3895
+ let newColor;
3896
+ if (key === "a") {
3897
+ newColor = Color4.FromColor3(color, value);
3898
+ }
3899
+ else {
3900
+ newColor = color.clone();
3901
+ newColor[key] = value / 255;
3902
+ }
3903
+ setColor(newColor); // Create a new object to trigger re-render
3904
+ props.onChange(newColor);
3905
+ };
3906
+ const onColorPickerChange = (newColor) => {
3907
+ setColor(newColor);
3908
+ props.onChange(newColor);
3909
+ };
3910
+ return (jsx(PropertyLine, { ref: ref, ...props, expandedContent: jsxs(Fragment, { children: [jsx(SyncedSliderPropertyLine, { label: "R", value: color.r * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "r") }), jsx(SyncedSliderPropertyLine, { label: "G", value: color.g * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "g") }), jsx(SyncedSliderPropertyLine, { label: "B", value: color.b * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "b") }), color instanceof Color4 && jsx(SyncedSliderPropertyLine, { label: "A", value: color.a, min: 0, max: 1, step: 0.01, onChange: (value) => onSliderChange(value, "a") })] }), children: jsx(ColorPickerPopup, { ...props, onChange: onColorPickerChange, value: color }) }));
3911
+ });
3912
+ const Color3PropertyLine = ColorPropertyLine;
3913
+ const Color4PropertyLine = ColorPropertyLine;
3914
+
3915
+ const useStyles$9 = makeStyles({
3916
+ dropdown: {
3917
+ ...UniformWidthStyling,
3918
+ },
3919
+ });
3920
+ /**
3921
+ * Wraps a dropdown in a property line
3922
+ * @param props - PropertyLineProps and DropdownProps
3923
+ * @returns property-line wrapped dropdown
3924
+ */
3925
+ const DropdownPropertyLine = forwardRef((props, ref) => {
3926
+ DropdownPropertyLine.displayName = "DropdownPropertyLine";
3927
+ const classes = useStyles$9();
3928
+ return (jsx(PropertyLine, { ...props, ref: ref, children: jsx(Dropdown, { ...props, className: classes.dropdown }) }));
3929
+ });
3930
+ /**
3931
+ * Dropdown component for number values.
3932
+ */
3933
+ const NumberDropdownPropertyLine = DropdownPropertyLine;
3934
+ /**
3935
+ * Dropdown component for string values
3936
+ */
3937
+ const StringDropdownPropertyLine = DropdownPropertyLine;
3938
+
3939
+ /**
3940
+ * Wraps a text input in a property line
3941
+ * @param props - PropertyLineProps and InputProps
3942
+ * @returns property-line wrapped input component
3943
+ */
3944
+ const TextInputPropertyLine = (props) => {
3945
+ TextInputPropertyLine.displayName = "TextInputPropertyLine";
3946
+ return (jsx(PropertyLine, { ...props, children: jsx(TextInput, { ...props }) }));
3947
+ };
3948
+ /**
3949
+ * Wraps a number input in a property line
3950
+ * To force integer values, use forceInt param (this is distinct from the 'step' param, which will still allow submitting an integer value. forceInt will not)
3951
+ * @param props - PropertyLineProps and InputProps
3952
+ * @returns property-line wrapped input component
3953
+ */
3954
+ const NumberInputPropertyLine = (props) => {
3955
+ NumberInputPropertyLine.displayName = "NumberInputPropertyLine";
3956
+ return (jsx(PropertyLine, { ...props, children: jsx(SpinButton, { ...props }) }));
3957
+ };
3958
+
3959
+ const HasZ = (vector) => !(vector instanceof Vector2);
3960
+ const HasW = (vector) => vector instanceof Vector4 || vector instanceof Quaternion;
3961
+ /**
3962
+ * Reusable component which renders a vector property line containing a label, vector value, and expandable XYZW values
3963
+ * The expanded section contains a slider/input box for each component of the vector (x, y, z, w)
3964
+ * @param props
3965
+ * @returns
3966
+ */
3967
+ const TensorPropertyLine = (props) => {
3968
+ TensorPropertyLine.displayName = "TensorPropertyLine";
3969
+ const converted = (val) => (props.valueConverter ? props.valueConverter.from(val) : val);
3970
+ const formatted = (val) => converted(val).toFixed(props.step !== undefined ? Math.max(0, CalculatePrecision(props.step)) : 2);
3971
+ const [vector, setVector] = useState(props.value);
3972
+ const { min, max } = props;
3973
+ const onChange = (val, key) => {
3974
+ const value = props.valueConverter ? props.valueConverter.to(val) : val;
3975
+ const newVector = vector.clone();
3976
+ newVector[key] = value; // The syncedSlider for 'w' is only rendered when vector is a Vector4, so this is safe
3977
+ setVector(newVector);
3978
+ props.onChange(newVector);
3979
+ };
3980
+ useEffect(() => {
3981
+ setVector(props.value);
3982
+ }, [props.value, props.expandedContent]);
3983
+ return (jsx(PropertyLine, { ...props, onCopy: () => `new ${props.value.getClassName()}(${vector.x},${vector.y}${HasZ(vector) ? `,${vector.z}` : ""}${HasW(vector) ? `,${vector.w}` : ""})`, expandedContent: jsxs(Fragment, { children: [jsx(SyncedSliderPropertyLine, { label: "X", value: converted(vector.x), min: min, max: max, onChange: (val) => onChange(val, "x"), unit: props.unit, step: props.step }), jsx(SyncedSliderPropertyLine, { label: "Y", value: converted(vector.y), min: min, max: max, onChange: (val) => onChange(val, "y"), unit: props.unit, step: props.step }), HasZ(vector) && (jsx(SyncedSliderPropertyLine, { label: "Z", value: converted(vector.z), min: min, max: max, onChange: (val) => onChange(val, "z"), unit: props.unit, step: props.step })), HasW(vector) && (jsx(SyncedSliderPropertyLine, { label: "W", value: converted(vector.w), min: min, max: max, onChange: (val) => onChange(val, "w"), unit: props.unit, step: props.step }))] }), children: jsx(Body1, { children: `[${formatted(props.value.x)}, ${formatted(props.value.y)}${HasZ(props.value) ? `, ${formatted(props.value.z)}` : ""}${HasW(props.value) ? `, ${formatted(props.value.w)}` : ""}]` }) }));
3984
+ };
3985
+ const ToDegreesConverter = { from: Tools.ToDegrees, to: Tools.ToRadians };
3986
+ const RotationVectorPropertyLine = (props) => {
3987
+ RotationVectorPropertyLine.displayName = "RotationVectorPropertyLine";
3988
+ const min = props.useDegrees ? 0 : undefined;
3989
+ const max = props.useDegrees ? 360 : undefined;
3990
+ return (jsx(Vector3PropertyLine, { ...props, unit: props.useDegrees ? "deg" : "rad", valueConverter: props.useDegrees ? ToDegreesConverter : undefined, min: min, max: max, step: 0.001 }));
3991
+ };
3992
+ const QuaternionPropertyLineInternal = TensorPropertyLine;
3993
+ const QuaternionPropertyLine = (props) => {
3994
+ QuaternionPropertyLine.displayName = "QuaternionPropertyLine";
3995
+ const min = props.useDegrees ? 0 : undefined;
3996
+ const max = props.useDegrees ? 360 : undefined;
3997
+ const [quat, setQuat] = useState(props.value);
3998
+ useEffect(() => {
3999
+ setQuat(props.value);
4000
+ }, [props.value]);
4001
+ // Extract only the properties that exist on QuaternionPropertyLineProps
4002
+ const { useDegrees, ...restProps } = props;
4003
+ const onQuatChange = (val) => {
4004
+ setQuat(val);
4005
+ props.onChange(val);
4006
+ };
4007
+ const onEulerChange = (val) => {
4008
+ const quat = Quaternion.FromEulerAngles(val.x, val.y, val.z);
4009
+ onQuatChange(quat);
4010
+ };
4011
+ return useDegrees ? (jsx(Vector3PropertyLine, { ...restProps, nullable: false, ignoreNullable: false, value: quat.toEulerAngles(), valueConverter: ToDegreesConverter, min: min, max: max, onChange: onEulerChange, unit: "deg" })) : (jsx(QuaternionPropertyLineInternal, { ...props, nullable: false, value: quat, min: min, max: max, onChange: onQuatChange }));
4012
+ };
4013
+ const Vector2PropertyLine = TensorPropertyLine;
4014
+ const Vector3PropertyLine = TensorPropertyLine;
4015
+ const Vector4PropertyLine = TensorPropertyLine;
4016
+
4017
+ function IsInspectableObject(entity) {
4018
+ return !!entity.inspectableCustomProperties;
4019
+ }
4020
+ const LegacyInspectableObjectPropertiesServiceDefinition = {
4021
+ friendlyName: "Additional Nodes",
4022
+ consumes: [PropertiesServiceIdentity],
4023
+ factory: (propertiesService) => {
4024
+ const propertiesSectionRegistration = propertiesService.addSection({
4025
+ identity: "Custom",
4026
+ order: Number.MAX_SAFE_INTEGER,
4027
+ });
4028
+ const propertiesContentRegistration = propertiesService.addSectionContent({
4029
+ key: "Additional Nodes Properties",
4030
+ predicate: (entity) => IsInspectableObject(entity),
4031
+ content: [
4032
+ {
4033
+ section: "Custom",
4034
+ component: ({ context }) => {
4035
+ return (jsx(Fragment, { children: (context.inspectableCustomProperties ?? []).map((prop) => {
4036
+ const commonProps = {
4037
+ target: context,
4038
+ propertyKey: prop.propertyName,
4039
+ label: prop.label,
4040
+ ignoreNullable: true,
4041
+ defaultValue: undefined,
4042
+ };
4043
+ switch (prop.type) {
4044
+ case 0 /* InspectableType.Checkbox */:
4045
+ return jsx(BoundProperty, { ...commonProps, component: SwitchPropertyLine }, prop.propertyName);
4046
+ case 1 /* InspectableType.Slider */:
4047
+ return (jsx(BoundProperty, { ...commonProps, min: prop.min, max: prop.max, step: prop.step, component: SyncedSliderPropertyLine }, prop.propertyName));
4048
+ case 2 /* InspectableType.Vector3 */:
4049
+ return jsx(BoundProperty, { ...commonProps, component: Vector3PropertyLine }, prop.propertyName);
4050
+ case 3 /* InspectableType.Quaternion */:
4051
+ return jsx(BoundProperty, { ...commonProps, component: QuaternionPropertyLine }, prop.propertyName);
4052
+ case 4 /* InspectableType.Color3 */:
4053
+ return jsx(BoundProperty, { ...commonProps, component: Color3PropertyLine }, prop.propertyName);
4054
+ case 5 /* InspectableType.String */:
4055
+ return jsx(BoundProperty, { ...commonProps, component: TextInputPropertyLine }, prop.propertyName);
4056
+ case 6 /* InspectableType.Button */:
4057
+ return jsx(ButtonLine, { label: prop.label, onClick: () => prop.callback?.() }, prop.propertyName);
4058
+ case 7 /* InspectableType.Options */:
4059
+ return jsx(BoundProperty, { ...commonProps, component: DropdownPropertyLine, options: prop.options ?? [] }, prop.propertyName);
4060
+ case 8 /* InspectableType.Tab */:
4061
+ return jsx(BoundProperty, { ...commonProps, component: TextPropertyLine }, prop.propertyName);
4062
+ case 9 /* InspectableType.FileButton */:
4063
+ return (jsx(FileUploadLine, { label: prop.label, accept: prop.accept ?? "", onClick: (files) => {
4064
+ if (files.length > 0 && prop.fileCallback) {
4065
+ prop.fileCallback(files[0]);
4066
+ }
4067
+ } }, prop.propertyName));
4068
+ case 10 /* InspectableType.Vector2 */:
4069
+ return jsx(BoundProperty, { ...commonProps, component: Vector2PropertyLine }, prop.propertyName);
4070
+ }
4071
+ }) }));
4072
+ },
4073
+ },
4074
+ ],
4075
+ });
4076
+ return {
4077
+ dispose: () => {
4078
+ propertiesSectionRegistration.dispose();
4079
+ propertiesContentRegistration.dispose();
4080
+ },
4081
+ };
4082
+ },
4083
+ };
4084
+
4085
+ const ExtensionManagerContext = createContext(undefined);
4086
+ function useExtensionManager() {
4087
+ return useContext(ExtensionManagerContext)?.extensionManager;
4088
+ }
4089
+
4090
+ const InstalledExtensionsKey = "Babylon/Extensions/InstalledExtensions";
4091
+ const ExtensionInstalledKeyPrefix = "Babylon/Extensions/IsExtensionInstalled";
4092
+ function GetExtensionInstalledKey(name) {
4093
+ return `${ExtensionInstalledKeyPrefix}/${name}`;
4094
+ }
4095
+ function GetExtensionIdentity(feed, name) {
4096
+ return `${feed}|${name}`;
4097
+ }
4098
+ /**
4099
+ * Manages the installation, uninstallation, enabling, and disabling of extensions.
4100
+ */
4101
+ class ExtensionManager {
4102
+ constructor(_serviceContainer, _feeds, _onInstallFailed) {
4103
+ this._serviceContainer = _serviceContainer;
4104
+ this._feeds = _feeds;
4105
+ this._onInstallFailed = _onInstallFailed;
4106
+ this._installedExtensions = new Map();
4107
+ this._stateChangedHandlers = new Map();
4108
+ }
4109
+ /**
4110
+ * Creates a new instance of the ExtensionManager.
4111
+ * This will automatically rehydrate previously installed and enabled extensions.
4112
+ * @param serviceContainer The service container to use.
4113
+ * @param feeds The extension feeds to include.
4114
+ * @param onInstallFailed A callback that is called when an extension installation fails.
4115
+ * @returns A promise that resolves to the new instance of the ExtensionManager.
4116
+ */
4117
+ static async CreateAsync(serviceContainer, feeds, onInstallFailed) {
4118
+ const extensionManager = new ExtensionManager(serviceContainer, feeds, onInstallFailed);
4119
+ // Rehydrate installed extensions.
4120
+ const installedExtensionNames = JSON.parse(localStorage.getItem(InstalledExtensionsKey) ?? "[]");
4121
+ for (const installedExtensionName of installedExtensionNames) {
4122
+ const installedExtensionRaw = localStorage.getItem(GetExtensionInstalledKey(installedExtensionName));
4123
+ if (installedExtensionRaw) {
4124
+ const installedExtensionData = JSON.parse(installedExtensionRaw);
4125
+ const feed = feeds.find((feed) => feed.name === installedExtensionData.feed);
4126
+ if (feed) {
4127
+ const installedExtension = extensionManager._createInstalledExtension(installedExtensionData.metadata, feed);
4128
+ extensionManager._installedExtensions.set(installedExtension.metadata.name, installedExtension);
4129
+ }
4130
+ }
4131
+ }
4132
+ // Load installed and enabled extensions.
4133
+ const enablePromises = [];
4134
+ for (const extension of extensionManager._installedExtensions.values()) {
4135
+ enablePromises.push((async () => {
4136
+ try {
4137
+ await extensionManager._enableAsync(extension.metadata, false, false);
4138
+ }
4139
+ catch {
4140
+ // If enabling the extension fails, uninstall it. The extension install fail callback will still be called,
4141
+ // so the owner of the ExtensionManager instance can decide what to do with the error.
4142
+ await extensionManager._uninstallAsync(extension.metadata, false);
4143
+ }
4144
+ })());
4145
+ }
4146
+ await Promise.all(enablePromises);
4147
+ return extensionManager;
4148
+ }
4149
+ /**
4150
+ * Gets the names of the feeds that are included in the extension manager.
4151
+ * @returns The names of the feeds.
4152
+ */
4153
+ get feedNames() {
4154
+ return this._feeds.map((feed) => feed.name);
4155
+ }
4156
+ /**
4157
+ * Queries the extension manager for extensions.
4158
+ * @param filter The filter to apply to the query.
4159
+ * @param feeds The feeds to include in the query.
4160
+ * @param installedOnly Whether to only include installed extensions.
4161
+ * @returns A promise that resolves to the extension query.
4162
+ */
4163
+ async queryExtensionsAsync(filter = "", feeds = this.feedNames, installedOnly = false) {
4164
+ if (installedOnly) {
4165
+ const installedExtensions = Array.from(this._installedExtensions.values()).filter((installedExtension) => feeds.includes(installedExtension.feed.name));
4166
+ return {
4167
+ totalCount: installedExtensions.length,
4168
+ getExtensionsAsync: async (index, count) => {
4169
+ return installedExtensions.slice(index, index + count).map((installedExtension) => this._createExtension(installedExtension.metadata, installedExtension.feed));
4170
+ },
4171
+ };
4172
+ }
4173
+ const queries = await Promise.all(this._feeds.filter((feed) => feeds.includes(feed.name)).map(async (feed) => Object.assign(await feed.queryExtensionsAsync(filter), { feed })));
4174
+ const totalCount = queries.reduce((sum, query) => sum + query.totalCount, 0);
4175
+ return {
4176
+ totalCount,
4177
+ getExtensionsAsync: async (index, count) => {
4178
+ const extensions = new Array();
4179
+ let remaining = count;
4180
+ for (const query of queries) {
4181
+ if (remaining <= 0) {
4182
+ break;
4183
+ }
4184
+ if (index >= query.totalCount) {
4185
+ index -= query.totalCount;
4186
+ continue;
4187
+ }
4188
+ // This is intentionally sequential as we are querying for results until the count of results is met.
4189
+ // eslint-disable-next-line no-await-in-loop
4190
+ const metadataSlice = await query.getExtensionMetadataAsync(index, remaining);
4191
+ extensions.push(...metadataSlice.map((metadata) => this._createExtension(metadata, query.feed)));
4192
+ remaining -= metadataSlice.length;
4193
+ index = 0;
4194
+ }
4195
+ return extensions;
4196
+ },
4197
+ };
4198
+ }
4199
+ /**
4200
+ * Disposes the extension manager.
4201
+ */
4202
+ dispose() {
4203
+ for (const installedExtension of this._installedExtensions.values()) {
4204
+ // eslint-disable-next-line github/no-then
4205
+ this._disableAsync(installedExtension.metadata, false).catch((error) => {
4206
+ Logger.Warn(`Failed to disable extension ${installedExtension.metadata.name}: ${error}`);
4207
+ });
4208
+ }
4209
+ this._stateChangedHandlers.clear();
4210
+ }
4211
+ async _installAsync(metadata, feed, isNestedStateChange) {
4212
+ let installedExtension = this._installedExtensions.get(metadata.name);
4213
+ if (!installedExtension) {
4214
+ installedExtension = this._createInstalledExtension(metadata, feed);
4215
+ installedExtension.isStateChanging = true;
4216
+ this._installedExtensions.set(metadata.name, installedExtension);
4217
+ try {
4218
+ // Enable the extension.
4219
+ await this._enableAsync(metadata, true, true);
4220
+ }
4221
+ catch (error) {
4222
+ this._installedExtensions.delete(metadata.name);
4223
+ throw error;
4224
+ }
4225
+ finally {
4226
+ !isNestedStateChange && (installedExtension.isStateChanging = false);
4227
+ }
4228
+ // Mark the extension as being installed.
4229
+ localStorage.setItem(GetExtensionInstalledKey(GetExtensionIdentity(feed.name, metadata.name)), JSON.stringify({
4230
+ feed: feed.name,
4231
+ metadata,
4232
+ }));
4233
+ localStorage.setItem(InstalledExtensionsKey, JSON.stringify(Array.from(this._installedExtensions.values()).map((extension) => GetExtensionIdentity(extension.feed.name, extension.metadata.name))));
4234
+ }
4235
+ return installedExtension;
4236
+ }
4237
+ async _uninstallAsync(metadata, isNestedStateChange) {
4238
+ const installedExtension = this._installedExtensions.get(metadata.name);
3629
4239
  if (installedExtension && (isNestedStateChange || !installedExtension.isStateChanging)) {
3630
4240
  try {
3631
4241
  !isNestedStateChange && (installedExtension.isStateChanging = true);
@@ -3888,7 +4498,7 @@ class ServiceContainer {
3888
4498
  }
3889
4499
  }
3890
4500
 
3891
- const useStyles$9 = makeStyles({
4501
+ const useStyles$8 = makeStyles({
3892
4502
  themeButton: {
3893
4503
  margin: 0,
3894
4504
  },
@@ -3907,7 +4517,7 @@ const ThemeSelectorServiceDefinition = {
3907
4517
  suppressTeachingMoment: true,
3908
4518
  order: -300,
3909
4519
  component: () => {
3910
- const classes = useStyles$9();
4520
+ const classes = useStyles$8();
3911
4521
  const { isDarkMode, themeMode, setThemeMode } = useThemeMode();
3912
4522
  const onSelectedThemeChange = useCallback((e, data) => {
3913
4523
  setThemeMode(data.checkedItems.includes("System") ? "system" : data.checkedItems[0].toLocaleLowerCase());
@@ -3925,7 +4535,7 @@ const ThemeSelectorServiceDefinition = {
3925
4535
  };
3926
4536
 
3927
4537
  // eslint-disable-next-line @typescript-eslint/naming-convention
3928
- const useStyles$8 = makeStyles({
4538
+ const useStyles$7 = makeStyles({
3929
4539
  app: {
3930
4540
  colorScheme: "light dark",
3931
4541
  flexGrow: 1,
@@ -3961,7 +4571,7 @@ function MakeModularTool(options) {
3961
4571
  SetThemeMode(themeMode);
3962
4572
  }
3963
4573
  const modularToolRootComponent = () => {
3964
- const classes = useStyles$8();
4574
+ const classes = useStyles$7();
3965
4575
  const [extensionManagerContext, setExtensionManagerContext] = useState();
3966
4576
  const [requiredExtensions, setRequiredExtensions] = useState();
3967
4577
  const [requiredExtensionsDeferred, setRequiredExtensionsDeferred] = useState();
@@ -3987,7 +4597,7 @@ function MakeModularTool(options) {
3987
4597
  });
3988
4598
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
3989
4599
  if (extensionFeeds.length > 0) {
3990
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-D9hRcNXP.js');
4600
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-DcpjIM_c.js');
3991
4601
  await serviceContainer.addServiceAsync(ExtensionListServiceDefinition);
3992
4602
  }
3993
4603
  // Register the theme selector service (for selecting the theme) if theming is configured.
@@ -4165,7 +4775,7 @@ const MeshIcon = createFluentIcon("Mesh", "16", '<path d="M14.03,3.54l-5.11-2.07
4165
4775
  const TranslateIcon = createFluentIcon("Translate", "24", '<path d="M20.16,12.98l-2.75-2.75c-.29-.29-.77-.29-1.06,0-.29.29-.29.77,0,1.06l1.47,1.47h-6.69v-6.69l1.47,1.47c.29.29.77.29,1.06,0,.29-.29.29-.77,0-1.06l-2.75-2.75c-.14-.14-.33-.22-.53-.22s-.39.08-.53.22l-2.75,2.75c-.29.29-.29.77,0,1.06.29.29.77.29,1.06,0l1.47-1.47v7.13l-3.52,3.52v-2.08c0-.41-.34-.75-.75-.75s-.75.34-.75.75v3.89c0,.2.08.39.22.53.14.14.33.22.53.22h3.89c.41,0,.75-.34.75-.75s-.34-.75-.75-.75h-2.08s3.52-3.52,3.52-3.52h7.13l-1.47,1.47c-.29.29-.29.77,0,1.06s.77.29,1.06,0l2.75-2.75c.14-.14.22-.33.22-.53s-.08-.39-.22-.53Z" />');
4166
4776
  const MaterialIcon = createFluentIcon("Material", "16", '<path d="M14.74,6.3c-.09-.36-.38-.64-.75-.72-.04-.09-.08-.18-.12-.27.1-.15.16-.32.16-.51,0-.18-.05-.34-.13-.48-1.23-1.97-3.41-3.28-5.9-3.28C4.16,1.04,1.04,4.16,1.04,7.99c0,.39.23.72.57.88.02.12.03.25.06.37-.18.18-.3.42-.3.7,0,.11.02.21.06.31.94,2.74,3.53,4.71,6.58,4.71,3.84,0,6.96-3.12,6.96-6.96,0-.59-.08-1.16-.22-1.7ZM2.07,8.58c-.02-.19-.03-.39-.03-.58,0-3.29,2.67-5.96,5.96-5.96,2.23,0,4.17,1.23,5.2,3.05.05.18-.07.45-.3.75-.57-.73-1.45-1.21-2.45-1.21-1.72,0-3.12,1.4-3.12,3.11,0,.33.07.65.16.95-3.05.82-5.17.52-5.42-.11ZM12.56,7.75c0,1.17-.95,2.11-2.11,2.11s-2.12-.95-2.12-2.11.95-2.11,2.12-2.11,2.11.95,2.11,2.11ZM8,13.96c-2.6,0-4.81-1.68-5.62-4.01.5.16,1.11.24,1.79.24,1.15,0,2.49-.22,3.79-.59.57.76,1.47,1.26,2.49,1.26,1.72,0,3.11-1.4,3.11-3.11,0-.34-.07-.65-.17-.96.13-.13.24-.26.34-.39.14.51.22,1.04.22,1.6,0,3.29-2.67,5.96-5.96,5.96Z"/>');
4167
4777
 
4168
- const useStyles$7 = makeStyles({
4778
+ const useStyles$6 = makeStyles({
4169
4779
  coordinatesModeButton: {
4170
4780
  margin: `0 0 0 ${tokens.spacingHorizontalXS}`,
4171
4781
  },
@@ -4175,7 +4785,7 @@ const useStyles$7 = makeStyles({
4175
4785
  });
4176
4786
  const GizmoToolbar = (props) => {
4177
4787
  const { scene, entity, gizmoService } = props;
4178
- const classes = useStyles$7();
4788
+ const classes = useStyles$6();
4179
4789
  const gizmoManager = useResource(useCallback(() => {
4180
4790
  const utilityLayerRef = gizmoService.getUtilityLayer(scene);
4181
4791
  const keepDepthUtilityLayerRef = gizmoService.getUtilityLayer(scene, "keepDepth");
@@ -4259,260 +4869,67 @@ const GizmoToolbar = (props) => {
4259
4869
  };
4260
4870
  }, [gizmoManager, gizmoMode, entity]);
4261
4871
  const updateGizmoMode = useCallback((mode) => {
4262
- setGizmoMode((currentMode) => (currentMode === mode ? undefined : mode));
4263
- }, []);
4264
- const onCoordinatesModeChange = useCallback((e, data) => {
4265
- gizmoManager.coordinatesMode = Number(data.checkedItems[0]);
4266
- }, []);
4267
- const toggleCoordinatesMode = useCallback(() => {
4268
- gizmoManager.coordinatesMode = coordinatesMode === 1 /* GizmoCoordinatesMode.Local */ ? 0 /* GizmoCoordinatesMode.World */ : 1 /* GizmoCoordinatesMode.Local */;
4269
- }, [gizmoManager, coordinatesMode]);
4270
- return (jsxs(Fragment, { children: [jsx(ToggleButton, { title: "Translate", checkedIcon: TranslateIcon, value: gizmoMode === "translate", onChange: () => updateGizmoMode("translate") }), jsx(ToggleButton, { title: "Rotate", checkedIcon: ArrowRotateClockwiseRegular, value: gizmoMode === "rotate", onChange: () => updateGizmoMode("rotate") }), jsx(ToggleButton, { title: "Scale", checkedIcon: ArrowExpandRegular, value: gizmoMode === "scale", onChange: () => updateGizmoMode("scale") }), jsx(ToggleButton, { title: "Bounding Box", checkedIcon: SelectObjectRegular, value: gizmoMode === "boundingBox", onChange: () => updateGizmoMode("boundingBox") }), jsx(Collapse, { visible: !!gizmoMode, orientation: "horizontal", children: jsxs(Menu, { positioning: "below-end", checkedValues: { coordinatesMode: [coordinatesMode.toString()] }, onCheckedValueChange: onCoordinatesModeChange, children: [jsx(MenuTrigger, { disableButtonEnhancement: true, children: (triggerProps) => (jsx(Tooltip, { content: "Coordinates Mode", relationship: "label", children: jsx(SplitButton, { className: classes.coordinatesModeButton, menuButton: triggerProps, primaryActionButton: {
4271
- onClick: toggleCoordinatesMode,
4272
- }, size: "small", appearance: "transparent", shape: "rounded", icon: coordinatesMode === 1 /* GizmoCoordinatesMode.Local */ ? jsx(CubeRegular, {}) : jsx(GlobeRegular, {}) }) })) }), jsx(MenuPopover, { className: classes.coordinatesModeMenu, children: jsxs(MenuList, { children: [jsx(MenuItemRadio, { name: "coordinatesMode", value: 1 /* GizmoCoordinatesMode.Local */.toString(), children: "Local" }), jsx(MenuItemRadio, { name: "coordinatesMode", value: 0 /* GizmoCoordinatesMode.World */.toString(), children: "World" })] }) })] }) })] }));
4273
- };
4274
-
4275
- const GizmoToolbarServiceDefinition = {
4276
- friendlyName: "Gizmo Toolbar",
4277
- consumes: [SceneContextIdentity, ShellServiceIdentity, SelectionServiceIdentity, GizmoServiceIdentity],
4278
- factory: (sceneContext, shellService, selectionService, gizmoService) => {
4279
- shellService.addToolbarItem({
4280
- key: "Gizmo Toolbar",
4281
- verticalLocation: "top",
4282
- horizontalLocation: "left",
4283
- suppressTeachingMoment: true,
4284
- component: () => {
4285
- const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
4286
- const selectedEntity = useObservableState(() => selectionService.selectedEntity, selectionService.onSelectedEntityChanged);
4287
- return scene ? jsx(GizmoToolbar, { scene: scene, entity: selectedEntity, gizmoService: gizmoService }) : null;
4288
- },
4289
- });
4290
- },
4291
- };
4292
-
4293
- const useStyles$6 = makeStyles({
4294
- badge: {
4295
- margin: tokens.spacingHorizontalXXS,
4296
- fontFamily: "monospace",
4297
- },
4298
- });
4299
- const MiniStatsServiceDefinition = {
4300
- friendlyName: "Mini Stats",
4301
- consumes: [SceneContextIdentity, ShellServiceIdentity],
4302
- factory: (sceneContext, shellService) => {
4303
- shellService.addToolbarItem({
4304
- key: "Mini Stats",
4305
- verticalLocation: "bottom",
4306
- horizontalLocation: "right",
4307
- suppressTeachingMoment: true,
4308
- component: () => {
4309
- const classes = useStyles$6();
4310
- const scene = useObservableState(useCallback(() => sceneContext.currentScene, [sceneContext.currentScene]), sceneContext.currentSceneObservable);
4311
- const engine = scene?.getEngine();
4312
- const fps = useObservableState(useCallback(() => (engine ? Math.round(engine.getFps()) : null), [engine]), engine?.onBeginFrameObservable);
4313
- return fps != null ? jsx(Badge, { appearance: "outline", className: classes.badge, children: `${fps} fps` }) : null;
4314
- },
4315
- });
4316
- },
4317
- };
4318
-
4319
- const TargetedAnimationGeneralProperties = (props) => {
4320
- const { selectionService } = props;
4321
- return (jsx(Fragment, { children: jsx(LinkToEntityPropertyLine, { label: "Target", description: "The entity animated by this animation.", entity: props.targetedAnimation.target, selectionService: selectionService }) }));
4322
- };
4323
-
4324
- /**
4325
- * Renders a label with an optional popup containing more info
4326
- * @param props
4327
- * @returns
4328
- */
4329
- const InfoLabel = (props) => {
4330
- InfoLabel.displayName = "InfoLabel";
4331
- return (jsx(InfoLabel$1, { htmlFor: props.htmlFor, info: props.info, children: jsx(Body1, { children: props.label }) }));
4332
- };
4333
-
4334
- const TextInput = (props) => {
4335
- TextInput.displayName = "TextInput";
4336
- const classes = useInputStyles$1();
4337
- const [value, setValue] = useState(props.value);
4338
- const lastCommittedValue = useRef(props.value);
4339
- const { size } = useContext(ToolContext);
4340
- useEffect(() => {
4341
- if (props.value !== lastCommittedValue.current) {
4342
- setValue(props.value); // Update local state when props.value changes
4343
- lastCommittedValue.current = props.value;
4344
- }
4345
- }, [props.value]);
4346
- const validateValue = (val) => {
4347
- const failsValidator = props.validator && !props.validator(val);
4348
- return !failsValidator;
4349
- };
4350
- const tryCommitValue = (currVal) => {
4351
- // Only commit if valid and different from last committed value
4352
- if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
4353
- lastCommittedValue.current = currVal;
4354
- props.onChange(currVal);
4355
- }
4356
- };
4357
- const handleChange = (event, data) => {
4358
- event.stopPropagation();
4359
- setValue(data.value);
4360
- tryCommitValue(data.value);
4361
- };
4362
- const handleKeyUp = (event) => {
4363
- event.stopPropagation();
4364
- setValue(event.currentTarget.value);
4365
- tryCommitValue(event.currentTarget.value);
4366
- };
4367
- const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
4368
- const id = useId("input-button");
4369
- return (jsxs("div", { className: classes.container, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(Input, { ...props, input: { className: classes.inputSlot }, id: id, size: size, value: value, onChange: handleChange, onKeyUp: handleKeyUp, onKeyDown: HandleKeyDown, onBlur: HandleOnBlur, className: mergedClassName })] }));
4370
- };
4371
-
4372
- const SpinButton = (props) => {
4373
- SpinButton.displayName = "SpinButton";
4374
- const classes = useInputStyles$1();
4375
- const { size } = useContext(ToolContext);
4376
- const { min, max } = props;
4377
- const [value, setValue] = useState(props.value);
4378
- const lastCommittedValue = useRef(props.value);
4379
- // step and forceInt are not mutually exclusive since there could be cases where you want to forceInt but have spinButton jump >1 int per spin
4380
- const step = props.step != undefined ? props.step : props.forceInt ? 1 : undefined;
4381
- const precision = Math.min(4, step !== undefined ? Math.max(0, CalculatePrecision(step)) : 2); // If no step, set precision to 2. Regardless, cap precision at 4 to avoid wild numbers
4382
- useEffect(() => {
4383
- if (props.value !== lastCommittedValue.current) {
4384
- lastCommittedValue.current = props.value;
4385
- setValue(props.value); // Update local state when props.value changes
4386
- }
4387
- }, [props.value]);
4388
- const validateValue = (numericValue) => {
4389
- const outOfBounds = (min !== undefined && numericValue < min) || (max !== undefined && numericValue > max);
4390
- const failsValidator = props.validator && !props.validator(numericValue);
4391
- const failsIntCheck = props.forceInt ? !Number.isInteger(numericValue) : false;
4392
- const invalid = !!outOfBounds || !!failsValidator || isNaN(numericValue) || !!failsIntCheck;
4393
- return !invalid;
4394
- };
4395
- const tryCommitValue = (currVal) => {
4396
- // Only commit if valid and different from last committed value
4397
- if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
4398
- lastCommittedValue.current = currVal;
4399
- props.onChange(currVal);
4400
- }
4401
- };
4402
- const handleChange = (event, data) => {
4403
- event.stopPropagation(); // Prevent event propagation
4404
- if (data.value != null && !Number.isNaN(data.value)) {
4405
- setValue(data.value);
4406
- tryCommitValue(data.value);
4407
- }
4408
- };
4409
- const handleKeyUp = (event) => {
4410
- event.stopPropagation(); // Prevent event propagation
4411
- if (event.key !== "Enter") {
4412
- const currVal = parseFloat(event.target.value); // Cannot use currentTarget.value as it won't have the most recently typed value
4413
- setValue(currVal);
4414
- tryCommitValue(currVal);
4415
- }
4416
- };
4417
- const id = useId("spin-button");
4418
- const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
4419
- return (jsxs("div", { className: classes.container, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(SpinButton$1, { ...props, input: { className: classes.inputSlot }, step: step, id: id, size: size, precision: precision, displayValue: `${value.toFixed(precision)}${props.unit ? " " + props.unit : ""}`, value: value, onChange: handleChange, onKeyUp: handleKeyUp, onKeyDown: HandleKeyDown, onBlur: HandleOnBlur, className: mergedClassName })] }));
4872
+ setGizmoMode((currentMode) => (currentMode === mode ? undefined : mode));
4873
+ }, []);
4874
+ const onCoordinatesModeChange = useCallback((e, data) => {
4875
+ gizmoManager.coordinatesMode = Number(data.checkedItems[0]);
4876
+ }, []);
4877
+ const toggleCoordinatesMode = useCallback(() => {
4878
+ gizmoManager.coordinatesMode = coordinatesMode === 1 /* GizmoCoordinatesMode.Local */ ? 0 /* GizmoCoordinatesMode.World */ : 1 /* GizmoCoordinatesMode.Local */;
4879
+ }, [gizmoManager, coordinatesMode]);
4880
+ return (jsxs(Fragment, { children: [jsx(ToggleButton, { title: "Translate", checkedIcon: TranslateIcon, value: gizmoMode === "translate", onChange: () => updateGizmoMode("translate") }), jsx(ToggleButton, { title: "Rotate", checkedIcon: ArrowRotateClockwiseRegular, value: gizmoMode === "rotate", onChange: () => updateGizmoMode("rotate") }), jsx(ToggleButton, { title: "Scale", checkedIcon: ArrowExpandRegular, value: gizmoMode === "scale", onChange: () => updateGizmoMode("scale") }), jsx(ToggleButton, { title: "Bounding Box", checkedIcon: SelectObjectRegular, value: gizmoMode === "boundingBox", onChange: () => updateGizmoMode("boundingBox") }), jsx(Collapse, { visible: !!gizmoMode, orientation: "horizontal", children: jsxs(Menu, { positioning: "below-end", checkedValues: { coordinatesMode: [coordinatesMode.toString()] }, onCheckedValueChange: onCoordinatesModeChange, children: [jsx(MenuTrigger, { disableButtonEnhancement: true, children: (triggerProps) => (jsx(Tooltip, { content: "Coordinates Mode", relationship: "label", children: jsx(SplitButton, { className: classes.coordinatesModeButton, menuButton: triggerProps, primaryActionButton: {
4881
+ onClick: toggleCoordinatesMode,
4882
+ }, size: "small", appearance: "transparent", shape: "rounded", icon: coordinatesMode === 1 /* GizmoCoordinatesMode.Local */ ? jsx(CubeRegular, {}) : jsx(GlobeRegular, {}) }) })) }), jsx(MenuPopover, { className: classes.coordinatesModeMenu, children: jsxs(MenuList, { children: [jsx(MenuItemRadio, { name: "coordinatesMode", value: 1 /* GizmoCoordinatesMode.Local */.toString(), children: "Local" }), jsx(MenuItemRadio, { name: "coordinatesMode", value: 0 /* GizmoCoordinatesMode.World */.toString(), children: "World" })] }) })] }) })] }));
4420
4883
  };
4421
4884
 
4422
- /**
4423
- * Wraps a text input in a property line
4424
- * @param props - PropertyLineProps and InputProps
4425
- * @returns property-line wrapped input component
4426
- */
4427
- const TextInputPropertyLine = (props) => {
4428
- TextInputPropertyLine.displayName = "TextInputPropertyLine";
4429
- return (jsx(PropertyLine, { ...props, children: jsx(TextInput, { ...props }) }));
4430
- };
4431
- /**
4432
- * Wraps a number input in a property line
4433
- * To force integer values, use forceInt param (this is distinct from the 'step' param, which will still allow submitting an integer value. forceInt will not)
4434
- * @param props - PropertyLineProps and InputProps
4435
- * @returns property-line wrapped input component
4436
- */
4437
- const NumberInputPropertyLine = (props) => {
4438
- NumberInputPropertyLine.displayName = "NumberInputPropertyLine";
4439
- return (jsx(PropertyLine, { ...props, children: jsx(SpinButton, { ...props }) }));
4885
+ const GizmoToolbarServiceDefinition = {
4886
+ friendlyName: "Gizmo Toolbar",
4887
+ consumes: [SceneContextIdentity, ShellServiceIdentity, SelectionServiceIdentity, GizmoServiceIdentity],
4888
+ factory: (sceneContext, shellService, selectionService, gizmoService) => {
4889
+ shellService.addToolbarItem({
4890
+ key: "Gizmo Toolbar",
4891
+ verticalLocation: "top",
4892
+ horizontalLocation: "left",
4893
+ suppressTeachingMoment: true,
4894
+ component: () => {
4895
+ const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
4896
+ const selectedEntity = useObservableState(() => selectionService.selectedEntity, selectionService.onSelectedEntityChanged);
4897
+ return scene ? jsx(GizmoToolbar, { scene: scene, entity: selectedEntity, gizmoService: gizmoService }) : null;
4898
+ },
4899
+ });
4900
+ },
4440
4901
  };
4441
4902
 
4442
- const useSyncedSliderStyles = makeStyles({
4443
- container: { display: "flex" },
4444
- syncedSlider: {
4445
- flex: "1 1 0",
4446
- flexDirection: "row",
4447
- display: "flex",
4448
- alignItems: "center",
4449
- },
4450
- slider: {
4451
- minWidth: CustomTokens.sliderMinWidth, // Minimum width for slider to remain usable
4452
- maxWidth: CustomTokens.sliderMaxWidth,
4903
+ const useStyles$5 = makeStyles({
4904
+ badge: {
4905
+ margin: tokens.spacingHorizontalXXS,
4906
+ fontFamily: "monospace",
4453
4907
  },
4454
4908
  });
4455
- /**
4456
- * Component which synchronizes a slider and an input field, allowing the user to change the value using either control
4457
- * @param props
4458
- * @returns SyncedSlider component
4459
- */
4460
- const SyncedSliderInput = (props) => {
4461
- SyncedSliderInput.displayName = "SyncedSliderInput";
4462
- const { infoLabel, ...passthroughProps } = props;
4463
- const classes = useSyncedSliderStyles();
4464
- const { size } = useContext(ToolContext);
4465
- const [value, setValue] = useState(props.value);
4466
- const pendingValueRef = useRef(undefined);
4467
- const isDraggingRef = useRef(false);
4468
- // NOTE: The Fluent slider will add tick marks if the step prop is anything other than undefined.
4469
- // To avoid this, we scale the min/max based on the step so we can always make step undefined.
4470
- // The actual step size in the Fluent slider is 1 when it is ste to undefined.
4471
- const min = props.min ?? 0;
4472
- const max = props.max ?? 100;
4473
- const step = props.step ?? 1;
4474
- useEffect(() => {
4475
- !isDraggingRef.current && setValue(props.value ?? ""); // Update local state when props.value changes as long as user is not actively dragging
4476
- }, [props.value]);
4477
- const handleSliderChange = (_, data) => {
4478
- const newValue = data.value * step;
4479
- setValue(newValue);
4480
- if (props.notifyOnlyOnRelease) {
4481
- // Store the value but don't notify parent yet
4482
- pendingValueRef.current = newValue;
4483
- }
4484
- else {
4485
- // Notify parent as slider changes
4486
- props.onChange(newValue);
4487
- }
4488
- };
4489
- const handleSliderPointerDown = () => {
4490
- isDraggingRef.current = true;
4491
- };
4492
- const handleSliderPointerUp = () => {
4493
- if (props.notifyOnlyOnRelease && isDraggingRef.current && pendingValueRef.current !== undefined) {
4494
- props.onChange(pendingValueRef.current);
4495
- pendingValueRef.current = undefined;
4496
- }
4497
- isDraggingRef.current = false;
4498
- };
4499
- const handleInputChange = (value) => {
4500
- setValue(value);
4501
- props.onChange(value); // Input always updates immediately
4502
- };
4503
- return (jsxs("div", { className: classes.container, children: [infoLabel && jsx(InfoLabel, { ...infoLabel, htmlFor: "syncedSlider" }), jsxs("div", { id: "syncedSlider", className: classes.syncedSlider, children: [props.min !== undefined && props.max !== undefined && (jsx(Slider, { ...passthroughProps, className: classes.slider, size: size, min: min / step, max: max / step, step: undefined, value: value / step, onChange: handleSliderChange, onPointerDown: handleSliderPointerDown, onPointerUp: handleSliderPointerUp })), jsx(SpinButton, { ...passthroughProps, value: value, onChange: handleInputChange, step: props.step })] })] }));
4909
+ const MiniStatsServiceDefinition = {
4910
+ friendlyName: "Mini Stats",
4911
+ consumes: [SceneContextIdentity, ShellServiceIdentity],
4912
+ factory: (sceneContext, shellService) => {
4913
+ shellService.addToolbarItem({
4914
+ key: "Mini Stats",
4915
+ verticalLocation: "bottom",
4916
+ horizontalLocation: "right",
4917
+ suppressTeachingMoment: true,
4918
+ component: () => {
4919
+ const classes = useStyles$5();
4920
+ const scene = useObservableState(useCallback(() => sceneContext.currentScene, [sceneContext.currentScene]), sceneContext.currentSceneObservable);
4921
+ const engine = scene?.getEngine();
4922
+ const fps = useObservableState(useCallback(() => (engine ? Math.round(engine.getFps()) : null), [engine]), engine?.onBeginFrameObservable);
4923
+ return fps != null ? jsx(Badge, { appearance: "outline", className: classes.badge, children: `${fps} fps` }) : null;
4924
+ },
4925
+ });
4926
+ },
4504
4927
  };
4505
4928
 
4506
- /**
4507
- * Renders a simple wrapper around the SyncedSliderInput
4508
- * @param props
4509
- * @returns
4510
- */
4511
- const SyncedSliderPropertyLine = forwardRef((props, ref) => {
4512
- SyncedSliderPropertyLine.displayName = "SyncedSliderPropertyLine";
4513
- const { label, description, ...sliderProps } = props;
4514
- return (jsx(PropertyLine, { ref: ref, ...props, children: jsx(SyncedSliderInput, { ...sliderProps }) }));
4515
- });
4929
+ const TargetedAnimationGeneralProperties = (props) => {
4930
+ const { selectionService } = props;
4931
+ return (jsx(Fragment, { children: jsx(LinkToEntityPropertyLine, { label: "Target", description: "The entity animated by this animation.", entity: props.targetedAnimation.target, selectionService: selectionService }) }));
4932
+ };
4516
4933
 
4517
4934
  const AnimationGroupControlProperties = (props) => {
4518
4935
  const { animationGroup } = props;
@@ -4557,440 +4974,136 @@ const AnimationGroupPropertiesServiceDefinition = {
4557
4974
  component: ({ context }) => jsx(AnimationGroupControlProperties, { animationGroup: context }),
4558
4975
  },
4559
4976
  {
4560
- section: "Info",
4561
- component: ({ context }) => jsx(AnimationGroupInfoProperties, { animationGroup: context }),
4562
- },
4563
- ],
4564
- });
4565
- const targetedAnimationContentRegistration = propertiesService.addSectionContent({
4566
- key: "Targeted Animation Properties",
4567
- predicate: (entity) => entity instanceof TargetedAnimation,
4568
- content: [
4569
- {
4570
- section: "General",
4571
- component: ({ context }) => jsx(TargetedAnimationGeneralProperties, { targetedAnimation: context, selectionService: selectionService }),
4572
- },
4573
- ],
4574
- });
4575
- return {
4576
- dispose: () => {
4577
- animationGroupContentRegistration.dispose();
4578
- targetedAnimationContentRegistration.dispose();
4579
- },
4580
- };
4581
- },
4582
- };
4583
-
4584
- const useClasses = makeStyles({
4585
- container: {
4586
- display: "flex",
4587
- flexDirection: "column",
4588
- gap: tokens.spacingVerticalS, // 8px
4589
- },
4590
- });
4591
- const MessageBar = (props) => {
4592
- MessageBar.displayName = "MessageBar";
4593
- const { message, title: header, intent, docLink } = props;
4594
- const classes = useClasses();
4595
- return (jsx("div", { className: classes.container, children: jsx(MessageBar$1, { intent: intent, layout: "multiline", children: jsxs(MessageBarBody, { children: [jsx(MessageBarTitle, { children: header }), message, docLink && (jsxs(Fragment, { children: [" - ", jsx(Link, { url: docLink, value: "Learn More" })] }))] }) }) }));
4596
- };
4597
-
4598
- const AnimationsProperties = (props) => {
4599
- const { scene, entity } = props;
4600
- const animations = entity.animations ?? [];
4601
- const ranges = entity.getAnimationRanges?.()?.filter((range) => !!range) ?? [];
4602
- const childAnimatablesAnimations = entity.getAnimatables?.().flatMap((animatable) => animatable.animations ?? []) ?? [];
4603
- animations.concat(childAnimatablesAnimations);
4604
- const lastFrom = useRef(0);
4605
- const lastTo = useRef(0);
4606
- const lastLoop = useRef(false);
4607
- const animatablesForTarget = scene.getAllAnimatablesByTarget(entity);
4608
- const isPlaying = animatablesForTarget.length > 0;
4609
- const mainAnimatable = isPlaying ? animatablesForTarget[0] : undefined;
4610
- const animationPropertiesOverride = useProperty(mainAnimatable, "animationPropertiesOverride");
4611
- if (mainAnimatable) {
4612
- lastFrom.current = mainAnimatable.fromFrame;
4613
- lastTo.current = mainAnimatable.toFrame;
4614
- lastLoop.current = mainAnimatable.loopAnimation;
4615
- }
4616
- const hasAnimations = animations.length > 0 || ranges.length > 0;
4617
- const currentFrame = useObservableState(useCallback(() => {
4618
- return mainAnimatable ? mainAnimatable.masterFrame : (scene.getAllAnimatablesByTarget(entity)[0]?.masterFrame ?? 0);
4619
- }, [scene, entity, mainAnimatable]), hasAnimations ? scene.onAfterAnimationsObservable : undefined);
4620
- return (jsx(Fragment, { children: !hasAnimations ? (jsx(MessageBar, { intent: "info", title: "No Animations", message: "To modify animations, attach an animation to this node.", docLink: "https://doc.babylonjs.com/features/featuresDeepDive/animation/" })) : (jsxs(Fragment, { children: [ranges.length > 0 && (jsx(PropertyLine, { label: "Ranges", expandedContent: jsx(Fragment, { children: ranges.map((range) => {
4621
- return (jsx(ButtonLine, { label: range.name, onClick: () => {
4622
- scene.beginAnimation(entity, range.from, range.to, true);
4623
- } }, range.name));
4624
- }) }), children: jsx(Badge, { appearance: "filled", children: ranges.length }) })), animations.length > 0 && (jsxs(Fragment, { children: [jsx(PropertyLine, { label: "Animations", expandedContent: jsx(Fragment, { children: animations.map((animation, index) => {
4625
- return jsx(TextPropertyLine, { label: `${index}: ${animation.name}`, value: animation.targetProperty }, animation.uniqueId);
4626
- }) }), children: jsx(Badge, { appearance: "filled", children: animations.length }) }), mainAnimatable && (jsx(Fragment, { children: jsx(PropertyLine, { label: "Animation Controls", expandedContent: jsxs(Fragment, { children: [jsx(NumberInputPropertyLine, { label: "From", value: mainAnimatable.fromFrame, onChange: (value) => {
4627
- scene.stopAnimation(entity);
4628
- scene.beginAnimation(entity, value, mainAnimatable.toFrame, true);
4629
- } }), jsx(NumberInputPropertyLine, { label: "To", value: mainAnimatable.toFrame, onChange: (value) => {
4630
- scene.stopAnimation(entity);
4631
- scene.beginAnimation(entity, mainAnimatable.fromFrame, value, true);
4632
- } }), jsx(SwitchPropertyLine, { label: "Loop", value: mainAnimatable.loopAnimation, onChange: (value) => {
4633
- for (const animatable of animatablesForTarget) {
4634
- animatable.loopAnimation = value;
4635
- }
4636
- } }), jsx(SyncedSliderPropertyLine, { label: "Current Frame", value: currentFrame, min: mainAnimatable.fromFrame, max: mainAnimatable.toFrame, step: (mainAnimatable.toFrame - mainAnimatable.fromFrame) / 1000, onChange: (value) => {
4637
- mainAnimatable.goToFrame(value);
4638
- } })] }), expandByDefault: true }) })), jsx(ButtonLine, { label: isPlaying ? "Stop Animation" : "Play Animation", onClick: () => {
4639
- if (isPlaying) {
4640
- scene.stopAnimation(entity);
4641
- }
4642
- else {
4643
- scene.beginAnimation(entity, lastFrom.current, lastTo.current, lastLoop.current);
4644
- }
4645
- } }), mainAnimatable && (ranges.length > 0 || animations.length > 0) ? (jsxs(Fragment, { children: [jsx(SwitchPropertyLine, { label: "Enable Override", value: animationPropertiesOverride != null, onChange: (value) => {
4646
- if (value) {
4647
- mainAnimatable.animationPropertiesOverride = new AnimationPropertiesOverride();
4648
- mainAnimatable.animationPropertiesOverride.blendingSpeed = 0.05;
4649
- }
4650
- else {
4651
- mainAnimatable.animationPropertiesOverride = undefined;
4652
- }
4653
- } }), jsx(Collapse, { visible: animationPropertiesOverride != null, children: jsxs("div", { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Enable Blending", target: animationPropertiesOverride, propertyKey: "enableBlending" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Blending Speed", target: animationPropertiesOverride, propertyKey: "blendingSpeed", min: 0, max: 0.1, step: 0.01 })] }) })] })) : null] }))] })) }));
4654
- };
4655
-
4656
- function IsAnimatable(entity) {
4657
- return entity.animations !== undefined;
4658
- }
4659
- function IsAnimationRangeContainer(entity) {
4660
- return entity.getAnimationRanges !== undefined;
4661
- }
4662
- function IsAnimatableContainer(entity) {
4663
- return entity.getAnimatables !== undefined;
4664
- }
4665
- const AnimationPropertiesServiceDefinition = {
4666
- friendlyName: "Animation Properties",
4667
- consumes: [PropertiesServiceIdentity, SelectionServiceIdentity, SceneContextIdentity],
4668
- factory: (propertiesService, selectionService, sceneContext) => {
4669
- const scene = sceneContext.currentScene;
4670
- if (!scene) {
4671
- return undefined;
4672
- }
4673
- const animationContentRegistration = propertiesService.addSectionContent({
4674
- key: "Animation Properties",
4675
- predicate: (entity) => IsAnimatable(entity) || IsAnimationRangeContainer(entity) || IsAnimatableContainer(entity),
4977
+ section: "Info",
4978
+ component: ({ context }) => jsx(AnimationGroupInfoProperties, { animationGroup: context }),
4979
+ },
4980
+ ],
4981
+ });
4982
+ const targetedAnimationContentRegistration = propertiesService.addSectionContent({
4983
+ key: "Targeted Animation Properties",
4984
+ predicate: (entity) => entity instanceof TargetedAnimation,
4676
4985
  content: [
4677
4986
  {
4678
- section: "Animation",
4679
- component: ({ context }) => jsx(AnimationsProperties, { scene: scene, entity: context }),
4987
+ section: "General",
4988
+ component: ({ context }) => jsx(TargetedAnimationGeneralProperties, { targetedAnimation: context, selectionService: selectionService }),
4680
4989
  },
4681
4990
  ],
4682
4991
  });
4683
4992
  return {
4684
4993
  dispose: () => {
4685
- animationContentRegistration.dispose();
4994
+ animationGroupContentRegistration.dispose();
4995
+ targetedAnimationContentRegistration.dispose();
4686
4996
  },
4687
4997
  };
4688
4998
  },
4689
4999
  };
4690
5000
 
4691
- const HasZ = (vector) => !(vector instanceof Vector2);
4692
- const HasW = (vector) => vector instanceof Vector4 || vector instanceof Quaternion;
4693
- /**
4694
- * Reusable component which renders a vector property line containing a label, vector value, and expandable XYZW values
4695
- * The expanded section contains a slider/input box for each component of the vector (x, y, z, w)
4696
- * @param props
4697
- * @returns
4698
- */
4699
- const TensorPropertyLine = (props) => {
4700
- TensorPropertyLine.displayName = "TensorPropertyLine";
4701
- const converted = (val) => (props.valueConverter ? props.valueConverter.from(val) : val);
4702
- const formatted = (val) => converted(val).toFixed(props.step !== undefined ? Math.max(0, CalculatePrecision(props.step)) : 2);
4703
- const [vector, setVector] = useState(props.value);
4704
- const { min, max } = props;
4705
- const onChange = (val, key) => {
4706
- const value = props.valueConverter ? props.valueConverter.to(val) : val;
4707
- const newVector = vector.clone();
4708
- newVector[key] = value; // The syncedSlider for 'w' is only rendered when vector is a Vector4, so this is safe
4709
- setVector(newVector);
4710
- props.onChange(newVector);
4711
- };
4712
- useEffect(() => {
4713
- setVector(props.value);
4714
- }, [props.value, props.expandedContent]);
4715
- return (jsx(PropertyLine, { ...props, onCopy: () => `new ${props.value.getClassName()}(${vector.x},${vector.y}${HasZ(vector) ? `,${vector.z}` : ""}${HasW(vector) ? `,${vector.w}` : ""})`, expandedContent: jsxs(Fragment, { children: [jsx(SyncedSliderPropertyLine, { label: "X", value: converted(vector.x), min: min, max: max, onChange: (val) => onChange(val, "x"), unit: props.unit, step: props.step }), jsx(SyncedSliderPropertyLine, { label: "Y", value: converted(vector.y), min: min, max: max, onChange: (val) => onChange(val, "y"), unit: props.unit, step: props.step }), HasZ(vector) && (jsx(SyncedSliderPropertyLine, { label: "Z", value: converted(vector.z), min: min, max: max, onChange: (val) => onChange(val, "z"), unit: props.unit, step: props.step })), HasW(vector) && (jsx(SyncedSliderPropertyLine, { label: "W", value: converted(vector.w), min: min, max: max, onChange: (val) => onChange(val, "w"), unit: props.unit, step: props.step }))] }), children: jsx(Body1, { children: `[${formatted(props.value.x)}, ${formatted(props.value.y)}${HasZ(props.value) ? `, ${formatted(props.value.z)}` : ""}${HasW(props.value) ? `, ${formatted(props.value.w)}` : ""}]` }) }));
4716
- };
4717
- const ToDegreesConverter = { from: Tools.ToDegrees, to: Tools.ToRadians };
4718
- const RotationVectorPropertyLine = (props) => {
4719
- RotationVectorPropertyLine.displayName = "RotationVectorPropertyLine";
4720
- const min = props.useDegrees ? 0 : undefined;
4721
- const max = props.useDegrees ? 360 : undefined;
4722
- return (jsx(Vector3PropertyLine, { ...props, unit: props.useDegrees ? "deg" : "rad", valueConverter: props.useDegrees ? ToDegreesConverter : undefined, min: min, max: max, step: 0.001 }));
4723
- };
4724
- const QuaternionPropertyLineInternal = TensorPropertyLine;
4725
- const QuaternionPropertyLine = (props) => {
4726
- QuaternionPropertyLine.displayName = "QuaternionPropertyLine";
4727
- const min = props.useDegrees ? 0 : undefined;
4728
- const max = props.useDegrees ? 360 : undefined;
4729
- const [quat, setQuat] = useState(props.value);
4730
- useEffect(() => {
4731
- setQuat(props.value);
4732
- }, [props.value]);
4733
- // Extract only the properties that exist on QuaternionPropertyLineProps
4734
- const { useDegrees, ...restProps } = props;
4735
- const onQuatChange = (val) => {
4736
- setQuat(val);
4737
- props.onChange(val);
4738
- };
4739
- const onEulerChange = (val) => {
4740
- const quat = Quaternion.FromEulerAngles(val.x, val.y, val.z);
4741
- onQuatChange(quat);
4742
- };
4743
- return useDegrees ? (jsx(Vector3PropertyLine, { ...restProps, nullable: false, ignoreNullable: false, value: quat.toEulerAngles(), valueConverter: ToDegreesConverter, min: min, max: max, onChange: onEulerChange, unit: "deg" })) : (jsx(QuaternionPropertyLineInternal, { ...props, nullable: false, value: quat, min: min, max: max, onChange: onQuatChange }));
4744
- };
4745
- const Vector2PropertyLine = TensorPropertyLine;
4746
- const Vector3PropertyLine = TensorPropertyLine;
4747
- const Vector4PropertyLine = TensorPropertyLine;
4748
-
4749
- const useDropdownStyles = makeStyles({
4750
- dropdown: {
4751
- minWidth: 0,
4752
- width: "100%",
4753
- },
5001
+ const useClasses = makeStyles({
4754
5002
  container: {
4755
5003
  display: "flex",
4756
5004
  flexDirection: "column",
4757
- justifyContent: "center", // align items vertically
4758
- },
4759
- dropdownText: { textAlign: "end", textOverflow: "ellipsis", whiteSpace: "nowrap", overflowX: "hidden" },
4760
- });
4761
- /**
4762
- * Renders a fluent UI dropdown component for the options passed in, and an additional 'Not Defined' option if null is set to true
4763
- * This component can handle both null and undefined values
4764
- * @param props
4765
- * @returns dropdown component
4766
- */
4767
- const Dropdown = (props) => {
4768
- Dropdown.displayName = "Dropdown";
4769
- const classes = useDropdownStyles();
4770
- const { options, value } = props;
4771
- const [defaultVal, setDefaultVal] = useState(props.value);
4772
- const { size } = useContext(ToolContext);
4773
- useEffect(() => {
4774
- setDefaultVal(value);
4775
- }, [props.value]);
4776
- const id = useId("dropdown");
4777
- const mergedClassName = mergeClasses(classes.container, props.className);
4778
- const optionLabel = options.find((o) => o.value === defaultVal)?.label;
4779
- return (jsxs("div", { className: mergedClassName, children: [props.infoLabel && jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), jsx(Dropdown$1, { id: id, disabled: props.disabled, size: size, className: classes.dropdown, button: jsx("span", { className: classes.dropdownText, children: optionLabel }), onOptionSelect: (evt, data) => {
4780
- const value = typeof props.value === "number" ? Number(data.optionValue) : data.optionValue;
4781
- if (value !== undefined) {
4782
- setDefaultVal(value);
4783
- props.onChange(value);
4784
- }
4785
- }, selectedOptions: [defaultVal.toString()], value: optionLabel, children: options.map((option) => (jsx(Option, { value: option.value.toString(), disabled: false, children: option.label }, option.label))) })] }));
4786
- };
4787
- const NumberDropdown = Dropdown;
4788
- const StringDropdown = Dropdown;
4789
-
4790
- const useColorPickerStyles = makeStyles({
4791
- container: {
4792
- width: "350px",
4793
- display: "flex", // becomes a flexbox
4794
- flexDirection: "column", // with children in a column
4795
- alignItems: "center", // centers children horizontally
4796
- justifyContent: "center", // centers children vertically (if height is set)
4797
- gap: tokens.spacingVerticalM,
4798
- overflow: "visible",
4799
- },
4800
- row: {
4801
- flex: 1, // is a row in the container's flex column
4802
- display: "flex", // becomes its own flexbox
4803
- flexDirection: "row", // with children in a row
4804
- gap: tokens.spacingHorizontalXL,
4805
- alignItems: "center", // align items vertically
4806
- width: "100%",
4807
- },
4808
- colorPicker: {
4809
- flex: 1,
4810
- width: "350px",
4811
- height: "350px",
4812
- },
4813
- previewColor: {
4814
- width: "60px",
4815
- height: "60px",
4816
- borderRadius: tokens.borderRadiusMedium, // 4px?
4817
- border: `${tokens.spacingVerticalXXS} solid ${tokens.colorNeutralShadowKeyLighter}`,
4818
- "@media (forced-colors: active)": {
4819
- forcedColorAdjust: "none", // ensures elmement maintains color in high constrast mode
4820
- },
4821
- },
4822
- inputRow: {
4823
- display: "flex",
4824
- flexDirection: "row",
4825
- flex: 1, // grow and fill available space
4826
- justifyContent: "center",
4827
- gap: "10px",
4828
- width: "100%",
4829
- },
4830
- inputField: {
4831
- flex: 1, // grow and fill available space
4832
- width: "auto",
4833
- minWidth: 0,
4834
- gap: tokens.spacingVerticalSNudge, // 6px
5005
+ gap: tokens.spacingVerticalS, // 8px
4835
5006
  },
4836
- });
4837
- const ColorPickerPopup = (props) => {
4838
- ColorPickerPopup.displayName = "ColorPickerPopup";
4839
- const classes = useColorPickerStyles();
4840
- const [color, setColor] = useState(props.value);
4841
- const [popoverOpen, setPopoverOpen] = useState(false);
4842
- const [isLinear, setIsLinear] = useState(props.isLinearMode ?? false);
4843
- const [isFloat, setFloat] = useState(false);
4844
- const { size } = useContext(ToolContext);
4845
- useEffect(() => {
4846
- setColor(props.value); // Ensures the trigger color updates when props.value changes
4847
- }, [props.value]);
4848
- const handleColorPickerChange = (_, data) => {
4849
- let color = Color3.FromHSV(data.color.h, data.color.s, data.color.v);
4850
- if (props.value instanceof Color4) {
4851
- color = Color4.FromColor3(color, data.color.a ?? 1);
4852
- }
4853
- handleChange(color);
4854
- };
4855
- const handleChange = (newColor) => {
4856
- setColor(newColor);
4857
- props.onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
4858
- };
4859
- return (jsxs(Popover, { positioning: {
4860
- align: "start",
4861
- overflowBoundary: document.body,
4862
- autoSize: true,
4863
- }, open: popoverOpen, trapFocus: true, onOpenChange: (_, data) => setPopoverOpen(data.open), children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: jsx(ColorSwatch, { borderColor: tokens.colorNeutralShadowKeyDarker, size: size === "small" ? "extra-small" : "small", shape: "rounded", color: color.toHexString(), value: color.toHexString().slice(1) }) }), jsx(PopoverSurface, { children: jsxs("div", { className: classes.container, children: [jsxs(ColorPicker, { className: classes.colorPicker, color: rgbaToHsv(color), onColorChange: handleColorPickerChange, children: [jsx(ColorArea, { inputX: { "aria-label": "Saturation" }, inputY: { "aria-label": "Brightness" } }), jsx(ColorSlider, { "aria-label": "Hue" }), color instanceof Color4 && jsx(AlphaSlider, { "aria-label": "Alpha" })] }), jsxs("div", { className: classes.row, children: [jsx("div", { className: classes.previewColor, style: { backgroundColor: color.toHexString() } }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
4864
- label: "Color Space",
4865
- info: jsx(Body1, { children: "Today this is not mutable as the color space is determined by the entity. Soon we will allow swapping" }),
4866
- }, options: [
4867
- { label: "Gamma", value: 0 },
4868
- { label: "Linear", value: 1 },
4869
- ], disabled: true, value: isLinear ? 1 : 0, onChange: (val) => setIsLinear(val === 1) }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
4870
- label: "Data Type",
4871
- info: jsx(Body1, { children: "We will introduce this functionality soon!" }),
4872
- }, options: [
4873
- { label: "Int", value: 0 },
4874
- { label: "Float", value: 1 },
4875
- ], disabled: true, value: isFloat ? 1 : 0, onChange: (val) => setFloat(val === 1) })] }), jsxs("div", { className: classes.inputRow, children: [jsx(InputRgbField, { title: "Red", value: color, rgbKey: "r", onChange: handleChange }), jsx(InputRgbField, { title: "Green", value: color, rgbKey: "g", onChange: handleChange }), jsx(InputRgbField, { title: "Blue", value: color, rgbKey: "b", onChange: handleChange }), jsx(InputAlphaField, { color: color, onChange: handleChange })] }), jsxs("div", { className: classes.inputRow, children: [jsx(InputHsvField, { title: "Hue", value: color, hsvKey: "h", max: 360, onChange: handleChange }), jsx(InputHsvField, { title: "Saturation", value: color, hsvKey: "s", max: 100, scale: 100, onChange: handleChange }), jsx(InputHsvField, { title: "Value", value: color, hsvKey: "v", max: 100, scale: 100, onChange: handleChange })] }), jsx("div", { className: classes.inputRow, children: jsx(InputHexField, { title: "Hexadecimal", linearHex: isLinear, isLinearMode: isLinear, value: color, onChange: handleChange }) })] }) })] }));
4876
- };
4877
- /**
4878
- * Component which displays the passed in color's HEX value, either in linearSpace (if linearHex is true) or in gamma space
4879
- * When the hex color is changed by user, component calculates the new Color3/4 value and calls onChange
4880
- *
4881
- * Component uses the isLinearMode boolean to display an informative label regarding linear / gamma space
4882
- * @param props - The properties for the InputHexField component.
4883
- * @returns
4884
- */
4885
- const InputHexField = (props) => {
4886
- const classes = useColorPickerStyles();
4887
- const { title, value, onChange, linearHex, isLinearMode } = props;
4888
- return (jsx(TextInput, { disabled: linearHex ? !isLinearMode : false, className: classes.inputField, value: linearHex ? value.toLinearSpace().toHexString() : value.toHexString(), validator: ValidateColorHex, onChange: (val) => (linearHex ? onChange(Color3.FromHexString(val).toGammaSpace()) : onChange(Color3.FromHexString(val))), infoLabel: title
4889
- ? {
4890
- label: title,
4891
- // If not representing a linearHex, no info is needed.
4892
- info: !props.linearHex ? undefined : !isLinearMode ? ( // If representing a linear hex but we are in gammaMode, simple message explaining why linearHex is disabled
4893
- jsx(Fragment, { children: " This color picker is attached to an entity whose color is stored in gamma space, so we are showing linear hex in disabled view " })) : (
4894
- // If representing a linear hex and we are in linearMode, give information about how to use these hex values
4895
- jsxs(Fragment, { children: ["This color picker is attached to an entity whose color is stored in linear space (ex: PBR Material), and Babylon converts the color to gamma space before rendering on screen because the human eye is best at processing colors in gamma space. We thus also want to display the color picker in gamma space so that the color chosen here will match the color seen in your entity.", jsx("br", {}), "If you want to copy/paste the HEX into your code, you can either use", jsx(Body1Strong, { children: "Color3.FromHexString(LINEAR_HEX)" }), jsx("br", {}), "or", jsx("br", {}), jsx(Body1Strong, { children: "Color3.FromHexString(GAMMA_HEX).toLinearSpace()" }), jsx("br", {}), jsx("br", {}), jsx(Link, { url: "https://doc.babylonjs.com/preparingArtForBabylon/controllingColorSpace/", value: "Read more in our docs!" })] })),
4896
- }
4897
- : undefined }));
4898
- };
4899
- const InputRgbField = (props) => {
4900
- const { value, onChange, title, rgbKey } = props;
4901
- const classes = useColorPickerStyles();
4902
- const handleChange = useCallback((val) => {
4903
- const newColor = value.clone();
4904
- newColor[rgbKey] = val / 255.0; // Convert to 0-1 range
4905
- onChange(newColor);
4906
- }, [value, onChange, rgbKey]);
4907
- return (jsx(SpinButton, { title: title, infoLabel: title ? { label: title } : undefined, className: classes.inputField, min: 0, max: 255, value: Math.round(value[rgbKey] * 255), forceInt: true, onChange: handleChange }));
4908
- };
4909
- function rgbaToHsv(color) {
4910
- const c = new Color3(color.r, color.g, color.b);
4911
- const hsv = c.toHSV();
4912
- return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a };
4913
- }
4914
- /**
4915
- * In the HSV (Hue, Saturation, Value) color model, Hue (H) ranges from 0 to 360 degrees, representing the color's position on the color wheel.
4916
- * Saturation (S) ranges from 0 to 100%, indicating the intensity or purity of the color, with 0 being shades of gray and 100 being a fully saturated color.
4917
- * Value (V) ranges from 0 to 100%, representing the brightness of the color, with 0 being black and 100 being the brightest.
4918
- * @param props - The properties for the InputHsvField component.
4919
- */
4920
- const InputHsvField = (props) => {
4921
- const { value, title, hsvKey, max, onChange, scale = 1 } = props;
4922
- const classes = useColorPickerStyles();
4923
- const handleChange = useCallback((val) => {
4924
- // Convert current color to HSV, update the new hsv value, then call onChange prop
4925
- const hsv = rgbaToHsv(value);
4926
- hsv[hsvKey] = val / scale;
4927
- let newColor = Color3.FromHSV(hsv.h, hsv.s, hsv.v);
4928
- if (value instanceof Color4) {
4929
- newColor = Color4.FromColor3(newColor, value.a ?? 1);
4930
- }
4931
- props.onChange(newColor);
4932
- }, [value, onChange, hsvKey, scale]);
4933
- return (jsx(SpinButton, { infoLabel: title ? { label: title } : undefined, title: title, className: classes.inputField, min: 0, max: max, value: Math.round(rgbaToHsv(value)[hsvKey] * scale), forceInt: true, onChange: handleChange }));
5007
+ });
5008
+ const MessageBar = (props) => {
5009
+ MessageBar.displayName = "MessageBar";
5010
+ const { message, title: header, intent, docLink } = props;
5011
+ const classes = useClasses();
5012
+ return (jsx("div", { className: classes.container, children: jsx(MessageBar$1, { intent: intent, layout: "multiline", children: jsxs(MessageBarBody, { children: [jsx(MessageBarTitle, { children: header }), message, docLink && (jsxs(Fragment, { children: [" - ", jsx(Link, { url: docLink, value: "Learn More" })] }))] }) }) }));
4934
5013
  };
4935
- /**
4936
- * Displays the alpha value of a color, either in the disabled state (if color is Color3) or as a spin button (if color is Color4).
4937
- * @param props
4938
- * @returns
4939
- */
4940
- const InputAlphaField = (props) => {
4941
- const classes = useColorPickerStyles();
4942
- const { color, onChange } = props;
4943
- const handleChange = useCallback((value) => {
4944
- if (Number.isNaN(value) || value < 0 || value > 1) {
4945
- return;
4946
- }
4947
- if (color instanceof Color4) {
4948
- const newColor = color.clone();
4949
- newColor.a = value;
4950
- return newColor;
4951
- }
4952
- else {
4953
- return Color4.FromColor3(color, value);
4954
- }
4955
- }, [onChange]);
4956
- return (jsx(SpinButton, { disabled: color instanceof Color3, min: 0, max: 1, className: classes.inputField, value: color instanceof Color3 ? 1 : color.a, step: 0.01, onChange: handleChange, infoLabel: {
4957
- label: "Alpha",
4958
- info: color instanceof Color3 ? (jsx(Fragment, { children: "Because this color picker is representing a Color3, we do not permit modifying alpha from the color picker. You can however modify the entity's alpha property directly, either in code via entity.alpha OR via inspector's transparency section." })) : undefined,
4959
- } }));
5014
+
5015
+ const AnimationsProperties = (props) => {
5016
+ const { scene, entity } = props;
5017
+ const animations = entity.animations ?? [];
5018
+ const ranges = entity.getAnimationRanges?.()?.filter((range) => !!range) ?? [];
5019
+ const childAnimatablesAnimations = entity.getAnimatables?.().flatMap((animatable) => animatable.animations ?? []) ?? [];
5020
+ animations.concat(childAnimatablesAnimations);
5021
+ const lastFrom = useRef(0);
5022
+ const lastTo = useRef(0);
5023
+ const lastLoop = useRef(false);
5024
+ const animatablesForTarget = scene.getAllAnimatablesByTarget(entity);
5025
+ const isPlaying = animatablesForTarget.length > 0;
5026
+ const mainAnimatable = isPlaying ? animatablesForTarget[0] : undefined;
5027
+ const animationPropertiesOverride = useProperty(mainAnimatable, "animationPropertiesOverride");
5028
+ if (mainAnimatable) {
5029
+ lastFrom.current = mainAnimatable.fromFrame;
5030
+ lastTo.current = mainAnimatable.toFrame;
5031
+ lastLoop.current = mainAnimatable.loopAnimation;
5032
+ }
5033
+ const hasAnimations = animations.length > 0 || ranges.length > 0;
5034
+ const currentFrame = useObservableState(useCallback(() => {
5035
+ return mainAnimatable ? mainAnimatable.masterFrame : (scene.getAllAnimatablesByTarget(entity)[0]?.masterFrame ?? 0);
5036
+ }, [scene, entity, mainAnimatable]), hasAnimations ? scene.onAfterAnimationsObservable : undefined);
5037
+ return (jsx(Fragment, { children: !hasAnimations ? (jsx(MessageBar, { intent: "info", title: "No Animations", message: "To modify animations, attach an animation to this node.", docLink: "https://doc.babylonjs.com/features/featuresDeepDive/animation/" })) : (jsxs(Fragment, { children: [ranges.length > 0 && (jsx(PropertyLine, { label: "Ranges", expandedContent: jsx(Fragment, { children: ranges.map((range) => {
5038
+ return (jsx(ButtonLine, { label: range.name, onClick: () => {
5039
+ scene.beginAnimation(entity, range.from, range.to, true);
5040
+ } }, range.name));
5041
+ }) }), children: jsx(Badge, { appearance: "filled", children: ranges.length }) })), animations.length > 0 && (jsxs(Fragment, { children: [jsx(PropertyLine, { label: "Animations", expandedContent: jsx(Fragment, { children: animations.map((animation, index) => {
5042
+ return jsx(TextPropertyLine, { label: `${index}: ${animation.name}`, value: animation.targetProperty }, animation.uniqueId);
5043
+ }) }), children: jsx(Badge, { appearance: "filled", children: animations.length }) }), mainAnimatable && (jsx(Fragment, { children: jsx(PropertyLine, { label: "Animation Controls", expandedContent: jsxs(Fragment, { children: [jsx(NumberInputPropertyLine, { label: "From", value: mainAnimatable.fromFrame, onChange: (value) => {
5044
+ scene.stopAnimation(entity);
5045
+ scene.beginAnimation(entity, value, mainAnimatable.toFrame, true);
5046
+ } }), jsx(NumberInputPropertyLine, { label: "To", value: mainAnimatable.toFrame, onChange: (value) => {
5047
+ scene.stopAnimation(entity);
5048
+ scene.beginAnimation(entity, mainAnimatable.fromFrame, value, true);
5049
+ } }), jsx(SwitchPropertyLine, { label: "Loop", value: mainAnimatable.loopAnimation, onChange: (value) => {
5050
+ for (const animatable of animatablesForTarget) {
5051
+ animatable.loopAnimation = value;
5052
+ }
5053
+ } }), jsx(SyncedSliderPropertyLine, { label: "Current Frame", value: currentFrame, min: mainAnimatable.fromFrame, max: mainAnimatable.toFrame, step: (mainAnimatable.toFrame - mainAnimatable.fromFrame) / 1000, onChange: (value) => {
5054
+ mainAnimatable.goToFrame(value);
5055
+ } })] }), expandByDefault: true }) })), jsx(ButtonLine, { label: isPlaying ? "Stop Animation" : "Play Animation", onClick: () => {
5056
+ if (isPlaying) {
5057
+ scene.stopAnimation(entity);
5058
+ }
5059
+ else {
5060
+ scene.beginAnimation(entity, lastFrom.current, lastTo.current, lastLoop.current);
5061
+ }
5062
+ } }), mainAnimatable && (ranges.length > 0 || animations.length > 0) ? (jsxs(Fragment, { children: [jsx(SwitchPropertyLine, { label: "Enable Override", value: animationPropertiesOverride != null, onChange: (value) => {
5063
+ if (value) {
5064
+ mainAnimatable.animationPropertiesOverride = new AnimationPropertiesOverride();
5065
+ mainAnimatable.animationPropertiesOverride.blendingSpeed = 0.05;
5066
+ }
5067
+ else {
5068
+ mainAnimatable.animationPropertiesOverride = undefined;
5069
+ }
5070
+ } }), jsx(Collapse, { visible: animationPropertiesOverride != null, children: jsxs("div", { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Enable Blending", target: animationPropertiesOverride, propertyKey: "enableBlending" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Blending Speed", target: animationPropertiesOverride, propertyKey: "blendingSpeed", min: 0, max: 0.1, step: 0.01 })] }) })] })) : null] }))] })) }));
4960
5071
  };
4961
5072
 
4962
- /**
4963
- * Reusable component which renders a color property line containing a label, colorPicker popout, and expandable RGBA values
4964
- * The expandable RGBA values are synced sliders that allow the user to modify the color's RGBA values directly
4965
- * @param props - PropertyLine props, replacing children with a color object so that we can properly display the color
4966
- * @returns Component wrapping a colorPicker component with a property line
4967
- */
4968
- const ColorPropertyLine = forwardRef((props, ref) => {
4969
- ColorPropertyLine.displayName = "ColorPropertyLine";
4970
- const [color, setColor] = useState(props.value);
4971
- useEffect(() => {
4972
- setColor(props.value);
4973
- }, [props.value]);
4974
- const onSliderChange = (value, key) => {
4975
- let newColor;
4976
- if (key === "a") {
4977
- newColor = Color4.FromColor3(color, value);
4978
- }
4979
- else {
4980
- newColor = color.clone();
4981
- newColor[key] = value / 255;
5073
+ function IsAnimatable(entity) {
5074
+ return entity.animations !== undefined;
5075
+ }
5076
+ function IsAnimationRangeContainer(entity) {
5077
+ return entity.getAnimationRanges !== undefined;
5078
+ }
5079
+ function IsAnimatableContainer(entity) {
5080
+ return entity.getAnimatables !== undefined;
5081
+ }
5082
+ const AnimationPropertiesServiceDefinition = {
5083
+ friendlyName: "Animation Properties",
5084
+ consumes: [PropertiesServiceIdentity, SelectionServiceIdentity, SceneContextIdentity],
5085
+ factory: (propertiesService, selectionService, sceneContext) => {
5086
+ const scene = sceneContext.currentScene;
5087
+ if (!scene) {
5088
+ return undefined;
4982
5089
  }
4983
- setColor(newColor); // Create a new object to trigger re-render
4984
- props.onChange(newColor);
4985
- };
4986
- const onColorPickerChange = (newColor) => {
4987
- setColor(newColor);
4988
- props.onChange(newColor);
4989
- };
4990
- return (jsx(PropertyLine, { ref: ref, ...props, expandedContent: jsxs(Fragment, { children: [jsx(SyncedSliderPropertyLine, { label: "R", value: color.r * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "r") }), jsx(SyncedSliderPropertyLine, { label: "G", value: color.g * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "g") }), jsx(SyncedSliderPropertyLine, { label: "B", value: color.b * 255, min: 0, max: 255, onChange: (value) => onSliderChange(value, "b") }), color instanceof Color4 && jsx(SyncedSliderPropertyLine, { label: "A", value: color.a, min: 0, max: 1, step: 0.01, onChange: (value) => onSliderChange(value, "a") })] }), children: jsx(ColorPickerPopup, { ...props, onChange: onColorPickerChange, value: color }) }));
4991
- });
4992
- const Color3PropertyLine = ColorPropertyLine;
4993
- const Color4PropertyLine = ColorPropertyLine;
5090
+ const animationContentRegistration = propertiesService.addSectionContent({
5091
+ key: "Animation Properties",
5092
+ predicate: (entity) => IsAnimatable(entity) || IsAnimationRangeContainer(entity) || IsAnimatableContainer(entity),
5093
+ content: [
5094
+ {
5095
+ section: "Animation",
5096
+ component: ({ context }) => jsx(AnimationsProperties, { scene: scene, entity: context }),
5097
+ },
5098
+ ],
5099
+ });
5100
+ return {
5101
+ dispose: () => {
5102
+ animationContentRegistration.dispose();
5103
+ },
5104
+ };
5105
+ },
5106
+ };
4994
5107
 
4995
5108
  const GeneralAtmosphereProperties = (props) => {
4996
5109
  const { entity: atmosphere } = props;
@@ -5093,30 +5206,6 @@ const ArcRotateCameraBehaviorsProperties = (props) => {
5093
5206
  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" })] }));
5094
5207
  };
5095
5208
 
5096
- const useStyles$5 = makeStyles({
5097
- dropdown: {
5098
- ...UniformWidthStyling,
5099
- },
5100
- });
5101
- /**
5102
- * Wraps a dropdown in a property line
5103
- * @param props - PropertyLineProps and DropdownProps
5104
- * @returns property-line wrapped dropdown
5105
- */
5106
- const DropdownPropertyLine = forwardRef((props, ref) => {
5107
- DropdownPropertyLine.displayName = "DropdownPropertyLine";
5108
- const classes = useStyles$5();
5109
- return (jsx(PropertyLine, { ...props, ref: ref, children: jsx(Dropdown, { ...props, className: classes.dropdown }) }));
5110
- });
5111
- /**
5112
- * Dropdown component for number values.
5113
- */
5114
- const NumberDropdownPropertyLine = DropdownPropertyLine;
5115
- /**
5116
- * Dropdown component for string values
5117
- */
5118
- const StringDropdownPropertyLine = DropdownPropertyLine;
5119
-
5120
5209
  // The below conversion functions are taken from old HexLineComponent and can likely be simplified
5121
5210
  const ConvertToHexString = (valueString) => {
5122
5211
  while (valueString.length < 10) {
@@ -9154,32 +9243,19 @@ const UserFeedbackServiceDefinition = {
9154
9243
  },
9155
9244
  };
9156
9245
 
9157
- let CurrentInspectorToken = null;
9158
- function IsInspectorVisible() {
9159
- return CurrentInspectorToken != null;
9160
- }
9161
- function ShowInspector(scene, options) {
9162
- _ShowInspector(scene, options ?? {});
9163
- }
9164
- function _ShowInspector(scene, options) {
9165
- // TODO: Lots more work to do to respect all the Inspector v1 options.
9246
+ // TODO: The key should probably be the Canvas, because we only want to show one inspector instance per canvas.
9247
+ // If it is called for a different scene that is rendering to the same canvas, then we should probably
9248
+ // switch the inspector instance to that scene (once this is supported).
9249
+ const InspectorTokens = new WeakMap();
9250
+ function ShowInspector(scene, options = {}) {
9166
9251
  options = {
9167
- overlay: false,
9168
- showExplorer: true,
9169
- showInspector: true,
9170
- embedMode: false,
9171
- enableClose: true,
9172
- handleResize: true,
9173
- enablePopup: true,
9252
+ autoResizeEngine: true,
9174
9253
  ...options,
9175
9254
  };
9176
- if (!scene) {
9177
- scene = EngineStore.LastCreatedScene;
9178
- }
9179
- if (!scene || scene.isDisposed) {
9180
- return;
9181
- }
9182
- let parentElement = options.globalRoot ?? null;
9255
+ const inspectorToken = {
9256
+ dispose: () => { },
9257
+ };
9258
+ let parentElement = options.containerElement ?? null;
9183
9259
  if (!parentElement) {
9184
9260
  parentElement = scene.getEngine().getRenderingCanvas()?.parentElement ?? null;
9185
9261
  while (parentElement) {
@@ -9192,10 +9268,12 @@ function _ShowInspector(scene, options) {
9192
9268
  }
9193
9269
  }
9194
9270
  if (!parentElement) {
9195
- return;
9271
+ return inspectorToken;
9196
9272
  }
9197
- if (IsInspectorVisible()) {
9198
- HideInspector();
9273
+ const existingToken = InspectorTokens.get(scene);
9274
+ if (existingToken) {
9275
+ existingToken.dispose();
9276
+ InspectorTokens.delete(scene);
9199
9277
  }
9200
9278
  const disposeActions = [];
9201
9279
  const canvasContainerDisplay = parentElement.style.display;
@@ -9249,11 +9327,10 @@ function _ShowInspector(scene, options) {
9249
9327
  };
9250
9328
  },
9251
9329
  };
9252
- if (options.handleResize) {
9330
+ if (options.autoResizeEngine) {
9253
9331
  const observer = scene.onBeforeRenderObservable.add(() => scene.getEngine().resize());
9254
9332
  disposeActions.push(() => observer.remove());
9255
9333
  }
9256
- if (options.showExplorer) ;
9257
9334
  const modularTool = MakeModularTool({
9258
9335
  containerElement: parentElement,
9259
9336
  serviceDefinitions: [
@@ -9318,15 +9395,147 @@ function _ShowInspector(scene, options) {
9318
9395
  UserFeedbackServiceDefinition,
9319
9396
  // Adds always present "mini stats" (like fps) to the toolbar, etc.
9320
9397
  MiniStatsServiceDefinition,
9398
+ // Legacy service to support custom inspectable properties on objects.
9399
+ LegacyInspectableObjectPropertiesServiceDefinition,
9321
9400
  // Additional services passed in to the Inspector.
9322
9401
  ...(options.serviceDefinitions ?? []),
9323
9402
  ],
9324
9403
  themeMode: options.themeMode,
9325
9404
  showThemeSelector: options.showThemeSelector,
9326
9405
  extensionFeeds: [DefaultInspectorExtensionFeed, ...(options.extensionFeeds ?? [])],
9406
+ layoutMode: options.layoutMode,
9327
9407
  toolbarMode: "compact",
9328
- sidePaneRemapper: options.embedMode
9329
- ? (sidePane) => {
9408
+ sidePaneRemapper: options.sidePaneRemapper,
9409
+ });
9410
+ disposeActions.push(() => modularTool.dispose());
9411
+ let disposed = false;
9412
+ inspectorToken.dispose = () => {
9413
+ if (disposed) {
9414
+ return;
9415
+ }
9416
+ disposeActions.reverse().forEach((dispose) => dispose());
9417
+ if (options.autoResizeEngine) {
9418
+ scene.getEngine().resize();
9419
+ }
9420
+ disposed = true;
9421
+ };
9422
+ const sceneDisposedObserver = scene.onDisposeObservable.addOnce(() => {
9423
+ inspectorToken.dispose();
9424
+ });
9425
+ disposeActions.push(() => sceneDisposedObserver.remove());
9426
+ disposeActions.push(() => {
9427
+ InspectorTokens.delete(scene);
9428
+ });
9429
+ return inspectorToken;
9430
+ }
9431
+
9432
+ function ConvertOptions(v1Options) {
9433
+ // Options not currently handled:
9434
+ // • enablePopup: Do users care about this one?
9435
+ // • enableClose: Currently Inspector v2 does not allow panes/tabs to be closed.
9436
+ // • gizmoCamera: Do users care about this one?
9437
+ // • skipDefaultFontLoading: Probably doesn't make sense for Inspector v2 using Fluent.
9438
+ // TODO:
9439
+ // • explorerExtensibility
9440
+ // • contextMenu
9441
+ // • contextMenuOverride
9442
+ v1Options = {
9443
+ overlay: false,
9444
+ showExplorer: true,
9445
+ showInspector: true,
9446
+ embedMode: false,
9447
+ enableClose: true,
9448
+ handleResize: true,
9449
+ enablePopup: true,
9450
+ ...v1Options,
9451
+ };
9452
+ const serviceDefinitions = [];
9453
+ if (v1Options.initialTab) {
9454
+ const paneKey = (() => {
9455
+ switch (v1Options.initialTab) {
9456
+ case 1 /* DebugLayerTab.Debug */:
9457
+ return "Debug";
9458
+ case 2 /* DebugLayerTab.Statistics */:
9459
+ return "Statistics";
9460
+ case 4 /* DebugLayerTab.Settings */:
9461
+ return "Settings";
9462
+ case 3 /* DebugLayerTab.Tools */:
9463
+ return "Tools";
9464
+ }
9465
+ })();
9466
+ const initialTabServiceDefinition = {
9467
+ friendlyName: "Initial Tab Selector",
9468
+ consumes: [ShellServiceIdentity],
9469
+ factory: (shellService) => {
9470
+ // Just find and select the requested initial tab.
9471
+ shellService.sidePanes.find((pane) => pane.key === paneKey)?.select();
9472
+ },
9473
+ };
9474
+ serviceDefinitions.push(initialTabServiceDefinition);
9475
+ }
9476
+ if (v1Options.additionalNodes && v1Options.additionalNodes.length > 0) {
9477
+ const { additionalNodes } = v1Options;
9478
+ const additionalNodesServiceDefinition = {
9479
+ friendlyName: "Additional Nodes",
9480
+ consumes: [SceneExplorerServiceIdentity],
9481
+ factory: (sceneExplorerService) => {
9482
+ const sceneExplorerSectionRegistrations = additionalNodes.map((node) => sceneExplorerService.addSection({
9483
+ displayName: node.name,
9484
+ order: Number.MAX_SAFE_INTEGER,
9485
+ getRootEntities: () => {
9486
+ const children = node.getContent();
9487
+ for (const child of children) {
9488
+ const entity = child;
9489
+ if (!entity.uniqueId) {
9490
+ entity.uniqueId = UniqueIdGenerator.UniqueId;
9491
+ }
9492
+ }
9493
+ return children;
9494
+ },
9495
+ getEntityDisplayInfo: (entity) => {
9496
+ const onChangeObservable = new Observable();
9497
+ const nameHookToken = InterceptProperty(entity, "name", {
9498
+ afterSet: () => {
9499
+ onChangeObservable.notifyObservers();
9500
+ },
9501
+ });
9502
+ return {
9503
+ get name() {
9504
+ return entity.name;
9505
+ },
9506
+ onChange: onChangeObservable,
9507
+ dispose: () => {
9508
+ nameHookToken.dispose();
9509
+ onChangeObservable.clear();
9510
+ },
9511
+ };
9512
+ },
9513
+ entityIcon: () => jsx(BranchRegular, {}),
9514
+ getEntityAddedObservables: () => [],
9515
+ getEntityRemovedObservables: () => [],
9516
+ }));
9517
+ return {
9518
+ dispose: () => {
9519
+ sceneExplorerSectionRegistrations.forEach((registration) => registration.dispose());
9520
+ },
9521
+ };
9522
+ },
9523
+ };
9524
+ serviceDefinitions.push(additionalNodesServiceDefinition);
9525
+ }
9526
+ const v2Options = {
9527
+ containerElement: v1Options.globalRoot,
9528
+ layoutMode: v1Options.overlay ? "overlay" : "inline",
9529
+ autoResizeEngine: v1Options.handleResize,
9530
+ sidePaneRemapper: (sidePane) => {
9531
+ if (v1Options.showExplorer === false && sidePane.key === "Scene Explorer") {
9532
+ return null;
9533
+ }
9534
+ if (v1Options.showInspector === false &&
9535
+ (sidePane.key === "Properties" || sidePane.key === "Debug" || sidePane.key === "Statistics" || sidePane.key === "Settings" || sidePane.key === "Tools")) {
9536
+ return null;
9537
+ }
9538
+ if (v1Options.embedMode) {
9330
9539
  if (sidePane.horizontalLocation === "right") {
9331
9540
  // All right panes go to right bottom.
9332
9541
  return {
@@ -9342,42 +9551,57 @@ function _ShowInspector(scene, options) {
9342
9551
  };
9343
9552
  }
9344
9553
  }
9345
- : undefined,
9346
- });
9347
- disposeActions.push(() => modularTool.dispose());
9348
- let disposed = false;
9349
- CurrentInspectorToken = {
9350
- dispose: () => {
9351
- if (disposed) {
9352
- return;
9353
- }
9354
- disposeActions.reverse().forEach((dispose) => dispose());
9355
- if (options.handleResize) {
9356
- scene.getEngine().resize();
9357
- }
9358
- disposed = true;
9554
+ return sidePane;
9359
9555
  },
9556
+ serviceDefinitions,
9360
9557
  };
9361
- const sceneDisposedObserver = scene.onDisposeObservable.addOnce(() => {
9362
- HideInspector();
9363
- });
9364
- disposeActions.push(() => sceneDisposedObserver.remove());
9365
- }
9366
- function HideInspector() {
9367
- CurrentInspectorToken?.dispose();
9368
- CurrentInspectorToken = null;
9558
+ return v2Options;
9369
9559
  }
9560
+ /**
9561
+ * @deprecated This class only exists for backward compatibility. Use the module-level ShowInspector function instead.
9562
+ */
9370
9563
  class Inspector {
9564
+ static MarkLineContainerTitleForHighlighting(title) {
9565
+ throw new Error("Not Implemented");
9566
+ }
9567
+ static MarkMultipleLineContainerTitlesForHighlighting(titles) {
9568
+ throw new Error("Not Implemented");
9569
+ }
9570
+ static PopupEmbed() {
9571
+ // Show with embed mode on (stacked right panes) and undocked?
9572
+ throw new Error("Not Implemented");
9573
+ }
9574
+ static PopupSceneExplorer() {
9575
+ // Show with all right panes (not stacked), scene explorer tab selected, and undocked?
9576
+ throw new Error("Not Implemented");
9577
+ }
9578
+ static PopupInspector() {
9579
+ // Show with all right panes (not stacked), properties tab selected, and undocked?
9580
+ throw new Error("Not Implemented");
9581
+ }
9371
9582
  static get IsVisible() {
9372
- return IsInspectorVisible();
9583
+ return !!this._CurrentInspectorToken;
9373
9584
  }
9374
9585
  static Show(scene, userOptions) {
9375
- _ShowInspector(scene, userOptions);
9586
+ this._Show(scene, userOptions);
9587
+ }
9588
+ static _Show(scene, userOptions) {
9589
+ if (!scene) {
9590
+ scene = EngineStore.LastCreatedScene;
9591
+ }
9592
+ if (!scene || scene.isDisposed) {
9593
+ return;
9594
+ }
9595
+ this._CurrentInspectorToken = ShowInspector(scene, ConvertOptions(userOptions));
9376
9596
  }
9377
9597
  static Hide() {
9378
- HideInspector();
9598
+ this._CurrentInspectorToken?.dispose();
9599
+ this._CurrentInspectorToken = null;
9379
9600
  }
9380
9601
  }
9602
+ Inspector._CurrentInspectorToken = null;
9603
+ Inspector.OnSelectionChangeObservable = new Observable();
9604
+ Inspector.OnPropertyChangedObservable = new Observable();
9381
9605
 
9382
9606
  const useStyles$2 = makeStyles({
9383
9607
  root: {
@@ -9634,5 +9858,5 @@ const TextAreaPropertyLine = (props) => {
9634
9858
  return (jsx(PropertyLine, { ...props, children: jsx(Textarea, { ...props }) }));
9635
9859
  };
9636
9860
 
9637
- export { ShowInspector as $, useCompactMode as A, ButtonLine as B, Collapse as C, DebugServiceIdentity as D, ExtensibleAccordion as E, FileUploadLine as F, useSidePaneDockOverrides as G, useAngleConverters as H, MakeTeachingMoment as I, MakeDialogTeachingMoment as J, InterceptFunction as K, Link as L, MakeLazyComponent as M, NumberDropdownPropertyLine as N, GetPropertyDescriptor as O, PropertiesServiceIdentity as P, IsPropertyReadonly as Q, InterceptProperty as R, SwitchPropertyLine as S, ToolsServiceIdentity as T, ObservableCollection as U, ConstructorFactory as V, SceneContextIdentity as W, SelectionServiceIdentity as X, SelectionServiceDefinition as Y, SettingsContextIdentity as Z, IsInspectorVisible as _, SyncedSliderPropertyLine as a, HideInspector as a0, Inspector as a1, AccordionSection as a2, Accordion as a3, Button as a4, Checkbox as a5, ColorPickerPopup as a6, InputHexField as a7, InputHsvField as a8, ComboBox as a9, Color3PropertyLine as aA, Color4PropertyLine as aB, StringDropdownPropertyLine as aC, HexPropertyLine as aD, TextInputPropertyLine as aE, NumberInputPropertyLine as aF, LinkPropertyLine as aG, PropertyLine as aH, LineContainer as aI, PlaceholderPropertyLine as aJ, SpinButtonPropertyLine as aK, StringifiedPropertyLine as aL, TextAreaPropertyLine as aM, TextPropertyLine as aN, RotationVectorPropertyLine as aO, QuaternionPropertyLine as aP, Vector2PropertyLine as aQ, Vector3PropertyLine as aR, Vector4PropertyLine as aS, DraggableLine as aa, Dropdown as ab, NumberDropdown as ac, StringDropdown as ad, FactorGradientComponent as ae, Color3GradientComponent as af, Color4GradientComponent as ag, ColorStepGradientComponent as ah, InfoLabel as ai, List as aj, MessageBar as ak, PositionedPopover as al, SearchBar as am, SearchBox as an, SpinButton as ao, Switch as ap, SyncedSliderInput as aq, Textarea as ar, TextInput as as, ToggleButton as at, FactorGradientList as au, Color3GradientList as av, Color4GradientList as aw, Pane as ax, BooleanBadgePropertyLine as ay, CheckboxPropertyLine as az, ShellServiceIdentity as b, MakePopoverTeachingMoment as c, TeachingMoment as d, SidePaneContainer as e, SceneExplorerServiceIdentity as f, SettingsServiceIdentity as g, StatsServiceIdentity as h, BoundProperty as i, LinkToEntityPropertyLine as j, BuiltInsExtensionFeed as k, useProperty as l, useVector3Property as m, useColor3Property as n, useColor4Property as o, useQuaternionProperty as p, MakePropertyHook as q, useInterceptObservable as r, useEventfulState as s, useObservableState as t, useExtensionManager as u, useObservableCollection as v, useOrderedObservableCollection as w, usePollingObservable as x, useResource as y, useAsyncResource as z };
9638
- //# sourceMappingURL=index-CmULGJMc.js.map
9861
+ export { SettingsContextIdentity as $, useAsyncResource as A, ButtonLine as B, Collapse as C, DebugServiceIdentity as D, ExtensibleAccordion as E, FileUploadLine as F, useCompactMode as G, useSidePaneDockOverrides as H, Inspector as I, useAngleConverters as J, MakeTeachingMoment as K, Link as L, MakeLazyComponent as M, NumberDropdownPropertyLine as N, MakeDialogTeachingMoment as O, PropertiesServiceIdentity as P, InterceptFunction as Q, GetPropertyDescriptor as R, SwitchPropertyLine as S, ToolsServiceIdentity as T, IsPropertyReadonly as U, InterceptProperty as V, ObservableCollection as W, ConstructorFactory as X, SceneContextIdentity as Y, SelectionServiceIdentity as Z, SelectionServiceDefinition as _, SyncedSliderPropertyLine as a, ShowInspector as a0, AccordionSection as a1, Accordion as a2, Button as a3, Checkbox as a4, ColorPickerPopup as a5, InputHexField as a6, InputHsvField as a7, ComboBox as a8, DraggableLine as a9, Color4PropertyLine as aA, HexPropertyLine as aB, TextInputPropertyLine as aC, NumberInputPropertyLine as aD, LinkPropertyLine as aE, PropertyLine as aF, LineContainer as aG, PlaceholderPropertyLine as aH, SpinButtonPropertyLine as aI, StringifiedPropertyLine as aJ, TextAreaPropertyLine as aK, TextPropertyLine as aL, RotationVectorPropertyLine as aM, QuaternionPropertyLine as aN, Vector2PropertyLine as aO, Vector3PropertyLine as aP, Vector4PropertyLine as aQ, Dropdown as aa, NumberDropdown as ab, StringDropdown as ac, FactorGradientComponent as ad, Color3GradientComponent as ae, Color4GradientComponent as af, ColorStepGradientComponent as ag, InfoLabel as ah, List as ai, MessageBar as aj, PositionedPopover as ak, SearchBar as al, SearchBox as am, SpinButton as an, Switch as ao, SyncedSliderInput as ap, Textarea as aq, TextInput as ar, ToggleButton as as, FactorGradientList as at, Color3GradientList as au, Color4GradientList as av, Pane as aw, BooleanBadgePropertyLine as ax, CheckboxPropertyLine as ay, Color3PropertyLine as az, ShellServiceIdentity as b, MakePopoverTeachingMoment as c, TeachingMoment as d, SidePaneContainer as e, SceneExplorerServiceIdentity as f, SettingsServiceIdentity as g, StatsServiceIdentity as h, StringDropdownPropertyLine as i, BoundProperty as j, LinkToEntityPropertyLine as k, BuiltInsExtensionFeed as l, useProperty as m, useVector3Property as n, useColor3Property as o, useColor4Property as p, useQuaternionProperty as q, MakePropertyHook as r, useInterceptObservable as s, useEventfulState as t, useExtensionManager as u, useObservableState as v, useObservableCollection as w, useOrderedObservableCollection as x, usePollingObservable as y, useResource as z };
9862
+ //# sourceMappingURL=index-B-XOu4uI.js.map