@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.
- package/lib/{extensionsListService-Dj1XtRzF.js → extensionsListService-qU9tqfBg.js} +2 -2
- package/lib/{extensionsListService-Dj1XtRzF.js.map → extensionsListService-qU9tqfBg.js.map} +1 -1
- package/lib/{index-DrQrky1o.js → index-Cmd13UXt.js} +498 -188
- package/lib/index-Cmd13UXt.js.map +1 -0
- package/lib/index.d.ts +28 -4
- package/lib/index.js +1 -1
- package/lib/{quickCreateToolsService-BuEys230.js → quickCreateToolsService-C9jZpIAl.js} +2 -2
- package/lib/{quickCreateToolsService-BuEys230.js.map → quickCreateToolsService-C9jZpIAl.js.map} +1 -1
- package/lib/{reflectorService-Dwsb8ijf.js → reflectorService-DGy36OAK.js} +2 -2
- package/lib/{reflectorService-Dwsb8ijf.js.map → reflectorService-DGy36OAK.js.map} +1 -1
- package/package.json +1 -1
- package/lib/index-DrQrky1o.js.map +0 -1
|
@@ -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,
|
|
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((
|
|
1664
|
-
const enabled = !
|
|
1663
|
+
setChecked((prevChecked) => {
|
|
1664
|
+
const enabled = !prevChecked;
|
|
1665
1665
|
onChange(enabled);
|
|
1666
1666
|
return enabled;
|
|
1667
1667
|
});
|
|
1668
|
-
}, [
|
|
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
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
8620
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
9191
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
9964
|
-
const
|
|
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
|
|
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
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10557
|
-
const
|
|
10558
|
-
|
|
10559
|
-
|
|
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
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
22410
|
+
//# sourceMappingURL=index-Cmd13UXt.js.map
|