@babylonjs/inspector 8.51.2 → 8.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { createContext, forwardRef, useContext, useState, useCallback, Component, useMemo, useEffect, useRef, useReducer, Children, isValidElement, useLayoutEffect, cloneElement, useImperativeHandle, createElement, Suspense, memo, Fragment as Fragment$1, lazy } from 'react';
3
- import { tokens, makeStyles, Tooltip as Tooltip$1, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, useFluent, Accordion as Accordion$1, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, MessageBar as MessageBar$1, MessageBarBody, AccordionItem, SearchBox as SearchBox$1, Portal, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, mergeClasses, Body1Strong, useId, useToastController, Toast, ToastTitle, FluentProvider, Toaster, Checkbox as Checkbox$1, createLightTheme, createDarkTheme, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Switch as Switch$1, RendererProvider, createDOMRenderer, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, treeItemLevelToken, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, MenuItemCheckbox, SpinButton as SpinButton$1, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, PresenceBadge, Slider, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, MessageBarTitle, Subtitle2, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, Field } from '@fluentui/react-components';
3
+ import { tokens, makeStyles, Tooltip as Tooltip$1, Button as Button$1, Spinner, Link as Link$1, Caption1, Body1, useFluent, Accordion as Accordion$1, AccordionHeader, Subtitle2Stronger, AccordionPanel, Divider, MessageBar as MessageBar$1, MessageBarBody, AccordionItem, SearchBox as SearchBox$1, Portal, ToggleButton as ToggleButton$1, InfoLabel as InfoLabel$1, mergeClasses, Body1Strong, useId, useToastController, Toast, ToastTitle, FluentProvider, Toaster, Checkbox as Checkbox$1, createLightTheme, createDarkTheme, TeachingPopover, TeachingPopoverSurface, TeachingPopoverHeader, TeachingPopoverBody, Switch as Switch$1, createDOMRenderer, RendererProvider, Menu, MenuTrigger, SplitButton, MenuPopover, MenuList, MenuItem, Toolbar as Toolbar$1, ToolbarRadioButton, MenuGroup, MenuGroupHeader, treeItemLevelToken, FlatTree, FlatTreeItem, TreeItemLayout, MenuDivider, MenuItemCheckbox, SpinButton as SpinButton$1, Input, Dropdown as Dropdown$1, Option, Popover as Popover$1, PopoverTrigger, PopoverSurface, ColorPicker, ColorArea, ColorSlider, AlphaSlider, ColorSwatch, PresenceBadge, Slider, MenuItemRadio, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, List as List$1, ListItem, Badge, Label, MessageBarTitle, Subtitle2, useComboboxFilter, Combobox, Textarea as Textarea$1, ToolbarButton, ToolbarDivider, Field } from '@fluentui/react-components';
4
4
  import { ErrorCircleRegular, EyeFilled, EyeOffRegular, CheckmarkFilled, EditRegular, FilterRegular, PinFilled, PinRegular, ArrowCircleUpRegular, ChevronCircleRight16Regular, ChevronCircleRight20Regular, ChevronCircleDown16Regular, ChevronCircleDown20Regular, Copy16Regular, CopyRegular, PanelLeftExpandRegular, PanelRightExpandRegular, PanelLeftContractRegular, PanelRightContractRegular, PictureInPictureEnterRegular, MoreHorizontalRegular, LayoutColumnTwoFocusLeftFilled, LayoutColumnTwoSplitLeftFocusTopLeftFilled, LayoutColumnTwoSplitLeftFocusBottomLeftFilled, LayoutColumnTwoFocusRightFilled, LayoutColumnTwoSplitRightFocusTopRightFilled, LayoutColumnTwoSplitRightFocusBottomRightFilled, SettingsRegular, DocumentTextRegular, createFluentIcon, TextSortAscendingRegular, GlobeRegular, ArrowExpandAllRegular, ArrowCollapseAllRegular, CubeTreeRegular, BugRegular, ArrowUploadRegular, ArrowDownloadRegular, StopRegular, RecordRegular, DataBarHorizontalRegular, WrenchRegular, WeatherSunnyRegular, WeatherMoonRegular, ArrowRotateClockwiseRegular, ArrowExpandRegular, SelectObjectRegular, CubeRegular, CameraRegular, AddRegular, DeleteRegular, FullScreenMaximizeRegular, ChevronDownRegular, ChevronRightRegular, CircleSmallFilled, SaveRegular, PreviousRegular, ArrowPreviousRegular, TriangleLeftRegular, RecordStopRegular, PlayRegular, ArrowNextRegular, NextRegular, PauseRegular, LinkDismissRegular, LinkEditRegular, ArrowUndoRegular, BracesRegular, BracesDismiss16Regular, EyeRegular, CloudArrowUpRegular, CloudArrowDownRegular, EyeOffFilled, ArrowMoveFilled, StopFilled, PlayFilled, LockOpenRegular, LockClosedRegular, ResizeRegular, ChevronUpRegular, ArrowResetRegular, CircleHalfFillRegular, EyedropperRegular, PaintBucketRegular, InkStrokeRegular, StackRegular, FilmstripRegular, PauseFilled, WeatherSunnyLowFilled, LayerRegular, FrameRegular, AppGenericRegular, RectangleLandscapeRegular, BorderOutsideRegular, BorderNoneRegular, MyLocationRegular, LightbulbRegular, VideoFilled, VideoRegular, FlashlightRegular, FlashlightOffRegular, DropRegular, BlurRegular, PipelineRegular, PersonWalkingRegular, DataLineRegular, SoundWaveCircleRegular, PersonSquareRegular, LayerDiagonalPersonRegular, ImageEditRegular, ImageRegular, TargetRegular, PersonFeedbackRegular, BranchRegular, DeleteFilled } from '@fluentui/react-icons';
5
5
  import { Color3, Color4 } from '@babylonjs/core/Maths/math.color.js';
6
6
  import { Vector3, Quaternion, Matrix, Vector2, Vector4, TmpVectors } from '@babylonjs/core/Maths/math.vector.js';
@@ -1258,9 +1258,9 @@ function useAccordionSectionItemState(props) {
1258
1258
  }
1259
1259
  prevItemIdRef.current = itemId;
1260
1260
  }, [itemId, sectionCtx?.sectionId]);
1261
- // Register item and detect duplicates
1261
+ // Register item and detect duplicates (skip nested items, as children of other AccordionSectionItem should not participate in pin/hide/search).
1262
1262
  useEffect(() => {
1263
- if (!accordionCtx || !itemUniqueId) {
1263
+ if (!accordionCtx || !itemUniqueId || isNested) {
1264
1264
  return;
1265
1265
  }
1266
1266
  const { registeredItemIds } = accordionCtx;
@@ -1272,7 +1272,7 @@ function useAccordionSectionItemState(props) {
1272
1272
  return () => {
1273
1273
  registeredItemIds.delete(itemUniqueId);
1274
1274
  };
1275
- }, [accordionCtx, itemUniqueId, itemId, itemLabel, sectionCtx?.sectionId]);
1275
+ }, [accordionCtx, itemUniqueId, itemId, itemLabel, sectionCtx?.sectionId, isNested]);
1276
1276
  // If no context, static item, or nested, return undefined
1277
1277
  if (!accordionCtx || staticItem) {
1278
1278
  return undefined;
@@ -1660,12 +1660,12 @@ const ToggleButton = (props) => {
1660
1660
  const classes = useStyles$R();
1661
1661
  const [checked, setChecked] = useState(value);
1662
1662
  const toggle = useCallback(() => {
1663
- setChecked((prev) => {
1664
- const enabled = !prev;
1663
+ setChecked((prevChecked) => {
1664
+ const enabled = !prevChecked;
1665
1665
  onChange(enabled);
1666
1666
  return enabled;
1667
1667
  });
1668
- }, [setChecked]);
1668
+ }, [onChange]);
1669
1669
  useEffect(() => {
1670
1670
  setChecked(props.value);
1671
1671
  }, [props.value]);
@@ -2752,22 +2752,9 @@ const ChildWindow = (props) => {
2752
2752
  body.style.padding = "0";
2753
2753
  body.style.display = "flex";
2754
2754
  body.style.overflow = "hidden";
2755
- const applyWindowState = () => {
2756
- // Setup the window state, including creating a Fluent/Griffel "renderer" for managing runtime styles/classes in the child window.
2757
- setWindowState({ mountNode: body, renderer: createDOMRenderer(childWindow.document) });
2758
- onOpenChange?.(true);
2759
- };
2760
- // Once the child window document is ready, setup the window state which will trigger another effect that renders into the child window.
2761
- if (childWindow.document.readyState === "complete") {
2762
- applyWindowState();
2763
- }
2764
- else {
2765
- const onChildWindowLoad = () => {
2766
- applyWindowState();
2767
- };
2768
- childWindow.addEventListener("load", onChildWindowLoad, { once: true });
2769
- disposeActions.push(() => childWindow.removeEventListener("load", onChildWindowLoad));
2770
- }
2755
+ // Setup the window state, including creating a Fluent/Griffel "renderer" for managing runtime styles/classes in the child window.
2756
+ setWindowState({ mountNode: body, renderer: createDOMRenderer(childWindow.document) });
2757
+ onOpenChange?.(true);
2771
2758
  // When the child window is closed for any reason, transition back to a closed state.
2772
2759
  const onChildWindowUnload = () => {
2773
2760
  setWindowState(undefined);
@@ -3640,7 +3627,7 @@ const SelectionServiceDefinition = {
3640
3627
  selectedEntityObservable.notifyObservers();
3641
3628
  if (item) {
3642
3629
  const disposable = item;
3643
- if (disposable.dispose) {
3630
+ if (typeof disposable.dispose === "function") {
3644
3631
  disposedHook = InterceptFunction(disposable, "dispose", { afterCall: () => setSelectedItem(null) });
3645
3632
  }
3646
3633
  }
@@ -4005,7 +3992,7 @@ function useSceneExplorerDragDrop(options) {
4005
3992
 
4006
3993
  const SyntheticUniqueIds = new WeakMap();
4007
3994
  function GetEntityId(entity) {
4008
- if (entity.uniqueId !== undefined) {
3995
+ if ("uniqueId" in entity && typeof entity.uniqueId === "number") {
4009
3996
  return entity.uniqueId;
4010
3997
  }
4011
3998
  let id = SyntheticUniqueIds.get(entity);
@@ -4014,6 +4001,13 @@ function GetEntityId(entity) {
4014
4001
  }
4015
4002
  return id;
4016
4003
  }
4004
+ function IsEntityHidden(entity) {
4005
+ return ("reservedDataStore" in entity &&
4006
+ typeof entity.reservedDataStore === "object" &&
4007
+ entity.reservedDataStore &&
4008
+ "hidden" in entity.reservedDataStore &&
4009
+ entity.reservedDataStore.hidden === true);
4010
+ }
4017
4011
  function GetEntitySection(entityItem) {
4018
4012
  let current = entityItem;
4019
4013
  while (current.type === "entity") {
@@ -4331,7 +4325,7 @@ const EntityTreeItem = (props) => {
4331
4325
  };
4332
4326
  const SceneExplorer = (props) => {
4333
4327
  const classes = useStyles$L();
4334
- const { sections, entityCommandProviders, sectionCommandProviders, scene, selectedEntity } = props;
4328
+ const { sections, entityCommandProviders, sectionCommandProviders, scene, selectedEntity = null } = props;
4335
4329
  const [openItems, setOpenItems] = useState(new Set());
4336
4330
  const [sceneVersion, setSceneVersion] = useState(0);
4337
4331
  const scrollViewRef = useRef(null);
@@ -4400,7 +4394,7 @@ const SceneExplorer = (props) => {
4400
4394
  scene: scene,
4401
4395
  };
4402
4396
  for (const section of sections) {
4403
- const rootEntities = section.getRootEntities().filter((entity) => !entity.reservedDataStore?.hidden);
4397
+ const rootEntities = section.getRootEntities().filter((entity) => !IsEntityHidden(entity));
4404
4398
  const sectionTreeItem = {
4405
4399
  type: "section",
4406
4400
  sectionName: section.displayName,
@@ -4432,7 +4426,7 @@ const SceneExplorer = (props) => {
4432
4426
  (treeItem) => {
4433
4427
  if (section.getEntityChildren) {
4434
4428
  const children = section.getEntityChildren(treeItem.entity);
4435
- return children.filter((child) => !child.reservedDataStore?.hidden).map((child) => createEntityTreeItemData(child, treeItem));
4429
+ return children.filter((child) => !IsEntityHidden(child)).map((child) => createEntityTreeItemData(child, treeItem));
4436
4430
  }
4437
4431
  return null;
4438
4432
  },
@@ -6042,7 +6036,13 @@ const SpinButton = forwardRef((props, ref) => {
6042
6036
  const [isFocusedShiftKeyPressed, setIsFocusedShiftKeyPressed] = useState(false);
6043
6037
  // step and forceInt are not mutually exclusive since there could be cases where you want to forceInt but have spinButton jump >1 int per spin
6044
6038
  const step = CoerceStepValue(props.step ?? 1, isUnfocusedAltKeyPressed || isFocusedAltKeyPressed, isUnfocusedShiftKeyPressed || isFocusedShiftKeyPressed);
6045
- const precision = Math.min(4, Math.max(0, CalculatePrecision(step))); // Cap precision at 4 to avoid wild numbers
6039
+ const stepPrecision = Math.max(0, CalculatePrecision(step));
6040
+ const valuePrecision = Math.max(0, CalculatePrecision(value));
6041
+ // Display precision: controls how many decimals are shown in the formatted displayValue. Cap at 4 to avoid wild numbers
6042
+ const displayPrecision = Math.min(4, Math.max(stepPrecision, valuePrecision));
6043
+ // Set to large const to prevent Fluent from rounding user-entered values on commit
6044
+ // We control display formatting ourselves via displayValue, so this only affects internal rounding. The value stored internally will still have max precision
6045
+ const fluentPrecision = 20;
6046
6046
  useEffect(() => {
6047
6047
  if (props.value !== lastCommittedValue.current) {
6048
6048
  lastCommittedValue.current = props.value;
@@ -6070,6 +6070,30 @@ const SpinButton = forwardRef((props, ref) => {
6070
6070
  tryCommitValue(data.value);
6071
6071
  }
6072
6072
  };
6073
+ // Strip the unit suffix (e.g. "deg" or " deg") from the raw input value before evaluating expressions.
6074
+ const stripUnit = (val) => {
6075
+ if (!props.unit) {
6076
+ return val;
6077
+ }
6078
+ const regex = new RegExp("\\s*" + props.unit + "$");
6079
+ const match = val.match(regex);
6080
+ if (match) {
6081
+ return val.slice(0, -match[0].length);
6082
+ }
6083
+ return val;
6084
+ };
6085
+ // Allow arbitrary expressions, primarily for math operations (e.g. 10*60 for 10 minutes in seconds).
6086
+ // Use Function constructor to safely evaluate the expression without allowing access to scope.
6087
+ // If the expression is invalid, fallback to NaN which will be caught by validateValue and prevent committing.
6088
+ const evaluateExpression = (rawValue) => {
6089
+ const val = stripUnit(rawValue).trim();
6090
+ try {
6091
+ return Number(Function(`"use strict";return (${val})`)());
6092
+ }
6093
+ catch {
6094
+ return NaN;
6095
+ }
6096
+ };
6073
6097
  const handleKeyDown = (event) => {
6074
6098
  if (event.key === "Alt") {
6075
6099
  setIsFocusedAltKeyPressed(true);
@@ -6077,28 +6101,32 @@ const SpinButton = forwardRef((props, ref) => {
6077
6101
  else if (event.key === "Shift") {
6078
6102
  setIsFocusedShiftKeyPressed(true);
6079
6103
  }
6104
+ // Evaluate on Enter in keyDown (before Fluent's internal commit clears the raw text
6105
+ // and re-renders with the truncated displayValue).
6106
+ if (event.key === "Enter") {
6107
+ const currVal = evaluateExpression(event.target.value);
6108
+ if (!isNaN(currVal)) {
6109
+ setValue(currVal);
6110
+ tryCommitValue(currVal);
6111
+ }
6112
+ }
6080
6113
  HandleKeyDown(event);
6081
6114
  };
6082
6115
  const handleKeyUp = (event) => {
6083
6116
  event.stopPropagation(); // Prevent event propagation
6084
- if (event.key !== "Enter") {
6085
- if (event.key === "Alt") {
6086
- setIsFocusedAltKeyPressed(false);
6087
- }
6088
- else if (event.key === "Shift") {
6089
- setIsFocusedShiftKeyPressed(false);
6090
- }
6091
- // Allow arbitrary expressions, primarily for math operations (e.g. 10*60 for 10 minutes in seconds).
6092
- // Use Function constructor to safely evaluate the expression without allowing access to scope.
6093
- // If the expression is invalid, fallback to NaN which will be caught by validateValue and prevent committing.
6094
- const currVal = ((val) => {
6095
- try {
6096
- return Number(Function(`"use strict";return (${val})`)());
6097
- }
6098
- catch {
6099
- return NaN;
6100
- }
6101
- })(event.target.value);
6117
+ if (event.key === "Alt") {
6118
+ setIsFocusedAltKeyPressed(false);
6119
+ }
6120
+ else if (event.key === "Shift") {
6121
+ setIsFocusedShiftKeyPressed(false);
6122
+ }
6123
+ // Skip Enter — it's handled in keyDown before Fluent's internal commit
6124
+ // clears the raw text and replaces it with the truncated displayValue.
6125
+ if (event.key === "Enter") {
6126
+ return;
6127
+ }
6128
+ const currVal = evaluateExpression(event.target.value);
6129
+ if (!isNaN(currVal)) {
6102
6130
  setValue(currVal);
6103
6131
  tryCommitValue(currVal);
6104
6132
  }
@@ -6109,7 +6137,7 @@ const SpinButton = forwardRef((props, ref) => {
6109
6137
  const inputSlot = {
6110
6138
  className: mergeClasses(classes.inputSlot, props.inputClassName),
6111
6139
  };
6112
- const spinButton = (jsx(SpinButton$1, { ref: ref, ...props, appearance: "outline", input: inputSlot, step: step, id: id, size: size, precision: precision, displayValue: `${value.toFixed(precision)}${props.unit ? " " + props.unit : ""}`, value: value, onChange: handleChange, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onBlur: HandleOnBlur, className: mergedClassName }));
6140
+ const spinButton = (jsx(SpinButton$1, { ref: ref, ...props, appearance: "outline", input: inputSlot, step: step, id: id, size: size, precision: fluentPrecision, displayValue: `${value.toFixed(displayPrecision)}${props.unit ? " " + props.unit : ""}`, value: value, onChange: handleChange, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onBlur: HandleOnBlur, className: mergedClassName }));
6113
6141
  return props.infoLabel ? (jsxs("div", { className: classes.container, children: [jsx(InfoLabel, { ...props.infoLabel, htmlFor: id }), spinButton] })) : (spinButton);
6114
6142
  });
6115
6143
 
@@ -7182,7 +7210,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
7182
7210
  keywords: ["creation", "tools"],
7183
7211
  ...BabylonWebResources,
7184
7212
  author: { name: "Babylon.js", forumUserName: "" },
7185
- getExtensionModuleAsync: async () => await import('./quickCreateToolsService-BuEys230.js'),
7213
+ getExtensionModuleAsync: async () => await import('./quickCreateToolsService-C9jZpIAl.js'),
7186
7214
  },
7187
7215
  {
7188
7216
  name: "Reflector",
@@ -7190,7 +7218,7 @@ const DefaultInspectorExtensionFeed = new BuiltInsExtensionFeed("Inspector", [
7190
7218
  keywords: ["reflector", "bridge", "sync", "sandbox", "tools"],
7191
7219
  ...BabylonWebResources,
7192
7220
  author: { name: "Babylon.js", forumUserName: "" },
7193
- getExtensionModuleAsync: async () => await import('./reflectorService-Dwsb8ijf.js'),
7221
+ getExtensionModuleAsync: async () => await import('./reflectorService-DGy36OAK.js'),
7194
7222
  },
7195
7223
  ]);
7196
7224
 
@@ -8044,7 +8072,7 @@ function MakeModularTool(options) {
8044
8072
  }
8045
8073
  // Register the extension list service (for browsing/installing extensions) if extension feeds are provided.
8046
8074
  if (extensionFeeds.length > 0) {
8047
- const { ExtensionListServiceDefinition } = await import('./extensionsListService-Dj1XtRzF.js');
8075
+ const { ExtensionListServiceDefinition } = await import('./extensionsListService-qU9tqfBg.js');
8048
8076
  await serviceContainer.addServiceAsync(ExtensionListServiceDefinition);
8049
8077
  }
8050
8078
  // Register all external services (that make up a unique tool).
@@ -8245,8 +8273,7 @@ const CurveEditorProvider = (props) => {
8245
8273
  const [activeAnimations, setActiveAnimations] = useState([]);
8246
8274
  const [activeChannels, setActiveChannels] = useState({});
8247
8275
  const [activeKeyPoints, setActiveKeyPoints] = useState(null);
8248
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
8249
- const [mainKeyPoint, _setMainKeyPoint] = useState(null);
8276
+ const [mainKeyPoint, setMainKeyPoint] = useState(null);
8250
8277
  const [activeFrame, setActiveFrame] = useState(0);
8251
8278
  const [fromKey, setFromKey] = useState(0);
8252
8279
  const [toKey, setToKey] = useState(100);
@@ -8498,6 +8525,7 @@ const CurveEditorProvider = (props) => {
8498
8525
  setReferenceMaxFrame,
8499
8526
  setFocusedInput,
8500
8527
  setActiveKeyPoints,
8528
+ setMainKeyPoint,
8501
8529
  setActiveChannels,
8502
8530
  play,
8503
8531
  stop,
@@ -8616,12 +8644,16 @@ const TopBar = () => {
8616
8644
  });
8617
8645
  const onActiveKeyPointChangedObserver = observables.onActiveKeyPointChanged.add(() => {
8618
8646
  const numKeys = state.activeKeyPoints?.length || 0;
8619
- // TODO: Properly type KeyPointComponent to access curve.animation
8620
- const numAnims = numKeys;
8621
- const frameEnabled = (numKeys === 1 && numAnims === 1) || (numKeys > 1 && numAnims > 1);
8647
+ const numAnims = state.activeKeyPoints ? new Set(state.activeKeyPoints.map((kp) => kp.curve.animation.uniqueId)).size : 0;
8648
+ const hasActiveQuaternion = state.activeKeyPoints?.some((kp) => kp.curve.animation.dataType === Animation.ANIMATIONTYPE_QUATERNION) ?? false;
8649
+ const frameEnabled = ((numKeys === 1 && numAnims === 1) || (numKeys > 1 && numAnims > 1)) && !hasActiveQuaternion;
8622
8650
  setFrameControlEnabled(frameEnabled);
8623
- setValueControlEnabled(numKeys > 0);
8624
- // Don't reset values here - they are set by onFrameSet/onValueSet observers
8651
+ setValueControlEnabled(numKeys > 0 && !hasActiveQuaternion);
8652
+ // Reset values when no keys are selected
8653
+ if (numKeys === 0) {
8654
+ setKeyFrameValue(null);
8655
+ setKeyValue(null);
8656
+ }
8625
8657
  });
8626
8658
  return () => {
8627
8659
  observables.onFrameSet.remove(onFrameSetObserver);
@@ -8938,6 +8970,7 @@ const useStyles$x = makeStyles({
8938
8970
  display: "flex",
8939
8971
  flexDirection: "column",
8940
8972
  gap: tokens.spacingVerticalM,
8973
+ minWidth: "150px",
8941
8974
  },
8942
8975
  header: {
8943
8976
  display: "flex",
@@ -8973,13 +9006,16 @@ const useStyles$x = makeStyles({
8973
9006
  const ANIMATION_TYPES = ["Float", "Vector2", "Vector3", "Quaternion", "Color3", "Color4"];
8974
9007
  const LOOP_MODES = ["Cycle", "Relative", "Relative from current", "Constant"];
8975
9008
  const MODES = ["List", "Custom"];
9009
+ const ModeOptions = MODES.map((m) => ({ label: m, value: m }));
9010
+ const AnimationTypeOptions = ANIMATION_TYPES.map((t) => ({ label: t, value: t }));
9011
+ const LoopModeOptions = LOOP_MODES.map((lm) => ({ label: lm, value: lm }));
8976
9012
  /**
8977
9013
  * Panel for adding new animations
8978
9014
  * @returns The add animation panel component
8979
9015
  */
8980
9016
  const AddAnimationPanel = ({ onClose }) => {
8981
9017
  const styles = useStyles$x();
8982
- const { state, observables } = useCurveEditor();
9018
+ const { state, actions, observables } = useCurveEditor();
8983
9019
  const [name, setName] = useState("");
8984
9020
  const [mode, setMode] = useState("List");
8985
9021
  const [customProperty, setCustomProperty] = useState("");
@@ -9184,11 +9220,15 @@ const AddAnimationPanel = ({ onClose }) => {
9184
9220
  state.target.animations = [...state.target.animations, animation];
9185
9221
  }
9186
9222
  }
9187
- // Close first so AnimationList mounts, then notify
9223
+ // Auto-select the newly created animation
9224
+ actions.setActiveAnimations([animation]);
9225
+ actions.resetAllActiveChannels();
9226
+ // Close panel, then notify so listeners (e.g. AnimationList) can react
9188
9227
  onClose();
9189
9228
  observables.onAnimationsLoaded.notifyObservers();
9190
- }, [name, currentProperty, currentType, loopMode, fps, minFrame, maxFrame, state, observables, onClose]);
9191
- 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(Input, { value: name, onChange: (_, data) => setName(data.value), placeholder: "Animation name" })] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Mode" }), jsx(Dropdown$1, { value: mode, selectedOptions: [mode], onOptionSelect: (_, data) => setMode(data.optionValue), disabled: properties.length === 0, children: MODES.map((m) => (jsx(Option, { value: m, children: m }, m))) })] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Property" }), isCustomMode ? (jsx(Input, { value: customProperty, onChange: (_, data) => setCustomProperty(data.value), placeholder: "e.g., position, rotation, scaling" })) : (jsx(Dropdown$1, { value: selectedProperty, selectedOptions: [selectedProperty], onOptionSelect: (_, data) => setSelectedProperty(data.optionValue), children: properties.map((prop) => (jsx(Option, { value: prop, children: prop }, prop))) }))] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Type" }), isCustomMode ? (jsx(Dropdown$1, { value: animationType, selectedOptions: [animationType], onOptionSelect: (_, data) => setAnimationType(data.optionValue), children: ANIMATION_TYPES.map((type) => (jsx(Option, { value: type, children: type }, type))) })) : (jsx("div", { className: styles.typeDisplay, children: inferredType }))] }), jsxs("div", { className: styles.row, children: [jsx(Label, { children: "Loop Mode" }), jsx(Dropdown$1, { value: loopMode, selectedOptions: [loopMode], onOptionSelect: (_, data) => setLoopMode(data.optionValue), children: LOOP_MODES.map((lm) => (jsx(Option, { value: lm, children: lm }, lm))) })] })] }), jsxs("div", { className: styles.buttons, children: [jsx(Button, { appearance: "primary", onClick: createAnimation, disabled: !isValid, label: "Create" }), jsx(Button, { appearance: "subtle", onClick: onClose, label: "Cancel" })] })] }));
9229
+ observables.onActiveAnimationChanged.notifyObservers({});
9230
+ }, [name, currentProperty, currentType, loopMode, fps, minFrame, maxFrame, state.target, actions, observables, onClose]);
9231
+ 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" })] })] }));
9192
9232
  };
9193
9233
 
9194
9234
  const useStyles$w = makeStyles({
@@ -9616,6 +9656,7 @@ class CurveData {
9616
9656
  constructor(color, animation, property, tangentBuilder, setDefaultInTangent, setDefaultOutTangent) {
9617
9657
  this.keys = [];
9618
9658
  this.onDataUpdatedObservable = new Observable();
9659
+ this.siblings = [];
9619
9660
  this.color = color;
9620
9661
  this.animation = animation;
9621
9662
  this.property = property;
@@ -9809,6 +9850,13 @@ class CurveData {
9809
9850
  const originalKey = this.animation.getKeys()[keyId];
9810
9851
  originalKey.frame = frame;
9811
9852
  this.keys[keyId].frame = frame;
9853
+ // Sync frame to all sibling curves (same animation, different property)
9854
+ for (const sibling of this.siblings) {
9855
+ if (sibling !== this && sibling.keys[keyId]) {
9856
+ sibling.keys[keyId].frame = frame;
9857
+ sibling.onDataUpdatedObservable.notifyObservers();
9858
+ }
9859
+ }
9812
9860
  this.onDataUpdatedObservable.notifyObservers();
9813
9861
  }
9814
9862
  updateKeyValue(keyId, value) {
@@ -9833,6 +9881,8 @@ CurveData.TangentLength = 50;
9833
9881
  */
9834
9882
  const Curve = ({ curve, convertX, convertY }) => {
9835
9883
  const isQuaternion = curve.animation.dataType === Animation.ANIMATIONTYPE_QUATERNION;
9884
+ // Derive path data, recomputing whenever the curve's key data changes
9885
+ const pathData = useObservableState(useCallback(() => curve.getPathData(convertX, convertY), [curve, convertX, convertY]), curve.onDataUpdatedObservable);
9836
9886
  // Path style - same as v1
9837
9887
  const pathStyle = {
9838
9888
  stroke: curve.color,
@@ -9843,7 +9893,7 @@ const Curve = ({ curve, convertX, convertY }) => {
9843
9893
  pathStyle["strokeDasharray"] = "5";
9844
9894
  pathStyle["strokeOpacity"] = "0.5";
9845
9895
  }
9846
- return (jsx("svg", { style: { cursor: "pointer", overflow: "auto" }, children: jsx("path", { d: curve.getPathData(convertX, convertY), style: pathStyle }) }));
9896
+ return (jsx("svg", { style: { cursor: "pointer", overflow: "auto" }, children: jsx("path", { d: pathData, style: pathStyle }) }));
9847
9897
  };
9848
9898
 
9849
9899
  // Inline SVG data URIs for key point icons
@@ -9921,6 +9971,9 @@ const KeyPointComponent = (props) => {
9921
9971
  if (isSelected()) {
9922
9972
  // This keypoint is directly selected
9923
9973
  setSelectedState(SelectionState.Selected);
9974
+ // Notify frame/value observers so the top bar displays correct values (like v1's _onActiveKeyPointChanged)
9975
+ observables.onFrameSet.notifyObservers(invertX(currentXRef.current));
9976
+ observables.onValueSet.notifyObservers(invertY(currentYRef.current));
9924
9977
  }
9925
9978
  else if (state.activeKeyPoints) {
9926
9979
  // Check if a sibling (same keyId, different curve, same animation) is selected
@@ -9943,8 +9996,8 @@ const KeyPointComponent = (props) => {
9943
9996
  setSelectedState(SelectionState.None);
9944
9997
  setTangentSelectedIndex(-1);
9945
9998
  }
9946
- }, [state.activeKeyPoints, state.mainKeyPoint, curve, keyId, isSelected, curvesMatch]);
9947
- // Extract slope from a tangent vector (matches v1's _extractSlope)
9999
+ }, [state.activeKeyPoints, state.mainKeyPoint, curve, keyId, isSelected, curvesMatch, observables, invertX, invertY]);
10000
+ // Extract slope from a tangent vector
9948
10001
  const extractSlope = useCallback((vec, storedLength, isIn) => {
9949
10002
  const keys = curve.keys;
9950
10003
  const keyValue = keys[keyId].value;
@@ -9960,10 +10013,13 @@ const KeyPointComponent = (props) => {
9960
10013
  const currentPosition = vec.clone();
9961
10014
  currentPosition.normalize();
9962
10015
  currentPosition.scaleInPlace(storedLength);
9963
- const value = isIn ? keyValue - invertY(currentPosition.y + currentY) : invertY(currentPosition.y + currentY) - keyValue;
9964
- const frame = isIn ? keyFrame - invertX(currentPosition.x + currentX) : invertX(currentPosition.x + currentX) - keyFrame;
10016
+ // Use refs for current position to avoid stale closure during drag
10017
+ const cx = currentXRef.current;
10018
+ const cy = currentYRef.current;
10019
+ const value = isIn ? keyValue - invertY(currentPosition.y + cy) : invertY(currentPosition.y + cy) - keyValue;
10020
+ const frame = isIn ? keyFrame - invertX(currentPosition.x + cx) : invertX(currentPosition.x + cx) - keyFrame;
9965
10021
  return value / frame;
9966
- }, [curve, keyId, invertX, invertY, currentX, currentY]);
10022
+ }, [curve, keyId, invertX, invertY]);
9967
10023
  // Tangent operations
9968
10024
  const flattenTangent = useCallback(() => {
9969
10025
  // First update the interpolation mode to NONE without triggering observers
@@ -9982,6 +10038,8 @@ const KeyPointComponent = (props) => {
9982
10038
  curve.updateOutTangentFromControlPoint(keyId, 0);
9983
10039
  }
9984
10040
  }
10041
+ // Notify curve to re-render path
10042
+ curve.onDataUpdatedObservable.notifyObservers();
9985
10043
  setForceUpdate((v) => v + 1);
9986
10044
  }, [keyId, tangentSelectedIndex, curve]);
9987
10045
  const linearTangent = useCallback(() => {
@@ -10095,6 +10153,46 @@ const KeyPointComponent = (props) => {
10095
10153
  observables.onSelectionRectangleMoved.remove(observer);
10096
10154
  };
10097
10155
  }, [observables, curve, keyId, actions]);
10156
+ // Handle frame manually entered from top bar
10157
+ useEffect(() => {
10158
+ const observer = observables.onFrameManuallyEntered.add((newValue) => {
10159
+ if (selectedState === SelectionState.None) {
10160
+ return;
10161
+ }
10162
+ let newX = convertX(newValue);
10163
+ // Clamp to neighbors
10164
+ const previousX = getPreviousX();
10165
+ const nextX = getNextX();
10166
+ if (previousX !== null) {
10167
+ newX = Math.max(previousX, newX);
10168
+ }
10169
+ if (nextX !== null) {
10170
+ newX = Math.min(nextX, newX);
10171
+ }
10172
+ const frame = invertX(newX);
10173
+ currentXRef.current = newX;
10174
+ setCurrentX(newX);
10175
+ onFrameValueChanged(frame);
10176
+ });
10177
+ return () => {
10178
+ observables.onFrameManuallyEntered.remove(observer);
10179
+ };
10180
+ }, [observables, selectedState, convertX, invertX, getPreviousX, getNextX, onFrameValueChanged]);
10181
+ // Handle value manually entered from top bar
10182
+ useEffect(() => {
10183
+ const observer = observables.onValueManuallyEntered.add((newValue) => {
10184
+ if (selectedState !== SelectionState.Selected) {
10185
+ return;
10186
+ }
10187
+ const newY = convertY(newValue);
10188
+ currentYRef.current = newY;
10189
+ setCurrentY(newY);
10190
+ onKeyValueChanged(newValue);
10191
+ });
10192
+ return () => {
10193
+ observables.onValueManuallyEntered.remove(observer);
10194
+ };
10195
+ }, [observables, selectedState, convertY, onKeyValueChanged]);
10098
10196
  // Handle select all keys
10099
10197
  useEffect(() => {
10100
10198
  const observer = observables.onSelectAllKeys.add(() => {
@@ -10112,6 +10210,60 @@ const KeyPointComponent = (props) => {
10112
10210
  observables.onSelectAllKeys.remove(observer);
10113
10211
  };
10114
10212
  }, [observables, actions, curve, keyId]);
10213
+ // Track active key points count in a ref to avoid stale closure in handlePointerMove
10214
+ const activeKeyPointsRef = useRef(state.activeKeyPoints);
10215
+ activeKeyPointsRef.current = state.activeKeyPoints;
10216
+ // Multi-point movement: store offset from main key point
10217
+ const offsetToMain = useRef({ x: 0, y: 0 });
10218
+ const isMainKeyPoint = useRef(false);
10219
+ // When a main key point is set (multi-selection), store offset from it
10220
+ useEffect(() => {
10221
+ const observer = observables.onMainKeyPointSet.add((info) => {
10222
+ // Check if WE are the main key point
10223
+ if (info.curve === curve && info.keyId === keyId) {
10224
+ isMainKeyPoint.current = true;
10225
+ return;
10226
+ }
10227
+ isMainKeyPoint.current = false;
10228
+ // Store offset from the main key point position
10229
+ offsetToMain.current = {
10230
+ x: currentXRef.current - info.x,
10231
+ y: currentYRef.current - info.y,
10232
+ };
10233
+ });
10234
+ return () => {
10235
+ observables.onMainKeyPointSet.remove(observer);
10236
+ };
10237
+ }, [observables, curve, keyId]);
10238
+ // When the main key point moves, follow it with offset
10239
+ useEffect(() => {
10240
+ const observer = observables.onMainKeyPointMoved.add((pos) => {
10241
+ // Skip if we ARE the main key point
10242
+ if (isMainKeyPoint.current) {
10243
+ return;
10244
+ }
10245
+ if (selectedState === SelectionState.None) {
10246
+ return;
10247
+ }
10248
+ // Move frame for selected + siblings (but not first key)
10249
+ if (keyId !== 0) {
10250
+ const newX = pos.x + offsetToMain.current.x;
10251
+ currentXRef.current = newX;
10252
+ setCurrentX(newX);
10253
+ onFrameValueChanged(invertX(newX));
10254
+ }
10255
+ // Move value only for directly selected points
10256
+ if (selectedState === SelectionState.Selected) {
10257
+ const newY = pos.y + offsetToMain.current.y;
10258
+ currentYRef.current = newY;
10259
+ setCurrentY(newY);
10260
+ onKeyValueChanged(invertY(newY));
10261
+ }
10262
+ });
10263
+ return () => {
10264
+ observables.onMainKeyPointMoved.remove(observer);
10265
+ };
10266
+ }, [observables, curve, keyId, selectedState, invertX, invertY, onFrameValueChanged, onKeyValueChanged]);
10115
10267
  // Mouse/pointer handlers
10116
10268
  const handlePointerDown = useCallback((evt) => {
10117
10269
  const animationType = curve.animation.dataType;
@@ -10143,29 +10295,64 @@ const KeyPointComponent = (props) => {
10143
10295
  lockY.current = false;
10144
10296
  accumulatedX.current = 0;
10145
10297
  accumulatedY.current = 0;
10146
- // Handle selection
10298
+ // Handle selection (matches v1's _select logic)
10147
10299
  if (!evt.ctrlKey) {
10148
10300
  if (!isSelected()) {
10301
+ // Not in list, not multi-select: clear and add self
10149
10302
  actions.setActiveKeyPoints([{ curve, keyId }]);
10303
+ // Single selection → no mainKeyPoint (like v1)
10304
+ actions.setMainKeyPoint(null);
10305
+ }
10306
+ else {
10307
+ // Already in list, not multi-select:
10308
+ // If >1 selected, promote this to mainKeyPoint (DON'T clear others — v1 behavior)
10309
+ // If only 1, mainKeyPoint = null
10310
+ if (state.activeKeyPoints && state.activeKeyPoints.length > 1) {
10311
+ const info = { x: currentXRef.current, y: currentYRef.current, curve, keyId };
10312
+ actions.setMainKeyPoint({ curve, keyId });
10313
+ queueMicrotask(() => {
10314
+ observables.onMainKeyPointSet.notifyObservers(info);
10315
+ });
10316
+ }
10317
+ else {
10318
+ actions.setMainKeyPoint(null);
10319
+ }
10150
10320
  }
10151
10321
  }
10152
10322
  else {
10153
- actions.setActiveKeyPoints((prev) => {
10154
- const current = prev || [];
10155
- const matchesCurve = (kp) => kp.curve.animation.uniqueId === curve.animation.uniqueId && kp.curve.property === curve.property && kp.keyId === keyId;
10156
- const isCurrentlySelected = current.some(matchesCurve);
10157
- if (isCurrentlySelected) {
10323
+ // Ctrl-click: toggle selection
10324
+ if (isSelected()) {
10325
+ // Remove from list
10326
+ actions.setActiveKeyPoints((prev) => {
10327
+ const current = prev || [];
10328
+ const matchesCurve = (kp) => kp.curve.animation.uniqueId === curve.animation.uniqueId && kp.curve.property === curve.property && kp.keyId === keyId;
10158
10329
  return current.filter((kp) => !matchesCurve(kp));
10330
+ });
10331
+ actions.setMainKeyPoint(null);
10332
+ }
10333
+ else {
10334
+ // Add to list
10335
+ actions.setActiveKeyPoints((prev) => {
10336
+ const current = prev || [];
10337
+ return [...current, { curve, keyId }];
10338
+ });
10339
+ // Multi selection is now engaged
10340
+ if ((state.activeKeyPoints?.length ?? 0) + 1 > 1) {
10341
+ const info = { x: currentXRef.current, y: currentYRef.current, curve, keyId };
10342
+ actions.setMainKeyPoint({ curve, keyId });
10343
+ queueMicrotask(() => {
10344
+ observables.onMainKeyPointSet.notifyObservers(info);
10345
+ });
10159
10346
  }
10160
10347
  else {
10161
- return [...current, { curve, keyId }];
10348
+ actions.setMainKeyPoint(null);
10162
10349
  }
10163
- });
10350
+ }
10164
10351
  }
10165
10352
  observables.onActiveKeyPointChanged.notifyObservers();
10166
10353
  // Capture pointer for drag
10167
10354
  evt.target.setPointerCapture(evt.pointerId);
10168
- }, [curve, keyId, actions, observables, isSelected]);
10355
+ }, [curve, keyId, actions, observables, isSelected, state.activeKeyPoints]);
10169
10356
  const handlePointerMove = useCallback((evt) => {
10170
10357
  if (!pointerIsDown.current || selectedState !== SelectionState.Selected) {
10171
10358
  return;
@@ -10226,6 +10413,13 @@ const KeyPointComponent = (props) => {
10226
10413
  currentYRef.current = newY;
10227
10414
  setCurrentX(newX);
10228
10415
  setCurrentY(newY);
10416
+ // Notify other selected key points to follow (multi-point movement)
10417
+ const activeKeyPoints = activeKeyPointsRef.current;
10418
+ if (activeKeyPoints && activeKeyPoints.length > 1) {
10419
+ requestAnimationFrame(() => {
10420
+ observables.onMainKeyPointMoved.notifyObservers({ x: newX, y: newY });
10421
+ });
10422
+ }
10229
10423
  }
10230
10424
  else {
10231
10425
  // Tangent manipulation
@@ -10435,6 +10629,129 @@ const useStyles$t = makeStyles({
10435
10629
  pointerEvents: "none",
10436
10630
  },
10437
10631
  });
10632
+ /**
10633
+ * Evaluates active animations and produces CurveData instances with extracted key values.
10634
+ * @param activeAnimations - The currently active animations to evaluate
10635
+ * @param activeChannels - The currently active channels to determine which curves to show for multi-channel animations
10636
+ * @returns An array of CurveData instances representing the curves to display in the graph
10637
+ */
10638
+ function EvaluateKeys(activeAnimations, activeChannels) {
10639
+ const result = [];
10640
+ // Helper to set default tangents across all curves
10641
+ const setDefaultInTangent = (keyId) => {
10642
+ for (const curve of result) {
10643
+ curve.storeDefaultInTangent(keyId);
10644
+ }
10645
+ };
10646
+ const setDefaultOutTangent = (keyId) => {
10647
+ for (const curve of result) {
10648
+ curve.storeDefaultOutTangent(keyId);
10649
+ }
10650
+ };
10651
+ for (const animation of activeAnimations) {
10652
+ const keys = animation.getKeys();
10653
+ if (keys.length === 0) {
10654
+ continue;
10655
+ }
10656
+ const channelColor = activeChannels[animation.uniqueId];
10657
+ const curvesToAdd = [];
10658
+ // Create curves based on data type
10659
+ switch (animation.dataType) {
10660
+ case Animation.ANIMATIONTYPE_FLOAT:
10661
+ curvesToAdd.push(new CurveData(channelColor || DefaultCurveColor, animation));
10662
+ break;
10663
+ case Animation.ANIMATIONTYPE_VECTOR2:
10664
+ if (!channelColor || channelColor === ChannelColors.X) {
10665
+ curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => Vector2.Zero(), setDefaultInTangent, setDefaultOutTangent));
10666
+ }
10667
+ if (!channelColor || channelColor === ChannelColors.Y) {
10668
+ curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => Vector2.Zero(), setDefaultInTangent, setDefaultOutTangent));
10669
+ }
10670
+ break;
10671
+ case Animation.ANIMATIONTYPE_VECTOR3:
10672
+ if (!channelColor || channelColor === ChannelColors.X) {
10673
+ curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10674
+ }
10675
+ if (!channelColor || channelColor === ChannelColors.Y) {
10676
+ curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10677
+ }
10678
+ if (!channelColor || channelColor === ChannelColors.Z) {
10679
+ curvesToAdd.push(new CurveData(ChannelColors.Z, animation, "z", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10680
+ }
10681
+ break;
10682
+ case Animation.ANIMATIONTYPE_COLOR3:
10683
+ if (!channelColor || channelColor === ColorChannelColors.R) {
10684
+ curvesToAdd.push(new CurveData(ColorChannelColors.R, animation, "r", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10685
+ }
10686
+ if (!channelColor || channelColor === ColorChannelColors.G) {
10687
+ curvesToAdd.push(new CurveData(ColorChannelColors.G, animation, "g", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10688
+ }
10689
+ if (!channelColor || channelColor === ColorChannelColors.B) {
10690
+ curvesToAdd.push(new CurveData(ColorChannelColors.B, animation, "b", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10691
+ }
10692
+ break;
10693
+ case Animation.ANIMATIONTYPE_COLOR4:
10694
+ if (!channelColor || channelColor === ColorChannelColors.R) {
10695
+ curvesToAdd.push(new CurveData(ColorChannelColors.R, animation, "r", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10696
+ }
10697
+ if (!channelColor || channelColor === ColorChannelColors.G) {
10698
+ curvesToAdd.push(new CurveData(ColorChannelColors.G, animation, "g", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10699
+ }
10700
+ if (!channelColor || channelColor === ColorChannelColors.B) {
10701
+ curvesToAdd.push(new CurveData(ColorChannelColors.B, animation, "b", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10702
+ }
10703
+ if (!channelColor || channelColor === ColorChannelColors.A) {
10704
+ curvesToAdd.push(new CurveData(ColorChannelColors.A, animation, "a", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10705
+ }
10706
+ break;
10707
+ case Animation.ANIMATIONTYPE_QUATERNION:
10708
+ if (!channelColor || channelColor === ChannelColors.X) {
10709
+ curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10710
+ }
10711
+ if (!channelColor || channelColor === ChannelColors.Y) {
10712
+ curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10713
+ }
10714
+ if (!channelColor || channelColor === ChannelColors.Z) {
10715
+ curvesToAdd.push(new CurveData(ChannelColors.Z, animation, "z", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10716
+ }
10717
+ if (!channelColor || channelColor === ChannelColors.W) {
10718
+ curvesToAdd.push(new CurveData(ChannelColors.W, animation, "w", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10719
+ }
10720
+ break;
10721
+ }
10722
+ ExtractValuesFromKeys(keys, curvesToAdd);
10723
+ // Wire up siblings so frame changes sync across all curves for the same animation
10724
+ for (const curve of curvesToAdd) {
10725
+ curve.siblings = curvesToAdd;
10726
+ }
10727
+ result.push(...curvesToAdd);
10728
+ }
10729
+ return result;
10730
+ }
10731
+ /**
10732
+ * Extracts key values, tangents, and interpolation data from animation keys into CurveData instances.
10733
+ * @param keys - The animation keys to extract data from
10734
+ * @param curves - CurveData to push changes to
10735
+ */
10736
+ function ExtractValuesFromKeys(keys, curves) {
10737
+ for (const key of keys) {
10738
+ const lockedTangent = key.lockedTangent ?? true;
10739
+ for (const curve of curves) {
10740
+ const prop = curve.property;
10741
+ const value = prop ? key.value[prop] : key.value;
10742
+ const inTangent = prop ? key.inTangent?.[prop] : key.inTangent;
10743
+ const outTangent = prop ? key.outTangent?.[prop] : key.outTangent;
10744
+ curve.keys.push({
10745
+ frame: key.frame,
10746
+ value,
10747
+ inTangent,
10748
+ outTangent,
10749
+ lockedTangent,
10750
+ interpolation: key.interpolation,
10751
+ });
10752
+ }
10753
+ }
10754
+ }
10438
10755
  /**
10439
10756
  * Main graph area for displaying and editing animation curves
10440
10757
  * @returns The graph component
@@ -10498,6 +10815,33 @@ const Graph = ({ width, height }) => {
10498
10815
  value: value,
10499
10816
  lockedTangent: true,
10500
10817
  };
10818
+ // Compute Hermite 1st-derivative tangents so the curve shape is preserved (like v1)
10819
+ if (leftKey?.outTangent !== undefined && rightKey?.inTangent !== undefined) {
10820
+ const invFrameDelta = 1.0 / (rightKey.frame - leftKey.frame);
10821
+ const cutTime = (currentFrame - leftKey.frame) * invFrameDelta;
10822
+ let derivative = null;
10823
+ switch (currentAnimation.dataType) {
10824
+ case Animation.ANIMATIONTYPE_FLOAT:
10825
+ derivative = Scalar.Hermite1stDerivative(leftKey.value * invFrameDelta, leftKey.outTangent, rightKey.value * invFrameDelta, rightKey.inTangent, cutTime);
10826
+ break;
10827
+ case Animation.ANIMATIONTYPE_VECTOR2:
10828
+ derivative = Vector2.Hermite1stDerivative(leftKey.value.scale(invFrameDelta), leftKey.outTangent, rightKey.value.scale(invFrameDelta), rightKey.inTangent, cutTime);
10829
+ break;
10830
+ case Animation.ANIMATIONTYPE_VECTOR3:
10831
+ derivative = Vector3.Hermite1stDerivative(leftKey.value.scale(invFrameDelta), leftKey.outTangent, rightKey.value.scale(invFrameDelta), rightKey.inTangent, cutTime);
10832
+ break;
10833
+ case Animation.ANIMATIONTYPE_COLOR3:
10834
+ derivative = Color3.Hermite1stDerivative(leftKey.value.scale(invFrameDelta), leftKey.outTangent, rightKey.value.scale(invFrameDelta), rightKey.inTangent, cutTime);
10835
+ break;
10836
+ case Animation.ANIMATIONTYPE_COLOR4:
10837
+ derivative = Color4.Hermite1stDerivative(leftKey.value.scale(invFrameDelta), leftKey.outTangent, rightKey.value.scale(invFrameDelta), rightKey.inTangent, cutTime);
10838
+ break;
10839
+ }
10840
+ if (derivative !== null) {
10841
+ newKey.inTangent = derivative;
10842
+ newKey.outTangent = derivative.clone ? derivative.clone() : derivative;
10843
+ }
10844
+ }
10501
10845
  keys.splice(indexToAdd + 1, 0, newKey);
10502
10846
  }
10503
10847
  currentAnimation.setKeys(keys);
@@ -10532,6 +10876,9 @@ const Graph = ({ width, height }) => {
10532
10876
  const keys = animation.getKeys();
10533
10877
  const sortedIndices = Array.from(keyIndices).sort((a, b) => b - a); // Sort descending
10534
10878
  for (const index of sortedIndices) {
10879
+ if (index === 0 || index === keys.length - 1) {
10880
+ continue; // Cannot delete first or last key
10881
+ }
10535
10882
  if (index >= 0 && index < keys.length) {
10536
10883
  keys.splice(index, 1);
10537
10884
  }
@@ -10544,119 +10891,31 @@ const Graph = ({ width, height }) => {
10544
10891
  observables.onActiveAnimationChanged.notifyObservers({});
10545
10892
  });
10546
10893
  // Note: Tangent operations (flatten, linear, break, unify, step) are handled by KeyPointComponent
10547
- // Each selected keypoint subscribes to the observables and handles its own tangent updates (like v1)
10894
+ // Each selected keypoint subscribes to the observables and handles its own tangent updates
10548
10895
  return () => {
10549
10896
  observables.onCreateOrUpdateKeyPointRequired.remove(onCreateOrUpdateKeyPointRequired);
10550
10897
  observables.onFrameRequired.remove(onFrameRequired);
10551
10898
  observables.onDeleteKeyActiveKeyPoints.remove(onDeleteKeyActiveKeyPoints);
10552
10899
  };
10553
10900
  }, [observables, state.activeAnimations, state.activeFrame, state.activeKeyPoints, actions]);
10554
- const curves = useMemo(() => {
10555
- const result = [];
10556
- // Helper to set default tangents across all curves (like v1)
10557
- const setDefaultInTangent = (keyId) => {
10558
- for (const curve of result) {
10559
- curve.storeDefaultInTangent(keyId);
10560
- }
10901
+ // Invalidation counter incremented when keys are added/deleted so curves recompute
10902
+ const [curveVersion, invalidateCurves] = useReducer((c) => c + 1, 0);
10903
+ useEffect(() => {
10904
+ const observer = observables.onActiveAnimationChanged.add(() => invalidateCurves());
10905
+ return () => {
10906
+ observables.onActiveAnimationChanged.remove(observer);
10561
10907
  };
10562
- const setDefaultOutTangent = (keyId) => {
10563
- for (const curve of result) {
10564
- curve.storeDefaultOutTangent(keyId);
10565
- }
10908
+ }, [observables]);
10909
+ const curves = useMemo(() => EvaluateKeys(state.activeAnimations, state.activeChannels), [state.activeAnimations, state.activeChannels, curveVersion]);
10910
+ // Re-render when any curve's key data is mutated (e.g. sibling frame sync)
10911
+ // so key point positions stay in sync with their curve paths
10912
+ const [, invalidateKeyPoints] = useReducer((c) => c + 1, 0);
10913
+ useEffect(() => {
10914
+ const observers = curves.map((curve) => curve.onDataUpdatedObservable.add(invalidateKeyPoints));
10915
+ return () => {
10916
+ curves.forEach((curve, i) => curve.onDataUpdatedObservable.remove(observers[i]));
10566
10917
  };
10567
- for (const animation of state.activeAnimations) {
10568
- const keys = animation.getKeys();
10569
- if (keys.length === 0) {
10570
- continue;
10571
- }
10572
- const channelColor = state.activeChannels[animation.uniqueId];
10573
- const curvesToAdd = [];
10574
- // Create curves based on data type (like v1's _evaluateKeys)
10575
- switch (animation.dataType) {
10576
- case Animation.ANIMATIONTYPE_FLOAT:
10577
- curvesToAdd.push(new CurveData(channelColor || DefaultCurveColor, animation));
10578
- break;
10579
- case Animation.ANIMATIONTYPE_VECTOR2:
10580
- if (!channelColor || channelColor === ChannelColors.X) {
10581
- curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => Vector2.Zero(), setDefaultInTangent, setDefaultOutTangent));
10582
- }
10583
- if (!channelColor || channelColor === ChannelColors.Y) {
10584
- curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => Vector2.Zero(), setDefaultInTangent, setDefaultOutTangent));
10585
- }
10586
- break;
10587
- case Animation.ANIMATIONTYPE_VECTOR3:
10588
- if (!channelColor || channelColor === ChannelColors.X) {
10589
- curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10590
- }
10591
- if (!channelColor || channelColor === ChannelColors.Y) {
10592
- curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10593
- }
10594
- if (!channelColor || channelColor === ChannelColors.Z) {
10595
- curvesToAdd.push(new CurveData(ChannelColors.Z, animation, "z", () => Vector3.Zero(), setDefaultInTangent, setDefaultOutTangent));
10596
- }
10597
- break;
10598
- case Animation.ANIMATIONTYPE_COLOR3:
10599
- if (!channelColor || channelColor === ColorChannelColors.R) {
10600
- curvesToAdd.push(new CurveData(ColorChannelColors.R, animation, "r", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10601
- }
10602
- if (!channelColor || channelColor === ColorChannelColors.G) {
10603
- curvesToAdd.push(new CurveData(ColorChannelColors.G, animation, "g", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10604
- }
10605
- if (!channelColor || channelColor === ColorChannelColors.B) {
10606
- curvesToAdd.push(new CurveData(ColorChannelColors.B, animation, "b", () => Color3.Black(), setDefaultInTangent, setDefaultOutTangent));
10607
- }
10608
- break;
10609
- case Animation.ANIMATIONTYPE_COLOR4:
10610
- if (!channelColor || channelColor === ColorChannelColors.R) {
10611
- curvesToAdd.push(new CurveData(ColorChannelColors.R, animation, "r", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10612
- }
10613
- if (!channelColor || channelColor === ColorChannelColors.G) {
10614
- curvesToAdd.push(new CurveData(ColorChannelColors.G, animation, "g", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10615
- }
10616
- if (!channelColor || channelColor === ColorChannelColors.B) {
10617
- curvesToAdd.push(new CurveData(ColorChannelColors.B, animation, "b", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10618
- }
10619
- if (!channelColor || channelColor === ColorChannelColors.A) {
10620
- curvesToAdd.push(new CurveData(ColorChannelColors.A, animation, "a", () => new Color4(), setDefaultInTangent, setDefaultOutTangent));
10621
- }
10622
- break;
10623
- case Animation.ANIMATIONTYPE_QUATERNION:
10624
- if (!channelColor || channelColor === ChannelColors.X) {
10625
- curvesToAdd.push(new CurveData(ChannelColors.X, animation, "x", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10626
- }
10627
- if (!channelColor || channelColor === ChannelColors.Y) {
10628
- curvesToAdd.push(new CurveData(ChannelColors.Y, animation, "y", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10629
- }
10630
- if (!channelColor || channelColor === ChannelColors.Z) {
10631
- curvesToAdd.push(new CurveData(ChannelColors.Z, animation, "z", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10632
- }
10633
- if (!channelColor || channelColor === ChannelColors.W) {
10634
- curvesToAdd.push(new CurveData(ChannelColors.W, animation, "w", () => new Quaternion(), setDefaultInTangent, setDefaultOutTangent));
10635
- }
10636
- break;
10637
- }
10638
- // Populate keys for each curve (like v1's _extractValuesFromKeys)
10639
- for (const key of keys) {
10640
- const lockedTangent = key.lockedTangent ?? true;
10641
- for (const curve of curvesToAdd) {
10642
- const prop = curve.property;
10643
- const value = prop ? key.value[prop] : key.value;
10644
- const inTangent = prop ? key.inTangent?.[prop] : key.inTangent;
10645
- const outTangent = prop ? key.outTangent?.[prop] : key.outTangent;
10646
- curve.keys.push({
10647
- frame: key.frame,
10648
- value,
10649
- inTangent,
10650
- outTangent,
10651
- lockedTangent,
10652
- interpolation: key.interpolation,
10653
- });
10654
- }
10655
- }
10656
- result.push(...curvesToAdd);
10657
- }
10658
- return result;
10659
- }, [state.activeAnimations, state.activeChannels]);
10918
+ }, [curves, invalidateKeyPoints]);
10660
10919
  // Calculate value range
10661
10920
  const valueRange = useMemo(() => {
10662
10921
  let minValue = 0;
@@ -11153,7 +11412,8 @@ const useStyles$q = makeStyles({
11153
11412
  backgroundColor: tokens.colorNeutralBackground2,
11154
11413
  overflow: "hidden",
11155
11414
  userSelect: "none",
11156
- pointerEvents: "none",
11415
+ cursor: "pointer",
11416
+ touchAction: "none",
11157
11417
  },
11158
11418
  svg: {
11159
11419
  width: "100%",
@@ -11186,10 +11446,11 @@ const OFFSET_X = 10;
11186
11446
  */
11187
11447
  const RangeFrameBar = ({ width }) => {
11188
11448
  const styles = useStyles$q();
11189
- const { state, observables } = useCurveEditor();
11449
+ const { state, actions, observables } = useCurveEditor();
11190
11450
  const svgRef = useRef(null);
11191
11451
  const [viewWidth, setViewWidth] = useState(width);
11192
11452
  const [displayFrame, setDisplayFrame] = useState(state.activeFrame);
11453
+ const pointerIsDown = useRef(false);
11193
11454
  // Re-render when range updates
11194
11455
  // useCallback stabilizes the accessor to prevent infinite re-render loops
11195
11456
  useObservableState(useCallback(() => ({}), []), observables.onRangeUpdated);
@@ -11215,6 +11476,26 @@ const RangeFrameBar = ({ width }) => {
11215
11476
  setDisplayFrame(state.activeFrame);
11216
11477
  }
11217
11478
  }, [state.activeFrame, state.isPlaying]);
11479
+ // Playhead scrubbing — linear interpolation from pointer position to frame
11480
+ const pointerToFrame = useCallback((offsetX) => {
11481
+ const { fromKey, toKey } = state;
11482
+ return Math.round(Math.max(fromKey, Math.min(toKey, (offsetX / viewWidth) * (toKey - fromKey) + fromKey)));
11483
+ }, [state.fromKey, state.toKey, viewWidth]);
11484
+ const handlePointerDown = useCallback((evt) => {
11485
+ pointerIsDown.current = true;
11486
+ evt.currentTarget.setPointerCapture(evt.pointerId);
11487
+ actions.moveToFrame(pointerToFrame(evt.nativeEvent.offsetX));
11488
+ }, [actions, pointerToFrame]);
11489
+ const handlePointerMove = useCallback((evt) => {
11490
+ if (!pointerIsDown.current) {
11491
+ return;
11492
+ }
11493
+ actions.moveToFrame(pointerToFrame(evt.nativeEvent.offsetX));
11494
+ }, [actions, pointerToFrame]);
11495
+ const handlePointerUp = useCallback((evt) => {
11496
+ pointerIsDown.current = false;
11497
+ evt.currentTarget.releasePointerCapture(evt.pointerId);
11498
+ }, []);
11218
11499
  // Compute frame ticks
11219
11500
  const frameTicks = useMemo(() => {
11220
11501
  if (state.activeAnimations.length === 0) {
@@ -11269,7 +11550,7 @@ const RangeFrameBar = ({ width }) => {
11269
11550
  return jsx("line", { className: styles.activeFrameLine, x1: x, y1: 0, x2: x, y2: 40 });
11270
11551
  }, [displayFrame, frameToX, styles.activeFrameLine]);
11271
11552
  const viewBox = `${ -10} 0 ${viewWidth + OFFSET_X * 4} 40`;
11272
- return (jsx("div", { className: styles.root, children: jsxs("svg", { ref: svgRef, className: styles.svg, viewBox: viewBox, children: [frameTicks.map((frame, i) => {
11553
+ return (jsx("div", { className: styles.root, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, children: jsxs("svg", { ref: svgRef, className: styles.svg, viewBox: viewBox, children: [frameTicks.map((frame, i) => {
11273
11554
  const x = frameToX(frame);
11274
11555
  return (jsxs("g", { children: [jsx("line", { className: styles.tickLine, x1: x, y1: 22, x2: x, y2: 40 }), jsx("text", { className: styles.tickLabel, x: x, y: 14, children: Math.round(frame) })] }, `tick-${frame}-${i}`));
11275
11556
  }), renderKeyframes, renderActiveFrame] }) }));
@@ -11528,6 +11809,22 @@ const RangeSelector = () => {
11528
11809
  }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, children: [jsx("div", { id: "left-handle", className: styles.handle, children: jsxs("div", { className: styles.handleIcon, children: [jsx("div", {}), jsx("div", {}), jsx("div", {})] }) }), jsx("div", { className: styles.label, children: Math.floor(state.fromKey) }), jsx("div", { className: styles.label, children: Math.floor(state.toKey) }), jsx("div", { id: "right-handle", className: styles.handle, children: jsxs("div", { className: styles.handleIcon, children: [jsx("div", {}), jsx("div", {}), jsx("div", {})] }) })] }) }));
11529
11810
  };
11530
11811
 
11812
+ /**
11813
+ * Checks whether any of the given animations has a key at the specified frame.
11814
+ * @param animations - The animations to check for keys
11815
+ * @param frame - The frame index to check for keys at
11816
+ * @returns True if a key exists at the frame, false otherwise.
11817
+ */
11818
+ function GetKeyAtAnyFrameIndex(animations, frame) {
11819
+ for (const animation of animations) {
11820
+ for (const key of animation.getKeys()) {
11821
+ if (Math.floor(frame - key.frame) === 0) {
11822
+ return true;
11823
+ }
11824
+ }
11825
+ }
11826
+ return false;
11827
+ }
11531
11828
  const useStyles$n = makeStyles({
11532
11829
  root: {
11533
11830
  display: "flex",
@@ -11621,21 +11918,34 @@ const BottomBar = () => {
11621
11918
  setClipLength(newLength);
11622
11919
  actions.setClipLength(newLength);
11623
11920
  actions.setReferenceMaxFrame(newLength);
11921
+ // Move playhead to new clip end
11922
+ observables.onMoveToFrameRequired.notifyObservers(newLength);
11923
+ // Create a key at the boundary if one doesn't exist
11924
+ if (!GetKeyAtAnyFrameIndex(state.activeAnimations, newLength)) {
11925
+ observables.onCreateOrUpdateKeyPointRequired.notifyObservers();
11926
+ }
11624
11927
  });
11625
11928
  const onClipLengthDecreased = observables.onClipLengthDecreased.add((newLength) => {
11626
11929
  setClipLength(newLength);
11627
11930
  actions.setClipLength(newLength);
11628
11931
  actions.setReferenceMaxFrame(newLength);
11932
+ // Move playhead to new clip end
11933
+ observables.onMoveToFrameRequired.notifyObservers(newLength);
11934
+ // Create a key at the boundary if one doesn't exist
11935
+ if (!GetKeyAtAnyFrameIndex(state.activeAnimations, newLength)) {
11936
+ observables.onCreateOrUpdateKeyPointRequired.notifyObservers();
11937
+ }
11629
11938
  // Clamp toKey to new clip length
11630
11939
  if (toKeyRef.current > newLength) {
11631
11940
  actions.setToKey(newLength);
11632
11941
  }
11942
+ observables.onRangeUpdated.notifyObservers();
11633
11943
  });
11634
11944
  return () => {
11635
11945
  observables.onClipLengthIncreased.remove(onClipLengthIncreased);
11636
11946
  observables.onClipLengthDecreased.remove(onClipLengthDecreased);
11637
11947
  };
11638
- }, [observables, actions]);
11948
+ }, [observables, actions, state.activeAnimations]);
11639
11949
  const handlePlayForward = useCallback(() => {
11640
11950
  actions.play(true);
11641
11951
  }, [actions]);
@@ -21537,7 +21847,7 @@ function ConvertOptions(v1Options) {
21537
21847
  consumes: [SceneExplorerServiceIdentity],
21538
21848
  factory: (sceneExplorerService) => {
21539
21849
  const sceneExplorerCommandRegistrations = explorerExtensibility.flatMap((command) => command.entries.map((entry) => sceneExplorerService.addEntityCommand({
21540
- predicate: (entity) => command.predicate(entity),
21850
+ predicate: (entity) => typeof entity === "object" && command.predicate(entity),
21541
21851
  getCommand: (entity) => {
21542
21852
  return {
21543
21853
  displayName: entry.label,
@@ -22097,4 +22407,4 @@ const TextAreaPropertyLine = (props) => {
22097
22407
  AttachDebugLayer();
22098
22408
 
22099
22409
  export { useEventfulState as $, Accordion as A, Button as B, CheckboxPropertyLine as C, DebugServiceIdentity as D, Property as E, LinkToEntityPropertyLine as F, GizmoServiceIdentity as G, ErrorBoundary as H, Inspector as I, ExtensibleAccordion as J, Theme as K, LinkToEntity as L, MessageBar as M, NumberInputPropertyLine as N, PropertyContext as O, Popover as P, usePropertyChangedNotifier as Q, BuiltInsExtensionFeed as R, SpinButtonPropertyLine as S, TextInputPropertyLine as T, useVector3Property as U, Vector3PropertyLine as V, useColor3Property as W, useColor4Property as X, useQuaternionProperty as Y, MakePropertyHook as Z, useInterceptObservable as _, useProperty as a, Pane as a$, useObservableCollection as a0, useOrderedObservableCollection as a1, usePollingObservable as a2, useResource as a3, useAsyncResource as a4, useSetting as a5, useAngleConverters as a6, MakeTeachingMoment as a7, MakeDialogTeachingMoment as a8, useThemeMode as a9, Color3GradientComponent as aA, Color4GradientComponent as aB, ColorStepGradientComponent as aC, InfoLabel as aD, MakeLazyComponent as aE, List as aF, MaterialSelector as aG, NodeSelector as aH, PositionedPopover as aI, SearchBar as aJ, SearchBox as aK, SkeletonSelector as aL, SpinButton as aM, Switch as aN, SyncedSliderInput as aO, Textarea as aP, TextInput as aQ, TextureSelector as aR, ToastProvider as aS, ToggleButton as aT, Tooltip as aU, UploadButton as aV, ChildWindow as aW, FileUploadLine as aX, FactorGradientList as aY, Color3GradientList as aZ, Color4GradientList as a_, useTheme as aa, InterceptFunction as ab, GetPropertyDescriptor as ac, IsPropertyReadonly as ad, InterceptProperty as ae, ObservableCollection as af, ConstructorFactory as ag, SelectionServiceDefinition as ah, SettingsStore as ai, ShowInspector as aj, useKeyListener as ak, useKeyState as al, useEventListener as am, AccordionSectionItem as an, Checkbox as ao, Collapse as ap, ColorPickerPopup as aq, InputHexField as ar, InputHsvField as as, ComboBox as at, DraggableLine as au, Dropdown as av, NumberDropdown as aw, StringDropdown as ax, EntitySelector as ay, FactorGradientComponent as az, ShellServiceIdentity as b, TextureUpload as b0, BooleanBadgePropertyLine as b1, Color3PropertyLine as b2, Color4PropertyLine as b3, ComboBoxPropertyLine as b4, HexPropertyLine as b5, LinkPropertyLine as b6, PropertyLine as b7, LineContainer as b8, PlaceholderPropertyLine as b9, StringifiedPropertyLine as ba, SwitchPropertyLine as bb, SyncedSliderPropertyLine as bc, TextAreaPropertyLine as bd, TextPropertyLine as be, RotationVectorPropertyLine as bf, QuaternionPropertyLine as bg, Vector2PropertyLine as bh, Vector4PropertyLine as bi, SceneContextIdentity as c, SelectionServiceIdentity as d, useObservableState as e, AccordionSection as f, ButtonLine as g, ToolsServiceIdentity as h, useExtensionManager as i, MakePopoverTeachingMoment as j, TeachingMoment as k, Link as l, SidePaneContainer as m, PropertiesServiceIdentity as n, SceneExplorerServiceIdentity as o, SettingsServiceIdentity as p, StatsServiceIdentity as q, ThemeServiceIdentity as r, SettingsStoreIdentity as s, ConvertOptions as t, useToast as u, AttachDebugLayer as v, DetachDebugLayer as w, NumberDropdownPropertyLine as x, StringDropdownPropertyLine as y, BoundProperty as z };
22100
- //# sourceMappingURL=index-DrQrky1o.js.map
22410
+ //# sourceMappingURL=index-Cmd13UXt.js.map