@@ -3,7 +3,7 @@ import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core";
33
44import { FaLink } from "react-icons/fa6" ;
55import { IoMdCube } from "react-icons/io" ;
6- import { GiSparkles } from "react-icons/gi" ;
6+ import { GiSparkles , GiSkeletonInside } from "react-icons/gi" ;
77import { BsSoundwave } from "react-icons/bs" ;
88import { AiOutlinePlus } from "react-icons/ai" ;
99import { HiSpeakerWave } from "react-icons/hi2" ;
@@ -12,9 +12,10 @@ import { MdOutlineQuestionMark } from "react-icons/md";
1212import { HiOutlineCubeTransparent } from "react-icons/hi" ;
1313import { IoCheckmark , IoSparklesSharp } from "react-icons/io5" ;
1414import { SiAdobeindesign , SiBabylondotjs } from "react-icons/si" ;
15+ import { PiBoneLight } from "react-icons/pi" ;
1516
1617import { 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
1920import { 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" ;
5759import {
5860 onNodeModifiedObservable ,
@@ -69,6 +71,7 @@ import { EditorGraphContextMenu } from "./graph/graph";
6971import { getMeshCommands } from "../dialogs/command-palette/mesh" ;
7072import { getLightCommands } from "../dialogs/command-palette/light" ;
7173import { getCameraCommands } from "../dialogs/command-palette/camera" ;
74+ import { SKELETON_CONTAINER_TYPE } from "./assets-browser/items/skeleton-item" ;
7275
7376export 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