2
2
import type { IReadonlyObservable , Nullable , Scene } from "core/index" ;
3
3
4
4
import type { TreeItemValue , TreeOpenChangeData , TreeOpenChangeEvent } from "@fluentui/react-components" ;
5
+ import type { ScrollToInterface } from "@fluentui/react-components/unstable" ;
5
6
import type { ComponentType , FunctionComponent } from "react" ;
6
7
7
8
import { Body1 , Body1Strong , Button , FlatTree , FlatTreeItem , makeStyles , ToggleButton , tokens , Tooltip , TreeItemLayout } from "@fluentui/react-components" ;
8
9
import { VirtualizerScrollView } from "@fluentui/react-components/unstable" ;
9
10
import { MoviesAndTvRegular } from "@fluentui/react-icons" ;
10
11
11
- import { useCallback , useEffect , useMemo , useState } from "react" ;
12
+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
12
13
import { TraverseGraph } from "../../misc/graphUtils" ;
13
14
14
15
export type EntityBase = Readonly < {
@@ -37,6 +38,11 @@ export type SceneExplorerSection<T extends EntityBase> = Readonly<{
37
38
*/
38
39
getEntityChildren ?: ( entity : T ) => readonly T [ ] ;
39
40
41
+ /**
42
+ * An optional function that returns the parent of a given entity.
43
+ */
44
+ getEntityParent ?: ( entity : T ) => Nullable < T > ;
45
+
40
46
/**
41
47
* A function that returns the display name for a given entity.
42
48
*/
@@ -122,7 +128,7 @@ type TreeItemData =
122
128
type : "entity" ;
123
129
entity : EntityBase ;
124
130
depth : number ;
125
- parent : Nullable < TreeItemValue > ;
131
+ parent : TreeItemValue ;
126
132
hasChildren : boolean ;
127
133
title : string ;
128
134
icon ?: ComponentType < { entity : EntityBase } > ;
@@ -189,11 +195,17 @@ export const SceneExplorer: FunctionComponent<{
189
195
} > = ( props ) => {
190
196
const classes = useStyles ( ) ;
191
197
192
- const { sections, commands, scene, selectedEntity, setSelectedEntity } = props ;
198
+ const { sections, commands, scene, selectedEntity } = props ;
193
199
194
200
const [ openItems , setOpenItems ] = useState ( new Set < TreeItemValue > ( ) ) ;
195
-
196
201
const [ sceneVersion , setSceneVersion ] = useState ( 0 ) ;
202
+ const scrollViewRef = useRef < ScrollToInterface > ( null ) ;
203
+ // We only want to scroll to the selected item if it was externally selected (outside of SceneExplorer).
204
+ const previousSelectedEntity = useRef ( selectedEntity ) ;
205
+ const setSelectedEntity = ( entity : unknown ) => {
206
+ previousSelectedEntity . current = entity ;
207
+ props . setSelectedEntity ?.( entity ) ;
208
+ } ;
197
209
198
210
// For the filter, we should maybe to the traversal but use onAfterNode so that if the filter matches, we make sure to include the full parent chain.
199
211
// Then just reverse the array of nodes before returning it.
@@ -235,31 +247,33 @@ export const SceneExplorer: FunctionComponent<{
235
247
236
248
const visibleItems = useMemo ( ( ) => {
237
249
const visibleItems : TreeItemData [ ] = [ ] ;
238
- const entityParents = new Map < EntityBase , EntityBase > ( ) ;
250
+ const entityParents = new Map < number , TreeItemValue > ( ) ;
239
251
240
252
visibleItems . push ( {
241
253
type : "scene" ,
242
254
scene : scene ,
243
255
} ) ;
244
256
245
257
for ( const section of sections ) {
258
+ const rootEntities = section . getRootEntities ( scene ) ;
259
+
246
260
visibleItems . push ( {
247
261
type : "section" ,
248
262
sectionName : section . displayName ,
249
- hasChildren : section . getRootEntities ( scene ) . length > 0 ,
263
+ hasChildren : rootEntities . length > 0 ,
250
264
} ) ;
251
265
252
266
if ( openItems . has ( section . displayName ) ) {
253
267
let depth = 1 ;
254
268
TraverseGraph (
255
- section . getRootEntities ( scene ) ,
269
+ rootEntities ,
256
270
( entity ) => {
257
271
if ( openItems . has ( entity . uniqueId ) && section . getEntityChildren ) {
258
272
const children = section . getEntityChildren ( entity ) ;
259
273
for ( const child of children ) {
260
- entityParents . set ( child , entity ) ;
274
+ entityParents . set ( child . uniqueId , entity . uniqueId ) ;
261
275
}
262
- return section . getEntityChildren ( entity ) ;
276
+ return children ;
263
277
}
264
278
return null ;
265
279
} ,
@@ -269,7 +283,7 @@ export const SceneExplorer: FunctionComponent<{
269
283
type : "entity" ,
270
284
entity,
271
285
depth,
272
- parent : entityParents . get ( entity ) ? .uniqueId ?? section . displayName ,
286
+ parent : entityParents . get ( entity . uniqueId ) ?? section . displayName ,
273
287
hasChildren : ! ! section . getEntityChildren && section . getEntityChildren ( entity ) . length > 0 ,
274
288
title : section . getEntityDisplayName ( entity ) ,
275
289
icon : section . entityIcon ,
@@ -285,6 +299,62 @@ export const SceneExplorer: FunctionComponent<{
285
299
return visibleItems ;
286
300
} , [ scene , sceneVersion , sections , openItems , itemsFilter ] ) ;
287
301
302
+ const getParentStack = useCallback (
303
+ ( entity : EntityBase ) => {
304
+ const parentStack : TreeItemValue [ ] = [ ] ;
305
+
306
+ for ( const section of sections ) {
307
+ for ( let parent = section . getEntityParent ?.( entity ) ; parent ; parent = section . getEntityParent ?.( parent ) ) {
308
+ parentStack . push ( parent . uniqueId ) ;
309
+ }
310
+
311
+ if ( parentStack . length > 0 || section . getRootEntities ( scene ) . includes ( entity ) ) {
312
+ parentStack . push ( section . displayName ) ;
313
+ break ;
314
+ }
315
+ }
316
+
317
+ return parentStack ;
318
+ } ,
319
+ [ scene , openItems , sections ]
320
+ ) ;
321
+
322
+ // We only want the effect below to execute when the selectedEntity changes, so we use a ref to keep the latest version of getParentStack.
323
+ const getParentStackRef = useRef ( getParentStack ) ;
324
+ getParentStackRef . current = getParentStack ;
325
+
326
+ const [ isScrollToPending , setIsScrollToPending ] = useState ( false ) ;
327
+
328
+ useEffect ( ( ) => {
329
+ if ( selectedEntity && selectedEntity !== previousSelectedEntity . current ) {
330
+ const entity = selectedEntity as EntityBase ;
331
+ if ( entity . uniqueId != undefined ) {
332
+ const parentStack = getParentStackRef . current ( entity ) ;
333
+ if ( parentStack . length > 0 ) {
334
+ const newOpenItems = new Set < TreeItemValue > ( openItems ) ;
335
+ for ( const parent of parentStack ) {
336
+ newOpenItems . add ( parent ) ;
337
+ }
338
+ setOpenItems ( newOpenItems ) ;
339
+ setIsScrollToPending ( true ) ;
340
+ }
341
+ }
342
+ }
343
+
344
+ previousSelectedEntity . current = selectedEntity ;
345
+ } , [ selectedEntity , setOpenItems , setIsScrollToPending ] ) ;
346
+
347
+ // We need to wait for a render to complete before we can scroll to the item, hence the isScrollToPending.
348
+ useEffect ( ( ) => {
349
+ if ( isScrollToPending ) {
350
+ const selectedItemIndex = visibleItems . findIndex ( ( item ) => item . type === "entity" && item . entity === selectedEntity ) ;
351
+ if ( selectedItemIndex >= 0 && scrollViewRef . current ) {
352
+ scrollViewRef . current . scrollTo ( selectedItemIndex , "smooth" ) ;
353
+ setIsScrollToPending ( false ) ;
354
+ }
355
+ }
356
+ } , [ isScrollToPending , selectedEntity , visibleItems ] ) ;
357
+
288
358
const onOpenChange = useCallback (
289
359
( event : TreeOpenChangeEvent , data : TreeOpenChangeData ) => {
290
360
// This makes it so we only consider a click on the chevron to be expanding/collapsing an item, not clicking anywhere on the item.
@@ -298,7 +368,7 @@ export const SceneExplorer: FunctionComponent<{
298
368
return (
299
369
< div className = { classes . rootDiv } >
300
370
< FlatTree className = { classes . tree } openItems = { openItems } onOpenChange = { onOpenChange } aria-label = "Scene Explorer Tree" >
301
- < VirtualizerScrollView numItems = { visibleItems . length } itemSize = { 32 } container = { { style : { overflowX : "hidden" } } } >
371
+ < VirtualizerScrollView imperativeRef = { scrollViewRef } numItems = { visibleItems . length } itemSize = { 32 } container = { { style : { overflowX : "hidden" } } } >
302
372
{ ( index : number ) => {
303
373
const item = visibleItems [ index ] ;
304
374
0 commit comments