Skip to content

Commit 4e5b23f

Browse files
committed
feat: rework support of animation groups in scene inspector
1 parent fd000de commit 4e5b23f

File tree

7 files changed

+237
-93
lines changed

7 files changed

+237
-93
lines changed

editor/src/editor/layout/inspector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { isNodeLocked } from "../../tools/node/metadata";
1818
import { setInspectorSearch } from "./inspector/fields/field";
1919
import { IEditorInspectorImplementationProps } from "./inspector/inspector";
2020

21-
import { EditorSceneInspector } from "./inspector/scene";
21+
import { EditorSceneInspector } from "./inspector/scene/scene";
2222

2323
import { EditorMeshInspector } from "./inspector/mesh/mesh";
2424
import { EditorTransformNodeInspector } from "./inspector/transform";

editor/src/editor/layout/inspector/fields/asset.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export function EditorInspectorAssetField(props: IEditorInspectorAssetFieldProps
7070
return showAlert("Can't assign asset", "Only Node Particle System Set files (.npss) are supported.");
7171
}
7272

73+
if (props.assetType === "scene" && extension !== ".scene") {
74+
return showAlert("Can't assign asset", "Only Scene files (.scene) are supported.");
75+
}
76+
7377
if (props.assetType === "material") {
7478
if (extension !== ".material") {
7579
return showAlert("Can't assign asset", "Only Material files (.material) are supported.");
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Reorder } from "framer-motion";
2+
3+
import { MouseEvent, useEffect, useState } from "react";
4+
5+
import { AiOutlineMinus } from "react-icons/ai";
6+
import { IoPlay, IoStop } from "react-icons/io5";
7+
8+
import { Scene, AnimationGroup } from "babylonjs";
9+
10+
import { Editor } from "../../../main";
11+
12+
import { Button } from "../../../../ui/shadcn/ui/button";
13+
14+
import { registerUndoRedo } from "../../../../tools/undoredo";
15+
16+
import { EditorInspectorSectionField } from "../fields/section";
17+
18+
export interface IEditorSceneAnimationGroupsInspectorProps {
19+
object: Scene;
20+
editor: Editor;
21+
}
22+
23+
export function EditorSceneAnimationGroupsInspector(props: IEditorSceneAnimationGroupsInspectorProps) {
24+
const [animationGroupsSearch, setAnimationGroupsSearch] = useState<string>("");
25+
const [selectedAnimationGroups, setSelectedAnimationGroups] = useState<AnimationGroup[]>([]);
26+
27+
const [animationGroups, setAnimationGroups] = useState<AnimationGroup[]>([]);
28+
const [playingAnimationGroups, setPlayingAnimationGroups] = useState<AnimationGroup[]>([]);
29+
30+
useEffect(() => {
31+
setAnimationGroups(props.object.animationGroups);
32+
setPlayingAnimationGroups(props.object.animationGroups.filter((animationGroup) => animationGroup.isPlaying));
33+
}, [props.object]);
34+
35+
function handleAnimationGroupClick(ev: MouseEvent<HTMLDivElement>, animationGroup: AnimationGroup): void {
36+
if (ev.ctrlKey || ev.metaKey) {
37+
const newSelectedAnimationGroups = selectedAnimationGroups.slice();
38+
if (newSelectedAnimationGroups.includes(animationGroup)) {
39+
const index = newSelectedAnimationGroups.indexOf(animationGroup);
40+
if (index !== -1) {
41+
newSelectedAnimationGroups.splice(index, 1);
42+
}
43+
} else {
44+
newSelectedAnimationGroups.push(animationGroup);
45+
}
46+
47+
setSelectedAnimationGroups(newSelectedAnimationGroups);
48+
} else if (ev.shiftKey) {
49+
const newSelectedAnimationGroups = selectedAnimationGroups.slice();
50+
const lastSelectedAnimationGroup = newSelectedAnimationGroups[newSelectedAnimationGroups.length - 1];
51+
if (!lastSelectedAnimationGroup) {
52+
return setSelectedAnimationGroups([animationGroup]);
53+
}
54+
55+
const lastIndex = props.object.animationGroups.indexOf(lastSelectedAnimationGroup);
56+
const currentIndex = props.object.animationGroups.indexOf(animationGroup);
57+
58+
const [start, end] = lastIndex < currentIndex ? [lastIndex, currentIndex] : [currentIndex, lastIndex];
59+
60+
for (let i = start; i <= end; i++) {
61+
const ag = props.object.animationGroups[i];
62+
if (!newSelectedAnimationGroups.includes(ag)) {
63+
newSelectedAnimationGroups.push(ag);
64+
}
65+
}
66+
67+
setSelectedAnimationGroups(newSelectedAnimationGroups);
68+
} else {
69+
setSelectedAnimationGroups([animationGroup]);
70+
}
71+
}
72+
73+
function handlePlayOrStopAnimationGroup(animationGroup: AnimationGroup): void {
74+
if (animationGroup.isPlaying) {
75+
animationGroup.stop();
76+
setPlayingAnimationGroups(playingAnimationGroups.filter((ag) => ag !== animationGroup));
77+
} else {
78+
animationGroup.play(true);
79+
setPlayingAnimationGroups([...playingAnimationGroups, animationGroup]);
80+
}
81+
}
82+
83+
function handlePlaySelectedAnimationGroups(): void {
84+
props.object.animationGroups.forEach((animationGroup) => {
85+
animationGroup.stop();
86+
});
87+
88+
selectedAnimationGroups.forEach((animationGroup) => {
89+
animationGroup.play(true);
90+
});
91+
92+
setPlayingAnimationGroups(props.object.animationGroups.filter((animationGroup) => animationGroup.isPlaying));
93+
}
94+
95+
function handleRemoveSelectedAnimationGroups(): void {
96+
registerUndoRedo({
97+
executeRedo: true,
98+
undo: () => {
99+
selectedAnimationGroups.forEach((animationGroup) => {
100+
props.object.addAnimationGroup(animationGroup);
101+
});
102+
},
103+
redo: () => {
104+
selectedAnimationGroups.forEach((animationGroup) => {
105+
props.object.removeAnimationGroup(animationGroup);
106+
});
107+
},
108+
});
109+
110+
setAnimationGroups(props.object.animationGroups.slice());
111+
}
112+
113+
const hasAnimations = animationGroups.length > 0;
114+
const animations = animationGroups.filter((animationGroup) => animationGroup.name.toLowerCase().includes(animationGroupsSearch.toLowerCase()));
115+
116+
return (
117+
<EditorInspectorSectionField title="Animation Groups">
118+
{!hasAnimations && <div className="text-center text-xl">No animation groups</div>}
119+
120+
{hasAnimations && (
121+
<>
122+
<input
123+
type="text"
124+
placeholder="Search..."
125+
value={animationGroupsSearch}
126+
onChange={(e) => setAnimationGroupsSearch(e.currentTarget.value)}
127+
className="px-5 py-2 rounded-lg bg-primary-foreground outline-none w-full"
128+
/>
129+
130+
<div className="flex justify-between items-center">
131+
<div className="p-2 font-bold">Actions</div>
132+
133+
<div className="flex gap-2">
134+
<Button variant="ghost" disabled={selectedAnimationGroups.length === 0} className="p-1 w-8 h-8" onClick={() => handleRemoveSelectedAnimationGroups()}>
135+
<AiOutlineMinus className="w-6 h-6" />
136+
</Button>
137+
138+
<Button variant="ghost" className="p-1 w-8 h-8" onClick={() => handlePlaySelectedAnimationGroups()}>
139+
<IoPlay className="w-6 h-6" />
140+
</Button>
141+
</div>
142+
</div>
143+
144+
<Reorder.Group
145+
axis="y"
146+
values={props.object.animationGroups}
147+
onReorder={(items) => {
148+
setAnimationGroups(items);
149+
props.object.animationGroups = items;
150+
}}
151+
className="flex flex-col rounded-lg bg-black/50 text-white/75 h-96 overflow-y-auto"
152+
>
153+
{animations.map((animationGroup) => (
154+
<Reorder.Item key={`${animationGroup.name}`} value={animationGroup} id={`${animationGroup.name}`}>
155+
<div
156+
onClick={(ev) => handleAnimationGroupClick(ev, animationGroup)}
157+
className={`
158+
flex items-center gap-2
159+
${selectedAnimationGroups.includes(animationGroup) ? "bg-muted" : "hover:bg-muted/35"}
160+
transition-all duration-300 ease-in-out
161+
`}
162+
>
163+
<Button variant="ghost" className="w-8 h-8 p-1" onClick={() => handlePlayOrStopAnimationGroup(animationGroup)}>
164+
{animationGroup.isPlaying ? <IoStop className="w-6 h-6" strokeWidth={1} /> : <IoPlay className="w-6 h-6" strokeWidth={1} />}
165+
</Button>
166+
{animationGroup.name}
167+
</div>
168+
</Reorder.Item>
169+
))}
170+
</Reorder.Group>
171+
</>
172+
)}
173+
</EditorInspectorSectionField>
174+
);
175+
}

editor/src/editor/layout/inspector/scene.tsx renamed to editor/src/editor/layout/inspector/scene/scene.tsx

Lines changed: 30 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,70 @@
11
import { Component, DragEvent, ReactNode } from "react";
22

3-
import { Grid } from "react-loader-spinner";
43
import { IoMdCube } from "react-icons/io";
5-
import { HiOutlineTrash } from "react-icons/hi2";
6-
import { IoPlay, IoStop } from "react-icons/io5";
7-
84
import { Divider } from "@blueprintjs/core";
95

106
import { DepthOfFieldEffectBlurLevel, Scene, TonemappingOperator, AnimationGroup, VolumetricLightScatteringPostProcess } from "babylonjs";
117

12-
import { Button } from "../../../ui/shadcn/ui/button";
8+
import { Button } from "../../../../ui/shadcn/ui/button";
139

14-
import { isMesh } from "../../../tools/guards/nodes";
15-
import { isScene } from "../../../tools/guards/scene";
10+
import { isMesh } from "../../../../tools/guards/nodes";
11+
import { isScene } from "../../../../tools/guards/scene";
1612

17-
import { registerUndoRedo } from "../../../tools/undoredo";
18-
import { updateAllLights } from "../../../tools/light/shadows";
19-
import { updateIblShadowsRenderPipeline } from "../../../tools/light/ibl";
13+
import { registerUndoRedo } from "../../../../tools/undoredo";
14+
import { updateAllLights } from "../../../../tools/light/shadows";
15+
import { updateIblShadowsRenderPipeline } from "../../../../tools/light/ibl";
2016

21-
import { createVLSPostProcess, disposeVLSPostProcess, getVLSPostProcess, parseVLSPostProcess, serializeVLSPostProcess } from "../../rendering/vls";
22-
import { createSSRRenderingPipeline, disposeSSRRenderingPipeline, getSSRRenderingPipeline, parseSSRRenderingPipeline, serializeSSRRenderingPipeline } from "../../rendering/ssr";
17+
import { createVLSPostProcess, disposeVLSPostProcess, getVLSPostProcess, parseVLSPostProcess, serializeVLSPostProcess } from "../../../rendering/vls";
18+
import { createSSRRenderingPipeline, disposeSSRRenderingPipeline, getSSRRenderingPipeline, parseSSRRenderingPipeline, serializeSSRRenderingPipeline } from "../../../rendering/ssr";
2319
import {
2420
createSSAO2RenderingPipeline,
2521
disposeSSAO2RenderingPipeline,
2622
getSSAO2RenderingPipeline,
2723
parseSSAO2RenderingPipeline,
2824
serializeSSAO2RenderingPipeline,
29-
} from "../../rendering/ssao";
25+
} from "../../../rendering/ssao";
3026
import {
3127
createMotionBlurPostProcess,
3228
disposeMotionBlurPostProcess,
3329
getMotionBlurPostProcess,
3430
parseMotionBlurPostProcess,
3531
serializeMotionBlurPostProcess,
36-
} from "../../rendering/motion-blur";
32+
} from "../../../rendering/motion-blur";
3733
import {
3834
createDefaultRenderingPipeline,
3935
disposeDefaultRenderingPipeline,
4036
getDefaultRenderingPipeline,
4137
parseDefaultRenderingPipeline,
4238
serializeDefaultRenderingPipeline,
43-
} from "../../rendering/default-pipeline";
39+
} from "../../../rendering/default-pipeline";
4440
import {
4541
createIblShadowsRenderingPipeline,
4642
disposeIblShadowsRenderingPipeline,
4743
getIblShadowsRenderingPipeline,
4844
parseIblShadowsRenderingPipeline,
4945
serializeIblShadowsRenderingPipeline,
50-
} from "../../rendering/ibl-shadows";
46+
} from "../../../rendering/ibl-shadows";
5147

52-
import { EditorInspectorSectionField } from "./fields/section";
48+
import { EditorInspectorSectionField } from "../fields/section";
5349

54-
import { EditorInspectorListField } from "./fields/list";
55-
import { EditorInspectorColorField } from "./fields/color";
56-
import { EditorInspectorSwitchField } from "./fields/switch";
57-
import { EditorInspectorNumberField } from "./fields/number";
58-
import { EditorInspectorVectorField } from "./fields/vector";
59-
import { EditorInspectorSliderField } from "./fields/slider";
60-
import { EditorInspectorTextureField } from "./fields/texture";
50+
import { EditorInspectorListField } from "../fields/list";
51+
import { EditorInspectorColorField } from "../fields/color";
52+
import { EditorInspectorSwitchField } from "../fields/switch";
53+
import { EditorInspectorNumberField } from "../fields/number";
54+
import { EditorInspectorVectorField } from "../fields/vector";
55+
import { EditorInspectorSliderField } from "../fields/slider";
56+
import { EditorInspectorTextureField } from "../fields/texture";
6157

62-
import { ScriptInspectorComponent } from "./script/script";
58+
import { ScriptInspectorComponent } from "../script/script";
6359

64-
import { IEditorInspectorImplementationProps } from "./inspector";
60+
import { IEditorInspectorImplementationProps } from "../inspector";
61+
import { EditorSceneAnimationGroupsInspector } from "./animation-groups";
6562

6663
export interface IEditorSceneInspectorState {
6764
dragOverVlsMesh: boolean;
65+
66+
animationGroupsSearch: string;
67+
selectedAnimationGroups: AnimationGroup[];
6868
}
6969

7070
export class EditorSceneInspector extends Component<IEditorInspectorImplementationProps<Scene>, IEditorSceneInspectorState> {
@@ -82,6 +82,9 @@ export class EditorSceneInspector extends Component<IEditorInspectorImplementati
8282

8383
this.state = {
8484
dragOverVlsMesh: false,
85+
86+
animationGroupsSearch: "",
87+
selectedAnimationGroups: [],
8588
};
8689
}
8790

@@ -149,7 +152,7 @@ export class EditorSceneInspector extends Component<IEditorInspectorImplementati
149152

150153
{/* {this.props.editor.state.enableExperimentalFeatures && this._getIblShadowsRenderingPipelineComponent()} */}
151154

152-
{this._getAnimationGroupsComponent()}
155+
<EditorSceneAnimationGroupsInspector editor={this.props.editor} object={this.props.object} />
153156
</>
154157
);
155158
}
@@ -998,62 +1001,4 @@ export class EditorSceneInspector extends Component<IEditorInspectorImplementati
9981001
</EditorInspectorSectionField>
9991002
);
10001003
}
1001-
1002-
private _getAnimationGroupsComponent(): ReactNode {
1003-
return (
1004-
<EditorInspectorSectionField title="Animation Groups">
1005-
{!this.props.object.animationGroups.length && <div className="text-center text-xl">No animation groups</div>}
1006-
<div className="flex flex-col">
1007-
{this.props.object.animationGroups.map((animationGroup) => (
1008-
<div key={animationGroup.name} className="flex flex-col">
1009-
<div
1010-
className={`
1011-
flex gap-2 justify-between items-center p-2 rounded-lg
1012-
hover:bg-accent
1013-
transition-all duration-300 ease-in-out
1014-
`}
1015-
>
1016-
<div className="flex gap-2 items-center">
1017-
<Button
1018-
variant="ghost"
1019-
className="w-8 h-8 p-1"
1020-
onClick={() => {
1021-
animationGroup.isPlaying ? animationGroup.stop() : animationGroup.start();
1022-
this.forceUpdate();
1023-
}}
1024-
>
1025-
{animationGroup.isPlaying ? <IoStop className="w-6 h-6" strokeWidth={1} /> : <IoPlay className="w-6 h-6" strokeWidth={1} />}
1026-
</Button>
1027-
1028-
<div className="flex flex-col">
1029-
<div>{animationGroup.name}</div>
1030-
1031-
<div className="text-xs">Duration: {Math.round(animationGroup.to - animationGroup.from)} frames</div>
1032-
</div>
1033-
</div>
1034-
1035-
<div className="flex gap-2 items-center">
1036-
{animationGroup.isPlaying && <Grid width={16} height={16} color="gray" />}
1037-
1038-
<Button variant="ghost" onClick={() => this._handleRemoveAnimationGroup(animationGroup)}>
1039-
<HiOutlineTrash className="w-5 h-5" />
1040-
</Button>
1041-
</div>
1042-
</div>
1043-
</div>
1044-
))}
1045-
</div>
1046-
</EditorInspectorSectionField>
1047-
);
1048-
}
1049-
1050-
private _handleRemoveAnimationGroup(animationGroup: AnimationGroup): void {
1051-
registerUndoRedo({
1052-
executeRedo: true,
1053-
undo: () => this.props.editor.layout.preview.scene.addAnimationGroup(animationGroup),
1054-
redo: () => this.props.editor.layout.preview.scene.removeAnimationGroup(animationGroup),
1055-
});
1056-
1057-
this.forceUpdate();
1058-
}
10591004
}

tools/src/decorators/inspector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ export function visibleAsKeyMap(label?: string, configuration?: Omit<VisibleInIn
294294
};
295295
}
296296

297-
export type VisibleInspectorDecoratorAssetPossibleTypes = "json" | "material" | "nodeParticleSystemSet";
297+
export type VisibleInspectorDecoratorAssetPossibleTypes = "json" | "material" | "nodeParticleSystemSet" | "scene";
298298

299299
export type VisibleInspectorDecoratorAssetConfiguration<T = VisibleInspectorDecoratorAssetPossibleTypes> = VisibleInInspectorDecoratorConfiguration & {
300300
assetType: T;

0 commit comments

Comments
 (0)