@babylonjs/inspector 8.47.0-preview → 8.47.1-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.
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { createContext, forwardRef, useContext, useState, useCallback, Component, useMemo, useEffect, useRef, isValidElement, cloneElement, Children, useLayoutEffect, useImperativeHandle, createElement, Suspense, memo, Fragment as Fragment$1, useReducer, lazy } from 'react';
3
- import { tokens, makeStyles, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, Body1Strong, Tooltip, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, createLightTheme, createDarkTheme, FluentProvider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Portal, RendererProvider, createDOMRenderer, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, treeItemLevelToken, MenuItemCheckbox, Switch as Switch$1, PresenceBadge, useId, SpinButton as SpinButton$1, Slider, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, webDarkTheme, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, Subtitle2, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, Field } from '@fluentui/react-components';
4
- import { ErrorCircleRegular, ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, CopyRegular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, AddRegular, DeleteRegular, FullScreenMaximizeRegular, ArrowMinimizeRegular, LineHorizontal1Regular, ChevronDoubleLeftRegular, ChevronDoubleRightRegular, ChevronDownRegular, ChevronRightRegular, CircleSmallFilled, ArrowDownloadRegular, SaveRegular, PreviousRegular, ArrowPreviousRegular, TriangleLeftRegular, RecordStopRegular, PlayRegular, ArrowNextRegular, NextRegular, EditRegular, LinkDismissRegular, LinkEditRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, EyeRegular, StopRegular, CloudArrowUpRegular, CloudArrowDownRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, EyeOffRegular, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, AppGenericRegular, RectangleLandscapeRegular, BorderOutsideRegular, BorderNoneRegular, MyLocationRegular, CameraRegular, LightbulbRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
3
+ import { tokens, makeStyles, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, Body1Strong, Tooltip, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, createLightTheme, createDarkTheme, FluentProvider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Portal, RendererProvider, createDOMRenderer, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, treeItemLevelToken, MenuItemCheckbox, Switch as Switch$1, useId, SpinButton as SpinButton$1, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, PresenceBadge, Slider, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, webDarkTheme, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, Subtitle2, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, Field } from '@fluentui/react-components';
4
+ import { ErrorCircleRegular, ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, CopyRegular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, ArrowDownloadRegular, StopRegular, RecordRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, AddRegular, DeleteRegular, FullScreenMaximizeRegular, ArrowMinimizeRegular, LineHorizontal1Regular, ChevronDoubleLeftRegular, ChevronDoubleRightRegular, ChevronDownRegular, ChevronRightRegular, CircleSmallFilled, SaveRegular, PreviousRegular, ArrowPreviousRegular, TriangleLeftRegular, RecordStopRegular, PlayRegular, ArrowNextRegular, NextRegular, EditRegular, LinkDismissRegular, LinkEditRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, EyeRegular, CloudArrowUpRegular, CloudArrowDownRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, EyeOffRegular, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, AppGenericRegular, RectangleLandscapeRegular, BorderOutsideRegular, BorderNoneRegular, MyLocationRegular, CameraRegular, LightbulbRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
5
5
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
6
6
  import { Vector3, Quaternion, Matrix, Vector2, Vector4, TmpVectors } from '@babylonjs/core/Maths/math.vector.js';
7
7
  import { Observable } from '@babylonjs/core/Misc/observable.js';
@@ -32,6 +32,8 @@ import '@babylonjs/core/Engines/WebGPU/Extensions/engine.query.js';
32
32
  import { PerfCollectionStrategy } from '@babylonjs/core/Misc/PerformanceViewer/performanceViewerCollectionStrategies.js';
33
33
  import '@babylonjs/core/Misc/PerformanceViewer/performanceViewerSceneExtension.js';
34
34
  import { PressureObserverWrapper } from '@babylonjs/core/Misc/pressureObserverWrapper.js';
35
+ import { Scalar } from '@babylonjs/core/Maths/math.scalar.js';
36
+ import { PerformanceViewerCollector } from '@babylonjs/core/Misc/PerformanceViewer/performanceViewerCollector.js';
35
37
  import { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js';
36
38
  import { createRoot } from 'react-dom/client';
37
39
  import { FrameGraphUtils } from '@babylonjs/core/FrameGraph/frameGraphUtils.js';
@@ -99,6 +101,8 @@ import { PointParticleEmitter } from '@babylonjs/core/Particles/EmitterTypes/poi
99
101
  import { SphereParticleEmitter } from '@babylonjs/core/Particles/EmitterTypes/sphereParticleEmitter.js';
100
102
  import { Attractor } from '@babylonjs/core/Particles/attractor.js';
101
103
  import { CreateSphere } from '@babylonjs/core/Meshes/Builders/sphereBuilder.js';
104
+ import { ParticleInputBlock } from '@babylonjs/core/Particles/Node/Blocks/particleInputBlock.js';
105
+ import { UpdateAttractorBlock } from '@babylonjs/core/Particles/Node/Blocks/Update/updateAttractorBlock.js';
102
106
  import { NodeParticleBlockConnectionPointTypes } from '@babylonjs/core/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.js';
103
107
  import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js';
104
108
  import { PhysicsPrestepType } from '@babylonjs/core/Physics/v2/IPhysicsEnginePlugin.js';
@@ -130,6 +134,16 @@ import '@babylonjs/core/Rendering/boundingBoxRenderer.js';
130
134
  import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent.js';
131
135
  import '@babylonjs/core/Sprites/spriteSceneComponent.js';
132
136
  import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
137
+ import { captureEquirectangularFromScene } from '@babylonjs/core/Misc/equirectangularCapture.js';
138
+ import { SceneRecorder } from '@babylonjs/core/Misc/sceneRecorder.js';
139
+ import { CreateScreenshotUsingRenderTargetAsync } from '@babylonjs/core/Misc/screenshotTools.js';
140
+ import { VideoRecorder } from '@babylonjs/core/Misc/videoRecorder.js';
141
+ import { SceneSerializer } from '@babylonjs/core/Misc/sceneSerializer.js';
142
+ import { EnvironmentTextureTools } from '@babylonjs/core/Misc/environmentTextureTools.js';
143
+ import { ImportAnimationsAsync, SceneLoader } from '@babylonjs/core/Loading/sceneLoader.js';
144
+ import { FilesInput } from '@babylonjs/core/Misc/filesInput.js';
145
+ import { GLTFLoaderAnimationStartMode, GLTFLoaderCoordinateSystemMode, GLTFLoaderDefaultOptions } from '@babylonjs/loaders/glTF/glTFFileLoader.js';
146
+ import { GLTFValidation } from '@babylonjs/loaders/glTF/glTFValidation.js';
133
147
  import { EngineStore } from '@babylonjs/core/Engines/engineStore.js';
134
148
  import { UniqueIdGenerator } from '@babylonjs/core/Misc/uniqueIdGenerator.js';
135
149
  import { DebugLayer } from '@babylonjs/core/Debug/debugLayer.js';
@@ -251,7 +265,7 @@ const Button = forwardRef((props, ref) => {
251
265
  });
252
266
  Button.displayName = "Button";
253
267
 
254
- const useStyles$R = makeStyles({
268
+ const useStyles$T = makeStyles({
255
269
  root: {
256
270
  display: "flex",
257
271
  flexDirection: "column",
@@ -332,7 +346,7 @@ class ErrorBoundary extends Component {
332
346
  }
333
347
  }
334
348
  function ErrorFallback({ error, onRetry }) {
335
- const styles = useStyles$R();
349
+ const styles = useStyles$T();
336
350
  return (jsxs("div", { className: styles.root, children: [jsx(ErrorCircleRegular, { className: styles.icon }), jsx("div", { className: styles.title, children: "Something went wrong" }), jsx("div", { className: styles.message, children: "An error occurred in this component. You can try again or continue using other parts of the inspector." }), jsx(Button, { label: "Try Again", appearance: "primary", onClick: onRetry }), error && jsx("div", { className: styles.details, children: error.message })] }));
337
351
  }
338
352
 
@@ -1037,7 +1051,7 @@ const Link = forwardRef((props, ref) => {
1037
1051
  });
1038
1052
  Link.displayName = "Link";
1039
1053
 
1040
- const useStyles$Q = makeStyles({
1054
+ const useStyles$S = makeStyles({
1041
1055
  button: {
1042
1056
  display: "flex",
1043
1057
  alignItems: "center",
@@ -1055,7 +1069,7 @@ const ToggleButton = (props) => {
1055
1069
  ToggleButton.displayName = "ToggleButton";
1056
1070
  const { value, onChange, title, appearance = "subtle" } = props;
1057
1071
  const { size } = useContext(ToolContext);
1058
- const classes = useStyles$Q();
1072
+ const classes = useStyles$S();
1059
1073
  const [checked, setChecked] = useState(value);
1060
1074
  const toggle = useCallback(() => {
1061
1075
  setChecked((prev) => {
@@ -1134,6 +1148,9 @@ const usePropertyLineStyles = makeStyles({
1134
1148
  expandedContentDiv: {
1135
1149
  overflow: "hidden",
1136
1150
  },
1151
+ expandedContentDivIndented: {
1152
+ paddingLeft: tokens.spacingHorizontalM,
1153
+ },
1137
1154
  checkbox: {
1138
1155
  display: "flex",
1139
1156
  alignItems: "center",
@@ -1181,7 +1198,7 @@ const PropertyLine = forwardRef((props, ref) => {
1181
1198
  cachedVal.current = props.value;
1182
1199
  props.onChange(null);
1183
1200
  }
1184
- } }) })), jsx("div", { className: classes.childWrapper, children: processedChildren }), onCopy && !disableCopy && (jsx(Button, { className: classes.copy, title: "Copy to clipboard", appearance: "transparent", icon: size === "small" ? Copy16Regular : CopyRegular, onClick: () => copyCommandToClipboard(onCopy()) }))] })] }), expandedContent && (jsx(Collapse, { visible: !!expanded, children: jsx("div", { className: classes.expandedContentDiv, children: expandedContent }) }))] }));
1201
+ } }) })), jsx("div", { className: classes.childWrapper, children: processedChildren }), onCopy && !disableCopy && (jsx(Button, { className: classes.copy, title: "Copy to clipboard", appearance: "transparent", icon: size === "small" ? Copy16Regular : CopyRegular, onClick: () => copyCommandToClipboard(onCopy()) }))] })] }), expandedContent && (jsx(Collapse, { visible: !!expanded, children: jsx("div", { className: mergeClasses(classes.expandedContentDiv, props.indentExpandedContent ? classes.expandedContentDivIndented : undefined), children: expandedContent }) }))] }));
1185
1202
  });
1186
1203
  const useLineStyles = makeStyles({
1187
1204
  container: {
@@ -1238,7 +1255,7 @@ const LinkToEntityPropertyLine = (props) => {
1238
1255
  !linkedEntity.reservedDataStore?.hidden && jsx(LinkPropertyLine, { ...rest, value: linkedEntity.name, onLink: () => (selectionService.selectedEntity = linkedEntity) }));
1239
1256
  };
1240
1257
 
1241
- const useStyles$P = makeStyles({
1258
+ const useStyles$R = makeStyles({
1242
1259
  accordion: {
1243
1260
  overflowX: "hidden",
1244
1261
  overflowY: "auto",
@@ -1282,13 +1299,13 @@ const useStyles$P = makeStyles({
1282
1299
  });
1283
1300
  const AccordionSection = (props) => {
1284
1301
  AccordionSection.displayName = "AccordionSection";
1285
- const classes = useStyles$P();
1302
+ const classes = useStyles$R();
1286
1303
  return jsx("div", { className: classes.panelDiv, children: props.children });
1287
1304
  };
1288
1305
  const StringAccordion = Accordion$1;
1289
1306
  const Accordion = forwardRef((props, ref) => {
1290
1307
  Accordion.displayName = "Accordion";
1291
- const classes = useStyles$P();
1308
+ const classes = useStyles$R();
1292
1309
  const { size } = useContext(ToolContext);
1293
1310
  const { children, highlightSections, ...rest } = props;
1294
1311
  const validChildren = useMemo(() => {
@@ -1419,7 +1436,7 @@ const CompactModeContextProvider = (props) => {
1419
1436
  function AsReadonlyArray(array) {
1420
1437
  return array;
1421
1438
  }
1422
- const useStyles$O = makeStyles({
1439
+ const useStyles$Q = makeStyles({
1423
1440
  rootDiv: {
1424
1441
  flex: 1,
1425
1442
  overflow: "hidden",
@@ -1428,7 +1445,7 @@ const useStyles$O = makeStyles({
1428
1445
  },
1429
1446
  });
1430
1447
  function ExtensibleAccordion(props) {
1431
- const classes = useStyles$O();
1448
+ const classes = useStyles$Q();
1432
1449
  const { children, sections, sectionContent, context, sectionsRef } = props;
1433
1450
  const defaultSections = useMemo(() => {
1434
1451
  const defaultSections = [];
@@ -1533,7 +1550,7 @@ function ExtensibleAccordion(props) {
1533
1550
  })] }) })) }));
1534
1551
  }
1535
1552
 
1536
- const useStyles$N = makeStyles({
1553
+ const useStyles$P = makeStyles({
1537
1554
  paneRootDiv: {
1538
1555
  display: "flex",
1539
1556
  flex: 1,
@@ -1546,7 +1563,7 @@ const useStyles$N = makeStyles({
1546
1563
  */
1547
1564
  const SidePaneContainer = forwardRef((props, ref) => {
1548
1565
  const { className, ...rest } = props;
1549
- const classes = useStyles$N();
1566
+ const classes = useStyles$P();
1550
1567
  return (jsx("div", { className: mergeClasses(classes.paneRootDiv, className), ref: ref, ...rest, children: props.children }));
1551
1568
  });
1552
1569
 
@@ -1612,7 +1629,7 @@ const Theme = (props) => {
1612
1629
  return (jsx(FluentProvider, { theme: isDarkMode !== invert ? DarkTheme : LightTheme, applyStylesToPortals: applyStylesToPortals, ...rest, children: props.children }));
1613
1630
  };
1614
1631
 
1615
- const useStyles$M = makeStyles({
1632
+ const useStyles$O = makeStyles({
1616
1633
  extensionTeachingPopover: {
1617
1634
  maxWidth: "320px",
1618
1635
  },
@@ -1623,7 +1640,7 @@ const useStyles$M = makeStyles({
1623
1640
  * @returns The teaching moment popover.
1624
1641
  */
1625
1642
  const TeachingMoment = ({ shouldDisplay, positioningRef, onOpenChange, title, description }) => {
1626
- const classes = useStyles$M();
1643
+ const classes = useStyles$O();
1627
1644
  return (jsx(TeachingPopover, { appearance: "brand", open: shouldDisplay, positioning: { positioningRef }, onOpenChange: onOpenChange, children: jsxs(TeachingPopoverSurface, { className: classes.extensionTeachingPopover, children: [jsx(TeachingPopoverHeader, { children: title }), jsx(TeachingPopoverBody, { children: description })] }) }));
1628
1645
  };
1629
1646
 
@@ -1878,13 +1895,13 @@ function ConstructorFactory(constructor) {
1878
1895
  return (...args) => new constructor(...args);
1879
1896
  }
1880
1897
 
1881
- const useStyles$L = makeStyles({
1898
+ const useStyles$N = makeStyles({
1882
1899
  placeholderDiv: {
1883
1900
  padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
1884
1901
  },
1885
1902
  });
1886
1903
  const PropertiesPane = (props) => {
1887
- const classes = useStyles$L();
1904
+ const classes = useStyles$N();
1888
1905
  const entity = props.context;
1889
1906
  return entity != null ? (jsx(ExtensibleAccordion, { ...props })) : (jsx("div", { className: classes.placeholderDiv, children: jsx(Body1Strong, { italic: true, children: "No entity selected." }) }));
1890
1907
  };
@@ -1909,14 +1926,6 @@ function ToFeaturesString(options) {
1909
1926
  features.push({ key: "location", value: "no" });
1910
1927
  return features.map((feature) => `${feature.key}=${feature.value}`).join(",");
1911
1928
  }
1912
- const useStyles$K = makeStyles({
1913
- container: {
1914
- display: "flex",
1915
- flexGrow: 1,
1916
- flexDirection: "column",
1917
- overflow: "hidden",
1918
- },
1919
- });
1920
1929
  /**
1921
1930
  * Allows displaying a child window that can contain child components.
1922
1931
  * @param props Props for the child window.
@@ -1924,7 +1933,6 @@ const useStyles$K = makeStyles({
1924
1933
  */
1925
1934
  const ChildWindow = (props) => {
1926
1935
  const { id, children, onOpenChange, imperativeRef: imperativeRef } = props;
1927
- const classes = useStyles$K();
1928
1936
  const [windowState, setWindowState] = useState();
1929
1937
  const [childWindow, setChildWindow] = useState();
1930
1938
  const storageKey = id ? `Babylon/Settings/ChildWindow/${id}/Bounds` : null;
@@ -2058,7 +2066,12 @@ const ChildWindow = (props) => {
2058
2066
  const { mountNode, renderer } = windowState;
2059
2067
  return (
2060
2068
  // Portal targets the body of the child window.
2061
- jsx(Portal, { mountNode: mountNode, children: jsx(RendererProvider, { renderer: renderer, targetDocument: mountNode.ownerDocument, children: jsx(FluentProvider, { className: classes.container, applyStylesToPortals: false, targetDocument: mountNode.ownerDocument, children: children }) }) }));
2069
+ jsx(Portal, { mountNode: mountNode, children: jsx(RendererProvider, { renderer: renderer, targetDocument: mountNode.ownerDocument, children: jsx(FluentProvider, { style: {
2070
+ display: "flex",
2071
+ flexGrow: 1,
2072
+ flexDirection: "column",
2073
+ overflow: "hidden",
2074
+ }, applyStylesToPortals: false, targetDocument: mountNode.ownerDocument, children: children }) }) }));
2062
2075
  };
2063
2076
 
2064
2077
  // NOTE: This is basically a super simplified version of https://github.com/microsoft/fluentui-contrib/blob/main/packages/react-resize-handle/src/hooks
@@ -2136,7 +2149,7 @@ function useResizeHandle(params) {
2136
2149
 
2137
2150
  const RootComponentServiceIdentity = Symbol("RootComponent");
2138
2151
  const ShellServiceIdentity = Symbol("ShellService");
2139
- const useStyles$J = makeStyles({
2152
+ const useStyles$M = makeStyles({
2140
2153
  mainView: {
2141
2154
  flex: 1,
2142
2155
  display: "flex",
@@ -2331,12 +2344,12 @@ const DockMenu = (props) => {
2331
2344
  };
2332
2345
  const PaneHeader = (props) => {
2333
2346
  const { id, title, dockOptions } = props;
2334
- const classes = useStyles$J();
2347
+ const classes = useStyles$M();
2335
2348
  return (jsxs("div", { className: classes.paneHeaderDiv, children: [jsx(Subtitle2Stronger, { className: classes.paneHeaderText, children: title }), jsx(DockMenu, { sidePaneId: id, dockOptions: dockOptions, children: jsx(Button$1, { className: classes.paneHeaderButton, appearance: "transparent", icon: jsx(MoreHorizontalRegular, {}) }) })] }));
2336
2349
  };
2337
2350
  // This is a wrapper for an item in a toolbar that simply adds a teaching moment, which is useful for dynamically added items, possibly from extensions.
2338
2351
  const ToolbarItem = ({ verticalLocation, horizontalLocation, id, component: Component, displayName: displayName, suppressTeachingMoment }) => {
2339
- const classes = useStyles$J();
2352
+ const classes = useStyles$M();
2340
2353
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Bar/${verticalLocation}/${horizontalLocation}/${displayName ?? id}`), [displayName, id]);
2341
2354
  const teachingMoment = useTeachingMoment(suppressTeachingMoment);
2342
2355
  return (jsxs(Fragment, { children: [jsx(TeachingMoment, { ...teachingMoment, shouldDisplay: teachingMoment.shouldDisplay && !suppressTeachingMoment, title: displayName ?? "Extension", description: `The "${displayName ?? id}" extension can be accessed here.` }), jsx("div", { className: classes.barItem, ref: teachingMoment.targetRef, children: jsx(Component, {}) })] }));
@@ -2344,7 +2357,7 @@ const ToolbarItem = ({ verticalLocation, horizontalLocation, id, component: Comp
2344
2357
  // TODO: Handle overflow, possibly via https://react.fluentui.dev/?path=/docs/components-overflow--docs with priority.
2345
2358
  // This component just renders a toolbar with left aligned toolbar items on the left and right aligned toolbar items on the right.
2346
2359
  const Toolbar = ({ location, components }) => {
2347
- const classes = useStyles$J();
2360
+ const classes = useStyles$M();
2348
2361
  const leftComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "left"), [components]);
2349
2362
  const rightComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "right"), [components]);
2350
2363
  return (jsx(Fragment, { children: components.length > 0 && (jsxs("div", { className: `${classes.bar} ${location === "top" ? classes.barTop : null}`, children: [jsx("div", { className: classes.barLeft, children: leftComponents.map((entry) => (jsx(ToolbarItem, { verticalLocation: location, horizontalLocation: entry.horizontalLocation, id: entry.key, component: entry.component, displayName: entry.displayName, suppressTeachingMoment: entry.suppressTeachingMoment }, entry.key))) }), jsx("div", { className: classes.barRight, children: rightComponents.map((entry) => (jsx(ToolbarItem, { verticalLocation: location, horizontalLocation: entry.horizontalLocation, id: entry.key, component: entry.component, displayName: entry.displayName, suppressTeachingMoment: entry.suppressTeachingMoment }, entry.key))) })] })) }));
@@ -2354,7 +2367,7 @@ const SidePaneTab = (props) => {
2354
2367
  const { location, id, isSelected, dockOptions,
2355
2368
  // eslint-disable-next-line @typescript-eslint/naming-convention
2356
2369
  icon: Icon, title, suppressTeachingMoment, } = props;
2357
- const classes = useStyles$J();
2370
+ const classes = useStyles$M();
2358
2371
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Pane/${location}/${title ?? id}`), [title, id]);
2359
2372
  const teachingMoment = useTeachingMoment(suppressTeachingMoment);
2360
2373
  const tabClass = mergeClasses(classes.tab, isSelected ? classes.selectedTab : classes.unselectedTab);
@@ -2367,7 +2380,7 @@ const SidePaneTab = (props) => {
2367
2380
  // In "compact" mode, the tab list is integrated into the pane itself.
2368
2381
  // In "full" mode, the returned tab list is later injected into the toolbar.
2369
2382
  function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane, dockOperations, toolbarMode, topBarItems, bottomBarItems) {
2370
- const classes = useStyles$J();
2383
+ const classes = useStyles$M();
2371
2384
  const [topSelectedTab, setTopSelectedTab] = useState();
2372
2385
  const [bottomSelectedTab, setBottomSelectedTab] = useState();
2373
2386
  const [collapsed, setCollapsed] = useState(false);
@@ -2558,7 +2571,7 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
2558
2571
  undock: () => onDockChanged.notifyObservers({ location: "right", dock: false }),
2559
2572
  };
2560
2573
  const rootComponent = () => {
2561
- const classes = useStyles$J();
2574
+ const classes = useStyles$M();
2562
2575
  const [sidePaneDockOverrides, setSidePaneDockOverrides] = useSidePaneDockOverrides();
2563
2576
  // This function returns a promise that resolves after the dock change takes effect so that
2564
2577
  // we can then select the re-docked pane.
@@ -2993,7 +3006,7 @@ function useCommandContextMenuState(commands) {
2993
3006
  checkmark: command.icon ? null : undefined, icon: command.icon ? jsx(command.icon, {}) : undefined, name: "toggleCommands", value: command.displayName, children: command.displayName }, command.displayName)));
2994
3007
  return [checkedContextMenuItems, onContextMenuCheckedValueChange, contextMenuItems];
2995
3008
  }
2996
- const useStyles$I = makeStyles({
3009
+ const useStyles$L = makeStyles({
2997
3010
  rootDiv: {
2998
3011
  flex: 1,
2999
3012
  overflow: "hidden",
@@ -3061,14 +3074,14 @@ function MakeInlineCommandElement(command, isPlaceholder) {
3061
3074
  }
3062
3075
  const SceneTreeItem = (props) => {
3063
3076
  const { isSelected, select } = props;
3064
- const classes = useStyles$I();
3077
+ const classes = useStyles$L();
3065
3078
  const [compactMode] = useCompactMode();
3066
3079
  const treeItemLayoutClass = mergeClasses(classes.sceneTreeItemLayout, compactMode ? classes.treeItemLayoutCompact : undefined);
3067
3080
  return (jsx(FlatTreeItem, { value: "scene", itemType: "leaf", parentValue: undefined, "aria-level": 1, "aria-setsize": 1, "aria-posinset": 1, onClick: select, children: jsx(TreeItemLayout, { iconBefore: jsx(GlobeRegular, {}), className: treeItemLayoutClass, style: isSelected ? { backgroundColor: tokens.colorNeutralBackground1Selected } : undefined, children: jsx(Body1Strong, { wrap: false, truncate: true, children: "Scene" }) }) }, "scene"));
3068
3081
  };
3069
3082
  const SectionTreeItem = (props) => {
3070
3083
  const { section, isFiltering, commandProviders, expandAll, collapseAll } = props;
3071
- const classes = useStyles$I();
3084
+ const classes = useStyles$L();
3072
3085
  const [compactMode] = useCompactMode();
3073
3086
  // Get the commands that apply to this section.
3074
3087
  const commands = useResource(useCallback(() => {
@@ -3085,7 +3098,7 @@ const SectionTreeItem = (props) => {
3085
3098
  };
3086
3099
  const EntityTreeItem = (props) => {
3087
3100
  const { entityItem, isSelected, select, isFiltering, commandProviders, expandAll, collapseAll } = props;
3088
- const classes = useStyles$I();
3101
+ const classes = useStyles$L();
3089
3102
  const [compactMode] = useCompactMode();
3090
3103
  const hasChildren = !!entityItem.children?.length;
3091
3104
  const displayInfo = useResource(useCallback(() => {
@@ -3178,7 +3191,7 @@ const EntityTreeItem = (props) => {
3178
3191
  }, children: jsx(Body1, { wrap: false, truncate: true, children: name }) }) }, entityItem.entity.uniqueId) }), jsx(MenuPopover, { hidden: !hasChildren && contextMenuCommands.length === 0, children: jsxs(MenuList, { children: [hasChildren && (jsxs(Fragment, { children: [jsx(MenuItem, { icon: jsx(ArrowExpandAllRegular, {}), onClick: expandAll, children: jsx(Body1, { children: "Expand All" }) }), jsx(MenuItem, { icon: jsx(ArrowCollapseAllRegular, {}), onClick: collapseAll, children: jsx(Body1, { children: "Collapse All" }) })] })), hasChildren && contextMenuCommands.length > 0 && jsx(MenuDivider, {}), contextMenuItems] }) })] }));
3179
3192
  };
3180
3193
  const SceneExplorer = (props) => {
3181
- const classes = useStyles$I();
3194
+ const classes = useStyles$L();
3182
3195
  const { sections, entityCommandProviders, sectionCommandProviders, scene, selectedEntity } = props;
3183
3196
  const [openItems, setOpenItems] = useState(new Set());
3184
3197
  const [sceneVersion, setSceneVersion] = useState(0);
@@ -3494,10 +3507,10 @@ const Switch = (props) => {
3494
3507
  }
3495
3508
  }, [props.value]);
3496
3509
  const onChange = (event, _) => {
3497
- props.onChange && props.onChange(props.invertedMode ? !event.target.checked : event.target.checked);
3510
+ props.onChange && props.onChange(event.target.checked);
3498
3511
  setChecked(event.target.checked);
3499
3512
  };
3500
- return (jsx(Switch$1, { className: mergeClasses(classes.switch, size === "small" && classes.switchSmall), indicator: { className: classes.indicator }, checked: props.invertedMode ? !checked : checked, disabled: props.disabled, onChange: onChange }));
3513
+ return (jsx(Switch$1, { className: mergeClasses(classes.switch, size === "small" && classes.switchSmall), indicator: { className: classes.indicator }, checked: checked, disabled: props.disabled, onChange: onChange }));
3501
3514
  };
3502
3515
 
3503
3516
  /**
@@ -3874,785 +3887,2084 @@ const FileUploadLine = ({ onClick, label, accept, ...buttonProps }) => {
3874
3887
  return (jsx(LineContainer, { children: jsx(UploadButton, { onUpload: onClick, accept: accept, label: label, ...buttonProps }) }));
3875
3888
  };
3876
3889
 
3877
- var PerfMetadataCategory;
3878
- (function (PerfMetadataCategory) {
3879
- PerfMetadataCategory["Count"] = "Count";
3880
- PerfMetadataCategory["FrameSteps"] = "Frame Steps Duration";
3881
- })(PerfMetadataCategory || (PerfMetadataCategory = {}));
3882
- // list of strategies to add to perf graph automatically.
3883
- const DefaultStrategiesList = [
3884
- { strategyCallback: PerfCollectionStrategy.FpsStrategy() },
3885
- { strategyCallback: PerfCollectionStrategy.TotalMeshesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3886
- { strategyCallback: PerfCollectionStrategy.ActiveMeshesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3887
- { strategyCallback: PerfCollectionStrategy.ActiveIndicesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3888
- { strategyCallback: PerfCollectionStrategy.ActiveBonesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3889
- { strategyCallback: PerfCollectionStrategy.ActiveParticlesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3890
- { strategyCallback: PerfCollectionStrategy.DrawCallsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3891
- { strategyCallback: PerfCollectionStrategy.TotalLightsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3892
- { strategyCallback: PerfCollectionStrategy.TotalVerticesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3893
- { strategyCallback: PerfCollectionStrategy.TotalMaterialsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3894
- { strategyCallback: PerfCollectionStrategy.TotalTexturesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
3895
- { strategyCallback: PerfCollectionStrategy.AbsoluteFpsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3896
- { strategyCallback: PerfCollectionStrategy.MeshesSelectionStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3897
- { strategyCallback: PerfCollectionStrategy.RenderTargetsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3898
- { strategyCallback: PerfCollectionStrategy.ParticlesStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3899
- { strategyCallback: PerfCollectionStrategy.SpritesStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3900
- { strategyCallback: PerfCollectionStrategy.AnimationsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3901
- { strategyCallback: PerfCollectionStrategy.PhysicsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3902
- { strategyCallback: PerfCollectionStrategy.RenderStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3903
- { strategyCallback: PerfCollectionStrategy.FrameTotalStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3904
- { strategyCallback: PerfCollectionStrategy.InterFrameStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3905
- { strategyCallback: PerfCollectionStrategy.GpuFrameTimeStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
3906
- ];
3907
- const PerformanceStats = ({ context: scene }) => {
3908
- const [isOpen, setIsOpen] = useState(false);
3909
- const [isLoadedFromCsv, setIsLoadedFromCsv] = useState(false);
3910
- const [performanceCollector, setPerformanceCollector] = useState();
3911
- useEffect(() => {
3912
- if (!isLoadedFromCsv) {
3913
- if (performanceCollector) {
3914
- performanceCollector.stop();
3915
- performanceCollector.clear(false);
3916
- addStrategies(performanceCollector);
3917
- }
3890
+ /**
3891
+ * Defines the supported timestamp units.
3892
+ */
3893
+ var TimestampUnit;
3894
+ (function (TimestampUnit) {
3895
+ TimestampUnit[TimestampUnit["Milliseconds"] = 0] = "Milliseconds";
3896
+ TimestampUnit[TimestampUnit["Seconds"] = 1] = "Seconds";
3897
+ TimestampUnit[TimestampUnit["Minutes"] = 2] = "Minutes";
3898
+ TimestampUnit[TimestampUnit["Hours"] = 3] = "Hours";
3899
+ })(TimestampUnit || (TimestampUnit = {}));
3900
+
3901
+ const DefaultColor = "#000";
3902
+ const AxisColor = "#c0c4c8";
3903
+ const FutureBoxColor = "#dfe9ed";
3904
+ const DividerColor = "#0a3066";
3905
+ const PlayheadColor = "#b9dbef";
3906
+ const PositionIndicatorColor = "#4d5960";
3907
+ const TooltipBackgroundColor = "#566268";
3908
+ const TooltipForegroundColor = "#fbfbfb";
3909
+ const TopOfGraphY = 0;
3910
+ const DefaultAlpha = 1;
3911
+ const TooltipBackgroundAlpha = 0.8;
3912
+ const BackgroundLineAlpha = 0.2;
3913
+ const MaxDistanceForHover = 10;
3914
+ const TooltipHorizontalPadding = 10;
3915
+ const SpaceBetweenTextAndBox = 5;
3916
+ const TooltipPaddingFromBottom = 20;
3917
+ // height of indicator triangle
3918
+ const TriangleHeight = 10;
3919
+ // width of indicator triangle
3920
+ const TriangleWidth = 20;
3921
+ // padding to indicate how far below the axis line the triangle should be.
3922
+ const TrianglePaddingFromAxisLine = 3;
3923
+ const TickerHorizontalPadding = 10;
3924
+ // pixels to pad the top and bottom of data so that it doesn't get cut off by the margins.
3925
+ const DataPadding = 2;
3926
+ const PlayheadSize = 8;
3927
+ const DividerSize = 2;
3928
+ const AxisLineLength = 10;
3929
+ const AxisPadding = 10;
3930
+ // Currently the scale factor is a constant but when we add panning this may become formula based.
3931
+ const ScaleFactor = 0.8;
3932
+ // This controls the scale factor at which we stop drawing the playhead. Below this value there tends to be flickering of the playhead as data comes in.
3933
+ const StopDrawingPlayheadThreshold = 0.95;
3934
+ // Threshold for the ratio at which we go from panning mode to live mode.
3935
+ const ReturnToLiveThreshold = 0.998;
3936
+ // Font to use on the addons such as tooltips and tickers!
3937
+ const GraphAddonFont = "12px Arial";
3938
+ // A string containing the alphabet, used in line height calculation for the font.
3939
+ const Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
3940
+ // Arbitrary maximum used to make some GC optimizations.
3941
+ const MaximumDatasetsAllowed = 64;
3942
+ const MsInSecond = 1000;
3943
+ const MsInMinute = MsInSecond * 60;
3944
+ const MsInHour = MsInMinute * 60;
3945
+ // time in ms to wait between tooltip draws inside the mouse move.
3946
+ const TooltipDebounceTime = 32;
3947
+ // time in ms to wait between draws
3948
+ const DrawThrottleTime = 15;
3949
+ // What distance percentage in the x axis between two points makes us break the line and draw a "no data" box instead
3950
+ const MaxXDistancePercBetweenLinePoints = 0.1;
3951
+ // Color used to draw the rectangle that indicates no collection of data
3952
+ const NoDataRectangleColor = "#aaaaaa";
3953
+ const SmoothingFactor = 0.2; // factor to smooth the graph with
3954
+ const RangeMargin = 0.1; // extra margin to expand the min/max range on the graph
3955
+ /**
3956
+ * This function will debounce calls to functions.
3957
+ *
3958
+ * @param callback callback to call.
3959
+ * @param time time to wait between calls in ms.
3960
+ * @returns a function that will call the callback after the time has passed.
3961
+ */
3962
+ function Debounce(callback, time) {
3963
+ let timerId;
3964
+ return function (...args) {
3965
+ clearTimeout(timerId);
3966
+ timerId = setTimeout(() => callback(...args), time);
3967
+ };
3968
+ }
3969
+ /**
3970
+ * This function will throttle calls to functions.
3971
+ *
3972
+ * @param callback callback to call.
3973
+ * @param time time to wait between calls in ms.
3974
+ * @returns a function that will call the callback after the time has passed.
3975
+ */
3976
+ function Throttle(callback, time) {
3977
+ let lastCalledTime = 0;
3978
+ return function (...args) {
3979
+ const now = Date.now();
3980
+ if (now - lastCalledTime < time) {
3981
+ return;
3918
3982
  }
3919
- }, [isLoadedFromCsv]);
3920
- const onPerformanceButtonClick = () => {
3921
- setIsOpen(true);
3922
- performanceCollector?.start(true);
3983
+ lastCalledTime = now;
3984
+ callback(...args);
3923
3985
  };
3924
- const onLoadClick = (fileList) => {
3925
- Tools.ReadFile(fileList[0], (data) => {
3926
- // reopen window and load data!
3927
- setIsOpen(false);
3928
- setIsLoadedFromCsv(true);
3929
- performanceCollector?.stop();
3930
- const isValid = performanceCollector?.loadFromFileData(data);
3931
- if (!isValid) {
3932
- // if our data isnt valid we close the window.
3933
- setIsOpen(false);
3934
- performanceCollector?.start(true);
3986
+ }
3987
+ /**
3988
+ * This class acts as the main API for graphing. Here is where you will find methods to let the service know new data needs to be drawn,
3989
+ * let it know something has been resized, etc!
3990
+ */
3991
+ class CanvasGraphService {
3992
+ /**
3993
+ * Creates an instance of CanvasGraphService.
3994
+ *
3995
+ * @param canvas a pointer to the canvas dom element we would like to write to.
3996
+ * @param settings settings for our service.
3997
+ */
3998
+ constructor(canvas, settings) {
3999
+ this._sizeOfWindow = 300;
4000
+ /**
4001
+ * This method lets the service know it should get ready to update what it is displaying.
4002
+ */
4003
+ this.update = Throttle(() => this._draw(), DrawThrottleTime);
4004
+ this._prevPointById = new Map();
4005
+ this._prevValueById = new Map();
4006
+ /**
4007
+ * Handles what to do when we are hovering over the canvas and not panning.
4008
+ *
4009
+ * @param event A reference to the event to be handled.
4010
+ */
4011
+ this._handleDataHover = (event) => {
4012
+ if (this._panPosition) {
4013
+ // we don't want to do anything if we are in the middle of panning
4014
+ return;
3935
4015
  }
3936
- });
3937
- };
3938
- const onExportClick = () => {
3939
- performanceCollector?.exportDataToCsv();
3940
- };
3941
- const onToggleRecording = () => {
3942
- if (!performanceCollector?.isStarted) {
3943
- performanceCollector?.start(true);
4016
+ this._hoverPosition = { xPos: event.clientX, yPos: event.clientY };
4017
+ // process and draw the tooltip.
4018
+ this._debouncedTooltip(this._hoverPosition, this._drawableArea);
4019
+ };
4020
+ /**
4021
+ * Debounced processing and drawing of tooltip.
4022
+ */
4023
+ this._debouncedTooltip = Debounce((pos, drawableArea) => {
4024
+ this._preprocessTooltip(pos, drawableArea);
4025
+ this._drawTooltip(pos, drawableArea);
4026
+ }, TooltipDebounceTime);
4027
+ /**
4028
+ * Handles what to do when we stop hovering over the canvas.
4029
+ */
4030
+ this._handleStopHover = () => {
4031
+ this._hoverPosition = null;
4032
+ };
4033
+ /**
4034
+ * The handler for when we want to zoom in and out of the graph.
4035
+ *
4036
+ * @param event a mouse wheel event.
4037
+ */
4038
+ this._handleZoom = (event) => {
4039
+ event.preventDefault();
4040
+ if (!event.deltaY) {
4041
+ return;
4042
+ }
4043
+ const amount = ((event.deltaY * -0.01) | 0) * 100;
4044
+ const minZoom = 60;
4045
+ // The max zoom is the number of slices.
4046
+ const maxZoom = this._getNumberOfSlices();
4047
+ if (this._shouldBecomeRealtime()) {
4048
+ this._position = null;
4049
+ }
4050
+ // Bind the zoom between [minZoom, maxZoom]
4051
+ this._sizeOfWindow = Scalar.Clamp(this._sizeOfWindow - amount, minZoom, maxZoom);
4052
+ };
4053
+ /**
4054
+ * Initializes the panning object and attaches appropriate listener.
4055
+ *
4056
+ * @param event the mouse event containing positional information.
4057
+ */
4058
+ this._handlePanStart = (event) => {
4059
+ const { _ctx: ctx } = this;
4060
+ if (!ctx || !ctx.canvas) {
4061
+ return;
4062
+ }
4063
+ const canvas = ctx.canvas;
4064
+ this._panPosition = {
4065
+ xPos: event.clientX,
4066
+ delta: 0,
4067
+ };
4068
+ this._hoverPosition = null;
4069
+ canvas.addEventListener("mousemove", this._handlePan);
4070
+ };
4071
+ /**
4072
+ * While panning this event will keep track of the delta and update the "positions".
4073
+ *
4074
+ * @param event The mouse event that contains positional information.
4075
+ */
4076
+ this._handlePan = (event) => {
4077
+ if (!this._panPosition || this._getNumberOfSlices() === 0) {
4078
+ return;
4079
+ }
4080
+ const pixelDelta = this._panPosition.delta + event.clientX - this._panPosition.xPos;
4081
+ const pixelsPerItem = (this._drawableArea.right - this._drawableArea.left) / this._sizeOfWindow;
4082
+ const itemsDelta = (pixelDelta / pixelsPerItem) | 0;
4083
+ const pos = this._position ?? this._getNumberOfSlices() - 1;
4084
+ // update our position without allowing the user to pan more than they need to (approximation)
4085
+ this._position = Scalar.Clamp(pos - itemsDelta, Math.floor(this._sizeOfWindow * ScaleFactor), this._getNumberOfSlices() - Math.floor(this._sizeOfWindow * (1 - ScaleFactor)));
4086
+ if (itemsDelta === 0) {
4087
+ this._panPosition.delta += pixelDelta;
4088
+ }
4089
+ else {
4090
+ this._panPosition.delta = 0;
4091
+ }
4092
+ this._panPosition.xPos = event.clientX;
4093
+ this._prevPointById.clear();
4094
+ this._prevValueById.clear();
4095
+ };
4096
+ /**
4097
+ * Clears the panning object and removes the appropriate listener.
4098
+ */
4099
+ this._handlePanStop = () => {
4100
+ const { _ctx: ctx } = this;
4101
+ if (!ctx || !ctx.canvas) {
4102
+ return;
4103
+ }
4104
+ // check if we should return to realtime.
4105
+ if (this._shouldBecomeRealtime()) {
4106
+ this._position = null;
4107
+ }
4108
+ const canvas = ctx.canvas;
4109
+ canvas.removeEventListener("mousemove", this._handlePan);
4110
+ this._panPosition = null;
4111
+ };
4112
+ this._ctx = canvas.getContext && canvas.getContext("2d");
4113
+ this._width = canvas.width;
4114
+ this._height = canvas.height;
4115
+ this._ticks = [];
4116
+ this._panPosition = null;
4117
+ this._hoverPosition = null;
4118
+ this._position = null;
4119
+ this._datasetBounds = { start: 0, end: 0 };
4120
+ this._globalTimeMinMax = { min: Infinity, max: 0 };
4121
+ this._drawableArea = { top: 0, left: 0, right: 0, bottom: 0 };
4122
+ this._tooltipTextCache = { text: "", width: 0 };
4123
+ this._tickerTextCache = { text: "", width: 0 };
4124
+ this._tooltipItems = [];
4125
+ this._tickerItems = [];
4126
+ this._preprocessedTooltipInfo = { focusedId: "", longestText: "", numberOfTooltipItems: 0, xForActualTimestamp: 0 };
4127
+ this._numberOfTickers = 0;
4128
+ this._onVisibleRangeChangedObservable = settings.onVisibleRangeChangedObservable;
4129
+ for (let i = 0; i < MaximumDatasetsAllowed; i++) {
4130
+ this._tooltipItems.push({ text: "", color: "" });
4131
+ this._tickerItems.push({ text: "", id: "", max: 0, min: 0 });
4132
+ }
4133
+ if (!this._ctx) {
4134
+ throw Error("No canvas context accessible");
4135
+ }
4136
+ const defaultMetrics = this._ctx.measureText(Alphabet);
4137
+ this._defaultLineHeight = defaultMetrics.actualBoundingBoxAscent + defaultMetrics.actualBoundingBoxDescent;
4138
+ this._axisHeight = AxisLineLength + AxisPadding + this._defaultLineHeight + AxisPadding;
4139
+ this._ctx.save();
4140
+ this._ctx.font = GraphAddonFont;
4141
+ const fontMetrics = this._ctx.measureText(Alphabet);
4142
+ this._addonFontLineHeight = fontMetrics.actualBoundingBoxAscent + fontMetrics.actualBoundingBoxDescent;
4143
+ this._ctx.restore();
4144
+ this.datasets = settings.datasets;
4145
+ this.metadata = new Map();
4146
+ this._attachEventListeners(canvas);
4147
+ }
4148
+ /**
4149
+ * Update the canvas graph service with the new height and width of the canvas.
4150
+ * @param size The new size of the canvas.
4151
+ */
4152
+ resize(size) {
4153
+ const { _ctx: ctx } = this;
4154
+ const { width, height } = size;
4155
+ if (!ctx || !ctx.canvas) {
4156
+ return;
3944
4157
  }
3945
- else {
3946
- performanceCollector?.stop();
4158
+ this._width = width;
4159
+ this._height = height;
4160
+ ctx.canvas.width = width;
4161
+ ctx.canvas.height = height;
4162
+ this.update();
4163
+ }
4164
+ /**
4165
+ * Force resets the position in the data, effectively returning to the most current data.
4166
+ */
4167
+ resetDataPosition() {
4168
+ this._position = null;
4169
+ }
4170
+ /**
4171
+ * This method draws the data and sets up the appropriate scales.
4172
+ */
4173
+ _draw() {
4174
+ const { _ctx: ctx } = this;
4175
+ if (!ctx) {
4176
+ return;
3947
4177
  }
3948
- };
3949
- const addStrategies = (perfCollector) => {
3950
- perfCollector.addCollectionStrategies(...DefaultStrategiesList);
3951
- if (PressureObserverWrapper.IsAvailable) {
3952
- // Do not enable for now as the Pressure API does not
3953
- // report factors at the moment.
3954
- // perfCollector.addCollectionStrategies({
3955
- // strategyCallback: PerfCollectionStrategy.ThermalStrategy(),
3956
- // category: IPerfMetadataCategory.FrameSteps,
3957
- // hidden: true,
3958
- // });
3959
- // perfCollector.addCollectionStrategies({
3960
- // strategyCallback: PerfCollectionStrategy.PowerSupplyStrategy(),
3961
- // category: IPerfMetadataCategory.FrameSteps,
3962
- // hidden: true,
3963
- // });
3964
- perfCollector.addCollectionStrategies({
3965
- strategyCallback: PerfCollectionStrategy.PressureStrategy(),
3966
- category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */,
3967
- hidden: true,
4178
+ const numSlices = this._getNumberOfSlices();
4179
+ if (numSlices === 0) {
4180
+ return;
4181
+ }
4182
+ // First we clear the canvas so we can draw our data!
4183
+ this.clear();
4184
+ // Get global min max of time axis (across all datasets).
4185
+ this._globalTimeMinMax.min = Infinity;
4186
+ this._globalTimeMinMax.max = 0;
4187
+ // First we must get the end positions of our view port.
4188
+ const pos = this._position ?? numSlices - 1;
4189
+ let start = pos - Math.ceil(this._sizeOfWindow * ScaleFactor);
4190
+ let startOverflow = 0;
4191
+ // account for overflow from start.
4192
+ if (start < 0) {
4193
+ startOverflow = 0 - start;
4194
+ start = 0;
4195
+ }
4196
+ let end = Math.ceil(pos + this._sizeOfWindow * (1 - ScaleFactor) + startOverflow);
4197
+ // account for overflow from end.
4198
+ if (end > numSlices) {
4199
+ const endOverflow = end - numSlices;
4200
+ end = numSlices;
4201
+ start = Math.max(start - endOverflow, 0);
4202
+ }
4203
+ // update the bounds
4204
+ this._datasetBounds.start = start;
4205
+ this._datasetBounds.end = end;
4206
+ // next we must find the min and max timestamp in bounds. (Timestamps are sorted)
4207
+ this._globalTimeMinMax.min = this.datasets.data.at(this.datasets.startingIndices.at(this._datasetBounds.start));
4208
+ this._globalTimeMinMax.max = this.datasets.data.at(this.datasets.startingIndices.at(this._datasetBounds.end - 1));
4209
+ // set the buffer region maximum by rescaling the max timestamp in bounds.
4210
+ const bufferMaximum = Math.ceil((this._globalTimeMinMax.max - this._globalTimeMinMax.min) / ScaleFactor + this._globalTimeMinMax.min);
4211
+ // we then need to update the end position based on the maximum for the buffer region
4212
+ // binary search to get closest point to the buffer maximum.
4213
+ this._datasetBounds.end = this._getClosestPointToTimestamp(bufferMaximum) + 1;
4214
+ // keep track of largest timestamp value in view!
4215
+ this._globalTimeMinMax.max = Math.max(this.datasets.data.at(this.datasets.startingIndices.at(this._datasetBounds.end - 1)), this._globalTimeMinMax.max);
4216
+ const updatedScaleFactor = Scalar.Clamp((this._globalTimeMinMax.max - this._globalTimeMinMax.min) / (bufferMaximum - this._globalTimeMinMax.min), ScaleFactor, 1);
4217
+ // we will now set the global maximum to the maximum of the buffer.
4218
+ this._globalTimeMinMax.max = bufferMaximum;
4219
+ this._drawableArea.top = 0;
4220
+ this._drawableArea.left = 0;
4221
+ this._drawableArea.bottom = this._height;
4222
+ this._drawableArea.right = this._width;
4223
+ this._drawTickers(this._drawableArea, this._datasetBounds);
4224
+ this._drawTimeAxis(this._globalTimeMinMax, this._drawableArea);
4225
+ this._drawPlayheadRegion(this._drawableArea, updatedScaleFactor);
4226
+ this._drawableArea.top += DataPadding;
4227
+ this._drawableArea.bottom -= DataPadding;
4228
+ // pre-process tooltip info so we can use it in determining opacity of lines.
4229
+ this._preprocessTooltip(this._hoverPosition, this._drawableArea);
4230
+ const { left, right, bottom, top } = this._drawableArea;
4231
+ // process, and then draw our points
4232
+ for (let idOffset = 0; idOffset < this.datasets.ids.length; idOffset++) {
4233
+ const id = this.datasets.ids[idOffset];
4234
+ let valueMinMax;
4235
+ let prevPoint = this._prevPointById.get(id);
4236
+ let prevValue = this._prevValueById.get(id);
4237
+ let ticker = false;
4238
+ for (let i = 0; i < this._numberOfTickers; i++) {
4239
+ if (this._tickerItems[i].id === id) {
4240
+ ticker = true;
4241
+ }
4242
+ }
4243
+ if (!ticker) {
4244
+ continue;
4245
+ }
4246
+ ctx.beginPath();
4247
+ ctx.strokeStyle = this.metadata.get(id)?.color ?? DefaultColor;
4248
+ // if we are focused on a line and not in live mode handle the opacities appropriately.
4249
+ if (this._preprocessedTooltipInfo.focusedId === id) {
4250
+ ctx.globalAlpha = DefaultAlpha;
4251
+ }
4252
+ else if (this._preprocessedTooltipInfo.focusedId !== "") {
4253
+ ctx.globalAlpha = BackgroundLineAlpha;
4254
+ }
4255
+ const values = new Array(this._datasetBounds.end - this._datasetBounds.start);
4256
+ for (let pointIndex = this._datasetBounds.start; pointIndex < this._datasetBounds.end; pointIndex++) {
4257
+ const numPoints = this.datasets.data.at(this.datasets.startingIndices.at(pointIndex) + PerformanceViewerCollector.NumberOfPointsOffset);
4258
+ if (idOffset >= numPoints) {
4259
+ continue;
4260
+ }
4261
+ const valueIndex = this.datasets.startingIndices.at(pointIndex) + PerformanceViewerCollector.SliceDataOffset + idOffset;
4262
+ const value = this.datasets.data.at(valueIndex);
4263
+ if (prevValue === undefined) {
4264
+ prevValue = value;
4265
+ this._prevValueById.set(id, prevValue);
4266
+ }
4267
+ // perform smoothing
4268
+ const smoothedValue = SmoothingFactor * value + (1 - SmoothingFactor) * prevValue;
4269
+ values[pointIndex - this._datasetBounds.start] = smoothedValue;
4270
+ if (!valueMinMax) {
4271
+ valueMinMax = {
4272
+ min: smoothedValue,
4273
+ max: smoothedValue,
4274
+ };
4275
+ }
4276
+ this._prevValueById.set(id, smoothedValue);
4277
+ valueMinMax.min = Math.min(valueMinMax.min, smoothedValue);
4278
+ valueMinMax.max = Math.max(valueMinMax.max, smoothedValue);
4279
+ }
4280
+ const delta = valueMinMax.max - valueMinMax.min;
4281
+ valueMinMax.min -= RangeMargin * delta;
4282
+ valueMinMax.max += RangeMargin * delta;
4283
+ for (let pointIndex = this._datasetBounds.start; pointIndex < this._datasetBounds.end; pointIndex++) {
4284
+ const timestamp = this.datasets.data.at(this.datasets.startingIndices.at(pointIndex));
4285
+ const smoothedValue = values[pointIndex - this._datasetBounds.start];
4286
+ const drawableTime = this._getPixelForNumber(timestamp, this._globalTimeMinMax, left, right - left, false);
4287
+ const drawableValue = this._getPixelForNumber(smoothedValue, valueMinMax, top, bottom - top, true);
4288
+ if (prevPoint === undefined) {
4289
+ prevPoint = [drawableTime, drawableValue];
4290
+ this._prevPointById.set(id, prevPoint);
4291
+ }
4292
+ const xDifference = drawableTime - prevPoint[0];
4293
+ const skipLine = xDifference > MaxXDistancePercBetweenLinePoints * (right - left);
4294
+ if (skipLine) {
4295
+ ctx.fillStyle = NoDataRectangleColor;
4296
+ ctx.fillRect(prevPoint[0], top, xDifference, bottom - top);
4297
+ }
4298
+ else {
4299
+ if (prevPoint[0] < drawableTime) {
4300
+ ctx.moveTo(prevPoint[0], prevPoint[1]);
4301
+ ctx.lineTo(drawableTime, drawableValue);
4302
+ }
4303
+ }
4304
+ prevPoint[0] = drawableTime;
4305
+ prevPoint[1] = drawableValue;
4306
+ }
4307
+ ctx.stroke();
4308
+ }
4309
+ ctx.globalAlpha = DefaultAlpha;
4310
+ // then draw the tooltip.
4311
+ this._drawTooltip(this._hoverPosition, this._drawableArea);
4312
+ }
4313
+ _drawTickers(drawableArea, bounds) {
4314
+ const { _ctx: ctx } = this;
4315
+ if (!ctx) {
4316
+ return;
4317
+ }
4318
+ // create the ticker objects for each of the non hidden items.
4319
+ let longestText = "";
4320
+ this._numberOfTickers = 0;
4321
+ const valueMap = new Map();
4322
+ for (let idOffset = 0; idOffset < this.datasets.ids.length; idOffset++) {
4323
+ const id = this.datasets.ids[idOffset];
4324
+ if (this.metadata.get(id)?.hidden) {
4325
+ continue;
4326
+ }
4327
+ const valueMinMax = this._getMinMax(bounds, idOffset);
4328
+ const latestValue = this.datasets.data.at(this.datasets.startingIndices.at(bounds.end - 1) + PerformanceViewerCollector.SliceDataOffset + idOffset);
4329
+ const text = `${id}: ${latestValue.toFixed(2)} (max: ${valueMinMax.max.toFixed(2)}, min: ${valueMinMax.min.toFixed(2)})`;
4330
+ valueMap.set(id, {
4331
+ min: valueMinMax.min,
4332
+ max: valueMinMax.max,
4333
+ current: latestValue,
3968
4334
  });
4335
+ if (text.length > longestText.length) {
4336
+ longestText = text;
4337
+ }
4338
+ this._tickerItems[this._numberOfTickers].id = id;
4339
+ this._tickerItems[this._numberOfTickers].max = valueMinMax.max;
4340
+ this._tickerItems[this._numberOfTickers].min = valueMinMax.min;
4341
+ this._tickerItems[this._numberOfTickers].text = text;
4342
+ this._numberOfTickers++;
4343
+ }
4344
+ this._onVisibleRangeChangedObservable?.notifyObservers({ valueMap });
4345
+ ctx.save();
4346
+ ctx.font = GraphAddonFont;
4347
+ ctx.textBaseline = "middle";
4348
+ ctx.textAlign = "left";
4349
+ let width;
4350
+ // if the lengths are the same the estimate should be good enough given the padding.
4351
+ if (this._tickerTextCache.text.length === longestText.length) {
4352
+ width = this._tickerTextCache.width;
4353
+ }
4354
+ else {
4355
+ width = ctx.measureText(longestText).width + 2 * TickerHorizontalPadding;
4356
+ this._tickerTextCache.text = longestText;
4357
+ this._tickerTextCache.width = width;
4358
+ }
4359
+ ctx.restore();
4360
+ }
4361
+ /**
4362
+ * Returns the index of the closest time for the datasets.
4363
+ * Uses a modified binary search to get value.
4364
+ *
4365
+ * @param targetTime the time we want to get close to.
4366
+ * @returns index of the item with the closest time to the targetTime
4367
+ */
4368
+ _getClosestPointToTimestamp(targetTime) {
4369
+ let low = 0;
4370
+ let high = this._getNumberOfSlices() - 1;
4371
+ let closestIndex = 0;
4372
+ while (low <= high) {
4373
+ const middle = Math.trunc((low + high) / 2);
4374
+ const middleTimestamp = this.datasets.data.at(this.datasets.startingIndices.at(middle));
4375
+ if (Math.abs(middleTimestamp - targetTime) < Math.abs(this.datasets.data.at(this.datasets.startingIndices.at(closestIndex)) - targetTime)) {
4376
+ closestIndex = middle;
4377
+ }
4378
+ if (middleTimestamp < targetTime) {
4379
+ low = middle + 1;
4380
+ }
4381
+ else if (middleTimestamp > targetTime) {
4382
+ high = middle - 1;
4383
+ }
4384
+ else {
4385
+ break;
4386
+ }
4387
+ }
4388
+ return closestIndex;
4389
+ }
4390
+ /**
4391
+ * This is a convenience method to get the number of collected slices.
4392
+ * @returns the total number of collected slices.
4393
+ */
4394
+ _getNumberOfSlices() {
4395
+ return this.datasets.startingIndices.itemLength;
4396
+ }
4397
+ /**
4398
+ * Draws the time axis, adjusts the drawable area for the graph.
4399
+ *
4400
+ * @param timeMinMax the minimum and maximum for the time axis.
4401
+ * @param drawableArea the current allocated drawable area.
4402
+ */
4403
+ _drawTimeAxis(timeMinMax, drawableArea) {
4404
+ const { _ctx: ctx } = this;
4405
+ if (!ctx) {
4406
+ return;
4407
+ }
4408
+ const spaceAvailable = drawableArea.right - drawableArea.left;
4409
+ this._generateTicks(timeMinMax, spaceAvailable);
4410
+ // remove the height of the axis from the available drawable area.
4411
+ drawableArea.bottom -= this._axisHeight;
4412
+ // draw axis box.
4413
+ ctx.save();
4414
+ ctx.fillStyle = AxisColor;
4415
+ ctx.fillRect(drawableArea.left, drawableArea.bottom, spaceAvailable, this._axisHeight);
4416
+ // draw time axis line
4417
+ ctx.beginPath();
4418
+ ctx.strokeStyle = DefaultColor;
4419
+ ctx.moveTo(drawableArea.left, drawableArea.bottom);
4420
+ ctx.lineTo(drawableArea.right, drawableArea.bottom);
4421
+ // draw ticks and text.
4422
+ ctx.fillStyle = DefaultColor;
4423
+ ctx.textAlign = "center";
4424
+ ctx.textBaseline = "middle";
4425
+ const timestampUnit = this._getTimestampUnit(this._ticks[this._ticks.length - 1]);
4426
+ for (const tick of this._ticks) {
4427
+ let position = this._getPixelForNumber(tick, timeMinMax, drawableArea.left, spaceAvailable, false);
4428
+ if (position > spaceAvailable) {
4429
+ position = spaceAvailable;
4430
+ }
4431
+ ctx.moveTo(position, drawableArea.bottom);
4432
+ ctx.lineTo(position, drawableArea.bottom + 10);
4433
+ ctx.fillText(this._parseTimestamp(tick, timestampUnit), position, drawableArea.bottom + 20);
4434
+ }
4435
+ ctx.stroke();
4436
+ ctx.restore();
4437
+ }
4438
+ /**
4439
+ * Given a timestamp (should be the maximum timestamp in view), this function returns the maximum unit the timestamp contains.
4440
+ * This information can be used for formatting purposes.
4441
+ * @param timestamp the maximum timestamp to find the maximum timestamp unit for.
4442
+ * @returns The maximum unit the timestamp has.
4443
+ */
4444
+ _getTimestampUnit(timestamp) {
4445
+ if (timestamp / MsInHour > 1) {
4446
+ return TimestampUnit.Hours;
4447
+ }
4448
+ else if (timestamp / MsInMinute > 1) {
4449
+ return TimestampUnit.Minutes;
4450
+ }
4451
+ else if (timestamp / MsInSecond > 1) {
4452
+ return TimestampUnit.Seconds;
4453
+ }
4454
+ else {
4455
+ return TimestampUnit.Milliseconds;
4456
+ }
4457
+ }
4458
+ /**
4459
+ * Given a timestamp and the interval unit, this function will parse the timestamp to the appropriate format.
4460
+ * @param timestamp The timestamp to parse
4461
+ * @param intervalUnit The maximum unit of the maximum timestamp in an interval.
4462
+ * @returns a string representing the parsed timestamp.
4463
+ */
4464
+ _parseTimestamp(timestamp, intervalUnit) {
4465
+ let parsedTimestamp = "";
4466
+ if (intervalUnit >= TimestampUnit.Hours) {
4467
+ const numHours = Math.floor(timestamp / MsInHour);
4468
+ timestamp -= numHours * MsInHour;
4469
+ parsedTimestamp += `${numHours.toString().padStart(intervalUnit > TimestampUnit.Hours ? 2 : 1, "0")}:`;
4470
+ }
4471
+ if (intervalUnit >= TimestampUnit.Minutes) {
4472
+ const numMinutes = Math.floor(timestamp / MsInMinute);
4473
+ timestamp -= numMinutes * MsInMinute;
4474
+ parsedTimestamp += `${numMinutes.toString().padStart(intervalUnit > TimestampUnit.Minutes ? 2 : 1, "0")}:`;
4475
+ }
4476
+ const numSeconds = Math.floor(timestamp / MsInSecond);
4477
+ timestamp -= numSeconds * MsInSecond;
4478
+ parsedTimestamp += numSeconds.toString().padStart(intervalUnit > TimestampUnit.Seconds ? 2 : 1, "0");
4479
+ if (timestamp > 0) {
4480
+ if (parsedTimestamp.length > 0) {
4481
+ parsedTimestamp += ".";
4482
+ }
4483
+ parsedTimestamp += Math.round(timestamp).toString().padStart(3, "0");
4484
+ }
4485
+ return parsedTimestamp;
4486
+ }
4487
+ /**
4488
+ * Generates a list of ticks given the min and max of the axis, and the space available in the axis.
4489
+ *
4490
+ * @param minMax the minimum and maximum values of the axis
4491
+ * @param spaceAvailable the total amount of space we have allocated to our axis
4492
+ */
4493
+ _generateTicks(minMax, spaceAvailable) {
4494
+ const { min, max } = minMax;
4495
+ const minTickSpacing = 40;
4496
+ this._ticks.length = 0;
4497
+ const maxTickCount = Math.ceil(spaceAvailable / minTickSpacing);
4498
+ const range = this._niceNumber(max - min, false);
4499
+ const spacing = this._niceNumber(range / (maxTickCount - 1), true);
4500
+ const niceMin = Math.floor(min / spacing) * spacing;
4501
+ const niceMax = Math.floor(max / spacing) * spacing;
4502
+ for (let i = niceMin; i <= niceMax + 0.5 * spacing; i += spacing) {
4503
+ this._ticks.push(i);
4504
+ }
4505
+ }
4506
+ /**
4507
+ * Nice number algorithm based on psueudo code defined in "Graphics Gems" by Andrew S. Glassner.
4508
+ * This will find a "nice" number approximately equal to num.
4509
+ *
4510
+ * @param num The number we want to get close to.
4511
+ * @param shouldRound if true we will round the number, otherwise we will get the ceiling.
4512
+ * @returns a "nice" number approximately equal to num.
4513
+ */
4514
+ _niceNumber(num, shouldRound) {
4515
+ const exp = Math.floor(Math.log10(num));
4516
+ const fraction = num / Math.pow(10, exp);
4517
+ let niceFraction;
4518
+ if (shouldRound) {
4519
+ if (fraction < 1.5) {
4520
+ niceFraction = 1;
4521
+ }
4522
+ else if (fraction < 3) {
4523
+ niceFraction = 2;
4524
+ }
4525
+ else if (fraction < 7) {
4526
+ niceFraction = 5;
4527
+ }
4528
+ else {
4529
+ niceFraction = 10;
4530
+ }
4531
+ }
4532
+ else {
4533
+ if (fraction <= 1) {
4534
+ niceFraction = 1;
4535
+ }
4536
+ else if (fraction <= 2) {
4537
+ niceFraction = 2;
4538
+ }
4539
+ else if (fraction <= 5) {
4540
+ niceFraction = 5;
4541
+ }
4542
+ else {
4543
+ niceFraction = 10;
4544
+ }
4545
+ }
4546
+ return niceFraction * Math.pow(10, exp);
4547
+ }
4548
+ /**
4549
+ * Gets the min and max as a single object from an array of numbers.
4550
+ * @param bounds
4551
+ * @param offset
4552
+ * @returns the min and max of the array.
4553
+ */
4554
+ _getMinMax(bounds, offset) {
4555
+ let min = Infinity, max = 0;
4556
+ for (let i = bounds.start; i < bounds.end; i++) {
4557
+ const numPoints = this.datasets.data.at(this.datasets.startingIndices.at(i) + PerformanceViewerCollector.NumberOfPointsOffset);
4558
+ if (offset >= numPoints) {
4559
+ continue;
4560
+ }
4561
+ const itemIndex = this.datasets.startingIndices.at(i) + PerformanceViewerCollector.SliceDataOffset + offset;
4562
+ const item = this.datasets.data.at(itemIndex);
4563
+ if (item < min) {
4564
+ min = item;
4565
+ }
4566
+ if (item > max) {
4567
+ max = item;
4568
+ }
3969
4569
  }
3970
- };
3971
- useEffect(() => {
3972
- const perfCollector = scene.getPerfCollector();
3973
- addStrategies(perfCollector);
3974
- setPerformanceCollector(perfCollector);
3975
- }, []);
3976
- return (jsxs(Fragment, { children: [!isOpen && jsx(ButtonLine, { label: "Open Realtime Perf Viewer", onClick: onPerformanceButtonClick }), !isOpen && jsx(FileUploadLine, { label: "Load Perf Viewer using CSV", accept: ".csv", onClick: onLoadClick }), jsx(ButtonLine, { label: "Export Perf to CSV", onClick: onExportClick }), !isOpen && jsx(ButtonLine, { label: performanceCollector?.isStarted ? "Stop Recording" : "Begin Recording", onClick: onToggleRecording })] }));
3977
- };
3978
-
3979
- /**
3980
- * Wraps text in a property line
3981
- * @param props - PropertyLineProps and TextProps
3982
- * @returns property-line wrapped text
3983
- */
3984
- const TextPropertyLine = (props) => {
3985
- TextPropertyLine.displayName = "TextPropertyLine";
3986
- const { value, title } = props;
3987
- return (jsx(PropertyLine, { ...props, children: jsx(Body1, { title: title, children: value ?? "" }) }));
3988
- };
3989
-
3990
- const useStyles$H = makeStyles({
3991
- pinnedStatsPane: {
3992
- flex: "0 1 auto",
3993
- paddingBottom: tokens.spacingHorizontalM,
3994
- },
3995
- });
3996
- const StatsPane = (props) => {
3997
- const classes = useStyles$H();
3998
- const scene = props.context;
3999
- const engine = scene.getEngine();
4000
- const fps = useObservableState(() => Math.round(engine.getFps()), engine.onBeginFrameObservable);
4001
- return (jsxs(Fragment, { children: [jsxs(SidePaneContainer, { className: classes.pinnedStatsPane, children: [jsx(TextPropertyLine, { label: "Version", description: "The Babylon.js engine version.", value: AbstractEngine.Version }, "EngineVersion"), jsx(StringifiedPropertyLine, { label: "FPS:", description: "The current framerate", value: fps }, "FPS")] }), jsx(ExtensibleAccordion, { ...props })] }));
4002
- };
4003
-
4004
- /**
4005
- * Displays an icon indicating enabled (green check) or disabled (red cross) state
4006
- * @param props - The properties for the PropertyLine, including the boolean value to display.
4007
- * @returns A PropertyLine component with a PresenceBadge indicating the boolean state.
4008
- */
4009
- const BooleanBadgePropertyLine = (props) => {
4010
- BooleanBadgePropertyLine.displayName = "BooleanBadgePropertyLine";
4011
- return (jsx(PropertyLine, { ...props, children: jsx(PresenceBadge, { status: props.value ? "available" : "do-not-disturb", outOfOffice: true }) }));
4012
- };
4013
-
4014
- const SystemStats = ({ context: scene }) => {
4015
- const engine = scene.getEngine();
4016
- const caps = engine.getCaps();
4017
- const resolution = useObservableState(() => `${engine.getRenderWidth()} x ${engine.getRenderHeight()}`, engine.onResizeObservable);
4018
- const hardwareScalingLevel = useObservableState(() => engine.getHardwareScalingLevel(), engine.onResizeObservable);
4019
- return (jsxs(Fragment, { children: [jsx(TextPropertyLine, { label: "Resolution", value: resolution }, "Resolution"), jsx(StringifiedPropertyLine, { label: "Hardware Scaling Level", value: hardwareScalingLevel }, "HardwareScalingLevel"), jsx(TextPropertyLine, { label: "Engine", value: engine.description }, "Engine"), jsx(BooleanBadgePropertyLine, { label: "StdDerivatives", value: caps.standardDerivatives }, "StdDerivatives"), jsx(BooleanBadgePropertyLine, { label: "Compressed Textures", value: caps.s3tc !== undefined }, "CompressedTextures"), jsx(BooleanBadgePropertyLine, { label: "Hardware Instances", value: caps.instancedArrays }, "HardwareInstances"), jsx(BooleanBadgePropertyLine, { label: "Texture Float", value: caps.textureFloat }, "TextureFloat"), jsx(BooleanBadgePropertyLine, { label: "Texture Half Float", value: caps.textureHalfFloat }, "TextureHalfFloat"), jsx(BooleanBadgePropertyLine, { label: "Render to Texture Float", value: caps.textureFloatRender }, "RenderToTextureFloat"), jsx(BooleanBadgePropertyLine, { label: "Render to Texture Half Float", value: caps.textureHalfFloatRender }, "RenderToTextureHalfFloat"), jsx(BooleanBadgePropertyLine, { label: "32bits Indices", value: caps.uintIndices }, "32bitsIndices"), jsx(BooleanBadgePropertyLine, { label: "Fragment Depth", value: caps.fragmentDepthSupported }, "FragmentDepth"), jsx(BooleanBadgePropertyLine, { label: "High Precision Shaders", value: caps.highPrecisionShaderSupported }, "HighPrecisionShaders"), jsx(BooleanBadgePropertyLine, { label: "Draw Buffers", value: caps.drawBuffersExtension }, "DrawBuffers"), jsx(BooleanBadgePropertyLine, { label: "Vertex Array Object", value: caps.vertexArrayObject }, "VertexArrayObject"), jsx(BooleanBadgePropertyLine, { label: "Timer Query", value: caps.timerQuery !== undefined }, "TimerQuery"), jsx(BooleanBadgePropertyLine, { label: "Stencil", value: engine.isStencilEnable }, "Stencil"), jsx(BooleanBadgePropertyLine, { label: "Parallel Shader Compilation", value: caps.parallelShaderCompile != null }, "ParallelShaderCompilation"), jsx(StringifiedPropertyLine, { label: "Max Textures Units", value: caps.maxTexturesImageUnits }, "MaxTexturesUnits"), jsx(StringifiedPropertyLine, { label: "Max Textures Size", value: caps.maxTextureSize }, "MaxTexturesSize"), jsx(StringifiedPropertyLine, { label: "Max Anisotropy", value: caps.maxAnisotropy }, "MaxAnisotropy"), jsx(TextPropertyLine, { label: "Driver", value: engine.extractDriverInfo() }, "Driver")] }));
4020
- };
4021
-
4022
- const StatsServiceIdentity = Symbol("StatsService");
4023
- /**
4024
- * Provides a scene stats pane.
4025
- */
4026
- const StatsServiceDefinition = {
4027
- friendlyName: "Stats",
4028
- produces: [StatsServiceIdentity],
4029
- consumes: [ShellServiceIdentity, SceneContextIdentity],
4030
- factory: (shellService, sceneContext) => {
4031
- const sectionsCollection = new ObservableCollection();
4032
- const sectionContentCollection = new ObservableCollection();
4033
- const registration = shellService.addSidePane({
4034
- key: "Statistics",
4035
- title: "Statistics",
4036
- icon: DataBarHorizontalRegular,
4037
- horizontalLocation: "right",
4038
- verticalLocation: "top",
4039
- order: 300,
4040
- suppressTeachingMoment: true,
4041
- content: () => {
4042
- const sections = useOrderedObservableCollection(sectionsCollection);
4043
- const sectionContent = useObservableCollection(sectionContentCollection);
4044
- const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
4045
- return jsx(Fragment, { children: scene && jsx(StatsPane, { sections: sections, sectionContent: sectionContent, context: scene }) });
4046
- },
4047
- });
4048
- // Default/built-in sections.
4049
- sectionsCollection.add({
4050
- identity: "Performance",
4051
- order: 0,
4052
- });
4053
- sectionsCollection.add({
4054
- identity: "Count",
4055
- order: 1,
4056
- });
4057
- sectionsCollection.add({
4058
- identity: "Frame Steps Duration",
4059
- order: 2,
4060
- });
4061
- sectionsCollection.add({
4062
- identity: "System Info",
4063
- order: 3,
4064
- });
4065
- // Default/built-in content.
4066
- sectionContentCollection.add({
4067
- key: "DefaultPerfStats",
4068
- section: "Performance",
4069
- order: 0,
4070
- component: PerformanceStats,
4071
- });
4072
- sectionContentCollection.add({
4073
- key: "DefaultCountStats",
4074
- section: "Count",
4075
- order: 1,
4076
- component: CountStats,
4077
- });
4078
- sectionContentCollection.add({
4079
- key: "DefaultFrameStats",
4080
- section: "Frame Steps Duration",
4081
- order: 2,
4082
- component: FrameStepsStats,
4083
- });
4084
- sectionContentCollection.add({
4085
- key: "DefaultSystemStats",
4086
- section: "System Info",
4087
- order: 3,
4088
- component: SystemStats,
4089
- });
4090
4570
  return {
4091
- addSection: (section) => sectionsCollection.add(section),
4092
- addSectionContent: (content) => sectionContentCollection.add(content),
4093
- dispose: () => registration.dispose(),
4571
+ min,
4572
+ max,
4094
4573
  };
4574
+ }
4575
+ /**
4576
+ * Converts a single number to a pixel coordinate in a single axis by normalizing the data to a [0, 1] scale using the minimum and maximum values.
4577
+ *
4578
+ * @param num the number we want to get the pixel coordinate for
4579
+ * @param minMax the min and max of the dataset in the axis we want the pixel coordinate for.
4580
+ * @param startingPixel the starting pixel coordinate (this means it takes account for any offset).
4581
+ * @param spaceAvailable the total space available in this axis.
4582
+ * @param shouldFlipValue if we should use a [1, 0] scale instead of a [0, 1] scale.
4583
+ * @returns the pixel coordinate of the value in a single axis.
4584
+ */
4585
+ _getPixelForNumber(num, minMax, startingPixel, spaceAvailable, shouldFlipValue) {
4586
+ const { min, max } = minMax;
4587
+ // Perform a min-max normalization to rescale the value onto a [0, 1] scale given the min and max of the dataset.
4588
+ let normalizedValue = Math.abs(max - min) > 0.001 ? (num - min) / (max - min) : 0.5;
4589
+ // if we should make this a [1, 0] range instead (higher numbers = smaller pixel value)
4590
+ if (shouldFlipValue) {
4591
+ normalizedValue = 1 - normalizedValue;
4592
+ }
4593
+ return startingPixel + normalizedValue * spaceAvailable;
4594
+ }
4595
+ /**
4596
+ * Add in any necessary event listeners.
4597
+ *
4598
+ * @param canvas The canvas we want to attach listeners to.
4599
+ */
4600
+ _attachEventListeners(canvas) {
4601
+ canvas.addEventListener("wheel", this._handleZoom);
4602
+ canvas.addEventListener("mousemove", this._handleDataHover);
4603
+ canvas.addEventListener("mousedown", this._handlePanStart);
4604
+ canvas.addEventListener("mouseleave", this._handleStopHover);
4605
+ // The user may stop panning outside of the canvas size so we should add the event listener to the document.
4606
+ canvas.ownerDocument.addEventListener("mouseup", this._handlePanStop);
4607
+ }
4608
+ /**
4609
+ * We remove all event listeners we added.
4610
+ *
4611
+ * @param canvas The canvas we want to remove listeners from.
4612
+ */
4613
+ _removeEventListeners(canvas) {
4614
+ canvas.removeEventListener("wheel", this._handleZoom);
4615
+ canvas.removeEventListener("mousemove", this._handleDataHover);
4616
+ canvas.removeEventListener("mousedown", this._handlePanStart);
4617
+ canvas.removeEventListener("mouseleave", this._handleStopHover);
4618
+ canvas.ownerDocument.removeEventListener("mouseup", this._handlePanStop);
4619
+ }
4620
+ /**
4621
+ * Given a line defined by P1: (x1, y1) and P2: (x2, y2) get the distance of P0 (x0, y0) from the line.
4622
+ * https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points
4623
+ * @param x1 x position of point P1
4624
+ * @param y1 y position of point P1
4625
+ * @param x2 x position of point P2
4626
+ * @param y2 y position of point P2
4627
+ * @param x0 x position of point P0
4628
+ * @param y0 y position of point P0
4629
+ * @returns distance of P0 from the line defined by P1 and P2
4630
+ */
4631
+ _getDistanceFromLine(x1, y1, x2, y2, x0, y0) {
4632
+ // if P1 and P2 are the same we just get the distance between P1 and P0
4633
+ if (x1 === x2 && y1 === y2) {
4634
+ return Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2));
4635
+ }
4636
+ // next we want to handle the case where our point is beyond the y position of our line
4637
+ let topX = 0;
4638
+ let topY = 0;
4639
+ let bottomX = 0;
4640
+ let bottomY = 0;
4641
+ if (y1 >= y2) {
4642
+ topX = x1;
4643
+ topY = y1;
4644
+ bottomX = x2;
4645
+ bottomY = y2;
4646
+ }
4647
+ else {
4648
+ topX = x2;
4649
+ topY = y2;
4650
+ bottomX = x1;
4651
+ bottomY = y1;
4652
+ }
4653
+ if (y0 < bottomY) {
4654
+ return Math.sqrt(Math.pow(bottomX - x0, 2) + Math.pow(bottomY - y0, 2));
4655
+ }
4656
+ if (y0 > topY) {
4657
+ return Math.sqrt(Math.pow(topX - x0, 2) + Math.pow(topY - y0, 2));
4658
+ }
4659
+ // the general case!
4660
+ const numerator = Math.abs((x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1));
4661
+ const denominator = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
4662
+ return numerator / denominator;
4663
+ }
4664
+ /**
4665
+ * This method does preprocessing calculations for the tooltip.
4666
+ * @param pos the position of our mouse.
4667
+ * @param drawableArea the remaining drawable area.
4668
+ */
4669
+ _preprocessTooltip(pos, drawableArea) {
4670
+ const { _ctx: ctx } = this;
4671
+ if (pos === null || !ctx || !ctx.canvas || this._getNumberOfSlices() === 0) {
4672
+ return;
4673
+ }
4674
+ const { left, top } = ctx.canvas.getBoundingClientRect();
4675
+ const adjustedYPos = pos.yPos - top;
4676
+ let adjustedXPos = pos.xPos - left;
4677
+ if (adjustedXPos > drawableArea.right) {
4678
+ adjustedXPos = drawableArea.right;
4679
+ }
4680
+ // convert the mouse x position in pixels to a timestamp.
4681
+ const inferredTimestamp = this._getNumberFromPixel(adjustedXPos, this._globalTimeMinMax, drawableArea.left, drawableArea.right, false);
4682
+ let longestText = "";
4683
+ let numberOfTooltipItems = 0;
4684
+ // get the closest timestamps to the target timestamp, and store the appropriate meta object.
4685
+ const closestIndex = this._getClosestPointToTimestamp(inferredTimestamp);
4686
+ let actualTimestamp = 0;
4687
+ let closestLineId = "";
4688
+ let closestLineValueMinMax = { min: 0, max: 0 };
4689
+ let closestLineDistance = Number.POSITIVE_INFINITY;
4690
+ for (let idOffset = 0; idOffset < this.datasets.ids.length; idOffset++) {
4691
+ const id = this.datasets.ids[idOffset];
4692
+ if (this.metadata.get(id)?.hidden) {
4693
+ continue;
4694
+ }
4695
+ const numPoints = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex) + PerformanceViewerCollector.NumberOfPointsOffset);
4696
+ if (idOffset >= numPoints) {
4697
+ continue;
4698
+ }
4699
+ const valueAtClosestPointIndex = this.datasets.startingIndices.at(closestIndex) + PerformanceViewerCollector.SliceDataOffset + idOffset;
4700
+ const valueAtClosestPoint = this.datasets.data.at(valueAtClosestPointIndex);
4701
+ let valueMinMax;
4702
+ // we would have already calculated the min and max while getting the tickers, so use those, and get first one.
4703
+ for (let i = 0; i < this._numberOfTickers; i++) {
4704
+ if (this._tickerItems[i].id === id) {
4705
+ valueMinMax = this._tickerItems[i];
4706
+ }
4707
+ }
4708
+ if (!valueMinMax) {
4709
+ continue;
4710
+ }
4711
+ actualTimestamp = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex));
4712
+ const valueAtClosestPointYPos = this._getPixelForNumber(valueAtClosestPoint, valueMinMax, drawableArea.top, drawableArea.bottom - drawableArea.top, true);
4713
+ const xForActualTimestamp = this._getPixelForNumber(actualTimestamp, this._globalTimeMinMax, drawableArea.left, drawableArea.right - drawableArea.left, false);
4714
+ const text = `${id}: ${valueAtClosestPoint.toFixed(2)}`;
4715
+ if (text.length > longestText.length) {
4716
+ longestText = text;
4717
+ }
4718
+ this._tooltipItems[numberOfTooltipItems].text = text;
4719
+ this._tooltipItems[numberOfTooltipItems].color = this.metadata.get(id)?.color ?? DefaultColor;
4720
+ numberOfTooltipItems++;
4721
+ // don't process rest if we aren't panned.
4722
+ if (!this._position) {
4723
+ continue;
4724
+ }
4725
+ // initially distance between closest data point and mouse point.
4726
+ let distance = this._getDistanceFromLine(xForActualTimestamp, valueAtClosestPointYPos, xForActualTimestamp, valueAtClosestPointYPos, pos.xPos - left, adjustedYPos);
4727
+ // get the shortest distance between the point and the line segment infront, and line segment behind, store the shorter distance (if shorter than distance between closest data point and mouse).
4728
+ if (closestIndex + 1 < this.datasets.data.itemLength &&
4729
+ this.datasets.data.at(this.datasets.startingIndices.at(closestIndex + 1) + PerformanceViewerCollector.NumberOfPointsOffset) > idOffset) {
4730
+ const secondPointTimestamp = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex + 1));
4731
+ const secondPointX = this._getPixelForNumber(secondPointTimestamp, this._globalTimeMinMax, drawableArea.left, drawableArea.right - drawableArea.left, false);
4732
+ const secondPointValue = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex + 1) + PerformanceViewerCollector.SliceDataOffset + idOffset);
4733
+ const secondPointY = this._getPixelForNumber(secondPointValue, valueMinMax, drawableArea.top, drawableArea.bottom - drawableArea.top, true);
4734
+ distance = Math.min(this._getDistanceFromLine(xForActualTimestamp, valueAtClosestPointYPos, secondPointX, secondPointY, pos.xPos - left, adjustedYPos), distance);
4735
+ }
4736
+ if (closestIndex - 1 >= 0 && this.datasets.data.at(this.datasets.startingIndices.at(closestIndex + 1) + PerformanceViewerCollector.NumberOfPointsOffset) > idOffset) {
4737
+ const secondPointTimestamp = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex - 1));
4738
+ const secondPointX = this._getPixelForNumber(secondPointTimestamp, this._globalTimeMinMax, drawableArea.left, drawableArea.right - drawableArea.left, false);
4739
+ const secondPointValue = this.datasets.data.at(this.datasets.startingIndices.at(closestIndex - 1) + PerformanceViewerCollector.SliceDataOffset + idOffset);
4740
+ const secondPointY = this._getPixelForNumber(secondPointValue, valueMinMax, drawableArea.top, drawableArea.bottom - drawableArea.top, true);
4741
+ distance = Math.min(this._getDistanceFromLine(xForActualTimestamp, valueAtClosestPointYPos, secondPointX, secondPointY, pos.xPos - left, adjustedYPos), distance);
4742
+ }
4743
+ if (distance < closestLineDistance) {
4744
+ closestLineId = id;
4745
+ closestLineDistance = distance;
4746
+ closestLineValueMinMax = valueMinMax;
4747
+ }
4748
+ }
4749
+ const xForActualTimestamp = this._getPixelForNumber(actualTimestamp, this._globalTimeMinMax, drawableArea.left, drawableArea.right - drawableArea.left, false);
4750
+ this._preprocessedTooltipInfo.xForActualTimestamp = xForActualTimestamp;
4751
+ // check if hover is within a certain distance, if so it is our only item in our tooltip.
4752
+ if (closestLineDistance <= MaxDistanceForHover && this._position) {
4753
+ this._preprocessedTooltipInfo.focusedId = closestLineId;
4754
+ const inferredValue = this._getNumberFromPixel(adjustedYPos, closestLineValueMinMax, drawableArea.top, drawableArea.bottom, true);
4755
+ const closestLineText = `${closestLineId}: ${inferredValue.toFixed(2)}`;
4756
+ this._preprocessedTooltipInfo.longestText = closestLineText;
4757
+ this._preprocessedTooltipInfo.numberOfTooltipItems = 1;
4758
+ this._tooltipItems[0].text = closestLineText;
4759
+ this._tooltipItems[0].color = this.metadata.get(closestLineId)?.color ?? DefaultColor;
4760
+ }
4761
+ else {
4762
+ this._preprocessedTooltipInfo.focusedId = "";
4763
+ this._preprocessedTooltipInfo.longestText = longestText;
4764
+ this._preprocessedTooltipInfo.numberOfTooltipItems = numberOfTooltipItems;
4765
+ }
4766
+ }
4767
+ /**
4768
+ * Draws the tooltip given the area it is allowed to draw in and the current pixel position.
4769
+ *
4770
+ * @param pos the position of the mouse cursor in pixels (x, y).
4771
+ * @param drawableArea the available area we can draw in.
4772
+ */
4773
+ _drawTooltip(pos, drawableArea) {
4774
+ const { _ctx: ctx } = this;
4775
+ if (pos === null || !ctx || !ctx.canvas || this._getNumberOfSlices() === 0) {
4776
+ return;
4777
+ }
4778
+ const { left, top } = ctx.canvas.getBoundingClientRect();
4779
+ const { numberOfTooltipItems, xForActualTimestamp, longestText } = this._preprocessedTooltipInfo;
4780
+ ctx.save();
4781
+ // draw pointer triangle
4782
+ ctx.fillStyle = PositionIndicatorColor;
4783
+ const yTriangle = drawableArea.bottom + TrianglePaddingFromAxisLine;
4784
+ ctx.beginPath();
4785
+ ctx.moveTo(xForActualTimestamp, yTriangle);
4786
+ ctx.lineTo(xForActualTimestamp + TriangleWidth / 2, yTriangle + TriangleHeight);
4787
+ ctx.lineTo(xForActualTimestamp - TriangleWidth / 2, yTriangle + TriangleHeight);
4788
+ ctx.closePath();
4789
+ ctx.fill();
4790
+ ctx.strokeStyle = PositionIndicatorColor;
4791
+ ctx.beginPath();
4792
+ // draw vertical or horizontal line depending on if focused on a point on the line.
4793
+ if (this._preprocessedTooltipInfo.focusedId === "") {
4794
+ ctx.moveTo(xForActualTimestamp, drawableArea.bottom);
4795
+ ctx.lineTo(xForActualTimestamp, TopOfGraphY);
4796
+ }
4797
+ else {
4798
+ const lineY = pos.yPos - top;
4799
+ ctx.moveTo(drawableArea.left, lineY);
4800
+ ctx.lineTo(drawableArea.right, lineY);
4801
+ }
4802
+ ctx.stroke();
4803
+ // draw the actual tooltip
4804
+ ctx.font = GraphAddonFont;
4805
+ ctx.textBaseline = "middle";
4806
+ ctx.textAlign = "left";
4807
+ const boxLength = this._addonFontLineHeight;
4808
+ const textHeight = this._addonFontLineHeight + Math.floor(TooltipHorizontalPadding / 2);
4809
+ // initialize width with cached value or measure width of longest text and update cache.
4810
+ let width;
4811
+ if (longestText === this._tooltipTextCache.text) {
4812
+ width = this._tooltipTextCache.width;
4813
+ }
4814
+ else {
4815
+ width = ctx.measureText(longestText).width + boxLength + 2 * TooltipHorizontalPadding + SpaceBetweenTextAndBox;
4816
+ this._tooltipTextCache.text = longestText;
4817
+ this._tooltipTextCache.width = width;
4818
+ }
4819
+ const tooltipHeight = textHeight * (numberOfTooltipItems + 1);
4820
+ let x = pos.xPos - left;
4821
+ let y = drawableArea.bottom - TooltipPaddingFromBottom - tooltipHeight;
4822
+ // We want the tool tip to always be inside the canvas so we adjust which way it is drawn.
4823
+ if (x + width > this._width) {
4824
+ x -= width;
4825
+ }
4826
+ ctx.globalAlpha = TooltipBackgroundAlpha;
4827
+ ctx.fillStyle = TooltipBackgroundColor;
4828
+ ctx.fillRect(x, y, width, tooltipHeight);
4829
+ ctx.globalAlpha = DefaultAlpha;
4830
+ x += TooltipHorizontalPadding;
4831
+ y += textHeight;
4832
+ for (let i = 0; i < numberOfTooltipItems; i++) {
4833
+ const tooltipItem = this._tooltipItems[i];
4834
+ ctx.fillStyle = tooltipItem.color;
4835
+ ctx.fillRect(x, y - Math.floor(boxLength / 2), boxLength, boxLength);
4836
+ ctx.fillStyle = TooltipForegroundColor;
4837
+ ctx.fillText(tooltipItem.text, x + boxLength + SpaceBetweenTextAndBox, y);
4838
+ y += textHeight;
4839
+ }
4840
+ ctx.restore();
4841
+ }
4842
+ /**
4843
+ * Gets the number from a pixel position given the minimum and maximum value in range, and the starting pixel and the ending pixel.
4844
+ *
4845
+ * @param pixel current pixel position we want to get the number for.
4846
+ * @param minMax the minimum and maximum number in the range.
4847
+ * @param startingPixel position of the starting pixel in range.
4848
+ * @param endingPixel position of ending pixel in range.
4849
+ * @param shouldFlip if we should use a [1, 0] scale instead of a [0, 1] scale.
4850
+ * @returns number corresponding to pixel position
4851
+ */
4852
+ _getNumberFromPixel(pixel, minMax, startingPixel, endingPixel, shouldFlip) {
4853
+ // normalize pixel to range [0, 1].
4854
+ let normalizedPixelPosition = (pixel - startingPixel) / (endingPixel - startingPixel);
4855
+ // we should use a [1, 0] scale instead.
4856
+ if (shouldFlip) {
4857
+ normalizedPixelPosition = 1 - normalizedPixelPosition;
4858
+ }
4859
+ return minMax.min + normalizedPixelPosition * (minMax.max - minMax.min);
4860
+ }
4861
+ /**
4862
+ * Method which returns true if the data should become realtime, false otherwise.
4863
+ *
4864
+ * @returns if the data should become realtime or not.
4865
+ */
4866
+ _shouldBecomeRealtime() {
4867
+ if (this._getNumberOfSlices() === 0) {
4868
+ return false;
4869
+ }
4870
+ // we need to compare our current slice to the latest slice to see if we should return to realtime mode.
4871
+ const pos = this._position;
4872
+ const latestSlicePos = this._getNumberOfSlices() - 1;
4873
+ if (pos === null) {
4874
+ return false;
4875
+ }
4876
+ // account for overflow on the left side only as it will be the one determining if we have sufficiently caught up to the realtime data.
4877
+ const overflow = Math.max(0 - (pos - Math.ceil(this._sizeOfWindow * ScaleFactor)), 0);
4878
+ const rightmostPos = Math.min(overflow + pos + Math.ceil(this._sizeOfWindow * (1 - ScaleFactor)), latestSlicePos);
4879
+ return (this.datasets.data.at(this.datasets.startingIndices.at(rightmostPos)) / this.datasets.data.at(this.datasets.startingIndices.at(latestSlicePos)) > ReturnToLiveThreshold);
4880
+ }
4881
+ /**
4882
+ * Will generate a playhead with a futurebox that takes up (1-scalefactor)*100% of the canvas.
4883
+ *
4884
+ * @param drawableArea The remaining drawable area.
4885
+ * @param scaleFactor The Percentage between 0.0 and 1.0 of the canvas the data gets drawn on.
4886
+ */
4887
+ _drawPlayheadRegion(drawableArea, scaleFactor) {
4888
+ const { _ctx: ctx } = this;
4889
+ if (!ctx || scaleFactor >= StopDrawingPlayheadThreshold) {
4890
+ return;
4891
+ }
4892
+ const dividerXPos = Math.ceil(drawableArea.right * scaleFactor);
4893
+ const playheadPos = dividerXPos - PlayheadSize;
4894
+ const futureBoxPos = dividerXPos + DividerSize;
4895
+ const rectangleHeight = drawableArea.bottom - drawableArea.top - 1;
4896
+ ctx.save();
4897
+ ctx.fillStyle = FutureBoxColor;
4898
+ ctx.fillRect(futureBoxPos, drawableArea.top, drawableArea.right - futureBoxPos, rectangleHeight);
4899
+ ctx.fillStyle = DividerColor;
4900
+ ctx.fillRect(dividerXPos, drawableArea.top, DividerSize, rectangleHeight);
4901
+ ctx.fillStyle = PlayheadColor;
4902
+ ctx.fillRect(playheadPos, drawableArea.top, PlayheadSize, rectangleHeight);
4903
+ ctx.restore();
4904
+ }
4905
+ /**
4906
+ * Method to do cleanup when the object is done being used.
4907
+ *
4908
+ */
4909
+ destroy() {
4910
+ if (!this._ctx || !this._ctx.canvas) {
4911
+ return;
4912
+ }
4913
+ this._removeEventListeners(this._ctx.canvas);
4914
+ this._ctx = null;
4915
+ }
4916
+ /**
4917
+ * This method clears the canvas
4918
+ */
4919
+ clear() {
4920
+ const { _ctx: ctx, _width, _height } = this;
4921
+ // If we do not have a context we can't really do much here!
4922
+ if (!ctx) {
4923
+ return;
4924
+ }
4925
+ // save the transformation matrix, clear the canvas then restore.
4926
+ ctx.save();
4927
+ ctx.resetTransform();
4928
+ ctx.clearRect(0, 0, _width, _height);
4929
+ ctx.restore();
4930
+ }
4931
+ }
4932
+
4933
+ const useStyles$K = makeStyles({
4934
+ canvas: {
4935
+ flexGrow: 1,
4936
+ width: "100%",
4937
+ height: "100%",
4095
4938
  },
4939
+ });
4940
+ const CanvasGraph = (props) => {
4941
+ const { collector, scene, layoutObservable, returnToPlayheadObservable, onVisibleRangeChangedObservable, initialGraphSize } = props;
4942
+ const classes = useStyles$K();
4943
+ const canvasRef = useRef(null);
4944
+ useEffect(() => {
4945
+ if (!canvasRef.current) {
4946
+ return;
4947
+ }
4948
+ if (initialGraphSize) {
4949
+ canvasRef.current.width = initialGraphSize.x;
4950
+ canvasRef.current.height = initialGraphSize.y;
4951
+ }
4952
+ let cs;
4953
+ try {
4954
+ cs = new CanvasGraphService(canvasRef.current, { datasets: collector.datasets, onVisibleRangeChangedObservable });
4955
+ }
4956
+ catch (error) {
4957
+ Logger.Error(error);
4958
+ return;
4959
+ }
4960
+ const layoutUpdated = (newSize) => {
4961
+ if (!canvasRef.current) {
4962
+ return;
4963
+ }
4964
+ const { left, top } = canvasRef.current.getBoundingClientRect();
4965
+ newSize.width = newSize.width - left;
4966
+ newSize.height = newSize.height - top;
4967
+ cs?.resize(newSize);
4968
+ };
4969
+ const dataUpdated = () => {
4970
+ cs?.update();
4971
+ };
4972
+ const metaUpdated = (meta) => {
4973
+ if (!cs) {
4974
+ return;
4975
+ }
4976
+ cs.metadata = meta;
4977
+ cs.update();
4978
+ };
4979
+ const resetDataPosition = () => {
4980
+ cs?.resetDataPosition();
4981
+ };
4982
+ scene.onAfterRenderObservable.add(dataUpdated);
4983
+ collector.metadataObservable.add(metaUpdated);
4984
+ layoutObservable?.add(layoutUpdated);
4985
+ returnToPlayheadObservable?.add(resetDataPosition);
4986
+ return () => {
4987
+ cs?.destroy();
4988
+ layoutObservable?.removeCallback(layoutUpdated);
4989
+ scene.onAfterRenderObservable.removeCallback(dataUpdated);
4990
+ collector.metadataObservable.removeCallback(metaUpdated);
4991
+ };
4992
+ }, [canvasRef, collector, scene, layoutObservable, returnToPlayheadObservable, onVisibleRangeChangedObservable, initialGraphSize]);
4993
+ return jsx("canvas", { className: classes.canvas, ref: canvasRef });
4096
4994
  };
4097
4995
 
4098
- const ToolsPane = (props) => {
4099
- return jsx(ExtensibleAccordion, { ...props });
4996
+ const SpinButton = (props) => {
4997
+ SpinButton.displayName = "SpinButton";
4998
+ const classes = useInputStyles$1();
4999
+ const { size } = useContext(ToolContext);
5000
+ const { min, max } = props;
5001
+ const [value, setValue] = useState(props.value);
5002
+ const lastCommittedValue = useRef(props.value);
5003
+ // 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
5004
+ const step = props.step != undefined ? props.step : props.forceInt ? 1 : undefined;
5005
+ 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
5006
+ useEffect(() => {
5007
+ if (props.value !== lastCommittedValue.current) {
5008
+ lastCommittedValue.current = props.value;
5009
+ setValue(props.value); // Update local state when props.value changes
5010
+ }
5011
+ }, [props.value]);
5012
+ const validateValue = (numericValue) => {
5013
+ const outOfBounds = (min !== undefined && numericValue < min) || (max !== undefined && numericValue > max);
5014
+ const failsValidator = props.validator && !props.validator(numericValue);
5015
+ const failsIntCheck = props.forceInt ? !Number.isInteger(numericValue) : false;
5016
+ const invalid = !!outOfBounds || !!failsValidator || isNaN(numericValue) || !!failsIntCheck;
5017
+ return !invalid;
5018
+ };
5019
+ const tryCommitValue = (currVal) => {
5020
+ // Only commit if valid and different from last committed value
5021
+ if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
5022
+ lastCommittedValue.current = currVal;
5023
+ props.onChange(currVal);
5024
+ }
5025
+ };
5026
+ const handleChange = (event, data) => {
5027
+ event.stopPropagation(); // Prevent event propagation
5028
+ if (data.value != null && !Number.isNaN(data.value)) {
5029
+ setValue(data.value);
5030
+ tryCommitValue(data.value);
5031
+ }
5032
+ };
5033
+ const handleKeyUp = (event) => {
5034
+ event.stopPropagation(); // Prevent event propagation
5035
+ if (event.key !== "Enter") {
5036
+ const currVal = parseFloat(event.target.value); // Cannot use currentTarget.value as it won't have the most recently typed value
5037
+ setValue(currVal);
5038
+ tryCommitValue(currVal);
5039
+ }
5040
+ };
5041
+ const id = useId("spin-button");
5042
+ const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
5043
+ // Build input slot from inputClassName
5044
+ const inputSlot = {
5045
+ className: mergeClasses(classes.inputSlot, props.inputClassName),
5046
+ };
5047
+ const spinButton = (jsx(SpinButton$1, { ...props, appearance: "outline", input: 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 }));
5048
+ return props.infoLabel ? (jsxs("div", { className: classes.container, children: [jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), spinButton] })) : (spinButton);
4100
5049
  };
4101
5050
 
4102
- const ToolsServiceIdentity = Symbol("ToolsService");
4103
- /**
4104
- * A collection of usually optional, dynamic extensions.
4105
- * Common examples includes importing/exporting, or other general creation tools.
4106
- */
4107
- const ToolsServiceDefinition = {
4108
- friendlyName: "Tools Editor",
4109
- produces: [ToolsServiceIdentity],
4110
- consumes: [ShellServiceIdentity, SceneContextIdentity],
4111
- factory: (shellService, sceneContext) => {
4112
- const sectionsCollection = new ObservableCollection();
4113
- const sectionContentCollection = new ObservableCollection();
4114
- // Only show the Tools pane if some tool content has been added.
4115
- let toolsPaneRegistration = null;
4116
- sectionContentCollection.observable.add(() => {
4117
- if (sectionContentCollection.items.length === 0) {
4118
- toolsPaneRegistration?.dispose();
4119
- toolsPaneRegistration = null;
4120
- }
4121
- else if (!toolsPaneRegistration) {
4122
- toolsPaneRegistration = shellService.addSidePane({
4123
- key: "Tools",
4124
- title: "Tools",
4125
- icon: WrenchRegular,
4126
- horizontalLocation: "right",
4127
- verticalLocation: "top",
4128
- order: 400,
4129
- suppressTeachingMoment: true,
4130
- content: () => {
4131
- const sections = useOrderedObservableCollection(sectionsCollection);
4132
- const sectionContent = useObservableCollection(sectionContentCollection);
4133
- const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
4134
- return scene && jsx(ToolsPane, { sections: sections, sectionContent: sectionContent, context: scene });
4135
- },
4136
- });
4137
- }
4138
- });
4139
- /**
4140
- * Left TODO: Implement the following sections from toolsTabComponent.tsx
4141
- * - GLTF Validator (see glTFComponent.tsx) (consider putting in Import tools)
4142
- * - Reflector
4143
- * - GIF (consider putting in Capture Tools)
4144
- * - Replay (consider putting in Capture Tools)
4145
- */
4146
- return {
4147
- addSection: (section) => sectionsCollection.add(section),
4148
- addSectionContent: (content) => sectionContentCollection.add(content),
4149
- dispose: () => toolsPaneRegistration?.dispose(),
4150
- };
5051
+ const TextInput = (props) => {
5052
+ TextInput.displayName = "TextInput";
5053
+ const classes = useInputStyles$1();
5054
+ const [value, setValue] = useState(props.value);
5055
+ const lastCommittedValue = useRef(props.value);
5056
+ const { size } = useContext(ToolContext);
5057
+ useEffect(() => {
5058
+ if (props.value !== lastCommittedValue.current) {
5059
+ setValue(props.value); // Update local state when props.value changes
5060
+ lastCommittedValue.current = props.value;
5061
+ }
5062
+ }, [props.value]);
5063
+ const validateValue = (val) => {
5064
+ const failsValidator = props.validator && !props.validator(val);
5065
+ return !failsValidator;
5066
+ };
5067
+ const tryCommitValue = (currVal) => {
5068
+ // Only commit if valid and different from last committed value
5069
+ if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
5070
+ lastCommittedValue.current = currVal;
5071
+ props.onChange(currVal);
5072
+ }
5073
+ };
5074
+ const handleChange = (event, data) => {
5075
+ event.stopPropagation();
5076
+ setValue(data.value);
5077
+ if (!props.validateOnlyOnBlur) {
5078
+ tryCommitValue(data.value);
5079
+ }
5080
+ };
5081
+ const handleKeyUp = (event) => {
5082
+ event.stopPropagation();
5083
+ if (!props.validateOnlyOnBlur) {
5084
+ tryCommitValue(event.currentTarget.value);
5085
+ }
5086
+ };
5087
+ const handleBlur = (event) => {
5088
+ HandleOnBlur(event);
5089
+ if (props.validateOnlyOnBlur) {
5090
+ tryCommitValue(event.currentTarget.value);
5091
+ }
5092
+ };
5093
+ const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
5094
+ const id = useId("input-button");
5095
+ 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: handleBlur, className: mergedClassName })] }));
5096
+ };
5097
+
5098
+ const useDropdownStyles = makeStyles({
5099
+ dropdown: {
5100
+ minWidth: 0,
5101
+ width: "100%",
5102
+ },
5103
+ container: {
5104
+ display: "flex",
5105
+ flexDirection: "column",
5106
+ justifyContent: "center", // align items vertically
4151
5107
  },
4152
- };
4153
-
4154
- const BabylonWebResources = {
4155
- homepage: "https://www.babylonjs.com",
4156
- repository: "https://github.com/BabylonJS/Babylon.js",
4157
- bugs: "https://github.com/BabylonJS/Babylon.js/issues",
4158
- };
5108
+ dropdownText: { textAlign: "end", textOverflow: "ellipsis", whiteSpace: "nowrap", overflowX: "hidden" },
5109
+ });
4159
5110
  /**
4160
- * Well-known default built in extensions for the Inspector.
5111
+ * Renders a fluent UI dropdown component for the options passed in, and an additional 'Not Defined' option if null is set to true
5112
+ * This component can handle both null and undefined values
5113
+ * @param props
5114
+ * @returns dropdown component
4161
5115
  */
4162
- const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
4163
- // {
4164
- // name: "Asset Creation",
4165
- // description: "Adds new features to enable creating Babylon assets such as node materials, flow graphs, and more.",
4166
- // keywords: ["creation"],
4167
- // getExtensionModuleAsync: async () => await import("../services/creationToolsService"),
4168
- // },
4169
- {
4170
- name: "Export Tools",
4171
- description: "Adds new features to enable exporting Babylon assets such as .gltf, .glb, .babylon, and more.",
4172
- keywords: ["export", "gltf", "glb", "babylon", "exporter", "tools"],
4173
- ...BabylonWebResources,
4174
- author: { name: "Alex Chuber", forumUserName: "alexchuber" },
4175
- getExtensionModuleAsync: async () => await import('./exportService-CB99mqRE.js'),
5116
+ const Dropdown = (props) => {
5117
+ Dropdown.displayName = "Dropdown";
5118
+ const classes = useDropdownStyles();
5119
+ const { options, value } = props;
5120
+ const [defaultVal, setDefaultVal] = useState(props.value);
5121
+ const { size } = useContext(ToolContext);
5122
+ useEffect(() => {
5123
+ setDefaultVal(value);
5124
+ }, [props.value]);
5125
+ const id = useId("dropdown");
5126
+ const mergedClassName = mergeClasses(classes.container, props.className);
5127
+ const optionLabel = options.find((o) => o.value === defaultVal)?.label;
5128
+ 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) => {
5129
+ const value = typeof props.value === "number" ? Number(data.optionValue) : data.optionValue;
5130
+ if (value !== undefined) {
5131
+ setDefaultVal(value);
5132
+ props.onChange(value);
5133
+ }
5134
+ }, selectedOptions: [defaultVal.toString()], value: optionLabel, children: options.map((option) => (jsx(Option, { value: option.value.toString(), disabled: false, children: option.label }, option.label))) })] }));
5135
+ };
5136
+ const NumberDropdown = Dropdown;
5137
+ const StringDropdown = Dropdown;
5138
+
5139
+ const useStyles$J = makeStyles({
5140
+ surface: {
5141
+ maxWidth: "400px",
4176
5142
  },
4177
- {
4178
- name: "Capture Tools",
4179
- description: "Adds new features to enable capturing screenshots, GIFs, videos, and more.",
4180
- keywords: ["capture", "screenshot", "gif", "video", "tools"],
4181
- ...BabylonWebResources,
4182
- author: { name: "Alex Chuber", forumUserName: "alexchuber" },
4183
- getExtensionModuleAsync: async () => await import('./captureService-BvHqJq2V.js'),
5143
+ content: {
5144
+ display: "flex",
5145
+ flexDirection: "column",
5146
+ gap: tokens.spacingVerticalM,
5147
+ padding: tokens.spacingHorizontalL,
5148
+ minWidth: "300px",
4184
5149
  },
4185
- {
4186
- name: "Import Tools",
4187
- description: "Adds new features related to importing Babylon assets.",
4188
- keywords: ["import", "tools"],
4189
- ...BabylonWebResources,
4190
- author: { name: "Alex Chuber", forumUserName: "alexchuber" },
4191
- getExtensionModuleAsync: async () => await import('./importService-BG3n3RAJ.js'),
5150
+ });
5151
+ const Popover = forwardRef((props, ref) => {
5152
+ const { children, open: controlledOpen, onOpenChange, positioning, surfaceClassName } = props;
5153
+ const [internalOpen, setInternalOpen] = useState(false);
5154
+ const classes = useStyles$J();
5155
+ const isControlled = controlledOpen !== undefined;
5156
+ const popoverOpen = isControlled ? controlledOpen : internalOpen;
5157
+ const handleOpenChange = (_, data) => {
5158
+ if (!isControlled) {
5159
+ setInternalOpen(data.open);
5160
+ }
5161
+ onOpenChange?.(data.open);
5162
+ };
5163
+ return (jsxs(Popover$1, { open: popoverOpen, onOpenChange: handleOpenChange, positioning: positioning ?? {
5164
+ align: "start",
5165
+ overflowBoundary: document.body,
5166
+ autoSize: true,
5167
+ }, children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: props.trigger ?? jsx(Button, { ref: ref, icon: props.icon, onClick: () => handleOpenChange(null, { open: true }) }) }), jsx(PopoverSurface, { className: surfaceClassName ?? classes.surface, children: jsx("div", { className: classes.content, children: children }) })] }));
5168
+ });
5169
+ Popover.displayName = "Popover";
5170
+
5171
+ const useColorPickerStyles = makeStyles({
5172
+ container: {
5173
+ width: "350px",
5174
+ display: "flex", // becomes a flexbox
5175
+ flexDirection: "column", // with children in a column
5176
+ alignItems: "center", // centers children horizontally
5177
+ justifyContent: "center", // centers children vertically (if height is set)
5178
+ gap: tokens.spacingVerticalM,
5179
+ overflow: "visible",
4192
5180
  },
4193
- {
4194
- name: "Quick Creation Tools (Preview)",
4195
- description: "Adds a new panel for easy creation of various Babylon assets. This is a WIP extension...expect changes!",
4196
- keywords: ["creation", "tools"],
4197
- ...BabylonWebResources,
4198
- author: { name: "Babylon.js", forumUserName: "" },
4199
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-CBcmm2Zr.js'),
5181
+ row: {
5182
+ flex: 1, // is a row in the container's flex column
5183
+ display: "flex", // becomes its own flexbox
5184
+ flexDirection: "row", // with children in a row
5185
+ gap: tokens.spacingHorizontalXL,
5186
+ alignItems: "center", // align items vertically
5187
+ width: "100%",
4200
5188
  },
4201
- ]);
4202
-
4203
- const SpinButton = (props) => {
4204
- SpinButton.displayName = "SpinButton";
4205
- const classes = useInputStyles$1();
5189
+ colorPicker: {
5190
+ flex: 1,
5191
+ width: "350px",
5192
+ height: "350px",
5193
+ },
5194
+ previewColor: {
5195
+ width: "60px",
5196
+ height: "60px",
5197
+ borderRadius: tokens.borderRadiusMedium, // 4px?
5198
+ border: `${tokens.spacingVerticalXXS} solid ${tokens.colorNeutralShadowKeyLighter}`,
5199
+ "@media (forced-colors: active)": {
5200
+ forcedColorAdjust: "none", // ensures elmement maintains color in high constrast mode
5201
+ },
5202
+ },
5203
+ inputRow: {
5204
+ display: "flex",
5205
+ flexDirection: "row",
5206
+ flex: 1, // grow and fill available space
5207
+ justifyContent: "center",
5208
+ gap: "10px",
5209
+ width: "100%",
5210
+ },
5211
+ inputField: {
5212
+ flex: 1, // grow and fill available space
5213
+ width: "auto",
5214
+ minWidth: 0,
5215
+ gap: tokens.spacingVerticalSNudge, // 6px
5216
+ },
5217
+ trigger: {
5218
+ display: "flex",
5219
+ alignItems: "center",
5220
+ },
5221
+ });
5222
+ const ColorPickerPopup = forwardRef((props, ref) => {
5223
+ ColorPickerPopup.displayName = "ColorPickerPopup";
5224
+ const { value, onChange, isLinearMode, ...rest } = props;
5225
+ const classes = useColorPickerStyles();
5226
+ const [color, setColor] = useState(value);
5227
+ const [isLinear, setIsLinear] = useState(isLinearMode ?? false);
5228
+ const [isFloat, setFloat] = useState(false);
4206
5229
  const { size } = useContext(ToolContext);
4207
- const { min, max } = props;
4208
- const [value, setValue] = useState(props.value);
4209
- const lastCommittedValue = useRef(props.value);
4210
- // 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
4211
- const step = props.step != undefined ? props.step : props.forceInt ? 1 : undefined;
4212
- 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
4213
5230
  useEffect(() => {
4214
- if (props.value !== lastCommittedValue.current) {
4215
- lastCommittedValue.current = props.value;
4216
- setValue(props.value); // Update local state when props.value changes
5231
+ setColor(value); // Ensures the trigger color updates when props.value changes
5232
+ }, [value]);
5233
+ const handleColorPickerChange = (_, data) => {
5234
+ let color = Color3.FromHSV(data.color.h, data.color.s, data.color.v);
5235
+ if (value instanceof Color4) {
5236
+ color = Color4.FromColor3(color, data.color.a ?? 1);
4217
5237
  }
4218
- }, [props.value]);
4219
- const validateValue = (numericValue) => {
4220
- const outOfBounds = (min !== undefined && numericValue < min) || (max !== undefined && numericValue > max);
4221
- const failsValidator = props.validator && !props.validator(numericValue);
4222
- const failsIntCheck = props.forceInt ? !Number.isInteger(numericValue) : false;
4223
- const invalid = !!outOfBounds || !!failsValidator || isNaN(numericValue) || !!failsIntCheck;
4224
- return !invalid;
5238
+ handleChange(color);
4225
5239
  };
4226
- const tryCommitValue = (currVal) => {
4227
- // Only commit if valid and different from last committed value
4228
- if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
4229
- lastCommittedValue.current = currVal;
4230
- props.onChange(currVal);
5240
+ const handleChange = (newColor) => {
5241
+ setColor(newColor);
5242
+ onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
5243
+ };
5244
+ return (jsx(Popover, { trigger: jsx(ColorSwatch, { className: classes.trigger, ref: ref, ...rest, borderColor: tokens.colorNeutralShadowKeyDarker, size: size === "small" ? "extra-small" : "small", shape: "rounded", color: color.toHexString(), value: color.toHexString().slice(1) }), 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: {
5245
+ label: "Color Space",
5246
+ info: jsx(Body1, { children: "Today this is not mutable as the color space is determined by the entity. Soon we will allow swapping" }),
5247
+ }, options: [
5248
+ { label: "Gamma", value: 0 },
5249
+ { label: "Linear", value: 1 },
5250
+ ], disabled: true, value: isLinear ? 1 : 0, onChange: (val) => setIsLinear(val === 1) }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
5251
+ label: "Data Type",
5252
+ info: jsx(Body1, { children: "We will introduce this functionality soon!" }),
5253
+ }, options: [
5254
+ { label: "Int", value: 0 },
5255
+ { label: "Float", value: 1 },
5256
+ ], 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 }) })] }) }));
5257
+ });
5258
+ /**
5259
+ * Component which displays the passed in color's HEX value, either in linearSpace (if linearHex is true) or in gamma space
5260
+ * When the hex color is changed by user, component calculates the new Color3/4 value and calls onChange
5261
+ *
5262
+ * Component uses the isLinearMode boolean to display an informative label regarding linear / gamma space
5263
+ * @param props - The properties for the InputHexField component.
5264
+ * @returns
5265
+ */
5266
+ const InputHexField = (props) => {
5267
+ const classes = useColorPickerStyles();
5268
+ const { title, value, onChange, linearHex, isLinearMode } = props;
5269
+ 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
5270
+ ? {
5271
+ label: title,
5272
+ // If not representing a linearHex, no info is needed.
5273
+ info: !props.linearHex ? undefined : !isLinearMode ? ( // If representing a linear hex but we are in gammaMode, simple message explaining why linearHex is disabled
5274
+ 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 " })) : (
5275
+ // If representing a linear hex and we are in linearMode, give information about how to use these hex values
5276
+ 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!" })] })),
5277
+ }
5278
+ : undefined }));
5279
+ };
5280
+ const InputRgbField = (props) => {
5281
+ const { value, onChange, title, rgbKey } = props;
5282
+ const classes = useColorPickerStyles();
5283
+ const handleChange = useCallback((val) => {
5284
+ const newColor = value.clone();
5285
+ newColor[rgbKey] = val / 255.0; // Convert to 0-1 range
5286
+ onChange(newColor);
5287
+ }, [value, onChange, rgbKey]);
5288
+ 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 }));
5289
+ };
5290
+ function rgbaToHsv(color) {
5291
+ const c = new Color3(color.r, color.g, color.b);
5292
+ const hsv = c.toHSV();
5293
+ return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a };
5294
+ }
5295
+ /**
5296
+ * 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.
5297
+ * 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.
5298
+ * Value (V) ranges from 0 to 100%, representing the brightness of the color, with 0 being black and 100 being the brightest.
5299
+ * @param props - The properties for the InputHsvField component.
5300
+ */
5301
+ const InputHsvField = (props) => {
5302
+ const { value, title, hsvKey, max, onChange, scale = 1 } = props;
5303
+ const classes = useColorPickerStyles();
5304
+ const handleChange = useCallback((val) => {
5305
+ // Convert current color to HSV, update the new hsv value, then call onChange prop
5306
+ const hsv = rgbaToHsv(value);
5307
+ hsv[hsvKey] = val / scale;
5308
+ let newColor = Color3.FromHSV(hsv.h, hsv.s, hsv.v);
5309
+ if (value instanceof Color4) {
5310
+ newColor = Color4.FromColor3(newColor, value.a ?? 1);
5311
+ }
5312
+ props.onChange(newColor);
5313
+ }, [value, onChange, hsvKey, scale]);
5314
+ 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 }));
5315
+ };
5316
+ /**
5317
+ * 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).
5318
+ * @param props
5319
+ * @returns
5320
+ */
5321
+ const InputAlphaField = (props) => {
5322
+ const classes = useColorPickerStyles();
5323
+ const { color, onChange } = props;
5324
+ const handleChange = useCallback((value) => {
5325
+ if (Number.isNaN(value) || value < 0 || value > 1) {
5326
+ return;
4231
5327
  }
4232
- };
4233
- const handleChange = (event, data) => {
4234
- event.stopPropagation(); // Prevent event propagation
4235
- if (data.value != null && !Number.isNaN(data.value)) {
4236
- setValue(data.value);
4237
- tryCommitValue(data.value);
5328
+ if (color instanceof Color4) {
5329
+ const newColor = color.clone();
5330
+ newColor.a = value;
5331
+ return newColor;
4238
5332
  }
4239
- };
4240
- const handleKeyUp = (event) => {
4241
- event.stopPropagation(); // Prevent event propagation
4242
- if (event.key !== "Enter") {
4243
- const currVal = parseFloat(event.target.value); // Cannot use currentTarget.value as it won't have the most recently typed value
4244
- setValue(currVal);
4245
- tryCommitValue(currVal);
5333
+ else {
5334
+ return Color4.FromColor3(color, value);
4246
5335
  }
4247
- };
4248
- const id = useId("spin-button");
4249
- const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
4250
- // Build input slot from inputClassName
4251
- const inputSlot = {
4252
- className: mergeClasses(classes.inputSlot, props.inputClassName),
4253
- };
4254
- const spinButton = (jsx(SpinButton$1, { ...props, appearance: "outline", input: 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 }));
4255
- return props.infoLabel ? (jsxs("div", { className: classes.container, children: [jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), spinButton] })) : (spinButton);
5336
+ }, [onChange]);
5337
+ 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: {
5338
+ label: "Alpha",
5339
+ 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,
5340
+ } }));
4256
5341
  };
4257
5342
 
4258
- const useSyncedSliderStyles = makeStyles({
4259
- container: { display: "flex", minWidth: 0 },
4260
- syncedSlider: {
4261
- flex: "1 1 0",
4262
- flexDirection: "row",
5343
+ const useStyles$I = makeStyles({
5344
+ sidebar: {
4263
5345
  display: "flex",
5346
+ flexDirection: "column",
5347
+ width: "280px",
5348
+ minWidth: "280px",
5349
+ overflowY: "auto",
5350
+ overflowX: "hidden",
5351
+ borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
5352
+ backgroundColor: tokens.colorNeutralBackground1,
5353
+ },
5354
+ sidebarItem: {
5355
+ display: "grid",
5356
+ width: "100%",
5357
+ minHeight: "30px",
5358
+ padding: `${tokens.spacingVerticalXXS} 0`,
4264
5359
  alignItems: "center",
4265
- minWidth: 0,
4266
5360
  },
4267
- slider: {
4268
- flex: "1 1 auto",
4269
- minWidth: "75px",
4270
- maxWidth: "75px",
5361
+ header: {
5362
+ color: tokens.colorNeutralForeground1,
5363
+ backgroundColor: tokens.colorBrandBackground,
5364
+ gridTemplateColumns: "10px 9fr 1fr 8px",
4271
5365
  },
4272
- compactSlider: {
4273
- flex: "1 1 auto",
4274
- minWidth: "50px", // Allow shrinking for compact mode
4275
- maxWidth: "75px",
5366
+ categoryHeader: {
5367
+ backgroundColor: tokens.colorNeutralBackground4,
5368
+ minHeight: "30px",
4276
5369
  },
4277
- growSlider: {
4278
- flex: "1 1 auto",
4279
- minWidth: "50px",
4280
- // No maxWidth - slider grows to fill available space
5370
+ categoryColumn2: {
5371
+ gridColumn: "2",
4281
5372
  },
4282
- compactSpinButton: {
4283
- width: "65px",
4284
- minWidth: "65px",
4285
- maxWidth: "65px",
5373
+ categoryColumn3: {
5374
+ gridColumn: "3",
5375
+ display: "flex",
5376
+ justifyContent: "end",
4286
5377
  },
4287
- compactSpinButtonInput: {
4288
- minWidth: "0",
5378
+ measure: {
5379
+ color: tokens.colorNeutralForeground1,
5380
+ gridTemplateColumns: "4px 6fr 1fr 10px",
5381
+ },
5382
+ measureOdd: {
5383
+ backgroundColor: tokens.colorNeutralBackground2,
5384
+ },
5385
+ measureEven: {
5386
+ backgroundColor: tokens.colorNeutralBackground1,
5387
+ },
5388
+ measureCategory: {
5389
+ display: "grid",
5390
+ gridTemplateColumns: "auto 7px 18px 10px 1fr",
5391
+ gridColumn: "2",
5392
+ alignItems: "center",
5393
+ },
5394
+ measureColorPicker: {
5395
+ gridColumn: "3",
5396
+ },
5397
+ measureLabel: {
5398
+ gridColumn: "5",
5399
+ },
5400
+ measureValue: {
5401
+ gridColumn: "3",
5402
+ textAlign: "right",
4289
5403
  },
4290
5404
  });
4291
- /**
4292
- * Component which synchronizes a slider and an input field, allowing the user to change the value using either control
4293
- * @param props
4294
- * @returns SyncedSlider component
4295
- */
4296
- const SyncedSliderInput = (props) => {
4297
- SyncedSliderInput.displayName = "SyncedSliderInput";
4298
- const { infoLabel, ...passthroughProps } = props;
4299
- const classes = useSyncedSliderStyles();
4300
- const { size } = useContext(ToolContext);
4301
- const [value, setValue] = useState(props.value ?? 0);
4302
- const pendingValueRef = useRef(undefined);
4303
- const isDraggingRef = useRef(false);
4304
- // NOTE: The Fluent slider will add tick marks if the step prop is anything other than undefined.
4305
- // To avoid this, we scale the min/max based on the step so we can always make step undefined.
4306
- // The actual step size in the Fluent slider is 1 when it is ste to undefined.
4307
- const min = props.min ?? 0;
4308
- const max = props.max ?? 100;
4309
- const step = props.step ?? 1;
5405
+ const PerformanceSidebar = (props) => {
5406
+ const { collector, onVisibleRangeChangedObservable } = props;
5407
+ const classes = useStyles$I();
5408
+ // Map from id to IPerfMetadata information
5409
+ const [metadataMap, setMetadataMap] = useState();
5410
+ // Map from category to all the ids belonging to that category
5411
+ const [metadataCategoryId, setMetadataCategoryId] = useState();
5412
+ // Count how many elements are checked for that category
5413
+ const [metadataCategoryChecked, setMetadataCategoryChecked] = useState();
5414
+ // List of ordered categories
5415
+ const [metadataCategories, setMetadataCategories] = useState();
5416
+ // Min/Max/Current values of the ids
5417
+ const [valueMap, setValueMap] = useState();
4310
5418
  useEffect(() => {
4311
- !isDraggingRef.current && setValue(props.value ?? 0); // Update local state when props.value changes as long as user is not actively dragging
4312
- }, [props.value]);
4313
- const handleSliderChange = (_, data) => {
4314
- const newValue = data.value * step;
4315
- setValue(newValue);
4316
- if (props.notifyOnlyOnRelease) {
4317
- // Store the value but don't notify parent yet
4318
- pendingValueRef.current = newValue;
4319
- }
4320
- else {
4321
- // Notify parent as slider changes
4322
- props.onChange(newValue);
4323
- }
4324
- };
4325
- const handleSliderPointerDown = () => {
4326
- isDraggingRef.current = true;
4327
- };
4328
- const handleSliderPointerUp = () => {
4329
- if (props.notifyOnlyOnRelease && isDraggingRef.current && pendingValueRef.current !== undefined) {
4330
- props.onChange(pendingValueRef.current);
4331
- pendingValueRef.current = undefined;
5419
+ if (!onVisibleRangeChangedObservable) {
5420
+ return;
4332
5421
  }
4333
- isDraggingRef.current = false;
4334
- };
4335
- const handleInputChange = (value) => {
4336
- setValue(value);
4337
- props.onChange(value); // Input always updates immediately
5422
+ const observer = (observedProps) => {
5423
+ setValueMap(observedProps.valueMap);
5424
+ };
5425
+ onVisibleRangeChangedObservable.add(observer);
5426
+ return () => {
5427
+ onVisibleRangeChangedObservable.removeCallback(observer);
5428
+ };
5429
+ }, [onVisibleRangeChangedObservable]);
5430
+ useEffect(() => {
5431
+ const onUpdateMetadata = (metadata) => {
5432
+ const newCategoryIdMap = new Map();
5433
+ const newCategoryCheckedMap = new Map();
5434
+ metadata.forEach((value, id) => {
5435
+ const currentCategory = value.category ?? "";
5436
+ const currentIds = newCategoryIdMap.get(currentCategory) ?? [];
5437
+ let currentChecked = newCategoryCheckedMap.get(currentCategory) ?? 0;
5438
+ currentIds.push(id);
5439
+ newCategoryIdMap.set(currentCategory, currentIds);
5440
+ if (!value.hidden) {
5441
+ currentChecked += 1;
5442
+ }
5443
+ newCategoryCheckedMap.set(currentCategory, currentChecked);
5444
+ });
5445
+ const orderedCategories = Array.from(newCategoryIdMap.keys());
5446
+ orderedCategories.sort();
5447
+ setMetadataCategoryId(newCategoryIdMap);
5448
+ setMetadataCategoryChecked(newCategoryCheckedMap);
5449
+ setMetadataMap(metadata);
5450
+ setMetadataCategories(orderedCategories);
5451
+ };
5452
+ collector.metadataObservable.add(onUpdateMetadata);
5453
+ return () => {
5454
+ collector.metadataObservable.removeCallback(onUpdateMetadata);
5455
+ };
5456
+ }, [collector]);
5457
+ const onCheckChange = (id) => (selected) => {
5458
+ collector.updateMetadata(id, "hidden", !selected);
4338
5459
  };
4339
- const hasSlider = props.min !== undefined && props.max !== undefined;
4340
- // Determine Slider className based on props
4341
- const getSliderClassName = () => {
4342
- if (props.growSlider) {
4343
- return classes.growSlider;
5460
+ const onCheckAllChange = (category) => (selected) => {
5461
+ const categoryIds = metadataCategoryId?.get(category);
5462
+ if (!categoryIds) {
5463
+ return;
4344
5464
  }
4345
- if (props.compact) {
4346
- return classes.compactSlider;
5465
+ for (const id of categoryIds) {
5466
+ collector.updateMetadata(id, "hidden", !selected);
4347
5467
  }
4348
- return classes.slider;
4349
5468
  };
4350
- return (jsxs("div", { className: classes.container, children: [infoLabel && jsx(InfoLabel, { ...infoLabel, htmlFor: "syncedSlider" }), jsxs("div", { id: "syncedSlider", className: classes.syncedSlider, children: [hasSlider && (jsx(Slider, { ...passthroughProps, className: getSliderClassName(), size: size, min: min / step, max: max / step, step: undefined, value: value / step, onChange: handleSliderChange, onPointerDown: handleSliderPointerDown, onPointerUp: handleSliderPointerUp })), jsx(SpinButton, { ...passthroughProps, className: hasSlider || props.compact ? classes.compactSpinButton : undefined, inputClassName: hasSlider || props.compact ? classes.compactSpinButtonInput : undefined, value: value, onChange: handleInputChange, step: props.step })] })] }));
5469
+ const onColorChange = (id) => (color) => {
5470
+ collector.updateMetadata(id, "color", color.toHexString());
5471
+ };
5472
+ return (jsx("div", { className: classes.sidebar, children: metadataCategories &&
5473
+ metadataCategories.map((category) => (jsxs("div", { children: [category ? (jsxs("div", { className: mergeClasses(classes.sidebarItem, classes.header, classes.categoryHeader), children: [jsx(Subtitle2Stronger, { className: classes.categoryColumn2, children: category }), jsx("div", { className: classes.categoryColumn3, children: jsx(Switch, { value: metadataCategoryChecked?.get(category) === metadataCategoryId?.get(category)?.length, onChange: onCheckAllChange(category) }) })] }, `header-${category}`)) : null, metadataCategoryId?.get(category)?.map((id, index) => {
5474
+ const metadata = metadataMap?.get(id);
5475
+ const range = valueMap?.get(id);
5476
+ return (metadata && (jsxs("div", { className: mergeClasses(classes.sidebarItem, classes.measure, index % 2 === 0 ? classes.measureEven : classes.measureOdd), children: [jsxs("div", { className: classes.measureCategory, children: [jsx(Switch, { value: !metadata.hidden, onChange: onCheckChange(id) }), jsx("div", { className: classes.measureColorPicker, children: jsx(ColorPickerPopup, { value: Color3.FromHexString(metadata.color ?? "#000"), onChange: onColorChange(id) }) }), jsx(Body1, { className: classes.measureLabel, children: id })] }), range && jsxs("div", { className: classes.measureValue, children: [" ", ((range.min + range.max) / 2).toFixed(2), " "] })] }, `perf-sidebar-item-${id}`)));
5477
+ })] }, `category-${category || "version"}`))) }));
4351
5478
  };
4352
5479
 
4353
- /**
4354
- * Renders a simple wrapper around the SyncedSliderInput
4355
- * @param props
4356
- * @returns
4357
- */
4358
- const SyncedSliderPropertyLine = forwardRef((props, ref) => {
4359
- SyncedSliderPropertyLine.displayName = "SyncedSliderPropertyLine";
4360
- const { label, description, ...sliderProps } = props;
4361
- return (jsx(PropertyLine, { ref: ref, ...props, children: jsx(SyncedSliderInput, { ...sliderProps }) }));
5480
+ const useStyles$H = makeStyles({
5481
+ container: {
5482
+ display: "flex",
5483
+ flexDirection: "row",
5484
+ height: "100%",
5485
+ width: "100%",
5486
+ fontFamily: "system-ui, -apple-system, sans-serif",
5487
+ overflow: "hidden",
5488
+ },
5489
+ returnButton: {
5490
+ position: "absolute",
5491
+ top: "10px",
5492
+ right: "10px",
5493
+ zIndex: 10,
5494
+ },
5495
+ sidebar: {
5496
+ flex: "0 0 auto",
5497
+ overflowY: "auto",
5498
+ overflowX: "hidden",
5499
+ },
5500
+ graph: {
5501
+ flex: "1 1 auto",
5502
+ position: "relative",
5503
+ backgroundColor: tokens.colorNeutralBackground1,
5504
+ overflow: "hidden",
5505
+ },
4362
5506
  });
4363
-
4364
- const TextInput = (props) => {
4365
- TextInput.displayName = "TextInput";
4366
- const classes = useInputStyles$1();
4367
- const [value, setValue] = useState(props.value);
4368
- const lastCommittedValue = useRef(props.value);
4369
- const { size } = useContext(ToolContext);
4370
- useEffect(() => {
4371
- if (props.value !== lastCommittedValue.current) {
4372
- setValue(props.value); // Update local state when props.value changes
4373
- lastCommittedValue.current = props.value;
4374
- }
4375
- }, [props.value]);
4376
- const validateValue = (val) => {
4377
- const failsValidator = props.validator && !props.validator(val);
4378
- return !failsValidator;
4379
- };
4380
- const tryCommitValue = (currVal) => {
4381
- // Only commit if valid and different from last committed value
4382
- if (validateValue(currVal) && currVal !== lastCommittedValue.current) {
4383
- lastCommittedValue.current = currVal;
4384
- props.onChange(currVal);
4385
- }
4386
- };
4387
- const handleChange = (event, data) => {
4388
- event.stopPropagation();
4389
- setValue(data.value);
4390
- if (!props.validateOnlyOnBlur) {
4391
- tryCommitValue(data.value);
4392
- }
5507
+ const PerformanceViewer = (props) => {
5508
+ const { scene, layoutObservable, returnToLiveObservable, performanceCollector, initialGraphSize } = props;
5509
+ const classes = useStyles$H();
5510
+ const [onVisibleRangeChangedObservable] = useState(() => new Observable());
5511
+ const onReturnToPlayheadClick = () => {
5512
+ returnToLiveObservable.notifyObservers();
4393
5513
  };
4394
- const handleKeyUp = (event) => {
4395
- event.stopPropagation();
4396
- if (!props.validateOnlyOnBlur) {
4397
- tryCommitValue(event.currentTarget.value);
5514
+ return (jsxs("div", { className: classes.container, children: [jsx(Button, { className: classes.returnButton, onClick: onReturnToPlayheadClick, label: "Return", title: "Return to Playhead" }), jsx("div", { className: classes.sidebar, children: jsx(PerformanceSidebar, { collector: performanceCollector, onVisibleRangeChangedObservable: onVisibleRangeChangedObservable }) }), jsx("div", { className: classes.graph, children: jsx(CanvasGraph, { returnToPlayheadObservable: returnToLiveObservable, layoutObservable: layoutObservable, scene: scene, collector: performanceCollector, onVisibleRangeChangedObservable: onVisibleRangeChangedObservable, initialGraphSize: initialGraphSize }) })] }));
5515
+ };
5516
+
5517
+ function AddStrategies(perfCollector) {
5518
+ perfCollector.addCollectionStrategies(...DefaultStrategiesList);
5519
+ if (PressureObserverWrapper.IsAvailable) {
5520
+ // Do not enable for now as the Pressure API does not
5521
+ // report factors at the moment.
5522
+ // perfCollector.addCollectionStrategies({
5523
+ // strategyCallback: PerfCollectionStrategy.ThermalStrategy(),
5524
+ // category: IPerfMetadataCategory.FrameSteps,
5525
+ // hidden: true,
5526
+ // });
5527
+ // perfCollector.addCollectionStrategies({
5528
+ // strategyCallback: PerfCollectionStrategy.PowerSupplyStrategy(),
5529
+ // category: IPerfMetadataCategory.FrameSteps,
5530
+ // hidden: true,
5531
+ // });
5532
+ perfCollector.addCollectionStrategies({
5533
+ strategyCallback: PerfCollectionStrategy.PressureStrategy(),
5534
+ category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */,
5535
+ hidden: true,
5536
+ });
5537
+ }
5538
+ }
5539
+ var PerfMetadataCategory;
5540
+ (function (PerfMetadataCategory) {
5541
+ PerfMetadataCategory["Count"] = "Count";
5542
+ PerfMetadataCategory["FrameSteps"] = "Frame Steps Duration";
5543
+ })(PerfMetadataCategory || (PerfMetadataCategory = {}));
5544
+ // list of strategies to add to perf graph automatically.
5545
+ const DefaultStrategiesList = [
5546
+ { strategyCallback: PerfCollectionStrategy.FpsStrategy() },
5547
+ { strategyCallback: PerfCollectionStrategy.TotalMeshesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5548
+ { strategyCallback: PerfCollectionStrategy.ActiveMeshesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5549
+ { strategyCallback: PerfCollectionStrategy.ActiveIndicesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5550
+ { strategyCallback: PerfCollectionStrategy.ActiveBonesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5551
+ { strategyCallback: PerfCollectionStrategy.ActiveParticlesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5552
+ { strategyCallback: PerfCollectionStrategy.DrawCallsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5553
+ { strategyCallback: PerfCollectionStrategy.TotalLightsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5554
+ { strategyCallback: PerfCollectionStrategy.TotalVerticesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5555
+ { strategyCallback: PerfCollectionStrategy.TotalMaterialsStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5556
+ { strategyCallback: PerfCollectionStrategy.TotalTexturesStrategy(), category: "Count" /* PerfMetadataCategory.Count */, hidden: true },
5557
+ { strategyCallback: PerfCollectionStrategy.AbsoluteFpsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5558
+ { strategyCallback: PerfCollectionStrategy.MeshesSelectionStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5559
+ { strategyCallback: PerfCollectionStrategy.RenderTargetsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5560
+ { strategyCallback: PerfCollectionStrategy.ParticlesStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5561
+ { strategyCallback: PerfCollectionStrategy.SpritesStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5562
+ { strategyCallback: PerfCollectionStrategy.AnimationsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5563
+ { strategyCallback: PerfCollectionStrategy.PhysicsStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5564
+ { strategyCallback: PerfCollectionStrategy.RenderStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5565
+ { strategyCallback: PerfCollectionStrategy.FrameTotalStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5566
+ { strategyCallback: PerfCollectionStrategy.InterFrameStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5567
+ { strategyCallback: PerfCollectionStrategy.GpuFrameTimeStrategy(), category: "Frame Steps Duration" /* PerfMetadataCategory.FrameSteps */, hidden: true },
5568
+ ];
5569
+ // arbitrary window size
5570
+ const InitialWindowSize = { width: 1024, height: 512 };
5571
+ const InitialGraphSize = new Vector2(724, 512);
5572
+ const PerformanceStats = ({ context: scene }) => {
5573
+ const [isOpen, setIsOpen] = useState(false);
5574
+ const [isRecording, setIsRecording] = useState(false);
5575
+ const [isLoadedFromCsv, setIsLoadedFromCsv] = useState(false);
5576
+ const [performanceCollector, setPerformanceCollector] = useState();
5577
+ const [layoutObservable] = useState(() => new Observable());
5578
+ const [returnToLiveObservable] = useState(() => new Observable());
5579
+ const childWindowRef = useRef(null);
5580
+ useEffect(() => {
5581
+ if (!isLoadedFromCsv) {
5582
+ if (performanceCollector) {
5583
+ setIsRecording(false);
5584
+ performanceCollector.stop();
5585
+ performanceCollector.clear(false);
5586
+ AddStrategies(performanceCollector);
5587
+ }
4398
5588
  }
5589
+ }, [isLoadedFromCsv, performanceCollector]);
5590
+ const onClosePerformanceViewer = useCallback(() => {
5591
+ setIsLoadedFromCsv(false);
5592
+ setIsOpen(false);
5593
+ }, []);
5594
+ const onResize = useCallback((childWindow) => {
5595
+ const width = childWindow?.innerWidth ?? 0;
5596
+ const height = childWindow?.innerHeight ?? 0;
5597
+ layoutObservable.notifyObservers({ width, height });
5598
+ }, [layoutObservable]);
5599
+ const startPerformanceViewerPopup = useCallback(() => {
5600
+ if (performanceCollector && childWindowRef.current) {
5601
+ childWindowRef.current.open({
5602
+ defaultWidth: InitialWindowSize.width,
5603
+ defaultHeight: InitialWindowSize.height,
5604
+ title: "Realtime Performance Viewer",
5605
+ });
5606
+ }
5607
+ }, [performanceCollector]);
5608
+ const onPerformanceButtonClick = () => {
5609
+ setIsOpen(true);
5610
+ setIsRecording(true);
5611
+ performanceCollector?.start(true);
5612
+ startPerformanceViewerPopup();
4399
5613
  };
4400
- const handleBlur = (event) => {
4401
- HandleOnBlur(event);
4402
- if (props.validateOnlyOnBlur) {
4403
- tryCommitValue(event.currentTarget.value);
5614
+ const onLoadClick = (fileList) => {
5615
+ Tools.ReadFile(fileList[0], (data) => {
5616
+ // reopen window and load data!
5617
+ setIsOpen(false);
5618
+ setIsLoadedFromCsv(true);
5619
+ setIsRecording(false);
5620
+ performanceCollector?.stop();
5621
+ const isValid = performanceCollector?.loadFromFileData(data);
5622
+ if (!isValid) {
5623
+ // if our data isn't valid we close the window.
5624
+ setIsOpen(false);
5625
+ setIsRecording(true);
5626
+ performanceCollector?.start(true);
5627
+ }
5628
+ else {
5629
+ startPerformanceViewerPopup();
5630
+ }
5631
+ });
5632
+ };
5633
+ const onExportClick = () => {
5634
+ performanceCollector?.exportDataToCsv();
5635
+ };
5636
+ const onToggleRecording = () => {
5637
+ if (performanceCollector) {
5638
+ if (!performanceCollector.isStarted) {
5639
+ setIsRecording(true);
5640
+ performanceCollector.start(true);
5641
+ }
5642
+ else {
5643
+ setIsRecording(false);
5644
+ performanceCollector.stop();
5645
+ }
4404
5646
  }
4405
5647
  };
4406
- const mergedClassName = mergeClasses(classes.input, !validateValue(value) ? classes.invalid : "", props.className);
4407
- const id = useId("input-button");
4408
- 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: handleBlur, className: mergedClassName })] }));
5648
+ useEffect(() => {
5649
+ const perfCollector = scene.getPerfCollector();
5650
+ AddStrategies(perfCollector);
5651
+ setPerformanceCollector(perfCollector);
5652
+ }, [scene]);
5653
+ // Handle child window resize
5654
+ useEffect(() => {
5655
+ const handleResize = () => {
5656
+ const win = window;
5657
+ if (win) {
5658
+ onResize(win);
5659
+ }
5660
+ };
5661
+ window.addEventListener("resize", handleResize);
5662
+ return () => window.removeEventListener("resize", handleResize);
5663
+ }, [onResize]);
5664
+ return (jsxs(Fragment, { children: [!isOpen && jsx(ButtonLine, { label: "Open Realtime Perf Viewer", onClick: onPerformanceButtonClick }), !isOpen && jsx(FileUploadLine, { label: "Load Perf Viewer using CSV", accept: ".csv", onClick: onLoadClick }), jsx(ButtonLine, { label: "Export Perf to CSV", icon: ArrowDownloadRegular, onClick: onExportClick }), !isOpen && jsx(ButtonLine, { label: isRecording ? "Stop Recording" : "Begin Recording", icon: isRecording ? StopRegular : RecordRegular, onClick: onToggleRecording }), jsx(ChildWindow, { id: "performance-viewer", imperativeRef: childWindowRef, onOpenChange: (open) => !open && onClosePerformanceViewer(), children: performanceCollector && (jsx(PerformanceViewer, { scene: scene, layoutObservable: layoutObservable, returnToLiveObservable: returnToLiveObservable, performanceCollector: performanceCollector, initialGraphSize: InitialGraphSize })) })] }));
4409
5665
  };
4410
5666
 
4411
- const useDropdownStyles = makeStyles({
4412
- dropdown: {
4413
- minWidth: 0,
4414
- width: "100%",
4415
- },
4416
- container: {
4417
- display: "flex",
4418
- flexDirection: "column",
4419
- justifyContent: "center", // align items vertically
5667
+ /**
5668
+ * Wraps text in a property line
5669
+ * @param props - PropertyLineProps and TextProps
5670
+ * @returns property-line wrapped text
5671
+ */
5672
+ const TextPropertyLine = (props) => {
5673
+ TextPropertyLine.displayName = "TextPropertyLine";
5674
+ const { value, title } = props;
5675
+ return (jsx(PropertyLine, { ...props, children: jsx(Body1, { title: title, children: value ?? "" }) }));
5676
+ };
5677
+
5678
+ const useStyles$G = makeStyles({
5679
+ pinnedStatsPane: {
5680
+ flex: "0 1 auto",
5681
+ paddingBottom: tokens.spacingHorizontalM,
4420
5682
  },
4421
- dropdownText: { textAlign: "end", textOverflow: "ellipsis", whiteSpace: "nowrap", overflowX: "hidden" },
4422
5683
  });
5684
+ const StatsPane = (props) => {
5685
+ const classes = useStyles$G();
5686
+ const scene = props.context;
5687
+ const engine = scene.getEngine();
5688
+ const fps = useObservableState(() => Math.round(engine.getFps()), engine.onBeginFrameObservable);
5689
+ return (jsxs(Fragment, { children: [jsxs(SidePaneContainer, { className: classes.pinnedStatsPane, children: [jsx(TextPropertyLine, { label: "Version", description: "The Babylon.js engine version.", value: AbstractEngine.Version }, "EngineVersion"), jsx(StringifiedPropertyLine, { label: "FPS:", description: "The current framerate", value: fps }, "FPS")] }), jsx(ExtensibleAccordion, { ...props })] }));
5690
+ };
5691
+
4423
5692
  /**
4424
- * Renders a fluent UI dropdown component for the options passed in, and an additional 'Not Defined' option if null is set to true
4425
- * This component can handle both null and undefined values
4426
- * @param props
4427
- * @returns dropdown component
5693
+ * Displays an icon indicating enabled (green check) or disabled (red cross) state
5694
+ * @param props - The properties for the PropertyLine, including the boolean value to display.
5695
+ * @returns A PropertyLine component with a PresenceBadge indicating the boolean state.
4428
5696
  */
4429
- const Dropdown = (props) => {
4430
- Dropdown.displayName = "Dropdown";
4431
- const classes = useDropdownStyles();
4432
- const { options, value } = props;
4433
- const [defaultVal, setDefaultVal] = useState(props.value);
4434
- const { size } = useContext(ToolContext);
4435
- useEffect(() => {
4436
- setDefaultVal(value);
4437
- }, [props.value]);
4438
- const id = useId("dropdown");
4439
- const mergedClassName = mergeClasses(classes.container, props.className);
4440
- const optionLabel = options.find((o) => o.value === defaultVal)?.label;
4441
- 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) => {
4442
- const value = typeof props.value === "number" ? Number(data.optionValue) : data.optionValue;
4443
- if (value !== undefined) {
4444
- setDefaultVal(value);
4445
- props.onChange(value);
4446
- }
4447
- }, selectedOptions: [defaultVal.toString()], value: optionLabel, children: options.map((option) => (jsx(Option, { value: option.value.toString(), disabled: false, children: option.label }, option.label))) })] }));
5697
+ const BooleanBadgePropertyLine = (props) => {
5698
+ BooleanBadgePropertyLine.displayName = "BooleanBadgePropertyLine";
5699
+ return (jsx(PropertyLine, { ...props, children: jsx(PresenceBadge, { status: props.value ? "available" : "do-not-disturb", outOfOffice: true }) }));
4448
5700
  };
4449
- const NumberDropdown = Dropdown;
4450
- const StringDropdown = Dropdown;
4451
5701
 
4452
- const useStyles$G = makeStyles({
4453
- surface: {
4454
- maxWidth: "400px",
5702
+ const SystemStats = ({ context: scene }) => {
5703
+ const engine = scene.getEngine();
5704
+ const caps = engine.getCaps();
5705
+ const resolution = useObservableState(() => `${engine.getRenderWidth()} x ${engine.getRenderHeight()}`, engine.onResizeObservable);
5706
+ const hardwareScalingLevel = useObservableState(() => engine.getHardwareScalingLevel(), engine.onResizeObservable);
5707
+ return (jsxs(Fragment, { children: [jsx(TextPropertyLine, { label: "Resolution", value: resolution }, "Resolution"), jsx(StringifiedPropertyLine, { label: "Hardware Scaling Level", value: hardwareScalingLevel }, "HardwareScalingLevel"), jsx(TextPropertyLine, { label: "Engine", value: engine.description }, "Engine"), jsx(BooleanBadgePropertyLine, { label: "StdDerivatives", value: caps.standardDerivatives }, "StdDerivatives"), jsx(BooleanBadgePropertyLine, { label: "Compressed Textures", value: caps.s3tc !== undefined }, "CompressedTextures"), jsx(BooleanBadgePropertyLine, { label: "Hardware Instances", value: caps.instancedArrays }, "HardwareInstances"), jsx(BooleanBadgePropertyLine, { label: "Texture Float", value: caps.textureFloat }, "TextureFloat"), jsx(BooleanBadgePropertyLine, { label: "Texture Half Float", value: caps.textureHalfFloat }, "TextureHalfFloat"), jsx(BooleanBadgePropertyLine, { label: "Render to Texture Float", value: caps.textureFloatRender }, "RenderToTextureFloat"), jsx(BooleanBadgePropertyLine, { label: "Render to Texture Half Float", value: caps.textureHalfFloatRender }, "RenderToTextureHalfFloat"), jsx(BooleanBadgePropertyLine, { label: "32bits Indices", value: caps.uintIndices }, "32bitsIndices"), jsx(BooleanBadgePropertyLine, { label: "Fragment Depth", value: caps.fragmentDepthSupported }, "FragmentDepth"), jsx(BooleanBadgePropertyLine, { label: "High Precision Shaders", value: caps.highPrecisionShaderSupported }, "HighPrecisionShaders"), jsx(BooleanBadgePropertyLine, { label: "Draw Buffers", value: caps.drawBuffersExtension }, "DrawBuffers"), jsx(BooleanBadgePropertyLine, { label: "Vertex Array Object", value: caps.vertexArrayObject }, "VertexArrayObject"), jsx(BooleanBadgePropertyLine, { label: "Timer Query", value: caps.timerQuery !== undefined }, "TimerQuery"), jsx(BooleanBadgePropertyLine, { label: "Stencil", value: engine.isStencilEnable }, "Stencil"), jsx(BooleanBadgePropertyLine, { label: "Parallel Shader Compilation", value: caps.parallelShaderCompile != null }, "ParallelShaderCompilation"), jsx(StringifiedPropertyLine, { label: "Max Textures Units", value: caps.maxTexturesImageUnits }, "MaxTexturesUnits"), jsx(StringifiedPropertyLine, { label: "Max Textures Size", value: caps.maxTextureSize }, "MaxTexturesSize"), jsx(StringifiedPropertyLine, { label: "Max Anisotropy", value: caps.maxAnisotropy }, "MaxAnisotropy"), jsx(TextPropertyLine, { label: "Driver", value: engine.extractDriverInfo() }, "Driver")] }));
5708
+ };
5709
+
5710
+ const StatsServiceIdentity = Symbol("StatsService");
5711
+ /**
5712
+ * Provides a scene stats pane.
5713
+ */
5714
+ const StatsServiceDefinition = {
5715
+ friendlyName: "Stats",
5716
+ produces: [StatsServiceIdentity],
5717
+ consumes: [ShellServiceIdentity, SceneContextIdentity],
5718
+ factory: (shellService, sceneContext) => {
5719
+ const sectionsCollection = new ObservableCollection();
5720
+ const sectionContentCollection = new ObservableCollection();
5721
+ const registration = shellService.addSidePane({
5722
+ key: "Statistics",
5723
+ title: "Statistics",
5724
+ icon: DataBarHorizontalRegular,
5725
+ horizontalLocation: "right",
5726
+ verticalLocation: "top",
5727
+ order: 300,
5728
+ suppressTeachingMoment: true,
5729
+ content: () => {
5730
+ const sections = useOrderedObservableCollection(sectionsCollection);
5731
+ const sectionContent = useObservableCollection(sectionContentCollection);
5732
+ const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
5733
+ return jsx(Fragment, { children: scene && jsx(StatsPane, { sections: sections, sectionContent: sectionContent, context: scene }) });
5734
+ },
5735
+ });
5736
+ // Default/built-in sections.
5737
+ sectionsCollection.add({
5738
+ identity: "Performance",
5739
+ order: 0,
5740
+ });
5741
+ sectionsCollection.add({
5742
+ identity: "Count",
5743
+ order: 1,
5744
+ });
5745
+ sectionsCollection.add({
5746
+ identity: "Frame Steps Duration",
5747
+ order: 2,
5748
+ });
5749
+ sectionsCollection.add({
5750
+ identity: "System Info",
5751
+ order: 3,
5752
+ });
5753
+ // Default/built-in content.
5754
+ sectionContentCollection.add({
5755
+ key: "DefaultPerfStats",
5756
+ section: "Performance",
5757
+ order: 0,
5758
+ component: PerformanceStats,
5759
+ });
5760
+ sectionContentCollection.add({
5761
+ key: "DefaultCountStats",
5762
+ section: "Count",
5763
+ order: 1,
5764
+ component: CountStats,
5765
+ });
5766
+ sectionContentCollection.add({
5767
+ key: "DefaultFrameStats",
5768
+ section: "Frame Steps Duration",
5769
+ order: 2,
5770
+ component: FrameStepsStats,
5771
+ });
5772
+ sectionContentCollection.add({
5773
+ key: "DefaultSystemStats",
5774
+ section: "System Info",
5775
+ order: 3,
5776
+ component: SystemStats,
5777
+ });
5778
+ return {
5779
+ addSection: (section) => sectionsCollection.add(section),
5780
+ addSectionContent: (content) => sectionContentCollection.add(content),
5781
+ dispose: () => registration.dispose(),
5782
+ };
5783
+ },
5784
+ };
5785
+
5786
+ const ToolsPane = (props) => {
5787
+ return jsx(ExtensibleAccordion, { ...props });
5788
+ };
5789
+
5790
+ const ToolsServiceIdentity = Symbol("ToolsService");
5791
+ /**
5792
+ * A collection of usually optional, dynamic extensions.
5793
+ * Common examples includes importing/exporting, or other general creation tools.
5794
+ */
5795
+ const ToolsServiceDefinition = {
5796
+ friendlyName: "Tools Editor",
5797
+ produces: [ToolsServiceIdentity],
5798
+ consumes: [ShellServiceIdentity, SceneContextIdentity],
5799
+ factory: (shellService, sceneContext) => {
5800
+ const sectionsCollection = new ObservableCollection();
5801
+ const sectionContentCollection = new ObservableCollection();
5802
+ // Only show the Tools pane if some tool content has been added.
5803
+ let toolsPaneRegistration = null;
5804
+ sectionContentCollection.observable.add(() => {
5805
+ if (sectionContentCollection.items.length === 0) {
5806
+ toolsPaneRegistration?.dispose();
5807
+ toolsPaneRegistration = null;
5808
+ }
5809
+ else if (!toolsPaneRegistration) {
5810
+ toolsPaneRegistration = shellService.addSidePane({
5811
+ key: "Tools",
5812
+ title: "Tools",
5813
+ icon: WrenchRegular,
5814
+ horizontalLocation: "right",
5815
+ verticalLocation: "top",
5816
+ order: 400,
5817
+ suppressTeachingMoment: true,
5818
+ content: () => {
5819
+ const sections = useOrderedObservableCollection(sectionsCollection);
5820
+ const sectionContent = useObservableCollection(sectionContentCollection);
5821
+ const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
5822
+ return scene && jsx(ToolsPane, { sections: sections, sectionContent: sectionContent, context: scene });
5823
+ },
5824
+ });
5825
+ }
5826
+ });
5827
+ return {
5828
+ addSection: (section) => sectionsCollection.add(section),
5829
+ addSectionContent: (content) => sectionContentCollection.add(content),
5830
+ dispose: () => toolsPaneRegistration?.dispose(),
5831
+ };
5832
+ },
5833
+ };
5834
+
5835
+ const BabylonWebResources = {
5836
+ homepage: "https://www.babylonjs.com",
5837
+ repository: "https://github.com/BabylonJS/Babylon.js",
5838
+ bugs: "https://github.com/BabylonJS/Babylon.js/issues",
5839
+ };
5840
+ /**
5841
+ * Well-known default built in extensions for the Inspector.
5842
+ */
5843
+ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
5844
+ {
5845
+ name: "Quick Creation Tools (Preview)",
5846
+ description: "Adds a new panel for easy creation of various Babylon assets. This is a WIP extension...expect changes!",
5847
+ keywords: ["creation", "tools"],
5848
+ ...BabylonWebResources,
5849
+ author: { name: "Babylon.js", forumUserName: "" },
5850
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-XuuTpQe7.js'),
4455
5851
  },
4456
- content: {
4457
- display: "flex",
4458
- flexDirection: "column",
4459
- gap: tokens.spacingVerticalM,
4460
- padding: tokens.spacingHorizontalL,
4461
- minWidth: "300px",
5852
+ {
5853
+ name: "Reflector",
5854
+ description: "Connects to the Reflector Bridge for real-time scene synchronization with the Babylon.js Sandbox.",
5855
+ keywords: ["reflector", "bridge", "sync", "sandbox", "tools"],
5856
+ ...BabylonWebResources,
5857
+ author: { name: "Babylon.js", forumUserName: "" },
5858
+ getExtensionModuleAsync: async () => await import('./reflectorService-ChA54PIt.js'),
4462
5859
  },
4463
- });
4464
- const Popover = forwardRef((props, ref) => {
4465
- const { children, open: controlledOpen, onOpenChange, positioning, surfaceClassName } = props;
4466
- const [internalOpen, setInternalOpen] = useState(false);
4467
- const classes = useStyles$G();
4468
- const isControlled = controlledOpen !== undefined;
4469
- const popoverOpen = isControlled ? controlledOpen : internalOpen;
4470
- const handleOpenChange = (_, data) => {
4471
- if (!isControlled) {
4472
- setInternalOpen(data.open);
4473
- }
4474
- onOpenChange?.(data.open);
4475
- };
4476
- return (jsxs(Popover$1, { open: popoverOpen, onOpenChange: handleOpenChange, positioning: positioning ?? {
4477
- align: "start",
4478
- overflowBoundary: document.body,
4479
- autoSize: true,
4480
- }, children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: props.trigger ?? jsx(Button, { ref: ref, icon: props.icon, onClick: () => handleOpenChange(null, { open: true }) }) }), jsx(PopoverSurface, { className: surfaceClassName ?? classes.surface, children: jsx("div", { className: classes.content, children: children }) })] }));
4481
- });
4482
- Popover.displayName = "Popover";
5860
+ ]);
4483
5861
 
4484
- const useColorPickerStyles = makeStyles({
4485
- container: {
4486
- width: "350px",
4487
- display: "flex", // becomes a flexbox
4488
- flexDirection: "column", // with children in a column
4489
- alignItems: "center", // centers children horizontally
4490
- justifyContent: "center", // centers children vertically (if height is set)
4491
- gap: tokens.spacingVerticalM,
4492
- overflow: "visible",
4493
- },
4494
- row: {
4495
- flex: 1, // is a row in the container's flex column
4496
- display: "flex", // becomes its own flexbox
4497
- flexDirection: "row", // with children in a row
4498
- gap: tokens.spacingHorizontalXL,
4499
- alignItems: "center", // align items vertically
4500
- width: "100%",
5862
+ const useSyncedSliderStyles = makeStyles({
5863
+ container: { display: "flex", minWidth: 0 },
5864
+ syncedSlider: {
5865
+ flex: "1 1 0",
5866
+ flexDirection: "row",
5867
+ display: "flex",
5868
+ alignItems: "center",
5869
+ minWidth: 0,
4501
5870
  },
4502
- colorPicker: {
4503
- flex: 1,
4504
- width: "350px",
4505
- height: "350px",
5871
+ slider: {
5872
+ flex: "1 1 auto",
5873
+ minWidth: "75px",
5874
+ maxWidth: "75px",
4506
5875
  },
4507
- previewColor: {
4508
- width: "60px",
4509
- height: "60px",
4510
- borderRadius: tokens.borderRadiusMedium, // 4px?
4511
- border: `${tokens.spacingVerticalXXS} solid ${tokens.colorNeutralShadowKeyLighter}`,
4512
- "@media (forced-colors: active)": {
4513
- forcedColorAdjust: "none", // ensures elmement maintains color in high constrast mode
4514
- },
5876
+ compactSlider: {
5877
+ flex: "1 1 auto",
5878
+ minWidth: "50px", // Allow shrinking for compact mode
5879
+ maxWidth: "75px",
4515
5880
  },
4516
- inputRow: {
4517
- display: "flex",
4518
- flexDirection: "row",
4519
- flex: 1, // grow and fill available space
4520
- justifyContent: "center",
4521
- gap: "10px",
4522
- width: "100%",
5881
+ growSlider: {
5882
+ flex: "1 1 auto",
5883
+ minWidth: "50px",
5884
+ // No maxWidth - slider grows to fill available space
4523
5885
  },
4524
- inputField: {
4525
- flex: 1, // grow and fill available space
4526
- width: "auto",
4527
- minWidth: 0,
4528
- gap: tokens.spacingVerticalSNudge, // 6px
5886
+ compactSpinButton: {
5887
+ width: "65px",
5888
+ minWidth: "65px",
5889
+ maxWidth: "65px",
4529
5890
  },
4530
- trigger: {
4531
- display: "flex",
4532
- alignItems: "center",
5891
+ compactSpinButtonInput: {
5892
+ minWidth: "0",
4533
5893
  },
4534
5894
  });
4535
- const ColorPickerPopup = forwardRef((props, ref) => {
4536
- ColorPickerPopup.displayName = "ColorPickerPopup";
4537
- const { value, onChange, isLinearMode, ...rest } = props;
4538
- const classes = useColorPickerStyles();
4539
- const [color, setColor] = useState(value);
4540
- const [isLinear, setIsLinear] = useState(isLinearMode ?? false);
4541
- const [isFloat, setFloat] = useState(false);
5895
+ /**
5896
+ * Component which synchronizes a slider and an input field, allowing the user to change the value using either control
5897
+ * @param props
5898
+ * @returns SyncedSlider component
5899
+ */
5900
+ const SyncedSliderInput = (props) => {
5901
+ SyncedSliderInput.displayName = "SyncedSliderInput";
5902
+ const { infoLabel, ...passthroughProps } = props;
5903
+ const classes = useSyncedSliderStyles();
4542
5904
  const { size } = useContext(ToolContext);
5905
+ const [value, setValue] = useState(props.value ?? 0);
5906
+ const pendingValueRef = useRef(undefined);
5907
+ const isDraggingRef = useRef(false);
5908
+ // NOTE: The Fluent slider will add tick marks if the step prop is anything other than undefined.
5909
+ // To avoid this, we scale the min/max based on the step so we can always make step undefined.
5910
+ // The actual step size in the Fluent slider is 1 when it is ste to undefined.
5911
+ const min = props.min ?? 0;
5912
+ const max = props.max ?? 100;
5913
+ const step = props.step ?? 1;
4543
5914
  useEffect(() => {
4544
- setColor(value); // Ensures the trigger color updates when props.value changes
4545
- }, [value]);
4546
- const handleColorPickerChange = (_, data) => {
4547
- let color = Color3.FromHSV(data.color.h, data.color.s, data.color.v);
4548
- if (value instanceof Color4) {
4549
- color = Color4.FromColor3(color, data.color.a ?? 1);
5915
+ !isDraggingRef.current && setValue(props.value ?? 0); // Update local state when props.value changes as long as user is not actively dragging
5916
+ }, [props.value]);
5917
+ const handleSliderChange = (_, data) => {
5918
+ const newValue = data.value * step;
5919
+ setValue(newValue);
5920
+ if (props.notifyOnlyOnRelease) {
5921
+ // Store the value but don't notify parent yet
5922
+ pendingValueRef.current = newValue;
5923
+ }
5924
+ else {
5925
+ // Notify parent as slider changes
5926
+ props.onChange(newValue);
4550
5927
  }
4551
- handleChange(color);
4552
5928
  };
4553
- const handleChange = (newColor) => {
4554
- setColor(newColor);
4555
- onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
5929
+ const handleSliderPointerDown = () => {
5930
+ isDraggingRef.current = true;
4556
5931
  };
4557
- return (jsx(Popover, { trigger: jsx(ColorSwatch, { className: classes.trigger, ref: ref, ...rest, borderColor: tokens.colorNeutralShadowKeyDarker, size: size === "small" ? "extra-small" : "small", shape: "rounded", color: color.toHexString(), value: color.toHexString().slice(1) }), 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: {
4558
- label: "Color Space",
4559
- info: jsx(Body1, { children: "Today this is not mutable as the color space is determined by the entity. Soon we will allow swapping" }),
4560
- }, options: [
4561
- { label: "Gamma", value: 0 },
4562
- { label: "Linear", value: 1 },
4563
- ], disabled: true, value: isLinear ? 1 : 0, onChange: (val) => setIsLinear(val === 1) }), jsx(NumberDropdown, { className: classes.inputField, infoLabel: {
4564
- label: "Data Type",
4565
- info: jsx(Body1, { children: "We will introduce this functionality soon!" }),
4566
- }, options: [
4567
- { label: "Int", value: 0 },
4568
- { label: "Float", value: 1 },
4569
- ], 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 }) })] }) }));
4570
- });
4571
- /**
4572
- * Component which displays the passed in color's HEX value, either in linearSpace (if linearHex is true) or in gamma space
4573
- * When the hex color is changed by user, component calculates the new Color3/4 value and calls onChange
4574
- *
4575
- * Component uses the isLinearMode boolean to display an informative label regarding linear / gamma space
4576
- * @param props - The properties for the InputHexField component.
4577
- * @returns
4578
- */
4579
- const InputHexField = (props) => {
4580
- const classes = useColorPickerStyles();
4581
- const { title, value, onChange, linearHex, isLinearMode } = props;
4582
- 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
4583
- ? {
4584
- label: title,
4585
- // If not representing a linearHex, no info is needed.
4586
- info: !props.linearHex ? undefined : !isLinearMode ? ( // If representing a linear hex but we are in gammaMode, simple message explaining why linearHex is disabled
4587
- 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 " })) : (
4588
- // If representing a linear hex and we are in linearMode, give information about how to use these hex values
4589
- 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!" })] })),
4590
- }
4591
- : undefined }));
4592
- };
4593
- const InputRgbField = (props) => {
4594
- const { value, onChange, title, rgbKey } = props;
4595
- const classes = useColorPickerStyles();
4596
- const handleChange = useCallback((val) => {
4597
- const newColor = value.clone();
4598
- newColor[rgbKey] = val / 255.0; // Convert to 0-1 range
4599
- onChange(newColor);
4600
- }, [value, onChange, rgbKey]);
4601
- 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 }));
4602
- };
4603
- function rgbaToHsv(color) {
4604
- const c = new Color3(color.r, color.g, color.b);
4605
- const hsv = c.toHSV();
4606
- return { h: hsv.r, s: hsv.g, v: hsv.b, a: color.a };
4607
- }
4608
- /**
4609
- * 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.
4610
- * 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.
4611
- * Value (V) ranges from 0 to 100%, representing the brightness of the color, with 0 being black and 100 being the brightest.
4612
- * @param props - The properties for the InputHsvField component.
4613
- */
4614
- const InputHsvField = (props) => {
4615
- const { value, title, hsvKey, max, onChange, scale = 1 } = props;
4616
- const classes = useColorPickerStyles();
4617
- const handleChange = useCallback((val) => {
4618
- // Convert current color to HSV, update the new hsv value, then call onChange prop
4619
- const hsv = rgbaToHsv(value);
4620
- hsv[hsvKey] = val / scale;
4621
- let newColor = Color3.FromHSV(hsv.h, hsv.s, hsv.v);
4622
- if (value instanceof Color4) {
4623
- newColor = Color4.FromColor3(newColor, value.a ?? 1);
4624
- }
4625
- props.onChange(newColor);
4626
- }, [value, onChange, hsvKey, scale]);
4627
- 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 }));
4628
- };
4629
- /**
4630
- * 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).
4631
- * @param props
4632
- * @returns
4633
- */
4634
- const InputAlphaField = (props) => {
4635
- const classes = useColorPickerStyles();
4636
- const { color, onChange } = props;
4637
- const handleChange = useCallback((value) => {
4638
- if (Number.isNaN(value) || value < 0 || value > 1) {
4639
- return;
5932
+ const handleSliderPointerUp = () => {
5933
+ if (props.notifyOnlyOnRelease && isDraggingRef.current && pendingValueRef.current !== undefined) {
5934
+ props.onChange(pendingValueRef.current);
5935
+ pendingValueRef.current = undefined;
4640
5936
  }
4641
- if (color instanceof Color4) {
4642
- const newColor = color.clone();
4643
- newColor.a = value;
4644
- return newColor;
5937
+ isDraggingRef.current = false;
5938
+ };
5939
+ const handleInputChange = (value) => {
5940
+ setValue(value);
5941
+ props.onChange(value); // Input always updates immediately
5942
+ };
5943
+ const hasSlider = props.min !== undefined && props.max !== undefined;
5944
+ // Determine Slider className based on props
5945
+ const getSliderClassName = () => {
5946
+ if (props.growSlider) {
5947
+ return classes.growSlider;
4645
5948
  }
4646
- else {
4647
- return Color4.FromColor3(color, value);
5949
+ if (props.compact) {
5950
+ return classes.compactSlider;
4648
5951
  }
4649
- }, [onChange]);
4650
- 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: {
4651
- label: "Alpha",
4652
- 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,
4653
- } }));
5952
+ return classes.slider;
5953
+ };
5954
+ return (jsxs("div", { className: classes.container, children: [infoLabel && jsx(InfoLabel, { ...infoLabel, htmlFor: "syncedSlider" }), jsxs("div", { id: "syncedSlider", className: classes.syncedSlider, children: [hasSlider && (jsx(Slider, { ...passthroughProps, className: getSliderClassName(), size: size, min: min / step, max: max / step, step: undefined, value: value / step, onChange: handleSliderChange, onPointerDown: handleSliderPointerDown, onPointerUp: handleSliderPointerUp })), jsx(SpinButton, { ...passthroughProps, className: hasSlider || props.compact ? classes.compactSpinButton : undefined, inputClassName: hasSlider || props.compact ? classes.compactSpinButtonInput : undefined, value: value, onChange: handleInputChange, step: props.step })] })] }));
4654
5955
  };
4655
5956
 
5957
+ /**
5958
+ * Renders a simple wrapper around the SyncedSliderInput
5959
+ * @param props
5960
+ * @returns
5961
+ */
5962
+ const SyncedSliderPropertyLine = forwardRef((props, ref) => {
5963
+ SyncedSliderPropertyLine.displayName = "SyncedSliderPropertyLine";
5964
+ const { label, description, ...sliderProps } = props;
5965
+ return (jsx(PropertyLine, { ref: ref, ...props, children: jsx(SyncedSliderInput, { ...sliderProps }) }));
5966
+ });
5967
+
4656
5968
  /**
4657
5969
  * Reusable component which renders a color property line containing a label, colorPicker popout, and expandable RGBA values
4658
5970
  * The expandable RGBA values are synced sliders that allow the user to modify the color's RGBA values directly
@@ -5373,7 +6685,7 @@ function MakeModularTool(options) {
5373
6685
  });
5374
6686
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
5375
6687
  if (extensionFeeds.length > 0) {
5376
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-Dsui74Id.js');
6688
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-Cze-L8Cg.js');
5377
6689
  await serviceContainer.addServiceAsync(ExtensionListServiceDefinition);
5378
6690
  }
5379
6691
  // Register the theme selector service (for selecting the theme) if theming is configured.
@@ -9240,7 +10552,7 @@ const MessageBar = (props) => {
9240
10552
  MessageBar.displayName = "MessageBar";
9241
10553
  const { message, title: header, intent, docLink } = props;
9242
10554
  const classes = useClasses();
9243
- 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" })] }))] }) }) }));
10555
+ return (jsx("div", { className: classes.container, children: jsx(MessageBar$1, { intent: intent, layout: "multiline", children: jsxs(MessageBarBody, { children: [header && jsx(MessageBarTitle, { children: header }), message, docLink && (jsxs(Fragment, { children: [" - ", jsx(Link, { url: docLink, value: "Learn More" })] }))] }) }) }));
9244
10556
  };
9245
10557
 
9246
10558
  const AnimationsProperties = (props) => {
@@ -9759,7 +11071,7 @@ const FrameGraphTaskProperties = (props) => {
9759
11071
  const tasks = frameGraph.tasks;
9760
11072
  return (jsx(Fragment, { children: tasks.length > 0 &&
9761
11073
  tasks.map((task, i) => {
9762
- return (jsx(BoundProperty, { component: SwitchPropertyLine, label: i + 1 + ". " + task.name, target: frameGraph.tasks[i], propertyKey: "disabled", invertedMode: true }, "task" + i));
11074
+ return (jsx(BoundProperty, { component: SwitchPropertyLine, label: i + 1 + ". " + task.name, target: frameGraph.tasks[i], propertyKey: "disabled", convertTo: (v) => !v, convertFrom: (v) => !v }, "task" + i));
9763
11075
  }) }));
9764
11076
  };
9765
11077
  const FrameGraphGeneralProperties = (props) => {
@@ -10194,7 +11506,7 @@ function List(props) {
10194
11506
  const { items, renderItem, onDelete, onAdd, addButtonLabel = "Add new item" } = props;
10195
11507
  const classes = useListStyles();
10196
11508
  const sortedItems = useMemo(() => [...items].sort((a, b) => a.sortBy - b.sortBy), [items]);
10197
- return (jsxs("div", { children: [jsx(ButtonLine, { label: addButtonLabel, icon: AddRegular, onClick: () => props.onAdd() }), jsx("div", { className: classes.list, children: sortedItems.map((item, index) => (jsxs("div", { className: classes.item, children: [jsxs(Body1Strong, { className: classes.itemId, children: ["#", index] }), jsx("div", { className: classes.itemContent, children: renderItem(item, items.indexOf(sortedItems[index])) }), jsxs("div", { className: classes.iconContainer, children: [jsx(CopyRegular, { onClick: () => onAdd(item) }), jsx(DeleteRegular, { onClick: () => onDelete(item, items.indexOf(sortedItems[index])) })] })] }, item.id))) })] }));
11509
+ return (jsxs("div", { children: [onAdd && jsx(ButtonLine, { label: addButtonLabel, icon: AddRegular, onClick: () => onAdd() }), jsx("div", { className: classes.list, children: sortedItems.map((item, index) => (jsxs("div", { className: classes.item, children: [jsxs(Body1Strong, { className: classes.itemId, children: ["#", index] }), jsx("div", { className: classes.itemContent, children: renderItem(item, items.indexOf(sortedItems[index])) }), (onAdd || onDelete) && (jsxs("div", { className: classes.iconContainer, children: [onAdd && jsx(CopyRegular, { onClick: () => onAdd(item) }), onDelete && jsx(DeleteRegular, { onClick: () => onDelete(item, items.indexOf(sortedItems[index])) })] }))] }, item.id))) })] }));
10198
11510
  }
10199
11511
 
10200
11512
  const useGradientStyles = makeStyles({
@@ -10523,6 +11835,30 @@ const OpenPBRMaterialSpecularProperties = (props) => {
10523
11835
  }
10524
11836
  } }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Specular IOR", target: material, propertyKey: "specularIor", min: 1, max: 3, step: 0.01 })] }));
10525
11837
  };
11838
+ const OpenPBRMaterialTransmissionProperties = (props) => {
11839
+ const { material } = props;
11840
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Weight", target: material, propertyKey: "transmissionWeight", min: 0, max: 1, step: 0.01 }), jsx(FileUploadLine, { label: "Transmission Weight", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11841
+ if (files.length > 0) {
11842
+ UpdateTexture(files[0], material, (texture) => (material.transmissionWeightTexture = texture));
11843
+ }
11844
+ } }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Color", target: material, propertyKey: "transmissionColor", isLinearMode: true }), jsx(FileUploadLine, { label: "Transmission Color", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11845
+ if (files.length > 0) {
11846
+ UpdateTexture(files[0], material, (texture) => (material.transmissionColorTexture = texture));
11847
+ }
11848
+ } }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Depth", target: material, propertyKey: "transmissionDepth", min: 0, step: 0.01 }), jsx(FileUploadLine, { label: "Transmission Depth", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11849
+ if (files.length > 0) {
11850
+ UpdateTexture(files[0], material, (texture) => (material.transmissionDepthTexture = texture));
11851
+ }
11852
+ } }), jsx(BoundProperty, { component: Color3PropertyLine, label: "Transmission Scatter", target: material, propertyKey: "transmissionScatter", isLinearMode: true }), jsx(FileUploadLine, { label: "Transmission Scatter", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11853
+ if (files.length > 0) {
11854
+ UpdateTexture(files[0], material, (texture) => (material.transmissionScatterTexture = texture));
11855
+ }
11856
+ } }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Scatter Anisotropy", target: material, propertyKey: "transmissionScatterAnisotropy", min: -1, max: 1, step: 0.01 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Abbe Number", target: material, propertyKey: "transmissionDispersionAbbeNumber", min: 1, max: 100, step: 1 }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Transmission Dispersion Scale", target: material, propertyKey: "transmissionDispersionScale", min: 0, max: 1, step: 0.01 }), jsx(FileUploadLine, { label: "Transmission Dispersion Scale", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11857
+ if (files.length > 0) {
11858
+ UpdateTexture(files[0], material, (texture) => (material.transmissionDispersionScaleTexture = texture));
11859
+ }
11860
+ } })] }));
11861
+ };
10526
11862
  /**
10527
11863
  * Displays the coat layer properties of an OpenPBR material.
10528
11864
  * @param props - The required properties
@@ -10630,6 +11966,10 @@ const OpenPBRMaterialGeometryProperties = (props) => {
10630
11966
  if (files.length > 0) {
10631
11967
  UpdateTexture(files[0], material, (texture) => (material.geometryCoatTangentTexture = texture));
10632
11968
  }
11969
+ } }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Geometry Thickness", target: material, propertyKey: "geometryThickness", min: 0, step: 0.1 }), jsx(FileUploadLine, { label: "Geometry Thickness", accept: ".jpg, .png, .tga, .dds, .env, .exr", onClick: (files) => {
11970
+ if (files.length > 0) {
11971
+ UpdateTexture(files[0], material, (texture) => (material.geometryThicknessTexture = texture));
11972
+ }
10633
11973
  } })] }));
10634
11974
  };
10635
11975
 
@@ -10717,7 +12057,7 @@ function EntitySelector(props) {
10717
12057
  return getEntities()
10718
12058
  .filter((e) => e.uniqueId !== undefined && (!filter || filter(e)))
10719
12059
  .map((entity) => ({
10720
- label: getName(entity).toString(),
12060
+ label: getName(entity)?.toString() || "",
10721
12061
  value: entity.uniqueId.toString(),
10722
12062
  }))
10723
12063
  .sort((a, b) => a.label.localeCompare(b.label));
@@ -10735,9 +12075,9 @@ function EntitySelector(props) {
10735
12075
  return (jsxs("div", { className: classes.linkDiv, children: [jsx(Tooltip, { content: getName(value), relationship: "label", children: jsx(Link, { className: classes.link, value: getName(value), onLink: () => onLink(value) }) }), onChange &&
10736
12076
  (defaultValue !== undefined ? (
10737
12077
  // If the defaultValue is specified, then allow resetting to the default
10738
- jsx(Button, { icon: LinkDismissRegular, onClick: () => onChange(defaultValue) })) : (
12078
+ jsx(Tooltip, { content: "Unlink", relationship: "label", children: jsx(Button, { icon: LinkDismissRegular, onClick: () => onChange(defaultValue) }) })) : (
10739
12079
  // Otherwise, just allow editing to a new value
10740
- jsx(Button, { icon: LinkEditRegular, onClick: () => setIsEditing(true) })))] }));
12080
+ jsx(Tooltip, { content: "Edit Link", relationship: "label", children: jsx(Button, { icon: LinkEditRegular, onClick: () => setIsEditing(true) }) })))] }));
10741
12081
  }
10742
12082
  else {
10743
12083
  // Otherwise, show the ComboBox for selection
@@ -11226,6 +12566,10 @@ const MaterialPropertiesServiceDefinition = {
11226
12566
  section: "Specular",
11227
12567
  component: ({ context }) => jsx(OpenPBRMaterialSpecularProperties, { material: context }),
11228
12568
  },
12569
+ {
12570
+ section: "Transmission",
12571
+ component: ({ context }) => jsx(OpenPBRMaterialTransmissionProperties, { material: context }),
12572
+ },
11229
12573
  {
11230
12574
  section: "Coat",
11231
12575
  component: ({ context }) => jsx(OpenPBRMaterialCoatProperties, { material: context }),
@@ -12696,11 +14040,16 @@ const useAttractorStyles = makeStyles({
12696
14040
  padding: `${tokens.spacingVerticalXS} 0px`,
12697
14041
  borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`,
12698
14042
  },
14043
+ strengthLabel: {
14044
+ flex: 1,
14045
+ display: "flex",
14046
+ alignItems: "center",
14047
+ },
12699
14048
  });
12700
- const CreateImpostor = (id, scene, attractor, initialScale, initialMaterial) => {
14049
+ const CreateImpostor = (id, scene, attractorData, initialScale, initialMaterial) => {
12701
14050
  const impostor = CreateSphere("Attractor impostor #" + id, { diameter: 1 }, scene);
12702
14051
  impostor.scaling.setAll(initialScale);
12703
- impostor.position.copyFrom(attractor.position);
14052
+ impostor.position.copyFrom(attractorData.position);
12704
14053
  impostor.material = initialMaterial;
12705
14054
  impostor.reservedDataStore = { hidden: true };
12706
14055
  return impostor;
@@ -12721,16 +14070,24 @@ async function CreateTextRendererAsync(id, scene, impostor, color) {
12721
14070
  * @returns
12722
14071
  */
12723
14072
  const AttractorComponent = (props) => {
12724
- const { attractor, id, impostorScale, impostorMaterial, impostorColor, scene, onControl, isControlled } = props;
14073
+ const { attractorData, id, impostorScale, impostorMaterial, impostorColor, scene, onControl, isControlled } = props;
12725
14074
  const classes = useAttractorStyles();
12726
- const [shown, setShown] = useState(true);
14075
+ // For read-only attractors (Node particles), start hidden by default
14076
+ const [shown, setShown] = useState(!attractorData.isReadOnly);
12727
14077
  // We only want to recreate the impostor mesh and associated if id, scene, or attractor/impostor changes
12728
- const impostor = useResource(useCallback(() => CreateImpostor(id, scene, attractor, impostorScale, impostorMaterial), [id, scene, attractor]));
14078
+ const impostor = useResource(useCallback(() => CreateImpostor(id, scene, attractorData, impostorScale, impostorMaterial), [id, scene, attractorData]));
12729
14079
  const label = useAsyncResource(useCallback(async () => await CreateTextRendererAsync(id, scene, impostor, impostorColor), [id, scene, impostor]));
14080
+ // Set initial visibility based on whether it should be shown
14081
+ useEffect(() => {
14082
+ impostor.visibility = shown ? 1 : 0;
14083
+ }, [impostor, shown]);
12730
14084
  // If impostor, color, or label change, recreate the observer function so that it isnt hooked to old state
14085
+ // For read-only attractors, don't sync position back (it can't be moved)
12731
14086
  useEffect(() => {
12732
14087
  const onAfterRender = scene.onAfterRenderObservable.add(() => {
12733
- attractor.position.copyFrom(impostor.position);
14088
+ if (!attractorData.isReadOnly) {
14089
+ attractorData.position.copyFrom(impostor.position);
14090
+ }
12734
14091
  if (label) {
12735
14092
  label.color = Color4.FromColor3(impostorColor);
12736
14093
  label.render(scene.getViewMatrix(), scene.getProjectionMatrix());
@@ -12739,15 +14096,15 @@ const AttractorComponent = (props) => {
12739
14096
  return () => {
12740
14097
  onAfterRender.remove();
12741
14098
  };
12742
- }, [impostor, scene, label, impostorColor]);
14099
+ }, [impostor, scene, label, impostorColor, attractorData]);
12743
14100
  // If impostor or impostorScale change, update impostor scaling
12744
14101
  useEffect(() => {
12745
14102
  impostor.scaling.setAll(impostorScale);
12746
14103
  }, [impostor, impostorScale]);
12747
- return (jsxs("div", { className: classes.container, children: [jsx(SyncedSliderInput, { value: attractor.strength, onChange: (value) => (attractor.strength = value), min: -10, max: 10, step: 0.1 }), jsx(ToggleButton, { title: "Show / hide particle attractor.", checkedIcon: EyeFilled, uncheckedIcon: EyeOffFilled, value: shown, onChange: (show) => {
12748
- show ? (impostor.visibility = 1) : (impostor.visibility = 0);
14104
+ return (jsxs("div", { className: classes.container, children: [attractorData.isReadOnly ? (jsxs(Body1, { className: classes.strengthLabel, children: ["Strength: ", attractorData.strength !== null ? attractorData.strength : "Dynamic"] })) : (jsx(SyncedSliderInput, { value: attractorData.strength, onChange: (value) => attractorData.setStrength(value), min: -10, max: 10, step: 0.1 })), jsx(ToggleButton, { title: "Show / hide particle attractor.", checkedIcon: EyeFilled, uncheckedIcon: EyeOffFilled, value: shown, onChange: (show) => {
14105
+ impostor.visibility = show ? 1 : 0;
12749
14106
  setShown(show);
12750
- } }), jsx(ToggleButton, { title: "Add / remove position gizmo from particle attractor", checkedIcon: ArrowMoveFilled, value: isControlled(impostor), onChange: (control) => onControl(control ? impostor : undefined) })] }));
14107
+ } }), !attractorData.isReadOnly && (jsx(ToggleButton, { title: "Add / remove position gizmo from particle attractor", checkedIcon: ArrowMoveFilled, value: isControlled(impostor), onChange: (control) => onControl(control ? impostor : undefined) }))] }));
12751
14108
  };
12752
14109
 
12753
14110
  const useStyles$b = makeStyles({
@@ -12755,15 +14112,15 @@ const useStyles$b = makeStyles({
12755
14112
  marginTop: tokens.spacingVerticalM,
12756
14113
  },
12757
14114
  });
12758
- // For each Attractor, create a listItem consisting of the attractor and its debugging impostor mesh
14115
+ // For each IAttractorData, create a listItem
12759
14116
  function AttractorsToListItems(attractors) {
12760
- return (attractors?.map((attractor, index) => {
14117
+ return attractors.map((attractor, index) => {
12761
14118
  return {
12762
14119
  id: index,
12763
14120
  data: attractor,
12764
14121
  sortBy: 0,
12765
14122
  };
12766
- }) ?? []);
14123
+ });
12767
14124
  }
12768
14125
  const CreateGizmoManager = (scene) => {
12769
14126
  const gizmoManager = new GizmoManager(scene, 1, UtilityLayerRenderer._CreateDefaultUtilityLayerFromScene(scene), UtilityLayerRenderer._CreateDefaultKeepUtilityLayerFromScene(scene));
@@ -12777,8 +14134,14 @@ const CreateSharedMaterial = (scene, impostorColor) => {
12777
14134
  material.reservedDataStore = { hidden: true }; // Ensure scene explorer doesn't show the material
12778
14135
  return material;
12779
14136
  };
14137
+ /**
14138
+ * Component that displays a list of attractors with debug visualization and editing controls.
14139
+ * Supports both CPU particle systems (editable) and Node particle systems (read-only).
14140
+ * @param props The component props containing the scene and attractor source.
14141
+ * @returns The rendered AttractorList component.
14142
+ */
12780
14143
  const AttractorList = (props) => {
12781
- const { scene, system } = props;
14144
+ const { scene, attractorSource } = props;
12782
14145
  const [items, setItems] = useState([]);
12783
14146
  // All impostors share a scale and material/color (for now!)
12784
14147
  const [impostorScale, setImpostorScale] = useState(1);
@@ -12789,8 +14152,8 @@ const AttractorList = (props) => {
12789
14152
  const [controlledImpostor, setControlledImpostor] = useState(null);
12790
14153
  // If attractors change, recreate the items to re-render attractor components
12791
14154
  useEffect(() => {
12792
- setItems(AttractorsToListItems(props.attractors));
12793
- }, [props.attractors]);
14155
+ setItems(AttractorsToListItems(attractorSource.attractors));
14156
+ }, [attractorSource.attractors]);
12794
14157
  // If color changes, update shared material to ensure children reflect new color
12795
14158
  useEffect(() => {
12796
14159
  impostorMaterial.diffuseColor = impostorColor;
@@ -12802,24 +14165,113 @@ const AttractorList = (props) => {
12802
14165
  setControlledImpostor(attached);
12803
14166
  };
12804
14167
  const classes = useStyles$b();
12805
- return (jsxs(Fragment, { children: [items.length > 0 && (jsxs(Fragment, { children: [jsx(Color3PropertyLine, { label: "Attractor Debug Color", value: impostorColor, onChange: setImpostorColor }), jsx(SyncedSliderPropertyLine, { label: "Attractor Debug Size", value: impostorScale, onChange: setImpostorScale, min: 0, max: 10, step: 0.1 }), jsx(Subtitle2, { className: classes.subsection, children: "Attractors list" })] })), jsx(List, { addButtonLabel: `Add New Attractor`, items: items, onDelete: (item, _index) => system.removeAttractor(item.data), onAdd: (item) => system.addAttractor(item?.data ?? new Attractor()), renderItem: (item) => {
12806
- return (jsx(AttractorComponent, { attractor: item.data, id: item.id, scene: scene, impostorColor: impostorColor, impostorScale: impostorScale, impostorMaterial: impostorMaterial, isControlled: (impostor) => impostor === controlledImpostor, onControl: onControlImpostor }));
14168
+ return (jsxs(Fragment, { children: [items.length > 0 && (jsxs(Fragment, { children: [jsx(Color3PropertyLine, { label: "Attractor Debug Color", value: impostorColor, onChange: setImpostorColor }), jsx(SyncedSliderPropertyLine, { label: "Attractor Debug Size", value: impostorScale, onChange: setImpostorScale, min: 0, max: 10, step: 0.1 }), jsx(Subtitle2, { className: classes.subsection, children: "Attractors list" })] })), jsx(List, { addButtonLabel: `Add New Attractor`, items: items, onDelete: attractorSource.removeAttractor
14169
+ ? (item, _index) => {
14170
+ // Only CPU attractors (Attractor instances) can be removed
14171
+ if (item.data.source instanceof Attractor) {
14172
+ attractorSource.removeAttractor(item.data.source);
14173
+ }
14174
+ }
14175
+ : undefined, onAdd: attractorSource.addAttractor
14176
+ ? (item) => {
14177
+ // Only CPU attractors can be added
14178
+ if (!item || item.data.source instanceof Attractor) {
14179
+ attractorSource.addAttractor(item?.data.source instanceof Attractor ? item.data.source : new Attractor());
14180
+ }
14181
+ }
14182
+ : undefined, renderItem: (item) => {
14183
+ return (jsx(AttractorComponent, { attractorData: item.data, id: item.id, scene: scene, impostorColor: impostorColor, impostorScale: impostorScale, impostorMaterial: impostorMaterial, isControlled: (impostor) => impostor === controlledImpostor, onControl: onControlImpostor }));
12807
14184
  } })] }));
12808
14185
  };
12809
14186
 
14187
+ /**
14188
+ * Creates an IAttractorData adapter from a CPU particle system Attractor.
14189
+ * @param attractor The CPU particle system attractor
14190
+ * @returns The IAttractorData adapter
14191
+ */
14192
+ function CreateCPUAttractorData(attractor) {
14193
+ return {
14194
+ position: attractor.position,
14195
+ strength: attractor.strength,
14196
+ setStrength: (value) => {
14197
+ attractor.strength = value;
14198
+ },
14199
+ source: attractor,
14200
+ isReadOnly: false,
14201
+ };
14202
+ }
14203
+ /**
14204
+ * Creates an IAttractorData adapter from a Node particle system UpdateAttractorBlock.
14205
+ * @param block The UpdateAttractorBlock from a Node particle system
14206
+ * @returns The IAttractorData adapter
14207
+ */
14208
+ function CreateNodeAttractorData(block) {
14209
+ // Get the connected blocks - only use values if they are InputBlocks (constant values)
14210
+ // If the value is a dynamic calculation, fall back to the block's default values
14211
+ const attractorConnected = block.attractor.connectedPoint?.ownerBlock;
14212
+ const strengthConnected = block.strength.connectedPoint?.ownerBlock;
14213
+ const attractorInput = attractorConnected instanceof ParticleInputBlock ? attractorConnected : undefined;
14214
+ const strengthInput = strengthConnected instanceof ParticleInputBlock ? strengthConnected : undefined;
14215
+ // Use InputBlock values if available, null if dynamic, otherwise use the block's default values
14216
+ const position = attractorInput?.value ?? block.attractor.value;
14217
+ const strength = strengthInput?.value ?? (strengthConnected ? null : block.strength.value);
14218
+ return {
14219
+ position: position,
14220
+ strength: strength,
14221
+ setStrength: (value) => {
14222
+ if (strengthInput) {
14223
+ strengthInput.value = value;
14224
+ }
14225
+ },
14226
+ source: block,
14227
+ isReadOnly: true,
14228
+ };
14229
+ }
14230
+ /**
14231
+ * Creates an IAttractorSource for a CPU particle system.
14232
+ * @param system The CPU particle system
14233
+ * @param attractors The current attractors array (from useObservableArray hook)
14234
+ * @returns The IAttractorSource adapter
14235
+ */
14236
+ function CreateCPUAttractorSource(system, attractors) {
14237
+ return {
14238
+ attractors: attractors.map(CreateCPUAttractorData),
14239
+ addAttractor: (attractor) => system.addAttractor(attractor ?? new Attractor()),
14240
+ removeAttractor: (attractor) => system.removeAttractor(attractor),
14241
+ };
14242
+ }
14243
+ /**
14244
+ * Creates an IAttractorSource for a Node particle system.
14245
+ * @param nodeSet The NodeParticleSystemSet
14246
+ * @returns The IAttractorSource adapter
14247
+ */
14248
+ function CreateNodeAttractorSource(nodeSet) {
14249
+ const attractorBlocks = nodeSet.attachedBlocks.filter((block) => block instanceof UpdateAttractorBlock);
14250
+ return {
14251
+ attractors: attractorBlocks.map(CreateNodeAttractorData),
14252
+ };
14253
+ }
14254
+
12810
14255
  /**
12811
14256
  * Display attractor-related properties for a particle system.
14257
+ * Supports both CPU particle systems (editable) and Node particle systems (read-only).
12812
14258
  * @param props Component props.
12813
14259
  * @returns Render property lines.
12814
14260
  */
12815
14261
  const ParticleSystemAttractorProperties = (props) => {
12816
14262
  const { particleSystem: system } = props;
12817
- const attractorsGetter = useCallback(() => system.attractors ?? [], [system]);
12818
- const attractors = useObservableArray(system, attractorsGetter, "addAttractor", "removeAttractor");
12819
14263
  const scene = system.getScene();
12820
- return (jsx(Fragment, { children: scene ? (jsx(AttractorList, { attractors: attractors, scene: scene, system: system })) : (
12821
- // Handle missing scene defensively.
12822
- jsx(MessageBar, { intent: "info", title: "No Scene Available", message: "Cannot display attractors without a scene" })) }));
14264
+ const isNodeGenerated = system.isNodeGenerated;
14265
+ // For non-node systems - Hook is called but inactive for Node systems
14266
+ const attractorsGetter = useCallback(() => system.attractors ?? [], [system]);
14267
+ const attractors = useObservableArray(isNodeGenerated ? null : system, attractorsGetter, "addAttractor", "removeAttractor");
14268
+ // Create appropriate source based on the particle system type
14269
+ const attractorSource = isNodeGenerated ? CreateNodeAttractorSource(system.source) : CreateCPUAttractorSource(system, attractors);
14270
+ // Show message for Node systems with no attractors
14271
+ if (isNodeGenerated && attractorSource.attractors.length === 0) {
14272
+ return jsx(MessageBar, { intent: "info", title: "No Attractors", message: "No attractor blocks found. Add them in the Node Particle Editor." });
14273
+ }
14274
+ return (jsx(Fragment, { children: scene ? (jsx(AttractorList, { attractorSource: attractorSource, scene: scene })) : (jsx(MessageBar, { intent: "info", title: "No Scene Available", message: "Cannot display attractors without a scene" })) }));
12823
14275
  };
12824
14276
 
12825
14277
  const useStyles$a = makeStyles({
@@ -12900,8 +14352,8 @@ function IsParticleSystem(entity) {
12900
14352
  function IsNodeParticleSystem(entity) {
12901
14353
  return entity instanceof ParticleSystem && entity.isNodeGenerated;
12902
14354
  }
12903
- function IsNonNodeCPUParticleSystem(entity) {
12904
- return entity instanceof ParticleSystem && !entity.isNodeGenerated;
14355
+ function IsCPUParticleSystem(entity) {
14356
+ return entity instanceof ParticleSystem;
12905
14357
  }
12906
14358
  function IsNonNodeParticleSystem(entity) {
12907
14359
  return (entity instanceof ParticleSystem && !entity.isNodeGenerated) || entity instanceof GPUParticleSystem;
@@ -12939,7 +14391,7 @@ const ParticleSystemPropertiesServiceDefinition = {
12939
14391
  // The Attractors section must not be visible at all (including the accordion entry) for CPU systems.
12940
14392
  const particleSystemAttractorsContent = propertiesService.addSectionContent({
12941
14393
  key: "Particle System Attractors Properties",
12942
- predicate: IsNonNodeCPUParticleSystem,
14394
+ predicate: IsCPUParticleSystem,
12943
14395
  content: [
12944
14396
  {
12945
14397
  section: "Attractors",
@@ -12948,17 +14400,6 @@ const ParticleSystemPropertiesServiceDefinition = {
12948
14400
  },
12949
14401
  ],
12950
14402
  });
12951
- const particleSystemNodeContent = propertiesService.addSectionContent({
12952
- key: "Particle System Node Properties",
12953
- predicate: IsNodeParticleSystem,
12954
- content: [
12955
- {
12956
- section: "Inputs",
12957
- order: 3,
12958
- component: ({ context }) => jsx(ParticleSystemNodeEditorProperties, { particleSystem: context, selectionService: selectionService }),
12959
- },
12960
- ],
12961
- });
12962
14403
  const particleSystemEmitterContent = propertiesService.addSectionContent({
12963
14404
  key: "Particle System Emitter Properties",
12964
14405
  predicate: IsNonNodeParticleSystem,
@@ -13041,12 +14482,22 @@ const ParticleSystemPropertiesServiceDefinition = {
13041
14482
  },
13042
14483
  ],
13043
14484
  });
14485
+ const particleSystemNodeContent = propertiesService.addSectionContent({
14486
+ key: "Node Particle System Inputs Properties",
14487
+ predicate: IsNodeParticleSystem,
14488
+ content: [
14489
+ {
14490
+ section: "Inputs",
14491
+ order: 12,
14492
+ component: ({ context }) => jsx(ParticleSystemNodeEditorProperties, { particleSystem: context, selectionService: selectionService }),
14493
+ },
14494
+ ],
14495
+ });
13044
14496
  return {
13045
14497
  dispose: () => {
13046
14498
  particleSystemSystemContent.dispose();
13047
14499
  particleSystemCommandsContent.dispose();
13048
14500
  particleSystemAttractorsContent.dispose();
13049
- particleSystemNodeContent.dispose();
13050
14501
  particleSystemEmitterContent.dispose();
13051
14502
  particleSystemEmissionContent.dispose();
13052
14503
  particleSystemSizeContent.dispose();
@@ -13054,6 +14505,7 @@ const ParticleSystemPropertiesServiceDefinition = {
13054
14505
  particleSystemColorContent.dispose();
13055
14506
  particleSystemRotationContent.dispose();
13056
14507
  particleSystemSpritesheetContent.dispose();
14508
+ particleSystemNodeContent.dispose();
13057
14509
  },
13058
14510
  };
13059
14511
  },
@@ -17082,6 +18534,529 @@ const TextureExplorerServiceDefinition = {
17082
18534
  },
17083
18535
  };
17084
18536
 
18537
+ const EquirectangularCaptureTool = ({ scene }) => {
18538
+ const captureEquirectangularAsync = useCallback(async () => {
18539
+ const currentActiveCamera = scene.activeCamera;
18540
+ if (!currentActiveCamera && scene.frameGraph) {
18541
+ scene.activeCamera = FrameGraphUtils.FindMainCamera(scene.frameGraph);
18542
+ }
18543
+ if (scene.activeCamera) {
18544
+ await captureEquirectangularFromScene(scene, { size: 1024, filename: "equirectangular_capture.png" });
18545
+ }
18546
+ // eslint-disable-next-line require-atomic-updates
18547
+ scene.activeCamera = currentActiveCamera;
18548
+ }, [scene]);
18549
+ return jsx(ButtonLine, { label: "Capture Equirectangular", icon: CameraRegular, onClick: captureEquirectangularAsync });
18550
+ };
18551
+
18552
+ const GIFCaptureTool = MakeLazyComponent(async () => {
18553
+ const gif = (await import('./gif-8Ty35Toc.js')).default;
18554
+ // TODO: Figure out how to grab this from NPM package instead of CDN
18555
+ const workerContent = await Tools.LoadFileAsync("https://cdn.jsdelivr.net/gh//terikon/gif.js.optimized@0.1.6/dist/gif.worker.js");
18556
+ const workerBlob = new Blob([workerContent], { type: "application/javascript" });
18557
+ const workerUrl = URL.createObjectURL(workerBlob);
18558
+ return ({ scene }) => {
18559
+ const [recordingSession, setRecordingSession] = useState({ state: "Idle" });
18560
+ const [targetWidth, setTargetWidth] = useState(512);
18561
+ const [frequency, setFrequency] = useState(200);
18562
+ useEffect(() => {
18563
+ return () => {
18564
+ if (recordingSession.state === "Recording") {
18565
+ // Reset session resources if component is unmounted
18566
+ scene.onAfterRenderObservable.remove(recordingSession.captureObserver);
18567
+ scene.getEngine().setHardwareScalingLevel(recordingSession.previousHardwareScaling);
18568
+ }
18569
+ };
18570
+ }, [recordingSession, scene]);
18571
+ // Use functional setState to guard against multiple rapid clicks
18572
+ const startRecording = useCallback(() => {
18573
+ setRecordingSession((currentSession) => {
18574
+ // If already recording/rendering, don't start a new session
18575
+ if (currentSession.state !== "Idle") {
18576
+ return currentSession;
18577
+ }
18578
+ const engine = scene.getEngine();
18579
+ const canvas = engine.getRenderingCanvas();
18580
+ if (!canvas) {
18581
+ return currentSession;
18582
+ }
18583
+ const gifInstance = new gif({
18584
+ workers: 2,
18585
+ quality: 10,
18586
+ workerScript: workerUrl,
18587
+ });
18588
+ // Adjust hardware scaling to match desired width
18589
+ const previousHardwareScaling = engine.getHardwareScalingLevel();
18590
+ engine.setHardwareScalingLevel(engine.getRenderWidth() / (targetWidth * globalThis.devicePixelRatio) || 1);
18591
+ // Capture frames after each render
18592
+ let lastCaptureTime = 0;
18593
+ const captureObserver = scene.onAfterRenderObservable.add(() => {
18594
+ const now = Date.now();
18595
+ if (now - lastCaptureTime >= frequency && gifInstance) {
18596
+ lastCaptureTime = now;
18597
+ gifInstance.addFrame(canvas, { delay: 1, copy: true });
18598
+ }
18599
+ });
18600
+ return {
18601
+ state: "Recording",
18602
+ gif: gifInstance,
18603
+ captureObserver: captureObserver,
18604
+ previousHardwareScaling: previousHardwareScaling,
18605
+ };
18606
+ });
18607
+ }, [scene, targetWidth, frequency]);
18608
+ const stopRecording = useCallback(() => {
18609
+ setRecordingSession((currentSession) => {
18610
+ if (currentSession.state !== "Recording") {
18611
+ return currentSession;
18612
+ }
18613
+ // Remove the frame capture observer
18614
+ scene.onAfterRenderObservable.remove(currentSession.captureObserver);
18615
+ // Restore previous hardware scaling
18616
+ scene.getEngine().setHardwareScalingLevel(currentSession.previousHardwareScaling);
18617
+ currentSession.gif.on("finished", (blob) => {
18618
+ // Download the rendered GIF
18619
+ Tools.Download(blob, "recording.gif");
18620
+ // Reset state
18621
+ setRecordingSession({ state: "Idle" });
18622
+ });
18623
+ // Start rendering the GIF
18624
+ currentSession.gif.render();
18625
+ return { state: "Rendering", gif: currentSession.gif };
18626
+ });
18627
+ }, [scene]);
18628
+ return (jsxs(Fragment, { children: [recordingSession.state === "Idle" && jsx(ButtonLine, { label: "Record GIF", icon: RecordRegular, onClick: startRecording }), recordingSession.state === "Recording" && jsx(ButtonLine, { label: "Stop", icon: RecordStopRegular, onClick: stopRecording }), recordingSession.state === "Rendering" && jsx(Label, { children: "Creating the GIF file..." }), jsxs(Collapse, { visible: recordingSession.state === "Idle", children: [jsx(SyncedSliderPropertyLine, { label: "Resolution", description: "The pixel width of the output. The height will be adjusted accordingly to maintain the aspect ratio.", value: targetWidth, onChange: (value) => setTargetWidth(Math.floor(value)), min: 128, max: 2048, step: 128 }), jsx(SyncedSliderPropertyLine, { label: "Frequency (ms)", description: "The time interval in milliseconds between each capture of the scene.", value: frequency, onChange: (value) => setFrequency(Math.floor(value)), min: 50, max: 1000, step: 50 })] })] }));
18629
+ };
18630
+ }, { spinnerSize: "extra-tiny", spinnerLabel: "Loading..." });
18631
+
18632
+ const SceneReplayTool = ({ scene }) => {
18633
+ const [isRecording, setIsRecording] = useState(false);
18634
+ const sceneRecorder = useResource(() => new SceneRecorder());
18635
+ const startRecording = useCallback(() => {
18636
+ sceneRecorder.track(scene);
18637
+ setIsRecording(true);
18638
+ }, [scene]);
18639
+ const exportReplay = useCallback(() => {
18640
+ const content = JSON.stringify(sceneRecorder.getDelta());
18641
+ const blob = new Blob([content], { type: "application/json" });
18642
+ Tools.Download(blob, "replay_delta.json");
18643
+ setIsRecording(false);
18644
+ }, []);
18645
+ const applyDelta = useCallback((files) => {
18646
+ const file = files[0];
18647
+ if (!file) {
18648
+ return;
18649
+ }
18650
+ Tools.ReadFile(file, (data) => {
18651
+ try {
18652
+ const json = JSON.parse(data);
18653
+ SceneRecorder.ApplyDelta(json, scene);
18654
+ }
18655
+ catch (error) {
18656
+ Logger.Error("Failed to apply replay delta:" + error);
18657
+ }
18658
+ }, undefined, false);
18659
+ }, [scene]);
18660
+ return (jsxs(Fragment, { children: [!isRecording && jsx(ButtonLine, { label: "Start Recording", icon: RecordRegular, onClick: startRecording }), isRecording && (jsxs(Fragment, { children: [jsx(Label, { children: "Recording in progress..." }), jsx(ButtonLine, { label: "Generate Delta File", icon: SaveRegular, onClick: exportReplay })] })), jsx(FileUploadLine, { label: "Apply Delta File", icon: ArrowDownloadRegular, onClick: applyDelta, accept: ".json" })] }));
18661
+ };
18662
+
18663
+ const ScreenshotTool = ({ scene }) => {
18664
+ const [precision, setPrecision] = useState(1);
18665
+ const [useCustomSize, setUseCustomSize] = useState(false);
18666
+ const [width, setWidth] = useState(512);
18667
+ const [height, setHeight] = useState(512);
18668
+ const captureScreenshot = useCallback(async () => {
18669
+ const engine = scene.getEngine();
18670
+ const camera = scene.frameGraph ? FrameGraphUtils.FindMainCamera(scene.frameGraph) : scene.activeCamera;
18671
+ const screenshotSize = useCustomSize ? { width, height, precision } : { precision };
18672
+ if (camera) {
18673
+ await CreateScreenshotUsingRenderTargetAsync(engine, camera, screenshotSize, "image/png", undefined, undefined, "screenshot.png");
18674
+ }
18675
+ }, [useCustomSize, precision, width, height, scene]);
18676
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Capture Screenshot", icon: CameraRegular, onClick: captureScreenshot }), jsx(SyncedSliderPropertyLine, { label: "Precision", description: "A multiplier allowing capture at a higher or lower resolution.", value: precision, onChange: setPrecision, min: 0.1, max: 10, step: 0.1 }), jsx(SwitchPropertyLine, { label: "Use Custom Size", value: useCustomSize, onChange: setUseCustomSize }), jsxs(Collapse, { visible: useCustomSize, children: [jsx(SyncedSliderPropertyLine, { label: "Width", description: "The width of the screenshot in pixels. ", value: width, onChange: setWidth, min: 1, step: 1 }), jsx(SyncedSliderPropertyLine, { label: "Height", description: "The height of the screenshot in pixels.", value: height, onChange: setHeight, min: 1, step: 1 })] })] }));
18677
+ };
18678
+
18679
+ const VideoCaptureTool = ({ scene }) => {
18680
+ const [isRecording, setIsRecording] = useState(false);
18681
+ const videoRecorder = useResource(useCallback(() => {
18682
+ return new VideoRecorder(scene.getEngine());
18683
+ }, [scene.getEngine()]));
18684
+ const recordVideoAsync = useCallback(async () => {
18685
+ if (videoRecorder && videoRecorder.isRecording) {
18686
+ videoRecorder.stopRecording();
18687
+ setIsRecording(false);
18688
+ return;
18689
+ }
18690
+ void videoRecorder.startRecording(undefined, 0); // Use 0 to prevent automatic stop; let the user stop it
18691
+ setIsRecording(true);
18692
+ }, [scene]);
18693
+ return (jsx(Fragment, { children: jsx(ButtonLine, { label: isRecording ? "Stop Recording" : "Record Video", icon: isRecording ? RecordStopRegular : RecordRegular, onClick: recordVideoAsync }) }));
18694
+ };
18695
+
18696
+ const CaptureToolsDefinition = {
18697
+ friendlyName: "Capture Tools",
18698
+ consumes: [ToolsServiceIdentity],
18699
+ factory: (toolsService) => {
18700
+ const contentRegistrations = [];
18701
+ // Screenshot
18702
+ contentRegistrations.push(toolsService.addSectionContent({
18703
+ key: "Screenshot",
18704
+ section: "Screenshot",
18705
+ order: 10,
18706
+ component: ({ context }) => jsx(ScreenshotTool, { scene: context }),
18707
+ }));
18708
+ // Equirectangular capture
18709
+ contentRegistrations.push(toolsService.addSectionContent({
18710
+ key: "Equirectangular",
18711
+ section: "Equirectangular",
18712
+ order: 15,
18713
+ component: ({ context }) => jsx(EquirectangularCaptureTool, { scene: context }),
18714
+ }));
18715
+ // Video recorder
18716
+ contentRegistrations.push(toolsService.addSectionContent({
18717
+ key: "Video",
18718
+ section: "Video",
18719
+ order: 20,
18720
+ component: ({ context }) => jsx(VideoCaptureTool, { scene: context }),
18721
+ }));
18722
+ // GIF recorder
18723
+ contentRegistrations.push(toolsService.addSectionContent({
18724
+ key: "GIF",
18725
+ section: "GIF",
18726
+ order: 25,
18727
+ component: ({ context }) => jsx(GIFCaptureTool, { scene: context }),
18728
+ }));
18729
+ // Scene replay
18730
+ contentRegistrations.push(toolsService.addSectionContent({
18731
+ key: "Scene Replay",
18732
+ section: "Scene Replay",
18733
+ order: 30,
18734
+ component: ({ context }) => jsx(SceneReplayTool, { scene: context }),
18735
+ }));
18736
+ return {
18737
+ dispose: () => {
18738
+ contentRegistrations.forEach((registration) => registration.dispose());
18739
+ },
18740
+ };
18741
+ },
18742
+ };
18743
+
18744
+ const EnvExportImageTypes = [
18745
+ { label: "PNG", value: 0, imageType: "image/png" },
18746
+ { label: "WebP", value: 1, imageType: "image/webp" },
18747
+ ];
18748
+ const ExportBabylonTools = ({ scene }) => {
18749
+ const [babylonExportOptions, setBabylonExportOptions] = useState({
18750
+ imageTypeIndex: 0,
18751
+ imageQuality: 0.8,
18752
+ iblDiffuse: false,
18753
+ });
18754
+ const exportBabylon = useCallback(async () => {
18755
+ const strScene = JSON.stringify(SceneSerializer.Serialize(scene));
18756
+ const blob = new Blob([strScene], { type: "octet/stream" });
18757
+ Tools.Download(blob, "scene.babylon");
18758
+ }, [scene]);
18759
+ const createEnvTexture = useCallback(async () => {
18760
+ if (!scene.environmentTexture) {
18761
+ return;
18762
+ }
18763
+ try {
18764
+ const buffer = await EnvironmentTextureTools.CreateEnvTextureAsync(scene.environmentTexture, {
18765
+ imageType: EnvExportImageTypes[babylonExportOptions.imageTypeIndex].imageType,
18766
+ imageQuality: babylonExportOptions.imageQuality,
18767
+ disableIrradianceTexture: !babylonExportOptions.iblDiffuse,
18768
+ });
18769
+ const blob = new Blob([buffer], { type: "octet/stream" });
18770
+ Tools.Download(blob, "environment.env");
18771
+ }
18772
+ catch (error) {
18773
+ Logger.Error(error);
18774
+ alert(error);
18775
+ }
18776
+ }, [scene, babylonExportOptions]);
18777
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to Babylon", icon: ArrowDownloadRegular, onClick: exportBabylon }), !scene.getEngine().premultipliedAlpha && scene.environmentTexture && scene.environmentTexture._prefiltered && scene.activeCamera && (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Generate .env texture", icon: ArrowDownloadRegular, onClick: createEnvTexture }), scene.environmentTexture.irradianceTexture && (jsx(SwitchPropertyLine, { label: "Diffuse Texture", description: "Export diffuse texture for IBL", value: babylonExportOptions.iblDiffuse, onChange: (value) => {
18778
+ setBabylonExportOptions((prev) => ({ ...prev, iblDiffuse: value }));
18779
+ } }, "iblDiffuse")), jsx(NumberDropdownPropertyLine, { label: "Image type", options: EnvExportImageTypes, value: babylonExportOptions.imageTypeIndex, onChange: (val) => {
18780
+ setBabylonExportOptions((prev) => ({ ...prev, imageTypeIndex: val }));
18781
+ } }), jsx(Collapse, { visible: babylonExportOptions.imageTypeIndex > 0, children: jsx(SyncedSliderPropertyLine, { label: "Quality", value: babylonExportOptions.imageQuality, onChange: (value) => setBabylonExportOptions((prev) => ({ ...prev, imageQuality: value })), min: 0, max: 1 }) })] }))] }));
18782
+ };
18783
+ const ExportGltfTools = MakeLazyComponent(async () => {
18784
+ // Defer importing anything from the serializers package until this component is actually mounted.
18785
+ const { GLTF2Export } = await import('@babylonjs/serializers/glTF/2.0/glTFSerializer.js');
18786
+ return (props) => {
18787
+ const [isExportingGltf, setIsExportingGltf] = useState(false);
18788
+ const [gltfExportOptions, setGltfExportOptions] = useState({
18789
+ exportDisabledNodes: false,
18790
+ exportSkyboxes: false,
18791
+ exportCameras: false,
18792
+ exportLights: false,
18793
+ dracoCompression: false,
18794
+ });
18795
+ const exportGLTF = useCallback(async () => {
18796
+ setIsExportingGltf(true);
18797
+ const shouldExport = (node) => {
18798
+ if (!gltfExportOptions.exportDisabledNodes) {
18799
+ if (!node.isEnabled()) {
18800
+ return false;
18801
+ }
18802
+ }
18803
+ if (!gltfExportOptions.exportSkyboxes) {
18804
+ if (node instanceof Mesh) {
18805
+ if (node.material) {
18806
+ const material = node.material;
18807
+ const reflectionTexture = material.reflectionTexture;
18808
+ if (reflectionTexture && reflectionTexture.coordinatesMode === Texture.SKYBOX_MODE) {
18809
+ return false;
18810
+ }
18811
+ }
18812
+ }
18813
+ }
18814
+ if (!gltfExportOptions.exportCameras) {
18815
+ if (node instanceof Camera) {
18816
+ return false;
18817
+ }
18818
+ }
18819
+ if (!gltfExportOptions.exportLights) {
18820
+ if (node instanceof Light) {
18821
+ return false;
18822
+ }
18823
+ }
18824
+ return true;
18825
+ };
18826
+ try {
18827
+ const glb = await GLTF2Export.GLBAsync(props.scene, "scene", {
18828
+ meshCompressionMethod: gltfExportOptions.dracoCompression ? "Draco" : undefined,
18829
+ shouldExportNode: (node) => shouldExport(node),
18830
+ });
18831
+ glb.downloadFiles();
18832
+ }
18833
+ catch (reason) {
18834
+ Logger.Error(`Failed to export GLB: ${reason}`);
18835
+ }
18836
+ finally {
18837
+ setIsExportingGltf(false);
18838
+ }
18839
+ }, [gltfExportOptions, props.scene]);
18840
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Export to GLB", icon: ArrowDownloadRegular, onClick: exportGLTF, disabled: isExportingGltf }), jsx(SwitchPropertyLine, { label: "Export Disabled Nodes", description: "Whether to export nodes that are disabled in the scene.", value: gltfExportOptions.exportDisabledNodes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportDisabledNodes: checked }) }, "GLTFExportDisabledNodes"), jsx(SwitchPropertyLine, { label: "Export Skyboxes", description: "Whether to export skybox nodes in the scene.", value: gltfExportOptions.exportSkyboxes, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportSkyboxes: checked }) }, "GLTFExportSkyboxes"), jsx(SwitchPropertyLine, { label: "Export Cameras", description: "Whether to export cameras in the scene.", value: gltfExportOptions.exportCameras, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportCameras: checked }) }, "GLTFExportCameras"), jsx(SwitchPropertyLine, { label: "Export Lights", description: "Whether to export lights in the scene.", value: gltfExportOptions.exportLights, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, exportLights: checked }) }, "GLTFExportLights"), jsx(SwitchPropertyLine, { label: "Draco Compression", description: "Whether to apply Draco compression to geometry.", value: gltfExportOptions.dracoCompression, onChange: (checked) => setGltfExportOptions({ ...gltfExportOptions, dracoCompression: checked }) }, "GLTFDracoCompression")] }));
18841
+ };
18842
+ });
18843
+
18844
+ const ExportServiceDefinition = {
18845
+ friendlyName: "Export Tools",
18846
+ consumes: [ToolsServiceIdentity],
18847
+ factory: (toolsService) => {
18848
+ const contentRegistrations = [];
18849
+ // glTF export content
18850
+ contentRegistrations.push(toolsService.addSectionContent({
18851
+ key: "GLTF Export",
18852
+ section: "GLTF Export",
18853
+ component: ({ context }) => jsx(ExportGltfTools, { scene: context }),
18854
+ }));
18855
+ // Babylon export content
18856
+ contentRegistrations.push(toolsService.addSectionContent({
18857
+ key: "Babylon Export",
18858
+ section: "Babylon Export",
18859
+ component: ({ context }) => jsx(ExportBabylonTools, { scene: context }),
18860
+ }));
18861
+ return {
18862
+ dispose: () => {
18863
+ contentRegistrations.forEach((registration) => registration.dispose());
18864
+ },
18865
+ };
18866
+ },
18867
+ };
18868
+
18869
+ const AnimationGroupLoadingModes = [
18870
+ { label: "Clean", value: 0 /* SceneLoaderAnimationGroupLoadingMode.Clean */ },
18871
+ { label: "Stop", value: 1 /* SceneLoaderAnimationGroupLoadingMode.Stop */ },
18872
+ { label: "Sync", value: 2 /* SceneLoaderAnimationGroupLoadingMode.Sync */ },
18873
+ { label: "NoSync", value: 3 /* SceneLoaderAnimationGroupLoadingMode.NoSync */ },
18874
+ ];
18875
+ const GLTFAnimationImportTool = ({ scene }) => {
18876
+ const [importDefaults, setImportDefaults] = useState({
18877
+ overwriteAnimations: true,
18878
+ animationGroupLoadingMode: 0 /* SceneLoaderAnimationGroupLoadingMode.Clean */,
18879
+ });
18880
+ const importAnimations = (event) => {
18881
+ const reloadAsync = async function (sceneFile) {
18882
+ if (sceneFile) {
18883
+ try {
18884
+ await ImportAnimationsAsync(sceneFile, scene, {
18885
+ overwriteAnimations: importDefaults.overwriteAnimations,
18886
+ animationGroupLoadingMode: importDefaults.animationGroupLoadingMode,
18887
+ });
18888
+ if (scene.animationGroups.length > 0) {
18889
+ const currentGroup = scene.animationGroups[0];
18890
+ currentGroup.play(true);
18891
+ }
18892
+ }
18893
+ catch (error) {
18894
+ Logger.Error(`Error importing animations: ${error}`);
18895
+ }
18896
+ }
18897
+ };
18898
+ const filesInputAnimation = new FilesInput(scene.getEngine(), scene, null, null, null, null, null, reloadAsync, null);
18899
+ filesInputAnimation.loadFiles(event);
18900
+ filesInputAnimation.dispose();
18901
+ };
18902
+ return (jsxs(Fragment, { children: [jsx(FileUploadLine, { label: "Import Animations", accept: "gltf", onClick: (evt) => importAnimations(evt) }), jsx(SwitchPropertyLine, { label: "Overwrite Animations", value: importDefaults.overwriteAnimations, onChange: (value) => {
18903
+ setImportDefaults({ ...importDefaults, overwriteAnimations: value });
18904
+ } }), jsx(Collapse, { visible: !importDefaults.overwriteAnimations, children: jsx(NumberDropdownPropertyLine, { label: "Animation Merge Mode", options: AnimationGroupLoadingModes, value: importDefaults.animationGroupLoadingMode, onChange: (value) => {
18905
+ setImportDefaults({ ...importDefaults, animationGroupLoadingMode: value });
18906
+ } }) })] }));
18907
+ };
18908
+
18909
+ const GLTFAnimationImportServiceDefinition = {
18910
+ friendlyName: "GLTF Animation Import",
18911
+ consumes: [ToolsServiceIdentity],
18912
+ factory: (toolsService) => {
18913
+ const contentRegistration = toolsService.addSectionContent({
18914
+ key: "AnimationImport",
18915
+ order: 40,
18916
+ section: "GLTF Animation Import",
18917
+ component: ({ context }) => jsx(GLTFAnimationImportTool, { scene: context }),
18918
+ });
18919
+ return {
18920
+ dispose: () => {
18921
+ contentRegistration.dispose();
18922
+ },
18923
+ };
18924
+ },
18925
+ };
18926
+
18927
+ const AnimationStartModeOptions = [
18928
+ { label: "None", value: GLTFLoaderAnimationStartMode.NONE },
18929
+ { label: "First", value: GLTFLoaderAnimationStartMode.FIRST },
18930
+ { label: "All", value: GLTFLoaderAnimationStartMode.ALL },
18931
+ ];
18932
+ const CoordinateSystemModeOptions = [
18933
+ { label: "Auto", value: GLTFLoaderCoordinateSystemMode.AUTO },
18934
+ { label: "Right Handed", value: GLTFLoaderCoordinateSystemMode.FORCE_RIGHT_HANDED },
18935
+ ];
18936
+ const GLTFLoaderOptionsTool = ({ loaderOptions }) => {
18937
+ return (jsx(PropertyLine, { label: "Loader Options", expandByDefault: false, indentExpandedContent: true, expandedContent: jsxs(Fragment, { children: [jsx(BoundProperty, { component: SwitchPropertyLine, label: "Always compute bounding box", target: loaderOptions, propertyKey: "alwaysComputeBoundingBox" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Always compute skeleton root node", target: loaderOptions, propertyKey: "alwaysComputeSkeletonRootNode" }), jsx(BoundProperty, { component: NumberDropdownPropertyLine, label: "Animation start mode", options: AnimationStartModeOptions, target: loaderOptions, propertyKey: "animationStartMode" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Capture performance counters", target: loaderOptions, propertyKey: "capturePerformanceCounters" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Compile materials", target: loaderOptions, propertyKey: "compileMaterials" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Compile shadow generators", target: loaderOptions, propertyKey: "compileShadowGenerators" }), jsx(BoundProperty, { component: NumberDropdownPropertyLine, label: "Coordinate system", options: CoordinateSystemModeOptions, target: loaderOptions, propertyKey: "coordinateSystemMode" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Create instances", target: loaderOptions, propertyKey: "createInstances" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Enable logging", target: loaderOptions, propertyKey: "loggingEnabled" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Load all materials", target: loaderOptions, propertyKey: "loadAllMaterials" }), jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Target FPS", target: loaderOptions, propertyKey: "targetFps", min: 1, max: 120, step: 1 }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Transparency as coverage", target: loaderOptions, propertyKey: "transparencyAsCoverage" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use clip plane", target: loaderOptions, propertyKey: "useClipPlane" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use sRGB buffers", target: loaderOptions, propertyKey: "useSRGBBuffers" })] }) }));
18938
+ };
18939
+ const GLTFExtensionOptionsTool = ({ extensionOptions }) => {
18940
+ return (jsx(PropertyLine, { label: "Extension Options", expandByDefault: false, indentExpandedContent: true, expandedContent: jsx(Fragment, { children: Object.entries(extensionOptions).map(([extensionName, options]) => {
18941
+ return (jsx(BoundProperty, { component: SwitchPropertyLine, label: extensionName, target: options, propertyKey: "enabled", expandedContent: (extensionName === "MSFT_lod" && (jsx(BoundProperty, { component: SyncedSliderPropertyLine, label: "Maximum LODs", target: extensionOptions[extensionName], propertyKey: "maxLODsToLoad", min: 1, max: 10, step: 1 }, extensionName + "_maxLODsToLoad"))) ||
18942
+ undefined }, extensionName));
18943
+ }) }) }));
18944
+ };
18945
+
18946
+ // Options exposed in Inspector includes all the properties from the default loader options (GLTFLoaderDefaultOptions)
18947
+ // plus some options that only exist directly on the GLTFFileLoader class itself.
18948
+ const CurrentLoaderOptions = Object.assign({
18949
+ capturePerformanceCounters: false,
18950
+ loggingEnabled: false,
18951
+ }, GLTFLoaderDefaultOptions);
18952
+ const CurrentExtensionOptions = {
18953
+ /* eslint-disable @typescript-eslint/naming-convention */
18954
+ EXT_lights_image_based: { enabled: true },
18955
+ EXT_mesh_gpu_instancing: { enabled: true },
18956
+ EXT_texture_webp: { enabled: true },
18957
+ EXT_texture_avif: { enabled: true },
18958
+ KHR_draco_mesh_compression: { enabled: true },
18959
+ KHR_materials_pbrSpecularGlossiness: { enabled: true },
18960
+ KHR_materials_clearcoat: { enabled: true },
18961
+ KHR_materials_iridescence: { enabled: true },
18962
+ KHR_materials_anisotropy: { enabled: true },
18963
+ KHR_materials_emissive_strength: { enabled: true },
18964
+ KHR_materials_ior: { enabled: true },
18965
+ KHR_materials_sheen: { enabled: true },
18966
+ KHR_materials_specular: { enabled: true },
18967
+ KHR_materials_unlit: { enabled: true },
18968
+ KHR_materials_variants: { enabled: true },
18969
+ KHR_materials_transmission: { enabled: true },
18970
+ KHR_materials_diffuse_transmission: { enabled: true },
18971
+ KHR_materials_volume: { enabled: true },
18972
+ KHR_materials_dispersion: { enabled: true },
18973
+ KHR_materials_diffuse_roughness: { enabled: true },
18974
+ KHR_mesh_quantization: { enabled: true },
18975
+ KHR_lights_punctual: { enabled: true },
18976
+ EXT_lights_area: { enabled: true },
18977
+ KHR_texture_basisu: { enabled: true },
18978
+ KHR_texture_transform: { enabled: true },
18979
+ KHR_xmp_json_ld: { enabled: true },
18980
+ MSFT_lod: { enabled: true, maxLODsToLoad: 10 },
18981
+ MSFT_minecraftMesh: { enabled: true },
18982
+ MSFT_sRGBFactors: { enabled: true },
18983
+ MSFT_audio_emitter: { enabled: true },
18984
+ };
18985
+ const GLTFLoaderOptionsServiceDefinition = {
18986
+ friendlyName: "GLTF Loader Options",
18987
+ consumes: [ToolsServiceIdentity],
18988
+ factory: (toolsService) => {
18989
+ // Subscribe to plugin activation
18990
+ const pluginObserver = SceneLoader.OnPluginActivatedObservable.add((plugin) => {
18991
+ if (plugin.name === "gltf") {
18992
+ const loader = plugin;
18993
+ // Apply loader settings
18994
+ Object.assign(loader, CurrentLoaderOptions);
18995
+ // Subscribe to extension loading
18996
+ loader.onExtensionLoadedObservable.add((extension) => {
18997
+ const extensionOptions = CurrentExtensionOptions[extension.name];
18998
+ if (extensionOptions) {
18999
+ // Apply extension settings
19000
+ Object.assign(extension, extensionOptions);
19001
+ }
19002
+ });
19003
+ }
19004
+ });
19005
+ const loaderToolsRegistration = toolsService.addSectionContent({
19006
+ key: "GLTFLoaderOptions",
19007
+ section: "GLTF Loader",
19008
+ order: 50,
19009
+ component: () => {
19010
+ return (jsxs(Fragment, { children: [jsx(MessageBar, { intent: "info", message: "Reload the file for changes to take effect" }), jsx(GLTFLoaderOptionsTool, { loaderOptions: CurrentLoaderOptions }), jsx(GLTFExtensionOptionsTool, { extensionOptions: CurrentExtensionOptions })] }));
19011
+ },
19012
+ });
19013
+ return {
19014
+ dispose: () => {
19015
+ pluginObserver.remove();
19016
+ loaderToolsRegistration.dispose();
19017
+ },
19018
+ };
19019
+ },
19020
+ };
19021
+
19022
+ const GLTFValidationTool = ({ validationResults }) => {
19023
+ const childWindow = useRef(null);
19024
+ const issues = validationResults.issues;
19025
+ const hasErrors = issues.numErrors > 0;
19026
+ return (jsxs(Fragment, { children: [jsx(MessageBar, { intent: hasErrors ? "error" : "success", message: hasErrors ? "Your file has validation issues" : "Your file is a valid glTF file" }), jsx(StringifiedPropertyLine, { label: "Errors", value: issues.numErrors }, "NumErrors"), jsx(StringifiedPropertyLine, { label: "Warnings", value: issues.numWarnings }, "NumWarnings"), jsx(StringifiedPropertyLine, { label: "Infos", value: issues.numInfos }, "NumInfos"), jsx(StringifiedPropertyLine, { label: "Hints", value: issues.numHints }, "NumHints"), jsx(ButtonLine, { label: "View Report Details", onClick: () => childWindow.current?.open() }), jsx(ChildWindow, { id: "gltfValidationResults", imperativeRef: childWindow, children: jsx("pre", { style: { margin: 0, overflow: "auto" }, children: jsx("code", { children: JSON.stringify(validationResults, null, 2) }) }) })] }));
19027
+ };
19028
+
19029
+ const GLTFValidationServiceDefinition = {
19030
+ friendlyName: "GLTF Validation",
19031
+ consumes: [ToolsServiceIdentity],
19032
+ factory: (toolsService) => {
19033
+ const pluginObserver = SceneLoader.OnPluginActivatedObservable.add((plugin) => {
19034
+ if (plugin.name === "gltf") {
19035
+ const loader = plugin;
19036
+ loader.validate = true;
19037
+ }
19038
+ });
19039
+ const sectionRegistration = toolsService.addSectionContent({
19040
+ key: "GLTFValidation",
19041
+ section: "GLTF Validation",
19042
+ order: 60,
19043
+ component: () => {
19044
+ const validationState = useProperty(GLTFValidation, "_LastResults");
19045
+ if (!validationState) {
19046
+ return jsx(MessageBar, { intent: "info", message: "Reload the file to see validation results" });
19047
+ }
19048
+ return jsx(GLTFValidationTool, { validationResults: validationState });
19049
+ },
19050
+ });
19051
+ return {
19052
+ dispose: () => {
19053
+ sectionRegistration.dispose();
19054
+ pluginObserver.remove();
19055
+ },
19056
+ };
19057
+ },
19058
+ };
19059
+
17085
19060
  const PickingToolbar = (props) => {
17086
19061
  const { scene, selectEntity, gizmoService, ignoreBackfaces } = props;
17087
19062
  const meshDataCache = useMemo(() => new WeakMap(), [scene]);
@@ -17210,13 +19185,24 @@ function ShowInspector(scene, options = {}) {
17210
19185
  let disposeAsync = async () => await Promise.resolve();
17211
19186
  // Create an inspector dispose token. The dispose will use the same async lock to
17212
19187
  // make sure async dispose (hide) does not actually start until async show is finished.
19188
+ let isDisposed = false;
19189
+ const onDisposed = new Observable();
17213
19190
  const inspectorToken = {
17214
- dispose: () => {
19191
+ dispose() {
17215
19192
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
17216
19193
  InspectorLock.lockAsync(async () => {
17217
19194
  await disposeAsync();
19195
+ isDisposed = true;
19196
+ onDisposed.notifyObservers();
19197
+ onDisposed.clear();
17218
19198
  });
17219
19199
  },
19200
+ get isDisposed() {
19201
+ return isDisposed;
19202
+ },
19203
+ get onDisposed() {
19204
+ return onDisposed;
19205
+ },
17220
19206
  };
17221
19207
  // Track the inspector token for the scene.
17222
19208
  InspectorTokens.set(scene, inspectorToken);
@@ -17303,11 +19289,7 @@ function ShowInspector(scene, options = {}) {
17303
19289
  const canvasContainerDisplay = parentElement.style.display;
17304
19290
  const canvasContainerChildren = [...parentElement.childNodes];
17305
19291
  parentElement.replaceChildren();
17306
- disposeActions.push(async () => {
17307
- // When the ModularTool token is disposed, it unmounts the react element, which asynchronously
17308
- // removes all children from the parentElement. We need to wait for that to complete before
17309
- // re-adding the canvas children back to the parentElement.
17310
- await new Promise((resolve) => setTimeout(resolve));
19292
+ disposeActions.push(() => {
17311
19293
  parentElement.replaceChildren(...canvasContainerChildren);
17312
19294
  });
17313
19295
  // This service is responsible for injecting the passed in canvas as the "central content" of the shell UI (the main area between the side panes and toolbars).
@@ -17369,7 +19351,7 @@ function ShowInspector(scene, options = {}) {
17369
19351
  // Stats pane tab and related services.
17370
19352
  StatsServiceDefinition,
17371
19353
  // Tools pane tab and related services.
17372
- ToolsServiceDefinition,
19354
+ ToolsServiceDefinition, ExportServiceDefinition, GLTFAnimationImportServiceDefinition, GLTFLoaderOptionsServiceDefinition, GLTFValidationServiceDefinition, CaptureToolsDefinition,
17373
19355
  // Settings pane tab and related services.
17374
19356
  SettingsServiceDefinition,
17375
19357
  // Tracks entity selection state (e.g. which Mesh or Material or other entity is currently selected in scene explorer and bound to the properties pane, etc.).
@@ -17878,10 +19860,10 @@ class Inspector {
17878
19860
  options,
17879
19861
  disposeToken: ShowInspector(scene, options),
17880
19862
  };
19863
+ this._CurrentInstance.disposeToken.onDisposed.addOnce(() => (this._CurrentInstance = null));
17881
19864
  }
17882
19865
  static Hide() {
17883
19866
  this._CurrentInstance?.disposeToken.dispose();
17884
- this._CurrentInstance = null;
17885
19867
  }
17886
19868
  // @ts-expect-error TS6133: This is private, but used by debugLayer (same as Inspector v1).
17887
19869
  static _SetNewScene(scene) {
@@ -18202,5 +20184,5 @@ const TextAreaPropertyLine = (props) => {
18202
20184
  // Attach Inspector v2 to Scene.debugLayer as a side effect for back compat.
18203
20185
  AttachDebugLayer();
18204
20186
 
18205
- export { useAsyncResource as $, Accordion as A, ButtonLine as B, Collapse as C, DebugServiceIdentity as D, ExtensibleAccordion as E, FileUploadLine as F, Theme as G, BuiltInsExtensionFeed as H, Inspector as I, useVector3Property as J, useColor3Property as K, Link as L, MakeLazyComponent as M, NumberDropdownPropertyLine as N, useColor4Property as O, Popover as P, useQuaternionProperty as Q, MakePropertyHook as R, SwitchPropertyLine as S, ToolsServiceIdentity as T, useInterceptObservable as U, Vector3PropertyLine as V, useEventfulState as W, useObservableCollection as X, useOrderedObservableCollection as Y, usePollingObservable as Z, useResource as _, SyncedSliderPropertyLine as a, useCompactMode as a0, useSidePaneDockOverrides as a1, useAngleConverters as a2, MakeTeachingMoment as a3, MakeDialogTeachingMoment as a4, InterceptFunction as a5, GetPropertyDescriptor as a6, IsPropertyReadonly as a7, InterceptProperty as a8, ObservableCollection as a9, Textarea as aA, TextInput as aB, ToggleButton as aC, ChildWindow as aD, FactorGradientList as aE, Color3GradientList as aF, Color4GradientList as aG, Pane as aH, BooleanBadgePropertyLine as aI, Color3PropertyLine as aJ, Color4PropertyLine as aK, HexPropertyLine as aL, NumberInputPropertyLine as aM, LinkPropertyLine as aN, PropertyLine as aO, LineContainer as aP, PlaceholderPropertyLine as aQ, StringifiedPropertyLine as aR, TextAreaPropertyLine as aS, TextPropertyLine as aT, RotationVectorPropertyLine as aU, QuaternionPropertyLine as aV, Vector2PropertyLine as aW, Vector4PropertyLine as aX, ConstructorFactory as aa, SelectionServiceIdentity as ab, SelectionServiceDefinition as ac, SettingsContextIdentity as ad, ShowInspector as ae, Checkbox as af, ColorPickerPopup as ag, InputHexField as ah, InputHsvField as ai, ComboBox as aj, DraggableLine as ak, Dropdown as al, NumberDropdown as am, StringDropdown as an, FactorGradientComponent as ao, Color3GradientComponent as ap, Color4GradientComponent as aq, ColorStepGradientComponent as ar, InfoLabel as as, List as at, PositionedPopover as au, SearchBar as av, SearchBox as aw, SpinButton as ax, Switch as ay, SyncedSliderInput as az, Button as b, TextInputPropertyLine as c, SpinButtonPropertyLine as d, CheckboxPropertyLine as e, MessageBar as f, ShellServiceIdentity as g, SceneContextIdentity as h, useObservableState as i, AccordionSection as j, useExtensionManager as k, MakePopoverTeachingMoment as l, TeachingMoment as m, SidePaneContainer as n, PropertiesServiceIdentity as o, SceneExplorerServiceIdentity as p, SettingsServiceIdentity as q, StatsServiceIdentity as r, ConvertOptions as s, AttachDebugLayer as t, useProperty as u, DetachDebugLayer as v, StringDropdownPropertyLine as w, BoundProperty as x, Property as y, LinkToEntityPropertyLine as z };
18206
- //# sourceMappingURL=index-D89pOD_y.js.map
20187
+ export { MakeTeachingMoment as $, Accordion as A, Button as B, CheckboxPropertyLine as C, DebugServiceIdentity as D, ExtensibleAccordion as E, useColor3Property as F, useColor4Property as G, useQuaternionProperty as H, Inspector as I, MakePropertyHook as J, useInterceptObservable as K, Link as L, MessageBar as M, NumberInputPropertyLine as N, useEventfulState as O, Popover as P, useObservableCollection as Q, useOrderedObservableCollection as R, SpinButtonPropertyLine as S, TextInputPropertyLine as T, usePollingObservable as U, Vector3PropertyLine as V, useResource as W, useAsyncResource as X, useCompactMode as Y, useSidePaneDockOverrides as Z, useAngleConverters as _, ShellServiceIdentity as a, MakeDialogTeachingMoment as a0, InterceptFunction as a1, GetPropertyDescriptor as a2, IsPropertyReadonly as a3, InterceptProperty as a4, ObservableCollection as a5, ConstructorFactory as a6, SelectionServiceIdentity as a7, SelectionServiceDefinition as a8, SettingsContextIdentity as a9, ToggleButton as aA, ChildWindow as aB, FileUploadLine as aC, FactorGradientList as aD, Color3GradientList as aE, Color4GradientList as aF, Pane as aG, BooleanBadgePropertyLine as aH, Color3PropertyLine as aI, Color4PropertyLine as aJ, HexPropertyLine as aK, LinkPropertyLine as aL, PropertyLine as aM, LineContainer as aN, PlaceholderPropertyLine as aO, StringifiedPropertyLine as aP, SwitchPropertyLine as aQ, SyncedSliderPropertyLine as aR, TextAreaPropertyLine as aS, TextPropertyLine as aT, RotationVectorPropertyLine as aU, QuaternionPropertyLine as aV, Vector2PropertyLine as aW, Vector4PropertyLine as aX, ShowInspector as aa, Checkbox as ab, Collapse as ac, ColorPickerPopup as ad, InputHexField as ae, InputHsvField as af, ComboBox as ag, DraggableLine as ah, Dropdown as ai, NumberDropdown as aj, StringDropdown as ak, FactorGradientComponent as al, Color3GradientComponent as am, Color4GradientComponent as an, ColorStepGradientComponent as ao, InfoLabel as ap, MakeLazyComponent as aq, List as ar, PositionedPopover as as, SearchBar as at, SearchBox as au, SpinButton as av, Switch as aw, SyncedSliderInput as ax, Textarea as ay, TextInput as az, SceneContextIdentity as b, useObservableState as c, AccordionSection as d, ButtonLine as e, ToolsServiceIdentity as f, useExtensionManager as g, MakePopoverTeachingMoment as h, TeachingMoment as i, SidePaneContainer as j, PropertiesServiceIdentity as k, SceneExplorerServiceIdentity as l, SettingsServiceIdentity as m, StatsServiceIdentity as n, ConvertOptions as o, AttachDebugLayer as p, DetachDebugLayer as q, NumberDropdownPropertyLine as r, StringDropdownPropertyLine as s, BoundProperty as t, useProperty as u, Property as v, LinkToEntityPropertyLine as w, Theme as x, BuiltInsExtensionFeed as y, useVector3Property as z };
20188
+ //# sourceMappingURL=index-BDtiVo5p.js.map