@@ -3,7 +3,7 @@ import { Button, Tree, TreeNodeInfo } from "@blueprintjs/core";
3
3
4
4
import { FaLink } from "react-icons/fa6" ;
5
5
import { IoMdCube } from "react-icons/io" ;
6
- import { GiSparkles } from "react-icons/gi" ;
6
+ import { GiSparkles , GiSkeletonInside } from "react-icons/gi" ;
7
7
import { BsSoundwave } from "react-icons/bs" ;
8
8
import { AiOutlinePlus } from "react-icons/ai" ;
9
9
import { HiSpeakerWave } from "react-icons/hi2" ;
@@ -12,9 +12,10 @@ import { MdOutlineQuestionMark } from "react-icons/md";
12
12
import { HiOutlineCubeTransparent } from "react-icons/hi" ;
13
13
import { IoCheckmark , IoSparklesSharp } from "react-icons/io5" ;
14
14
import { SiAdobeindesign , SiBabylondotjs } from "react-icons/si" ;
15
+ import { PiBoneLight } from "react-icons/pi" ;
15
16
16
17
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" ;
18
19
19
20
import { Editor } from "../main" ;
20
21
@@ -47,12 +48,13 @@ import {
47
48
isCamera ,
48
49
isCollisionInstancedMesh ,
49
50
isCollisionMesh ,
50
- isEditorCamera ,
51
51
isInstancedMesh ,
52
52
isLight ,
53
53
isMesh ,
54
54
isNode ,
55
55
isTransformNode ,
56
+ isSkeleton ,
57
+ isBone ,
56
58
} from "../../tools/guards/nodes" ;
57
59
import {
58
60
onNodeModifiedObservable ,
@@ -69,6 +71,7 @@ import { EditorGraphContextMenu } from "./graph/graph";
69
71
import { getMeshCommands } from "../dialogs/command-palette/mesh" ;
70
72
import { getLightCommands } from "../dialogs/command-palette/light" ;
71
73
import { getCameraCommands } from "../dialogs/command-palette/camera" ;
74
+ import { SKELETON_CONTAINER_TYPE } from "./assets-browser/items/skeleton-item" ;
72
75
73
76
export interface IEditorGraphProps {
74
77
/**
@@ -262,8 +265,19 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
262
265
if ( this . state . showOnlyDecals ) {
263
266
nodes . push ( ...scene . meshes . filter ( ( mesh ) => mesh . metadata ?. decal ) . map ( ( mesh ) => this . _parseSceneNode ( mesh , true ) ) ) ;
264
267
}
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 ) ;
267
281
}
268
282
269
283
const guiNode = this . _parseGuiNode ( scene ) ;
@@ -295,8 +309,25 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
295
309
* become unselected to have only the given node selected. All parents are expanded.
296
310
* @param node defines the reference tot the node to select in the graph.
297
311
*/
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
+ }
300
331
301
332
if ( ! source ) {
302
333
return ;
@@ -586,6 +617,68 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
586
617
return rootSoundNode ;
587
618
}
588
619
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
+ // Add bones as children
636
+ if ( skeleton . bones . length > 0 ) {
637
+ info . childNodes = skeleton . bones . map ( ( bone ) => this . _parseBoneNode ( bone ) ) . filter ( ( b ) => b !== null ) as TreeNodeInfo [ ] ;
638
+ info . hasCaret = true ;
639
+ }
640
+
641
+ this . _forEachNode ( this . state . nodes , ( n ) => {
642
+ if ( n . id === info . id ) {
643
+ info . isSelected = n . isSelected ;
644
+ info . isExpanded = n . isExpanded ;
645
+ }
646
+ } ) ;
647
+
648
+ return info ;
649
+ }
650
+
651
+ private _parseBoneNode ( bone : any ) : TreeNodeInfo | null {
652
+ if ( ! bone . name . toLowerCase ( ) . includes ( this . state . search . toLowerCase ( ) ) ) {
653
+ return null ;
654
+ }
655
+
656
+ const info = {
657
+ id : bone . id ,
658
+ nodeData : bone ,
659
+ isSelected : false ,
660
+ childNodes : [ ] ,
661
+ hasCaret : false ,
662
+ icon : this . _getBoneIconComponent ( bone ) ,
663
+ label : this . _getNodeLabelComponent ( bone , bone . name ) ,
664
+ } as TreeNodeInfo ;
665
+
666
+ // Add child bones as children
667
+ if ( bone . children && bone . children . length > 0 ) {
668
+ info . childNodes = bone . children . map ( ( childBone : any ) => this . _parseBoneNode ( childBone ) ) . filter ( ( b ) => b !== null ) as TreeNodeInfo [ ] ;
669
+ info . hasCaret = true ;
670
+ }
671
+
672
+ this . _forEachNode ( this . state . nodes , ( n ) => {
673
+ if ( n . id === info . id ) {
674
+ info . isSelected = n . isSelected ;
675
+ info . isExpanded = n . isExpanded ;
676
+ }
677
+ } ) ;
678
+
679
+ return info ;
680
+ }
681
+
589
682
private _getSoundNode ( sound : Sound ) : TreeNodeInfo {
590
683
const info = {
591
684
nodeData : sound ,
@@ -674,6 +767,40 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
674
767
return rootGuiNode ;
675
768
}
676
769
770
+ private _parseSkeletonContainerNode ( container : Node ) : TreeNodeInfo {
771
+ const info = {
772
+ id : container . id ,
773
+ nodeData : container ,
774
+ isSelected : false ,
775
+ childNodes : [ ] ,
776
+ hasCaret : false ,
777
+ icon : this . _getIcon ( container ) ,
778
+ label : this . _getNodeLabelComponent ( container , container . name ) ,
779
+ } as TreeNodeInfo ;
780
+
781
+ // Add skeleton as child
782
+ if ( container . metadata ?. type === "SkeletonContainer" && container . metadata ?. skeleton ) {
783
+ const skeletonNode = this . _parseSkeletonNode ( container . metadata . skeleton ) ;
784
+ if ( skeletonNode ) {
785
+ info . childNodes ! . push ( skeletonNode ) ;
786
+ }
787
+ }
788
+
789
+ // Set hasCaret if we have children
790
+ if ( info . childNodes && info . childNodes . length > 0 ) {
791
+ info . hasCaret = true ;
792
+ }
793
+
794
+ this . _forEachNode ( this . state . nodes , ( n ) => {
795
+ if ( n . id === info . id ) {
796
+ info . isSelected = n . isSelected ;
797
+ info . isExpanded = n . isExpanded ;
798
+ }
799
+ } ) ;
800
+
801
+ return info ;
802
+ }
803
+
677
804
private _parseSceneNode ( node : Node , noChildren ?: boolean ) : TreeNodeInfo | null {
678
805
if ( ( isMesh ( node ) && ( node . _masterMesh || ! isNodeVisibleInGraph ( node ) ) ) || isCollisionMesh ( node ) || isCollisionInstancedMesh ( node ) ) {
679
806
return null ;
@@ -745,6 +872,14 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
745
872
info . childNodes ?. push ( this . _getParticleSystemNode ( particleSystem ) ) ;
746
873
}
747
874
} ) ;
875
+
876
+ // Handle skeleton
877
+ if ( node . skeleton ) {
878
+ const skeletonNode = this . _parseSkeletonNode ( node . skeleton ) ;
879
+ if ( skeletonNode ) {
880
+ info . childNodes ?. push ( skeletonNode ) ;
881
+ }
882
+ }
748
883
}
749
884
750
885
if ( info . childNodes ?. length ) {
@@ -818,6 +953,14 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
818
953
) ;
819
954
}
820
955
956
+ private _getSkeletonIconComponent ( skeleton : Skeleton ) : ReactNode {
957
+ return < div className = "cursor-pointer opacity-100" > { this . _getIcon ( skeleton ) } </ div > ;
958
+ }
959
+
960
+ private _getBoneIconComponent ( bone : any ) : ReactNode {
961
+ return < div className = "cursor-pointer opacity-100" > { this . _getIcon ( bone ) } </ div > ;
962
+ }
963
+
821
964
private _getIcon ( object : any ) : ReactNode {
822
965
if ( isTransformNode ( object ) ) {
823
966
return < HiOutlineCubeTransparent className = "w-4 h-4" /> ;
@@ -855,6 +998,14 @@ export class EditorGraph extends Component<IEditorGraphProps, IEditorGraphState>
855
998
return < GiSparkles className = "w-4 h-4" /> ;
856
999
}
857
1000
1001
+ if ( isSkeleton ( object ) ) {
1002
+ return < GiSkeletonInside className = "w-4 h-4" /> ;
1003
+ }
1004
+
1005
+ if ( isBone ( object ) ) {
1006
+ return < PiBoneLight className = "w-4 h-4" /> ;
1007
+ }
1008
+
858
1009
return < MdOutlineQuestionMark className = "w-4 h-4" /> ;
859
1010
}
860
1011
0 commit comments