@@ -11,15 +11,70 @@ import { actionToKey } from '@/src/composables/useKeyboardShortcuts';
1111import { useSegmentGroupStore } from '@/src/store/segmentGroups' ;
1212import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool' ;
1313import useLoadDataStore from '@/src/store/load-data' ;
14+ import type { Layout , LayoutItem } from '@/src/types/layout' ;
15+ import type { ViewInfoInit } from '@/src/types/views' ;
1416
1517// --------------------------------------------------------------------------
16- // Interface
18+ // View Specifications
1719
18- const layout = z
19- . object ( {
20- gridSize : z . tuple ( [ z . number ( ) , z . number ( ) ] ) . optional ( ) ,
21- } )
22- . optional ( ) ;
20+ const viewString = z . enum ( [
21+ 'axial' ,
22+ 'coronal' ,
23+ 'sagittal' ,
24+ 'volume' ,
25+ 'oblique' ,
26+ ] ) ;
27+
28+ const view2D = z . object ( {
29+ type : z . literal ( '2D' ) ,
30+ name : z . string ( ) . optional ( ) ,
31+ orientation : z . enum ( [ 'Axial' , 'Coronal' , 'Sagittal' ] ) ,
32+ } ) ;
33+
34+ const view3D = z . object ( {
35+ type : z . literal ( '3D' ) ,
36+ name : z . string ( ) . optional ( ) ,
37+ viewDirection : z . enum ( [ 'Left' , 'Right' , 'Posterior' , 'Anterior' , 'Superior' , 'Inferior' ] ) . optional ( ) ,
38+ viewUp : z . enum ( [ 'Left' , 'Right' , 'Posterior' , 'Anterior' , 'Superior' , 'Inferior' ] ) . optional ( ) ,
39+ } ) ;
40+
41+ const viewOblique = z . object ( {
42+ type : z . literal ( 'Oblique' ) ,
43+ name : z . string ( ) . optional ( ) ,
44+ } ) ;
45+
46+ const viewSpec = z . union ( [ viewString , view2D , view3D , viewOblique ] ) ;
47+
48+ // --------------------------------------------------------------------------
49+ // Layout Specifications
50+
51+ type LayoutConfigItem = z . infer < typeof viewSpec > | {
52+ direction : 'H' | 'V' ;
53+ items : LayoutConfigItem [ ] ;
54+ } ;
55+
56+ const layoutConfigItem : z . ZodType < LayoutConfigItem > = z . lazy ( ( ) =>
57+ z . union ( [
58+ viewSpec ,
59+ z . object ( {
60+ direction : z . enum ( [ 'H' , 'V' ] ) ,
61+ items : z . array ( layoutConfigItem ) ,
62+ } ) ,
63+ ] )
64+ ) ;
65+
66+ const layoutConfig = z . union ( [
67+ z . array ( z . array ( viewString ) ) ,
68+ z . object ( {
69+ direction : z . enum ( [ 'H' , 'V' ] ) ,
70+ items : z . array ( layoutConfigItem ) ,
71+ } ) ,
72+ z . object ( {
73+ gridSize : z . tuple ( [ z . number ( ) , z . number ( ) ] ) ,
74+ } ) ,
75+ ] ) ;
76+
77+ const layout = layoutConfig . optional ( ) ;
2378
2479const shortcuts = z . record ( zodEnumFromObjKeys ( ACTIONS ) , z . string ( ) ) . optional ( ) ;
2580
@@ -89,6 +144,138 @@ export const readConfigFile = async (configFile: File) => {
89144 return config . parse ( JSON . parse ( text ) ) ;
90145} ;
91146
147+ // --------------------------------------------------------------------------
148+ // Layout Parsing
149+
150+ const stringToViewInfoInit = ( str : z . infer < typeof viewString > ) : ViewInfoInit => {
151+ switch ( str ) {
152+ case 'axial' :
153+ return { name : 'Axial' , type : '2D' , dataID : null , options : { orientation : 'Axial' } } ;
154+ case 'coronal' :
155+ return { name : 'Coronal' , type : '2D' , dataID : null , options : { orientation : 'Coronal' } } ;
156+ case 'sagittal' :
157+ return { name : 'Sagittal' , type : '2D' , dataID : null , options : { orientation : 'Sagittal' } } ;
158+ case 'volume' :
159+ return { name : 'Volume' , type : '3D' , dataID : null , options : { viewDirection : 'Posterior' , viewUp : 'Superior' } } ;
160+ case 'oblique' :
161+ return { name : 'Oblique' , type : 'Oblique' , dataID : null , options : { } } ;
162+ default :
163+ throw new Error ( `Unknown view string: ${ str } ` ) ;
164+ }
165+ } ;
166+
167+ const viewSpecToViewInfoInit = ( spec : z . infer < typeof viewSpec > ) : ViewInfoInit => {
168+ if ( typeof spec === 'string' ) {
169+ return stringToViewInfoInit ( spec ) ;
170+ }
171+
172+ if ( spec . type === '2D' ) {
173+ return {
174+ name : spec . name ?? spec . orientation ,
175+ type : '2D' ,
176+ dataID : null ,
177+ options : { orientation : spec . orientation } ,
178+ } ;
179+ }
180+
181+ if ( spec . type === '3D' ) {
182+ return {
183+ name : spec . name ?? 'Volume' ,
184+ type : '3D' ,
185+ dataID : null ,
186+ options : {
187+ viewDirection : spec . viewDirection ?? 'Posterior' ,
188+ viewUp : spec . viewUp ?? 'Superior' ,
189+ } ,
190+ } ;
191+ }
192+
193+ if ( spec . type === 'Oblique' ) {
194+ return {
195+ name : spec . name ?? 'Oblique' ,
196+ type : 'Oblique' ,
197+ dataID : null ,
198+ options : { } ,
199+ } ;
200+ }
201+
202+ throw new Error ( `Unknown view spec type` ) ;
203+ } ;
204+
205+ const parseGridLayout = ( grid : z . infer < typeof viewString > [ ] [ ] ) : { layout : Layout ; views : ViewInfoInit [ ] } => {
206+ const views : ViewInfoInit [ ] = [ ] ;
207+ let slotIndex = 0 ;
208+
209+ const items : LayoutItem [ ] = grid . map ( ( row ) => {
210+ const rowItems : LayoutItem [ ] = row . map ( ( ) => {
211+ const currentSlot = slotIndex ;
212+ slotIndex += 1 ;
213+ return { type : 'slot' as const , slotIndex : currentSlot } ;
214+ } ) ;
215+
216+ return {
217+ type : 'layout' as const ,
218+ direction : 'V' as const ,
219+ items : rowItems ,
220+ } ;
221+ } ) ;
222+
223+ grid . flat ( ) . forEach ( ( viewStr ) => {
224+ views . push ( stringToViewInfoInit ( viewStr ) ) ;
225+ } ) ;
226+
227+ return {
228+ layout : {
229+ direction : 'H' ,
230+ items,
231+ } ,
232+ views,
233+ } ;
234+ } ;
235+
236+ const parseNestedLayout = (
237+ layoutItem : LayoutConfigItem
238+ ) : { layout : Layout ; views : ViewInfoInit [ ] } => {
239+ const views : ViewInfoInit [ ] = [ ] ;
240+ let slotIndex = 0 ;
241+
242+ const processItem = ( item : LayoutConfigItem ) : LayoutItem => {
243+ if ( typeof item === 'string' || 'type' in item ) {
244+ const viewInfo = viewSpecToViewInfoInit ( item as z . infer < typeof viewSpec > ) ;
245+ views . push ( viewInfo ) ;
246+ const currentSlot = slotIndex ;
247+ slotIndex += 1 ;
248+ return { type : 'slot' , slotIndex : currentSlot } ;
249+ }
250+
251+ return {
252+ type : 'layout' ,
253+ direction : item . direction ,
254+ items : item . items . map ( processItem ) ,
255+ } ;
256+ } ;
257+
258+ if ( typeof layoutItem === 'string' || 'type' in layoutItem ) {
259+ const viewInfo = viewSpecToViewInfoInit ( layoutItem as z . infer < typeof viewSpec > ) ;
260+ views . push ( viewInfo ) ;
261+ return {
262+ layout : {
263+ direction : 'H' ,
264+ items : [ { type : 'slot' , slotIndex : 0 } ] ,
265+ } ,
266+ views,
267+ } ;
268+ }
269+
270+ return {
271+ layout : {
272+ direction : layoutItem . direction ,
273+ items : layoutItem . items . map ( processItem ) ,
274+ } ,
275+ views,
276+ } ;
277+ } ;
278+
92279const applyLabels = ( manifest : Config ) => {
93280 if ( ! manifest . labels ) return ;
94281
@@ -115,8 +302,16 @@ const applyLabels = (manifest: Config) => {
115302} ;
116303
117304const applyLayout = ( manifest : Config ) => {
118- if ( manifest . layout ?. gridSize ) {
305+ if ( ! manifest . layout ) return ;
306+
307+ if ( Array . isArray ( manifest . layout ) ) {
308+ const parsedLayout = parseGridLayout ( manifest . layout ) ;
309+ useViewStore ( ) . setLayoutWithViews ( parsedLayout . layout , parsedLayout . views ) ;
310+ } else if ( 'gridSize' in manifest . layout ) {
119311 useViewStore ( ) . setLayoutFromGrid ( manifest . layout . gridSize ) ;
312+ } else {
313+ const parsedLayout = parseNestedLayout ( manifest . layout ) ;
314+ useViewStore ( ) . setLayoutWithViews ( parsedLayout . layout , parsedLayout . views ) ;
120315 }
121316} ;
122317
0 commit comments