Skip to content

Commit e411789

Browse files
yuripourreYuri Pourre
authored andcommitted
Add skeleton to graph
1 parent 5c2749e commit e411789

File tree

5 files changed

+464
-246
lines changed

5 files changed

+464
-246
lines changed
Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import { ReactNode } from "react";
2+
import { extname, basename } from "path";
23

34
import { GiSkeletonInside } from "react-icons/gi";
45

5-
import { SceneLoader, Debug } from "babylonjs";
6+
import { TransformNode, Tools, Debug, SceneLoader, Skeleton, Scene } from "babylonjs";
67

78
import { AssetsBrowserItem } from "./item";
89
import { ContextMenuItem } from "../../../../ui/shadcn/ui/context-menu";
10+
import { UniqueNumber } from "../../../../tools/tools";
11+
12+
const DEFAULT_ANIMATION_FRAMES = 60;
13+
const SKELETON_VIEWER_SCALE = 1;
14+
const SKELETON_CONTAINER_SUFFIX = "_Container";
15+
export const SKELETON_CONTAINER_TYPE = "SkeletonContainer";
916

1017
export class AssetBrowserSkeletonItem extends AssetsBrowserItem {
1118
/**
@@ -28,45 +35,53 @@ export class AssetBrowserSkeletonItem extends AssetsBrowserItem {
2835
return <GiSkeletonInside size="64px" />;
2936
}
3037

31-
/**
32-
* @override
33-
*/
34-
protected async onDoubleClick(): Promise<void> {
35-
await this._handleLoadSkeletonToScene();
36-
}
37-
3838
private async _handleLoadSkeletonToScene(): Promise<void> {
3939
const scene = this.props.editor.layout.preview.scene;
40-
if (!scene) {
41-
return;
42-
}
43-
4440
try {
45-
// Load the BVH file using SceneLoader
4641
const result = await SceneLoader.ImportMeshAsync("", "", this.props.absolutePath, scene);
4742

48-
// Get the skeleton from the result
4943
const skeleton = result.skeletons[0];
5044

5145
if (skeleton) {
52-
// Create a skeleton viewer to visualize the skeleton
53-
const viewer = new Debug.SkeletonViewer(skeleton, null, scene, false, 1, {
54-
displayMode: Debug.SkeletonViewer.DISPLAY_SPHERE_AND_SPURS,
55-
});
56-
viewer.isEnabled = true;
57-
58-
// Start the animation if available
59-
const highestFrame = skeleton.bones[0]?.animations[0]?.getHighestFrame() ?? 60;
60-
scene.beginAnimation(skeleton, 0, highestFrame, true);
46+
// Define the skeleton name from the file name
47+
const extension = extname(this.props.absolutePath).toLowerCase();
48+
if (extension === ".bvh") {
49+
const skeletonName = basename(this.props.absolutePath, extension);
50+
skeleton.name = skeletonName;
51+
}
6152

62-
// Notify the user
63-
this.props.editor.layout.console.log(`Loaded skeleton: ${skeleton.name} with ${skeleton.bones.length} bones`);
64-
} else {
65-
this.props.editor.layout.console.warn("No skeleton found in the BVH file");
53+
this._createSkeletonContainer(skeleton, scene);
54+
this.props.editor.layout.graph.refresh();
6655
}
6756
} catch (error) {
68-
console.error("Failed to load BVH file:", error);
6957
this.props.editor.layout.console.error(`Failed to load BVH file: ${error}`);
7058
}
7159
}
60+
61+
/**
62+
* Creates a skeleton container with viewer and animation
63+
*/
64+
private _createSkeletonContainer(skeleton: Skeleton, scene: Scene): void {
65+
const skeletonContainer = new TransformNode(`${skeleton.name}${SKELETON_CONTAINER_SUFFIX}`, scene);
66+
skeletonContainer.id = Tools.RandomId();
67+
skeletonContainer.uniqueId = UniqueNumber.Get();
68+
69+
const viewer = new Debug.SkeletonViewer(skeleton, null, scene, false, SKELETON_VIEWER_SCALE, {
70+
displayMode: Debug.SkeletonViewer.DISPLAY_SPHERE_AND_SPURS,
71+
});
72+
viewer.isEnabled = true;
73+
74+
// Store the skeleton reference and viewer in the container's metadata for easy access
75+
skeletonContainer.metadata = {
76+
...skeletonContainer.metadata,
77+
skeleton: skeleton,
78+
viewer: viewer,
79+
type: SKELETON_CONTAINER_TYPE,
80+
};
81+
82+
const highestFrame = skeleton.bones[0]?.animations[0]?.getHighestFrame() ?? DEFAULT_ANIMATION_FRAMES;
83+
scene.beginAnimation(skeleton, 0, highestFrame, true);
84+
85+
this.props.editor.layout.console.log(`Loaded skeleton: ${skeleton.name} with ${skeleton.bones.length} bones`);
86+
}
7287
}

editor/src/editor/layout/graph.tsx

Lines changed: 153 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core";
33

44
import { FaLink } from "react-icons/fa6";
55
import { IoMdCube } from "react-icons/io";
6-
import { GiSparkles } from "react-icons/gi";
6+
import { GiSparkles, GiSkeletonInside } from "react-icons/gi";
77
import { BsSoundwave } from "react-icons/bs";
88
import { AiOutlinePlus } from "react-icons/ai";
99
import { HiSpeakerWave } from "react-icons/hi2";
@@ -12,9 +12,10 @@ import { MdOutlineQuestionMark } from "react-icons/md";
1212
import { HiOutlineCubeTransparent } from "react-icons/hi";
1313
import { IoCheckmark, IoSparklesSharp } from "react-icons/io5";
1414
import { SiAdobeindesign, SiBabylondotjs } from "react-icons/si";
15+
import { PiBoneLight } from "react-icons/pi";
1516

1617
import { AdvancedDynamicTexture } from "babylonjs-gui";
17-
import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem } from "babylonjs";
18+
import { BaseTexture, Node, Scene, Sound, Tools, IParticleSystem, ParticleSystem, Skeleton } from "babylonjs";
1819

1920
import { Editor } from "../main";
2021

@@ -47,12 +48,13 @@ import {
4748
isCamera,
4849
isCollisionInstancedMesh,
4950
isCollisionMesh,
50-
isEditorCamera,
5151
isInstancedMesh,
5252
isLight,
5353
isMesh,
5454
isNode,
5555
isTransformNode,
56+
isSkeleton,
57+
isBone,
5658
} from "../../tools/guards/nodes";
5759
import {
5860
onNodeModifiedObservable,
@@ -69,6 +71,7 @@ import { EditorGraphContextMenu } from "./graph/graph";
6971
import { getMeshCommands } from "../dialogs/command-palette/mesh";
7072
import { getLightCommands } from "../dialogs/command-palette/light";
7173
import { getCameraCommands } from "../dialogs/command-palette/camera";
74+
import { SKELETON_CONTAINER_TYPE } from "./assets-browser/items/skeleton-item";
7275

7376
export interface IEditorGraphProps {
7477
/**
@@ -262,8 +265,19 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
262265
if (this.state.showOnlyDecals) {
263266
nodes.push(...scene.meshes.filter((mesh) => mesh.metadata?.decal).map((mesh) => this._parseSceneNode(mesh, true)));
264267
}
265-
} else {
266-
nodes = scene.rootNodes.filter((n) => !isEditorCamera(n)).map((n) => this._parseSceneNode(n));
268+
}
269+
270+
// Add skeleton containers (TransformNodes that contain skeletons) to avoid duplication
271+
const skeletonContainers = scene.transformNodes.filter((transformNode) => {
272+
return transformNode.metadata?.type === SKELETON_CONTAINER_TYPE;
273+
});
274+
275+
// Add skeleton containers to the graph
276+
if (skeletonContainers.length > 0) {
277+
const containerNodes = skeletonContainers.map((container) => {
278+
return this._parseSkeletonContainerNode(container);
279+
});
280+
nodes.push(...containerNodes);
267281
}
268282

269283
const guiNode = this._parseGuiNode(scene);
@@ -295,8 +309,25 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
295309
* become unselected to have only the given node selected. All parents are expanded.
296310
* @param node defines the reference tot the node to select in the graph.
297311
*/
298-
public setSelectedNode(node: Node | Sound | IParticleSystem): void {
299-
let source = isSound(node) ? node["_connectedTransformNode"] : isAnyParticleSystem(node) ? node.emitter : node;
312+
public setSelectedNode(node: Node | Sound | IParticleSystem | Skeleton): void {
313+
let source: Node | null = null;
314+
315+
if (isSound(node)) {
316+
source = node["_connectedTransformNode"];
317+
} else if (isAnyParticleSystem(node)) {
318+
if (isNode(node.emitter)) {
319+
source = node.emitter;
320+
}
321+
} else if (isSkeleton(node)) {
322+
// For skeletons, we don't have a parent to expand, just select the skeleton
323+
this._forEachNode(this.state.nodes, (n) => {
324+
n.isSelected = n.nodeData === node;
325+
});
326+
this.setState({ nodes: this.state.nodes });
327+
return;
328+
} else {
329+
source = node;
330+
}
300331

301332
if (!source) {
302333
return;
@@ -586,6 +617,66 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
586617
return rootSoundNode;
587618
}
588619

620+
private _parseSkeletonNode(skeleton: Skeleton): TreeNodeInfo | null {
621+
if (!skeleton.name.toLowerCase().includes(this.state.search.toLowerCase())) {
622+
return null;
623+
}
624+
625+
const info = {
626+
id: skeleton.id,
627+
nodeData: skeleton,
628+
isSelected: false,
629+
childNodes: [],
630+
hasCaret: false,
631+
icon: this._getSkeletonIconComponent(skeleton),
632+
label: this._getNodeLabelComponent(skeleton, skeleton.name),
633+
} as TreeNodeInfo;
634+
635+
if (skeleton.bones.length > 0) {
636+
info.childNodes = skeleton.bones.map((bone) => this._parseBoneNode(bone)).filter((b) => b !== null) as TreeNodeInfo[];
637+
info.hasCaret = true;
638+
}
639+
640+
this._forEachNode(this.state.nodes, (n) => {
641+
if (n.id === info.id) {
642+
info.isSelected = n.isSelected;
643+
info.isExpanded = n.isExpanded;
644+
}
645+
});
646+
647+
return info;
648+
}
649+
650+
private _parseBoneNode(bone: any): TreeNodeInfo | null {
651+
if (!bone.name.toLowerCase().includes(this.state.search.toLowerCase())) {
652+
return null;
653+
}
654+
655+
const info = {
656+
id: bone.id,
657+
nodeData: bone,
658+
isSelected: false,
659+
childNodes: [],
660+
hasCaret: false,
661+
icon: this._getBoneIconComponent(bone),
662+
label: this._getNodeLabelComponent(bone, bone.name),
663+
} as TreeNodeInfo;
664+
665+
if (bone.children && bone.children.length > 0) {
666+
info.childNodes = bone.children.map((childBone: any) => this._parseBoneNode(childBone)).filter((b) => b !== null) as TreeNodeInfo[];
667+
info.hasCaret = true;
668+
}
669+
670+
this._forEachNode(this.state.nodes, (n) => {
671+
if (n.id === info.id) {
672+
info.isSelected = n.isSelected;
673+
info.isExpanded = n.isExpanded;
674+
}
675+
});
676+
677+
return info;
678+
}
679+
589680
private _getSoundNode(sound: Sound): TreeNodeInfo {
590681
const info = {
591682
nodeData: sound,
@@ -674,6 +765,38 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
674765
return rootGuiNode;
675766
}
676767

768+
private _parseSkeletonContainerNode(container: Node): TreeNodeInfo {
769+
const info = {
770+
id: container.id,
771+
nodeData: container,
772+
isSelected: false,
773+
childNodes: [],
774+
hasCaret: false,
775+
icon: this._getIcon(container),
776+
label: this._getNodeLabelComponent(container, container.name),
777+
} as TreeNodeInfo;
778+
779+
if (container.metadata?.type === SKELETON_CONTAINER_TYPE && container.metadata?.skeleton) {
780+
const skeletonNode = this._parseSkeletonNode(container.metadata.skeleton);
781+
if (skeletonNode) {
782+
info.childNodes!.push(skeletonNode);
783+
}
784+
}
785+
786+
if (info.childNodes && info.childNodes.length > 0) {
787+
info.hasCaret = true;
788+
}
789+
790+
this._forEachNode(this.state.nodes, (n) => {
791+
if (n.id === info.id) {
792+
info.isSelected = n.isSelected;
793+
info.isExpanded = n.isExpanded;
794+
}
795+
});
796+
797+
return info;
798+
}
799+
677800
private _parseSceneNode(node: Node, noChildren?: boolean): TreeNodeInfo | null {
678801
if ((isMesh(node) && (node._masterMesh || !isNodeVisibleInGraph(node))) || isCollisionMesh(node) || isCollisionInstancedMesh(node)) {
679802
return null;
@@ -745,6 +868,13 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
745868
info.childNodes?.push(this._getParticleSystemNode(particleSystem));
746869
}
747870
});
871+
872+
if (node.skeleton) {
873+
const skeletonNode = this._parseSkeletonNode(node.skeleton);
874+
if (skeletonNode) {
875+
info.childNodes?.push(skeletonNode);
876+
}
877+
}
748878
}
749879

750880
if (info.childNodes?.length) {
@@ -818,6 +948,14 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
818948
);
819949
}
820950

951+
private _getSkeletonIconComponent(skeleton: Skeleton): ReactNode {
952+
return <div className="cursor-pointer opacity-100">{this._getIcon(skeleton)}</div>;
953+
}
954+
955+
private _getBoneIconComponent(bone: any): ReactNode {
956+
return <div className="cursor-pointer opacity-100">{this._getIcon(bone)}</div>;
957+
}
958+
821959
private _getIcon(object: any): ReactNode {
822960
if (isTransformNode(object)) {
823961
return <HiOutlineCubeTransparent className="w-4 h-4" />;
@@ -855,6 +993,14 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
855993
return <GiSparkles className="w-4 h-4" />;
856994
}
857995

996+
if (isSkeleton(object)) {
997+
return <GiSkeletonInside className="w-4 h-4" />;
998+
}
999+
1000+
if (isBone(object)) {
1001+
return <PiBoneLight className="w-4 h-4" />;
1002+
}
1003+
8581004
return <MdOutlineQuestionMark className="w-4 h-4" />;
8591005
}
8601006

0 commit comments

Comments
 (0)