@babylonjs/inspector 8.42.0-preview → 8.43.0-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,13 +3,14 @@ import { createContext, useContext, useMemo, useEffect, useState, useRef, useCal
3
3
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
4
4
  import { Vector3, Quaternion, Matrix, Vector2, Vector4, TmpVectors } from '@babylonjs/core/Maths/math.vector.js';
5
5
  import { Observable } from '@babylonjs/core/Misc/observable.js';
6
- import { makeStyles, Link as Link$1, Body1, ToggleButton as ToggleButton$1, Button as Button$1, tokens, InfoLabel as InfoLabel$1, Body1Strong, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, createLightTheme, createDarkTheme, FluentProvider, Tooltip, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, Portal, RendererProvider, ToolbarRadioButton, createDOMRenderer, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, treeItemLevelToken, MenuItemCheckbox, Switch as Switch$1, PresenceBadge, useId, SpinButton as SpinButton$1, Slider, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Spinner, Badge, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, Field } from '@fluentui/react-components';
7
- import { ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, Copy20Regular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ErrorCircleRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, SaveRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, CopyRegular, DeleteRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, PlayRegular, AppGenericRegular, MyLocationRegular, CameraRegular, LightbulbRegular, BorderOutsideRegular, BorderNoneRegular, EyeRegular, EyeOffRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
6
+ import { makeStyles, Link as Link$1, Body1, ToggleButton as ToggleButton$1, Button as Button$1, tokens, InfoLabel as InfoLabel$1, Body1Strong, Checkbox as Checkbox$1, mergeClasses, Accordion as Accordion$1, AccordionItem, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, createLightTheme, createDarkTheme, FluentProvider, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Portal, RendererProvider, createDOMRenderer, Tooltip, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, SearchBox as SearchBox$1, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, treeItemLevelToken, MenuItemCheckbox, Switch as Switch$1, PresenceBadge, useId, SpinButton as SpinButton$1, Slider, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Spinner, Badge, MessageBar as MessageBar$1, MessageBarBody, MessageBarTitle, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, Label, ToolbarDivider, Field } from '@fluentui/react-components';
7
+ import { ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, Copy20Regular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, DocumentTextRegular, createFluentIcon, FilterRegular, GlobeRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, SettingsRegular, ArrowUploadRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ErrorCircleRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, SaveRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, CopyRegular, DeleteRegular, EyeOffFilled, EyeFilled, ArrowMoveFilled, StopFilled, PlayFilled, EyeRegular, EyeOffRegular, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ChevronDownRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, PlayRegular, AppGenericRegular, MyLocationRegular, CameraRegular, LightbulbRegular, BorderOutsideRegular, BorderNoneRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
8
8
  import { Collapse as Collapse$1, Fade } from '@fluentui/react-motion-components-preview';
9
9
  import '@babylonjs/core/Misc/typeStore.js';
10
10
  import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
11
11
  import { AsyncLock } from '@babylonjs/core/Misc/asyncLock.js';
12
12
  import { Deferred } from '@babylonjs/core/Misc/deferred.js';
13
+ import { Logger } from '@babylonjs/core/Misc/logger.js';
13
14
  import { Clamp } from '@babylonjs/core/Maths/math.scalar.functions.js';
14
15
  import { VirtualizerScrollView } from '@fluentui-contrib/react-virtualizer';
15
16
  import { FontAsset } from '@babylonjs/addons/msdfText/fontAsset.js';
@@ -32,7 +33,6 @@ import { PerfCollectionStrategy } from '@babylonjs/core/Misc/PerformanceViewer/p
32
33
  import '@babylonjs/core/Misc/PerformanceViewer/performanceViewerSceneExtension.js';
33
34
  import { PressureObserverWrapper } from '@babylonjs/core/Misc/pressureObserverWrapper.js';
34
35
  import { AbstractEngine } from '@babylonjs/core/Engines/abstractEngine.js';
35
- import { Logger } from '@babylonjs/core/Misc/logger.js';
36
36
  import { createRoot } from 'react-dom/client';
37
37
  import { FrameGraphUtils } from '@babylonjs/core/FrameGraph/frameGraphUtils.js';
38
38
  import { CameraGizmo } from '@babylonjs/core/Gizmos/cameraGizmo.js';
@@ -94,17 +94,21 @@ import { ImageProcessingConfiguration } from '@babylonjs/core/Materials/imagePro
94
94
  import { Skeleton } from '@babylonjs/core/Bones/skeleton.js';
95
95
  import { Sprite } from '@babylonjs/core/Sprites/sprite.js';
96
96
  import { SpriteManager } from '@babylonjs/core/Sprites/spriteManager.js';
97
- import { WhenTextureReadyAsync, GetTextureDataAsync } from '@babylonjs/core/Misc/textureTools.js';
97
+ import { GetTextureDataAsync, WhenTextureReadyAsync } from '@babylonjs/core/Misc/textureTools.js';
98
98
  import { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture.js';
99
99
  import { MultiRenderTarget } from '@babylonjs/core/Materials/Textures/multiRenderTarget.js';
100
100
  import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture.js';
101
101
  import { ThinTexture } from '@babylonjs/core/Materials/Textures/thinTexture.js';
102
+ import { KeyboardEventTypes } from '@babylonjs/core/Events/keyboardEvents.js';
103
+ import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js';
104
+ import { HtmlElementTexture } from '@babylonjs/core/Materials/Textures/htmlElementTexture.js';
105
+ import { ShaderMaterial } from '@babylonjs/core/Materials/shaderMaterial.js';
106
+ import { CreatePlane } from '@babylonjs/core/Meshes/Builders/planeBuilder.js';
102
107
  import { ClusteredLightContainer } from '@babylonjs/core/Lights/Clustered/clusteredLightContainer.js';
103
108
  import '@babylonjs/core/Rendering/boundingBoxRenderer.js';
104
109
  import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent.js';
105
110
  import '@babylonjs/core/Sprites/spriteSceneComponent.js';
106
111
  import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture.js';
107
- import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js';
108
112
  import { EngineStore } from '@babylonjs/core/Engines/engineStore.js';
109
113
  import { UniqueIdGenerator } from '@babylonjs/core/Misc/uniqueIdGenerator.js';
110
114
  import { DebugLayer } from '@babylonjs/core/Debug/debugLayer.js';
@@ -598,7 +602,10 @@ function CreateGenericForwardRef(render) {
598
602
  *
599
603
  * NOTE: BoundProperty has strict nullable enforcement!
600
604
  *
601
- * If Target[PropertyKey] is Nullable, caller can only bind to a component that explicitly handles nullable (and caller must send nullable/defaultValue props)
605
+ * If Target[PropertyKey] is Nullable, caller has three options:
606
+ * 1. `nullable: true` + `defaultValue: NonNullable<T>` - Shows enable/disable checkbox UI
607
+ * 2. `ignoreNullable: true` + `defaultValue: NonNullable<T>` - Shows disabled state when null
608
+ * 3. `defaultValue: null` - Skips nullable handling entirely, passes value through as-is
602
609
  *
603
610
  * @param props BoundPropertyProps with strict nullable validation
604
611
  * @returns JSX element
@@ -885,7 +892,7 @@ const LinkToEntityPropertyLine = (props) => {
885
892
  !linkedEntity.reservedDataStore?.hidden && jsx(LinkPropertyLine, { ...rest, value: linkedEntity.name, onLink: () => (selectionService.selectedEntity = linkedEntity) }));
886
893
  };
887
894
 
888
- const useStyles$j = makeStyles({
895
+ const useStyles$r = makeStyles({
889
896
  accordion: {
890
897
  overflowX: "hidden",
891
898
  overflowY: "auto",
@@ -929,13 +936,13 @@ const useStyles$j = makeStyles({
929
936
  });
930
937
  const AccordionSection = (props) => {
931
938
  AccordionSection.displayName = "AccordionSection";
932
- const classes = useStyles$j();
939
+ const classes = useStyles$r();
933
940
  return jsx("div", { className: classes.panelDiv, children: props.children });
934
941
  };
935
942
  const StringAccordion = Accordion$1;
936
943
  const Accordion = forwardRef((props, ref) => {
937
944
  Accordion.displayName = "Accordion";
938
- const classes = useStyles$j();
945
+ const classes = useStyles$r();
939
946
  const { size } = useContext(ToolContext);
940
947
  const { children, highlightSections, ...rest } = props;
941
948
  const validChildren = useMemo(() => {
@@ -1067,7 +1074,7 @@ function AsReadonlyArray(array) {
1067
1074
  return array;
1068
1075
  }
1069
1076
  // eslint-disable-next-line @typescript-eslint/naming-convention
1070
- const useStyles$i = makeStyles({
1077
+ const useStyles$q = makeStyles({
1071
1078
  rootDiv: {
1072
1079
  flex: 1,
1073
1080
  overflow: "hidden",
@@ -1076,7 +1083,7 @@ const useStyles$i = makeStyles({
1076
1083
  },
1077
1084
  });
1078
1085
  function ExtensibleAccordion(props) {
1079
- const classes = useStyles$i();
1086
+ const classes = useStyles$q();
1080
1087
  const { children, sections, sectionContent, context, sectionsRef } = props;
1081
1088
  const defaultSections = useMemo(() => {
1082
1089
  const defaultSections = [];
@@ -1181,7 +1188,7 @@ function ExtensibleAccordion(props) {
1181
1188
  })] }) })) }));
1182
1189
  }
1183
1190
 
1184
- const useStyles$h = makeStyles({
1191
+ const useStyles$p = makeStyles({
1185
1192
  paneRootDiv: {
1186
1193
  display: "flex",
1187
1194
  flex: 1,
@@ -1194,12 +1201,73 @@ const useStyles$h = makeStyles({
1194
1201
  */
1195
1202
  const SidePaneContainer = forwardRef((props, ref) => {
1196
1203
  const { className, ...rest } = props;
1197
- const classes = useStyles$h();
1204
+ const classes = useStyles$p();
1198
1205
  return (jsx("div", { className: mergeClasses(classes.paneRootDiv, className), ref: ref, ...rest, children: props.children }));
1199
1206
  });
1200
1207
 
1208
+ const ThemeModeStorageKey = `Babylon/Settings/ThemeMode`;
1209
+ /**
1210
+ * Custom hook to manage the theme mode (system/dark/light).
1211
+ * @returns An object containing the theme mode state and helper functions.
1212
+ */
1213
+ function useThemeMode() {
1214
+ const { isDarkMode, ternaryDarkMode, setTernaryDarkMode } = useTernaryDarkMode({
1215
+ localStorageKey: ThemeModeStorageKey,
1216
+ });
1217
+ // Make sure there is a stored value initially, even before changing the theme.
1218
+ // This way, other usages of this hook will get the correct initial value.
1219
+ if (!localStorage.getItem(ThemeModeStorageKey)) {
1220
+ SetThemeMode(ternaryDarkMode);
1221
+ }
1222
+ return { isDarkMode, themeMode: ternaryDarkMode, setThemeMode: setTernaryDarkMode };
1223
+ }
1224
+ /**
1225
+ * Sets the theme mode.
1226
+ * @param mode The desired theme mode (system/dark/light).
1227
+ */
1228
+ function SetThemeMode(mode) {
1229
+ localStorage.setItem(ThemeModeStorageKey, JSON.stringify(mode));
1230
+ }
1231
+
1232
+ /* eslint-disable @typescript-eslint/naming-convention */
1233
+ // Generated from https://react.fluentui.dev/?path=/docs/theme-theme-designer--docs
1234
+ // Key color: #3A94FC
1235
+ const babylonRamp = {
1236
+ 10: "#020305",
1237
+ 20: "#121721",
1238
+ 30: "#1A263A",
1239
+ 40: "#1F314F",
1240
+ 50: "#243E64",
1241
+ 60: "#294B7B",
1242
+ 70: "#2D5892",
1243
+ 80: "#3166AA",
1244
+ 90: "#3473C3",
1245
+ 100: "#3782DC",
1246
+ 110: "#3990F6",
1247
+ 120: "#5A9EFD",
1248
+ 130: "#7BACFE",
1249
+ 140: "#96BAFF",
1250
+ 150: "#AFC9FF",
1251
+ 160: "#C6D8FF",
1252
+ };
1253
+ const LightTheme = {
1254
+ ...createLightTheme(babylonRamp),
1255
+ };
1256
+ const DarkTheme = {
1257
+ ...createDarkTheme(babylonRamp),
1258
+ };
1259
+
1260
+ const Theme = (props) => {
1261
+ // NOTE: We do not want to applyStylesToPortals by default. If makes classes flow into portals
1262
+ // (like popovers), and if those styles do things like disable overflow, they can completely
1263
+ // break any UI within the portal. Therefore, default to false.
1264
+ const { invert = false, applyStylesToPortals = false, ...rest } = props;
1265
+ const { isDarkMode } = useThemeMode();
1266
+ return (jsx(FluentProvider, { theme: isDarkMode !== invert ? DarkTheme : LightTheme, applyStylesToPortals: applyStylesToPortals, ...rest, children: props.children }));
1267
+ };
1268
+
1201
1269
  // eslint-disable-next-line @typescript-eslint/naming-convention
1202
- const useStyles$g = makeStyles({
1270
+ const useStyles$o = makeStyles({
1203
1271
  extensionTeachingPopover: {
1204
1272
  maxWidth: "320px",
1205
1273
  },
@@ -1210,7 +1278,7 @@ const useStyles$g = makeStyles({
1210
1278
  * @returns The teaching moment popover.
1211
1279
  */
1212
1280
  const TeachingMoment = ({ shouldDisplay, positioningRef, onOpenChange, title, description }) => {
1213
- const classes = useStyles$g();
1281
+ const classes = useStyles$o();
1214
1282
  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 })] }) }));
1215
1283
  };
1216
1284
 
@@ -1466,75 +1534,187 @@ function ConstructorFactory(constructor) {
1466
1534
  }
1467
1535
 
1468
1536
  // eslint-disable-next-line @typescript-eslint/naming-convention
1469
- const useStyles$f = makeStyles({
1537
+ const useStyles$n = makeStyles({
1470
1538
  placeholderDiv: {
1471
1539
  padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
1472
1540
  },
1473
1541
  });
1474
1542
  const PropertiesPane = (props) => {
1475
- const classes = useStyles$f();
1543
+ const classes = useStyles$n();
1476
1544
  const entity = props.context;
1477
1545
  return entity != null ? (jsx(ExtensibleAccordion, { ...props })) : (jsx("div", { className: classes.placeholderDiv, children: jsx(Body1Strong, { italic: true, children: "No entity selected." }) }));
1478
1546
  };
1479
1547
 
1480
1548
  const SettingsContextIdentity = Symbol("SettingsContext");
1481
1549
 
1482
- const ThemeModeStorageKey = `Babylon/Settings/ThemeMode`;
1483
- /**
1484
- * Custom hook to manage the theme mode (system/dark/light).
1485
- * @returns An object containing the theme mode state and helper functions.
1486
- */
1487
- function useThemeMode() {
1488
- const { isDarkMode, ternaryDarkMode, setTernaryDarkMode } = useTernaryDarkMode({
1489
- localStorageKey: ThemeModeStorageKey,
1490
- });
1491
- // Make sure there is a stored value initially, even before changing the theme.
1492
- // This way, other usages of this hook will get the correct initial value.
1493
- if (!localStorage.getItem(ThemeModeStorageKey)) {
1494
- SetThemeMode(ternaryDarkMode);
1550
+ function ToFeaturesString(options) {
1551
+ const { defaultWidth, defaultHeight, defaultLeft, defaultTop } = options;
1552
+ const features = [];
1553
+ if (defaultWidth !== undefined) {
1554
+ features.push({ key: "width", value: defaultWidth.toString() });
1495
1555
  }
1496
- return { isDarkMode, themeMode: ternaryDarkMode, setThemeMode: setTernaryDarkMode };
1556
+ if (defaultHeight !== undefined) {
1557
+ features.push({ key: "height", value: defaultHeight.toString() });
1558
+ }
1559
+ if (defaultLeft !== undefined) {
1560
+ features.push({ key: "left", value: defaultLeft.toString() });
1561
+ }
1562
+ if (defaultTop !== undefined) {
1563
+ features.push({ key: "top", value: defaultTop.toString() });
1564
+ }
1565
+ features.push({ key: "location", value: "no" });
1566
+ return features.map((feature) => `${feature.key}=${feature.value}`).join(",");
1497
1567
  }
1568
+ const useStyles$m = makeStyles({
1569
+ container: {
1570
+ display: "flex",
1571
+ flexGrow: 1,
1572
+ flexDirection: "column",
1573
+ overflow: "hidden",
1574
+ },
1575
+ });
1498
1576
  /**
1499
- * Sets the theme mode.
1500
- * @param mode The desired theme mode (system/dark/light).
1577
+ * Allows displaying a child window that can contain child components.
1578
+ * @param props Props for the child window.
1579
+ * @returns The child window component.
1501
1580
  */
1502
- function SetThemeMode(mode) {
1503
- localStorage.setItem(ThemeModeStorageKey, JSON.stringify(mode));
1504
- }
1505
-
1506
- /* eslint-disable @typescript-eslint/naming-convention */
1507
- // Generated from https://react.fluentui.dev/?path=/docs/theme-theme-designer--docs
1508
- // Key color: #3A94FC
1509
- const babylonRamp = {
1510
- 10: "#020305",
1511
- 20: "#121721",
1512
- 30: "#1A263A",
1513
- 40: "#1F314F",
1514
- 50: "#243E64",
1515
- 60: "#294B7B",
1516
- 70: "#2D5892",
1517
- 80: "#3166AA",
1518
- 90: "#3473C3",
1519
- 100: "#3782DC",
1520
- 110: "#3990F6",
1521
- 120: "#5A9EFD",
1522
- 130: "#7BACFE",
1523
- 140: "#96BAFF",
1524
- 150: "#AFC9FF",
1525
- 160: "#C6D8FF",
1526
- };
1527
- const LightTheme = {
1528
- ...createLightTheme(babylonRamp),
1529
- };
1530
- const DarkTheme = {
1531
- ...createDarkTheme(babylonRamp),
1532
- };
1533
-
1534
- const Theme = (props) => {
1535
- const { invert = false, ...rest } = props;
1536
- const { isDarkMode } = useThemeMode();
1537
- return (jsx(FluentProvider, { theme: isDarkMode !== invert ? DarkTheme : LightTheme, ...rest, children: props.children }));
1581
+ const ChildWindow = (props) => {
1582
+ const { id, children, onOpenChange, imperativeRef: imperativeRef } = props;
1583
+ const classes = useStyles$m();
1584
+ const [windowState, setWindowState] = useState();
1585
+ const [childWindow, setChildWindow] = useState();
1586
+ const storageKey = id ? `Babylon/Settings/ChildWindow/${id}/Bounds` : null;
1587
+ // This function is just for creating the child window itself. It is a function because
1588
+ // it must be called synchronously in response to a user interaction (e.g. button click),
1589
+ // otherwise the browser will block it as a scripted popup.
1590
+ const createWindow = useCallback((options = {}) => {
1591
+ if (storageKey) {
1592
+ // If we are persisting window bounds, but the window is already open, just use the existing bounds.
1593
+ // Otherwise, try to load bounds from storage.
1594
+ if (childWindow) {
1595
+ options.defaultLeft = childWindow.screenX;
1596
+ options.defaultTop = childWindow.screenY;
1597
+ options.defaultWidth = childWindow.innerWidth;
1598
+ options.defaultHeight = childWindow.innerHeight;
1599
+ }
1600
+ else {
1601
+ const savedBounds = localStorage.getItem(storageKey);
1602
+ if (savedBounds) {
1603
+ try {
1604
+ const bounds = JSON.parse(savedBounds);
1605
+ options.defaultLeft = bounds.left;
1606
+ options.defaultTop = bounds.top;
1607
+ options.defaultWidth = bounds.width;
1608
+ options.defaultHeight = bounds.height;
1609
+ }
1610
+ catch {
1611
+ Logger.Warn(`Could not parse saved bounds for child window with key ${storageKey}`);
1612
+ }
1613
+ }
1614
+ }
1615
+ }
1616
+ // Half width by default.
1617
+ if (!options.defaultWidth) {
1618
+ options.defaultWidth = window.innerWidth * (2 / 3);
1619
+ }
1620
+ // Half height by default.
1621
+ if (!options.defaultHeight) {
1622
+ options.defaultHeight = window.innerHeight * (2 / 3);
1623
+ }
1624
+ // Horizontally centered by default.
1625
+ if (!options.defaultLeft) {
1626
+ options.defaultLeft = window.screenX + (window.innerWidth - options.defaultWidth) * (2 / 3);
1627
+ }
1628
+ // Vertically centered by default.
1629
+ if (!options.defaultTop) {
1630
+ options.defaultTop = window.screenY + (window.innerHeight - options.defaultHeight) * (2 / 3);
1631
+ }
1632
+ // Try to create the child window (can be null if popups are blocked).
1633
+ const newChildWindow = window.open("", "", ToFeaturesString(options));
1634
+ if (newChildWindow) {
1635
+ // Set the title if provided.
1636
+ newChildWindow.document.title = options.title ?? id ?? "";
1637
+ // Set the child window state.
1638
+ setChildWindow((current) => {
1639
+ // But first close any existing child window.
1640
+ current?.close();
1641
+ return newChildWindow;
1642
+ });
1643
+ }
1644
+ }, [childWindow, storageKey]);
1645
+ useImperativeHandle(imperativeRef, () => {
1646
+ return {
1647
+ open: createWindow,
1648
+ close: () => setChildWindow(undefined),
1649
+ };
1650
+ }, [createWindow]);
1651
+ // This side effect runs any time the child window instance changes. It does the rest of the child window
1652
+ // setup work, including creating resources and state needed to properly render the content of the child window.
1653
+ useEffect(() => {
1654
+ const disposeActions = [];
1655
+ if (childWindow) {
1656
+ const body = childWindow.document.body;
1657
+ body.style.width = "100%";
1658
+ body.style.height = "100%";
1659
+ body.style.margin = "0";
1660
+ body.style.padding = "0";
1661
+ body.style.display = "flex";
1662
+ body.style.overflow = "hidden";
1663
+ const applyWindowState = () => {
1664
+ // Setup the window state, including creating a Fluent/Griffel "renderer" for managing runtime styles/classes in the child window.
1665
+ setWindowState({ mountNode: body, renderer: createDOMRenderer(childWindow.document) });
1666
+ onOpenChange?.(true);
1667
+ };
1668
+ // Once the child window document is ready, setup the window state which will trigger another effect that renders into the child window.
1669
+ if (childWindow.document.readyState === "complete") {
1670
+ applyWindowState();
1671
+ }
1672
+ else {
1673
+ const onChildWindowLoad = () => {
1674
+ applyWindowState();
1675
+ };
1676
+ childWindow.addEventListener("load", onChildWindowLoad, { once: true });
1677
+ disposeActions.push(() => childWindow.removeEventListener("load", onChildWindowLoad));
1678
+ }
1679
+ // When the child window is closed for any reason, transition back to a closed state.
1680
+ const onChildWindowUnload = () => {
1681
+ setWindowState(undefined);
1682
+ setChildWindow(undefined);
1683
+ onOpenChange?.(false);
1684
+ };
1685
+ childWindow.addEventListener("unload", onChildWindowUnload, { once: true });
1686
+ disposeActions.push(() => childWindow.removeEventListener("unload", onChildWindowUnload));
1687
+ // If the main window closes, close any open child windows as well (don't leave them orphaned).
1688
+ const onParentWindowUnload = () => {
1689
+ childWindow.close();
1690
+ };
1691
+ window.addEventListener("unload", onParentWindowUnload, { once: true });
1692
+ disposeActions.push(() => window.removeEventListener("unload", onParentWindowUnload));
1693
+ // On dispose, close the child window.
1694
+ disposeActions.push(() => childWindow.close());
1695
+ // On dispose, save the window bounds.
1696
+ disposeActions.push(() => {
1697
+ if (storageKey) {
1698
+ localStorage.setItem(storageKey, JSON.stringify({
1699
+ left: childWindow.screenX,
1700
+ top: childWindow.screenY,
1701
+ width: childWindow.innerWidth,
1702
+ height: childWindow.innerHeight,
1703
+ }));
1704
+ }
1705
+ });
1706
+ }
1707
+ return () => {
1708
+ disposeActions.reverse().forEach((dispose) => dispose());
1709
+ };
1710
+ }, [childWindow]);
1711
+ if (!windowState) {
1712
+ return null;
1713
+ }
1714
+ const { mountNode, renderer } = windowState;
1715
+ return (
1716
+ // Portal targets the body of the child window.
1717
+ jsx(Portal, { mountNode: mountNode, children: jsx(RendererProvider, { renderer: renderer, targetDocument: mountNode.ownerDocument, children: jsx(FluentProvider, { className: classes.container, applyStylesToPortals: false, targetDocument: mountNode.ownerDocument, children: children }) }) }));
1538
1718
  };
1539
1719
 
1540
1720
  // NOTE: This is basically a super simplified version of https://github.com/microsoft/fluentui-contrib/blob/main/packages/react-resize-handle/src/hooks
@@ -1613,7 +1793,7 @@ function useResizeHandle(params) {
1613
1793
  const RootComponentServiceIdentity = Symbol("RootComponent");
1614
1794
  const ShellServiceIdentity = Symbol("ShellService");
1615
1795
  // eslint-disable-next-line @typescript-eslint/naming-convention
1616
- const useStyles$e = makeStyles({
1796
+ const useStyles$l = makeStyles({
1617
1797
  mainView: {
1618
1798
  flex: 1,
1619
1799
  display: "flex",
@@ -1801,12 +1981,12 @@ const DockMenu = (props) => {
1801
1981
  };
1802
1982
  const PaneHeader = (props) => {
1803
1983
  const { id, title, dockOptions } = props;
1804
- const classes = useStyles$e();
1984
+ const classes = useStyles$l();
1805
1985
  return (jsx(Theme, { invert: true, children: 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, {}) }) })] }) }));
1806
1986
  };
1807
1987
  // 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.
1808
1988
  const ToolbarItem = ({ verticalLocation, horizontalLocation, id, component: Component, displayName: displayName, suppressTeachingMoment }) => {
1809
- const classes = useStyles$e();
1989
+ const classes = useStyles$l();
1810
1990
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Bar/${verticalLocation}/${horizontalLocation}/${displayName ?? id}`), [displayName, id]);
1811
1991
  const teachingMoment = useTeachingMoment(suppressTeachingMoment);
1812
1992
  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, {}) })] }));
@@ -1814,7 +1994,7 @@ const ToolbarItem = ({ verticalLocation, horizontalLocation, id, component: Comp
1814
1994
  // TODO: Handle overflow, possibly via https://react.fluentui.dev/?path=/docs/components-overflow--docs with priority.
1815
1995
  // This component just renders a toolbar with left aligned toolbar items on the left and right aligned toolbar items on the right.
1816
1996
  const Toolbar = ({ location, components }) => {
1817
- const classes = useStyles$e();
1997
+ const classes = useStyles$l();
1818
1998
  const leftComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "left"), [components]);
1819
1999
  const rightComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "right"), [components]);
1820
2000
  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))) })] })) }));
@@ -1824,7 +2004,7 @@ const SidePaneTab = (props) => {
1824
2004
  const { location, id, isSelected, dockOptions,
1825
2005
  // eslint-disable-next-line @typescript-eslint/naming-convention
1826
2006
  icon: Icon, title, suppressTeachingMoment, } = props;
1827
- const classes = useStyles$e();
2007
+ const classes = useStyles$l();
1828
2008
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Pane/${location}/${title ?? id}`), [title, id]);
1829
2009
  const teachingMoment = useTeachingMoment(suppressTeachingMoment);
1830
2010
  const tabClass = mergeClasses(classes.tab, isSelected ? undefined : classes.unselectedTab);
@@ -1837,11 +2017,13 @@ const SidePaneTab = (props) => {
1837
2017
  // In "compact" mode, the tab list is integrated into the pane itself.
1838
2018
  // In "full" mode, the returned tab list is later injected into the toolbar.
1839
2019
  function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSelectSidePane, dockOperations, toolbarMode, topBarItems, bottomBarItems) {
1840
- const classes = useStyles$e();
2020
+ const classes = useStyles$l();
1841
2021
  const [topSelectedTab, setTopSelectedTab] = useState();
1842
2022
  const [bottomSelectedTab, setBottomSelectedTab] = useState();
1843
2023
  const [collapsed, setCollapsed] = useState(false);
1844
- const [undocked, setUndocked] = useState(false);
2024
+ const childWindow = useRef(null);
2025
+ const [isChildWindowOpen, setIsChildWindowOpen] = useState(false);
2026
+ const paneContainerRef = useRef(null);
1845
2027
  const onExpandCollapseClick = useCallback(() => {
1846
2028
  setCollapsed((collapsed) => !collapsed);
1847
2029
  }, []);
@@ -1898,6 +2080,34 @@ function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSele
1898
2080
  });
1899
2081
  return () => observer.remove();
1900
2082
  }, [topPanes, bottomPanes, onSelectSidePane]);
2083
+ const setUndocked = useCallback((undocked) => {
2084
+ if (!undocked) {
2085
+ childWindow.current?.close();
2086
+ }
2087
+ else {
2088
+ const paneContainer = paneContainerRef.current;
2089
+ if (!paneContainer) {
2090
+ // It shouldn't be possible to get here and have this ref be null, but just in case,
2091
+ // bail out of the undock operation.
2092
+ childWindow.current?.close();
2093
+ }
2094
+ else {
2095
+ // This is the extra buffer needed on top of minWidth to account for window chrome to avoid a horizontal scrollbar.
2096
+ const widthBuffer = 4;
2097
+ // This offsets the window's top position to account for window chrome/title bar.
2098
+ const topOffset = 100;
2099
+ // Create the child window with approximately the same location and size as the side pane.
2100
+ const bounds = paneContainer.getBoundingClientRect();
2101
+ childWindow.current?.open({
2102
+ defaultWidth: Math.max(bounds.width, minWidth + widthBuffer),
2103
+ defaultHeight: bounds.height - topOffset,
2104
+ defaultTop: bounds.top + window.screenY + topOffset,
2105
+ defaultLeft: bounds.left + window.screenX,
2106
+ title: location === "left" ? "Left" : "Right",
2107
+ });
2108
+ }
2109
+ }
2110
+ }, [childWindow, location]);
1901
2111
  const expandCollapseButton = useMemo(() => {
1902
2112
  const expandCollapseIcon = location === "left" ? collapsed ? jsx(PanelLeftExpandRegular, {}) : jsx(PanelLeftContractRegular, {}) : collapsed ? jsx(PanelRightExpandRegular, {}) : jsx(PanelRightContractRegular, {});
1903
2113
  return (jsxs(Menu, { positioning: "below-end", children: [jsx(MenuTrigger, { disableButtonEnhancement: true, children: (triggerProps) => (jsx(Tooltip, { content: collapsed ? "Show Side Pane" : "Hide Side Pane", relationship: "label", children: jsx(SplitButton, { className: classes.paneCollapseButton, menuButton: triggerProps, primaryActionButton: { onClick: onExpandCollapseClick }, size: "small", appearance: "transparent", icon: expandCollapseIcon }) })) }), jsx(MenuPopover, { className: classes.collapseMenuPopover, children: jsx(MenuList, { children: jsx(MenuItem, { icon: jsx(PictureInPictureEnterRegular, {}), onClick: () => setUndocked(true), children: "Undock" }) }) })] }));
@@ -1910,8 +2120,8 @@ function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSele
1910
2120
  }, children: paneComponents.map((entry) => {
1911
2121
  const isSelected = selectedTab?.key === entry.key;
1912
2122
  return (jsx(SidePaneTab, { location: location, id: entry.key, title: entry.title, icon: entry.icon, suppressTeachingMoment: entry.suppressTeachingMoment, isSelected: isSelected && !collapsed, dockOptions: dockOptions }, entry.key));
1913
- }) }) })), toolbarMode === "full" && (jsxs(Fragment, { children: [paneComponents.length > 1 && (jsxs(Fragment, { children: [jsx(Divider, { vertical: true, inset: true, style: { minHeight: 0 } }), " "] })), jsx(Collapse, { visible: !undocked, orientation: "horizontal", children: expandCollapseButton })] }))] })) }));
1914
- }, [location, collapsed, undocked, expandCollapseButton]);
2123
+ }) }) })), toolbarMode === "full" && (jsxs(Fragment, { children: [paneComponents.length > 1 && (jsxs(Fragment, { children: [jsx(Divider, { vertical: true, inset: true, style: { minHeight: 0 } }), " "] })), jsx(Collapse, { visible: !isChildWindowOpen, orientation: "horizontal", children: expandCollapseButton })] }))] })) }));
2124
+ }, [location, collapsed, isChildWindowOpen, expandCollapseButton]);
1915
2125
  // This memos the TabList to make it easy for the JSX to be inserted at the top of the pane (in "compact" mode) or returned to the caller to be used in the toolbar (in "full" mode).
1916
2126
  const topPaneTabList = useMemo(() => createPaneTabList(topPanes, toolbarMode, topSelectedTab, setTopSelectedTab, validTopDockOptions), [createPaneTabList, topPanes, toolbarMode, topSelectedTab]);
1917
2127
  const bottomPaneTabList = useMemo(() => createPaneTabList(bottomPanes, "compact", bottomSelectedTab, setBottomSelectedTab, validBottomDockOptions), [createPaneTabList, bottomPanes, bottomSelectedTab]);
@@ -1947,82 +2157,15 @@ function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSele
1947
2157
  setPaneHeightAdjust(Number.parseInt(storedPaneHeightAdjust));
1948
2158
  }
1949
2159
  }, []);
1950
- const paneContainerRef = useRef(null);
1951
- const [windowState, setWindowState] = useState();
1952
- useEffect(() => {
1953
- const disposeActions = [];
1954
- if (undocked) {
1955
- const paneContainer = paneContainerRef.current;
1956
- if (!paneContainer) {
1957
- // It shouldn't be possible to get here and have this ref be null, but just in case,
1958
- // bail out of the undock operation.
1959
- setUndocked(false);
1960
- }
1961
- else {
1962
- // This is the extra buffer needed on top of minWidth to account for window chrome to avoid a horizontal scrollbar.
1963
- const widthBuffer = 4;
1964
- // This offsets the window's top position to account for window chrome/title bar.
1965
- const topOffset = 100;
1966
- // Create the child window with approximately the same location and size as the side pane.
1967
- const bounds = paneContainer.getBoundingClientRect();
1968
- const top = bounds.top + window.screenY + topOffset;
1969
- const left = bounds.left + window.screenX;
1970
- const width = Math.max(bounds.width, minWidth + widthBuffer);
1971
- const height = bounds.height - topOffset;
1972
- const childWindow = window.open("", "", `width=${width},height=${height},left=${left},top=${top},location=no`);
1973
- if (childWindow) {
1974
- const body = childWindow.document.body;
1975
- body.style.width = "100%";
1976
- body.style.height = "100%";
1977
- body.style.margin = "0";
1978
- body.style.padding = "0";
1979
- body.style.display = "flex";
1980
- body.style.overflowY = "hidden";
1981
- body.style.overflowX = "auto";
1982
- childWindow.document.title = location === "left" ? "Left" : "Right";
1983
- const applyWindowState = () => {
1984
- // Setup the window state, including creating a Fluent/Griffel "renderer" for managing runtime styles/classes in the child window.
1985
- setWindowState({ window: childWindow, mountNode: body, renderer: createDOMRenderer(childWindow.document) });
1986
- };
1987
- // Once the child window document is ready, setup the window state which will trigger another effect that renders into the child window.
1988
- if (childWindow.document.readyState === "complete") {
1989
- applyWindowState();
1990
- }
1991
- else {
1992
- const onChildWindowLoad = () => {
1993
- applyWindowState();
1994
- };
1995
- childWindow.addEventListener("load", onChildWindowLoad, { once: true });
1996
- disposeActions.push(() => childWindow.removeEventListener("load", onChildWindowLoad));
1997
- }
1998
- // When the child window is closed for any reason, transition back to a docked state.
1999
- childWindow.addEventListener("unload", () => {
2000
- setWindowState(undefined);
2001
- setUndocked(false);
2002
- }, { once: true });
2003
- // If the main window closes, close any undocked child windows as well (don't leave them orphaned).
2004
- const onParentWindowUnload = () => childWindow.close();
2005
- window.addEventListener("unload", onParentWindowUnload);
2006
- disposeActions.push(() => window.removeEventListener("unload", onParentWindowUnload));
2007
- }
2008
- else {
2009
- // If creating a child window failed (e.g. popup blocked), then just revert to docked mode.
2010
- setUndocked(false);
2011
- }
2012
- disposeActions.push(() => childWindow?.close());
2013
- }
2014
- }
2015
- return () => disposeActions.reverse().forEach((dispose) => dispose());
2016
- }, [undocked]);
2017
2160
  // This effect closes the window if all panes have been removed.
2018
2161
  useEffect(() => {
2019
- if (windowState && topPanes.length === 0 && bottomPanes.length === 0) {
2020
- windowState.window.close();
2162
+ if (isChildWindowOpen && topPanes.length === 0 && bottomPanes.length === 0) {
2163
+ childWindow.current?.close();
2021
2164
  }
2022
- }, [windowState, topPanes, bottomPanes]);
2165
+ }, [childWindow, isChildWindowOpen, topPanes, bottomPanes]);
2023
2166
  // This memoizes the pane itself, which may or may not include the tab list, depending on the toolbar mode.
2024
2167
  const corePane = useMemo(() => {
2025
- return (jsxs(Fragment, { children: [toolbarMode === "compact" && (topPanes.length > 1 || topBarItems.length > 0) && (jsx(Fragment, { children: jsxs("div", { className: classes.barDiv, children: [!undocked && location === "left" && expandCollapseButton, topPaneTabList, jsx(Toolbar, { location: "top", components: topBarItems }), !undocked && location === "right" && expandCollapseButton] }) })), topPanes.length > 0 && (jsx("div", { className: classes.paneContent, children: topSelectedTab && (jsxs(Fragment, { children: [jsx(PaneHeader, { id: topSelectedTab.key, title: topSelectedTab.title, dockOptions: validTopDockOptions }), topPanes.map((pane) => (jsx("div", { className: mergeClasses(classes.paneContent, pane.key !== topSelectedTab.key ? classes.unselectedPane : undefined), children: jsx(pane.content, {}) }, pane.key)))] })) })), topPanes.length > 0 && bottomPanes.length > 0 && jsx(Divider, { ref: paneVerticalResizeHandleRef, className: classes.paneDivider }), bottomPanes.length > 1 && (jsx(Fragment, { children: jsx("div", { className: classes.barDiv, children: bottomPaneTabList }) })), bottomPanes.length > 0 && (jsx("div", { ref: paneVerticalResizeElementRef, className: classes.paneContent, style: { height: `clamp(200px, calc(45% + var(${paneHeightAdjustCSSVar}, 0px)), 100% - 300px)`, flex: "0 0 auto" }, children: bottomSelectedTab && (jsxs(Fragment, { children: [jsx(PaneHeader, { id: bottomSelectedTab.key, title: bottomSelectedTab.title, dockOptions: validBottomDockOptions }), bottomPanes.map((pane) => (jsx("div", { className: mergeClasses(classes.paneContent, pane.key !== bottomSelectedTab.key ? classes.unselectedPane : undefined), children: jsx(pane.content, {}) }, pane.key)))] })) })), toolbarMode === "compact" && bottomBarItems.length > 0 && (jsx(Fragment, { children: jsx("div", { className: classes.barDiv, children: jsx(Toolbar, { location: "bottom", components: bottomBarItems }) }) }))] }));
2168
+ return (jsxs(Fragment, { children: [toolbarMode === "compact" && (topPanes.length > 1 || topBarItems.length > 0) && (jsx(Fragment, { children: jsxs("div", { className: classes.barDiv, children: [!isChildWindowOpen && location === "left" && expandCollapseButton, topPaneTabList, jsx(Toolbar, { location: "top", components: topBarItems }), !isChildWindowOpen && location === "right" && expandCollapseButton] }) })), topPanes.length > 0 && (jsx("div", { className: classes.paneContent, children: topSelectedTab && (jsxs(Fragment, { children: [jsx(PaneHeader, { id: topSelectedTab.key, title: topSelectedTab.title, dockOptions: validTopDockOptions }), topPanes.map((pane) => (jsx("div", { className: mergeClasses(classes.paneContent, pane.key !== topSelectedTab.key ? classes.unselectedPane : undefined), children: jsx(pane.content, {}) }, pane.key)))] })) })), topPanes.length > 0 && bottomPanes.length > 0 && jsx(Divider, { ref: paneVerticalResizeHandleRef, className: classes.paneDivider }), bottomPanes.length > 1 && (jsx(Fragment, { children: jsx("div", { className: classes.barDiv, children: bottomPaneTabList }) })), bottomPanes.length > 0 && (jsx("div", { ref: paneVerticalResizeElementRef, className: classes.paneContent, style: { height: `clamp(200px, calc(45% + var(${paneHeightAdjustCSSVar}, 0px)), 100% - 300px)`, flex: "0 0 auto" }, children: bottomSelectedTab && (jsxs(Fragment, { children: [jsx(PaneHeader, { id: bottomSelectedTab.key, title: bottomSelectedTab.title, dockOptions: validBottomDockOptions }), bottomPanes.map((pane) => (jsx("div", { className: mergeClasses(classes.paneContent, pane.key !== bottomSelectedTab.key ? classes.unselectedPane : undefined), children: jsx(pane.content, {}) }, pane.key)))] })) })), toolbarMode === "compact" && bottomBarItems.length > 0 && (jsx(Fragment, { children: jsx("div", { className: classes.barDiv, children: jsx(Toolbar, { location: "bottom", components: bottomBarItems }) }) }))] }));
2026
2169
  }, [
2027
2170
  topPanes,
2028
2171
  topSelectedTab,
@@ -2034,25 +2177,15 @@ function usePane(location, layoutMode, defaultWidth, minWidth, sidePanes, onSele
2034
2177
  bottomBarItems,
2035
2178
  topPaneTabList,
2036
2179
  bottomPaneTabList,
2037
- undocked,
2180
+ isChildWindowOpen,
2038
2181
  ]);
2039
2182
  // This deals with docked vs undocked state, where undocked is rendered into a separate window via a portal.
2040
2183
  const pane = useMemo(() => {
2041
- if (!windowState) {
2042
- // If there is no window state, then we are docked, so render the resizable div and the collapse container.
2043
- return (jsx("div", { ref: paneContainerRef, className: mergeClasses(classes.paneContainer, layoutMode === "inline"
2044
- ? undefined
2045
- : mergeClasses(classes.paneContainerOverlay, location === "left" ? classes.paneContainerOverlayLeft : classes.paneContainerOverlayRight)), children: (topPanes.length > 0 || bottomPanes.length > 0) && (jsxs("div", { className: `${classes.pane} ${location === "left" ? classes.paneLeft : classes.paneRight}`, children: [jsx(Collapse, { orientation: "horizontal", visible: !collapsed, children: jsx("div", { ref: paneHorizontalResizeElementRef, className: classes.paneContainer, style: { width: `clamp(${minWidth}px, calc(${defaultWidth}px + var(${paneWidthAdjustCSSVar}, 0px)), 1000px)` }, children: corePane }) }), jsx("div", { ref: paneHorizontalResizeHandleRef, className: `${classes.resizer} ${location === "left" ? classes.resizerLeft : classes.resizerRight}`, style: { pointerEvents: `${collapsed ? "none" : "auto"}` } })] })) }));
2046
- }
2047
- else {
2048
- // Otherwise we are undocked, so render into the portal that targets the body of the child window.
2049
- const { mountNode, renderer } = windowState;
2050
- return (
2051
- // Portal targets the body of the child window.
2052
- jsx(Portal, { mountNode: mountNode, children: jsx(RendererProvider, { renderer: renderer, targetDocument: mountNode.ownerDocument, children: jsx(Theme, { className: classes.paneContent, style: { minWidth }, targetDocument: mountNode.ownerDocument, children: corePane }) }) }));
2053
- }
2054
- }, [collapsed, corePane, windowState]);
2055
- return [topPaneTabList, pane, collapsed, setCollapsed, undocked, setUndocked];
2184
+ return (jsxs(Fragment, { children: [!isChildWindowOpen && (jsx("div", { ref: paneContainerRef, className: mergeClasses(classes.paneContainer, layoutMode === "inline"
2185
+ ? undefined
2186
+ : mergeClasses(classes.paneContainerOverlay, location === "left" ? classes.paneContainerOverlayLeft : classes.paneContainerOverlayRight)), children: (topPanes.length > 0 || bottomPanes.length > 0) && (jsxs("div", { className: `${classes.pane} ${location === "left" ? classes.paneLeft : classes.paneRight}`, children: [jsx(Collapse, { orientation: "horizontal", visible: !collapsed, children: jsx("div", { ref: paneHorizontalResizeElementRef, className: classes.paneContainer, style: { width: `clamp(${minWidth}px, calc(${defaultWidth}px + var(${paneWidthAdjustCSSVar}, 0px)), 1000px)` }, children: corePane }) }), jsx("div", { ref: paneHorizontalResizeHandleRef, className: `${classes.resizer} ${location === "left" ? classes.resizerLeft : classes.resizerRight}`, style: { pointerEvents: `${collapsed ? "none" : "auto"}` } })] })) })), jsx(ChildWindow, { imperativeRef: childWindow, onOpenChange: (isOpen) => setIsChildWindowOpen(isOpen), children: corePane })] }));
2187
+ }, [collapsed, corePane]);
2188
+ return [topPaneTabList, pane, collapsed, setCollapsed, isChildWindowOpen, setUndocked];
2056
2189
  }
2057
2190
  function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWidth = 350, rightPaneDefaultWidth = 350, rightPaneMinWidth = 350, toolbarMode = "full", sidePaneRemapper = undefined, layoutMode = "inline", } = {}) {
2058
2191
  return {
@@ -2077,7 +2210,7 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
2077
2210
  undock: () => onDockChanged.notifyObservers({ location: "right", dock: false }),
2078
2211
  };
2079
2212
  const rootComponent = () => {
2080
- const classes = useStyles$e();
2213
+ const classes = useStyles$l();
2081
2214
  const [sidePaneDockOverrides, setSidePaneDockOverrides] = useSidePaneDockOverrides();
2082
2215
  // This function returns a promise that resolves after the dock change takes effect so that
2083
2216
  // we can then select the re-docked pane.
@@ -2513,7 +2646,7 @@ function useCommandContextMenuState(commands) {
2513
2646
  return [checkedContextMenuItems, onContextMenuCheckedValueChange, contextMenuItems];
2514
2647
  }
2515
2648
  // eslint-disable-next-line @typescript-eslint/naming-convention
2516
- const useStyles$d = makeStyles({
2649
+ const useStyles$k = makeStyles({
2517
2650
  rootDiv: {
2518
2651
  flex: 1,
2519
2652
  overflow: "hidden",
@@ -2581,14 +2714,14 @@ function MakeInlineCommandElement(command, isPlaceholder) {
2581
2714
  }
2582
2715
  const SceneTreeItem = (props) => {
2583
2716
  const { isSelected, select } = props;
2584
- const classes = useStyles$d();
2717
+ const classes = useStyles$k();
2585
2718
  const [compactMode] = useCompactMode();
2586
2719
  const treeItemLayoutClass = mergeClasses(classes.sceneTreeItemLayout, compactMode ? classes.treeItemLayoutCompact : undefined);
2587
2720
  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"));
2588
2721
  };
2589
2722
  const SectionTreeItem = (props) => {
2590
2723
  const { section, isFiltering, commandProviders, expandAll, collapseAll } = props;
2591
- const classes = useStyles$d();
2724
+ const classes = useStyles$k();
2592
2725
  const [compactMode] = useCompactMode();
2593
2726
  // Get the commands that apply to this section.
2594
2727
  const commands = useResource(useCallback(() => {
@@ -2605,7 +2738,7 @@ const SectionTreeItem = (props) => {
2605
2738
  };
2606
2739
  const EntityTreeItem = (props) => {
2607
2740
  const { entityItem, isSelected, select, isFiltering, commandProviders, expandAll, collapseAll } = props;
2608
- const classes = useStyles$d();
2741
+ const classes = useStyles$k();
2609
2742
  const [compactMode] = useCompactMode();
2610
2743
  const hasChildren = !!entityItem.children?.length;
2611
2744
  const displayInfo = useResource(useCallback(() => {
@@ -2698,7 +2831,7 @@ const EntityTreeItem = (props) => {
2698
2831
  }, 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] }) })] }));
2699
2832
  };
2700
2833
  const SceneExplorer = (props) => {
2701
- const classes = useStyles$d();
2834
+ const classes = useStyles$k();
2702
2835
  const { sections, entityCommandProviders, sectionCommandProviders, scene, selectedEntity } = props;
2703
2836
  const [openItems, setOpenItems] = useState(new Set());
2704
2837
  const [sceneVersion, setSceneVersion] = useState(0);
@@ -3493,14 +3626,14 @@ const TextPropertyLine = (props) => {
3493
3626
  return (jsx(PropertyLine, { ...props, children: jsx(Body1, { title: title, children: value }) }));
3494
3627
  };
3495
3628
 
3496
- const useStyles$c = makeStyles({
3629
+ const useStyles$j = makeStyles({
3497
3630
  pinnedStatsPane: {
3498
3631
  flex: "0 1 auto",
3499
3632
  paddingBottom: tokens.spacingHorizontalM,
3500
3633
  },
3501
3634
  });
3502
3635
  const StatsPane = (props) => {
3503
- const classes = useStyles$c();
3636
+ const classes = useStyles$j();
3504
3637
  const scene = props.context;
3505
3638
  const engine = scene.getEngine();
3506
3639
  const fps = useObservableState(() => Math.round(engine.getFps()), engine.onBeginFrameObservable);
@@ -3678,7 +3811,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3678
3811
  keywords: ["export", "gltf", "glb", "babylon", "exporter", "tools"],
3679
3812
  ...BabylonWebResources,
3680
3813
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3681
- getExtensionModuleAsync: async () => await import('./exportService-LaUVRgd_.js'),
3814
+ getExtensionModuleAsync: async () => await import('./exportService-D1VDR7BX.js'),
3682
3815
  },
3683
3816
  {
3684
3817
  name: "Capture Tools",
@@ -3686,7 +3819,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3686
3819
  keywords: ["capture", "screenshot", "gif", "video", "tools"],
3687
3820
  ...BabylonWebResources,
3688
3821
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3689
- getExtensionModuleAsync: async () => await import('./captureService-CbpbBn5F.js'),
3822
+ getExtensionModuleAsync: async () => await import('./captureService-B8vCTKYD.js'),
3690
3823
  },
3691
3824
  {
3692
3825
  name: "Import Tools",
@@ -3694,7 +3827,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3694
3827
  keywords: ["import", "tools"],
3695
3828
  ...BabylonWebResources,
3696
3829
  author: { name: "Alex Chuber", forumUserName: "alexchuber" },
3697
- getExtensionModuleAsync: async () => await import('./importService-B98QFvNM.js'),
3830
+ getExtensionModuleAsync: async () => await import('./importService-BPzwIgjH.js'),
3698
3831
  },
3699
3832
  {
3700
3833
  name: "Quick Creation Tools (Preview)",
@@ -3702,7 +3835,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
3702
3835
  keywords: ["creation", "tools"],
3703
3836
  ...BabylonWebResources,
3704
3837
  author: { name: "Babylon.js", forumUserName: "" },
3705
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-Bg2plbI-.js'),
3838
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-DEdXDqIl.js'),
3706
3839
  },
3707
3840
  ]);
3708
3841
 
@@ -3920,7 +4053,7 @@ const Dropdown = (props) => {
3920
4053
  const NumberDropdown = Dropdown;
3921
4054
  const StringDropdown = Dropdown;
3922
4055
 
3923
- const useStyles$b = makeStyles({
4056
+ const useStyles$i = makeStyles({
3924
4057
  surface: {
3925
4058
  maxWidth: "400px",
3926
4059
  },
@@ -3932,16 +4065,16 @@ const useStyles$b = makeStyles({
3932
4065
  minWidth: "300px",
3933
4066
  },
3934
4067
  });
3935
- const Popover = (props) => {
4068
+ const Popover = forwardRef((props, ref) => {
3936
4069
  const { children } = props;
3937
4070
  const [popoverOpen, setPopoverOpen] = useState(false);
3938
- const classes = useStyles$b();
4071
+ const classes = useStyles$i();
3939
4072
  return (jsxs(Popover$1, { open: popoverOpen, onOpenChange: (_, data) => setPopoverOpen(data.open), positioning: {
3940
4073
  align: "start",
3941
4074
  overflowBoundary: document.body,
3942
4075
  autoSize: true,
3943
- }, trapFocus: true, children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: props.trigger ?? jsx(Button, { icon: props.icon, onClick: () => setPopoverOpen(true) }) }), jsx(PopoverSurface, { className: classes.surface, children: jsx("div", { className: classes.content, children: children }) })] }));
3944
- };
4076
+ }, trapFocus: true, children: [jsx(PopoverTrigger, { disableButtonEnhancement: true, children: props.trigger ?? jsx(Button, { ref: ref, icon: props.icon, onClick: () => setPopoverOpen(true) }) }), jsx(PopoverSurface, { className: classes.surface, children: jsx("div", { className: classes.content, children: children }) })] }));
4077
+ });
3945
4078
 
3946
4079
  const useColorPickerStyles = makeStyles({
3947
4080
  container: {
@@ -3990,28 +4123,29 @@ const useColorPickerStyles = makeStyles({
3990
4123
  gap: tokens.spacingVerticalSNudge, // 6px
3991
4124
  },
3992
4125
  });
3993
- const ColorPickerPopup = (props) => {
4126
+ const ColorPickerPopup = forwardRef((props, ref) => {
3994
4127
  ColorPickerPopup.displayName = "ColorPickerPopup";
4128
+ const { value, onChange, isLinearMode, ...rest } = props;
3995
4129
  const classes = useColorPickerStyles();
3996
- const [color, setColor] = useState(props.value);
3997
- const [isLinear, setIsLinear] = useState(props.isLinearMode ?? false);
4130
+ const [color, setColor] = useState(value);
4131
+ const [isLinear, setIsLinear] = useState(isLinearMode ?? false);
3998
4132
  const [isFloat, setFloat] = useState(false);
3999
4133
  const { size } = useContext(ToolContext);
4000
4134
  useEffect(() => {
4001
- setColor(props.value); // Ensures the trigger color updates when props.value changes
4002
- }, [props.value]);
4135
+ setColor(value); // Ensures the trigger color updates when props.value changes
4136
+ }, [value]);
4003
4137
  const handleColorPickerChange = (_, data) => {
4004
4138
  let color = Color3.FromHSV(data.color.h, data.color.s, data.color.v);
4005
- if (props.value instanceof Color4) {
4139
+ if (value instanceof Color4) {
4006
4140
  color = Color4.FromColor3(color, data.color.a ?? 1);
4007
4141
  }
4008
4142
  handleChange(color);
4009
4143
  };
4010
4144
  const handleChange = (newColor) => {
4011
4145
  setColor(newColor);
4012
- props.onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
4146
+ onChange(newColor); // Ensures the parent is notified when color changes from within colorPicker
4013
4147
  };
4014
- return (jsx(Popover, { trigger: jsx(ColorSwatch, { 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: {
4148
+ return (jsx(Popover, { trigger: jsx(ColorSwatch, { 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: {
4015
4149
  label: "Color Space",
4016
4150
  info: jsx(Body1, { children: "Today this is not mutable as the color space is determined by the entity. Soon we will allow swapping" }),
4017
4151
  }, options: [
@@ -4024,7 +4158,7 @@ const ColorPickerPopup = (props) => {
4024
4158
  { label: "Int", value: 0 },
4025
4159
  { label: "Float", value: 1 },
4026
4160
  ], 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 }) })] }) }));
4027
- };
4161
+ });
4028
4162
  /**
4029
4163
  * Component which displays the passed in color's HEX value, either in linearSpace (if linearHex is true) or in gamma space
4030
4164
  * When the hex color is changed by user, component calculates the new Color3/4 value and calls onChange
@@ -4143,7 +4277,7 @@ const ColorPropertyLine = forwardRef((props, ref) => {
4143
4277
  const Color3PropertyLine = ColorPropertyLine;
4144
4278
  const Color4PropertyLine = ColorPropertyLine;
4145
4279
 
4146
- const useStyles$a = makeStyles({
4280
+ const useStyles$h = makeStyles({
4147
4281
  dropdown: {
4148
4282
  ...UniformWidthStyling,
4149
4283
  },
@@ -4155,7 +4289,7 @@ const useStyles$a = makeStyles({
4155
4289
  */
4156
4290
  const DropdownPropertyLine = forwardRef((props, ref) => {
4157
4291
  DropdownPropertyLine.displayName = "DropdownPropertyLine";
4158
- const classes = useStyles$a();
4292
+ const classes = useStyles$h();
4159
4293
  return (jsx(PropertyLine, { ...props, ref: ref, children: jsx(Dropdown, { ...props, className: classes.dropdown }) }));
4160
4294
  });
4161
4295
  /**
@@ -4729,7 +4863,7 @@ class ServiceContainer {
4729
4863
  }
4730
4864
  }
4731
4865
 
4732
- const useStyles$9 = makeStyles({
4866
+ const useStyles$g = makeStyles({
4733
4867
  themeButton: {
4734
4868
  margin: 0,
4735
4869
  },
@@ -4748,7 +4882,7 @@ const ThemeSelectorServiceDefinition = {
4748
4882
  suppressTeachingMoment: true,
4749
4883
  order: -300,
4750
4884
  component: () => {
4751
- const classes = useStyles$9();
4885
+ const classes = useStyles$g();
4752
4886
  const { isDarkMode, themeMode, setThemeMode } = useThemeMode();
4753
4887
  const onSelectedThemeChange = useCallback((e, data) => {
4754
4888
  setThemeMode(data.checkedItems.includes("System") ? "system" : data.checkedItems[0].toLocaleLowerCase());
@@ -4766,7 +4900,7 @@ const ThemeSelectorServiceDefinition = {
4766
4900
  };
4767
4901
 
4768
4902
  // eslint-disable-next-line @typescript-eslint/naming-convention
4769
- const useStyles$8 = makeStyles({
4903
+ const useStyles$f = makeStyles({
4770
4904
  app: {
4771
4905
  colorScheme: "light dark",
4772
4906
  flexGrow: 1,
@@ -4802,7 +4936,7 @@ function MakeModularTool(options) {
4802
4936
  SetThemeMode(themeMode);
4803
4937
  }
4804
4938
  const modularToolRootComponent = () => {
4805
- const classes = useStyles$8();
4939
+ const classes = useStyles$f();
4806
4940
  const [extensionManagerContext, setExtensionManagerContext] = useState();
4807
4941
  const [requiredExtensions, setRequiredExtensions] = useState();
4808
4942
  const [requiredExtensionsDeferred, setRequiredExtensionsDeferred] = useState();
@@ -4828,7 +4962,7 @@ function MakeModularTool(options) {
4828
4962
  });
4829
4963
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
4830
4964
  if (extensionFeeds.length > 0) {
4831
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-CwnEf0dV.js');
4965
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-DS-uWdsz.js');
4832
4966
  await serviceContainer.addServiceAsync(ExtensionListServiceDefinition);
4833
4967
  }
4834
4968
  // Register the theme selector service (for selecting the theme) if theming is configured.
@@ -5010,7 +5144,7 @@ const MeshIcon = createFluentIcon("Mesh", "16", '<path d="M14.03,3.54l-5.11-2.07
5010
5144
  const TranslateIcon = createFluentIcon("Translate", "24", '<path d="M20.16,12.98l-2.75-2.75c-.29-.29-.77-.29-1.06,0-.29.29-.29.77,0,1.06l1.47,1.47h-6.69v-6.69l1.47,1.47c.29.29.77.29,1.06,0,.29-.29.29-.77,0-1.06l-2.75-2.75c-.14-.14-.33-.22-.53-.22s-.39.08-.53.22l-2.75,2.75c-.29.29-.29.77,0,1.06.29.29.77.29,1.06,0l1.47-1.47v7.13l-3.52,3.52v-2.08c0-.41-.34-.75-.75-.75s-.75.34-.75.75v3.89c0,.2.08.39.22.53.14.14.33.22.53.22h3.89c.41,0,.75-.34.75-.75s-.34-.75-.75-.75h-2.08s3.52-3.52,3.52-3.52h7.13l-1.47,1.47c-.29.29-.29.77,0,1.06s.77.29,1.06,0l2.75-2.75c.14-.14.22-.33.22-.53s-.08-.39-.22-.53Z" />');
5011
5145
  const MaterialIcon = createFluentIcon("Material", "16", '<path d="M14.74,6.3c-.09-.36-.38-.64-.75-.72-.04-.09-.08-.18-.12-.27.1-.15.16-.32.16-.51,0-.18-.05-.34-.13-.48-1.23-1.97-3.41-3.28-5.9-3.28C4.16,1.04,1.04,4.16,1.04,7.99c0,.39.23.72.57.88.02.12.03.25.06.37-.18.18-.3.42-.3.7,0,.11.02.21.06.31.94,2.74,3.53,4.71,6.58,4.71,3.84,0,6.96-3.12,6.96-6.96,0-.59-.08-1.16-.22-1.7ZM2.07,8.58c-.02-.19-.03-.39-.03-.58,0-3.29,2.67-5.96,5.96-5.96,2.23,0,4.17,1.23,5.2,3.05.05.18-.07.45-.3.75-.57-.73-1.45-1.21-2.45-1.21-1.72,0-3.12,1.4-3.12,3.11,0,.33.07.65.16.95-3.05.82-5.17.52-5.42-.11ZM12.56,7.75c0,1.17-.95,2.11-2.11,2.11s-2.12-.95-2.12-2.11.95-2.11,2.12-2.11,2.11.95,2.11,2.11ZM8,13.96c-2.6,0-4.81-1.68-5.62-4.01.5.16,1.11.24,1.79.24,1.15,0,2.49-.22,3.79-.59.57.76,1.47,1.26,2.49,1.26,1.72,0,3.11-1.4,3.11-3.11,0-.34-.07-.65-.17-.96.13-.13.24-.26.34-.39.14.51.22,1.04.22,1.6,0,3.29-2.67,5.96-5.96,5.96Z"/>');
5012
5146
 
5013
- const useStyles$7 = makeStyles({
5147
+ const useStyles$e = makeStyles({
5014
5148
  coordinatesModeButton: {
5015
5149
  margin: `0 0 0 ${tokens.spacingHorizontalXS}`,
5016
5150
  },
@@ -5020,7 +5154,7 @@ const useStyles$7 = makeStyles({
5020
5154
  });
5021
5155
  const GizmoToolbar = (props) => {
5022
5156
  const { scene, entity, gizmoService } = props;
5023
- const classes = useStyles$7();
5157
+ const classes = useStyles$e();
5024
5158
  const gizmoManager = useResource(useCallback(() => {
5025
5159
  const utilityLayerRef = gizmoService.getUtilityLayer(scene);
5026
5160
  const keepDepthUtilityLayerRef = gizmoService.getUtilityLayer(scene, "keepDepth");
@@ -5135,7 +5269,7 @@ const GizmoToolbarServiceDefinition = {
5135
5269
  },
5136
5270
  };
5137
5271
 
5138
- const useStyles$6 = makeStyles({
5272
+ const useStyles$d = makeStyles({
5139
5273
  badge: {
5140
5274
  margin: tokens.spacingHorizontalXXS,
5141
5275
  fontFamily: "monospace",
@@ -5151,7 +5285,7 @@ const MiniStatsServiceDefinition = {
5151
5285
  horizontalLocation: "right",
5152
5286
  suppressTeachingMoment: true,
5153
5287
  component: () => {
5154
- const classes = useStyles$6();
5288
+ const classes = useStyles$d();
5155
5289
  const scene = useObservableState(useCallback(() => sceneContext.currentScene, [sceneContext.currentScene]), sceneContext.currentSceneObservable);
5156
5290
  const engine = scene?.getEngine();
5157
5291
  const fps = useObservableState(useCallback(() => (engine ? Math.round(engine.getFps()) : null), [engine]), engine?.onBeginFrameObservable);
@@ -6121,6 +6255,95 @@ const PBRBaseMaterialSheenProperties = (props) => {
6121
6255
  } }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Roughness", target: material.sheen, propertyKey: "_useRoughness" }), jsx(Collapse, { visible: useRoughness, children: jsx(BoundProperty, { nullable: true, component: SyncedSliderPropertyLine, label: "Roughness", target: material.sheen, propertyKey: "roughness", defaultValue: 0, min: 0, max: 1, step: 0.01 }) }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use Roughness from Main Texture", target: material.sheen, propertyKey: "useRoughnessFromMainTexture" }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Albedo Scaling", target: material.sheen, propertyKey: "albedoScaling" })] })] }));
6122
6256
  };
6123
6257
 
6258
+ const useStyles$c = makeStyles({
6259
+ root: {
6260
+ display: "grid",
6261
+ gridTemplateRows: "repeat(1fr)",
6262
+ justifyItems: "start",
6263
+ gap: "2px",
6264
+ maxWidth: "400px",
6265
+ },
6266
+ comboBox: {
6267
+ width: CustomTokens.inputWidth,
6268
+ minWidth: CustomTokens.inputWidth,
6269
+ boxSizing: "border-box",
6270
+ },
6271
+ input: {
6272
+ minWidth: 0,
6273
+ },
6274
+ });
6275
+ /**
6276
+ * Wrapper around a Fluent ComboBox that allows for filtering options.
6277
+ * @param props
6278
+ * @returns
6279
+ */
6280
+ const ComboBox = (props) => {
6281
+ ComboBox.displayName = "ComboBox";
6282
+ const comboId = useId();
6283
+ const styles = useStyles$c();
6284
+ const { size } = useContext(ToolContext);
6285
+ // Find the label for the current value
6286
+ const getLabel = (value) => props.options.find((opt) => opt.value === value)?.label ?? "";
6287
+ const [query, setQuery] = useState(getLabel(props.value ?? ""));
6288
+ useEffect(() => {
6289
+ setQuery(getLabel(props.value ?? ""));
6290
+ }, [props.value, props.options]);
6291
+ // Convert to Fluent's { children, value } format
6292
+ const normalizedOptions = props.options.map((opt) => ({ children: opt.label, value: opt.value }));
6293
+ const children = useComboboxFilter(query, normalizedOptions, {
6294
+ noOptionsMessage: "No items match your search.",
6295
+ optionToReactKey: (option) => option.value,
6296
+ optionToText: (option) => option.children,
6297
+ renderOption: (option) => (jsx(Option, { value: option.value, text: option.children, children: option.children }, option.value)),
6298
+ });
6299
+ const onOptionSelect = (_e, data) => {
6300
+ setQuery(data.optionText ?? "");
6301
+ data.optionValue && props.onChange(data.optionValue);
6302
+ };
6303
+ return (jsxs("div", { className: styles.root, children: [jsx("label", { id: comboId, children: props.label }), jsx(Combobox, { size: size, root: { className: styles.comboBox }, input: { className: styles.input }, onOptionSelect: onOptionSelect, "aria-labelledby": comboId, placeholder: "Search..", onChange: (ev) => setQuery(ev.target.value), value: query, children: children })] }));
6304
+ };
6305
+
6306
+ /**
6307
+ * A generic primitive component with a ComboBox for selecting from a list of entities.
6308
+ * Supports entities with duplicate names by using uniqueId for identity.
6309
+ * @param props ChooseEntityProps
6310
+ * @returns EntitySelector component
6311
+ */
6312
+ function EntitySelector(props) {
6313
+ const { value, onChange, getEntities, getName, filter } = props;
6314
+ // Build options with uniqueId as key
6315
+ const options = useMemo(() => {
6316
+ return getEntities()
6317
+ .filter((e) => e.uniqueId !== undefined && (!filter || filter(e)))
6318
+ .map((entity) => ({
6319
+ label: getName(entity),
6320
+ value: entity.uniqueId.toString(),
6321
+ }))
6322
+ .sort((a, b) => a.label.localeCompare(b.label));
6323
+ }, [getEntities, getName, filter]);
6324
+ const handleEntitySelect = (key) => {
6325
+ const entity = getEntities().find((e) => e.uniqueId.toString() === key);
6326
+ onChange(entity ?? null);
6327
+ };
6328
+ // Get current entity key for display
6329
+ const currentKey = value ? value.uniqueId.toString() : "";
6330
+ return jsx(ComboBox, { label: "", options: options, value: currentKey, onChange: handleEntitySelect });
6331
+ }
6332
+ EntitySelector.displayName = "EntitySelector";
6333
+
6334
+ /**
6335
+ * A primitive component with a ComboBox for selecting from existing scene materials.
6336
+ * @param props MaterialSelectorProps
6337
+ * @returns MaterialSelector component
6338
+ */
6339
+ const MaterialSelector = (props) => {
6340
+ MaterialSelector.displayName = "MaterialSelector";
6341
+ const { scene, ...rest } = props;
6342
+ const getMaterials = useCallback(() => scene.materials, [scene.materials]);
6343
+ const getName = useCallback((material) => material.name, []);
6344
+ return jsx(EntitySelector, { ...rest, getEntities: getMaterials, getName: getName });
6345
+ };
6346
+
6124
6347
  /**
6125
6348
  * A button that uploads a file and either:
6126
6349
  * - Updates an existing Texture or CubeTexture via updateURL (if texture prop is provided)
@@ -6179,50 +6402,7 @@ const TextureUpload = (props) => {
6179
6402
  return jsx(UploadButton, { onUpload: handleUpload, accept: accept, title: "Upload Texture", label: label });
6180
6403
  };
6181
6404
 
6182
- const useStyles$5 = makeStyles({
6183
- root: {
6184
- // Stack the label above the field with a gap
6185
- display: "grid",
6186
- gridTemplateRows: "repeat(1fr)",
6187
- justifyItems: "start",
6188
- gap: "2px",
6189
- maxWidth: "400px",
6190
- },
6191
- comboBox: {
6192
- width: CustomTokens.inputWidth,
6193
- minWidth: CustomTokens.inputWidth,
6194
- boxSizing: "border-box",
6195
- },
6196
- input: {
6197
- minWidth: 0, // Override Fluent's default minWidth on the input
6198
- },
6199
- });
6200
- /**
6201
- * Wrapper around a Fluent ComboBox that allows for filtering options
6202
- * @param props
6203
- * @returns
6204
- */
6205
- const ComboBox = (props) => {
6206
- ComboBox.displayName = "ComboBox";
6207
- const comboId = useId();
6208
- const styles = useStyles$5();
6209
- const { size } = useContext(ToolContext);
6210
- const [query, setQuery] = useState(props.value ?? "");
6211
- // Sync query with props.value when it changes externally
6212
- useEffect(() => {
6213
- setQuery(props.value ?? "");
6214
- }, [props.value]);
6215
- const children = useComboboxFilter(query, props.options, {
6216
- noOptionsMessage: "No items match your search.",
6217
- });
6218
- const onOptionSelect = (_e, data) => {
6219
- setQuery(data.optionText ?? "");
6220
- data.optionText && props.onChange(data.optionText);
6221
- };
6222
- return (jsxs("div", { className: styles.root, children: [jsx("label", { id: comboId, children: props.label }), jsx(Combobox, { size: size, root: { className: styles.comboBox }, input: { className: styles.input }, onOptionSelect: onOptionSelect, onBlur: () => props.onChange(query), "aria-labelledby": comboId, placeholder: "Search..", onChange: (ev) => setQuery(ev.target.value), value: query, children: children })] }));
6223
- };
6224
-
6225
- const useStyles$4 = makeStyles({
6405
+ const useStyles$b = makeStyles({
6226
6406
  container: {
6227
6407
  display: "flex",
6228
6408
  flexDirection: "row",
@@ -6233,55 +6413,21 @@ const useStyles$4 = makeStyles({
6233
6413
  /**
6234
6414
  * A primitive component with a ComboBox for selecting from existing scene textures
6235
6415
  * and a button for uploading new texture files.
6236
- * @param props ChooseTextureProps
6237
- * @returns ChooseTexture component
6416
+ * @param props TextureSelectorProps
6417
+ * @returns TextureSelector component
6238
6418
  */
6239
- const ChooseTexture = (props) => {
6240
- ChooseTexture.displayName = "ChooseTexture";
6419
+ const TextureSelector = (props) => {
6420
+ TextureSelector.displayName = "TextureSelector";
6241
6421
  const { scene, cubeOnly, value, onChange } = props;
6242
- const classes = useStyles$4();
6243
- // Get sorted texture names from scene
6244
- const textureOptions = useMemo(() => {
6245
- return scene.textures
6246
- .filter((t) => t.name && (!cubeOnly || t.isCube))
6247
- .map((t) => t.displayName || t.name)
6248
- .sort((a, b) => a.localeCompare(b));
6249
- }, [scene.textures, cubeOnly]);
6250
- const handleTextureSelect = (textureName) => {
6251
- const texture = scene.textures.find((t) => (t.displayName || t.name) === textureName);
6252
- onChange(texture ?? null);
6253
- };
6254
- // Get current texture name for initial display
6255
- const currentTextureName = value ? value.displayName || value.name : "";
6256
- return (jsxs("div", { className: classes.container, children: [jsx(ComboBox, { label: "", options: textureOptions, value: currentTextureName, onChange: handleTextureSelect }), jsx(TextureUpload, { scene: scene, onChange: onChange, cubeOnly: cubeOnly })] }));
6257
- };
6258
-
6259
- /**
6260
- * A property line with a ComboBox for selecting from existing scene textures
6261
- * and a button for uploading new texture files.
6262
- * @param props - ChooseTextureProps & PropertyLineProps
6263
- * @returns property-line wrapped ChooseTexture component
6264
- */
6265
- const ChooseTexturePropertyLine = (props) => {
6266
- ChooseTexturePropertyLine.displayName = "ChooseTexturePropertyLine";
6267
- return (jsx(PropertyLine, { ...props, children: jsx(ChooseTexture, { ...props }) }));
6422
+ const classes = useStyles$b();
6423
+ const getTextures = useCallback(() => scene.textures, [scene.textures]);
6424
+ const getName = useCallback((texture) => texture.displayName || texture.name, []);
6425
+ const filter = useCallback((texture) => !cubeOnly || texture.isCube, [cubeOnly]);
6426
+ return (jsxs("div", { className: classes.container, children: [jsx(EntitySelector, { value: value, onChange: onChange, getEntities: getTextures, getName: getName, filter: filter }), jsx(TextureUpload, { scene: scene, onChange: onChange, cubeOnly: cubeOnly })] }));
6268
6427
  };
6269
6428
 
6270
- /**
6271
- * Helper to bind texture properties without needing defaultValue
6272
- * @param props - The required properties
6273
- * @returns ChooseTexturePropertyLine component
6274
- */
6275
- function BoundTextureProperty(props) {
6276
- const { label, target, propertyKey, scene, cubeOnly } = props;
6277
- const value = useProperty(target, propertyKey);
6278
- const notifyPropertyChanged = usePropertyChangedNotifier();
6279
- return (jsx(ChooseTexturePropertyLine, { label: label, value: value, onChange: (texture) => {
6280
- const oldValue = target[propertyKey];
6281
- target[propertyKey] = texture;
6282
- notifyPropertyChanged(target, propertyKey, oldValue, texture);
6283
- }, scene: scene, cubeOnly: cubeOnly }));
6284
- }
6429
+ const MaterialSelectorPropertyLine = (props) => jsx(PropertyLine, { ...props, children: jsx(MaterialSelector, { ...props }) });
6430
+ const TextureSelectorPropertyLine = (props) => jsx(PropertyLine, { ...props, children: jsx(TextureSelector, { ...props }) });
6285
6431
 
6286
6432
  /**
6287
6433
  * Displays the lighting and color properties of a PBR material.
@@ -6300,7 +6446,7 @@ const PBRMaterialLightingAndColorProperties = (props) => {
6300
6446
  const PBRMaterialTextureProperties = (props) => {
6301
6447
  const { material } = props;
6302
6448
  const scene = material.getScene();
6303
- return (jsxs(Fragment, { children: [jsx(BoundTextureProperty, { label: "Albedo", target: material, propertyKey: "albedoTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Base Weight", target: material, propertyKey: "baseWeightTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughnessTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Metallic Roughness", target: material, propertyKey: "metallicTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Reflection", target: material, propertyKey: "reflectionTexture", scene: scene, cubeOnly: true }), jsx(BoundTextureProperty, { label: "Refraction", target: material, propertyKey: "refractionTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Reflectivity", target: material, propertyKey: "reflectivityTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Micro-surface", target: material, propertyKey: "microSurfaceTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Bump", target: material, propertyKey: "bumpTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Emissive", target: material, propertyKey: "emissiveTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Opacity", target: material, propertyKey: "opacityTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Ambient", target: material, propertyKey: "ambientTexture", scene: scene }), jsx(BoundTextureProperty, { label: "Lightmap", target: material, propertyKey: "lightmapTexture", scene: scene })] }));
6449
+ return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Albedo", target: material, propertyKey: "albedoTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Weight", target: material, propertyKey: "baseWeightTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Base Diffuse Roughness", target: material, propertyKey: "baseDiffuseRoughnessTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Metallic Roughness", target: material, propertyKey: "metallicTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Reflection", target: material, propertyKey: "reflectionTexture", scene: scene, cubeOnly: true, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Refraction", target: material, propertyKey: "refractionTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Reflectivity", target: material, propertyKey: "reflectivityTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Micro-surface", target: material, propertyKey: "microSurfaceTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Bump", target: material, propertyKey: "bumpTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Emissive", target: material, propertyKey: "emissiveTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Opacity", target: material, propertyKey: "opacityTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Ambient", target: material, propertyKey: "ambientTexture", scene: scene, defaultValue: null }), jsx(BoundProperty, { component: TextureSelectorPropertyLine, label: "Lightmap", target: material, propertyKey: "lightmapTexture", scene: scene, defaultValue: null })] }));
6304
6450
  };
6305
6451
 
6306
6452
  // TODO: ryamtrem / gehalper This function is temporal until there is a line control to handle texture links (similar to the old TextureLinkLineComponent)
@@ -6800,7 +6946,7 @@ function SaveMetadata(entity, metadata) {
6800
6946
  entity.metadata = metadata;
6801
6947
  }
6802
6948
  }
6803
- const useStyles$3 = makeStyles({
6949
+ const useStyles$a = makeStyles({
6804
6950
  mainDiv: {
6805
6951
  display: "flex",
6806
6952
  flexDirection: "column",
@@ -6820,7 +6966,7 @@ const useStyles$3 = makeStyles({
6820
6966
  */
6821
6967
  const MetadataProperties = (props) => {
6822
6968
  const { entity } = props;
6823
- const classes = useStyles$3();
6969
+ const classes = useStyles$a();
6824
6970
  const { size } = useContext(ToolContext);
6825
6971
  const metadata = useProperty(entity, "metadata");
6826
6972
  const stringifiedMetadata = useMemo(() => StringifyMetadata(metadata) ?? "", [metadata]);
@@ -6875,7 +7021,7 @@ const AbstractMeshGeneralProperties = (props) => {
6875
7021
  const isAnInstance = useProperty(mesh, "isAnInstance");
6876
7022
  // TODO: Handle case where array is mutated
6877
7023
  const subMeshes = useProperty(mesh, "subMeshes");
6878
- return (jsxs(Fragment, { children: [jsx(StringifiedPropertyLine, { label: "Vertices", value: mesh.getTotalVertices() }), jsx(StringifiedPropertyLine, { label: "Faces", value: mesh.getTotalIndices() / 3 }), jsx(StringifiedPropertyLine, { label: "Sub-Meshes", value: subMeshes.length }), jsx(LinkToEntityPropertyLine, { label: "Skeleton", description: "The skeleton associated with the mesh.", entity: skeleton, selectionService: selectionService }), jsx(LinkToEntityPropertyLine, { label: "Material", description: "The material used by the mesh.", entity: material, selectionService: selectionService }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Is Pickable", target: mesh, propertyKey: "isPickable" }), isAnInstance && mesh instanceof InstancedMesh && (jsx(LinkToEntityPropertyLine, { label: "Source", description: "The source mesh from which this instance was created.", entity: mesh.sourceMesh, selectionService: selectionService }))] }));
7024
+ return (jsxs(Fragment, { children: [jsx(StringifiedPropertyLine, { label: "Vertices", value: mesh.getTotalVertices() }), jsx(StringifiedPropertyLine, { label: "Faces", value: mesh.getTotalIndices() / 3 }), jsx(StringifiedPropertyLine, { label: "Sub-Meshes", value: subMeshes.length }), jsx(LinkToEntityPropertyLine, { label: "Skeleton", description: "The skeleton associated with the mesh.", entity: skeleton, selectionService: selectionService }), jsx(LinkToEntityPropertyLine, { label: "Material", description: "The material used by the mesh.", entity: material, selectionService: selectionService }), !mesh.isAnInstance && (jsx(BoundProperty, { defaultValue: null, component: MaterialSelectorPropertyLine, label: "Active Material", target: mesh, propertyKey: "material", scene: mesh.getScene() })), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Is Pickable", target: mesh, propertyKey: "isPickable" }), isAnInstance && mesh instanceof InstancedMesh && (jsx(LinkToEntityPropertyLine, { label: "Source", description: "The source mesh from which this instance was created.", entity: mesh.sourceMesh, selectionService: selectionService }))] }));
6879
7025
  };
6880
7026
  const AbstractMeshDisplayProperties = (props) => {
6881
7027
  const { mesh } = props;
@@ -8052,90 +8198,6 @@ const SpriteManagerCellProperties = (props) => {
8052
8198
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: SpinButtonPropertyLine, label: "Cell Width", target: spriteManager, propertyKey: "cellWidth", min: 1, step: 1 }, "CellWidth"), jsx(BoundProperty, { component: SpinButtonPropertyLine, label: "Cell Height", target: spriteManager, propertyKey: "cellHeight", min: 1, step: 1 }, "CellHeight")] }));
8053
8199
  };
8054
8200
 
8055
- const useStyles$2 = makeStyles({
8056
- root: {
8057
- display: "flex",
8058
- flexDirection: "column",
8059
- },
8060
- controls: {
8061
- display: "flex",
8062
- gap: tokens.spacingHorizontalXS,
8063
- },
8064
- controlButton: {
8065
- minWidth: "auto",
8066
- flex: "1 1 0", // Equal flex grow/shrink with 0 basis
8067
- paddingVertical: tokens.spacingVerticalXS,
8068
- paddingHorizontal: tokens.spacingHorizontalS,
8069
- overflow: "hidden",
8070
- textOverflow: "ellipsis",
8071
- },
8072
- preview: {
8073
- border: `1px solid ${tokens.colorNeutralStroke1}`,
8074
- display: "block",
8075
- objectFit: "contain",
8076
- },
8077
- previewContainer: {
8078
- display: "flex",
8079
- justifyContent: "center",
8080
- marginTop: tokens.spacingVerticalXS,
8081
- marginBottom: tokens.spacingVerticalS,
8082
- width: "100%",
8083
- },
8084
- });
8085
- // This method of holding TextureChannels was brought over from inspectorv1 and can likely be refactored/simplified
8086
- const TextureChannelStates = {
8087
- R: { R: true, G: false, B: false, A: false },
8088
- G: { R: false, G: true, B: false, A: false },
8089
- B: { R: false, G: false, B: true, A: false },
8090
- A: { R: false, G: false, B: false, A: true },
8091
- ALL: { R: true, G: true, B: true, A: true },
8092
- };
8093
- const TexturePreview = (props) => {
8094
- const { texture, disableToolbar = false, maxWidth = "100%", maxHeight = "384px", offsetX = 0, offsetY = 0, width, height } = props;
8095
- const classes = useStyles$2();
8096
- const canvasRef = useRef(null);
8097
- const [channels, setChannels] = useState(TextureChannelStates.ALL);
8098
- const [face, setFace] = useState(0);
8099
- const [canvasStyle, setCanvasStyle] = useState();
8100
- const internalTexture = useProperty(texture, "_texture");
8101
- const { size } = useContext(ToolContext);
8102
- const updatePreviewAsync = useCallback(async () => {
8103
- const canvas = canvasRef.current;
8104
- if (!canvas) {
8105
- return;
8106
- }
8107
- try {
8108
- await WhenTextureReadyAsync(texture); // Ensure texture is loaded before grabbing size
8109
- const { width: textureWidth, height: textureHeight } = texture.getSize();
8110
- // Set canvas dimensions to the sub-region size
8111
- canvas.width = width ?? textureWidth;
8112
- canvas.height = height ?? textureHeight;
8113
- // Calculate the width that corresponds to maxHeight while maintaining aspect ratio
8114
- const aspectRatio = canvas.width / canvas.height;
8115
- // Use CSS min() to pick the smaller of maxWidth or the width that corresponds to maxHeight
8116
- const imageWidth = `min(${maxWidth}, calc(${maxHeight} * ${aspectRatio}))`;
8117
- setCanvasStyle({ width: imageWidth });
8118
- // Get full texture data, then draw only the sub-region
8119
- const data = await ApplyChannelsToTextureDataAsync(texture, textureWidth, textureHeight, face, channels);
8120
- const context = canvas.getContext("2d");
8121
- if (context) {
8122
- const fullImageData = context.createImageData(textureWidth, textureHeight);
8123
- fullImageData.data.set(data);
8124
- // Use putImageData with dirty rect to draw only the sub-region
8125
- context.putImageData(fullImageData, -offsetX, -offsetY, offsetX, offsetY, canvas.width, canvas.height);
8126
- }
8127
- }
8128
- catch {
8129
- // If we fail, leave the canvas empty
8130
- }
8131
- }, [texture, face, channels, offsetX, offsetY, width, height, internalTexture]);
8132
- useEffect(() => {
8133
- void updatePreviewAsync();
8134
- }, [updatePreviewAsync]);
8135
- return (jsxs("div", { className: classes.root, children: [disableToolbar ? null : texture.isCube ? (jsx(Toolbar$1, { className: classes.controls, size: size, "aria-label": "Cube Faces", children: ["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => (jsx(ToolbarButton, { className: classes.controlButton, appearance: face === idx ? "primary" : "subtle", onClick: () => setFace(idx), children: label }, label))) })) : (jsx(Toolbar$1, { className: classes.controls, size: size, "aria-label": "Channels", children: ["R", "G", "B", "A", "ALL"].map((ch) => (jsx(ToolbarButton, { className: classes.controlButton, appearance: channels === TextureChannelStates[ch] ? "primary" : "subtle", onClick: () => setChannels(TextureChannelStates[ch]), children: ch }, ch))) })), jsx("div", { className: classes.previewContainer, children: jsx("canvas", { ref: canvasRef, className: classes.preview, style: canvasStyle }) }), texture.isRenderTarget && (jsx(Button$1, { appearance: "outline", onClick: () => {
8136
- void updatePreviewAsync();
8137
- }, children: "Refresh" }))] }));
8138
- };
8139
8201
  /**
8140
8202
  * Gets the data of the specified texture by rendering it to an intermediate RGBA texture and retrieving the bytes from it.
8141
8203
  * This is convienent to get 8-bit RGBA values for a texture in a GPU compressed format.
@@ -8222,6 +8284,92 @@ async function ApplyChannelsToTextureDataAsync(texture, width, height, face, cha
8222
8284
  return data;
8223
8285
  }
8224
8286
 
8287
+ const useStyles$9 = makeStyles({
8288
+ root: {
8289
+ display: "flex",
8290
+ flexDirection: "column",
8291
+ },
8292
+ controls: {
8293
+ display: "flex",
8294
+ gap: tokens.spacingHorizontalXS,
8295
+ },
8296
+ controlButton: {
8297
+ minWidth: "auto",
8298
+ flex: "1 1 0", // Equal flex grow/shrink with 0 basis
8299
+ paddingVertical: tokens.spacingVerticalXS,
8300
+ paddingHorizontal: tokens.spacingHorizontalS,
8301
+ overflow: "hidden",
8302
+ textOverflow: "ellipsis",
8303
+ },
8304
+ preview: {
8305
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
8306
+ display: "block",
8307
+ objectFit: "contain",
8308
+ },
8309
+ previewContainer: {
8310
+ display: "flex",
8311
+ justifyContent: "center",
8312
+ marginTop: tokens.spacingVerticalXS,
8313
+ marginBottom: tokens.spacingVerticalS,
8314
+ width: "100%",
8315
+ },
8316
+ });
8317
+ // This method of holding TextureChannels was brought over from inspectorv1 and can likely be refactored/simplified
8318
+ const TextureChannelStates = {
8319
+ R: { R: true, G: false, B: false, A: false },
8320
+ G: { R: false, G: true, B: false, A: false },
8321
+ B: { R: false, G: false, B: true, A: false },
8322
+ A: { R: false, G: false, B: false, A: true },
8323
+ ALL: { R: true, G: true, B: true, A: true },
8324
+ };
8325
+ const TexturePreview = (props) => {
8326
+ const { texture, disableToolbar = false, maxWidth = "100%", maxHeight = "384px", offsetX = 0, offsetY = 0, width, height, imperativeRef } = props;
8327
+ const classes = useStyles$9();
8328
+ const canvasRef = useRef(null);
8329
+ const [channels, setChannels] = useState(TextureChannelStates.ALL);
8330
+ const [face, setFace] = useState(0);
8331
+ const [canvasStyle, setCanvasStyle] = useState();
8332
+ const internalTexture = useProperty(texture, "_texture");
8333
+ const { size } = useContext(ToolContext);
8334
+ const updatePreviewAsync = useCallback(async () => {
8335
+ const canvas = canvasRef.current;
8336
+ if (!canvas) {
8337
+ return;
8338
+ }
8339
+ try {
8340
+ await WhenTextureReadyAsync(texture); // Ensure texture is loaded before grabbing size
8341
+ const { width: textureWidth, height: textureHeight } = texture.getSize();
8342
+ // Set canvas dimensions to the sub-region size
8343
+ canvas.width = width ?? textureWidth;
8344
+ canvas.height = height ?? textureHeight;
8345
+ // Calculate the width that corresponds to maxHeight while maintaining aspect ratio
8346
+ const aspectRatio = canvas.width / canvas.height;
8347
+ // Use CSS min() to pick the smaller of maxWidth or the width that corresponds to maxHeight
8348
+ const imageWidth = `min(${maxWidth}, calc(${maxHeight} * ${aspectRatio}))`;
8349
+ setCanvasStyle({ width: imageWidth });
8350
+ // Get full texture data, then draw only the sub-region
8351
+ const data = await ApplyChannelsToTextureDataAsync(texture, textureWidth, textureHeight, face, channels);
8352
+ const context = canvas.getContext("2d");
8353
+ if (context) {
8354
+ const fullImageData = context.createImageData(textureWidth, textureHeight);
8355
+ fullImageData.data.set(data);
8356
+ // Use putImageData with dirty rect to draw only the sub-region
8357
+ context.putImageData(fullImageData, -offsetX, -offsetY, offsetX, offsetY, canvas.width, canvas.height);
8358
+ }
8359
+ }
8360
+ catch {
8361
+ // If we fail, leave the canvas empty
8362
+ }
8363
+ }, [texture, face, channels, offsetX, offsetY, width, height, internalTexture]);
8364
+ useImperativeHandle(imperativeRef, () => ({ refresh: updatePreviewAsync }), [updatePreviewAsync]);
8365
+ useEffect(() => {
8366
+ void updatePreviewAsync();
8367
+ }, [updatePreviewAsync]);
8368
+ return (jsxs("div", { className: classes.root, children: [disableToolbar ? null : texture.isCube ? (jsx(Toolbar$1, { className: classes.controls, size: size, "aria-label": "Cube Faces", children: ["+X", "-X", "+Y", "-Y", "+Z", "-Z"].map((label, idx) => (jsx(ToolbarButton, { className: classes.controlButton, appearance: face === idx ? "primary" : "subtle", onClick: () => setFace(idx), children: label }, label))) })) : (jsx(Toolbar$1, { className: classes.controls, size: size, "aria-label": "Channels", children: ["R", "G", "B", "A", "ALL"].map((ch) => (jsx(ToolbarButton, { className: classes.controlButton, appearance: channels === TextureChannelStates[ch] ? "primary" : "subtle", onClick: () => setChannels(TextureChannelStates[ch]), children: ch }, ch))) })), jsx("div", { className: classes.previewContainer, children: jsx("canvas", { ref: canvasRef, className: classes.preview, style: canvasStyle }) }), texture.isRenderTarget && (jsx(Button$1, { appearance: "outline", onClick: () => {
8369
+ void updatePreviewAsync();
8370
+ }, children: "Refresh" }))] }));
8371
+ };
8372
+
8225
8373
  function useMaxCellCount(sprite) {
8226
8374
  const manager = sprite.manager;
8227
8375
  const texture = useProperty(manager, "texture");
@@ -8434,8 +8582,11 @@ function FindTextureType(type) {
8434
8582
  }
8435
8583
 
8436
8584
  const BaseTexturePreviewProperties = (props) => {
8437
- const { texture } = props;
8438
- return (jsxs(Fragment, { children: [jsx(TexturePreview, { texture: texture }), jsx(TextureUpload, { texture: texture }), jsx(ButtonLine, { label: "Edit Texture (coming soon!)", onClick: () => { } })] }));
8585
+ // eslint-disable-next-line @typescript-eslint/naming-convention
8586
+ const { texture, textureEditor: TextureEditor } = props;
8587
+ const texturePreviewImperativeRef = useRef(null);
8588
+ const childWindow = useRef(null);
8589
+ return (jsxs(Fragment, { children: [jsx(TexturePreview, { imperativeRef: texturePreviewImperativeRef, texture: texture }), jsx(TextureUpload, { texture: texture }), jsx(ButtonLine, { label: "Edit Texture", onClick: () => childWindow.current?.open() }), jsx(ChildWindow, { id: "Texture Editor", imperativeRef: childWindow, children: jsx(TextureEditor, { texture: texture, onUpdate: async () => await texturePreviewImperativeRef.current?.refresh() }) })] }));
8439
8590
  };
8440
8591
  const BaseTextureGeneralProperties = (props) => {
8441
8592
  const { texture } = props;
@@ -8539,21 +8690,1621 @@ const ThinTextureSamplingProperties = (props) => {
8539
8690
  return jsx(NumberDropdownPropertyLine, { label: "Sampling", value: samplingMode, options: SamplingMode, onChange: (value) => texture.updateSamplingMode(value) });
8540
8691
  };
8541
8692
 
8542
- // Don't use instanceof in this case as we don't want to bring in the gui package just to check if the entity is an AdvancedDynamicTexture.
8543
- function IsAdvancedDynamicTexture$1(entity) {
8544
- return entity?.getClassName?.() === "AdvancedDynamicTexture";
8545
- }
8546
- const TexturePropertiesServiceDefinition = {
8547
- friendlyName: "Texture Properties",
8548
- consumes: [PropertiesServiceIdentity, SettingsContextIdentity],
8549
- factory: (propertiesService, settingsContext) => {
8693
+ // eslint-disable-next-line @typescript-eslint/naming-convention
8694
+ const canvasShader = {
8695
+ path: {
8696
+ vertexSource: `
8697
+ precision highp float;
8698
+
8699
+ attribute vec3 position;
8700
+ attribute vec2 uv;
8701
+
8702
+ uniform mat4 worldViewProjection;
8703
+
8704
+ varying vec2 vUV;
8705
+
8706
+ void main(void) {
8707
+ gl_Position = worldViewProjection * vec4(position, 1.0);
8708
+ vUV = uv;
8709
+ }
8710
+ `,
8711
+ fragmentSource: `
8712
+ precision highp float;
8713
+
8714
+ uniform sampler2D textureSampler;
8715
+
8716
+ uniform bool r;
8717
+ uniform bool g;
8718
+ uniform bool b;
8719
+ uniform bool a;
8720
+
8721
+ uniform int x1;
8722
+ uniform int y1;
8723
+ uniform int x2;
8724
+ uniform int y2;
8725
+ uniform int w;
8726
+ uniform int h;
8727
+
8728
+ uniform int time;
8729
+ uniform bool showGrid;
8730
+
8731
+ varying vec2 vUV;
8732
+
8733
+ float scl = 200.0;
8734
+ float speed = 10.0 / 1000.0;
8735
+ float smoothing = 0.2;
8736
+
8737
+ void main(void) {
8738
+ vec2 pos2 = vec2(gl_FragCoord.x, gl_FragCoord.y);
8739
+ vec2 pos = floor(pos2 * 0.05);
8740
+ float pattern = mod(pos.x + pos.y, 2.0);
8741
+ if (pattern == 0.0) {
8742
+ pattern = 0.7;
8743
+ }
8744
+ vec4 bg = vec4(pattern, pattern, pattern, 1.0);
8745
+ vec4 col = texture(textureSampler, vUV);
8746
+ if (!r && !g && !b) {
8747
+ if (a) {
8748
+ col = vec4(col.a, col.a, col.a, 1.0);
8749
+ } else {
8750
+ col = vec4(0.0,0.0,0.0,0.0);
8751
+ }
8752
+ } else {
8753
+ if (!r) {
8754
+ col.r = 0.0;
8755
+ if (!b) {
8756
+ col.r = col.g;
8757
+ }
8758
+ else if (!g) {
8759
+ col.r = col.b;
8760
+ }
8761
+ }
8762
+ if (!g) {
8763
+ col.g = 0.0;
8764
+ if (!b) {
8765
+ col.g = col.r;
8766
+ }
8767
+ else if (!r) {
8768
+ col.g = col.b;
8769
+ }
8770
+ }
8771
+ if (!b) {
8772
+ col.b = 0.0;
8773
+ if (!r) {
8774
+ col.b = col.g;
8775
+ } else if (!g) {
8776
+ col.b = col.r;
8777
+ }
8778
+ }
8779
+ if (!a) {
8780
+ col.a = 1.0;
8781
+ }
8782
+ }
8783
+ gl_FragColor = col * (col.a) + bg * (1.0 - col.a);
8784
+ float wF = float(w);
8785
+ float hF = float(h);
8786
+ int xPixel = int(floor(vUV.x * wF));
8787
+ int yPixel = int(floor((1.0 - vUV.y) * hF));
8788
+ int xDis = min(abs(xPixel - x1), abs(xPixel - x2));
8789
+ int yDis = min(abs(yPixel - y1), abs(yPixel - y2));
8790
+ if (showGrid) {
8791
+ vec2 frac = fract(vUV * vec2(wF,hF));
8792
+ float thickness = 0.1;
8793
+ if (abs(frac.x) < thickness || abs (frac.y) < thickness) {
8794
+ gl_FragColor = vec4(0.75,0.75,0.75,1.0);
8795
+ }
8796
+ }
8797
+ if (xPixel >= x1 && yPixel >= y1 && xPixel <= x2 && yPixel <= y2) {
8798
+ if (xDis <= 4 || yDis <= 4) {
8799
+ float c = sin(vUV.x * scl + vUV.y * scl + float(time) * speed);
8800
+ c = smoothstep(-smoothing,smoothing,c);
8801
+ float val = 1.0 - c;
8802
+ gl_FragColor = vec4(val, val, val, 1.0) * 0.7 + gl_FragColor * 0.3;
8803
+ }
8804
+ }
8805
+ }`,
8806
+ },
8807
+ options: {
8808
+ attributes: ["position", "uv"],
8809
+ uniforms: ["worldViewProjection", "textureSampler", "r", "g", "b", "a", "x1", "y1", "x2", "y2", "w", "h", "time", "showGrid"],
8810
+ },
8811
+ };
8812
+
8813
+ class TextureCanvasManager {
8814
+ constructor(texture, window, canvasUI, canvas2D, canvas3D, setPixelData, metadata, onUpdate, setMetadata, setMipLevel) {
8815
+ this._isPanning = false;
8816
+ this._mouseX = 0;
8817
+ this._mouseY = 0;
8818
+ this._size = { width: 0, height: 0 };
8819
+ this._channels = [];
8820
+ this._face = 0;
8821
+ this._mipLevel = 0;
8822
+ /** This is a hidden texture which is only responsible for holding the actual texture memory in the original engine */
8823
+ this._target = null;
8824
+ /** Keeps track of whether we have modified the texture */
8825
+ this._didEdit = false;
8826
+ this._plane = null;
8827
+ /** Tracks which keys are currently pressed */
8828
+ this._keyMap = new Map();
8829
+ /** Tracks which mouse buttons are currently pressed */
8830
+ this._buttonsPressed = 0;
8831
+ this.ZOOM_MOUSE_SPEED = 0.001;
8832
+ this.ZOOM_KEYBOARD_SPEED = 0.4;
8833
+ this.ZOOM_IN_KEY = "+";
8834
+ this.ZOOM_OUT_KEY = "-";
8835
+ this.PAN_SPEED = 0.003;
8836
+ this.PAN_KEY = "Space";
8837
+ this.MIN_SCALE = 0.01;
8838
+ this.GRID_SCALE = 0.047;
8839
+ this.MAX_SCALE = 10;
8840
+ this.SELECT_ALL_KEY = "KeyA";
8841
+ this.SAVE_KEY = "KeyS";
8842
+ this.RESET_KEY = "KeyR";
8843
+ this.DESELECT_KEY = "Escape";
8844
+ /** The number of milliseconds between texture updates */
8845
+ this.PUSH_FREQUENCY = 32;
8846
+ this._tool = null;
8847
+ this._editing3D = false;
8848
+ this._imageData = null;
8849
+ this._canPush = true;
8850
+ this._shouldPush = false;
8851
+ this._window = window;
8852
+ this._uiCanvas = canvasUI;
8853
+ this._2DCanvas = canvas2D;
8854
+ this._3DCanvas = canvas3D;
8855
+ this._paintCanvas = document.createElement("canvas");
8856
+ this._setPixelData = setPixelData;
8857
+ this._metadata = metadata;
8858
+ this._onUpdate = onUpdate;
8859
+ this._setMetadata = setMetadata;
8860
+ this._setMipLevel = setMipLevel;
8861
+ this._originalTexture = texture;
8862
+ this._originalTextureProperties = {
8863
+ _texture: this._originalTexture._texture,
8864
+ url: this._originalTexture.url,
8865
+ _forceSerialize: this._originalTexture._forceSerialize,
8866
+ };
8867
+ this._engine = new Engine(this._uiCanvas, true);
8868
+ this._scene = new Scene(this._engine, { virtual: true });
8869
+ this._scene.clearColor = new Color4(0, 0, 0, 0);
8870
+ this._camera = new FreeCamera("camera", new Vector3(0, 0, -1), this._scene);
8871
+ this._camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
8872
+ this._camera.minZ = 0.5;
8873
+ this._camera.maxZ = 1.5;
8874
+ this._cameraPos = new Vector2();
8875
+ this.setSize(texture.getSize());
8876
+ this._channelsTexture = new HtmlElementTexture("ct", this._2DCanvas, {
8877
+ engine: this._engine,
8878
+ scene: null,
8879
+ samplingMode: Texture.NEAREST_SAMPLINGMODE,
8880
+ generateMipMaps: true,
8881
+ });
8882
+ this._3DEngine = new Engine(this._3DCanvas);
8883
+ this._3DScene = new Scene(this._3DEngine, { virtual: true });
8884
+ this._3DScene.clearColor = new Color4(0, 0, 0, 0);
8885
+ this._3DCanvasTexture = new HtmlElementTexture("canvas", this._2DCanvas, { engine: this._3DEngine, scene: this._3DScene });
8886
+ this._3DCanvasTexture.hasAlpha = true;
8887
+ const cam = new FreeCamera("camera", new Vector3(0, 0, -1), this._3DScene);
8888
+ cam.mode = Camera.ORTHOGRAPHIC_CAMERA;
8889
+ [cam.orthoBottom, cam.orthoLeft, cam.orthoTop, cam.orthoRight] = [-0.5, -0.5, 0.5, 0.5];
8890
+ this._3DPlane = CreatePlane("texture", { width: 1, height: 1 }, this._3DScene);
8891
+ this._3DPlane.hasVertexAlpha = true;
8892
+ const mat = new StandardMaterial("material", this._3DScene);
8893
+ mat.diffuseTexture = this._3DCanvasTexture;
8894
+ mat.useAlphaFromDiffuseTexture = true;
8895
+ mat.disableLighting = true;
8896
+ mat.emissiveColor = Color3.White();
8897
+ this._3DPlane.material = mat;
8898
+ this._planeMaterial = new ShaderMaterial("canvasShader", this._scene, canvasShader.path, canvasShader.options);
8899
+ this.grabOriginalTexture();
8900
+ this._planeMaterial.setTexture("textureSampler", this._channelsTexture);
8901
+ this._planeMaterial.setFloat("r", 1.0);
8902
+ this._planeMaterial.setFloat("g", 1.0);
8903
+ this._planeMaterial.setFloat("b", 1.0);
8904
+ this._planeMaterial.setFloat("a", 1.0);
8905
+ this._planeMaterial.setInt("x1", -1);
8906
+ this._planeMaterial.setInt("y1", -1);
8907
+ this._planeMaterial.setInt("x2", -1);
8908
+ this._planeMaterial.setInt("y2", -1);
8909
+ this._planeMaterial.setInt("w", this._size.width);
8910
+ this._planeMaterial.setInt("h", this._size.height);
8911
+ this._planeMaterial.setInt("time", 0);
8912
+ this._planeMaterial.setFloat("showGrid", 0.0);
8913
+ if (this._plane) {
8914
+ this._plane.material = this._planeMaterial;
8915
+ }
8916
+ this._window.addEventListener("keydown", (evt) => {
8917
+ this._keyMap.set(evt.code, true);
8918
+ if (evt.code === this.SELECT_ALL_KEY && evt.ctrlKey) {
8919
+ this._setMetadata({
8920
+ select: {
8921
+ x1: 0,
8922
+ y1: 0,
8923
+ x2: this._size.width,
8924
+ y2: this._size.height,
8925
+ },
8926
+ });
8927
+ evt.preventDefault();
8928
+ }
8929
+ if (evt.code === this.SAVE_KEY && evt.ctrlKey) {
8930
+ this.saveTexture();
8931
+ evt.preventDefault();
8932
+ }
8933
+ if (evt.code === this.RESET_KEY && evt.ctrlKey) {
8934
+ this.reset();
8935
+ evt.preventDefault();
8936
+ }
8937
+ if (evt.code === this.DESELECT_KEY) {
8938
+ this._setMetadata({
8939
+ select: {
8940
+ x1: -1,
8941
+ y1: -1,
8942
+ x2: -1,
8943
+ y2: -1,
8944
+ },
8945
+ });
8946
+ }
8947
+ });
8948
+ this._window.addEventListener("keyup", (evt) => {
8949
+ this._keyMap.set(evt.code, false);
8950
+ });
8951
+ this._engine.runRenderLoop(() => {
8952
+ this._engine.resize();
8953
+ this._scene.render();
8954
+ this._planeMaterial.setInt("time", new Date().getTime());
8955
+ });
8956
+ this._scale = 1.5 / Math.max(this._size.width, this._size.height);
8957
+ this._isPanning = false;
8958
+ this._scene.onBeforeRenderObservable.add(() => {
8959
+ this._scale = Math.min(Math.max(this._scale, this.MIN_SCALE / Math.log2(Math.min(this._size.width, this._size.height))), this.MAX_SCALE);
8960
+ if (this._scale > this.GRID_SCALE) {
8961
+ this._planeMaterial.setFloat("showGrid", 1.0);
8962
+ }
8963
+ else {
8964
+ this._planeMaterial.setFloat("showGrid", 0.0);
8965
+ }
8966
+ const ratio = this._uiCanvas?.width / this._uiCanvas?.height;
8967
+ const { x, y } = this._cameraPos;
8968
+ this._camera.orthoBottom = y - 1 / this._scale;
8969
+ this._camera.orthoTop = y + 1 / this._scale;
8970
+ this._camera.orthoLeft = x - ratio / this._scale;
8971
+ this._camera.orthoRight = x + ratio / this._scale;
8972
+ });
8973
+ this._scene.onPointerObservable.add((pointerInfo) => {
8974
+ const leftButtonPressed = pointerInfo.event.buttons & 1;
8975
+ const middleButtonPressed = pointerInfo.event.buttons & 4;
8976
+ if (!this._isPanning) {
8977
+ if ((leftButtonPressed && !(this._buttonsPressed & 1) && this._keyMap.get(this.PAN_KEY)) || middleButtonPressed) {
8978
+ this._isPanning = true;
8979
+ this._mouseX = pointerInfo.event.x;
8980
+ this._mouseY = pointerInfo.event.y;
8981
+ }
8982
+ if (middleButtonPressed) {
8983
+ this._isPanning = true;
8984
+ }
8985
+ }
8986
+ else if ((!leftButtonPressed || !this._keyMap.get(this.PAN_KEY)) && !middleButtonPressed) {
8987
+ this._isPanning = false;
8988
+ }
8989
+ switch (pointerInfo.type) {
8990
+ case PointerEventTypes.POINTERWHEEL: {
8991
+ const event = pointerInfo.event;
8992
+ this._scale -= event.deltaY * this.ZOOM_MOUSE_SPEED * this._scale;
8993
+ break;
8994
+ }
8995
+ case PointerEventTypes.POINTERMOVE:
8996
+ if (this._isPanning) {
8997
+ this._cameraPos.x -= ((pointerInfo.event.x - this._mouseX) * this.PAN_SPEED) / this._scale;
8998
+ this._cameraPos.y += ((pointerInfo.event.y - this._mouseY) * this.PAN_SPEED) / this._scale;
8999
+ this._mouseX = pointerInfo.event.x;
9000
+ this._mouseY = pointerInfo.event.y;
9001
+ }
9002
+ if (pointerInfo.pickInfo?.hit) {
9003
+ if (this._imageData) {
9004
+ const pos = this.getMouseCoordinates(pointerInfo);
9005
+ const idx = (pos.x + pos.y * this._size.width) * 4;
9006
+ this._setPixelData({
9007
+ x: pos.x,
9008
+ y: pos.y,
9009
+ r: this._imageData[idx],
9010
+ g: this._imageData[idx + 1],
9011
+ b: this._imageData[idx + 2],
9012
+ a: this._imageData[idx + 3],
9013
+ });
9014
+ }
9015
+ }
9016
+ else {
9017
+ this._setPixelData({});
9018
+ }
9019
+ break;
9020
+ }
9021
+ this._buttonsPressed = pointerInfo.event.buttons;
9022
+ });
9023
+ this._scene.onKeyboardObservable.add((kbInfo) => {
9024
+ switch (kbInfo.type) {
9025
+ case KeyboardEventTypes.KEYDOWN:
9026
+ this._keyMap.set(kbInfo.event.key, true);
9027
+ switch (kbInfo.event.key) {
9028
+ case this.ZOOM_IN_KEY:
9029
+ this._scale += this.ZOOM_KEYBOARD_SPEED * this._scale;
9030
+ break;
9031
+ case this.ZOOM_OUT_KEY:
9032
+ this._scale -= this.ZOOM_KEYBOARD_SPEED * this._scale;
9033
+ break;
9034
+ }
9035
+ break;
9036
+ case KeyboardEventTypes.KEYUP:
9037
+ this._keyMap.set(kbInfo.event.key, false);
9038
+ break;
9039
+ }
9040
+ });
9041
+ }
9042
+ async updateTexture() {
9043
+ if (this._mipLevel !== 0) {
9044
+ this._setMipLevel(0);
9045
+ }
9046
+ this._didEdit = true;
9047
+ const element = this._editing3D ? this._3DCanvas : this._2DCanvas;
9048
+ if (this._editing3D) {
9049
+ this._3DCanvasTexture.update();
9050
+ this._3DScene.render();
9051
+ }
9052
+ if (this._originalTexture.isCube) ;
9053
+ else {
9054
+ if (!this._target) {
9055
+ this._target = new HtmlElementTexture("editor", element, {
9056
+ engine: this._originalTexture.getScene()?.getEngine(),
9057
+ scene: null,
9058
+ samplingMode: this._originalTexture.samplingMode,
9059
+ generateMipMaps: this._originalTextureProperties._texture?.generateMipMaps,
9060
+ });
9061
+ }
9062
+ else {
9063
+ this._target.element = element;
9064
+ }
9065
+ this.pushTexture();
9066
+ }
9067
+ this._originalTexture._texture = this._target?._texture ?? null;
9068
+ this._originalTexture.url = null;
9069
+ this._originalTexture._forceSerialize = true;
9070
+ this._channelsTexture.element = element;
9071
+ this.updateDisplay();
9072
+ this._onUpdate();
9073
+ }
9074
+ async pushTexture() {
9075
+ if (this._canPush && this._target) {
9076
+ const invertY = this._target.constructor.name === HtmlElementTexture.name ? false : this._originalTexture.invertY;
9077
+ this._target.update(invertY);
9078
+ this._target._texture?.updateSize(this._size.width, this._size.height);
9079
+ if (this._editing3D) {
9080
+ const bufferView = await this._3DEngine.readPixels(0, 0, this._size.width, this._size.height);
9081
+ this._imageData = new Uint8Array(bufferView.buffer, 0, bufferView.byteLength);
9082
+ }
9083
+ else {
9084
+ this._imageData = this._2DCanvas.getContext("2d").getImageData(0, 0, this._size.width, this._size.height).data;
9085
+ }
9086
+ this._canPush = false;
9087
+ this._shouldPush = false;
9088
+ setTimeout(() => {
9089
+ this._canPush = true;
9090
+ if (this._shouldPush) {
9091
+ this.pushTexture();
9092
+ }
9093
+ }, this.PUSH_FREQUENCY);
9094
+ }
9095
+ else {
9096
+ this._shouldPush = true;
9097
+ }
9098
+ }
9099
+ async startPainting() {
9100
+ if (this._mipLevel != 0) {
9101
+ this._setMipLevel(0);
9102
+ }
9103
+ let x = 0, y = 0, w = this._size.width, h = this._size.height;
9104
+ if (this._metadata.select.x1 != -1) {
9105
+ x = this._metadata.select.x1;
9106
+ y = this._metadata.select.y1;
9107
+ w = this._metadata.select.x2 - this._metadata.select.x1;
9108
+ h = this._metadata.select.y2 - this._metadata.select.y1;
9109
+ }
9110
+ this._paintCanvas.width = w;
9111
+ this._paintCanvas.height = h;
9112
+ const ctx = this._paintCanvas.getContext("2d");
9113
+ ctx.imageSmoothingEnabled = false;
9114
+ ctx.drawImage(this._2DCanvas, x, y, w, h, 0, 0, w, h);
9115
+ return ctx;
9116
+ }
9117
+ updatePainting() {
9118
+ let x = 0, y = 0, w = this._size.width, h = this._size.height;
9119
+ if (this._metadata.select.x1 != -1) {
9120
+ x = this._metadata.select.x1;
9121
+ y = this._metadata.select.y1;
9122
+ w = this._metadata.select.x2 - this._metadata.select.x1;
9123
+ h = this._metadata.select.y2 - this._metadata.select.y1;
9124
+ }
9125
+ let editingAllChannels = true;
9126
+ for (const channel of this._channels) {
9127
+ if (!channel.editable) {
9128
+ editingAllChannels = false;
9129
+ }
9130
+ }
9131
+ let oldData;
9132
+ if (!editingAllChannels) {
9133
+ oldData = this._2DCanvas.getContext("2d").getImageData(x, y, w, h).data;
9134
+ }
9135
+ const ctx = this._paintCanvas.getContext("2d");
9136
+ const ctx2D = this.canvas2D.getContext("2d");
9137
+ ctx2D.globalAlpha = 1.0;
9138
+ ctx2D.globalCompositeOperation = "destination-out";
9139
+ ctx2D.fillStyle = "white";
9140
+ ctx2D.fillRect(x, y, w, h);
9141
+ ctx2D.imageSmoothingEnabled = false;
9142
+ // If we're not editing all channels, we must process the pixel data
9143
+ if (!editingAllChannels) {
9144
+ const newData = ctx.getImageData(0, 0, w, h);
9145
+ const nd = newData.data;
9146
+ for (let index = 0; index < this._channels.length; index++) {
9147
+ const channel = this._channels[index];
9148
+ if (!channel.editable) {
9149
+ for (let i = index; i < w * h * 4; i += 4) {
9150
+ nd[i] = oldData[i];
9151
+ }
9152
+ }
9153
+ }
9154
+ ctx2D.globalCompositeOperation = "source-over";
9155
+ ctx2D.globalAlpha = 1.0;
9156
+ ctx2D.putImageData(newData, x, y);
9157
+ }
9158
+ else {
9159
+ ctx2D.globalCompositeOperation = "source-over";
9160
+ ctx2D.globalAlpha = 1.0;
9161
+ // We want to use drawImage wherever possible since it is much faster than putImageData
9162
+ ctx2D.drawImage(ctx.canvas, x, y);
9163
+ }
9164
+ this.updateTexture();
9165
+ }
9166
+ stopPainting() {
9167
+ this._paintCanvas.getContext("2d").clearRect(0, 0, this._paintCanvas.width, this._paintCanvas.height);
9168
+ }
9169
+ updateDisplay() {
9170
+ this._3DScene.render();
9171
+ this._channelsTexture.update(true);
9172
+ }
9173
+ set channels(channels) {
9174
+ // Determine if we need to re-render the texture. This is an expensive operation, so we should only do it if channel visibility has changed.
9175
+ let needsRender = false;
9176
+ if (channels.length !== this._channels.length) {
9177
+ needsRender = true;
9178
+ }
9179
+ else {
9180
+ for (let i = 0; i < channels.length; i++) {
9181
+ const channel = channels[i];
9182
+ if (channel.visible !== this._channels[i].visible) {
9183
+ needsRender = true;
9184
+ this._planeMaterial.setFloat(channel.id.toLowerCase(), channel.visible ? 1.0 : 0.0);
9185
+ }
9186
+ }
9187
+ }
9188
+ this._channels = channels;
9189
+ if (needsRender) {
9190
+ this.updateDisplay();
9191
+ }
9192
+ }
9193
+ paintPixelsOnCanvas(pixelData, canvas) {
9194
+ const ctx = canvas.getContext("2d");
9195
+ const imgData = ctx.createImageData(canvas.width, canvas.height);
9196
+ imgData.data.set(pixelData);
9197
+ ctx.putImageData(imgData, 0, 0);
9198
+ }
9199
+ async grabOriginalTexture() {
9200
+ // Grab image data from original texture and paint it onto the context of a DynamicTexture
9201
+ this.setSize(this._originalTexture.getSize());
9202
+ const data = await ApplyChannelsToTextureDataAsync(this._originalTexture, this._size.width, this._size.height, this._face, { R: true, G: true, B: true, A: true }, this._mipLevel);
9203
+ this._imageData = data;
9204
+ this.paintPixelsOnCanvas(data, this._2DCanvas);
9205
+ this._3DCanvasTexture.update();
9206
+ this.updateDisplay();
9207
+ return data;
9208
+ }
9209
+ getMouseCoordinates(pointerInfo) {
9210
+ if (pointerInfo.pickInfo?.hit) {
9211
+ const x = Math.floor(pointerInfo.pickInfo.getTextureCoordinates().x * this._size.width);
9212
+ const y = Math.floor((1 - pointerInfo.pickInfo.getTextureCoordinates().y) * this._size.height);
9213
+ return new Vector2(x, y);
9214
+ }
9215
+ else {
9216
+ return new Vector2();
9217
+ }
9218
+ }
9219
+ get scene() {
9220
+ return this._scene;
9221
+ }
9222
+ get canvas2D() {
9223
+ return this._2DCanvas;
9224
+ }
9225
+ get size() {
9226
+ return this._size;
9227
+ }
9228
+ set tool(tool) {
9229
+ if (this._tool) {
9230
+ this._tool.deactivate();
9231
+ }
9232
+ this._tool = tool;
9233
+ if (this._tool) {
9234
+ this._tool.activate();
9235
+ if (this._editing3D && !this._tool.is3D) {
9236
+ this._editing3D = false;
9237
+ this._2DCanvas.getContext("2d")?.drawImage(this._3DCanvas, 0, 0);
9238
+ }
9239
+ else if (!this._editing3D && this._tool.is3D) {
9240
+ this._editing3D = true;
9241
+ this.updateTexture();
9242
+ }
9243
+ }
9244
+ }
9245
+ get tool() {
9246
+ return this._tool;
9247
+ }
9248
+ set face(face) {
9249
+ if (this._face !== face) {
9250
+ this._face = face;
9251
+ this.grabOriginalTexture();
9252
+ this.updateDisplay();
9253
+ }
9254
+ }
9255
+ set mipLevel(mipLevel) {
9256
+ if (this._mipLevel === mipLevel) {
9257
+ return;
9258
+ }
9259
+ this._mipLevel = mipLevel;
9260
+ this.grabOriginalTexture();
9261
+ }
9262
+ /** Returns the 3D scene used for postprocesses */
9263
+ get scene3D() {
9264
+ return this._3DScene;
9265
+ }
9266
+ set metadata(metadata) {
9267
+ this._metadata = metadata;
9268
+ const { x1, y1, x2, y2 } = metadata.select;
9269
+ this._planeMaterial.setInt("x1", x1);
9270
+ this._planeMaterial.setInt("y1", y1);
9271
+ this._planeMaterial.setInt("x2", x2);
9272
+ this._planeMaterial.setInt("y2", y2);
9273
+ }
9274
+ makePlane() {
9275
+ if (this._plane) {
9276
+ this._plane.dispose();
9277
+ }
9278
+ this._plane = CreatePlane("plane", { width: this._size.width, height: this._size.height }, this._scene);
9279
+ this._plane.enableEdgesRendering();
9280
+ this._plane.edgesWidth = 4.0;
9281
+ this._plane.edgesColor = new Color4(1, 1, 1, 1);
9282
+ this._plane.enablePointerMoveEvents = true;
9283
+ this._plane.material = this._planeMaterial;
9284
+ }
9285
+ reset() {
9286
+ if (this._tool && this._tool.reset) {
9287
+ this._tool.reset();
9288
+ }
9289
+ this._originalTexture._texture = this._originalTextureProperties._texture;
9290
+ this._originalTexture.url = this._originalTextureProperties.url;
9291
+ this._originalTexture._forceSerialize = this._originalTextureProperties._forceSerialize;
9292
+ this.grabOriginalTexture();
9293
+ this.makePlane();
9294
+ this._didEdit = false;
9295
+ this._onUpdate();
9296
+ }
9297
+ async resize(newSize) {
9298
+ const data = await ApplyChannelsToTextureDataAsync(this._originalTexture, newSize.width, newSize.height, this._face, { R: true, G: true, B: true, A: true });
9299
+ this.setSize(newSize);
9300
+ this.paintPixelsOnCanvas(data, this._2DCanvas);
9301
+ this.updateTexture();
9302
+ this._didEdit = true;
9303
+ }
9304
+ setSize(size) {
9305
+ const oldSize = this._size;
9306
+ this._size = size;
9307
+ this._2DCanvas.width = this._size.width;
9308
+ this._2DCanvas.height = this._size.height;
9309
+ this._3DCanvas.width = this._size.width;
9310
+ this._3DCanvas.height = this._size.height;
9311
+ if (this._planeMaterial) {
9312
+ this._planeMaterial.setInt("w", this._size.width);
9313
+ this._planeMaterial.setInt("h", this._size.height);
9314
+ }
9315
+ if (!oldSize || oldSize.width != size.width || oldSize.height != size.height) {
9316
+ this._cameraPos.x = 0;
9317
+ this._cameraPos.y = 0;
9318
+ this._scale = 1.5 / Math.max(this._size.width, this._size.height);
9319
+ }
9320
+ this.makePlane();
9321
+ }
9322
+ upload(file) {
9323
+ Tools.ReadFile(file, (data) => {
9324
+ const blob = new Blob([data], { type: "octet/stream" });
9325
+ let extension = undefined;
9326
+ if (file.name.toLowerCase().indexOf(".dds") > 0) {
9327
+ extension = ".dds";
9328
+ }
9329
+ else if (file.name.toLowerCase().indexOf(".env") > 0) {
9330
+ extension = ".env";
9331
+ }
9332
+ const reader = new FileReader();
9333
+ reader.readAsDataURL(blob);
9334
+ reader.onloadend = () => {
9335
+ const base64data = reader.result;
9336
+ if (extension === ".dds" || extension === ".env") {
9337
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
9338
+ this._originalTexture.updateURL(base64data, extension, async () => await this.grabOriginalTexture());
9339
+ }
9340
+ else {
9341
+ const texture = new Texture(base64data, this._scene, this._originalTexture.noMipmap, false, Texture.NEAREST_SAMPLINGMODE, () => {
9342
+ // eslint-disable-next-line github/no-then
9343
+ ApplyChannelsToTextureDataAsync(texture, texture.getSize().width, texture.getSize().height, 0, { R: true, G: true, B: true, A: true }).then(async (pixels) => {
9344
+ if (this._tool && this._tool.reset) {
9345
+ this._tool.reset();
9346
+ }
9347
+ texture.dispose();
9348
+ this.setSize(texture.getSize());
9349
+ this.paintPixelsOnCanvas(pixels, this._2DCanvas);
9350
+ await this.updateTexture();
9351
+ this._setMipLevel(0);
9352
+ });
9353
+ });
9354
+ }
9355
+ };
9356
+ }, undefined, true);
9357
+ }
9358
+ saveTexture() {
9359
+ const canvas = this._editing3D ? this._3DCanvas : this._2DCanvas;
9360
+ Tools.ToBlob(canvas, (blob) => {
9361
+ Tools.Download(blob, this._originalTexture.name);
9362
+ });
9363
+ }
9364
+ toolInteractionEnabled() {
9365
+ return !(this._keyMap.get(this.PAN_KEY) || this._isPanning);
9366
+ }
9367
+ dispose() {
9368
+ if (this._didEdit) {
9369
+ this._originalTextureProperties._texture?.dispose();
9370
+ }
9371
+ if (this._tool) {
9372
+ this._tool.deactivate();
9373
+ }
9374
+ this._paintCanvas.parentNode?.removeChild(this._paintCanvas);
9375
+ this._3DPlane.dispose();
9376
+ this._3DCanvasTexture.dispose();
9377
+ this._3DScene.dispose();
9378
+ this._3DEngine.dispose();
9379
+ this._plane?.dispose();
9380
+ this._channelsTexture.dispose();
9381
+ this._planeMaterial.dispose();
9382
+ this._camera.dispose();
9383
+ this._scene.dispose();
9384
+ this._engine.dispose();
9385
+ }
9386
+ }
9387
+
9388
+ const useStyles$8 = makeStyles({
9389
+ channelsBar: {
9390
+ display: "flex",
9391
+ flexDirection: "column",
9392
+ backgroundColor: tokens.colorNeutralBackground1,
9393
+ padding: tokens.spacingVerticalXS,
9394
+ gap: tokens.spacingVerticalXS,
9395
+ borderRadius: tokens.borderRadiusMedium,
9396
+ boxShadow: tokens.shadow8,
9397
+ },
9398
+ channel: {
9399
+ display: "flex",
9400
+ alignItems: "center",
9401
+ gap: tokens.spacingHorizontalXS,
9402
+ padding: tokens.spacingVerticalXS,
9403
+ borderRadius: tokens.borderRadiusMedium,
9404
+ },
9405
+ channelLabel: {
9406
+ fontWeight: tokens.fontWeightSemibold,
9407
+ margin: `0 ${tokens.spacingHorizontalXS}`,
9408
+ textAlign: "center",
9409
+ },
9410
+ channelR: {
9411
+ color: tokens.colorPaletteRedBorderActive,
9412
+ },
9413
+ channelG: {
9414
+ color: tokens.colorPaletteGreenBorderActive,
9415
+ },
9416
+ channelB: {
9417
+ color: tokens.colorPaletteBlueBorderActive,
9418
+ },
9419
+ channelA: {
9420
+ color: tokens.colorNeutralForeground1,
9421
+ },
9422
+ uneditable: {
9423
+ opacity: 0.5,
9424
+ },
9425
+ });
9426
+ /**
9427
+ * Displays channel visibility and editability controls
9428
+ * @param props - The channels bar properties
9429
+ * @returns The channels bar component
9430
+ */
9431
+ const ChannelsBar = (props) => {
9432
+ const { channels, setChannels } = props;
9433
+ const classes = useStyles$8();
9434
+ const toggleVisibility = useCallback((index) => {
9435
+ const newChannels = [...channels];
9436
+ newChannels[index] = { ...newChannels[index], visible: !newChannels[index].visible };
9437
+ setChannels(newChannels);
9438
+ }, [channels, setChannels]);
9439
+ const toggleEditable = useCallback((index) => {
9440
+ const newChannels = [...channels];
9441
+ newChannels[index] = { ...newChannels[index], editable: !newChannels[index].editable };
9442
+ setChannels(newChannels);
9443
+ }, [channels, setChannels]);
9444
+ const getChannelColorClass = (id) => {
9445
+ switch (id) {
9446
+ case "R":
9447
+ return classes.channelR;
9448
+ case "G":
9449
+ return classes.channelG;
9450
+ case "B":
9451
+ return classes.channelB;
9452
+ default:
9453
+ return classes.channelA;
9454
+ }
9455
+ };
9456
+ return (jsx("div", { className: classes.channelsBar, children: channels.map((channel, index) => {
9457
+ const visTip = channel.visible ? "Hide" : "Show";
9458
+ const editTip = channel.editable ? "Lock" : "Unlock";
9459
+ return (jsxs("div", { className: `${classes.channel} ${!channel.editable ? classes.uneditable : ""}`, children: [jsx(Tooltip, { content: `${visTip} ${channel.name}`, relationship: "label", positioning: "before", children: jsx(ToggleButton$1, { appearance: "transparent", size: "small", checked: channel.visible, icon: channel.visible ? jsx(EyeRegular, {}) : jsx(EyeOffRegular, {}), onClick: () => toggleVisibility(index) }) }), jsx(Tooltip, { content: `${editTip} ${channel.name}`, relationship: "label", positioning: "before", children: jsx(ToggleButton$1, { appearance: "transparent", size: "small", checked: channel.editable, icon: channel.editable ? jsx(LockOpenRegular, {}) : jsx(LockClosedRegular, {}), onClick: () => toggleEditable(index) }) }), jsx(Body1, { className: mergeClasses(classes.channelLabel, getChannelColorClass(channel.id)), children: channel.id })] }, channel.id));
9460
+ }) }));
9461
+ };
9462
+
9463
+ const useStyles$7 = makeStyles({
9464
+ propertiesBar: {
9465
+ display: "flex",
9466
+ backgroundColor: tokens.colorNeutralBackground1,
9467
+ alignItems: "center",
9468
+ padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
9469
+ gap: tokens.spacingHorizontalS,
9470
+ borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
9471
+ flexWrap: "wrap",
9472
+ },
9473
+ section: {
9474
+ display: "flex",
9475
+ alignItems: "center",
9476
+ gap: tokens.spacingHorizontalXS,
9477
+ },
9478
+ dimensionsForm: {
9479
+ display: "flex",
9480
+ alignItems: "center",
9481
+ gap: tokens.spacingHorizontalXS,
9482
+ },
9483
+ dimensionInput: {
9484
+ width: "60px",
9485
+ },
9486
+ pixelData: {
9487
+ display: "flex",
9488
+ alignItems: "center",
9489
+ gap: tokens.spacingHorizontalXS,
9490
+ fontSize: tokens.fontSizeBase200,
9491
+ fontFamily: tokens.fontFamilyMonospace,
9492
+ },
9493
+ pixelDataLabel: {
9494
+ color: tokens.colorNeutralForeground3,
9495
+ },
9496
+ pixelDataValue: {
9497
+ color: tokens.colorNeutralForeground1,
9498
+ minWidth: "32px",
9499
+ },
9500
+ faceButton: {
9501
+ minWidth: "auto",
9502
+ paddingLeft: tokens.spacingHorizontalS,
9503
+ paddingRight: tokens.spacingHorizontalS,
9504
+ },
9505
+ spacer: {
9506
+ flex: 1,
9507
+ },
9508
+ uploadInput: {
9509
+ display: "none",
9510
+ },
9511
+ });
9512
+ const PixelDataDisplay = ({ label, value }) => {
9513
+ const classes = useStyles$7();
9514
+ return (jsxs("span", { className: classes.pixelData, children: [jsxs(Label, { className: classes.pixelDataLabel, children: [label, ":"] }), jsx(Label, { className: classes.pixelDataValue, children: value !== undefined ? value : "-" })] }));
9515
+ };
9516
+ const CubeFaces = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
9517
+ /**
9518
+ * Properties bar component showing texture info and actions
9519
+ * @param props - The properties bar properties
9520
+ * @returns The properties bar component
9521
+ */
9522
+ const PropertiesBar = (props) => {
9523
+ const { texture, size, saveTexture, pixelData, face, setFace, resetTexture, resizeTexture, uploadTexture, mipLevel, setMipLevel } = props;
9524
+ const classes = useStyles$7();
9525
+ const uploadInputRef = useRef(null);
9526
+ const [width, setWidth] = useState(size.width);
9527
+ const [height, setHeight] = useState(size.height);
9528
+ // Update local state when size prop changes
9529
+ useEffect(() => {
9530
+ setWidth(size.width);
9531
+ setHeight(size.height);
9532
+ }, [size.width, size.height]);
9533
+ const maxLevels = Math.floor(Math.log2(Math.max(texture.getSize().width, texture.getSize().height)));
9534
+ const engine = texture.getScene()?.getEngine();
9535
+ const mipsEnabled = !texture.noMipmap && engine?.getCaps().textureLOD;
9536
+ const handleUploadClick = useCallback(() => {
9537
+ uploadInputRef.current?.click();
9538
+ }, []);
9539
+ const handleFileChange = useCallback((evt) => {
9540
+ const files = evt.target.files;
9541
+ if (files && files.length) {
9542
+ uploadTexture(files[0]);
9543
+ }
9544
+ evt.target.value = "";
9545
+ }, [uploadTexture]);
9546
+ const handleResize = useCallback(() => {
9547
+ resizeTexture(width, height);
9548
+ }, [width, height, resizeTexture]);
9549
+ const getNewDimension = (oldDim, newDim) => {
9550
+ const parsed = parseInt(newDim);
9551
+ if (!isNaN(parsed) && parsed > 0 && Number.isInteger(parsed)) {
9552
+ return parsed;
9553
+ }
9554
+ return oldDim;
9555
+ };
9556
+ return (jsxs("div", { className: classes.propertiesBar, children: [jsxs("div", { className: classes.section, children: [jsx(Label, { children: "W:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: width.toString(), readOnly: texture.isCube, onChange: (_, data) => setWidth(getNewDimension(width, data.value)) }), jsx(Label, { children: "H:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: height.toString(), readOnly: texture.isCube, onChange: (_, data) => setHeight(getNewDimension(height, data.value)) }), !texture.isCube && (jsx(Tooltip, { content: "Resize", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ResizeRegular, {}), onClick: handleResize }) }))] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "X", value: pixelData.x }), jsx(PixelDataDisplay, { label: "Y", value: pixelData.y })] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "R", value: pixelData.r }), jsx(PixelDataDisplay, { label: "G", value: pixelData.g }), jsx(PixelDataDisplay, { label: "B", value: pixelData.b }), jsx(PixelDataDisplay, { label: "A", value: pixelData.a })] }), texture.isCube && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsx(Toolbar$1, { size: "small", children: CubeFaces.map((label, index) => (jsx(ToolbarButton, { className: classes.faceButton, appearance: face === index ? "primary" : "subtle", onClick: () => setFace(index), children: label }, label))) })] })), mipsEnabled && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(Label, { children: "MIP:" }), jsx(Tooltip, { content: "Mip Preview Up", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronUpRegular, {}), disabled: mipLevel <= 0, onClick: () => setMipLevel(mipLevel - 1) }) }), jsx(Label, { children: mipLevel }), jsx(Tooltip, { content: "Mip Preview Down", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronDownRegular, {}), disabled: mipLevel >= maxLevels, onClick: () => setMipLevel(mipLevel + 1) }) })] })] })), jsx("div", { className: classes.spacer }), jsxs(Toolbar$1, { size: "small", children: [jsx(Tooltip, { content: "Reset", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowResetRegular, {}), onClick: resetTexture }) }), jsx(Tooltip, { content: "Upload", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowUploadRegular, {}), onClick: handleUploadClick }) }), jsx("input", { ref: uploadInputRef, className: classes.uploadInput, type: "file", accept: ".jpg, .png, .tga, .dds, .env, .exr", onChange: handleFileChange }), jsx(Tooltip, { content: "Save", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(SaveRegular, {}), onClick: saveTexture }) })] })] }));
9557
+ };
9558
+
9559
+ const useStyles$6 = makeStyles({
9560
+ statusBar: {
9561
+ display: "flex",
9562
+ backgroundColor: tokens.colorNeutralBackground1,
9563
+ alignItems: "center",
9564
+ justifyContent: "space-between",
9565
+ padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM}`,
9566
+ fontSize: tokens.fontSizeBase200,
9567
+ color: tokens.colorNeutralForeground2,
9568
+ borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
9569
+ minHeight: "24px",
9570
+ },
9571
+ fileName: {
9572
+ overflow: "hidden",
9573
+ textOverflow: "ellipsis",
9574
+ whiteSpace: "nowrap",
9575
+ },
9576
+ mipInfo: {
9577
+ flexShrink: 0,
9578
+ },
9579
+ });
9580
+ /**
9581
+ * Displays status information about the texture
9582
+ * @param props - The status bar properties
9583
+ * @returns The status bar component
9584
+ */
9585
+ const StatusBar = (props) => {
9586
+ const { texture, mipLevel } = props;
9587
+ const classes = useStyles$6();
9588
+ const factor = Math.pow(2, mipLevel);
9589
+ const width = Math.ceil(texture.getSize().width / factor);
9590
+ const height = Math.ceil(texture.getSize().height / factor);
9591
+ return (jsxs("div", { className: classes.statusBar, children: [jsx("span", { className: classes.fileName, children: texture.name }), !texture.noMipmap && (jsxs("span", { className: classes.mipInfo, children: ["MIP Preview: ", mipLevel, " (", width, "\u00D7", height, ")"] }))] }));
9592
+ };
9593
+
9594
+ const useStyles$5 = makeStyles({
9595
+ toolbar: {
9596
+ display: "flex",
9597
+ flexDirection: "column",
9598
+ backgroundColor: tokens.colorNeutralBackground1,
9599
+ padding: tokens.spacingVerticalXS,
9600
+ gap: tokens.spacingVerticalXS,
9601
+ borderRadius: tokens.borderRadiusMedium,
9602
+ boxShadow: tokens.shadow8,
9603
+ },
9604
+ toolsSection: {
9605
+ display: "flex",
9606
+ flexDirection: "column",
9607
+ gap: tokens.spacingVerticalXXS,
9608
+ },
9609
+ toolButton: {
9610
+ minWidth: "36px",
9611
+ minHeight: "36px",
9612
+ padding: tokens.spacingVerticalXS,
9613
+ },
9614
+ toolIcon: {
9615
+ width: "24px",
9616
+ height: "24px",
9617
+ },
9618
+ colorSection: {
9619
+ display: "flex",
9620
+ justifyContent: "center",
9621
+ margin: tokens.spacingVerticalS,
9622
+ },
9623
+ });
9624
+ /**
9625
+ * Toolbar component for texture editing tools
9626
+ * @param props - The toolbar properties
9627
+ * @returns The toolbar component
9628
+ */
9629
+ const ToolBar = (props) => {
9630
+ const { tools, changeTool, activeToolIndex, metadata, setMetadata, hasAlpha } = props;
9631
+ const classes = useStyles$5();
9632
+ const computeRGBAColor = useCallback(() => {
9633
+ const opacityInt = Math.floor(metadata.alpha * 255);
9634
+ const opacityHex = opacityInt.toString(16).padStart(2, "0");
9635
+ return Color4.FromHexString(`${metadata.color}${opacityHex}`);
9636
+ }, [metadata.color, metadata.alpha]);
9637
+ const handleColorChange = useCallback((color) => {
9638
+ const newMetadata = {
9639
+ color: color.toHexString(true),
9640
+ alpha: color.a ?? 1,
9641
+ };
9642
+ if (newMetadata.color !== metadata.color || newMetadata.alpha !== metadata.alpha) {
9643
+ setMetadata(newMetadata);
9644
+ }
9645
+ }, [metadata, setMetadata]);
9646
+ const handleToolClick = useCallback((index) => {
9647
+ if (activeToolIndex === index) {
9648
+ // Deselect current tool
9649
+ changeTool(-1);
9650
+ }
9651
+ else {
9652
+ changeTool(index);
9653
+ }
9654
+ }, [activeToolIndex, changeTool]);
9655
+ return (jsxs("div", { className: classes.toolbar, children: [jsx("div", { className: classes.colorSection, children: jsx(Tooltip, { relationship: "label", content: "Pick Tool Color", positioning: "after", children: jsx(ColorPickerPopup, { value: hasAlpha ? computeRGBAColor() : Color3.FromHexString(metadata.color), onChange: handleColorChange }) }) }), jsx(Divider, {}), jsx("div", { className: classes.toolsSection, children: tools.map((tool, index) => {
9656
+ // eslint-disable-next-line @typescript-eslint/naming-convention
9657
+ const IconComponent = tool.icon;
9658
+ return (jsx(Tooltip, { content: tool.name, relationship: "label", positioning: "after", children: jsx(ToggleButton$1, { className: classes.toolButton, appearance: "subtle", checked: index === activeToolIndex, onClick: () => handleToolClick(index), icon: jsx(IconComponent, {}) }) }, index));
9659
+ }) })] }));
9660
+ };
9661
+
9662
+ const useStyles$4 = makeStyles({
9663
+ textureEditor: {
9664
+ display: "flex",
9665
+ flexDirection: "column",
9666
+ height: "100%",
9667
+ width: "100%",
9668
+ backgroundColor: tokens.colorNeutralBackground3,
9669
+ color: tokens.colorNeutralForeground1,
9670
+ overflow: "hidden",
9671
+ },
9672
+ mainContent: {
9673
+ display: "flex",
9674
+ flex: 1,
9675
+ overflow: "hidden",
9676
+ position: "relative",
9677
+ },
9678
+ canvasContainer: {
9679
+ flex: 1,
9680
+ position: "relative",
9681
+ overflow: "hidden",
9682
+ },
9683
+ canvasUI: {
9684
+ width: "100%",
9685
+ height: "100%",
9686
+ outline: "none",
9687
+ },
9688
+ canvas2D: {
9689
+ display: "none",
9690
+ },
9691
+ canvas3D: {
9692
+ display: "none",
9693
+ },
9694
+ sidebarLeft: {
9695
+ display: "flex",
9696
+ flexDirection: "column",
9697
+ position: "absolute",
9698
+ left: tokens.spacingHorizontalM,
9699
+ top: tokens.spacingVerticalM,
9700
+ },
9701
+ sidebarRight: {
9702
+ display: "flex",
9703
+ flexDirection: "column",
9704
+ position: "absolute",
9705
+ right: tokens.spacingHorizontalM,
9706
+ top: tokens.spacingVerticalM,
9707
+ },
9708
+ toolSettingsContainer: {
9709
+ position: "absolute",
9710
+ left: tokens.spacingHorizontalM,
9711
+ bottom: tokens.spacingVerticalM,
9712
+ backgroundColor: tokens.colorNeutralBackground1,
9713
+ borderRadius: tokens.borderRadiusMedium,
9714
+ padding: tokens.spacingVerticalS,
9715
+ boxShadow: tokens.shadow8,
9716
+ },
9717
+ });
9718
+ const PREVIEW_UPDATE_DELAY_MS = 160;
9719
+ /**
9720
+ * Main texture editor component
9721
+ * @param props - The texture editor properties
9722
+ * @returns The texture editor component
9723
+ */
9724
+ const TextureEditor = (props) => {
9725
+ const { texture, toolProviders = [], window: editorWindow, onUpdate } = props;
9726
+ const classes = useStyles$4();
9727
+ // Canvas refs
9728
+ const uiCanvasRef = useRef(null);
9729
+ const canvas2DRef = useRef(null);
9730
+ const canvas3DRef = useRef(null);
9731
+ const timerRef = useRef(null);
9732
+ const canvasManagerRef = useRef(null);
9733
+ // State
9734
+ const [activeToolIndex, setActiveToolIndex] = useState(-1);
9735
+ const [metadata, setMetadataState] = useState({
9736
+ color: "#ffffff",
9737
+ alpha: 1,
9738
+ select: {
9739
+ x1: -1,
9740
+ y1: -1,
9741
+ x2: -1,
9742
+ y2: -1,
9743
+ },
9744
+ });
9745
+ const [channels, setChannels] = useState(() => {
9746
+ const baseChannels = [
9747
+ { name: "Red", visible: true, editable: true, id: "R" },
9748
+ { name: "Green", visible: true, editable: true, id: "G" },
9749
+ { name: "Blue", visible: true, editable: true, id: "B" },
9750
+ ];
9751
+ baseChannels.push({
9752
+ name: texture.isCube ? "Display" : "Alpha",
9753
+ visible: true,
9754
+ editable: true,
9755
+ id: "A",
9756
+ });
9757
+ return baseChannels;
9758
+ });
9759
+ const [pixelData, setPixelData] = useState({});
9760
+ const [face, setFace] = useState(0);
9761
+ const [mipLevel, setMipLevel] = useState(0);
9762
+ const [size, setSize] = useState(texture.getSize());
9763
+ // Callbacks
9764
+ const textureDidUpdate = useCallback(() => {
9765
+ if (timerRef.current != null) {
9766
+ window.clearTimeout(timerRef.current);
9767
+ }
9768
+ timerRef.current = window.setTimeout(() => {
9769
+ onUpdate?.();
9770
+ timerRef.current = null;
9771
+ }, PREVIEW_UPDATE_DELAY_MS);
9772
+ }, [onUpdate]);
9773
+ const setMetadata = useCallback((newMetadata) => {
9774
+ setMetadataState((prev) => {
9775
+ const data = { ...prev, ...newMetadata };
9776
+ if (canvasManagerRef.current) {
9777
+ canvasManagerRef.current.metadata = data;
9778
+ }
9779
+ return data;
9780
+ });
9781
+ }, []);
9782
+ const getToolParameters = () => {
9783
+ const manager = canvasManagerRef.current;
9784
+ return {
9785
+ scene: manager.scene,
9786
+ canvas2D: manager.canvas2D,
9787
+ scene3D: manager.scene3D,
9788
+ size: manager.size,
9789
+ updateTexture: () => void manager.updateTexture(),
9790
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
9791
+ startPainting: () => manager.startPainting(),
9792
+ stopPainting: () => manager.stopPainting(),
9793
+ updatePainting: () => manager.updatePainting(),
9794
+ metadata,
9795
+ setMetadata,
9796
+ getMouseCoordinates: (pointerInfo) => manager.getMouseCoordinates(pointerInfo),
9797
+ interactionEnabled: () => manager.toolInteractionEnabled(),
9798
+ };
9799
+ };
9800
+ const getToolParametersRef = useRef(getToolParameters);
9801
+ getToolParametersRef.current = getToolParameters;
9802
+ const tools = useMemo(() => toolProviders?.map((provider) => provider.getTool({ getParameters: () => getToolParametersRef.current() })), [toolProviders]);
9803
+ const changeTool = useCallback((index) => {
9804
+ if (canvasManagerRef.current) {
9805
+ if (index !== -1 && tools[index]) {
9806
+ canvasManagerRef.current.tool = {
9807
+ is3D: toolProviders[index].is3D ?? false,
9808
+ activate: () => tools[index].activate(),
9809
+ deactivate: () => tools[index].deactivate(),
9810
+ reset: () => tools[index].reset?.(),
9811
+ };
9812
+ }
9813
+ else {
9814
+ canvasManagerRef.current.tool = null;
9815
+ }
9816
+ }
9817
+ setActiveToolIndex(index);
9818
+ }, [toolProviders, tools]);
9819
+ const saveTexture = useCallback(() => {
9820
+ canvasManagerRef.current?.saveTexture();
9821
+ }, []);
9822
+ const resetTexture = useCallback(() => {
9823
+ canvasManagerRef.current?.reset();
9824
+ }, []);
9825
+ const resizeTexture = useCallback((width, height) => {
9826
+ void canvasManagerRef.current?.resize({ width, height });
9827
+ }, []);
9828
+ const uploadTexture = useCallback((file) => {
9829
+ canvasManagerRef.current?.upload(file);
9830
+ }, []);
9831
+ // Initialize canvas manager
9832
+ useEffect(() => {
9833
+ if (!uiCanvasRef.current || !canvas2DRef.current || !canvas3DRef.current) {
9834
+ return;
9835
+ }
9836
+ const manager = new TextureCanvasManager(texture, editorWindow ?? uiCanvasRef.current.ownerDocument.defaultView ?? window, uiCanvasRef.current, canvas2DRef.current, canvas3DRef.current, setPixelData, metadata, textureDidUpdate, setMetadata, setMipLevel);
9837
+ canvasManagerRef.current = manager;
9838
+ setSize(manager.size);
9839
+ return () => {
9840
+ manager.dispose();
9841
+ canvasManagerRef.current = null;
9842
+ };
9843
+ }, [texture, editorWindow]);
9844
+ // Update canvas manager when channels/face/mipLevel change
9845
+ useEffect(() => {
9846
+ if (canvasManagerRef.current) {
9847
+ canvasManagerRef.current.channels = [...channels];
9848
+ }
9849
+ }, [channels]);
9850
+ useEffect(() => {
9851
+ if (canvasManagerRef.current) {
9852
+ canvasManagerRef.current.face = face;
9853
+ }
9854
+ }, [face]);
9855
+ useEffect(() => {
9856
+ if (canvasManagerRef.current) {
9857
+ canvasManagerRef.current.mipLevel = mipLevel;
9858
+ }
9859
+ }, [mipLevel]);
9860
+ // Compute cursor style
9861
+ let cursor = "default";
9862
+ if (canvasManagerRef.current && !canvasManagerRef.current.toolInteractionEnabled()) {
9863
+ cursor = "grab";
9864
+ }
9865
+ else if (toolProviders[activeToolIndex]?.cursor) {
9866
+ cursor = toolProviders[activeToolIndex].cursor;
9867
+ }
9868
+ const hasAlpha = texture.textureFormat === -1 || texture.textureFormat === Constants.TEXTUREFORMAT_RGBA;
9869
+ // eslint-disable-next-line @typescript-eslint/naming-convention
9870
+ const CurrentToolSettings = useMemo(() => tools[activeToolIndex]?.settingsComponent, [tools, activeToolIndex]);
9871
+ return (jsxs("div", { className: classes.textureEditor, children: [jsx(PropertiesBar, { texture: texture, saveTexture: saveTexture, pixelData: pixelData, face: face, setFace: setFace, resetTexture: resetTexture, resizeTexture: resizeTexture, uploadTexture: uploadTexture, mipLevel: mipLevel, setMipLevel: setMipLevel, size: canvasManagerRef.current?.size || size }), jsxs("div", { className: classes.mainContent, children: [jsxs("div", { className: classes.canvasContainer, style: { cursor }, children: [jsx("canvas", { ref: uiCanvasRef, className: classes.canvasUI, tabIndex: 1 }), jsx("canvas", { ref: canvas2DRef, className: classes.canvas2D }), jsx("canvas", { ref: canvas3DRef, className: classes.canvas3D })] }), CurrentToolSettings && (jsx("div", { className: classes.toolSettingsContainer, children: jsx(CurrentToolSettings, {}) })), !texture.isCube && (jsx("div", { className: classes.sidebarLeft, children: jsx(ToolBar, { tools: toolProviders, activeToolIndex: activeToolIndex, changeTool: changeTool, metadata: metadata, setMetadata: setMetadata, hasAlpha: hasAlpha }) })), jsx("div", { className: classes.sidebarRight, children: jsx(ChannelsBar, { channels: channels, setChannels: setChannels }) })] }), jsx(StatusBar, { texture: texture, mipLevel: mipLevel })] }));
9872
+ };
9873
+
9874
+ const useStyles$3 = makeStyles({
9875
+ settingsContainer: {
9876
+ display: "flex",
9877
+ flexDirection: "column",
9878
+ gap: tokens.spacingVerticalS,
9879
+ minWidth: "150px",
9880
+ },
9881
+ sliderRow: {
9882
+ display: "flex",
9883
+ flexDirection: "column",
9884
+ gap: tokens.spacingVerticalXS,
9885
+ },
9886
+ icon: {
9887
+ rotate: "-90deg",
9888
+ },
9889
+ });
9890
+ const Contrast = {
9891
+ name: "Contrast/Exposure",
9892
+ order: 500,
9893
+ icon: () => {
9894
+ const classes = useStyles$3();
9895
+ return jsx(CircleHalfFillRegular, { className: classes.icon });
9896
+ },
9897
+ is3D: true,
9898
+ getTool: (context) => {
9899
+ let _contrast = 0;
9900
+ let _exposure = 0;
9901
+ const stateChangedObservable = new Observable();
9902
+ /**
9903
+ * Maps slider values to post processing values using an exponential regression
9904
+ * @param sliderValue - The slider value
9905
+ * @returns exposure value
9906
+ */
9907
+ function computeExposure(sliderValue) {
9908
+ if (sliderValue <= 0) {
9909
+ return 1 - -sliderValue / 100;
9910
+ }
9911
+ else {
9912
+ return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
9913
+ }
9914
+ }
9915
+ /**
9916
+ * Maps slider values to post processing values using an exponential regression
9917
+ * @param sliderValue - The slider value
9918
+ * @returns contrast value
9919
+ */
9920
+ function computeContrast(sliderValue) {
9921
+ if (sliderValue <= 0) {
9922
+ return 1 - -sliderValue / 100;
9923
+ }
9924
+ else {
9925
+ return Math.pow(1.05698, sliderValue) + 0.0000392163 * sliderValue;
9926
+ }
9927
+ }
9928
+ function setExposure(exposure) {
9929
+ _exposure = exposure;
9930
+ stateChangedObservable.notifyObservers();
9931
+ const { scene3D, updateTexture } = context.getParameters();
9932
+ scene3D.imageProcessingConfiguration.isEnabled = true;
9933
+ scene3D.imageProcessingConfiguration.exposure = computeExposure(_exposure);
9934
+ updateTexture();
9935
+ }
9936
+ function setContrast(contrast) {
9937
+ _contrast = contrast;
9938
+ stateChangedObservable.notifyObservers();
9939
+ const { scene3D, updateTexture } = context.getParameters();
9940
+ scene3D.imageProcessingConfiguration.isEnabled = true;
9941
+ scene3D.imageProcessingConfiguration.contrast = computeContrast(_contrast);
9942
+ updateTexture();
9943
+ }
9944
+ return {
9945
+ activate: () => {
9946
+ _contrast = 0;
9947
+ _exposure = 0;
9948
+ setExposure(_exposure);
9949
+ setContrast(_contrast);
9950
+ },
9951
+ deactivate: () => {
9952
+ // No cleanup needed
9953
+ },
9954
+ reset: () => {
9955
+ setExposure(0);
9956
+ setContrast(0);
9957
+ },
9958
+ settingsComponent: () => {
9959
+ const classes = useStyles$3();
9960
+ const [contrast, exposure] = useObservableState(useCallback(() => [_contrast, _exposure], []), stateChangedObservable);
9961
+ const handleContrastChange = (_, data) => {
9962
+ setContrast(data.value);
9963
+ };
9964
+ const handleExposureChange = (_, data) => {
9965
+ setExposure(data.value);
9966
+ };
9967
+ return (jsxs("div", { className: classes.settingsContainer, children: [jsxs("div", { className: classes.sliderRow, children: [jsxs(Label, { children: ["Contrast: ", contrast] }), jsx(Slider, { min: -100, max: 100, value: contrast, onChange: handleContrastChange })] }), jsxs("div", { className: classes.sliderRow, children: [jsxs(Label, { children: ["Exposure: ", exposure] }), jsx(Slider, { min: -100, max: 100, value: exposure, onChange: handleExposureChange })] })] }));
9968
+ },
9969
+ };
9970
+ },
9971
+ };
9972
+
9973
+ /**
9974
+ * Eyedropper tool for picking colors from the texture
9975
+ */
9976
+ const Eyedropper = {
9977
+ name: "Eyedropper",
9978
+ order: 300,
9979
+ icon: () => jsx(EyedropperRegular, {}),
9980
+ cursor: "crosshair",
9981
+ getTool: (context) => {
9982
+ let pointerObserver = null;
9983
+ let isPicking = false;
9984
+ function pick(pointerInfo) {
9985
+ const { canvas2D, setMetadata, getMouseCoordinates } = context.getParameters();
9986
+ const ctx = canvas2D.getContext("2d");
9987
+ const { x, y } = getMouseCoordinates(pointerInfo);
9988
+ const pixel = ctx.getImageData(x, y, 1, 1).data;
9989
+ setMetadata({
9990
+ color: Color3.FromInts(pixel[0], pixel[1], pixel[2]).toHexString(),
9991
+ alpha: pixel[3] / 255,
9992
+ });
9993
+ }
9994
+ return {
9995
+ activate: () => {
9996
+ pointerObserver = context.getParameters().scene.onPointerObservable.add((pointerInfo) => {
9997
+ if (pointerInfo.pickInfo?.hit) {
9998
+ if (pointerInfo.type === PointerEventTypes.POINTERDOWN && pointerInfo.event.buttons === 1 && context.getParameters().interactionEnabled()) {
9999
+ isPicking = true;
10000
+ pick(pointerInfo);
10001
+ }
10002
+ if (isPicking) {
10003
+ if (pointerInfo.event.buttons !== 1 || !context.getParameters().interactionEnabled()) {
10004
+ isPicking = false;
10005
+ }
10006
+ else {
10007
+ pick(pointerInfo);
10008
+ }
10009
+ }
10010
+ }
10011
+ });
10012
+ isPicking = false;
10013
+ },
10014
+ deactivate: () => {
10015
+ pointerObserver?.remove();
10016
+ },
10017
+ };
10018
+ },
10019
+ };
10020
+
10021
+ /**
10022
+ * Floodfill tool for filling regions with a solid color
10023
+ */
10024
+ const Floodfill = {
10025
+ name: "Floodfill",
10026
+ order: 400,
10027
+ icon: () => jsx(PaintBucketRegular, {}),
10028
+ cursor: "crosshair",
10029
+ getTool: (context) => {
10030
+ let pointerObserver = null;
10031
+ async function fillAsync() {
10032
+ const { metadata, startPainting, updatePainting, stopPainting } = context.getParameters();
10033
+ const ctx = await startPainting();
10034
+ ctx.fillStyle = metadata.color;
10035
+ ctx.globalAlpha = metadata.alpha;
10036
+ ctx.globalCompositeOperation = "copy";
10037
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
10038
+ updatePainting();
10039
+ stopPainting();
10040
+ }
10041
+ return {
10042
+ activate: () => {
10043
+ pointerObserver = context.getParameters().scene.onPointerObservable.add((pointerInfo) => {
10044
+ if (pointerInfo.type === PointerEventTypes.POINTERDOWN &&
10045
+ pointerInfo.event.buttons === 1 &&
10046
+ context.getParameters().interactionEnabled() &&
10047
+ pointerInfo.pickInfo?.hit) {
10048
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
10049
+ fillAsync();
10050
+ }
10051
+ });
10052
+ },
10053
+ deactivate: () => {
10054
+ pointerObserver?.remove();
10055
+ },
10056
+ };
10057
+ },
10058
+ };
10059
+
10060
+ const useStyles$2 = makeStyles({
10061
+ settingsContainer: {
10062
+ display: "flex",
10063
+ flexDirection: "column",
10064
+ gap: tokens.spacingVerticalS,
10065
+ minWidth: "150px",
10066
+ },
10067
+ sliderRow: {
10068
+ display: "flex",
10069
+ flexDirection: "column",
10070
+ gap: tokens.spacingVerticalXS,
10071
+ },
10072
+ });
10073
+ const Paintbrush = {
10074
+ name: "Paintbrush",
10075
+ order: 200,
10076
+ icon: () => jsx(InkStrokeRegular, {}),
10077
+ cursor: "crosshair",
10078
+ getTool: (context) => {
10079
+ let pointerObserver = null;
10080
+ let isPainting = false;
10081
+ let _width = 15;
10082
+ let mousePos = null;
10083
+ let ctx = null;
10084
+ let circleCanvas = null;
10085
+ const stateChangedObservable = new Observable();
10086
+ function setWidth(width) {
10087
+ _width = width;
10088
+ stateChangedObservable.notifyObservers();
10089
+ }
10090
+ function paint(pointerInfo) {
10091
+ if (ctx && circleCanvas) {
10092
+ const { getMouseCoordinates, metadata, updatePainting } = context.getParameters();
10093
+ let { x, y } = getMouseCoordinates(pointerInfo);
10094
+ if (metadata.select.x1 !== -1) {
10095
+ x -= metadata.select.x1;
10096
+ y -= metadata.select.y1;
10097
+ }
10098
+ let numSteps, stepVector;
10099
+ stepVector = new Vector2();
10100
+ if (mousePos === null) {
10101
+ mousePos = new Vector2(x, y);
10102
+ numSteps = 1;
10103
+ }
10104
+ else {
10105
+ const maxDistance = _width / 4;
10106
+ const diffVector = new Vector2(x - mousePos.x, y - mousePos.y);
10107
+ numSteps = Math.ceil(diffVector.length() / maxDistance);
10108
+ const trueDistance = diffVector.length() / numSteps;
10109
+ stepVector = diffVector.normalize().multiplyByFloats(trueDistance, trueDistance);
10110
+ }
10111
+ const paintVector = mousePos.clone();
10112
+ for (let stepCount = 0; stepCount < numSteps; stepCount++) {
10113
+ ctx.globalAlpha = 1.0;
10114
+ ctx.globalCompositeOperation = "destination-out";
10115
+ ctx.drawImage(circleCanvas, Math.ceil(paintVector.x - _width / 2), Math.ceil(paintVector.y - _width / 2));
10116
+ ctx.globalAlpha = metadata.alpha;
10117
+ ctx.globalCompositeOperation = "source-over";
10118
+ ctx.drawImage(circleCanvas, Math.ceil(paintVector.x - _width / 2), Math.ceil(paintVector.y - _width / 2));
10119
+ paintVector.addInPlace(stepVector);
10120
+ }
10121
+ updatePainting();
10122
+ mousePos = new Vector2(x, y);
10123
+ }
10124
+ }
10125
+ return {
10126
+ activate: () => {
10127
+ const { scene } = context.getParameters();
10128
+ pointerObserver = scene.onPointerObservable.add(async (pointerInfo) => {
10129
+ const { startPainting, stopPainting, metadata } = context.getParameters();
10130
+ if (!isPainting) {
10131
+ if (pointerInfo.type === PointerEventTypes.POINTERDOWN &&
10132
+ pointerInfo.event.buttons === 1 &&
10133
+ context.getParameters().interactionEnabled() &&
10134
+ pointerInfo.pickInfo?.hit) {
10135
+ isPainting = true;
10136
+ circleCanvas = document.createElement("canvas");
10137
+ circleCanvas.width = _width;
10138
+ circleCanvas.height = _width;
10139
+ const circleCtx = circleCanvas.getContext("2d");
10140
+ circleCtx.imageSmoothingEnabled = false;
10141
+ const pixels = new Array(4 * _width * _width);
10142
+ const dis = (_width * _width) / 4;
10143
+ const rgb = Color3.FromHexString(metadata.color);
10144
+ const r = Math.floor(rgb.r * 255);
10145
+ const g = Math.floor(rgb.g * 255);
10146
+ const b = Math.floor(rgb.b * 255);
10147
+ let idx = 0;
10148
+ const x1 = -Math.floor(_width / 2), x2 = Math.ceil(_width / 2);
10149
+ const y1 = -Math.floor(_width / 2), y2 = Math.ceil(_width / 2);
10150
+ for (let y = y1; y < y2; y++) {
10151
+ for (let x = x1; x < x2; x++) {
10152
+ pixels[idx++] = r;
10153
+ pixels[idx++] = g;
10154
+ pixels[idx++] = b;
10155
+ pixels[idx++] = x * x + y * y <= dis ? 255 : 0;
10156
+ }
10157
+ }
10158
+ circleCtx.putImageData(new ImageData(Uint8ClampedArray.from(pixels), _width, _width), 0, 0);
10159
+ ctx = await startPainting();
10160
+ paint(pointerInfo);
10161
+ }
10162
+ }
10163
+ else {
10164
+ if (pointerInfo.event.buttons !== 1 || !context.getParameters().interactionEnabled()) {
10165
+ isPainting = false;
10166
+ circleCanvas?.parentNode?.removeChild(circleCanvas);
10167
+ stopPainting();
10168
+ mousePos = null;
10169
+ }
10170
+ else {
10171
+ if (pointerInfo.pickInfo?.hit && pointerInfo.type === PointerEventTypes.POINTERMOVE) {
10172
+ paint(pointerInfo);
10173
+ }
10174
+ }
10175
+ }
10176
+ });
10177
+ isPainting = false;
10178
+ },
10179
+ deactivate: () => {
10180
+ isPainting = false;
10181
+ pointerObserver?.remove();
10182
+ },
10183
+ settingsComponent: () => {
10184
+ const classes = useStyles$2();
10185
+ const width = useObservableState(useCallback(() => _width, []), stateChangedObservable);
10186
+ const handleWidthChange = (_, data) => {
10187
+ setWidth(data.value);
10188
+ };
10189
+ return (jsx("div", { className: classes.settingsContainer, children: jsxs("div", { className: classes.sliderRow, children: [jsxs(Label, { children: ["Size: ", width] }), jsx(Slider, { min: 1, max: 100, value: width, onChange: handleWidthChange })] }) }));
10190
+ },
10191
+ };
10192
+ },
10193
+ };
10194
+
10195
+ /**
10196
+ * Rectangle selection tool for selecting regions of the texture
10197
+ */
10198
+ const RectangleSelect = {
10199
+ name: "Rectangle Select",
10200
+ order: 100,
10201
+ icon: () => jsx(SelectObjectRegular, {}),
10202
+ cursor: "crosshair",
10203
+ getTool: (context) => {
10204
+ let pointerObserver = null;
10205
+ let isSelecting = false;
10206
+ let xStart = -1;
10207
+ let yStart = -1;
10208
+ return {
10209
+ activate: () => {
10210
+ const { scene } = context.getParameters();
10211
+ pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
10212
+ const { getMouseCoordinates, setMetadata, metadata } = context.getParameters();
10213
+ if (!isSelecting) {
10214
+ if (pointerInfo.type === PointerEventTypes.POINTERDOWN &&
10215
+ pointerInfo &&
10216
+ pointerInfo.event.buttons === 1 &&
10217
+ context.getParameters().interactionEnabled() &&
10218
+ pointerInfo.pickInfo?.hit) {
10219
+ isSelecting = true;
10220
+ const { x, y } = ({ x: xStart, y: yStart } = getMouseCoordinates(pointerInfo));
10221
+ setMetadata({
10222
+ select: {
10223
+ x1: x,
10224
+ y1: y,
10225
+ x2: x,
10226
+ y2: y,
10227
+ },
10228
+ });
10229
+ }
10230
+ }
10231
+ else {
10232
+ if (pointerInfo.event.buttons !== 1 || !context.getParameters().interactionEnabled()) {
10233
+ isSelecting = false;
10234
+ if (metadata.select.x1 === metadata.select.x2 || metadata.select.y1 === metadata.select.y2) {
10235
+ setMetadata({
10236
+ select: {
10237
+ x1: -1,
10238
+ y1: -1,
10239
+ x2: -1,
10240
+ y2: -1,
10241
+ },
10242
+ });
10243
+ }
10244
+ }
10245
+ else {
10246
+ if (pointerInfo.pickInfo?.hit && pointerInfo.type === PointerEventTypes.POINTERMOVE) {
10247
+ if (pointerInfo.type === PointerEventTypes.POINTERMOVE && isSelecting) {
10248
+ const { x, y } = getMouseCoordinates(pointerInfo);
10249
+ setMetadata({
10250
+ select: {
10251
+ x1: Math.min(x, xStart),
10252
+ y1: Math.min(y, yStart),
10253
+ x2: Math.max(x, xStart),
10254
+ y2: Math.max(y, yStart),
10255
+ },
10256
+ });
10257
+ }
10258
+ }
10259
+ }
10260
+ }
10261
+ });
10262
+ },
10263
+ deactivate() {
10264
+ isSelecting = false;
10265
+ pointerObserver?.remove();
10266
+ },
10267
+ };
10268
+ },
10269
+ };
10270
+
10271
+ const TextureEditorServiceIdentity = Symbol("TextureEditorService");
10272
+ const TextureEditorServiceDefinition = {
10273
+ friendlyName: "Texture Editor",
10274
+ produces: [TextureEditorServiceIdentity],
10275
+ factory: () => {
10276
+ const toolsCollection = new ObservableCollection();
10277
+ // Add the default tools.
10278
+ toolsCollection.add(RectangleSelect);
10279
+ toolsCollection.add(Paintbrush);
10280
+ toolsCollection.add(Eyedropper);
10281
+ toolsCollection.add(Floodfill);
10282
+ toolsCollection.add(Contrast);
10283
+ return {
10284
+ addTool: (toolProvider) => toolsCollection.add(toolProvider),
10285
+ component: (props) => {
10286
+ const tools = useOrderedObservableCollection(toolsCollection);
10287
+ return jsx(TextureEditor, { ...props, toolProviders: tools });
10288
+ },
10289
+ };
10290
+ },
10291
+ };
10292
+
10293
+ // Don't use instanceof in this case as we don't want to bring in the gui package just to check if the entity is an AdvancedDynamicTexture.
10294
+ function IsAdvancedDynamicTexture$1(entity) {
10295
+ return entity?.getClassName?.() === "AdvancedDynamicTexture";
10296
+ }
10297
+ const TexturePropertiesServiceDefinition = {
10298
+ friendlyName: "Texture Properties",
10299
+ consumes: [PropertiesServiceIdentity, SettingsContextIdentity, TextureEditorServiceIdentity],
10300
+ factory: (propertiesService, settingsContext, textureEditorService) => {
8550
10301
  const baseTextureContentRegistration = propertiesService.addSectionContent({
8551
10302
  key: "Base Texture Properties",
8552
10303
  predicate: (entity) => entity instanceof BaseTexture,
8553
10304
  content: [
8554
10305
  {
8555
10306
  section: "Preview",
8556
- component: ({ context }) => jsx(BaseTexturePreviewProperties, { texture: context }),
10307
+ component: ({ context }) => jsx(BaseTexturePreviewProperties, { texture: context, textureEditor: textureEditorService.component }),
8557
10308
  },
8558
10309
  {
8559
10310
  section: "General",
@@ -9832,6 +11583,8 @@ function ShowInspector(scene, options = {}) {
9832
11583
  AnimationGroupPropertiesServiceDefinition,
9833
11584
  MetadataPropertiesServiceDefinition,
9834
11585
  AtmospherePropertiesServiceDefinition,
11586
+ // Texture editor and related services.
11587
+ TextureEditorServiceDefinition,
9835
11588
  // Debug pane tab and related services.
9836
11589
  DebugServiceDefinition,
9837
11590
  // Stats pane tab and related services.
@@ -10666,5 +12419,5 @@ const TextAreaPropertyLine = (props) => {
10666
12419
  // Attach Inspector v2 to Scene.debugLayer as a side effect for back compat.
10667
12420
  AttachDebugLayer();
10668
12421
 
10669
- export { useAngleConverters as $, Accordion as A, ButtonLine as B, Collapse as C, DebugServiceIdentity as D, ExtensibleAccordion as E, FileUploadLine as F, useColor3Property as G, useColor4Property as H, Inspector as I, useQuaternionProperty as J, MakePropertyHook as K, Link as L, MakeLazyComponent as M, NumberDropdownPropertyLine as N, useInterceptObservable as O, Popover as P, useEventfulState as Q, useObservableCollection as R, SwitchPropertyLine as S, ToolsServiceIdentity as T, useOrderedObservableCollection as U, Vector3PropertyLine as V, usePollingObservable as W, useResource as X, useAsyncResource as Y, useCompactMode as Z, useSidePaneDockOverrides as _, SyncedSliderPropertyLine as a, MakeTeachingMoment as a0, MakeDialogTeachingMoment as a1, InterceptFunction as a2, GetPropertyDescriptor as a3, IsPropertyReadonly as a4, InterceptProperty as a5, ObservableCollection as a6, ConstructorFactory as a7, SelectionServiceIdentity as a8, SelectionServiceDefinition as a9, ToggleButton as aA, FactorGradientList as aB, Color3GradientList as aC, Color4GradientList as aD, Pane as aE, BooleanBadgePropertyLine as aF, Color3PropertyLine as aG, Color4PropertyLine as aH, HexPropertyLine as aI, NumberInputPropertyLine as aJ, LinkPropertyLine as aK, PropertyLine as aL, LineContainer as aM, PlaceholderPropertyLine as aN, StringifiedPropertyLine as aO, TextAreaPropertyLine as aP, TextPropertyLine as aQ, RotationVectorPropertyLine as aR, QuaternionPropertyLine as aS, Vector2PropertyLine as aT, Vector4PropertyLine as aU, SettingsContextIdentity as aa, ShowInspector as ab, Checkbox 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, List as aq, MessageBar 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, Button as b, TextInputPropertyLine as c, SpinButtonPropertyLine as d, CheckboxPropertyLine as e, ShellServiceIdentity as f, SceneContextIdentity as g, AccordionSection as h, useExtensionManager as i, MakePopoverTeachingMoment as j, TeachingMoment as k, SidePaneContainer as l, PropertiesServiceIdentity as m, SceneExplorerServiceIdentity as n, SettingsServiceIdentity as o, StatsServiceIdentity as p, ConvertOptions as q, AttachDebugLayer as r, DetachDebugLayer as s, StringDropdownPropertyLine as t, useObservableState as u, BoundProperty as v, LinkToEntityPropertyLine as w, BuiltInsExtensionFeed as x, useProperty as y, useVector3Property as z };
10670
- //# sourceMappingURL=index-BgzFAhky.js.map
12422
+ export { useSidePaneDockOverrides as $, Accordion as A, ButtonLine as B, Collapse as C, DebugServiceIdentity as D, ExtensibleAccordion as E, FileUploadLine as F, useVector3Property as G, useColor3Property as H, Inspector as I, useColor4Property as J, useQuaternionProperty as K, Link as L, MakeLazyComponent as M, NumberDropdownPropertyLine as N, MakePropertyHook as O, Popover as P, useInterceptObservable as Q, useEventfulState as R, SwitchPropertyLine as S, ToolsServiceIdentity as T, useObservableCollection as U, Vector3PropertyLine as V, useOrderedObservableCollection as W, usePollingObservable as X, useResource as Y, useAsyncResource as Z, useCompactMode as _, SyncedSliderPropertyLine as a, useAngleConverters as a0, MakeTeachingMoment as a1, MakeDialogTeachingMoment as a2, InterceptFunction as a3, GetPropertyDescriptor as a4, IsPropertyReadonly as a5, InterceptProperty as a6, ObservableCollection as a7, ConstructorFactory as a8, SelectionServiceIdentity as a9, TextInput as aA, ToggleButton as aB, ChildWindow 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, NumberInputPropertyLine as aL, LinkPropertyLine as aM, PropertyLine as aN, LineContainer as aO, PlaceholderPropertyLine as aP, StringifiedPropertyLine as aQ, TextAreaPropertyLine as aR, TextPropertyLine as aS, RotationVectorPropertyLine as aT, QuaternionPropertyLine as aU, Vector2PropertyLine as aV, Vector4PropertyLine as aW, SelectionServiceDefinition as aa, SettingsContextIdentity as ab, ShowInspector as ac, Checkbox as ad, ColorPickerPopup as ae, InputHexField as af, InputHsvField as ag, ComboBox as ah, DraggableLine as ai, Dropdown as aj, NumberDropdown as ak, StringDropdown as al, FactorGradientComponent as am, Color3GradientComponent as an, Color4GradientComponent as ao, ColorStepGradientComponent as ap, InfoLabel as aq, List as ar, MessageBar as as, PositionedPopover as at, SearchBar as au, SearchBox as av, SpinButton as aw, Switch as ax, SyncedSliderInput as ay, Textarea as az, Button as b, TextInputPropertyLine as c, SpinButtonPropertyLine as d, CheckboxPropertyLine as e, ShellServiceIdentity as f, SceneContextIdentity as g, AccordionSection as h, useExtensionManager as i, MakePopoverTeachingMoment as j, TeachingMoment as k, SidePaneContainer as l, PropertiesServiceIdentity as m, SceneExplorerServiceIdentity as n, SettingsServiceIdentity as o, StatsServiceIdentity as p, ConvertOptions as q, AttachDebugLayer as r, DetachDebugLayer as s, StringDropdownPropertyLine as t, useObservableState as u, BoundProperty as v, LinkToEntityPropertyLine as w, Theme as x, BuiltInsExtensionFeed as y, useProperty as z };
12423
+ //# sourceMappingURL=index-2Bq-qBwV.js.map