@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.
- package/lib/{extensionsListService-Dsui74Id.js → extensionsListService-Cze-L8Cg.js} +16 -2
- package/lib/{extensionsListService-Dsui74Id.js.map → extensionsListService-Cze-L8Cg.js.map} +1 -1
- package/lib/gif-8Ty35Toc.js +2 -0
- package/lib/gif-8Ty35Toc.js.map +1 -0
- package/lib/{index-D89pOD_y.js → index-BDtiVo5p.js} +2777 -795
- package/lib/index-BDtiVo5p.js.map +1 -0
- package/lib/index.d.ts +27 -14
- package/lib/index.js +15 -1
- package/lib/index.js.map +1 -1
- package/lib/{quickCreateToolsService-CBcmm2Zr.js → quickCreateToolsService-XuuTpQe7.js} +15 -2
- package/lib/{quickCreateToolsService-CBcmm2Zr.js.map → quickCreateToolsService-XuuTpQe7.js.map} +1 -1
- package/lib/{importService-BG3n3RAJ.js → reflectorService-ChA54PIt.js} +44 -56
- package/lib/reflectorService-ChA54PIt.js.map +1 -0
- package/package.json +1 -1
- package/lib/captureService-BvHqJq2V.js +0 -220
- package/lib/captureService-BvHqJq2V.js.map +0 -1
- package/lib/exportService-CB99mqRE.js +0 -270
- package/lib/exportService-CB99mqRE.js.map +0 -1
- package/lib/importService-BG3n3RAJ.js.map +0 -1
- package/lib/index-D89pOD_y.js.map +0 -1
|
@@ -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,
|
|
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,
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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, {
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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(
|
|
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:
|
|
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
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
const
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
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
|
-
|
|
3920
|
-
|
|
3921
|
-
setIsOpen(true);
|
|
3922
|
-
performanceCollector?.start(true);
|
|
3983
|
+
lastCalledTime = now;
|
|
3984
|
+
callback(...args);
|
|
3923
3985
|
};
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
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
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
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
|
-
|
|
3946
|
-
|
|
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
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
-
|
|
4092
|
-
|
|
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
|
|
4099
|
-
|
|
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
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
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
|
-
*
|
|
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
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
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
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
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
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
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
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
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
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
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
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
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
|
-
|
|
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
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
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
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
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
|
|
4259
|
-
|
|
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
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
5361
|
+
header: {
|
|
5362
|
+
color: tokens.colorNeutralForeground1,
|
|
5363
|
+
backgroundColor: tokens.colorBrandBackground,
|
|
5364
|
+
gridTemplateColumns: "10px 9fr 1fr 8px",
|
|
4271
5365
|
},
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
maxWidth: "75px",
|
|
5366
|
+
categoryHeader: {
|
|
5367
|
+
backgroundColor: tokens.colorNeutralBackground4,
|
|
5368
|
+
minHeight: "30px",
|
|
4276
5369
|
},
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
minWidth: "50px",
|
|
4280
|
-
// No maxWidth - slider grows to fill available space
|
|
5370
|
+
categoryColumn2: {
|
|
5371
|
+
gridColumn: "2",
|
|
4281
5372
|
},
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
5373
|
+
categoryColumn3: {
|
|
5374
|
+
gridColumn: "3",
|
|
5375
|
+
display: "flex",
|
|
5376
|
+
justifyContent: "end",
|
|
4286
5377
|
},
|
|
4287
|
-
|
|
4288
|
-
|
|
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
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4301
|
-
const [
|
|
4302
|
-
|
|
4303
|
-
const
|
|
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
|
-
|
|
4312
|
-
|
|
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
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
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
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
return classes.growSlider;
|
|
5460
|
+
const onCheckAllChange = (category) => (selected) => {
|
|
5461
|
+
const categoryIds = metadataCategoryId?.get(category);
|
|
5462
|
+
if (!categoryIds) {
|
|
5463
|
+
return;
|
|
4344
5464
|
}
|
|
4345
|
-
|
|
4346
|
-
|
|
5465
|
+
for (const id of categoryIds) {
|
|
5466
|
+
collector.updateMetadata(id, "hidden", !selected);
|
|
4347
5467
|
}
|
|
4348
|
-
return classes.slider;
|
|
4349
5468
|
};
|
|
4350
|
-
|
|
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
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
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
|
|
4365
|
-
|
|
4366
|
-
const
|
|
4367
|
-
const
|
|
4368
|
-
|
|
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
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
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
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
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
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
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
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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
|
-
*
|
|
4425
|
-
*
|
|
4426
|
-
* @
|
|
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
|
|
4430
|
-
|
|
4431
|
-
|
|
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
|
|
4453
|
-
|
|
4454
|
-
|
|
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
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
|
4485
|
-
container: {
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
flexDirection: "
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
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
|
-
|
|
4503
|
-
flex: 1,
|
|
4504
|
-
|
|
4505
|
-
|
|
5871
|
+
slider: {
|
|
5872
|
+
flex: "1 1 auto",
|
|
5873
|
+
minWidth: "75px",
|
|
5874
|
+
maxWidth: "75px",
|
|
4506
5875
|
},
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
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
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
gap: tokens.spacingVerticalSNudge, // 6px
|
|
5886
|
+
compactSpinButton: {
|
|
5887
|
+
width: "65px",
|
|
5888
|
+
minWidth: "65px",
|
|
5889
|
+
maxWidth: "65px",
|
|
4529
5890
|
},
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
alignItems: "center",
|
|
5891
|
+
compactSpinButtonInput: {
|
|
5892
|
+
minWidth: "0",
|
|
4533
5893
|
},
|
|
4534
5894
|
});
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
-
|
|
4545
|
-
}, [value]);
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
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
|
|
4554
|
-
|
|
4555
|
-
onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
|
|
5929
|
+
const handleSliderPointerDown = () => {
|
|
5930
|
+
isDraggingRef.current = true;
|
|
4556
5931
|
};
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
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
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
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
|
-
|
|
4647
|
-
return
|
|
5949
|
+
if (props.compact) {
|
|
5950
|
+
return classes.compactSlider;
|
|
4648
5951
|
}
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
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-
|
|
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",
|
|
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: () =>
|
|
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)
|
|
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,
|
|
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(
|
|
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 {
|
|
14073
|
+
const { attractorData, id, impostorScale, impostorMaterial, impostorColor, scene, onControl, isControlled } = props;
|
|
12725
14074
|
const classes = useAttractorStyles();
|
|
12726
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
12748
|
-
|
|
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
|
|
14115
|
+
// For each IAttractorData, create a listItem
|
|
12759
14116
|
function AttractorsToListItems(attractors) {
|
|
12760
|
-
return
|
|
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,
|
|
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(
|
|
12793
|
-
}, [
|
|
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:
|
|
12806
|
-
|
|
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
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
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
|
|
12904
|
-
return entity instanceof ParticleSystem
|
|
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:
|
|
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(
|
|
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 {
|
|
18206
|
-
//# sourceMappingURL=index-
|
|
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
|