@babylonjs/inspector 9.9.0 → 9.9.1

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.
@@ -144,7 +144,9 @@ import { SceneRecorder } from '@babylonjs/core/Misc/sceneRecorder.js';
144
144
  import { VideoRecorder } from '@babylonjs/core/Misc/videoRecorder.js';
145
145
  import { SceneSerializer } from '@babylonjs/core/Misc/sceneSerializer.js';
146
146
  import { EnvironmentTextureTools } from '@babylonjs/core/Misc/environmentTextureTools.js';
147
- import { SerializeSmartAssetManagerMap, GetAllSmartAssets, RemoveSmartAssetAsync, LoadSmartAssetMapAsync, GetSmartAssetTextureExtensions, GetSmartAssetManager, LoadSmartAssetTextureAsync, LoadSmartAssetAsync, ReloadSmartAssetAsync, FindSmartAssetKeyForObject, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.js';
147
+ import { FindSmartAssetKeyForObject, SerializeSmartAssetManagerMap, GetSmartAssetTextureExtensions, GetAllSmartAssets, RemoveSmartAssetAsync, RegisterSmartAsset, LoadAllSmartAssetsAsync, LoadSmartAssetAsync, GetSmartAssetManager, LoadSmartAssetTextureAsync, ReloadSmartAssetAsync, UnloadSmartAssetAsync } from '@babylonjs/core/SmartAssets/smartAssetManager.js';
148
+ import '@babylonjs/core/Loading/Plugins/babylonFileLoader.js';
149
+ import { ReadJsonSourceAsync, ResolveAssetUrl, DeserializeSmartAssetMap } from '@babylonjs/core/SmartAssets/smartAssetSerializer.js';
148
150
  import { ImportAnimationsAsync, SceneLoader } from '@babylonjs/core/Loading/sceneLoader.js';
149
151
  import { FilesInput } from '@babylonjs/core/Misc/filesInput.js';
150
152
  import { registeredGLTFExtensions } from '@babylonjs/loaders/glTF/2.0/glTFLoaderExtensionRegistry.js';
@@ -285,7 +287,7 @@ const Button = forwardRef((props, ref) => {
285
287
  });
286
288
  Button.displayName = "Button";
287
289
 
288
- const useStyles$11 = makeStyles({
290
+ const useStyles$10 = makeStyles({
289
291
  root: {
290
292
  display: "flex",
291
293
  flexDirection: "column",
@@ -366,7 +368,7 @@ class ErrorBoundary extends Component {
366
368
  }
367
369
  }
368
370
  function ErrorFallback({ error, onRetry }) {
369
- const styles = useStyles$11();
371
+ const styles = useStyles$10();
370
372
  return (jsxs("div", { className: styles.root, children: [jsx(ErrorCircleRegular, { className: styles.icon }), jsx("div", { className: styles.title, children: "Something went wrong" }), jsx("div", { className: styles.message, children: "An error occurred in this component. You can try again or continue using other parts of the tool." }), jsx(Button, { label: "Try Again", appearance: "primary", onClick: onRetry }), error && jsx("div", { className: styles.details, children: error.message })] }));
371
373
  }
372
374
 
@@ -1367,7 +1369,7 @@ function useIsSectionEmpty(sectionId) {
1367
1369
  return hasItems;
1368
1370
  }
1369
1371
 
1370
- const useStyles$10 = makeStyles({
1372
+ const useStyles$$ = makeStyles({
1371
1373
  accordion: {
1372
1374
  display: "flex",
1373
1375
  flexDirection: "column",
@@ -1459,7 +1461,7 @@ const useStyles$10 = makeStyles({
1459
1461
  */
1460
1462
  const AccordionMenuBar = () => {
1461
1463
  AccordionMenuBar.displayName = "AccordionMenuBar";
1462
- const classes = useStyles$10();
1464
+ const classes = useStyles$$();
1463
1465
  const accordionCtx = useContext(AccordionContext);
1464
1466
  if (!accordionCtx) {
1465
1467
  return null;
@@ -1483,7 +1485,7 @@ const AccordionMenuBar = () => {
1483
1485
  const AccordionSectionBlock = (props) => {
1484
1486
  AccordionSectionBlock.displayName = "AccordionSectionBlock";
1485
1487
  const { children, sectionId } = props;
1486
- const classes = useStyles$10();
1488
+ const classes = useStyles$$();
1487
1489
  const accordionCtx = useContext(AccordionContext);
1488
1490
  const { context: sectionContext, isEmpty } = useAccordionSectionBlockContext(props);
1489
1491
  if (accordionCtx) {
@@ -1503,7 +1505,7 @@ const AccordionSectionBlock = (props) => {
1503
1505
  const AccordionSectionItem = (props) => {
1504
1506
  AccordionSectionItem.displayName = "AccordionSectionItem";
1505
1507
  const { children, staticItem } = props;
1506
- const classes = useStyles$10();
1508
+ const classes = useStyles$$();
1507
1509
  const accordionCtx = useContext(AccordionContext);
1508
1510
  const itemState = useAccordionSectionItemState(props);
1509
1511
  const [ctrlMode, setCtrlMode] = useState(false);
@@ -1543,7 +1545,7 @@ const AccordionSectionItem = (props) => {
1543
1545
  */
1544
1546
  const AccordionPinnedContainer = () => {
1545
1547
  AccordionPinnedContainer.displayName = "AccordionPinnedContainer";
1546
- const classes = useStyles$10();
1548
+ const classes = useStyles$$();
1547
1549
  const accordionCtx = useContext(AccordionContext);
1548
1550
  return (jsx("div", { ref: accordionCtx?.pinnedContainerRef, className: classes.pinnedContainer, children: jsx(MessageBar$1, { className: classes.pinnedContainerEmpty, children: jsx(MessageBarBody, { children: "No pinned items" }) }) }));
1549
1551
  };
@@ -1554,7 +1556,7 @@ const AccordionPinnedContainer = () => {
1554
1556
  */
1555
1557
  const AccordionSearchBox = () => {
1556
1558
  AccordionSearchBox.displayName = "AccordionSearchBox";
1557
- const classes = useStyles$10();
1559
+ const classes = useStyles$$();
1558
1560
  const accordionCtx = useContext(AccordionContext);
1559
1561
  if (!accordionCtx?.features.search) {
1560
1562
  return null;
@@ -1570,7 +1572,7 @@ const AccordionSearchBox = () => {
1570
1572
  */
1571
1573
  const AccordionSection = (props) => {
1572
1574
  AccordionSection.displayName = "AccordionSection";
1573
- const classes = useStyles$10();
1575
+ const classes = useStyles$$();
1574
1576
  return jsx("div", { className: classes.panelDiv, children: props.children });
1575
1577
  };
1576
1578
  const StringAccordion = Accordion$1;
@@ -1578,7 +1580,7 @@ const Accordion = forwardRef((props, ref) => {
1578
1580
  Accordion.displayName = "Accordion";
1579
1581
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1580
1582
  const { children, highlightSections, uniqueId, enablePinnedItems, enableHiddenItems, enableSearchItems, ...rest } = props;
1581
- const classes = useStyles$10();
1583
+ const classes = useStyles$$();
1582
1584
  const { size } = useContext(ToolContext);
1583
1585
  const accordionCtx = useAccordionContext(props);
1584
1586
  const hasPinning = accordionCtx?.features.pinning ?? false;
@@ -1675,7 +1677,7 @@ const Collapse = (props) => {
1675
1677
  return (jsx(Collapse$1, { visible: props.visible, orientation: props.orientation, unmountOnExit: true, children: jsx("div", { className: `${classes.collapseContent} ${props.orientation === "horizontal" ? classes.horizontal : classes.vertical}`, children: props.children }) }));
1676
1678
  };
1677
1679
 
1678
- const useStyles$$ = makeStyles({
1680
+ const useStyles$_ = makeStyles({
1679
1681
  button: {
1680
1682
  display: "flex",
1681
1683
  alignItems: "center",
@@ -1693,7 +1695,7 @@ const ToggleButton = (props) => {
1693
1695
  ToggleButton.displayName = "ToggleButton";
1694
1696
  const { value, onChange, title, appearance = "subtle" } = props;
1695
1697
  const { size } = useContext(ToolContext);
1696
- const classes = useStyles$$();
1698
+ const classes = useStyles$_();
1697
1699
  const [checked, setChecked] = useState(value);
1698
1700
  const toggle = useCallback(() => {
1699
1701
  setChecked((prevChecked) => {
@@ -2002,7 +2004,7 @@ const UXContextProvider = (props) => {
2002
2004
  function AsReadonlyArray(array) {
2003
2005
  return array;
2004
2006
  }
2005
- const useStyles$_ = makeStyles({
2007
+ const useStyles$Z = makeStyles({
2006
2008
  rootDiv: {
2007
2009
  flex: 1,
2008
2010
  overflow: "hidden",
@@ -2017,7 +2019,7 @@ const useStyles$_ = makeStyles({
2017
2019
  * @returns The extensible accordion component.
2018
2020
  */
2019
2021
  function ExtensibleAccordion(props) {
2020
- const classes = useStyles$_();
2022
+ const classes = useStyles$Z();
2021
2023
  const { children, sections, sectionContent, context, sectionsRef, ...rest } = props;
2022
2024
  const defaultSections = useMemo(() => {
2023
2025
  const defaultSections = [];
@@ -2142,7 +2144,7 @@ function ExtensibleAccordion(props) {
2142
2144
  })] }) })) }));
2143
2145
  }
2144
2146
 
2145
- const useStyles$Z = makeStyles({
2147
+ const useStyles$Y = makeStyles({
2146
2148
  paneRootDiv: {
2147
2149
  display: "flex",
2148
2150
  flex: 1,
@@ -2155,7 +2157,7 @@ const useStyles$Z = makeStyles({
2155
2157
  */
2156
2158
  const SidePaneContainer = forwardRef((props, ref) => {
2157
2159
  const { className, ...rest } = props;
2158
- const classes = useStyles$Z();
2160
+ const classes = useStyles$Y();
2159
2161
  return (jsx("div", { className: mergeClasses(classes.paneRootDiv, className), ref: ref, ...rest, children: props.children }));
2160
2162
  });
2161
2163
 
@@ -2426,7 +2428,7 @@ function useTheme(invert = false) {
2426
2428
  }
2427
2429
 
2428
2430
  // Fluent doesn't apply styling to scrollbars by default, so provide our own reasonable default.
2429
- const useStyles$Y = makeStyles({
2431
+ const useStyles$X = makeStyles({
2430
2432
  root: {
2431
2433
  scrollbarColor: `${tokens.colorNeutralForeground3} ${tokens.colorTransparentBackground}`,
2432
2434
  },
@@ -2442,11 +2444,11 @@ const Theme = (props) => {
2442
2444
  // break any UI within the portal. Therefore, default to false.
2443
2445
  const { invert = false, applyStylesToPortals = false, className, ...rest } = props;
2444
2446
  const theme = useTheme(invert);
2445
- const classes = useStyles$Y();
2447
+ const classes = useStyles$X();
2446
2448
  return (jsx(FluentProvider, { theme: theme, className: mergeClasses(classes.root, className), applyStylesToPortals: applyStylesToPortals, ...rest, children: props.children }));
2447
2449
  };
2448
2450
 
2449
- const useStyles$X = makeStyles({
2451
+ const useStyles$W = makeStyles({
2450
2452
  extensionTeachingPopover: {
2451
2453
  maxWidth: "320px",
2452
2454
  },
@@ -2457,7 +2459,7 @@ const useStyles$X = makeStyles({
2457
2459
  * @returns The teaching moment popover.
2458
2460
  */
2459
2461
  const TeachingMoment = ({ shouldDisplay, positioningRef, onOpenChange, title, description }) => {
2460
- const classes = useStyles$X();
2462
+ const classes = useStyles$W();
2461
2463
  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 })] }) }));
2462
2464
  };
2463
2465
 
@@ -2890,7 +2892,7 @@ const RootComponentServiceIdentity = Symbol("RootComponent");
2890
2892
  * The unique identity symbol for the shell service.
2891
2893
  */
2892
2894
  const ShellServiceIdentity = Symbol("ShellService");
2893
- const useStyles$W = makeStyles({
2895
+ const useStyles$V = makeStyles({
2894
2896
  mainView: {
2895
2897
  flex: 1,
2896
2898
  display: "flex",
@@ -3103,14 +3105,14 @@ const DockMenu = (props) => {
3103
3105
  };
3104
3106
  const PaneHeader = (props) => {
3105
3107
  const { id, title, dockOptions } = props;
3106
- const classes = useStyles$W();
3108
+ const classes = useStyles$V();
3107
3109
  return (jsxs("div", { className: classes.paneHeaderDiv, children: [props.icon && (jsx("div", { className: classes.paneHeaderIcon, children: jsx(props.icon, {}) })), jsx(Subtitle2Stronger, { className: mergeClasses(classes.paneHeaderText, !props.icon && classes.paneHeaderTextNoIcon), children: title }), jsx(DockMenu, { sidePaneId: id, dockOptions: dockOptions, children: jsx(Button$1, { className: classes.paneHeaderButton, appearance: "transparent", icon: jsx(MoreHorizontalRegular, {}) }) })] }));
3108
3110
  };
3109
3111
  // 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.
3110
3112
  const ToolbarItem = (props) => {
3111
3113
  // eslint-disable-next-line @typescript-eslint/naming-convention
3112
3114
  const { verticalLocation, horizontalLocation, id, component: Component, displayName } = props;
3113
- const classes = useStyles$W();
3115
+ const classes = useStyles$V();
3114
3116
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Bar/${verticalLocation}/${horizontalLocation}/${displayName ?? id}`), [displayName, id]);
3115
3117
  const teachingMoment = useTeachingMoment(props.teachingMoment === false);
3116
3118
  const title = typeof props.teachingMoment === "object" ? props.teachingMoment.title : (displayName ?? id);
@@ -3120,7 +3122,7 @@ const ToolbarItem = (props) => {
3120
3122
  // TODO: Handle overflow, possibly via https://react.fluentui.dev/?path=/docs/components-overflow--docs with priority.
3121
3123
  // This component just renders a toolbar with left aligned toolbar items on the left and right aligned toolbar items on the right.
3122
3124
  const Toolbar = ({ location, components }) => {
3123
- const classes = useStyles$W();
3125
+ const classes = useStyles$V();
3124
3126
  const leftComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "left"), [components]);
3125
3127
  const rightComponents = useMemo(() => components.filter((entry) => entry.horizontalLocation === "right"), [components]);
3126
3128
  return (jsx(Fragment, { children: components.length > 0 && (jsxs("div", { className: `${classes.bar} ${location === "top" ? classes.barTop : classes.barBottom}`, 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, teachingMoment: entry.teachingMoment }, 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, teachingMoment: entry.teachingMoment }, entry.key))) })] })) }));
@@ -3130,7 +3132,7 @@ const SidePaneTab = (props) => {
3130
3132
  const { location, id, isSelected, isFirst, isLast, dockOptions,
3131
3133
  // eslint-disable-next-line @typescript-eslint/naming-convention
3132
3134
  icon: Icon, title, } = props;
3133
- const classes = useStyles$W();
3135
+ const classes = useStyles$V();
3134
3136
  const useTeachingMoment = useMemo(() => MakePopoverTeachingMoment(`Pane/${location}/${title ?? id}`), [title, id]);
3135
3137
  const teachingMoment = useTeachingMoment(props.teachingMoment === false);
3136
3138
  const tabClass = mergeClasses(classes.tab, isSelected ? classes.selectedTab : classes.unselectedTab, isFirst ? classes.firstTab : undefined, isLast ? classes.lastTab : undefined);
@@ -3142,7 +3144,7 @@ const SidePaneTab = (props) => {
3142
3144
  // In "compact" mode, the tab list is integrated into the pane itself.
3143
3145
  // In "full" mode, the returned tab list is later injected into the toolbar.
3144
3146
  function usePane(location, defaultWidth, minWidth, sidePanes, onSelectSidePane, dockOperations, toolbarMode, topBarItems, bottomBarItems, initialCollapsed) {
3145
- const classes = useStyles$W();
3147
+ const classes = useStyles$V();
3146
3148
  const [topSelectedTab, setTopSelectedTab] = useState();
3147
3149
  const [bottomSelectedTab, setBottomSelectedTab] = useState();
3148
3150
  const [collapsed, setCollapsed] = useState(initialCollapsed);
@@ -3371,7 +3373,7 @@ function MakeShellServiceDefinition({ leftPaneDefaultWidth = 350, leftPaneMinWid
3371
3373
  expand: () => onCollapseChanged.notifyObservers({ location: "right", collapsed: false }),
3372
3374
  };
3373
3375
  const rootComponent = () => {
3374
- const classes = useStyles$W();
3376
+ const classes = useStyles$V();
3375
3377
  const [sidePaneDockOverrides, setSidePaneDockOverrides] = useSetting(SidePaneDockOverridesSettingDescriptor);
3376
3378
  // This function returns a promise that resolves after the dock change takes effect so that
3377
3379
  // we can then select the re-docked pane.
@@ -3758,13 +3760,13 @@ function useImpulse() {
3758
3760
  return [value, pulse];
3759
3761
  }
3760
3762
 
3761
- const useStyles$V = makeStyles({
3763
+ const useStyles$U = makeStyles({
3762
3764
  placeholderDiv: {
3763
3765
  padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
3764
3766
  },
3765
3767
  });
3766
3768
  const PropertiesPane = (props) => {
3767
- const classes = useStyles$V();
3769
+ const classes = useStyles$U();
3768
3770
  const entity = props.context;
3769
3771
  return entity != null ? (jsx(ExtensibleAccordion, { ...props })) : (jsx("div", { className: classes.placeholderDiv, children: jsx(Body1Strong, { italic: true, children: "No entity selected." }) }));
3770
3772
  };
@@ -4204,7 +4206,7 @@ function CoerceEntityArray(entities, sort) {
4204
4206
  }
4205
4207
  return entities;
4206
4208
  }
4207
- const useStyles$U = makeStyles({
4209
+ const useStyles$T = makeStyles({
4208
4210
  rootDiv: {
4209
4211
  flex: 1,
4210
4212
  overflow: "hidden",
@@ -4313,14 +4315,14 @@ function MakeInlineCommandElement(command, isPlaceholder) {
4313
4315
  }
4314
4316
  const SceneTreeItem = (props) => {
4315
4317
  const { isSelected, select } = props;
4316
- const classes = useStyles$U();
4318
+ const classes = useStyles$T();
4317
4319
  const [compactMode] = useSetting(CompactModeSettingDescriptor);
4318
4320
  const treeItemLayoutClass = mergeClasses(classes.sceneTreeItemLayout, compactMode ? classes.treeItemLayoutCompact : undefined);
4319
4321
  return (jsx(FlatTreeItem, { className: classes.treeItem, 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"));
4320
4322
  };
4321
4323
  const SectionTreeItem = (props) => {
4322
4324
  const { section, isFiltering, commandProviders, expandAll, collapseAll, isDropTarget, ...dropProps } = props;
4323
- const classes = useStyles$U();
4325
+ const classes = useStyles$T();
4324
4326
  const [compactMode] = useSetting(CompactModeSettingDescriptor);
4325
4327
  // Get the commands that apply to this section.
4326
4328
  const commands = useResource(useCallback(() => {
@@ -4337,7 +4339,7 @@ const SectionTreeItem = (props) => {
4337
4339
  };
4338
4340
  const EntityTreeItem = (props) => {
4339
4341
  const { entityItem, isSelected, select, isFiltering, commandProviders, expandAll, collapseAll, isDragging, isDropTarget, ...dragProps } = props;
4340
- const classes = useStyles$U();
4342
+ const classes = useStyles$T();
4341
4343
  const [compactMode] = useSetting(CompactModeSettingDescriptor);
4342
4344
  const hasChildren = !!entityItem.children?.length;
4343
4345
  const displayInfo = useResource(useCallback(() => {
@@ -4453,7 +4455,7 @@ const EntityTreeItem = (props) => {
4453
4455
  }, children: jsx(Tooltip$1, { content: name, relationship: "description", children: jsx(Body1, { wrap: false, truncate: true, children: name }) }) }) }, GetEntityId$1(entityItem.entity)) }), jsx(MenuPopover, { hidden: !hasChildren && contextMenuCommands.length === 0, children: jsxs(MenuList, { children: [hasChildren && (jsxs(Fragment, { children: [jsx(MenuItem, { icon: jsx(ArrowExpandAllRegular, {}), onClick: expandAll, children: jsx(Body1, { children: "Expand All" }) }), jsx(MenuItem, { icon: jsx(ArrowCollapseAllRegular, {}), onClick: collapseAll, children: jsx(Body1, { children: "Collapse All" }) })] })), hasChildren && contextMenuCommands.length > 0 && jsx(MenuDivider, {}), contextMenuItems] }) })] }));
4454
4456
  };
4455
4457
  const SceneExplorer = (props) => {
4456
- const classes = useStyles$U();
4458
+ const classes = useStyles$T();
4457
4459
  const { sections, entityCommandProviders, sectionCommandProviders, scene, selectedEntity = null } = props;
4458
4460
  const [openItems, setOpenItems] = useState(new Set());
4459
4461
  const [sceneVersion, setSceneVersion] = useState(0);
@@ -6079,7 +6081,7 @@ class CanvasGraphService {
6079
6081
  }
6080
6082
  }
6081
6083
 
6082
- const useStyles$T = makeStyles({
6084
+ const useStyles$S = makeStyles({
6083
6085
  canvas: {
6084
6086
  flexGrow: 1,
6085
6087
  width: "100%",
@@ -6088,7 +6090,7 @@ const useStyles$T = makeStyles({
6088
6090
  });
6089
6091
  const CanvasGraph = (props) => {
6090
6092
  const { collector, scene, layoutObservable, returnToPlayheadObservable, onVisibleRangeChangedObservable, initialGraphSize } = props;
6091
- const classes = useStyles$T();
6093
+ const classes = useStyles$S();
6092
6094
  const canvasRef = useRef(null);
6093
6095
  useEffect(() => {
6094
6096
  if (!canvasRef.current) {
@@ -6153,19 +6155,31 @@ function CoerceStepValue(step, isFineKeyPressed, isCourseKeyPressed) {
6153
6155
  }
6154
6156
  return step;
6155
6157
  }
6156
- // Allow arbitrary expressions, primarily for math operations (e.g. 10*60 for 10 minutes in seconds).
6157
- // Use Function constructor to safely evaluate the expression without allowing access to scope.
6158
- // If the expression is invalid, fallback to NaN which will be caught by validateValue and prevent committing.
6158
+ // Parse the raw input into a number, supporting plain numeric values and arbitrary math expressions
6159
+ // (e.g. "10*60" for 10 minutes in seconds).
6160
+ // First, try Number() so plain numeric input works even under a strict Content-Security-Policy that
6161
+ // disallows eval/Function. Only fall back to the Function constructor for non-numeric inputs that may
6162
+ // be expressions. Empty/whitespace input returns NaN so validateValue rejects it rather than committing
6163
+ // 0 (which is what Number("") would otherwise return).
6164
+ // Non-finite results (NaN, +/-Infinity) are rejected from both paths so callers don't have to handle them.
6159
6165
  function EvaluateExpression(rawValue) {
6160
- const val = rawValue.trim();
6166
+ rawValue = rawValue.trim();
6167
+ if (rawValue.length === 0) {
6168
+ return NaN;
6169
+ }
6170
+ const value = Number(rawValue);
6171
+ if (Number.isFinite(value)) {
6172
+ return value;
6173
+ }
6161
6174
  try {
6162
- return Number(Function(`"use strict";return (${val})`)());
6175
+ const result = Number(Function(`"use strict";return (${rawValue})`)());
6176
+ return Number.isFinite(result) ? result : NaN;
6163
6177
  }
6164
6178
  catch {
6165
6179
  return NaN;
6166
6180
  }
6167
6181
  }
6168
- const useStyles$S = makeStyles({
6182
+ const useStyles$R = makeStyles({
6169
6183
  icon: {
6170
6184
  "&:hover": {
6171
6185
  color: tokens.colorBrandForeground1,
@@ -6179,7 +6193,7 @@ const useStyles$S = makeStyles({
6179
6193
  const SpinButton = forwardRef((props, ref) => {
6180
6194
  SpinButton.displayName = "SpinButton2";
6181
6195
  const inputClasses = useInputStyles$1();
6182
- const classes = useStyles$S();
6196
+ const classes = useStyles$R();
6183
6197
  const { size } = useContext(ToolContext);
6184
6198
  const { min, max } = props;
6185
6199
  const baseStep = props.step ?? 1;
@@ -6446,7 +6460,7 @@ const Dropdown = (props) => {
6446
6460
  const NumberDropdown = Dropdown;
6447
6461
  const StringDropdown = Dropdown;
6448
6462
 
6449
- const useStyles$R = makeStyles({
6463
+ const useStyles$Q = makeStyles({
6450
6464
  surface: {
6451
6465
  maxWidth: "400px",
6452
6466
  },
@@ -6461,7 +6475,7 @@ const useStyles$R = makeStyles({
6461
6475
  const Popover = forwardRef((props, ref) => {
6462
6476
  const { children, open: controlledOpen, onOpenChange, positioning, surfaceClassName } = props;
6463
6477
  const [internalOpen, setInternalOpen] = useState(false);
6464
- const classes = useStyles$R();
6478
+ const classes = useStyles$Q();
6465
6479
  const isControlled = controlledOpen !== undefined;
6466
6480
  const popoverOpen = isControlled ? controlledOpen : internalOpen;
6467
6481
  const handleOpenChange = (_, data) => {
@@ -6705,7 +6719,7 @@ const InputAlphaField = (props) => {
6705
6719
  } }));
6706
6720
  };
6707
6721
 
6708
- const useStyles$Q = makeStyles({
6722
+ const useStyles$P = makeStyles({
6709
6723
  sidebar: {
6710
6724
  display: "flex",
6711
6725
  flexDirection: "column",
@@ -6769,7 +6783,7 @@ const useStyles$Q = makeStyles({
6769
6783
  });
6770
6784
  const PerformanceSidebar = (props) => {
6771
6785
  const { collector, onVisibleRangeChangedObservable } = props;
6772
- const classes = useStyles$Q();
6786
+ const classes = useStyles$P();
6773
6787
  // Map from id to IPerfMetadata information
6774
6788
  const [metadataMap, setMetadataMap] = useState();
6775
6789
  // Map from category to all the ids belonging to that category
@@ -6842,7 +6856,7 @@ const PerformanceSidebar = (props) => {
6842
6856
  })] }, `category-${category || "version"}`))) }));
6843
6857
  };
6844
6858
 
6845
- const useStyles$P = makeStyles({
6859
+ const useStyles$O = makeStyles({
6846
6860
  container: {
6847
6861
  display: "flex",
6848
6862
  flexDirection: "row",
@@ -6871,7 +6885,7 @@ const useStyles$P = makeStyles({
6871
6885
  });
6872
6886
  const PerformanceViewer = (props) => {
6873
6887
  const { scene, layoutObservable, returnToLiveObservable, performanceCollector, initialGraphSize } = props;
6874
- const classes = useStyles$P();
6888
+ const classes = useStyles$O();
6875
6889
  const [onVisibleRangeChangedObservable] = useState(() => new Observable());
6876
6890
  const onReturnToPlayheadClick = () => {
6877
6891
  returnToLiveObservable.notifyObservers();
@@ -7038,14 +7052,14 @@ const TextPropertyLine = (props) => {
7038
7052
  return (jsx(PropertyLine, { ...props, children: jsx(Body1, { title: title, children: value ?? "" }) }));
7039
7053
  };
7040
7054
 
7041
- const useStyles$O = makeStyles({
7055
+ const useStyles$N = makeStyles({
7042
7056
  pinnedStatsPane: {
7043
7057
  flex: "0 1 auto",
7044
7058
  paddingBottom: tokens.spacingHorizontalM,
7045
7059
  },
7046
7060
  });
7047
7061
  const StatsPane = (props) => {
7048
- const classes = useStyles$O();
7062
+ const classes = useStyles$N();
7049
7063
  const scene = props.context;
7050
7064
  const engine = scene.getEngine();
7051
7065
  const pollingObservable = usePollingObservable(250);
@@ -7208,7 +7222,7 @@ const ToolsServiceDefinition = {
7208
7222
  */
7209
7223
  const ReactContextServiceIdentity = Symbol("ReactContextService");
7210
7224
 
7211
- const useStyles$N = makeStyles({
7225
+ const useStyles$M = makeStyles({
7212
7226
  dropdown: {
7213
7227
  ...UniformWidthStyling,
7214
7228
  },
@@ -7220,7 +7234,7 @@ const useStyles$N = makeStyles({
7220
7234
  */
7221
7235
  const DropdownPropertyLine = forwardRef((props, ref) => {
7222
7236
  DropdownPropertyLine.displayName = "DropdownPropertyLine";
7223
- const classes = useStyles$N();
7237
+ const classes = useStyles$M();
7224
7238
  return (jsx(PropertyLine, { ...props, ref: ref, children: jsx(Dropdown, { ...props, className: classes.dropdown }) }));
7225
7239
  });
7226
7240
  /**
@@ -7378,7 +7392,7 @@ const SyncedSliderInput = (props) => {
7378
7392
  return (jsxs("div", { className: mergeClasses(classes.container, props.className), children: [infoLabel && jsx(InfoLabel, { ...infoLabel, htmlFor: "syncedSlider" }), jsxs("div", { id: "syncedSlider", className: classes.syncedSlider, children: [hasSlider && (jsx(Slider, { className: getSliderClassName(), value: value, onChange: handleSliderChange, min: props.min, max: props.max, step: props.step, disabled: props.disabled, onPointerDown: handleSliderPointerDown, onPointerUp: handleSliderPointerUp })), jsx(SpinButton, { ...passthroughProps, className: useCompactSizing ? classes.compactSpinButton : classes.spinButton, inputClassName: useCompactSizing ? classes.compactSpinButtonInput : classes.spinButtonInput, value: value, onChange: handleInputChange, step: props.step, disabled: props.disabled, disableDragButton: true })] })] }));
7379
7393
  };
7380
7394
 
7381
- const useStyles$M = makeStyles({
7395
+ const useStyles$L = makeStyles({
7382
7396
  uniformWidth: {
7383
7397
  ...UniformWidthStyling,
7384
7398
  },
@@ -7390,7 +7404,7 @@ const useStyles$M = makeStyles({
7390
7404
  */
7391
7405
  const SyncedSliderPropertyLine = forwardRef((props, ref) => {
7392
7406
  SyncedSliderPropertyLine.displayName = "SyncedSliderPropertyLine";
7393
- const classes = useStyles$M();
7407
+ const classes = useStyles$L();
7394
7408
  const { label, description, ...sliderProps } = props;
7395
7409
  return (jsx(PropertyLine, { ref: ref, ...props, children: jsx(SyncedSliderInput, { ...sliderProps, className: mergeClasses(classes.uniformWidth, props.className) }) }));
7396
7410
  });
@@ -8350,7 +8364,7 @@ function useExtensionManager() {
8350
8364
  return useContext(ExtensionManagerContext)?.extensionManager;
8351
8365
  }
8352
8366
 
8353
- const useStyles$L = makeStyles({
8367
+ const useStyles$K = makeStyles({
8354
8368
  themeButton: {
8355
8369
  margin: 0,
8356
8370
  },
@@ -8369,7 +8383,7 @@ const ThemeSelectorServiceDefinition = {
8369
8383
  teachingMoment: false,
8370
8384
  order: -300,
8371
8385
  component: () => {
8372
- const classes = useStyles$L();
8386
+ const classes = useStyles$K();
8373
8387
  const { isDarkMode, themeMode, setThemeMode } = useThemeMode();
8374
8388
  const onSelectedThemeChange = useCallback((e, data) => {
8375
8389
  setThemeMode(data.checkedItems.includes("System") ? "system" : data.checkedItems[0].toLocaleLowerCase());
@@ -8386,7 +8400,7 @@ const ThemeSelectorServiceDefinition = {
8386
8400
  },
8387
8401
  };
8388
8402
 
8389
- const useStyles$K = makeStyles({
8403
+ const useStyles$J = makeStyles({
8390
8404
  app: {
8391
8405
  colorScheme: "light dark",
8392
8406
  flexGrow: 1,
@@ -8431,7 +8445,7 @@ function MakeModularTool(options) {
8431
8445
  // This deferred resolves once the React effect cleanup (which disposes the ServiceContainer) is complete.
8432
8446
  const disposeDeferred = new Deferred();
8433
8447
  const modularToolRootComponent = () => {
8434
- const classes = useStyles$K();
8448
+ const classes = useStyles$J();
8435
8449
  const [extensionManagerContext, setExtensionManagerContext] = useState();
8436
8450
  const [requiredExtensions, setRequiredExtensions] = useState();
8437
8451
  const [requiredExtensionsDeferred, setRequiredExtensionsDeferred] = useState();
@@ -8518,7 +8532,7 @@ function MakeModularTool(options) {
8518
8532
  }
8519
8533
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
8520
8534
  if (extensionFeeds.length > 0) {
8521
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-DTrjNf_v.js');
8535
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-Drj94Pma.js');
8522
8536
  serviceContainer.addService(ExtensionListServiceDefinition);
8523
8537
  }
8524
8538
  // Register all external services (that make up a unique tool).
@@ -8639,14 +8653,14 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
8639
8653
  description: "Adds a new panel for easy creation of various Babylon assets. This is a WIP extension...expect changes!",
8640
8654
  keywords: ["creation", "tools"],
8641
8655
  ...BabylonWebResources,
8642
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-8d6rBO-A.js'),
8656
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-CXUBvaDf.js'),
8643
8657
  },
8644
8658
  {
8645
8659
  name: "Reflector",
8646
8660
  description: "Connects to the Reflector Bridge for real-time scene synchronization with the Babylon.js Sandbox.",
8647
8661
  keywords: ["reflector", "bridge", "sync", "sandbox", "tools"],
8648
8662
  ...BabylonWebResources,
8649
- getExtensionModuleAsync: async () => await import('./reflectorService-B7LcD1Sn.js'),
8663
+ getExtensionModuleAsync: async () => await import('./reflectorService-D3JRLq4p.js'),
8650
8664
  },
8651
8665
  ]);
8652
8666
 
@@ -9687,7 +9701,7 @@ const ColorSliders = ({ color, onSliderChange }) => (jsxs(Fragment, { children:
9687
9701
  const Color3PropertyLine = ColorPropertyLine;
9688
9702
  const Color4PropertyLine = ColorPropertyLine;
9689
9703
 
9690
- const useStyles$J = makeStyles({
9704
+ const useStyles$I = makeStyles({
9691
9705
  uniformWidth: {
9692
9706
  ...UniformWidthStyling,
9693
9707
  },
@@ -9699,7 +9713,7 @@ const useStyles$J = makeStyles({
9699
9713
  */
9700
9714
  const TextInputPropertyLine = (props) => {
9701
9715
  TextInputPropertyLine.displayName = "TextInputPropertyLine";
9702
- const classes = useStyles$J();
9716
+ const classes = useStyles$I();
9703
9717
  return (jsx(PropertyLine, { ...props, children: jsx(TextInput, { ...props, className: mergeClasses(classes.uniformWidth, props.className) }) }));
9704
9718
  };
9705
9719
  /**
@@ -9710,7 +9724,7 @@ const TextInputPropertyLine = (props) => {
9710
9724
  */
9711
9725
  const NumberInputPropertyLine = (props) => {
9712
9726
  NumberInputPropertyLine.displayName = "NumberInputPropertyLine";
9713
- const classes = useStyles$J();
9727
+ const classes = useStyles$I();
9714
9728
  return (jsx(PropertyLine, { ...props, children: jsx(SpinButton, { ...props, className: mergeClasses(classes.uniformWidth, props.className) }) }));
9715
9729
  };
9716
9730
 
@@ -9842,7 +9856,7 @@ const LegacyInspectableObjectPropertiesServiceDefinition = {
9842
9856
  };
9843
9857
 
9844
9858
  const DocUrl = "https://www.npmjs.com/package/@babylonjs/inspector#inspector-cli";
9845
- const useStyles$I = makeStyles({
9859
+ const useStyles$H = makeStyles({
9846
9860
  tooltipContent: {
9847
9861
  display: "flex",
9848
9862
  flexDirection: "column",
@@ -9860,7 +9874,7 @@ const CliConnectionStatusServiceDefinition = {
9860
9874
  teachingMoment: false,
9861
9875
  order: 0 /* DefaultToolbarItemOrder.CliStatus */,
9862
9876
  component: () => {
9863
- const classes = useStyles$I();
9877
+ const classes = useStyles$H();
9864
9878
  const isEnabled = useObservableState(() => cliConnectionStatus.isEnabled, cliConnectionStatus.onConnectionStatusChanged);
9865
9879
  const isConnected = useObservableState(() => cliConnectionStatus.isConnected, cliConnectionStatus.onConnectionStatusChanged);
9866
9880
  const { showToast } = useToast();
@@ -9921,7 +9935,7 @@ const BreakTangentIcon = createFluentIcon("BreakTangent", "20", '<g transform="s
9921
9935
  const UnifyTangentIcon = createFluentIcon("UnifyTangent", "20", '<g transform="scale(0.5)"><path d="M27.94,18.28a1.49,1.49,0,0,0-1.41,1h-5l-1.62-1.63-1.62,1.63h-5a1.5,1.5,0,1,0,0,1h5l1.62,1.62,1.62-1.62h5a1.5,1.5,0,1,0,1.41-2Z"/></g>');
9922
9936
  const StepTangentIcon = createFluentIcon("StepTangent", "20", '<g transform="scale(0.5)"><path d="M29,16.71a1.5,1.5,0,1,0-2,1.41v5.67H11v1H28V18.12A1.51,1.51,0,0,0,29,16.71Z"/></g>');
9923
9937
 
9924
- const useStyles$H = makeStyles({
9938
+ const useStyles$G = makeStyles({
9925
9939
  coordinatesModeButton: {
9926
9940
  margin: `0 0 0 ${tokens.spacingHorizontalXS}`,
9927
9941
  },
@@ -9937,7 +9951,7 @@ const useStyles$H = makeStyles({
9937
9951
  });
9938
9952
  const GizmoToolbar = (props) => {
9939
9953
  const { gizmoService, sceneContext } = props;
9940
- const classes = useStyles$H();
9954
+ const classes = useStyles$G();
9941
9955
  const gizmoMode = useObservableState(() => gizmoService.gizmoMode, gizmoService.onGizmoModeChanged);
9942
9956
  const coordinatesMode = useObservableState(() => gizmoService.coordinatesMode, gizmoService.onCoordinatesModeChanged);
9943
9957
  const cameraGizmo = useObservableState(() => gizmoService.gizmoCamera, gizmoService.onCameraGizmoChanged);
@@ -10066,7 +10080,7 @@ const HighlightServiceDefinition = {
10066
10080
  },
10067
10081
  };
10068
10082
 
10069
- const useStyles$G = makeStyles({
10083
+ const useStyles$F = makeStyles({
10070
10084
  badge: {
10071
10085
  margin: tokens.spacingHorizontalXXS,
10072
10086
  fontFamily: "monospace",
@@ -10083,7 +10097,7 @@ const MiniStatsServiceDefinition = {
10083
10097
  order: 300 /* DefaultToolbarItemOrder.FrameRate */,
10084
10098
  teachingMoment: false,
10085
10099
  component: () => {
10086
- const classes = useStyles$G();
10100
+ const classes = useStyles$F();
10087
10101
  const scene = useObservableState(useCallback(() => sceneContext.currentScene, [sceneContext.currentScene]), sceneContext.currentSceneObservable);
10088
10102
  const engine = scene?.getEngine();
10089
10103
  const pollingObservable = usePollingObservable(250);
@@ -10412,7 +10426,7 @@ function useCurveEditor() {
10412
10426
  return context;
10413
10427
  }
10414
10428
 
10415
- const useStyles$F = makeStyles({
10429
+ const useStyles$E = makeStyles({
10416
10430
  root: {
10417
10431
  display: "flex",
10418
10432
  flexDirection: "row",
@@ -10456,7 +10470,7 @@ const useStyles$F = makeStyles({
10456
10470
  * @returns The top bar component
10457
10471
  */
10458
10472
  const TopBar = () => {
10459
- const styles = useStyles$F();
10473
+ const styles = useStyles$E();
10460
10474
  const { state, observables } = useCurveEditor();
10461
10475
  const [keyFrameValue, setKeyFrameValue] = useState(null);
10462
10476
  const [keyValue, setKeyValue] = useState(null);
@@ -10549,7 +10563,7 @@ const ColorChannelColors = {
10549
10563
  */
10550
10564
  const DefaultCurveColor = "#ffffff";
10551
10565
 
10552
- const useStyles$E = makeStyles({
10566
+ const useStyles$D = makeStyles({
10553
10567
  root: {
10554
10568
  display: "flex",
10555
10569
  flexDirection: "column",
@@ -10591,7 +10605,7 @@ const LOOP_MODES$1 = [
10591
10605
  * @returns The edit animation panel component
10592
10606
  */
10593
10607
  const EditAnimationPanel = ({ animation, onClose }) => {
10594
- const styles = useStyles$E();
10608
+ const styles = useStyles$D();
10595
10609
  const { observables } = useCurveEditor();
10596
10610
  const [name, setName] = useState(animation.name);
10597
10611
  const [property, setProperty] = useState(animation.targetProperty);
@@ -10622,7 +10636,7 @@ const EditAnimationPanel = ({ animation, onClose }) => {
10622
10636
  return (jsxs("div", { className: styles.root, children: [jsxs("div", { className: styles.form, children: [jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Display Name" }), jsx(Input, { value: name, onChange: (_, data) => setName(data.value), placeholder: "Animation name" })] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Property" }), jsx(Input, { value: property, onChange: (_, data) => setProperty(data.value), placeholder: "e.g., position, rotation, scaling" })] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Loop Mode" }), jsx(Dropdown$1, { value: getLoopModeLabel(loopMode), selectedOptions: [loopMode.toString()], onOptionSelect: (_, data) => setLoopMode(Number(data.optionValue)), positioning: "below", inlinePopup: true, children: LOOP_MODES$1.map((mode) => (jsx(Option, { value: mode.value.toString(), children: mode.label }, mode.value))) })] })] }), jsxs("div", { className: styles.buttons, children: [jsx(Button, { appearance: "primary", onClick: saveChanges, label: "Save", disabled: !isValid }), jsx(Button, { appearance: "subtle", onClick: onClose, label: "Cancel" })] })] }));
10623
10637
  };
10624
10638
 
10625
- const useStyles$D = makeStyles({
10639
+ const useStyles$C = makeStyles({
10626
10640
  root: {
10627
10641
  display: "flex",
10628
10642
  flexDirection: "column",
@@ -10688,7 +10702,7 @@ const useStyles$D = makeStyles({
10688
10702
  * @returns Animation entry component
10689
10703
  */
10690
10704
  const AnimationEntry = ({ animation }) => {
10691
- const styles = useStyles$D();
10705
+ const styles = useStyles$C();
10692
10706
  const { state, actions, observables } = useCurveEditor();
10693
10707
  const [isExpanded, setIsExpanded] = useState(false);
10694
10708
  const [isHovered, setIsHovered] = useState(false);
@@ -10767,7 +10781,7 @@ const AnimationEntry = ({ animation }) => {
10767
10781
  * @returns Animation sub-entry component
10768
10782
  */
10769
10783
  const AnimationSubEntry = ({ animation, subName, color }) => {
10770
- const styles = useStyles$D();
10784
+ const styles = useStyles$C();
10771
10785
  const { actions, observables } = useCurveEditor();
10772
10786
  const activeChannel = actions.getActiveChannel(animation);
10773
10787
  const isThisChannelActive = activeChannel === color;
@@ -10790,7 +10804,7 @@ const AnimationSubEntry = ({ animation, subName, color }) => {
10790
10804
  * @returns Animation list component
10791
10805
  */
10792
10806
  const AnimationList = () => {
10793
- const styles = useStyles$D();
10807
+ const styles = useStyles$C();
10794
10808
  const { state, observables } = useCurveEditor();
10795
10809
  // Re-render when animations are loaded or changed (e.g. animation deleted)
10796
10810
  // useCallback stabilizes the accessor to prevent infinite re-render loops
@@ -10803,7 +10817,7 @@ const AnimationList = () => {
10803
10817
  }) }));
10804
10818
  };
10805
10819
 
10806
- const useStyles$C = makeStyles({
10820
+ const useStyles$B = makeStyles({
10807
10821
  root: {
10808
10822
  display: "flex",
10809
10823
  flexDirection: "column",
@@ -10852,7 +10866,7 @@ const LoopModeOptions = LOOP_MODES.map((lm) => ({ label: lm, value: lm }));
10852
10866
  * @returns The add animation panel component
10853
10867
  */
10854
10868
  const AddAnimationPanel = ({ onClose }) => {
10855
- const styles = useStyles$C();
10869
+ const styles = useStyles$B();
10856
10870
  const { state, actions, observables } = useCurveEditor();
10857
10871
  const [name, setName] = useState("");
10858
10872
  const [mode, setMode] = useState("List");
@@ -11069,7 +11083,7 @@ const AddAnimationPanel = ({ onClose }) => {
11069
11083
  return (jsxs("div", { className: styles.root, children: [jsx("div", { className: styles.header, children: jsx("div", { className: styles.title, children: "Add Animation" }) }), jsxs("div", { className: styles.form, children: [jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Display Name" }), jsx(TextInput, { value: name, onChange: setName })] }), jsx("div", { className: styles.row, children: jsx(StringDropdown, { value: mode, onChange: (val) => setMode(val), options: ModeOptions, disabled: properties.length === 0, infoLabel: { label: "Mode" } }) }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Property" }), isCustomMode ? (jsx(TextInput, { value: customProperty, onChange: setCustomProperty })) : (jsx(StringDropdown, { value: selectedProperty, onChange: (val) => setSelectedProperty(val), options: properties.map((p) => ({ label: p, value: p })) }))] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Type" }), isCustomMode ? (jsx(StringDropdown, { value: animationType, onChange: (val) => setAnimationType(val), options: AnimationTypeOptions })) : (jsx("div", { className: styles.typeDisplay, children: inferredType }))] }), jsx("div", { className: styles.row, children: jsx(StringDropdown, { value: loopMode, onChange: (val) => setLoopMode(val), options: LoopModeOptions, infoLabel: { label: "Loop Mode" } }) })] }), jsxs("div", { className: styles.buttons, children: [jsx(Button, { appearance: "primary", onClick: createAnimation, disabled: !isValid, label: "Create" }), jsx(Button, { appearance: "subtle", onClick: onClose, label: "Cancel" })] })] }));
11070
11084
  };
11071
11085
 
11072
- const useStyles$B = makeStyles({
11086
+ const useStyles$A = makeStyles({
11073
11087
  root: {
11074
11088
  display: "flex",
11075
11089
  flexDirection: "column",
@@ -11112,7 +11126,7 @@ const useStyles$B = makeStyles({
11112
11126
  * @returns The load animation panel component
11113
11127
  */
11114
11128
  const LoadAnimationPanel = ({ onClose }) => {
11115
- const styles = useStyles$B();
11129
+ const styles = useStyles$A();
11116
11130
  const { state, actions, observables } = useCurveEditor();
11117
11131
  const [snippetIdInput, setSnippetIdInput] = useState("");
11118
11132
  const [loadedSnippetId, setLoadedSnippetId] = useState(null);
@@ -11251,7 +11265,7 @@ class StringTools {
11251
11265
  }
11252
11266
  }
11253
11267
 
11254
- const useStyles$A = makeStyles({
11268
+ const useStyles$z = makeStyles({
11255
11269
  root: {
11256
11270
  display: "flex",
11257
11271
  flexDirection: "column",
@@ -11296,7 +11310,7 @@ const useStyles$A = makeStyles({
11296
11310
  * @returns The save animation panel component
11297
11311
  */
11298
11312
  const SaveAnimationPanel = ({ onClose: _onClose }) => {
11299
- const styles = useStyles$A();
11313
+ const styles = useStyles$z();
11300
11314
  const { state } = useCurveEditor();
11301
11315
  const [selectedAnimations, setSelectedAnimations] = useState(() => {
11302
11316
  if (!state.animations) {
@@ -11369,7 +11383,7 @@ const SaveAnimationPanel = ({ onClose: _onClose }) => {
11369
11383
  }) }), jsxs("div", { className: styles.buttons, children: [jsx(Button, { appearance: "primary", onClick: saveToSnippetServer, disabled: selectedAnimations.length === 0 || isSaving, label: isSaving ? "Saving..." : "Save to Snippet Server" }), jsx(Button, { appearance: "secondary", onClick: saveToFile, disabled: selectedAnimations.length === 0, label: "Save to File" })] }), saveError && jsx("div", { className: styles.errorText, children: saveError }), snippetId && jsxs("div", { className: styles.snippetId, children: ["Saved! Snippet ID: ", snippetId] })] }));
11370
11384
  };
11371
11385
 
11372
- const useStyles$z = makeStyles({
11386
+ const useStyles$y = makeStyles({
11373
11387
  root: {
11374
11388
  display: "flex",
11375
11389
  flexDirection: "column",
@@ -11423,7 +11437,7 @@ const useStyles$z = makeStyles({
11423
11437
  * @returns The sidebar component
11424
11438
  */
11425
11439
  const SideBar = () => {
11426
- const styles = useStyles$z();
11440
+ const styles = useStyles$y();
11427
11441
  const { state, actions, observables } = useCurveEditor();
11428
11442
  const [openPopover, setOpenPopover] = useState(null);
11429
11443
  const [fps, setFps] = useState(60);
@@ -12411,7 +12425,7 @@ const KeyPointComponent = (props) => {
12411
12425
  } })] }))] }))] }));
12412
12426
  };
12413
12427
 
12414
- const useStyles$y = makeStyles({
12428
+ const useStyles$x = makeStyles({
12415
12429
  root: {
12416
12430
  position: "absolute",
12417
12431
  top: 0,
@@ -12595,7 +12609,7 @@ function ExtractValuesFromKeys(keys, curves) {
12595
12609
  * @returns The graph component
12596
12610
  */
12597
12611
  const Graph = ({ width, height }) => {
12598
- const styles = useStyles$y();
12612
+ const styles = useStyles$x();
12599
12613
  const { state, actions, observables } = useCurveEditor();
12600
12614
  const svgRef = useRef(null);
12601
12615
  const [scale, setScale] = useState(1);
@@ -12958,7 +12972,7 @@ const Graph = ({ width, height }) => {
12958
12972
  }), renderValueAxis()] })] }));
12959
12973
  };
12960
12974
 
12961
- const useStyles$x = makeStyles({
12975
+ const useStyles$w = makeStyles({
12962
12976
  root: {
12963
12977
  position: "absolute",
12964
12978
  top: 0,
@@ -13001,7 +13015,7 @@ const useStyles$x = makeStyles({
13001
13015
  * @returns The playhead component
13002
13016
  */
13003
13017
  const PlayHead = ({ width, height: _height }) => {
13004
- const styles = useStyles$x();
13018
+ const styles = useStyles$w();
13005
13019
  const { state, actions, observables } = useCurveEditor();
13006
13020
  const [isDragging, setIsDragging] = useState(false);
13007
13021
  // Use refs for all mutable values to avoid render cycles
@@ -13152,7 +13166,7 @@ const PlayHead = ({ width, height: _height }) => {
13152
13166
  return (jsxs("div", { className: styles.root, children: [jsx("div", { ref: lineRef, className: styles.line, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp }), jsx("div", { ref: handleRef, className: styles.handle, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp })] }));
13153
13167
  };
13154
13168
 
13155
- const useStyles$w = makeStyles({
13169
+ const useStyles$v = makeStyles({
13156
13170
  root: {
13157
13171
  display: "flex",
13158
13172
  flexDirection: "row",
@@ -13184,7 +13198,7 @@ const useStyles$w = makeStyles({
13184
13198
  * @returns The frame bar component
13185
13199
  */
13186
13200
  const FrameBar = ({ width }) => {
13187
- const styles = useStyles$w();
13201
+ const styles = useStyles$v();
13188
13202
  const { state, observables } = useCurveEditor();
13189
13203
  const containerRef = useRef(null);
13190
13204
  const [scale, setScale] = useState(1);
@@ -13242,7 +13256,7 @@ const FrameBar = ({ width }) => {
13242
13256
  return (jsx("div", { className: styles.root, ref: containerRef, children: renderTicks() }));
13243
13257
  };
13244
13258
 
13245
- const useStyles$v = makeStyles({
13259
+ const useStyles$u = makeStyles({
13246
13260
  root: {
13247
13261
  display: "flex",
13248
13262
  flexDirection: "column",
@@ -13283,7 +13297,7 @@ const OFFSET_X = 10;
13283
13297
  * @returns The range frame bar component
13284
13298
  */
13285
13299
  const RangeFrameBar = ({ width }) => {
13286
- const styles = useStyles$v();
13300
+ const styles = useStyles$u();
13287
13301
  const { state, actions, observables } = useCurveEditor();
13288
13302
  const svgRef = useRef(null);
13289
13303
  const [viewWidth, setViewWidth] = useState(width);
@@ -13394,7 +13408,7 @@ const RangeFrameBar = ({ width }) => {
13394
13408
  }), renderKeyframes, renderActiveFrame] }) }));
13395
13409
  };
13396
13410
 
13397
- const useStyles$u = makeStyles({
13411
+ const useStyles$t = makeStyles({
13398
13412
  root: {
13399
13413
  display: "flex",
13400
13414
  flexDirection: "column",
@@ -13426,7 +13440,7 @@ const useStyles$u = makeStyles({
13426
13440
  * @returns The canvas component
13427
13441
  */
13428
13442
  const Canvas = () => {
13429
- const styles = useStyles$u();
13443
+ const styles = useStyles$t();
13430
13444
  const { observables } = useCurveEditor();
13431
13445
  const containerRef = useRef(null);
13432
13446
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
@@ -13456,7 +13470,7 @@ const Canvas = () => {
13456
13470
  return (jsxs("div", { className: styles.root, ref: containerRef, children: [jsx("div", { className: styles.frameBar, children: jsx(FrameBar, { width: dimensions.width }) }), jsxs("div", { className: styles.canvasArea, children: [jsx(Graph, { width: dimensions.width, height: dimensions.height - 70 }), jsx(PlayHead, { width: dimensions.width, height: dimensions.height - 70 })] }), jsx("div", { className: styles.rangeFrameBar, children: jsx(RangeFrameBar, { width: dimensions.width }) })] }));
13457
13471
  };
13458
13472
 
13459
- const useStyles$t = makeStyles({
13473
+ const useStyles$s = makeStyles({
13460
13474
  root: {
13461
13475
  flex: 1,
13462
13476
  height: "25px",
@@ -13518,7 +13532,7 @@ const useStyles$t = makeStyles({
13518
13532
  * @returns The range selector component
13519
13533
  */
13520
13534
  const RangeSelector = () => {
13521
- const styles = useStyles$t();
13535
+ const styles = useStyles$s();
13522
13536
  const { state, actions, observables } = useCurveEditor();
13523
13537
  const containerRef = useRef(null);
13524
13538
  const scrollbarRef = useRef(null);
@@ -13663,7 +13677,7 @@ function GetKeyAtAnyFrameIndex(animations, frame) {
13663
13677
  }
13664
13678
  return false;
13665
13679
  }
13666
- const useStyles$s = makeStyles({
13680
+ const useStyles$r = makeStyles({
13667
13681
  root: {
13668
13682
  display: "flex",
13669
13683
  flexDirection: "row",
@@ -13720,7 +13734,7 @@ MediaControls.displayName = "MediaControls";
13720
13734
  * @returns The BottomBar component.
13721
13735
  */
13722
13736
  const BottomBar = () => {
13723
- const styles = useStyles$s();
13737
+ const styles = useStyles$r();
13724
13738
  const { state, actions, observables } = useCurveEditor();
13725
13739
  // Track display frame separately for smooth updates during playback
13726
13740
  const [displayFrame, setDisplayFrame] = useState(state.activeFrame);
@@ -13835,7 +13849,7 @@ const BottomBar = () => {
13835
13849
  return (jsxs("div", { className: styles.root, children: [jsx("div", { className: styles.mediaControls, children: jsx(MediaControls, { hasActiveAnimations: hasActiveAnimations, isPlaying: state.isPlaying, forwardAnimation: state.forwardAnimation, onPlayForward: handlePlayForward, onPlayBackward: handlePlayBackward, onStop: handleStop, onPrevKey: handlePrevKey, onNextKey: handleNextKey, onFirstFrame: handleFirstFrame, onLastFrame: handleLastFrame }) }), jsxs("div", { className: styles.frameDisplay, children: [jsx("div", { className: styles.frameLabel, children: "Frame:" }), jsx(SpinButton, { className: styles.spinButton, value: displayFrame, onChange: handleFrameChange, min: state.fromKey, max: state.toKey, disabled: !hasActiveAnimations })] }), jsx(RangeSelector, {}), jsxs("div", { className: styles.clipLengthSection, children: [jsx("div", { className: styles.frameLabel, children: "Clip Length:" }), jsx(SpinButton, { className: styles.spinButton, value: clipLength, onChange: handleClipLengthChange, min: 1, disabled: !hasActiveAnimations })] })] }));
13836
13850
  };
13837
13851
 
13838
- const useStyles$r = makeStyles({
13852
+ const useStyles$q = makeStyles({
13839
13853
  root: {
13840
13854
  display: "flex",
13841
13855
  flexDirection: "column",
@@ -13884,7 +13898,7 @@ const useStyles$r = makeStyles({
13884
13898
  * @returns The curve editor content
13885
13899
  */
13886
13900
  const CurveEditorContent = () => {
13887
- const styles = useStyles$r();
13901
+ const styles = useStyles$q();
13888
13902
  const { state, actions, observables } = useCurveEditor();
13889
13903
  const rootRef = useRef(null);
13890
13904
  const prepareRef = useRef(() => actions.prepare());
@@ -14726,7 +14740,7 @@ function useObservableArray(target, getItems, addFn, removeFn, changeFn) {
14726
14740
  }, [getItems]), useInterceptObservable("function", target, addFn), useInterceptObservable("function", target, removeFn), changeFn ? useInterceptObservable("function", target, changeFn) : undefined);
14727
14741
  }
14728
14742
 
14729
- const useStyles$q = makeStyles({
14743
+ const useStyles$p = makeStyles({
14730
14744
  lightsListDiv: {
14731
14745
  display: "flex",
14732
14746
  flexDirection: "column",
@@ -14736,7 +14750,7 @@ const ClusteredLightContainerSetupProperties = ({ context: container }) => {
14736
14750
  return (jsxs(Fragment, { children: [jsx(BooleanBadgePropertyLine, { label: "Is Supported", value: container.isSupported }), jsx(BoundProperty, { label: "Horizontal Tiles", component: NumberInputPropertyLine, target: container, propertyKey: "horizontalTiles", step: 1, min: 1, forceInt: true }), jsx(BoundProperty, { label: "Vertical Tiles", component: NumberInputPropertyLine, target: container, propertyKey: "verticalTiles", step: 1, min: 1, forceInt: true }), jsx(BoundProperty, { label: "Depth Slices", component: NumberInputPropertyLine, target: container, propertyKey: "depthSlices", step: 1, min: 1, forceInt: true }), jsx(BoundProperty, { label: "Max Range", component: NumberInputPropertyLine, target: container, propertyKey: "maxRange", min: 1 })] }));
14737
14751
  };
14738
14752
  const ClusteredLightContainerLightsProperties = ({ container, selectionService, }) => {
14739
- const classes = useStyles$q();
14753
+ const classes = useStyles$p();
14740
14754
  const lights = useObservableArray(container, useCallback(() => container.lights, [container]), "addLight", "removeLight");
14741
14755
  return (jsx(PropertyLine, { label: "Lights", expandedContent: jsx("div", { className: classes.lightsListDiv, children: lights.map((light) => (jsx(LinkToEntityPropertyLine, { label: light.getClassName(), entity: light, selectionService: selectionService }, light.uniqueId))) }), children: jsx(Badge, { appearance: "filled", children: lights.length }) }));
14742
14756
  };
@@ -14786,7 +14800,7 @@ const HemisphericLightSetupProperties = ({ context: hemisphericLight }) => {
14786
14800
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { label: "Direction", component: Vector3PropertyLine, target: hemisphericLight, propertyKey: "direction" }), jsx(BoundProperty, { label: "Diffuse", component: Color3PropertyLine, target: hemisphericLight, propertyKey: "diffuse" }), jsx(BoundProperty, { label: "Ground", component: Color3PropertyLine, target: hemisphericLight, propertyKey: "groundColor" }), jsx(BoundProperty, { label: "Intensity", component: NumberInputPropertyLine, target: hemisphericLight, propertyKey: "intensity" })] }));
14787
14801
  };
14788
14802
 
14789
- const useStyles$p = makeStyles({
14803
+ const useStyles$o = makeStyles({
14790
14804
  root: {
14791
14805
  display: "grid",
14792
14806
  gridTemplateRows: "repeat(1fr)",
@@ -14816,7 +14830,7 @@ const useStyles$p = makeStyles({
14816
14830
  const ComboBox = forwardRef((props, ref) => {
14817
14831
  ComboBox.displayName = "ComboBox";
14818
14832
  const comboId = useId();
14819
- const styles = useStyles$p();
14833
+ const styles = useStyles$o();
14820
14834
  const { size } = useContext(ToolContext);
14821
14835
  // Find the label for the current value
14822
14836
  const getLabel = (value) => props.options.find((opt) => opt.value === value)?.label ?? "";
@@ -14839,7 +14853,7 @@ const ComboBox = forwardRef((props, ref) => {
14839
14853
  return (jsxs("div", { className: styles.root, children: [jsx("label", { id: comboId, children: props.label }), jsx(Combobox, { ref: ref, defaultOpen: props.defaultOpen, size: size, root: { className: styles.comboBox }, input: { className: styles.input }, listbox: { className: styles.listbox }, onOptionSelect: onOptionSelect, "aria-labelledby": comboId, placeholder: "Search..", onChange: (ev) => setQuery(ev.target.value), value: query, children: children })] }));
14840
14854
  });
14841
14855
 
14842
- const useStyles$o = makeStyles({
14856
+ const useStyles$n = makeStyles({
14843
14857
  linkDiv: {
14844
14858
  display: "flex",
14845
14859
  flexDirection: "row",
@@ -14864,7 +14878,7 @@ const useStyles$o = makeStyles({
14864
14878
  function EntitySelector(props) {
14865
14879
  const { value, onLink, getEntities, getName, filter, defaultValue } = props;
14866
14880
  const onChange = props.onChange;
14867
- const classes = useStyles$o();
14881
+ const classes = useStyles$n();
14868
14882
  const comboBoxRef = useRef(null);
14869
14883
  // Build options with uniqueId as key
14870
14884
  const options = useMemo(() => {
@@ -15018,7 +15032,7 @@ const TextureUpload = (props) => {
15018
15032
  return jsx(UploadButton, { onUpload: handleUpload, accept: accept, title: "Upload Texture", label: label });
15019
15033
  };
15020
15034
 
15021
- const useStyles$n = makeStyles({
15035
+ const useStyles$m = makeStyles({
15022
15036
  container: {
15023
15037
  display: "flex",
15024
15038
  flexDirection: "row",
@@ -15035,7 +15049,7 @@ const useStyles$n = makeStyles({
15035
15049
  const TextureSelector = (props) => {
15036
15050
  TextureSelector.displayName = "TextureSelector";
15037
15051
  const { scene, cubeOnly, value, onChange, onLink, defaultValue } = props;
15038
- const classes = useStyles$n();
15052
+ const classes = useStyles$m();
15039
15053
  const getTextures = useCallback(() => scene.textures, [scene.textures]);
15040
15054
  const getName = useCallback((texture) => texture.displayName || texture.name || `${texture.getClassName() || "Unnamed Texture"} (${texture.uniqueId})`, []);
15041
15055
  const filter = useCallback((texture) => !cubeOnly || texture.isCube, [cubeOnly]);
@@ -15640,7 +15654,7 @@ async function EditNodeMaterial(material) {
15640
15654
  await material.edit({ nodeEditorConfig: { backgroundColor: material.getScene().clearColor } });
15641
15655
  }
15642
15656
 
15643
- const useStyles$m = makeStyles({
15657
+ const useStyles$l = makeStyles({
15644
15658
  subsection: {
15645
15659
  marginTop: tokens.spacingVerticalM,
15646
15660
  },
@@ -15714,7 +15728,7 @@ const GradientBlockPropertyLine = (props) => {
15714
15728
  };
15715
15729
  const NodeMaterialInputProperties = (props) => {
15716
15730
  const { material } = props;
15717
- const classes = useStyles$m();
15731
+ const classes = useStyles$l();
15718
15732
  const inputBlocks = useObservableState(useCallback(() => {
15719
15733
  const inspectorVisibleInputBlocks = material
15720
15734
  .getInputBlocks()
@@ -16440,7 +16454,7 @@ function SaveMetadata(entity, metadata) {
16440
16454
  entity.metadata = metadata;
16441
16455
  }
16442
16456
  }
16443
- const useStyles$l = makeStyles({
16457
+ const useStyles$k = makeStyles({
16444
16458
  mainDiv: {
16445
16459
  display: "flex",
16446
16460
  flexDirection: "column",
@@ -16460,7 +16474,7 @@ const useStyles$l = makeStyles({
16460
16474
  */
16461
16475
  const MetadataProperties = (props) => {
16462
16476
  const { entity } = props;
16463
- const classes = useStyles$l();
16477
+ const classes = useStyles$k();
16464
16478
  const { size } = useContext(ToolContext);
16465
16479
  const metadata = useProperty(entity, "metadata");
16466
16480
  const stringifiedMetadata = useMemo(() => StringifyMetadata(metadata, false) ?? "", [metadata]);
@@ -17395,7 +17409,7 @@ const ParticleSystemEmitterProperties = (props) => {
17395
17409
  } })) : (jsx(Property, { component: TextPropertyLine, propertyPath: "source", label: "Source", value: "No meshes in scene." })), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Use normals for direction", target: particleEmitterType, propertyKey: "useMeshNormalsForDirection" }), !useMeshNormalsForDirection && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" })] }))] })), particleEmitterType instanceof BoxParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Min emit box", target: particleEmitterType, propertyKey: "minEmitBox" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Max emit box", target: particleEmitterType, propertyKey: "maxEmitBox" })] })), particleEmitterType instanceof ConeParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Height range", target: particleEmitterType, propertyKey: "heightRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: SwitchPropertyLine, label: "Emit from spawn point only", target: particleEmitterType, propertyKey: "emitFromSpawnPointOnly" }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof SphereParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof CylinderParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Height", target: particleEmitterType, propertyKey: "height", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof HemisphericParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius", target: particleEmitterType, propertyKey: "radius", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Radius range", target: particleEmitterType, propertyKey: "radiusRange", min: 0, max: 1, step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Direction randomizer", target: particleEmitterType, propertyKey: "directionRandomizer", min: 0, max: 1, step: 0.01 })] })), particleEmitterType instanceof PointParticleEmitter && (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction1", target: particleEmitterType, propertyKey: "direction1" }), jsx(BoundProperty, { component: Vector3PropertyLine, label: "Direction2", target: particleEmitterType, propertyKey: "direction2" })] }))] })), !scene && jsx(TextPropertyLine, { label: "Emitter", value: "No scene available." })] }));
17396
17410
  };
17397
17411
 
17398
- const useStyles$k = makeStyles({
17412
+ const useStyles$j = makeStyles({
17399
17413
  subsection: {
17400
17414
  marginTop: tokens.spacingVerticalM,
17401
17415
  },
@@ -17415,7 +17429,7 @@ const ParticleSystemSizeProperties = (props) => {
17415
17429
  const sizeGradientGetter = useCallback(() => system.getSizeGradients(), [system]);
17416
17430
  const sizeGradient = useObservableArray(system, sizeGradientGetter, "addSizeGradient", "removeSizeGradient", "forceRefreshGradients");
17417
17431
  const useSizeGradients = (sizeGradient?.length ?? 0) > 0;
17418
- const classes = useStyles$k();
17432
+ const classes = useStyles$j();
17419
17433
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min size", target: system, propertyKey: "minSize", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max size", target: system, propertyKey: "maxSize", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min scale x", target: system, propertyKey: "minScaleX", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max scale x", target: system, propertyKey: "maxScaleX", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min scale y", target: system, propertyKey: "minScaleY", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max scale y", target: system, propertyKey: "maxScaleY", min: 0, step: 0.1 }), isCpuParticleSystem && !useStartSizeGradients && (jsx(ButtonLine, { label: "Use Start Size gradients", onClick: () => {
17420
17434
  system.addStartSizeGradient(0, system.minSize, system.maxSize);
17421
17435
  system.forceRefreshGradients();
@@ -17451,7 +17465,7 @@ const ParticleSystemSizeProperties = (props) => {
17451
17465
  } })] }))] }));
17452
17466
  };
17453
17467
 
17454
- const useStyles$j = makeStyles({
17468
+ const useStyles$i = makeStyles({
17455
17469
  subsection: {
17456
17470
  marginTop: tokens.spacingVerticalM,
17457
17471
  },
@@ -17477,7 +17491,7 @@ const ParticleSystemEmissionProperties = (props) => {
17477
17491
  const useVelocityGradients = (velocityGradients?.length ?? 0) > 0;
17478
17492
  const useLimitVelocityGradients = (limitVelocityGradients?.length ?? 0) > 0;
17479
17493
  const useDragGradients = (dragGradients?.length ?? 0) > 0;
17480
- const classes = useStyles$j();
17494
+ const classes = useStyles$i();
17481
17495
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Emit rate", target: system, propertyKey: "emitRate", min: 0, step: 1 }), isCpuParticleSystem && !useEmitRateGradients && (jsx(ButtonLine, { label: "Use Emit rate gradients", onClick: () => {
17482
17496
  system.addEmitRateGradient(0, system.emitRate, system.emitRate);
17483
17497
  system.forceRefreshGradients();
@@ -17545,7 +17559,7 @@ const ParticleSystemEmissionProperties = (props) => {
17545
17559
  } })] }))] }));
17546
17560
  };
17547
17561
 
17548
- const useStyles$i = makeStyles({
17562
+ const useStyles$h = makeStyles({
17549
17563
  subsection: {
17550
17564
  marginTop: tokens.spacingVerticalM,
17551
17565
  },
@@ -17562,7 +17576,7 @@ const ParticleSystemLifetimeProperties = (props) => {
17562
17576
  const lifeTimeGradientsGetter = useCallback(() => system.getLifeTimeGradients(), [system]);
17563
17577
  const lifeTimeGradients = useObservableArray(system, lifeTimeGradientsGetter, "addLifeTimeGradient", "removeLifeTimeGradient", "forceRefreshGradients");
17564
17578
  const useLifeTimeGradients = (lifeTimeGradients?.length ?? 0) > 0;
17565
- const classes = useStyles$i();
17579
+ const classes = useStyles$h();
17566
17580
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min lifetime", target: system, propertyKey: "minLifeTime", min: 0, step: 0.1 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max lifetime", target: system, propertyKey: "maxLifeTime", min: 0, step: 0.1 }), isCpuParticleSystem && !useLifeTimeGradients && (jsx(ButtonLine, { label: "Use Lifetime gradients", onClick: () => {
17567
17581
  system.addLifeTimeGradient(0, system.minLifeTime, system.maxLifeTime);
17568
17582
  system.forceRefreshGradients();
@@ -17582,7 +17596,7 @@ const ParticleSystemLifetimeProperties = (props) => {
17582
17596
  } })] })), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Target stop duration", target: system, propertyKey: "targetStopDuration", min: 0, step: 0.1 })] }));
17583
17597
  };
17584
17598
 
17585
- const useStyles$h = makeStyles({
17599
+ const useStyles$g = makeStyles({
17586
17600
  subsection: {
17587
17601
  marginTop: tokens.spacingVerticalM,
17588
17602
  },
@@ -17609,7 +17623,7 @@ const ParticleSystemColorProperties = (props) => {
17609
17623
  const hasRampGradients = (rampGradients?.length ?? 0) > 0;
17610
17624
  const hasColorRemapGradients = (colorRemapGradients?.length ?? 0) > 0;
17611
17625
  const hasAlphaRemapGradients = (alphaRemapGradients?.length ?? 0) > 0;
17612
- const classes = useStyles$h();
17626
+ const classes = useStyles$g();
17613
17627
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: Color4PropertyLine, label: "Color 1", target: system, propertyKey: "color1" }), jsx(BoundProperty, { component: Color4PropertyLine, label: "Color 2", target: system, propertyKey: "color2" }), jsx(BoundProperty, { component: Color4PropertyLine, label: "Color dead", target: system, propertyKey: "colorDead" }), !hasColorGradients && (jsx(ButtonLine, { label: "Use Color gradients", onClick: () => {
17614
17628
  system.addColorGradient(0, system.color1, system.color1);
17615
17629
  system.addColorGradient(1, system.color2, system.color2);
@@ -17681,7 +17695,7 @@ const ParticleSystemColorProperties = (props) => {
17681
17695
  } })] }))] }))] }));
17682
17696
  };
17683
17697
 
17684
- const useStyles$g = makeStyles({
17698
+ const useStyles$f = makeStyles({
17685
17699
  subsection: {
17686
17700
  marginTop: tokens.spacingVerticalM,
17687
17701
  },
@@ -17696,7 +17710,7 @@ const ParticleSystemRotationProperties = (props) => {
17696
17710
  const angularSpeedGradientsGetter = useCallback(() => system.getAngularSpeedGradients(), [system]);
17697
17711
  const angularSpeedGradients = useObservableArray(system, angularSpeedGradientsGetter, "addAngularSpeedGradient", "removeAngularSpeedGradient", "forceRefreshGradients");
17698
17712
  const useAngularSpeedGradients = (angularSpeedGradients?.length ?? 0) > 0;
17699
- const classes = useStyles$g();
17713
+ const classes = useStyles$f();
17700
17714
  return (jsxs(Fragment, { children: [jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min Angular speed", target: system, propertyKey: "minAngularSpeed", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max Angular speed", target: system, propertyKey: "maxAngularSpeed", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Min initial rotation", target: system, propertyKey: "minInitialRotation", step: 0.01 }), jsx(BoundProperty, { component: NumberInputPropertyLine, label: "Max initial rotation", target: system, propertyKey: "maxInitialRotation", step: 0.01 }), !useAngularSpeedGradients && (jsx(ButtonLine, { label: "Use Angular speed gradients", onClick: () => {
17701
17715
  system.addAngularSpeedGradient(0, system.minAngularSpeed, system.maxAngularSpeed);
17702
17716
  system.forceRefreshGradients();
@@ -17802,7 +17816,7 @@ const AttractorComponent = (props) => {
17802
17816
  } }), !attractorData.isReadOnly && (jsx(ToggleButton, { title: "Add / remove position gizmo from particle attractor", checkedIcon: ArrowMoveFilled, value: isControlled(impostor), onChange: (control) => onControl(control ? impostor : undefined) }))] }));
17803
17817
  };
17804
17818
 
17805
- const useStyles$f = makeStyles({
17819
+ const useStyles$e = makeStyles({
17806
17820
  subsection: {
17807
17821
  marginTop: tokens.spacingVerticalM,
17808
17822
  },
@@ -17859,7 +17873,7 @@ const AttractorList = (props) => {
17859
17873
  gizmoManager.attachToMesh(attached);
17860
17874
  setControlledImpostor(attached);
17861
17875
  };
17862
- const classes = useStyles$f();
17876
+ const classes = useStyles$e();
17863
17877
  return (jsxs(Fragment, { children: [items.length > 0 && (jsxs(Fragment, { children: [jsx(Color3PropertyLine, { label: "Attractor Debug Color", value: impostorColor, onChange: setImpostorColor }), jsx(SyncedSliderPropertyLine, { label: "Attractor Debug Size", value: impostorScale, onChange: setImpostorScale, min: 0, max: 10, step: 0.1 }), jsx(Subtitle2, { className: classes.subsection, children: "Attractors list" })] })), jsx(List, { addButtonLabel: `Add New Attractor`, items: items, onDelete: attractorSource.removeAttractor
17864
17878
  ? (item, _index) => {
17865
17879
  // Only CPU attractors (Attractor instances) can be removed
@@ -17969,7 +17983,7 @@ const ParticleSystemAttractorProperties = (props) => {
17969
17983
  return (jsx(Fragment, { children: scene ? (jsx(AttractorList, { attractorSource: attractorSource, scene: scene })) : (jsx(MessageBar, { intent: "info", title: "No Scene Available", message: "Cannot display attractors without a scene" })) }));
17970
17984
  };
17971
17985
 
17972
- const useStyles$e = makeStyles({
17986
+ const useStyles$d = makeStyles({
17973
17987
  subsection: {
17974
17988
  marginTop: tokens.spacingVerticalM,
17975
17989
  },
@@ -18023,7 +18037,7 @@ const InputBlockPropertyLine = (props) => {
18023
18037
  */
18024
18038
  const ParticleSystemNodeEditorProperties = (props) => {
18025
18039
  const { particleSystem: system } = props;
18026
- const classes = useStyles$e();
18040
+ const classes = useStyles$d();
18027
18041
  const source = system.source;
18028
18042
  const inputBlocks = useObservableState(useCallback(() => {
18029
18043
  if (!source) {
@@ -18884,14 +18898,14 @@ const SkeletonPropertiesServiceDefinition = {
18884
18898
  },
18885
18899
  };
18886
18900
 
18887
- const useStyles$d = makeStyles({
18901
+ const useStyles$c = makeStyles({
18888
18902
  uniformWidth: {
18889
18903
  ...UniformWidthStyling,
18890
18904
  },
18891
18905
  });
18892
18906
  const SpinButtonPropertyLine = (props) => {
18893
18907
  SpinButtonPropertyLine.displayName = "SpinButtonPropertyLine";
18894
- const classes = useStyles$d();
18908
+ const classes = useStyles$c();
18895
18909
  return (jsx(PropertyLine, { ...props, children: jsx(SpinButton, { ...props, className: mergeClasses(classes.uniformWidth, props.className) }) }));
18896
18910
  };
18897
18911
 
@@ -19139,7 +19153,7 @@ function _TextureFormatHasNoAlpha(format) {
19139
19153
  }
19140
19154
  }
19141
19155
 
19142
- const useStyles$c = makeStyles({
19156
+ const useStyles$b = makeStyles({
19143
19157
  root: {
19144
19158
  display: "flex",
19145
19159
  flexDirection: "column",
@@ -19191,7 +19205,7 @@ const TextureChannelStates = {
19191
19205
  */
19192
19206
  const TexturePreview = (props) => {
19193
19207
  const { texture, disableToolbar = false, maxWidth = "100%", maxHeight = "384px", offsetX = 0, offsetY = 0, width, height, imperativeRef } = props;
19194
- const classes = useStyles$c();
19208
+ const classes = useStyles$b();
19195
19209
  const canvasRef = useRef(null);
19196
19210
  const [channels, setChannels] = useState(TextureChannelStates.ALL);
19197
19211
  const [face, setFace] = useState(0);
@@ -20101,7 +20115,7 @@ class TextureCanvasManager {
20101
20115
  }
20102
20116
  }
20103
20117
 
20104
- const useStyles$b = makeStyles({
20118
+ const useStyles$a = makeStyles({
20105
20119
  channelsBar: {
20106
20120
  display: "flex",
20107
20121
  flexDirection: "column",
@@ -20146,7 +20160,7 @@ const useStyles$b = makeStyles({
20146
20160
  */
20147
20161
  const ChannelsBar = (props) => {
20148
20162
  const { channels, setChannels } = props;
20149
- const classes = useStyles$b();
20163
+ const classes = useStyles$a();
20150
20164
  const toggleVisibility = useCallback((index) => {
20151
20165
  const newChannels = [...channels];
20152
20166
  newChannels[index] = { ...newChannels[index], visible: !newChannels[index].visible };
@@ -20176,7 +20190,7 @@ const ChannelsBar = (props) => {
20176
20190
  }) }));
20177
20191
  };
20178
20192
 
20179
- const useStyles$a = makeStyles({
20193
+ const useStyles$9 = makeStyles({
20180
20194
  propertiesBar: {
20181
20195
  display: "flex",
20182
20196
  backgroundColor: tokens.colorNeutralBackground1,
@@ -20226,7 +20240,7 @@ const useStyles$a = makeStyles({
20226
20240
  },
20227
20241
  });
20228
20242
  const PixelDataDisplay = ({ label, value }) => {
20229
- const classes = useStyles$a();
20243
+ const classes = useStyles$9();
20230
20244
  return (jsxs("span", { className: classes.pixelData, children: [jsxs(Label, { className: classes.pixelDataLabel, children: [label, ":"] }), jsx(Label, { className: classes.pixelDataValue, children: value !== undefined ? value : "-" })] }));
20231
20245
  };
20232
20246
  const CubeFaces = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
@@ -20237,7 +20251,7 @@ const CubeFaces = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
20237
20251
  */
20238
20252
  const PropertiesBar = (props) => {
20239
20253
  const { texture, size, saveTexture, pixelData, face, setFace, resetTexture, resizeTexture, uploadTexture, mipLevel, setMipLevel } = props;
20240
- const classes = useStyles$a();
20254
+ const classes = useStyles$9();
20241
20255
  const uploadInputRef = useRef(null);
20242
20256
  const [width, setWidth] = useState(size.width);
20243
20257
  const [height, setHeight] = useState(size.height);
@@ -20272,7 +20286,7 @@ const PropertiesBar = (props) => {
20272
20286
  return (jsxs("div", { className: classes.propertiesBar, children: [jsxs("div", { className: classes.section, children: [jsx(Label, { children: "W:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: width.toString(), readOnly: texture.isCube, onChange: (_, data) => setWidth(getNewDimension(width, data.value)) }), jsx(Label, { children: "H:" }), jsx(Input, { className: classes.dimensionInput, size: "small", type: "text", value: height.toString(), readOnly: texture.isCube, onChange: (_, data) => setHeight(getNewDimension(height, data.value)) }), !texture.isCube && (jsx(Tooltip$1, { content: "Resize", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ResizeRegular, {}), onClick: handleResize }) }))] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "X", value: pixelData.x }), jsx(PixelDataDisplay, { label: "Y", value: pixelData.y })] }), jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(PixelDataDisplay, { label: "R", value: pixelData.r }), jsx(PixelDataDisplay, { label: "G", value: pixelData.g }), jsx(PixelDataDisplay, { label: "B", value: pixelData.b }), jsx(PixelDataDisplay, { label: "A", value: pixelData.a })] }), texture.isCube && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsx(Toolbar$1, { size: "small", children: CubeFaces.map((label, index) => (jsx(ToolbarButton, { className: classes.faceButton, appearance: face === index ? "primary" : "subtle", onClick: () => setFace(index), children: label }, label))) })] })), mipsEnabled && (jsxs(Fragment, { children: [jsx(ToolbarDivider, {}), jsxs("div", { className: classes.section, children: [jsx(Label, { children: "MIP:" }), jsx(Tooltip$1, { content: "Mip Preview Up", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronUpRegular, {}), disabled: mipLevel <= 0, onClick: () => setMipLevel(mipLevel - 1) }) }), jsx(Label, { children: mipLevel }), jsx(Tooltip$1, { content: "Mip Preview Down", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ChevronDownRegular, {}), disabled: mipLevel >= maxLevels, onClick: () => setMipLevel(mipLevel + 1) }) })] })] })), jsx("div", { className: classes.spacer }), jsxs(Toolbar$1, { size: "small", children: [jsx(Tooltip$1, { content: "Reset", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowResetRegular, {}), onClick: resetTexture }) }), jsx(Tooltip$1, { content: "Upload", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(ArrowUploadRegular, {}), onClick: handleUploadClick }) }), jsx("input", { ref: uploadInputRef, className: classes.uploadInput, type: "file", accept: ".jpg, .png, .tga, .dds, .env, .exr", onChange: handleFileChange }), jsx(Tooltip$1, { content: "Save", relationship: "label", children: jsx(ToolbarButton, { icon: jsx(SaveRegular, {}), onClick: saveTexture }) })] })] }));
20273
20287
  };
20274
20288
 
20275
- const useStyles$9 = makeStyles({
20289
+ const useStyles$8 = makeStyles({
20276
20290
  statusBar: {
20277
20291
  display: "flex",
20278
20292
  backgroundColor: tokens.colorNeutralBackground1,
@@ -20300,14 +20314,14 @@ const useStyles$9 = makeStyles({
20300
20314
  */
20301
20315
  const StatusBar = (props) => {
20302
20316
  const { texture, mipLevel } = props;
20303
- const classes = useStyles$9();
20317
+ const classes = useStyles$8();
20304
20318
  const factor = Math.pow(2, mipLevel);
20305
20319
  const width = Math.ceil(texture.getSize().width / factor);
20306
20320
  const height = Math.ceil(texture.getSize().height / factor);
20307
20321
  return (jsxs("div", { className: classes.statusBar, children: [jsx("span", { className: classes.fileName, children: texture.name }), !texture.noMipmap && (jsxs("span", { className: classes.mipInfo, children: ["MIP Preview: ", mipLevel, " (", width, "\u00D7", height, ")"] }))] }));
20308
20322
  };
20309
20323
 
20310
- const useStyles$8 = makeStyles({
20324
+ const useStyles$7 = makeStyles({
20311
20325
  toolbar: {
20312
20326
  display: "flex",
20313
20327
  flexDirection: "column",
@@ -20344,7 +20358,7 @@ const useStyles$8 = makeStyles({
20344
20358
  */
20345
20359
  const ToolBar = (props) => {
20346
20360
  const { tools, changeTool, activeToolIndex, metadata, setMetadata, hasAlpha } = props;
20347
- const classes = useStyles$8();
20361
+ const classes = useStyles$7();
20348
20362
  const computeRGBAColor = useCallback(() => {
20349
20363
  const opacityInt = Math.floor(metadata.alpha * 255);
20350
20364
  const opacityHex = opacityInt.toString(16).padStart(2, "0");
@@ -20373,7 +20387,7 @@ const ToolBar = (props) => {
20373
20387
  }) })] }));
20374
20388
  };
20375
20389
 
20376
- const useStyles$7 = makeStyles({
20390
+ const useStyles$6 = makeStyles({
20377
20391
  textureEditor: {
20378
20392
  display: "flex",
20379
20393
  flexDirection: "column",
@@ -20437,7 +20451,7 @@ const PREVIEW_UPDATE_DELAY_MS = 160;
20437
20451
  */
20438
20452
  const TextureEditor = (props) => {
20439
20453
  const { texture, toolProviders = [], window: editorWindow, onUpdate } = props;
20440
- const classes = useStyles$7();
20454
+ const classes = useStyles$6();
20441
20455
  // Canvas refs
20442
20456
  const uiCanvasRef = useRef(null);
20443
20457
  const canvas2DRef = useRef(null);
@@ -20585,7 +20599,7 @@ const TextureEditor = (props) => {
20585
20599
  return (jsxs("div", { className: classes.textureEditor, children: [jsx(PropertiesBar, { texture: texture, saveTexture: saveTexture, pixelData: pixelData, face: face, setFace: setFace, resetTexture: resetTexture, resizeTexture: resizeTexture, uploadTexture: uploadTexture, mipLevel: mipLevel, setMipLevel: setMipLevel, size: canvasManagerRef.current?.size || size }), jsxs("div", { className: classes.mainContent, children: [jsxs("div", { className: classes.canvasContainer, style: { cursor }, children: [jsx("canvas", { ref: uiCanvasRef, className: classes.canvasUI, tabIndex: 1 }), jsx("canvas", { ref: canvas2DRef, className: classes.canvas2D }), jsx("canvas", { ref: canvas3DRef, className: classes.canvas3D })] }), CurrentToolSettings && (jsx("div", { className: classes.toolSettingsContainer, children: jsx(CurrentToolSettings, {}) })), !texture.isCube && (jsx("div", { className: classes.sidebarLeft, children: jsx(ToolBar, { tools: toolProviders, activeToolIndex: activeToolIndex, changeTool: changeTool, metadata: metadata, setMetadata: setMetadata, hasAlpha: hasAlpha }) })), jsx("div", { className: classes.sidebarRight, children: jsx(ChannelsBar, { channels: channels, setChannels: setChannels }) })] }), jsx(StatusBar, { texture: texture, mipLevel: mipLevel })] }));
20586
20600
  };
20587
20601
 
20588
- const useStyles$6 = makeStyles({
20602
+ const useStyles$5 = makeStyles({
20589
20603
  settingsContainer: {
20590
20604
  display: "flex",
20591
20605
  flexDirection: "column",
@@ -20605,7 +20619,7 @@ const Contrast = {
20605
20619
  name: "Contrast/Exposure",
20606
20620
  order: 500,
20607
20621
  icon: () => {
20608
- const classes = useStyles$6();
20622
+ const classes = useStyles$5();
20609
20623
  return jsx(CircleHalfFillRegular, { className: classes.icon });
20610
20624
  },
20611
20625
  is3D: true,
@@ -20670,7 +20684,7 @@ const Contrast = {
20670
20684
  setContrast(0);
20671
20685
  },
20672
20686
  settingsComponent: () => {
20673
- const classes = useStyles$6();
20687
+ const classes = useStyles$5();
20674
20688
  const [contrast, exposure] = useObservableState(useCallback(() => [_contrast, _exposure], []), stateChangedObservable);
20675
20689
  const handleContrastChange = (_, data) => {
20676
20690
  setContrast(data.value);
@@ -20771,7 +20785,7 @@ const Floodfill = {
20771
20785
  },
20772
20786
  };
20773
20787
 
20774
- const useStyles$5 = makeStyles({
20788
+ const useStyles$4 = makeStyles({
20775
20789
  settingsContainer: {
20776
20790
  display: "flex",
20777
20791
  flexDirection: "column",
@@ -20895,7 +20909,7 @@ const Paintbrush = {
20895
20909
  pointerObserver?.remove();
20896
20910
  },
20897
20911
  settingsComponent: () => {
20898
- const classes = useStyles$5();
20912
+ const classes = useStyles$4();
20899
20913
  const width = useObservableState(useCallback(() => _width, []), stateChangedObservable);
20900
20914
  const handleWidthChange = (_, data) => {
20901
20915
  setWidth(data.value);
@@ -22889,74 +22903,1428 @@ const ExportServiceDefinition = {
22889
22903
  },
22890
22904
  };
22891
22905
 
22892
- const useStyles$4 = makeStyles({
22893
- statusMessage: {
22894
- padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
22895
- opacity: 0.7,
22896
- },
22897
- busyMessage: {
22898
- display: "flex",
22899
- alignItems: "center",
22900
- gap: tokens.spacingHorizontalXS,
22901
- padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
22902
- },
22903
- });
22906
+ const OverrideManagerKey = Symbol("babylonjs:overrideManager");
22907
+ const OverrideManagerInternals = new WeakMap();
22908
+ const OnOverrideManagerCreatedObservable = new Observable();
22904
22909
  /**
22905
- * Save/load controls for a scene's Smart Asset map.
22906
- * @param props - Component props.
22907
- * @returns The Smart Asset map controls.
22910
+ * Creates a new OverrideManager state object and attaches it to the scene.
22911
+ *
22912
+ * Internal: callers should use {@link GetOverrideManager} which returns the
22913
+ * existing manager when one is already attached.
22914
+ * @param scene - The scene this manager operates on.
22915
+ * @returns The created override manager state.
22908
22916
  */
22909
- const SmartAssetProjectTools = (props) => {
22910
- const { scene } = props;
22911
- const [statusMessage, setStatusMessage] = useState("");
22912
- const [busyMessage, setBusyMessage] = useState("");
22913
- const styles = useStyles$4();
22914
- const isBusy = busyMessage !== "";
22915
- const onSaveAssetMap = useCallback(async () => {
22916
- if (isBusy) {
22917
- return;
22917
+ function CreateOverrideManager(scene) {
22918
+ const manager = {
22919
+ scene,
22920
+ onChangedObservable: new Observable(),
22921
+ };
22922
+ const internal = {
22923
+ overrides: [],
22924
+ originalValues: new Map(),
22925
+ sceneDisposeObserver: null,
22926
+ };
22927
+ OverrideManagerInternals.set(manager, internal);
22928
+ if (!scene.metadata) {
22929
+ scene.metadata = {};
22930
+ }
22931
+ scene.metadata[OverrideManagerKey] = manager;
22932
+ // Auto-dispose when the scene is disposed so the manager doesn't outlive it.
22933
+ internal.sceneDisposeObserver = scene.onDisposeObservable.add(() => DisposeOverrideManager(manager));
22934
+ OnOverrideManagerCreatedObservable.notifyObservers(manager);
22935
+ return manager;
22936
+ }
22937
+ /**
22938
+ * Returns the OverrideManager attached to the given scene, creating and
22939
+ * attaching one if none exists.
22940
+ * @param scene - The scene to look up or attach a manager to.
22941
+ * @returns The existing or newly created OverrideManager.
22942
+ */
22943
+ function GetOverrideManager(scene) {
22944
+ const existing = scene.metadata?.[OverrideManagerKey];
22945
+ if (existing) {
22946
+ return existing;
22947
+ }
22948
+ return CreateOverrideManager(scene);
22949
+ }
22950
+ /**
22951
+ * Adds an override entry and immediately applies it.
22952
+ * If an override with the same target coordinates already exists, it is replaced.
22953
+ *
22954
+ * When the caller has already mutated the target (e.g. an Inspector edit),
22955
+ * pass `{ originalValue }` containing the property's prior value — this seeds
22956
+ * the original-value map (so {@link RemoveOverride} can restore it) and skips
22957
+ * the redundant apply step.
22958
+ * @param scene - The scene whose override registry to update.
22959
+ * @param entry - The override to add.
22960
+ * @param options - Optional behavior modifiers; see {@link AddOverrideOptions}.
22961
+ */
22962
+ function AddOverride(scene, entry, options) {
22963
+ const manager = GetOverrideManager(scene);
22964
+ const internal = GetOverrideInternals(manager);
22965
+ RemoveMatchingOverride(internal, entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
22966
+ internal.overrides.push(entry);
22967
+ if (options && "originalValue" in options) {
22968
+ // Caller already applied the new value. Seed the captured original from
22969
+ // their pre-edit snapshot — otherwise ApplyOverrideEntry would capture
22970
+ // the post-edit value and RemoveOverride would have nothing to restore.
22971
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
22972
+ if (!internal.originalValues.has(origKey)) {
22973
+ internal.originalValues.set(origKey, CloneValue(options.originalValue));
22918
22974
  }
22919
- setBusyMessage("Saving assets...");
22920
- setStatusMessage("");
22921
- try {
22922
- const assetMap = SerializeSmartAssetManagerMap(scene);
22923
- const jsonBlob = new Blob([JSON.stringify(assetMap, null, 2)], { type: "application/json" });
22924
- Tools.Download(jsonBlob, "smart-assets.json");
22925
- setStatusMessage(`Saved: ${Object.keys(assetMap.assets).length} assets`);
22975
+ }
22976
+ else {
22977
+ ApplyOverrideEntry(manager, internal, entry);
22978
+ }
22979
+ manager.onChangedObservable.notifyObservers();
22980
+ }
22981
+ /**
22982
+ * Returns all overrides currently registered with the scene.
22983
+ * @param scene - The scene whose override registry to read.
22984
+ * @returns A read-only array of override entries.
22985
+ */
22986
+ function GetOverrides(scene) {
22987
+ return GetOverrideInternals(GetOverrideManager(scene)).overrides;
22988
+ }
22989
+ /**
22990
+ * Removes all overrides, optionally restoring original values.
22991
+ * @param scene - The scene whose override registry to clear.
22992
+ * @param restoreOriginals - If true, restores all captured original values.
22993
+ */
22994
+ function ClearOverrides(scene, restoreOriginals = false) {
22995
+ const manager = GetOverrideManager(scene);
22996
+ const internal = GetOverrideInternals(manager);
22997
+ if (restoreOriginals) {
22998
+ // Snapshot the entries so we can restore each original without firing
22999
+ // an onChangedObservable notification per entry; consumers only need
23000
+ // one signal that the registry was emptied.
23001
+ const entries = [...internal.overrides];
23002
+ internal.overrides.length = 0;
23003
+ for (const entry of entries) {
23004
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23005
+ const original = internal.originalValues.get(origKey);
23006
+ if (original !== undefined) {
23007
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
23008
+ if (target) {
23009
+ SetNestedProperty(target, entry.propertyPath, original);
23010
+ }
23011
+ }
22926
23012
  }
22927
- catch (err) {
22928
- setStatusMessage(`Save error: ${err}`);
23013
+ internal.originalValues.clear();
23014
+ manager.onChangedObservable.notifyObservers();
23015
+ return;
23016
+ }
23017
+ internal.overrides.length = 0;
23018
+ internal.originalValues.clear();
23019
+ manager.onChangedObservable.notifyObservers();
23020
+ }
23021
+ /**
23022
+ * Updates the target coordinates on the override matching a specific (type,
23023
+ * old-name, old-index) so it follows an entity rename. Used by capture services
23024
+ * to keep overrides attached to a specific object after the user renames it.
23025
+ *
23026
+ * Only the override at the exact `(targetType, oldName, oldIndex)` slot is
23027
+ * updated, so other same-named siblings keep their own overrides untouched.
23028
+ *
23029
+ * @param scene - The scene whose override registry to update.
23030
+ * @param targetType - The target type.
23031
+ * @param oldName - The previous name of the renamed entity.
23032
+ * @param oldIndex - The previous index of the renamed entity among same-named siblings.
23033
+ * @param newName - The new name of the renamed entity.
23034
+ * @param newIndex - The new index of the renamed entity among same-named siblings.
23035
+ */
23036
+ function RenameOverrideTarget(scene, targetType, oldName, oldIndex, newName, newIndex) {
23037
+ const manager = GetOverrideManager(scene);
23038
+ const internal = GetOverrideInternals(manager);
23039
+ let changed = false;
23040
+ for (let i = 0; i < internal.overrides.length; i++) {
23041
+ const entry = internal.overrides[i];
23042
+ if (entry.targetType === targetType && entry.targetName === oldName && entry.targetIndex === oldIndex) {
23043
+ internal.overrides[i] = { ...entry, targetName: newName, targetIndex: newIndex };
23044
+ changed = true;
23045
+ }
23046
+ }
23047
+ // Update original-value keys to match the new identity
23048
+ const oldPrefix = `${targetType}::${oldName}::${oldIndex}::`;
23049
+ const newPrefix = `${targetType}::${newName}::${newIndex}::`;
23050
+ for (const [origKey, value] of Array.from(internal.originalValues.entries())) {
23051
+ if (origKey.startsWith(oldPrefix)) {
23052
+ const propertyPath = origKey.substring(oldPrefix.length);
23053
+ internal.originalValues.set(newPrefix + propertyPath, value);
23054
+ internal.originalValues.delete(origKey);
23055
+ }
23056
+ }
23057
+ if (changed) {
23058
+ manager.onChangedObservable.notifyObservers();
23059
+ }
23060
+ }
23061
+ /**
23062
+ * Rewrites override *values* that reference an entity by name when that entity
23063
+ * has been renamed. Mirrors {@link RenameOverrideTarget} but operates on the
23064
+ * `value` field rather than the `targetName` field, so overrides whose value
23065
+ * is `"ref:oldName"` (material/light/camera references) or `"texture:oldName"`
23066
+ * (non-SmartAsset texture references) follow the rename instead of silently
23067
+ * pointing at a non-existent entity.
23068
+ *
23069
+ * SmartAsset texture references (`"samTexture:<key>"`) are unaffected because
23070
+ * the SmartAsset key is decoupled from the texture's runtime `name` field.
23071
+ *
23072
+ * @param scene - The scene whose override registry to update.
23073
+ * @param valueScheme - Which encoded-reference prefix to rewrite: `"ref"` for
23074
+ * material/light/camera references, `"texture"` for non-SAM textures.
23075
+ * @param oldName - The previous name embedded in the reference.
23076
+ * @param newName - The new name embedded in the reference.
23077
+ */
23078
+ function RenameOverrideValueReferences(scene, valueScheme, oldName, newName) {
23079
+ if (oldName === newName) {
23080
+ return;
23081
+ }
23082
+ const manager = GetOverrideManager(scene);
23083
+ const internal = GetOverrideInternals(manager);
23084
+ const oldValue = `${valueScheme}:${oldName}`;
23085
+ const newValue = `${valueScheme}:${newName}`;
23086
+ let changed = false;
23087
+ for (let i = 0; i < internal.overrides.length; i++) {
23088
+ const entry = internal.overrides[i];
23089
+ if (entry.value === oldValue) {
23090
+ internal.overrides[i] = { ...entry, value: newValue };
23091
+ changed = true;
22929
23092
  }
22930
- finally {
22931
- setBusyMessage("");
23093
+ }
23094
+ if (changed) {
23095
+ manager.onChangedObservable.notifyObservers();
23096
+ }
23097
+ }
23098
+ // ── Application ──
23099
+ /**
23100
+ * Applies all overrides to their current targets in the scene.
23101
+ *
23102
+ * Call this after any scene mutation that might have invalidated previously
23103
+ * applied state (asset reload, object recreation, project load). The override
23104
+ * manager does not auto-subscribe to other scene subsystems — coordination is
23105
+ * the caller's responsibility, which keeps the override system independent.
23106
+ * @param scene - The scene whose overrides to apply.
23107
+ */
23108
+ function ApplyAllOverrides(scene) {
23109
+ const manager = GetOverrideManager(scene);
23110
+ const internal = GetOverrideInternals(manager);
23111
+ for (const entry of internal.overrides) {
23112
+ ApplyOverrideEntry(manager, internal, entry);
23113
+ }
23114
+ }
23115
+ // ── Serialization ──
23116
+ /**
23117
+ * Serializes all overrides to a JSON-compatible array.
23118
+ * The on-disk shape is identical to the in-memory `IOverrideEntry`.
23119
+ * @param scene - The scene whose overrides to serialize.
23120
+ * @returns An array of override entries (shallow copies).
23121
+ */
23122
+ function SerializeOverrides(scene) {
23123
+ const internal = GetOverrideInternals(GetOverrideManager(scene));
23124
+ return internal.overrides.map((o) => ({ ...o }));
23125
+ }
23126
+ /**
23127
+ * Loads overrides from a serialized array and applies them.
23128
+ * @param scene - The scene whose override registry to populate.
23129
+ * @param data - Array of override entries.
23130
+ */
23131
+ function DeserializeAndApplyOverrides(scene, data) {
23132
+ if (!Array.isArray(data)) {
23133
+ throw new Error("OverrideManager: Expected an array of override entries.");
23134
+ }
23135
+ for (const entry of data) {
23136
+ if (!entry.targetType || entry.targetName === undefined || typeof entry.targetIndex !== "number" || !entry.propertyPath || entry.value === undefined) {
23137
+ Logger.Warn("OverrideManager: Skipping invalid override entry.");
23138
+ continue;
22932
23139
  }
22933
- }, [scene, isBusy]);
22934
- const onLoadAssetMap = useCallback(async (files) => {
22935
- const file = files[0];
22936
- if (!file) {
22937
- return;
23140
+ AddOverride(scene, entry);
23141
+ }
23142
+ }
23143
+ // ── Lifecycle ──
23144
+ /**
23145
+ * Disposes the manager, clearing all overrides and detaching it from its scene.
23146
+ * Safe to call multiple times; subsequent calls are no-ops. Automatically invoked when the
23147
+ * owning scene is disposed.
23148
+ * @param manager - The override manager state.
23149
+ */
23150
+ function DisposeOverrideManager(manager) {
23151
+ const internal = OverrideManagerInternals.get(manager);
23152
+ if (!internal) {
23153
+ return;
23154
+ }
23155
+ OverrideManagerInternals.delete(manager);
23156
+ if (internal.sceneDisposeObserver) {
23157
+ manager.scene.onDisposeObservable.remove(internal.sceneDisposeObserver);
23158
+ internal.sceneDisposeObserver = null;
23159
+ }
23160
+ internal.overrides.length = 0;
23161
+ internal.originalValues.clear();
23162
+ manager.onChangedObservable.clear();
23163
+ if (manager.scene.metadata) {
23164
+ delete manager.scene.metadata[OverrideManagerKey];
23165
+ }
23166
+ }
23167
+ // ── Private ──
23168
+ function GetOverrideInternals(manager) {
23169
+ const internal = OverrideManagerInternals.get(manager);
23170
+ if (!internal) {
23171
+ throw new Error("OverrideManager: Unknown manager state.");
23172
+ }
23173
+ return internal;
23174
+ }
23175
+ /**
23176
+ * Applies a single override entry to its target, capturing the original value
23177
+ * on the first application so {@link RemoveOverride} can restore it later.
23178
+ * @param manager - The override manager owning the entry.
23179
+ * @param internal - The manager's internal state.
23180
+ * @param entry - The override to apply.
23181
+ */
23182
+ function ApplyOverrideEntry(manager, internal, entry) {
23183
+ const target = ResolveTarget(manager.scene, entry.targetType, entry.targetName, entry.targetIndex);
23184
+ if (!target) {
23185
+ Logger.Warn(`OverrideManager: target not found for type="${entry.targetType}" name="${entry.targetName}" index=${entry.targetIndex} prop="${entry.propertyPath}"`);
23186
+ return; // Target not loaded yet — override will be applied on next ApplyAllOverrides
23187
+ }
23188
+ // Capture original value before first override
23189
+ const origKey = MakeOriginalValueKey(entry.targetType, entry.targetName, entry.targetIndex, entry.propertyPath);
23190
+ if (!internal.originalValues.has(origKey)) {
23191
+ const currentValue = GetNestedProperty(target, entry.propertyPath);
23192
+ if (currentValue !== undefined) {
23193
+ internal.originalValues.set(origKey, CloneValue(currentValue));
22938
23194
  }
22939
- if (isBusy) {
23195
+ }
23196
+ const resolvedValue = ResolveOverrideValue(manager.scene, entry.value);
23197
+ SetNestedProperty(target, entry.propertyPath, resolvedValue);
23198
+ }
23199
+ /**
23200
+ * Locates a scene object by (targetType, targetName, targetIndex). The scene
23201
+ * collection is filtered to objects matching `targetName`; the N-th survivor
23202
+ * (per `targetIndex`) is returned. Falls back to the first match if the index
23203
+ * is out of range — useful when the scene shape has changed since capture.
23204
+ * @param scene - The scene to search.
23205
+ * @param targetType - The override target type.
23206
+ * @param targetName - The target object name (or "" for scene-level).
23207
+ * @param targetIndex - The target's position among same-named siblings.
23208
+ * @returns The matching scene object, or null if not found.
23209
+ */
23210
+ function ResolveTarget(scene, targetType, targetName, targetIndex) {
23211
+ // Scene-level overrides target the scene itself
23212
+ if (targetType === "scene") {
23213
+ return scene;
23214
+ }
23215
+ const collection = GetCollection$1(scene, targetType);
23216
+ if (!collection) {
23217
+ return null;
23218
+ }
23219
+ const matches = collection.filter((obj) => obj.name === targetName);
23220
+ if (matches.length === 0) {
23221
+ return null;
23222
+ }
23223
+ return (matches[targetIndex] ?? matches[0]);
23224
+ }
23225
+ /**
23226
+ * Returns the scene collection corresponding to an override target type.
23227
+ * @param scene - The scene to inspect.
23228
+ * @param targetType - The target type.
23229
+ * @returns The collection, or null if the type has no collection.
23230
+ */
23231
+ function GetCollection$1(scene, targetType) {
23232
+ switch (targetType) {
23233
+ case "meshes":
23234
+ return scene.meshes;
23235
+ case "materials":
23236
+ return scene.materials;
23237
+ case "textures":
23238
+ return scene.textures;
23239
+ case "lights":
23240
+ return scene.lights;
23241
+ case "cameras":
23242
+ return scene.cameras;
23243
+ case "animationGroups":
23244
+ return scene.animationGroups;
23245
+ default:
23246
+ return null;
23247
+ }
23248
+ }
23249
+ /**
23250
+ * Resolves an override value, expanding string references like "ref:name",
23251
+ * "samTexture:key", or "texture:name" into the actual scene object they refer to.
23252
+ * @param scene - The scene used to look up references.
23253
+ * @param value - The serialized override value.
23254
+ * @returns The runtime value to assign to the target property.
23255
+ */
23256
+ function ResolveOverrideValue(scene, value) {
23257
+ if (typeof value === "string") {
23258
+ if (value.startsWith("ref:")) {
23259
+ return ResolveObjectReference(scene, value.substring(4));
23260
+ }
23261
+ if (value.startsWith("samTexture:")) {
23262
+ return ResolveSamTextureReference(scene, value.substring(11));
23263
+ }
23264
+ if (value.startsWith("texture:")) {
23265
+ return ResolveTextureReference(scene, value.substring(8));
23266
+ }
23267
+ }
23268
+ // Number arrays are passed through as-is. SetNestedProperty will use
23269
+ // the live target's `fromArray` method (Vector3, Color3, etc.) to push
23270
+ // values in-place, preserving the math instance identity.
23271
+ return value;
23272
+ }
23273
+ /**
23274
+ * Resolves a "ref:name" value by looking up a material, light, or camera
23275
+ * in the scene by name.
23276
+ * @param scene - The scene to search.
23277
+ * @param name - The object name to resolve.
23278
+ * @returns The matching material, light, or camera, or undefined if not found.
23279
+ */
23280
+ function ResolveObjectReference(scene, name) {
23281
+ const mat = scene.materials.find((m) => m.name === name);
23282
+ if (mat) {
23283
+ return mat;
23284
+ }
23285
+ const light = scene.lights.find((l) => l.name === name);
23286
+ if (light) {
23287
+ return light;
23288
+ }
23289
+ const camera = scene.cameras.find((c) => c.name === name);
23290
+ if (camera) {
23291
+ return camera;
23292
+ }
23293
+ Logger.Warn(`OverrideManager: Object reference "${name}" not found in scene.`);
23294
+ return undefined;
23295
+ }
23296
+ /**
23297
+ * Resolves a "texture:name" value by looking up a texture in the scene by name.
23298
+ * @param scene - The scene to search.
23299
+ * @param name - The texture name to resolve.
23300
+ * @returns The matching texture, or undefined if not found.
23301
+ */
23302
+ function ResolveTextureReference(scene, name) {
23303
+ const tex = scene.textures.find((t) => t.name === name);
23304
+ if (tex) {
23305
+ return tex;
23306
+ }
23307
+ Logger.Warn(`OverrideManager: Texture reference "${name}" not found.`);
23308
+ return undefined;
23309
+ }
23310
+ /**
23311
+ * Resolves a "samTexture:key" value by looking up a SmartAsset-tracked texture
23312
+ * by its registry key. The SAM key is stable across save/load whereas the
23313
+ * texture's `name` (for SAM textures, the blob URL) changes on every reload,
23314
+ * so this is the only reliable way to round-trip texture references on
23315
+ * user-uploaded SmartAsset textures.
23316
+ * @param scene - The scene to search.
23317
+ * @param key - The SmartAsset key to resolve.
23318
+ * @returns The matching texture, or undefined if not found.
23319
+ */
23320
+ function ResolveSamTextureReference(scene, key) {
23321
+ const tex = scene.textures.find((t) => FindSmartAssetKeyForObject(scene, t) === key);
23322
+ if (tex) {
23323
+ return tex;
23324
+ }
23325
+ Logger.Warn(`OverrideManager: SmartAsset texture "${key}" not found.`);
23326
+ return undefined;
23327
+ }
23328
+ /**
23329
+ * Finds the index of an override matching the given coordinates.
23330
+ * @param internal - The manager's internal state.
23331
+ * @param targetType - The target type.
23332
+ * @param targetName - The target object name.
23333
+ * @param targetIndex - The target index among same-named siblings.
23334
+ * @param propertyPath - The property path.
23335
+ * @returns The matching index, or -1 if none found.
23336
+ */
23337
+ function FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath) {
23338
+ return internal.overrides.findIndex((o) => o.targetType === targetType && o.targetName === targetName && o.targetIndex === targetIndex && o.propertyPath === propertyPath);
23339
+ }
23340
+ /**
23341
+ * Removes any existing override that matches the given coordinates. Used by
23342
+ * {@link AddOverride} to enforce one entry per (type, name, index, property).
23343
+ * @param internal - The manager's internal state.
23344
+ * @param targetType - The target type.
23345
+ * @param targetName - The target object name.
23346
+ * @param targetIndex - The target index among same-named siblings.
23347
+ * @param propertyPath - The property path.
23348
+ */
23349
+ function RemoveMatchingOverride(internal, targetType, targetName, targetIndex, propertyPath) {
23350
+ const idx = FindOverrideIndex(internal, targetType, targetName, targetIndex, propertyPath);
23351
+ if (idx >= 0) {
23352
+ internal.overrides.splice(idx, 1);
23353
+ }
23354
+ }
23355
+ /**
23356
+ * Creates a unique key for storing original values.
23357
+ * @param targetType - The override target type.
23358
+ * @param targetName - The target object name.
23359
+ * @param targetIndex - The target index among same-named siblings.
23360
+ * @param propertyPath - The property path.
23361
+ * @returns A composite string key uniquely identifying the original value slot.
23362
+ */
23363
+ function MakeOriginalValueKey(targetType, targetName, targetIndex, propertyPath) {
23364
+ return `${targetType}::${targetName}::${targetIndex}::${propertyPath}`;
23365
+ }
23366
+ /**
23367
+ * Gets a nested property from an object using a dot-separated path.
23368
+ * @param obj - The root object to traverse.
23369
+ * @param path - The dot-separated property path.
23370
+ * @returns The value at the path, or undefined if any segment is missing.
23371
+ */
23372
+ function GetNestedProperty(obj, path) {
23373
+ const parts = path.split(".");
23374
+ let current = obj;
23375
+ for (const part of parts) {
23376
+ if (current === null || current === undefined || typeof current !== "object") {
23377
+ return undefined;
23378
+ }
23379
+ current = current[part];
23380
+ }
23381
+ return current;
23382
+ }
23383
+ /**
23384
+ * Sets a nested property on an object using a dot-separated path.
23385
+ *
23386
+ * When the value is a number array and the existing property is a Babylon
23387
+ * math type (Vector*, Quaternion, Color3/4, Matrix), uses the math type's
23388
+ * `fromArray` method to mutate it in place — preserving the live instance
23389
+ * identity that consumers may already hold references to. Otherwise falls
23390
+ * back to direct property replacement.
23391
+ * @param obj - The root object to mutate.
23392
+ * @param path - The dot-separated property path.
23393
+ * @param value - The new value to assign.
23394
+ */
23395
+ function SetNestedProperty(obj, path, value) {
23396
+ const parts = path.split(".");
23397
+ let current = obj;
23398
+ for (let i = 0; i < parts.length - 1; i++) {
23399
+ if (current === null || current === undefined || typeof current !== "object") {
22940
23400
  return;
22941
23401
  }
22942
- setBusyMessage("Loading assets...");
22943
- setStatusMessage("");
23402
+ current = current[parts[i]];
23403
+ }
23404
+ if (current === null || current === undefined || typeof current !== "object") {
23405
+ return;
23406
+ }
23407
+ const lastPart = parts[parts.length - 1];
23408
+ const existing = current[lastPart];
23409
+ if (Array.isArray(value) && existing && typeof existing === "object" && typeof existing.fromArray === "function") {
23410
+ existing.fromArray(value);
23411
+ return;
23412
+ }
23413
+ current[lastPart] = value;
23414
+ }
23415
+ /**
23416
+ * Snapshots a value for original-value tracking.
23417
+ *
23418
+ * Scene entities (textures, materials, meshes, etc.) are stored by reference
23419
+ * because cloning them would register unwanted duplicates in the scene.
23420
+ * Plain math types (Vector3, Color3, etc.) are cloned so mutations to the
23421
+ * live object don't corrupt the saved original.
23422
+ * @param value - The value to snapshot.
23423
+ * @returns The snapshot value (cloned for plain math types, by reference for entities).
23424
+ */
23425
+ function CloneValue(value) {
23426
+ if (value === null || value === undefined) {
23427
+ return value;
23428
+ }
23429
+ if (typeof value !== "object") {
23430
+ return value;
23431
+ }
23432
+ if (typeof value.getScene === "function") {
23433
+ return value;
23434
+ }
23435
+ if ("clone" in value && typeof value.clone === "function") {
23436
+ return value.clone();
23437
+ }
23438
+ return { ...value };
23439
+ }
23440
+
23441
+ /**
23442
+ * Inspector service that captures property edits made through Inspector and
23443
+ * feeds them to the OverrideManager as persistent overrides.
23444
+ *
23445
+ * Works on any scene object — overrides have no concept of "which asset"
23446
+ * owns an object. When multiple objects share a name, an entity's position
23447
+ * among same-named siblings (`targetIndex`) is captured so the override
23448
+ * re-applies to the same object after reload.
23449
+ *
23450
+ * The service re-attaches to the current scene whenever it changes, so
23451
+ * overrides are captured against the active scene even after loads/swaps.
23452
+ */
23453
+ const OverrideCaptureServiceDefinition = {
23454
+ friendlyName: "Override Capture",
23455
+ consumes: [SceneContextIdentity, PropertiesServiceIdentity],
23456
+ factory: (sceneContext, propertiesService) => {
23457
+ // Track each entity's name + index at first contact so we can update
23458
+ // existing overrides when the user renames the entity in Inspector.
23459
+ // Re-created on each scene attach so identities don't leak across scenes.
23460
+ let previousIdentity = new WeakMap();
23461
+ let changeObserver = null;
23462
+ function attachToScene(scene) {
23463
+ if (changeObserver) {
23464
+ changeObserver.remove();
23465
+ changeObserver = null;
23466
+ }
23467
+ previousIdentity = new WeakMap();
23468
+ if (!scene) {
23469
+ return;
23470
+ }
23471
+ changeObserver = propertiesService.onPropertyChanged.add((changeInfo) => {
23472
+ const { entity, propertyKey, oldValue, newValue } = changeInfo;
23473
+ // When "name" changes, update the matching override so it follows the rename
23474
+ // instead of creating a new (orphaned) one. Also rewrite any overrides
23475
+ // whose *value* referenced this entity by name so cross-references
23476
+ // (e.g. `mesh.material = ref:redMat`) survive the rename too.
23477
+ if (propertyKey === "name" && typeof newValue === "string") {
23478
+ const targetType = ClassifyEntity(entity, scene);
23479
+ if (targetType !== null && targetType !== "scene") {
23480
+ const previous = previousIdentity.get(entity);
23481
+ if (previous && previous.name !== newValue) {
23482
+ // The entity already has the new name at this point, so compute its new index among same-named siblings.
23483
+ const newIndex = ComputeTargetIndex(scene, targetType, entity, newValue);
23484
+ RenameOverrideTarget(scene, targetType, previous.name, previous.index, newValue, newIndex);
23485
+ // Mirror the rename into override values that reference
23486
+ // this entity by name. SmartAsset textures use the
23487
+ // `samTexture:<key>` form and stay decoupled from the
23488
+ // texture's runtime name, so they don't need rewriting.
23489
+ const valueScheme = targetType === "textures" ? "texture" : "ref";
23490
+ RenameOverrideValueReferences(scene, valueScheme, previous.name, newValue);
23491
+ }
23492
+ previousIdentity.set(entity, { name: newValue, index: ComputeTargetIndex(scene, targetType, entity, newValue) });
23493
+ }
23494
+ return;
23495
+ }
23496
+ if (propertyKey === "id") {
23497
+ return;
23498
+ }
23499
+ let targetType = ClassifyEntity(entity, scene);
23500
+ let targetName;
23501
+ let targetIndex;
23502
+ let propertyPath = String(propertyKey);
23503
+ let targetEntity;
23504
+ if (targetType !== null) {
23505
+ if (targetType === "scene") {
23506
+ targetName = "";
23507
+ targetIndex = 0;
23508
+ targetEntity = scene;
23509
+ }
23510
+ else {
23511
+ targetName = GetEntityName(entity);
23512
+ targetIndex = ComputeTargetIndex(scene, targetType, entity, targetName);
23513
+ targetEntity = entity;
23514
+ }
23515
+ // Seed identity on first contact so rename tracking works
23516
+ if (!previousIdentity.has(targetEntity) && targetName) {
23517
+ previousIdentity.set(targetEntity, { name: targetName, index: targetIndex });
23518
+ }
23519
+ }
23520
+ else {
23521
+ // Sub-object: check if this is a property of a known parent
23522
+ const parentInfo = FindParentEntity(entity, scene);
23523
+ if (!parentInfo) {
23524
+ return;
23525
+ }
23526
+ targetType = parentInfo.targetType;
23527
+ targetName = parentInfo.targetName;
23528
+ targetIndex = parentInfo.targetIndex;
23529
+ propertyPath = `${parentInfo.parentProperty}.${propertyPath}`;
23530
+ }
23531
+ const serializedValue = SerializeOverrideValueForCapture(newValue, scene);
23532
+ if (serializedValue === undefined) {
23533
+ return;
23534
+ }
23535
+ // The Inspector binding has already written `newValue` to the
23536
+ // entity, so pass `oldValue` so the manager can record the
23537
+ // true pre-edit value (without this, RemoveOverride would have
23538
+ // no record of the original and could not restore it).
23539
+ AddOverride(scene, {
23540
+ targetType,
23541
+ targetName,
23542
+ targetIndex,
23543
+ propertyPath,
23544
+ value: serializedValue,
23545
+ }, { originalValue: oldValue });
23546
+ });
23547
+ }
23548
+ attachToScene(sceneContext.currentScene);
23549
+ const sceneSubObserver = sceneContext.currentSceneObservable.add((scene) => attachToScene(scene));
23550
+ return {
23551
+ dispose: () => {
23552
+ sceneSubObserver.remove();
23553
+ if (changeObserver) {
23554
+ changeObserver.remove();
23555
+ changeObserver = null;
23556
+ }
23557
+ },
23558
+ };
23559
+ },
23560
+ };
23561
+ /**
23562
+ * Classifies an entity into an OverrideTargetType by membership in the
23563
+ * scene's standard collections (or by being the scene itself).
23564
+ * @param entity - The entity to classify.
23565
+ * @param scene - The scene to check collections against.
23566
+ * @returns The target type, or null if unrecognized.
23567
+ */
23568
+ function ClassifyEntity(entity, scene) {
23569
+ if (entity === scene) {
23570
+ return "scene";
23571
+ }
23572
+ const obj = entity;
23573
+ if (!obj || typeof obj !== "object") {
23574
+ return null;
23575
+ }
23576
+ if (scene.materials.includes(obj)) {
23577
+ return "materials";
23578
+ }
23579
+ if (scene.meshes.includes(obj)) {
23580
+ return "meshes";
23581
+ }
23582
+ if (scene.lights.includes(obj)) {
23583
+ return "lights";
23584
+ }
23585
+ if (scene.cameras.includes(obj)) {
23586
+ return "cameras";
23587
+ }
23588
+ if (scene.textures.includes(obj)) {
23589
+ return "textures";
23590
+ }
23591
+ if (scene.animationGroups.includes(obj)) {
23592
+ return "animationGroups";
23593
+ }
23594
+ return null;
23595
+ }
23596
+ /**
23597
+ * Gets the name of a scene entity.
23598
+ * @param entity - The entity to get the name from.
23599
+ * @returns The entity name, or an empty string if unavailable.
23600
+ */
23601
+ function GetEntityName(entity) {
23602
+ const obj = entity;
23603
+ return obj?.name ?? "";
23604
+ }
23605
+ /**
23606
+ * Returns the position of `entity` among scene[targetType] objects with the
23607
+ * same name. Used so overrides can disambiguate same-named siblings.
23608
+ * @param scene - The scene to inspect.
23609
+ * @param targetType - The target type / collection name.
23610
+ * @param entity - The entity to locate.
23611
+ * @param name - The name to filter by.
23612
+ * @returns The index within the same-name filter, or 0 if not found.
23613
+ */
23614
+ function ComputeTargetIndex(scene, targetType, entity, name) {
23615
+ const collection = GetCollection(scene, targetType);
23616
+ if (!collection) {
23617
+ return 0;
23618
+ }
23619
+ const sameName = collection.filter((obj) => obj.name === name);
23620
+ const idx = sameName.indexOf(entity);
23621
+ return idx >= 0 ? idx : 0;
23622
+ }
23623
+ /**
23624
+ * Returns the scene collection matching a target type.
23625
+ * @param scene - The scene to inspect.
23626
+ * @param targetType - The target type.
23627
+ * @returns The collection, or null if `targetType` doesn't map to one.
23628
+ */
23629
+ function GetCollection(scene, targetType) {
23630
+ switch (targetType) {
23631
+ case "meshes":
23632
+ return scene.meshes;
23633
+ case "materials":
23634
+ return scene.materials;
23635
+ case "textures":
23636
+ return scene.textures;
23637
+ case "lights":
23638
+ return scene.lights;
23639
+ case "cameras":
23640
+ return scene.cameras;
23641
+ case "animationGroups":
23642
+ return scene.animationGroups;
23643
+ default:
23644
+ return null;
23645
+ }
23646
+ }
23647
+ /**
23648
+ * Serializes a property value into an OverrideValue.
23649
+ * Returns undefined for unsupported types.
23650
+ * @param value - The value to serialize.
23651
+ * @param scene - Optional scene for resolving object references.
23652
+ * @returns The serialized value, or undefined if unsupported.
23653
+ */
23654
+ function SerializeOverrideValueForCapture(value, scene) {
23655
+ // null is a legitimate override value (e.g. clearing a material slot) and
23656
+ // must round-trip as null — substituting "" here would silently corrupt
23657
+ // object-typed slots with an empty string on reload.
23658
+ if (value === null) {
23659
+ return null;
23660
+ }
23661
+ if (typeof value === "number" || typeof value === "string" || typeof value === "boolean") {
23662
+ return value;
23663
+ }
23664
+ // Material reference → "ref:materialName"
23665
+ if (value && typeof value === "object" && "getClassName" in value && typeof value.getClassName === "function") {
23666
+ const className = value.getClassName();
23667
+ if (className.includes("Material") || className.includes("material")) {
23668
+ return `ref:${value.name}`;
23669
+ }
23670
+ }
23671
+ // Texture reference → "samTexture:<key>" if SmartAsset-tracked, else "texture:<name>".
23672
+ // The SAM key is stable across save/load; `texture.name` for a SAM-tracked
23673
+ // texture is the blob URL, which dies on page reload — using it as the
23674
+ // override identifier would break the override after every reload.
23675
+ if (value && typeof value === "object" && "getClassName" in value && scene) {
23676
+ const className = value.getClassName();
23677
+ if (className.includes("Texture") || className.includes("texture")) {
23678
+ const samKey = FindSmartAssetKeyForObject(scene, value);
23679
+ if (samKey !== undefined) {
23680
+ return `samTexture:${samKey}`;
23681
+ }
23682
+ return `texture:${value.name}`;
23683
+ }
23684
+ }
23685
+ // Color3 / Color4
23686
+ if (value && typeof value === "object" && "r" in value && "g" in value && "b" in value) {
23687
+ const color = value;
23688
+ if ("a" in color && color.a !== undefined) {
23689
+ return [color.r, color.g, color.b, color.a];
23690
+ }
23691
+ return [color.r, color.g, color.b];
23692
+ }
23693
+ // Vector3 / Vector4
23694
+ if (value && typeof value === "object" && "x" in value && "y" in value && "z" in value) {
23695
+ const vec = value;
23696
+ if ("w" in vec && vec.w !== undefined) {
23697
+ return [vec.x, vec.y, vec.z, vec.w];
23698
+ }
23699
+ return [vec.x, vec.y, vec.z];
23700
+ }
23701
+ // Vector2
23702
+ if (value && typeof value === "object" && "x" in value && "y" in value && !("z" in value)) {
23703
+ const vec2 = value;
23704
+ return [vec2.x, vec2.y];
23705
+ }
23706
+ return undefined;
23707
+ }
23708
+ /**
23709
+ * Checks if an entity is a sub-object of a known scene entity by scanning
23710
+ * well-known sub-object properties on the scene and its collections.
23711
+ * Returns the parent entity info with the property path prefix.
23712
+ * @param entity - The entity to search for.
23713
+ * @param scene - The scene to search in.
23714
+ * @returns The parent entity info, or null if not found.
23715
+ */
23716
+ function FindParentEntity(entity, scene) {
23717
+ // Check scene sub-objects (imageProcessingConfiguration, fogSettings, etc.)
23718
+ const sceneSubProps = ["imageProcessingConfiguration", "postProcessRenderPipelineManager", "ambientColor", "gravity"];
23719
+ for (const prop of sceneSubProps) {
23720
+ if (scene[prop] === entity) {
23721
+ return { targetType: "scene", targetName: "", targetIndex: 0, parentProperty: prop };
23722
+ }
23723
+ }
23724
+ const collections = [
23725
+ { type: "materials", items: scene.materials },
23726
+ { type: "cameras", items: scene.cameras },
23727
+ { type: "meshes", items: scene.meshes },
23728
+ { type: "lights", items: scene.lights },
23729
+ ];
23730
+ for (const { type, items } of collections) {
23731
+ for (const parent of items) {
23732
+ for (const prop of Object.keys(parent)) {
23733
+ if (prop.startsWith("_")) {
23734
+ continue;
23735
+ }
23736
+ try {
23737
+ if (parent[prop] === entity) {
23738
+ const targetIndex = items.filter((p) => p.name === parent.name).indexOf(parent);
23739
+ return { targetType: type, targetName: parent.name, targetIndex: Math.max(targetIndex, 0), parentProperty: prop };
23740
+ }
23741
+ }
23742
+ catch {
23743
+ // Skip properties that throw on access
23744
+ }
23745
+ }
23746
+ }
23747
+ }
23748
+ return null;
23749
+ }
23750
+
23751
+ // Side-effect import: registers the `.babylon` SceneLoader plugin so the
23752
+ // companion `.babylon` file produced by SerializeProject can be loaded back.
23753
+ // Without this, LoadAssetContainerAsync logs "Unable to find a plugin to
23754
+ // load .babylon files" and the companion load fails.
23755
+ /**
23756
+ * ## `.babylonproj` project file format
23757
+ *
23758
+ * The `.babylonproj` zip on disk packages three layers:
23759
+ * 1. **SmartAsset registry** — URL references to external assets (glb/gltf/textures
23760
+ * loaded via SAM). Local blob/data assets are bundled inside the zip and
23761
+ * extracted to fresh blob URLs on load.
23762
+ * 2. **OverrideManager state** — declarative property overrides applied after load.
23763
+ * 3. **Companion `.babylon`** — meshes, lights, cameras, transform nodes, and
23764
+ * materials that are *not* tracked by SAM (i.e. user-created scene content).
23765
+ * Plus a `companionBindings` side table mapping material texture slots back
23766
+ * to SAM-tracked textures so re-attachment works without embedding texture
23767
+ * bytes in the companion.
23768
+ *
23769
+ * ### What round-trips cleanly
23770
+ * - SAM-tracked assets (re-fetched from their URLs or extracted from the zip)
23771
+ * - User-created `Mesh` geometry, `Material`s (Standard/PBR/Multi/Node), and
23772
+ * `*Texture` slot bindings to SAM textures
23773
+ * - `Light`s, `Camera`s, `TransformNode`s, scene/material image processing,
23774
+ * clear color, fog, environment intensity
23775
+ * - Property overrides on any of the above
23776
+ *
23777
+ * ### Known gaps (not preserved on save/load)
23778
+ * - `PostProcess` attachments to cameras (a post-process attaches to a *specific*
23779
+ * camera instance; we dispose+recreate cameras, leaving post-processes orphaned).
23780
+ * - `AdvancedDynamicTexture` GUI controls — not in `.babylon` format.
23781
+ * - Audio (`Sound` / `AudioEngine` state).
23782
+ * - Particle systems with runtime state, baked vertex animations.
23783
+ * - Complex shader-driven content like GaussianSplatting: the mesh round-trips
23784
+ * but its companion utility materials (`gaussianSplattingDepth`, `ProxyMaterial`)
23785
+ * get duplicated on each load cycle.
23786
+ * - Skeleton animation playback state.
23787
+ *
23788
+ * If you hit a "the scene looks different after load" issue, it's almost
23789
+ * certainly one of the gaps above rather than camera or mesh state drift.
23790
+ */
23791
+ /**
23792
+ * Reserved smart asset key for user-created objects (materials, lights, cameras)
23793
+ * that are persisted as a companion `.babylon` file alongside the project JSON.
23794
+ */
23795
+ const ProjectLocalsKey = "__project_locals__";
23796
+ // ── JSON layer (scene ↔ ISerializedProject) ──
23797
+ /**
23798
+ * Serializes a scene's smart asset map and override registry into a project
23799
+ * bundle. User-created objects (materials, lights, cameras not owned by any
23800
+ * smart asset) are serialized into a companion `.babylon` file rather than
23801
+ * embedded in the project JSON.
23802
+ *
23803
+ * Both managers are looked up (and created if missing) via their respective
23804
+ * `Get…Manager(scene)` accessors, so this function can be called on any scene.
23805
+ *
23806
+ * @param scene - The scene to serialize.
23807
+ * @param baseUrl - Optional base URL for making asset paths relative.
23808
+ * @returns A project bundle containing the JSON document and optional companion file.
23809
+ */
23810
+ function SerializeProject(scene, baseUrl) {
23811
+ const assetMap = SerializeSmartAssetManagerMap(scene, baseUrl);
23812
+ const overrides = SerializeOverrides(scene);
23813
+ // Build a minimal .babylon JSON with only user-created objects, plus the
23814
+ // texture-binding side table that records which SmartAsset textures should
23815
+ // be re-attached to which material slots after load.
23816
+ const companionResult = SerializeCompanionBabylon(scene);
23817
+ let companionBabylon;
23818
+ const assets = { ...assetMap.assets };
23819
+ if (companionResult) {
23820
+ companionBabylon = new Blob([JSON.stringify(companionResult.companion)], { type: "application/json" });
23821
+ assets[ProjectLocalsKey] = { url: ProjectLocalsKey + ".babylon" };
23822
+ }
23823
+ else {
23824
+ // Remove stale companion entry if no locals exist
23825
+ delete assets[ProjectLocalsKey];
23826
+ }
23827
+ const hasBindings = companionResult && Object.keys(companionResult.bindings).length > 0;
23828
+ const project = {
23829
+ version: 2,
23830
+ assets,
23831
+ overrides,
23832
+ ...(hasBindings ? { companionBindings: companionResult.bindings } : {}),
23833
+ };
23834
+ return { project, companionBabylon };
23835
+ }
23836
+ /**
23837
+ * Loads a project file from a URL, File, or pre-parsed object.
23838
+ * Registers all asset entries on the scene's SmartAssetManager, loads all
23839
+ * assets (including the companion `.babylon` for user-created objects), then
23840
+ * applies all overrides via the OverrideManager.
23841
+ *
23842
+ * For loading the `.babylonproj` zip on-disk format, use {@link LoadProjectFileAsync}
23843
+ * instead — it extracts the zip and then calls this function with the embedded
23844
+ * JSON document.
23845
+ *
23846
+ * @param scene - The scene to populate.
23847
+ * @param source - A URL string, File object, or pre-parsed ISerializedProject.
23848
+ * @param rootUrl - Optional root URL for resolving relative asset paths.
23849
+ */
23850
+ async function LoadProjectAsync(scene, source, rootUrl) {
23851
+ let resolvedRootUrl = "";
23852
+ if (typeof source === "string" && true) {
23853
+ const { Tools } = await import('@babylonjs/core/Misc/tools.js');
23854
+ resolvedRootUrl = Tools.GetFolderPath(source);
23855
+ }
23856
+ const raw = await ReadJsonSourceAsync(source);
23857
+ const doc = DeserializeProject(raw);
23858
+ // Pause the engine's render loops for the duration of the swap. Disposing
23859
+ // cameras mid-frame would throw "No camera defined" out of `scene.render`,
23860
+ // which kills the render loop entirely (it is not re-queued after an
23861
+ // uncaught exception). Snapshot the active loops first so we can restore
23862
+ // exactly what was running, even if multiple callbacks were registered.
23863
+ const engine = scene.getEngine();
23864
+ const savedRenderLoops = [...engine.activeRenderLoops];
23865
+ engine.stopRenderLoop();
23866
+ try {
23867
+ // Clear existing state so we load fresh from the project file.
23868
+ // The companion `.babylon` (when present) is the source of truth for all
23869
+ // user-created scene content, so dispose user-owned meshes, lights,
23870
+ // cameras, materials, and animation groups before reloading.
23871
+ await Promise.all(Array.from(GetAllSmartAssets(scene).keys()).map(async (existingKey) => await RemoveSmartAssetAsync(scene, existingKey)));
23872
+ ClearOverrides(scene);
23873
+ for (const mesh of [...scene.meshes]) {
23874
+ mesh.dispose();
23875
+ }
23876
+ for (const tn of [...scene.transformNodes]) {
23877
+ tn.dispose();
23878
+ }
23879
+ for (const ag of [...scene.animationGroups]) {
23880
+ ag.dispose();
23881
+ }
23882
+ for (const mat of [...scene.materials]) {
23883
+ mat.dispose();
23884
+ }
23885
+ for (const light of [...scene.lights]) {
23886
+ light.dispose();
23887
+ }
23888
+ for (const camera of [...scene.cameras]) {
23889
+ camera.dispose();
23890
+ }
23891
+ // Register all assets. Defer the companion .babylon — it must load after
23892
+ // textures are available so binding re-attachment can find them.
23893
+ let hasCompanion = false;
23894
+ for (const [key, entry] of Object.entries(doc.assets)) {
23895
+ if (key === ProjectLocalsKey) {
23896
+ hasCompanion = true;
23897
+ continue;
23898
+ }
23899
+ const resolved = resolvedRootUrl ? ResolveAssetUrl(entry.url, resolvedRootUrl) : entry.url;
23900
+ RegisterSmartAsset(scene, key, resolved, { type: entry.type, extension: entry.extension, metadata: entry.metadata });
23901
+ }
23902
+ await LoadAllSmartAssetsAsync(scene);
23903
+ // Now load the companion .babylon. Its materials were saved with texture
23904
+ // slots stripped (the binding side table records which SmartAsset texture
23905
+ // each slot should be re-attached to), so the loader never sees a broken
23906
+ // texture URL. Pass the .babylon extension hint because blob URLs have
23907
+ // no file extension.
23908
+ if (hasCompanion) {
23909
+ const companionEntry = doc.assets[ProjectLocalsKey];
23910
+ const companionUrl = resolvedRootUrl ? ResolveAssetUrl(companionEntry.url, resolvedRootUrl) : companionEntry.url;
23911
+ await LoadSmartAssetAsync(scene, ProjectLocalsKey, companionUrl, { extension: ".babylon" });
23912
+ if (doc.companionBindings) {
23913
+ ApplyCompanionBindings(scene, doc.companionBindings);
23914
+ }
23915
+ }
23916
+ // Apply overrides
23917
+ if (doc.overrides.length > 0) {
23918
+ DeserializeAndApplyOverrides(scene, doc.overrides);
23919
+ }
23920
+ // Re-assign the active camera if the companion brought in fresh cameras.
23921
+ // The .babylon scene loader populates scene.cameras but does not set
23922
+ // scene.activeCamera, so render would otherwise throw "No camera defined".
23923
+ if (!scene.activeCamera && scene.cameras.length > 0) {
23924
+ scene.activeCamera = scene.cameras[0];
23925
+ }
23926
+ // Attach controls so the user can rotate/zoom/pan after load. New
23927
+ // camera instances from the companion .babylon are not attached to
23928
+ // the canvas by the loader — without this, the camera renders but
23929
+ // ignores mouse/touch input.
23930
+ const canvas = engine.getRenderingCanvas();
23931
+ if (scene.activeCamera && canvas) {
23932
+ scene.activeCamera.attachControl(canvas, true);
23933
+ }
23934
+ }
23935
+ finally {
23936
+ // Always restore the render loops, even if loading threw — otherwise
23937
+ // the canvas stays frozen forever and the user has no way to recover.
23938
+ for (const loop of savedRenderLoops) {
23939
+ engine.runRenderLoop(loop);
23940
+ }
23941
+ }
23942
+ }
23943
+ /**
23944
+ * Validates and parses a serialized project document.
23945
+ * @param data - The raw data to validate (typically parsed JSON).
23946
+ * @returns The validated project document.
23947
+ * @throws If the data does not conform to the expected schema.
23948
+ */
23949
+ function DeserializeProject(data) {
23950
+ if (!data || typeof data !== "object") {
23951
+ throw new Error("ProjectFile: Invalid project file — expected an object.");
23952
+ }
23953
+ const doc = data;
23954
+ if (doc.version !== 2) {
23955
+ throw new Error(`ProjectFile: Unsupported project version "${doc.version}". Expected version 2.`);
23956
+ }
23957
+ // Validate the asset map portion
23958
+ DeserializeSmartAssetMap({ version: 1, assets: doc.assets });
23959
+ // Validate overrides array
23960
+ if (!Array.isArray(doc.overrides)) {
23961
+ throw new Error("ProjectFile: Invalid project file — 'overrides' must be an array.");
23962
+ }
23963
+ // Validate optional companion bindings (shape-only check)
23964
+ if (doc.companionBindings !== undefined) {
23965
+ if (typeof doc.companionBindings !== "object" || doc.companionBindings === null || Array.isArray(doc.companionBindings)) {
23966
+ throw new Error("ProjectFile: Invalid project file — 'companionBindings' must be an object.");
23967
+ }
23968
+ }
23969
+ return data;
23970
+ }
23971
+ // ── Zip layer (.babylonproj on disk) ──
23972
+ /**
23973
+ * Serializes a scene's project (smart assets + overrides) into a `.babylonproj`
23974
+ * zip bundle.
23975
+ *
23976
+ * The zip contains:
23977
+ * - `project.json` — the project document (assets + overrides)
23978
+ * - `__project_locals__.babylon` — companion file for user-created objects (if any)
23979
+ * - Bundled local asset files (blobs the user dragged in from disk)
23980
+ *
23981
+ * Remote URLs (http/https) are left as references and not bundled.
23982
+ *
23983
+ * @param scene - The scene to serialize.
23984
+ * @returns A Blob containing the zip bundle.
23985
+ */
23986
+ async function SaveProjectFileAsync(scene) {
23987
+ const bundle = SerializeProject(scene);
23988
+ const files = {};
23989
+ // Collect local (blob/data URI) assets to bundle inside the zip.
23990
+ // Rewrite their URLs in the project JSON to relative paths.
23991
+ const projectAssets = { ...bundle.project.assets };
23992
+ // Fetch all blob/data URIs in parallel (avoid serial awaits in a loop).
23993
+ const blobEntries = Object.entries(projectAssets).filter(([key, entry]) => key !== ProjectLocalsKey && (entry.url.startsWith("blob:") || entry.url.startsWith("data:")));
23994
+ const fetched = await Promise.all(blobEntries.map(async ([key, entry]) => {
22944
23995
  try {
22945
- await Promise.all(Array.from(GetAllSmartAssets(scene).keys()).map(async (key) => await RemoveSmartAssetAsync(scene, key)));
22946
- await LoadSmartAssetMapAsync(scene, file);
22947
- setStatusMessage(`Loaded: ${GetAllSmartAssets(scene).size} assets`);
23996
+ const response = await fetch(entry.url);
23997
+ const arrayBuffer = await response.arrayBuffer();
23998
+ return { key, entry, arrayBuffer };
22948
23999
  }
22949
- catch (err) {
22950
- setStatusMessage(`Load error: ${err}`);
24000
+ catch {
24001
+ // Can't fetch blob — leave the URL as-is (will break on reload,
24002
+ // but at least the project structure is preserved).
24003
+ return null;
22951
24004
  }
22952
- finally {
22953
- setBusyMessage("");
24005
+ }));
24006
+ for (const result of fetched) {
24007
+ if (!result) {
24008
+ continue;
22954
24009
  }
22955
- }, [scene, isBusy]);
22956
- return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Save Smart Assets", icon: ArrowDownloadRegular, onClick: onSaveAssetMap, disabled: isBusy }), jsx(FileUploadLine, { label: "Load Smart Assets", accept: ".json", onClick: onLoadAssetMap, disabled: isBusy }), jsx(Collapse, { visible: isBusy, children: jsxs("div", { className: styles.busyMessage, children: [jsx(Spinner, { size: "extra-small" }), jsx(Caption1, { children: busyMessage })] }) }), jsx(Collapse, { visible: statusMessage !== "", children: jsx(Caption1, { className: styles.statusMessage, children: statusMessage }) })] }));
22957
- };
24010
+ const { key, entry, arrayBuffer } = result;
24011
+ const ext = GuessExtension(entry.url, IsTextureEntry(entry.url, entry.extension, entry.type));
24012
+ const filename = `assets/${key}${ext}`;
24013
+ files[filename] = new Uint8Array(arrayBuffer);
24014
+ projectAssets[key] = { ...entry, url: filename };
24015
+ }
24016
+ // Add companion .babylon if it exists
24017
+ if (bundle.companionBabylon) {
24018
+ const companionBuffer = await bundle.companionBabylon.arrayBuffer();
24019
+ const companionFilename = ProjectLocalsKey + ".babylon";
24020
+ files[companionFilename] = new Uint8Array(companionBuffer);
24021
+ projectAssets[ProjectLocalsKey] = { url: companionFilename };
24022
+ }
24023
+ // Write the project JSON with updated asset paths
24024
+ const projectWithBundledPaths = {
24025
+ ...bundle.project,
24026
+ assets: projectAssets,
24027
+ };
24028
+ const { zip, strToU8 } = await import('./browser-CANgtOiM.js');
24029
+ files["project.json"] = strToU8(JSON.stringify(projectWithBundledPaths, null, 2));
24030
+ // Create the zip (async — runs in a Web Worker to avoid blocking the UI thread)
24031
+ const zipped = await new Promise((resolve, reject) => {
24032
+ zip(files, { level: 6 }, (err, data) => (err ? reject(err) : resolve(data)));
24033
+ });
24034
+ return new Blob([zipped], { type: "application/zip" });
24035
+ }
24036
+ /**
24037
+ * Loads a `.babylonproj` zip bundle into a scene. Extracts all files, creates
24038
+ * blob URLs for bundled assets, and loads the project through SAM.
24039
+ *
24040
+ * @param scene - The scene to load the project into.
24041
+ * @param zipFile - The `.babylonproj` zip file to load.
24042
+ */
24043
+ async function LoadProjectFileAsync(scene, zipFile) {
24044
+ const arrayBuffer = await zipFile.arrayBuffer();
24045
+ const { unzip, strFromU8 } = await import('./browser-CANgtOiM.js');
24046
+ const extracted = await new Promise((resolve, reject) => {
24047
+ unzip(new Uint8Array(arrayBuffer), (err, data) => (err ? reject(err) : resolve(data)));
24048
+ });
24049
+ // Parse project.json
24050
+ const projectJsonBytes = extracted["project.json"];
24051
+ if (!projectJsonBytes) {
24052
+ throw new Error("ProjectFile: Invalid project bundle — missing project.json");
24053
+ }
24054
+ const projectJson = JSON.parse(strFromU8(projectJsonBytes));
24055
+ // Create blob URLs for all bundled files and rewrite asset URLs
24056
+ for (const [, entry] of Object.entries(projectJson.assets)) {
24057
+ const filename = entry.url;
24058
+ const fileBytes = extracted[filename];
24059
+ if (fileBytes) {
24060
+ const mimeType = GuessMimeType(filename);
24061
+ // Use a named File so LoadAssetContainerAsync can detect the
24062
+ // format from the filename (blob URLs alone have no extension).
24063
+ const file = new File([fileBytes], filename, { type: mimeType });
24064
+ const blobUrl = URL.createObjectURL(file);
24065
+ entry.url = blobUrl;
24066
+ }
24067
+ // If no file found in zip, assume the URL is a remote reference — leave it as-is
24068
+ }
24069
+ // Load through the standard JSON path
24070
+ await LoadProjectAsync(scene, projectJson);
24071
+ // Note: textures and scene files may still reference the blob URLs created
24072
+ // above, so we do NOT revoke them here. They'll be cleaned up when SAM disposes.
24073
+ }
24074
+ // ── Private ──
24075
+ /**
24076
+ * Returns true if a scene object is a "local" — not owned by any external
24077
+ * smart asset, or owned by the reserved `__project_locals__` key.
24078
+ * @param scene - The scene that owns the object.
24079
+ * @param obj - The scene object to check.
24080
+ * @returns True if the object should be included in the companion file.
24081
+ */
24082
+ function IsLocalObject(scene, obj) {
24083
+ const key = FindSmartAssetKeyForObject(scene, obj);
24084
+ return key === undefined || key === ProjectLocalsKey;
24085
+ }
24086
+ /**
24087
+ * Builds a `.babylon`-compatible JSON containing all user-created scene
24088
+ * content (meshes, lights, cameras, transform nodes, and materials not owned
24089
+ * by any external smart asset), plus a side table recording which `*Texture`
24090
+ * slots on each material should be re-attached to which SmartAsset textures
24091
+ * after load.
24092
+ *
24093
+ * Mesh, light, camera, and standalone material serialization is delegated to
24094
+ * `SceneSerializer.SerializeMesh`, which auto-handles geometries, sub-materials,
24095
+ * and skeletons. Texture slots that map to a SmartAsset-tracked texture are
24096
+ * stripped from the serialized material so the `.babylon` loader never sees a
24097
+ * broken URL; the binding table is the sole source of truth for re-attachment.
24098
+ *
24099
+ * @param scene - The scene to extract locals from.
24100
+ * @returns The companion document and binding table, or null if there are no local objects.
24101
+ */
24102
+ function SerializeCompanionBabylon(scene) {
24103
+ const meshes = scene.meshes.filter((m) => m instanceof Mesh && m.name !== "__root__" && IsLocalObject(scene, m));
24104
+ const lights = scene.lights.filter((l) => IsLocalObject(scene, l));
24105
+ const cameras = scene.cameras.filter((c) => IsLocalObject(scene, c));
24106
+ const transformNodes = scene.transformNodes.filter((t) => IsLocalObject(scene, t));
24107
+ // Standalone materials (not attached to any included mesh) need to be
24108
+ // added explicitly — SerializeMesh only picks up materials reachable from
24109
+ // the supplied meshes.
24110
+ const meshMaterialIds = new Set(meshes.map((m) => m.material?.uniqueId).filter((id) => id !== undefined));
24111
+ const standaloneMaterials = scene.materials.filter((mat) => mat.name !== "default material" && IsLocalObject(scene, mat) && !meshMaterialIds.has(mat.uniqueId));
24112
+ if (meshes.length === 0 && lights.length === 0 && cameras.length === 0 && transformNodes.length === 0 && standaloneMaterials.length === 0) {
24113
+ return null;
24114
+ }
24115
+ const companion = SceneSerializer.SerializeMesh([...meshes, ...lights, ...cameras, ...transformNodes], false, false);
24116
+ const allMaterials = companion.materials ?? [];
24117
+ companion.materials = allMaterials;
24118
+ for (const mat of standaloneMaterials) {
24119
+ const serialized = mat.serialize();
24120
+ if (serialized && !allMaterials.some((m) => m.uniqueId === serialized.uniqueId)) {
24121
+ allMaterials.push(serialized);
24122
+ }
24123
+ }
24124
+ // Strip non-JSON-serializable metadata (e.g. metadata that references
24125
+ // another scene object) to avoid `Converting circular structure to JSON`
24126
+ // when the companion is stringified. Simple JSON metadata is preserved.
24127
+ SanitizeMetadataInPlace(companion);
24128
+ // Walk every serialized material (mesh-attached + standalone + multi-material
24129
+ // children) and extract SmartAsset texture bindings, stripping those slots
24130
+ // from the serialized data.
24131
+ const bindings = {};
24132
+ for (const serializedMat of allMaterials) {
24133
+ const matBindings = ExtractTextureBindings(scene, serializedMat);
24134
+ if (Object.keys(matBindings).length > 0 && typeof serializedMat.name === "string") {
24135
+ bindings[serializedMat.name] = matBindings;
24136
+ }
24137
+ }
24138
+ const multiMaterials = companion.multiMaterials ?? [];
24139
+ for (const serializedMat of multiMaterials) {
24140
+ const matBindings = ExtractTextureBindings(scene, serializedMat);
24141
+ if (Object.keys(matBindings).length > 0 && typeof serializedMat.name === "string") {
24142
+ bindings[serializedMat.name] = matBindings;
24143
+ }
24144
+ }
24145
+ return { companion, bindings };
24146
+ }
24147
+ /**
24148
+ * Walks the top-level entity arrays in a serialized companion document and
24149
+ * strips `metadata` fields that cannot be JSON-stringified (typically because
24150
+ * the user put a reference to another scene object in metadata). Simple
24151
+ * JSON-serializable metadata is preserved.
24152
+ * @param companion - The serialized companion document to mutate in-place.
24153
+ */
24154
+ function SanitizeMetadataInPlace(companion) {
24155
+ const arrays = ["meshes", "transformNodes", "lights", "cameras", "materials", "multiMaterials"];
24156
+ for (const arrayKey of arrays) {
24157
+ const arr = companion[arrayKey];
24158
+ if (!Array.isArray(arr)) {
24159
+ continue;
24160
+ }
24161
+ for (const item of arr) {
24162
+ if (item && typeof item === "object" && "metadata" in item && item.metadata !== undefined) {
24163
+ try {
24164
+ item.metadata = JSON.parse(JSON.stringify(item.metadata));
24165
+ }
24166
+ catch {
24167
+ delete item.metadata;
24168
+ }
24169
+ }
24170
+ }
24171
+ }
24172
+ }
24173
+ /**
24174
+ * Walks a serialized material's `*Texture` slots and, for any that reference a
24175
+ * SmartAsset-tracked texture, records a `{slot: samKey}` binding and removes
24176
+ * the slot from the serialized data so the `.babylon` loader does not try to
24177
+ * fetch the (now-dead) original URL.
24178
+ * @param scene - The scene that owns the textures.
24179
+ * @param serializedMaterial - The serialized material data to rewrite in-place.
24180
+ * @returns A map of stripped slot names to their SmartAsset keys.
24181
+ */
24182
+ function ExtractTextureBindings(scene, serializedMaterial) {
24183
+ const bindings = {};
24184
+ for (const [propName, propValue] of Object.entries(serializedMaterial)) {
24185
+ if (!propName.endsWith("Texture") || typeof propValue !== "object" || propValue === null) {
24186
+ continue;
24187
+ }
24188
+ const texData = propValue;
24189
+ const texName = typeof texData.name === "string" ? texData.name : undefined;
24190
+ const texUrl = typeof texData.url === "string" ? texData.url : undefined;
24191
+ if (!texName && !texUrl) {
24192
+ continue;
24193
+ }
24194
+ for (const tex of scene.textures) {
24195
+ const key = FindSmartAssetKeyForObject(scene, tex);
24196
+ if (key && (tex.name === texName || tex.url === texName || tex.name === texUrl || tex.url === texUrl)) {
24197
+ bindings[propName] = key;
24198
+ delete serializedMaterial[propName];
24199
+ break;
24200
+ }
24201
+ }
24202
+ }
24203
+ return bindings;
24204
+ }
24205
+ /**
24206
+ * Re-attaches SmartAsset-tracked textures to user-created material slots
24207
+ * after the companion `.babylon` has loaded. Silently skips bindings whose
24208
+ * material or texture is no longer present (e.g. the underlying SmartAsset
24209
+ * was removed before reload).
24210
+ * @param scene - The scene that owns the materials and textures.
24211
+ * @param bindings - The binding table from the project document.
24212
+ */
24213
+ function ApplyCompanionBindings(scene, bindings) {
24214
+ for (const [materialName, slots] of Object.entries(bindings)) {
24215
+ const mat = scene.materials.find((m) => m.name === materialName);
24216
+ if (!mat) {
24217
+ continue;
24218
+ }
24219
+ for (const [slotName, samKey] of Object.entries(slots)) {
24220
+ const texture = scene.textures.find((tex) => FindSmartAssetKeyForObject(scene, tex) === samKey);
24221
+ if (texture) {
24222
+ mat[slotName] = texture;
24223
+ }
24224
+ }
24225
+ }
24226
+ }
24227
+ /**
24228
+ * Returns true if a serialized asset entry refers to a standalone texture,
24229
+ * based on the registered options or the URL extension.
24230
+ * @param url - The asset URL.
24231
+ * @param extension - Optional explicit extension hint from the registration options.
24232
+ * @param type - Optional explicit type from the registration options.
24233
+ * @returns True if the entry should be treated as a texture.
24234
+ */
24235
+ function IsTextureEntry(url, extension, type) {
24236
+ if (type === "texture") {
24237
+ return true;
24238
+ }
24239
+ const textureExts = GetSmartAssetTextureExtensions();
24240
+ if (extension && textureExts.has(extension.toLowerCase())) {
24241
+ return true;
24242
+ }
24243
+ const ext = ExtractExtension(url);
24244
+ return ext !== "" && textureExts.has(ext);
24245
+ }
24246
+ /**
24247
+ * Extracts the file extension (with leading dot, lowercased) from a URL,
24248
+ * stripping query/hash and ignoring blob/data prefixes.
24249
+ * @param url - The URL to inspect.
24250
+ * @returns The extension including the leading dot, or "" if none found.
24251
+ */
24252
+ function ExtractExtension(url) {
24253
+ if (url.startsWith("blob:") || url.startsWith("data:")) {
24254
+ return "";
24255
+ }
24256
+ const clean = url.split("?")[0].split("#")[0];
24257
+ const lastDot = clean.lastIndexOf(".");
24258
+ const lastSlash = Math.max(clean.lastIndexOf("/"), clean.lastIndexOf("\\"));
24259
+ if (lastDot > lastSlash && lastDot >= 0) {
24260
+ return clean.substring(lastDot).toLowerCase();
24261
+ }
24262
+ return "";
24263
+ }
24264
+ /**
24265
+ * Guesses a file extension for a blob/data URL when bundling into the zip.
24266
+ * @param url - The original URL.
24267
+ * @param isTexture - Whether the key is known to be a texture.
24268
+ * @returns A file extension including the dot (e.g. ".glb", ".png").
24269
+ */
24270
+ function GuessExtension(url, isTexture) {
24271
+ // Try to extract from data URI mime type
24272
+ if (url.startsWith("data:")) {
24273
+ const mimeMatch = url.match(/^data:([^;,]+)/);
24274
+ if (mimeMatch) {
24275
+ const ext = MimeToExtension(mimeMatch[1]);
24276
+ if (ext) {
24277
+ return ext;
24278
+ }
24279
+ }
24280
+ }
24281
+ return isTexture ? ".png" : ".glb";
24282
+ }
24283
+ // Tuple-array `Map`s rather than object literals so the MIME and extension
24284
+ // keys (e.g. "model/gltf-binary", ".glb") don't trigger the naming-convention
24285
+ // rule that runs on object-literal property names.
24286
+ const MimeToExtensionMap = new Map([
24287
+ ["model/gltf-binary", ".glb"],
24288
+ ["model/gltf+json", ".gltf"],
24289
+ ["image/png", ".png"],
24290
+ ["image/jpeg", ".jpg"],
24291
+ ["image/webp", ".webp"],
24292
+ ["application/octet-stream", ".glb"],
24293
+ ["application/json", ".babylon"],
24294
+ ]);
24295
+ const ExtensionToMimeMap = new Map([
24296
+ [".glb", "model/gltf-binary"],
24297
+ [".gltf", "model/gltf+json"],
24298
+ [".babylon", "application/json"],
24299
+ [".png", "image/png"],
24300
+ [".jpg", "image/jpeg"],
24301
+ [".jpeg", "image/jpeg"],
24302
+ [".env", "application/octet-stream"],
24303
+ [".hdr", "application/octet-stream"],
24304
+ [".dds", "application/octet-stream"],
24305
+ [".ktx", "application/octet-stream"],
24306
+ [".ktx2", "application/octet-stream"],
24307
+ [".json", "application/json"],
24308
+ ]);
24309
+ /**
24310
+ * Maps a MIME type to a file extension.
24311
+ * @param mime - The MIME type string.
24312
+ * @returns The file extension including the dot, or empty string if unknown.
24313
+ */
24314
+ function MimeToExtension(mime) {
24315
+ return MimeToExtensionMap.get(mime) ?? "";
24316
+ }
24317
+ /**
24318
+ * Guesses a MIME type from a filename.
24319
+ * @param filename - The filename to check.
24320
+ * @returns The guessed MIME type string.
24321
+ */
24322
+ function GuessMimeType(filename) {
24323
+ const ext = filename.substring(filename.lastIndexOf(".")).toLowerCase();
24324
+ return ExtensionToMimeMap.get(ext) ?? "application/octet-stream";
24325
+ }
22958
24326
 
22959
- const SmartAssetsPaneKey$1 = "Smart Assets";
24327
+ const ProjectAuthoringPaneKey = "Project Authoring";
22960
24328
  const SceneFileAccept = [".glb", ".gltf", ".babylon", ".obj"];
22961
24329
  const TextureFileAccept = Array.from(GetSmartAssetTextureExtensions());
22962
24330
  const AllAcceptString = [...SceneFileAccept, ...TextureFileAccept].join(",");
@@ -22965,15 +24333,17 @@ function _isTextureExtension(ext) {
22965
24333
  return GetSmartAssetTextureExtensions().has(ext);
22966
24334
  }
22967
24335
  /**
22968
- * Inspector pane service that hosts the Smart Assets pane.
24336
+ * Inspector pane service that hosts the Project Authoring pane. Combines
24337
+ * smart-asset management (assets list, asset map I/O) with override-driven
24338
+ * scene authoring (material assignment, override summary).
22969
24339
  */
22970
- const SmartAssetsServiceDefinition = {
22971
- friendlyName: "Smart Assets",
24340
+ const BabylonProjectAuthoringServiceDefinition = {
24341
+ friendlyName: "Project Authoring",
22972
24342
  consumes: [ShellServiceIdentity, SceneContextIdentity, SelectionServiceIdentity],
22973
24343
  factory: (shellService, sceneContext, selectionService) => {
22974
24344
  const paneRegistration = shellService.addSidePane({
22975
- key: SmartAssetsPaneKey$1,
22976
- title: SmartAssetsPaneKey$1,
24345
+ key: ProjectAuthoringPaneKey,
24346
+ title: ProjectAuthoringPaneKey,
22977
24347
  icon: CubeRegular,
22978
24348
  horizontalLocation: "right",
22979
24349
  verticalLocation: "top",
@@ -22981,7 +24351,7 @@ const SmartAssetsServiceDefinition = {
22981
24351
  teachingMoment: false,
22982
24352
  content: () => {
22983
24353
  const scene = useObservableState(() => sceneContext.currentScene, sceneContext.currentSceneObservable);
22984
- return scene ? jsx(SmartAssetsPane, { scene: scene, selectionService: selectionService }) : null;
24354
+ return scene ? jsx(BabylonProjectAuthoringPane, { scene: scene, selectionService: selectionService }) : null;
22985
24355
  },
22986
24356
  });
22987
24357
  return {
@@ -23036,11 +24406,81 @@ const useStyles$3 = makeStyles({
23036
24406
  hiddenInput: {
23037
24407
  display: "none",
23038
24408
  },
24409
+ overrideRow: {
24410
+ display: "flex",
24411
+ gap: tokens.spacingHorizontalXS,
24412
+ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
24413
+ fontSize: "10px",
24414
+ fontFamily: "monospace",
24415
+ },
24416
+ dimSeparator: {
24417
+ opacity: 0.5,
24418
+ },
24419
+ overrideValue: {
24420
+ color: tokens.colorPaletteGreenForeground1,
24421
+ },
24422
+ busyMessage: {
24423
+ display: "flex",
24424
+ alignItems: "center",
24425
+ gap: tokens.spacingHorizontalXS,
24426
+ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
24427
+ },
23039
24428
  });
23040
- // ── Smart Assets Pane ──
23041
- const SmartAssetsPane = (props) => {
24429
+ // ── Project Authoring Pane ──
24430
+ const BabylonProjectAuthoringPane = (props) => {
23042
24431
  const { scene, selectionService } = props;
23043
- return (jsxs(Accordion, { uniqueId: "SmartAssets", enablePinnedItems: true, enableHiddenItems: true, enableSearchItems: true, children: [jsx(AccordionSection, { title: "Assets", children: jsx(SmartAssetList, { scene: scene, selectionService: selectionService }) }), jsx(AccordionSection, { title: "Asset Map", children: jsx(SmartAssetProjectTools, { scene: scene }) })] }));
24432
+ return (jsxs(Accordion, { uniqueId: "ProjectAuthoring", enablePinnedItems: true, enableHiddenItems: true, enableSearchItems: true, children: [jsx(AccordionSection, { title: "Project File", children: jsx(ProjectFileTools, { scene: scene }) }), jsx(AccordionSection, { title: "Assets", children: jsx(SmartAssetList, { scene: scene, selectionService: selectionService }) }), jsx(AccordionSection, { title: "Override Summary", children: jsx(OverrideSummary, { scene: scene }) })] }));
24433
+ };
24434
+ // ── Project File ──
24435
+ /**
24436
+ * Save/load controls for the `.babylonproj` zip bundle that captures the
24437
+ * scene's smart assets and overrides as a single project file.
24438
+ * @param props - Component props.
24439
+ * @returns The project file controls.
24440
+ */
24441
+ const ProjectFileTools = (props) => {
24442
+ const { scene } = props;
24443
+ const styles = useStyles$3();
24444
+ const [status, setStatus] = useState("");
24445
+ const [busy, setBusy] = useState("");
24446
+ const isBusy = busy !== "";
24447
+ const onSaveProject = useCallback(async () => {
24448
+ if (isBusy) {
24449
+ return;
24450
+ }
24451
+ setBusy("Saving project...");
24452
+ setStatus("");
24453
+ try {
24454
+ const blob = await SaveProjectFileAsync(scene);
24455
+ Tools.Download(blob, "scene.babylonproj");
24456
+ setStatus("Saved scene.babylonproj");
24457
+ }
24458
+ catch (err) {
24459
+ setStatus(`Save error: ${err}`);
24460
+ }
24461
+ finally {
24462
+ setBusy("");
24463
+ }
24464
+ }, [scene, isBusy]);
24465
+ const onLoadProject = useCallback(async (files) => {
24466
+ const file = files[0];
24467
+ if (!file || isBusy) {
24468
+ return;
24469
+ }
24470
+ setBusy("Loading project...");
24471
+ setStatus("");
24472
+ try {
24473
+ await LoadProjectFileAsync(scene, file);
24474
+ setStatus(`Loaded ${file.name}`);
24475
+ }
24476
+ catch (err) {
24477
+ setStatus(`Load error: ${err}`);
24478
+ }
24479
+ finally {
24480
+ setBusy("");
24481
+ }
24482
+ }, [scene, isBusy]);
24483
+ return (jsxs(Fragment, { children: [jsx(ButtonLine, { label: "Save Project (.babylonproj)", icon: SaveRegular, onClick: onSaveProject, disabled: isBusy }), jsx(FileUploadLine, { label: "Load Project (.babylonproj)", accept: ".babylonproj", onClick: onLoadProject, disabled: isBusy }), isBusy && (jsxs("div", { className: styles.busyMessage, children: [jsx(Spinner, { size: "extra-small" }), jsx(Caption1, { children: busy })] })), status && jsx("div", { className: styles.statusMessage, children: status })] }));
23044
24484
  };
23045
24485
  // ── Smart Asset List ──
23046
24486
  const SmartAssetList = (props) => {
@@ -23055,8 +24495,10 @@ const SmartAssetList = (props) => {
23055
24495
  const compactToolContext = useMemo(() => ({ ...toolContext, size: "small" }), [toolContext]);
23056
24496
  // Subscribe reactively to changes — re-renders the asset list whenever
23057
24497
  // RegisterSmartAsset / Load / Remove / Reload fire onChangedObservable.
24498
+ // Filter out the reserved companion key so it does not appear as a user
24499
+ // asset alongside dragged-in textures/models.
23058
24500
  const sam = GetSmartAssetManager(scene);
23059
- const assets = useObservableState(useCallback(() => Array.from(GetAllSmartAssets(scene), ([key, url]) => ({ key, url })), [scene]), sam.onChangedObservable);
24501
+ const assets = useObservableState(useCallback(() => Array.from(GetAllSmartAssets(scene), ([key, url]) => ({ key, url })).filter((a) => a.key !== ProjectLocalsKey), [scene]), sam.onChangedObservable);
23060
24502
  const onAddAsset = useCallback(() => {
23061
24503
  fileInputRef.current?.click();
23062
24504
  }, []);
@@ -23107,6 +24549,12 @@ const SmartAssetList = (props) => {
23107
24549
  }, [scene]);
23108
24550
  const onReloadAsset = useCallback(async (key) => {
23109
24551
  await ReloadSmartAssetAsync(scene, key);
24552
+ // ReloadSmartAssetAsync disposes the old asset and loads fresh
24553
+ // instances — those new objects have no knowledge of previously
24554
+ // applied overrides, so we must re-apply them. (The swap path
24555
+ // does the same.) Without this, overrides on a smart asset
24556
+ // appear to silently revert whenever the user hits Reload.
24557
+ ApplyAllOverrides(scene);
23110
24558
  setStatus(`Reloaded: ${key}`);
23111
24559
  }, [scene]);
23112
24560
  const onSwapAsset = useCallback((key) => {
@@ -23234,6 +24682,34 @@ const SmartAssetList = (props) => {
23234
24682
  }), jsx(ButtonLine, { label: "Add Asset", icon: AddRegular, onClick: onAddAsset }), jsx("input", { ref: fileInputRef, type: "file", accept: AllAcceptString, multiple: true, className: styles.hiddenInput, onChange: onFileSelected }), status && jsx(Caption1, { className: styles.statusMessage, children: status })] }));
23235
24683
  };
23236
24684
  // ── Utilities ──
24685
+ // ── Override Summary ──
24686
+ /**
24687
+ * Pane content that lists all registered overrides for the scene. Subscribes
24688
+ * directly to the manager's change observable so loads, deletes, and
24689
+ * Inspector-driven edits update the view instantly.
24690
+ * @param props - Component props.
24691
+ * @returns The override list view.
24692
+ */
24693
+ const OverrideSummary = (props) => {
24694
+ const { scene } = props;
24695
+ const styles = useStyles$3();
24696
+ const overrideManager = GetOverrideManager(scene);
24697
+ const overrideList = useObservableState(useCallback(() => {
24698
+ return GetOverrides(scene).map((o) => {
24699
+ const nameLabel = o.targetName === "" ? "(scene)" : o.targetIndex > 0 ? `${o.targetName}[${o.targetIndex}]` : o.targetName;
24700
+ return {
24701
+ target: `${o.targetType}.${nameLabel}`,
24702
+ prop: o.propertyPath,
24703
+ value: String(o.value),
24704
+ };
24705
+ });
24706
+ }, [scene]), overrideManager.onChangedObservable);
24707
+ if (overrideList.length === 0) {
24708
+ return jsx("div", { className: styles.emptyMessage, children: "No overrides tracked. Edit properties in Inspector to create overrides." });
24709
+ }
24710
+ return (jsx(Fragment, { children: overrideList.map((o, i) => (jsxs("div", { className: styles.overrideRow, children: [jsx("span", { children: o.target }), jsx("span", { className: styles.dimSeparator, children: "." }), jsx("span", { children: o.prop }), jsx("span", { className: styles.dimSeparator, children: "=" }), jsx("span", { className: styles.overrideValue, children: ShortenValue(o.value) })] }, i))) }));
24711
+ };
24712
+ // ── Utilities ──
23237
24713
  /**
23238
24714
  * Finds the first scene entity produced by a smart asset key, for click-to-select.
23239
24715
  * Prefers non-root meshes, then materials, then textures.
@@ -23293,6 +24769,14 @@ function _getExtension(url) {
23293
24769
  }
23294
24770
  return "";
23295
24771
  }
24772
+ /**
24773
+ * Truncates a value string to a maximum display length.
24774
+ * @param value - The value string to shorten.
24775
+ * @returns The truncated string, with an ellipsis if it was shortened.
24776
+ */
24777
+ function ShortenValue(value) {
24778
+ return value.length > 30 ? value.substring(0, 27) + "…" : value;
24779
+ }
23296
24780
 
23297
24781
  const AnimationGroupLoadingModes = [
23298
24782
  { label: "Clean", value: 0 /* SceneLoaderAnimationGroupLoadingMode.Clean */ },
@@ -24096,7 +25580,7 @@ function ShowInspector(scene, options = {}) {
24096
25580
  // Stats pane tab and related services.
24097
25581
  StatsServiceDefinition,
24098
25582
  // Tools pane tab and related services.
24099
- ToolsServiceDefinition, ExportServiceDefinition, SmartAssetPromptServiceDefinition, SmartAssetsServiceDefinition, GLTFAnimationImportServiceDefinition, GLTFLoaderOptionsServiceDefinition, GLTFValidationServiceDefinition, CaptureToolsDefinition,
25583
+ ToolsServiceDefinition, ExportServiceDefinition, SmartAssetPromptServiceDefinition, BabylonProjectAuthoringServiceDefinition, OverrideCaptureServiceDefinition, GLTFAnimationImportServiceDefinition, GLTFLoaderOptionsServiceDefinition, GLTFValidationServiceDefinition, CaptureToolsDefinition,
24100
25584
  // Settings pane tab and related services.
24101
25585
  SettingsServiceDefinition, InspectorSettingsServiceDefinition, WatcherSettingsServiceDefinition, ShellSettingsServiceDefinition,
24102
25586
  // Adds a button to refresh all properties manually (when watcher is in "manual" mode).
@@ -24958,4 +26442,4 @@ const TextAreaPropertyLine = (props) => {
24958
26442
  AttachDebugLayer();
24959
26443
 
24960
26444
  export { GetPropertyDescriptor as $, Accordion as A, Button as B, CheckboxPropertyLine as C, Color4PropertyLine as D, ColorPickerPopup as E, ColorStepGradientComponent as F, ComboBox as G, ComboBoxPropertyLine as H, ConstructorFactory as I, ConvertOptions as J, DebugServiceIdentity as K, Link as L, MessageBar as M, NumberInputPropertyLine as N, DetachDebugLayer as O, Popover as P, DraggableLine as Q, Dropdown as R, ShellServiceIdentity as S, TextInputPropertyLine as T, EntitySelector as U, Vector3PropertyLine as V, ErrorBoundary as W, ExtensibleAccordion as X, FactorGradientComponent as Y, FactorGradientList as Z, FileUploadLine as _, useToast as a, ThemeServiceIdentity as a$, GizmoServiceIdentity as a0, HexPropertyLine as a1, InfoLabel as a2, InputHexField as a3, InputHsvField as a4, Inspector as a5, InterceptFunction as a6, InterceptProperty as a7, IsPropertyReadonly as a8, LineContainer as a9, SearchBox as aA, SelectionServiceDefinition as aB, SettingsServiceIdentity as aC, SettingsStore as aD, SettingsStoreIdentity as aE, ShowInspector as aF, SidePaneContainer as aG, SkeletonSelector as aH, Slider as aI, SpinButton as aJ, StartInspectable as aK, StatsServiceIdentity as aL, StringDropdown as aM, StringDropdownPropertyLine as aN, StringifiedPropertyLine as aO, Switch as aP, SwitchPropertyLine as aQ, SyncedSliderInput as aR, SyncedSliderPropertyLine as aS, TeachingMoment as aT, TextAreaPropertyLine as aU, TextInput as aV, TextPropertyLine as aW, Textarea as aX, TextureSelector as aY, TextureUpload as aZ, Theme as a_, LinkPropertyLine as aa, LinkToEntityPropertyLine as ab, List as ac, MakeDialogTeachingMoment as ad, MakeLazyComponent as ae, MakeModularBridge as af, MakeModularTool as ag, MakePopoverTeachingMoment as ah, MakePropertyHook as ai, MakeTeachingMoment as aj, MaterialSelector as ak, NodeSelector as al, NumberDropdown as am, NumberDropdownPropertyLine as an, ObservableCollection as ao, Pane as ap, PlaceholderPropertyLine as aq, PositionedPopover as ar, PropertiesServiceIdentity as as, Property as at, PropertyContext as au, PropertyLine as av, QuaternionPropertyLine as aw, RotationVectorPropertyLine as ax, SceneExplorerServiceIdentity as ay, SearchBar as az, useInterceptObservable as b, ToastProvider as b0, ToggleButton as b1, Tooltip as b2, UploadButton as b3, Vector2PropertyLine as b4, Vector4PropertyLine as b5, WatcherServiceIdentity as b6, inspectorAssetNotFoundHandler as b7, useAngleConverters as b8, useAsyncResource as b9, useColor3Property as ba, useColor4Property as bb, useEventListener as bc, useEventfulState as bd, useKeyListener as be, useKeyState as bf, useObservableCollection as bg, useOrderedObservableCollection as bh, usePollingObservable as bi, usePropertyChangedNotifier as bj, useQuaternionProperty as bk, useResource as bl, useTheme as bm, useThemeMode as bn, useVector3Property as bo, LinkToEntity as c, SpinButtonPropertyLine as d, useProperty as e, SceneContextIdentity as f, SelectionServiceIdentity as g, useObservableState as h, AccordionSection as i, ButtonLine as j, ToolsServiceIdentity as k, AccordionSectionItem as l, AttachDebugLayer as m, BooleanBadgePropertyLine as n, BoundProperty as o, BridgeCommandRegistryIdentity as p, BuiltInsExtensionFeed as q, Checkbox as r, ChildWindow as s, Collapse as t, useExtensionManager as u, Color3GradientComponent as v, Color3GradientList as w, Color3PropertyLine as x, Color4GradientComponent as y, Color4GradientList as z };
24961
- //# sourceMappingURL=index-PYblOaAV.js.map
26445
+ //# sourceMappingURL=index-UoPnMkyH.js.map