dazscript-framework 0.3.2 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { BasicDialog } from '@dsf/dialog/basic-dialog'
2
+ import { PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
3
+ import { getDataItem } from '@dsf/helpers/list-view-helper'
4
+ import { Observable } from '@dsf/lib/observable'
5
+ import { TreeNode } from '@dsf/lib/tree-node'
6
+
7
+ // Types
8
+
9
+ export interface FileItem {
10
+ name: string
11
+ type: 'folder' | 'file'
12
+ size: number
13
+ }
14
+
15
+ // Model
16
+
17
+ export class ListDialogModel {
18
+ keywords$ = new Observable('')
19
+ files$ = new Observable<TreeNode<FileItem>[]>([])
20
+ selected$ = new Observable<FileItem>()
21
+ recentFiles$ = new Observable<string[]>([])
22
+ selectedRecent$ = new Observable<string[]>([])
23
+ }
24
+
25
+ // Dialog
26
+
27
+ export class ListDialog extends BasicDialog {
28
+ constructor(private readonly model: ListDialogModel) {
29
+ super('05 List View')
30
+ }
31
+
32
+ protected build(): void {
33
+ let add = this.add
34
+ let model = this.model
35
+
36
+ this.builder.options({ resizable: true, width: 640, height: 520 })
37
+
38
+ // Search bar
39
+ add.horizontal((layout) => {
40
+ layout.spacing = 5
41
+ add.label('Search:').minWidth(50)
42
+ add.edit()
43
+ .value(model.keywords$)
44
+ .placeholder('Filter files...')
45
+ .focus()
46
+ })
47
+
48
+ // Tree list with context menu and keyword filter
49
+ add.list.view<FileItem>()
50
+ .expanded(true)
51
+ .items(model.files$)
52
+ .columns(['Name', 'Type', 'Size (KB)'])
53
+ .text(item => [
54
+ item.name,
55
+ item.value?.type ?? '',
56
+ item.value ? String(Math.round(item.value.size / 1024)) : '',
57
+ ])
58
+ .data(item => item.value)
59
+ .selected(model.selected$)
60
+ .filter({
61
+ keywords: model.keywords$,
62
+ field: li => li.text(0),
63
+ selectOnFilter: true,
64
+ })
65
+ .contextMenu((_, li) => {
66
+ let file = getDataItem<FileItem>(li)
67
+ let label = file?.name ?? 'item'
68
+ let items: PopupMenuItem[] = [
69
+ { text: `Open "${label}"`, activated: () => { /* open */ } },
70
+ { text: `Rename "${label}"`, activated: () => { /* rename */ } },
71
+ { text: '' },
72
+ { text: 'Copy Path', activated: () => { /* copy to clipboard */ } },
73
+ { text: 'Delete', activated: () => { /* delete */ } },
74
+ ]
75
+ return add.contextMenu().items(...items).build()
76
+ })
77
+ .build()
78
+
79
+ // Simple list box for recently opened files
80
+ add.group('Recent Files').build(() => {
81
+ add.list.box()
82
+ .items(model.recentFiles$)
83
+ .mode('extended')
84
+ .selected(model.selectedRecent$)
85
+ .build()
86
+ })
87
+ }
88
+ }
@@ -0,0 +1,87 @@
1
+ import { debug } from '@dsf/common/log'
2
+ import { action } from '@dsf/core/action'
3
+ import { info } from '@dsf/helpers/message-box-helper'
4
+ import { TreeNode } from '@dsf/lib/tree-node'
5
+ import { SceneObject, ShowcaseDialog, ShowcaseDialogModel } from './06-showcase-dialog'
6
+
7
+ // Sample data
8
+
9
+ function buildSceneTree(): TreeNode<SceneObject>[] {
10
+ let id = 0
11
+ const next = () => ++id
12
+
13
+ const group = (name: string, children: TreeNode<SceneObject>[] = []) =>
14
+ new TreeNode<SceneObject>(
15
+ name, name,
16
+ { id: next(), name, type: 'Group', visible: true, locked: false },
17
+ children,
18
+ )
19
+
20
+ const obj = (name: string, type: SceneObject['type'], visible = true, locked = false) =>
21
+ new TreeNode<SceneObject>(name, name, { id: next(), name, type, visible, locked })
22
+
23
+ return [
24
+ group('Characters', [
25
+ obj('Victoria 9', 'Figure'),
26
+ obj('Michael 9', 'Figure', true, true),
27
+ obj('Genesis 9', 'Figure', false),
28
+ ]),
29
+ group('Environment', [
30
+ obj('Studio Floor', 'Prop'),
31
+ obj('Background', 'Prop', false),
32
+ group('Lights', [
33
+ obj('Key Light', 'Light'),
34
+ obj('Fill Light', 'Light', false),
35
+ obj('Rim Light', 'Light'),
36
+ ]),
37
+ ]),
38
+ obj('Camera 1', 'Camera'),
39
+ obj('Camera 2', 'Camera', false),
40
+ ]
41
+ }
42
+
43
+ // Action
44
+
45
+ action({ text: '06 Showcase Dialog' }, () => {
46
+ let model = new ShowcaseDialogModel()
47
+ let dialog = new ShowcaseDialog(model)
48
+
49
+ model.objects$.value = buildSceneTree()
50
+
51
+ model.logEntries$.value = [
52
+ '[Info] Script started',
53
+ '[Info] Scene loaded - 4 root nodes',
54
+ '[Debug] Settings restored from DzAppSettings',
55
+ ]
56
+
57
+ // Keep the detail panel in sync with the selected object.
58
+ model.selectedObject$.connect(obj => {
59
+ model.detailName$.value = obj?.name ?? ''
60
+ model.detailType$.value = obj?.type ?? ''
61
+ model.detailVisible$.value = obj?.visible ?? false
62
+ model.detailLocked$.value = obj?.locked ?? false
63
+ })
64
+
65
+ if (!dialog.run()) {
66
+ debug('Showcase dialog cancelled')
67
+ return
68
+ }
69
+
70
+ let sel = model.selectedObject$.value
71
+ let settings = model.settings
72
+ const lines = sel
73
+ ? [
74
+ `Selected : ${sel.name}`,
75
+ `Type : ${sel.type}`,
76
+ `Visible : ${sel.visible}`,
77
+ `Locked : ${sel.locked}`,
78
+ ``,
79
+ `Quality : ${settings.quality$.value}`,
80
+ `Samples : ${settings.samples$.value}`,
81
+ `Format : ${settings.exportFormat$.value}`,
82
+ ]
83
+ : ['No object selected']
84
+
85
+ debug(lines.join('\n'))
86
+ info(lines.join('\n'))
87
+ })
@@ -0,0 +1,518 @@
1
+ import { BasicDialog } from '@dsf/dialog/basic-dialog'
2
+ import { PopupMenuItem } from '@dsf/dialog/builders/popup-menu-builder'
3
+ import { getDataItem } from '@dsf/helpers/list-view-helper'
4
+ import { AppSettings } from '@dsf/lib/settings'
5
+ import { Observable } from '@dsf/lib/observable'
6
+ import { TreeNode } from '@dsf/lib/tree-node'
7
+ import { config } from './config'
8
+
9
+ // Types
10
+
11
+ export interface SceneObject {
12
+ id: number
13
+ name: string
14
+ type: 'Figure' | 'Prop' | 'Light' | 'Camera' | 'Group'
15
+ visible: boolean
16
+ locked: boolean
17
+ }
18
+
19
+ // Settings (all persisted via DzAppSettings)
20
+
21
+ export class ShowcaseDialogSettings extends AppSettings {
22
+ constructor() {
23
+ super(`${config.author}/06-ShowcaseDialog`)
24
+ }
25
+
26
+ // Objects tab
27
+ activeTab$ = this.bindInt('activeTab$', 0)
28
+ flatList$ = this.bindBoolean('flatList$', false)
29
+ showIcons$ = this.bindBoolean('showIcons$', true)
30
+ showVisibleOnly$ = this.bindBoolean('showVisibleOnly$', false)
31
+ splitterState$ = this.bindString('splitterState$', '')
32
+ filterType$ = this.bindString('filterType$', 'All')
33
+
34
+ // Settings tab - Render
35
+ quality$ = this.bindString('quality$', 'Standard')
36
+ samples$ = this.bindInt('samples$', 64)
37
+ scale$ = this.bindFloat('scale$', 1.0)
38
+
39
+ // Settings tab - Export
40
+ exportFormat$ = this.bindString('exportFormat$', 'JSON')
41
+ outputPath$ = this.bindString('outputPath$', '')
42
+ autoSave$ = this.bindBoolean('autoSave$', false)
43
+
44
+ // Settings tab - Behaviour
45
+ notifications$ = this.bindBoolean('notifications$', true)
46
+ logLevel$ = this.bindString('logLevel$', 'Info')
47
+
48
+ // About tab
49
+ showFilters$ = this.bindBoolean('showFilters$', false)
50
+ }
51
+
52
+ // Model
53
+
54
+ export class ShowcaseDialogModel {
55
+ readonly settings = new ShowcaseDialogSettings()
56
+
57
+ objects$ = new Observable<TreeNode<SceneObject>[]>([])
58
+ selectedObject$ = new Observable<SceneObject>()
59
+ refreshObjects$ = new Observable<void>()
60
+ keywords$ = new Observable('')
61
+ logEntries$ = new Observable<string[]>([])
62
+ sceneStats$ = new Observable('')
63
+ selectedSummary$ = new Observable('No object selected')
64
+
65
+ // Detail panel fields - updated externally when selectedObject$ changes
66
+ detailName$ = new Observable('')
67
+ detailType$ = new Observable('')
68
+ detailVisible$ = new Observable(false)
69
+ detailLocked$ = new Observable(false)
70
+ }
71
+
72
+ // Dialog
73
+
74
+ export class ShowcaseDialog extends BasicDialog {
75
+ private sceneListView: DzListView | null = null
76
+
77
+ constructor(private readonly model: ShowcaseDialogModel) {
78
+ super('06 Showcase Dialog', `${config.author}/06-ShowcaseDialog`)
79
+ }
80
+
81
+ protected build(): void {
82
+ let add = this.add
83
+ let settings = this.model.settings
84
+
85
+ this.builder.options({ resizable: true, width: 780, height: 640 })
86
+ this.dialog.setAcceptButtonText('Apply')
87
+ this.dialog.setCancelButtonText('Close')
88
+
89
+ this.connectInteractions()
90
+
91
+ // Active tab index persists across runs via bindInt.
92
+ add.tab('Objects').bind(settings.activeTab$).build(() => {
93
+ this.buildObjectsTab()
94
+ })
95
+ add.tab('Settings').build(() => {
96
+ this.buildSettingsTab()
97
+ })
98
+ add.tab('About').build(() => {
99
+ this.buildAboutTab()
100
+ })
101
+
102
+ this.updateSceneStats()
103
+ }
104
+
105
+ // Tab 1: Objects
106
+
107
+ private buildObjectsTab(): void {
108
+ let add = this.add
109
+ let model = this.model
110
+ let settings = model.settings
111
+
112
+ // Toolbar row
113
+ add.group('Search').horizontal().build((layout) => {
114
+ layout.spacing = 4
115
+ add.edit()
116
+ .value(model.keywords$)
117
+ .placeholder('Filter objects...')
118
+ .focus()
119
+ add.label('Type:')
120
+ add.combo()
121
+ .items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
122
+ .selected(settings.filterType$)
123
+ add.button('Refresh')
124
+ .clicked(() => model.refreshObjects$.trigger())
125
+ .toolTip('Reload the object list')
126
+ add.button('Apply Checked Rows')
127
+ .clicked(() => this.applyCheckedRows())
128
+ .toolTip('Copies the list checkbox states back to the scene objects.')
129
+ })
130
+
131
+ add.horizontal((layout) => {
132
+ layout.spacing = 6
133
+ add.checkbox('Flat List').value(settings.flatList$)
134
+ add.checkbox('Show Icons').value(settings.showIcons$)
135
+ add.checkbox('Visible Only').value(settings.showVisibleOnly$)
136
+ const statsLabel = add.label(model.sceneStats$.value).build()
137
+ model.sceneStats$.connect(text => { statsLabel.text = text })
138
+ })
139
+
140
+ // Splitter - position persisted via bindString
141
+ add.splitter()
142
+ .state(settings.splitterState$)
143
+ .strech(2, 1)
144
+ .items(
145
+ // Left pane: tree list with filter + context menu
146
+ add.group().style({ flat: true }).build(() => {
147
+ add.list.view<SceneObject, SceneObject>()
148
+ .row((item, parent, id) => {
149
+ let obj = item.value as SceneObject
150
+ let listItem = new DzCheckListItem(parent, item.isLeaf ? DzCheckListItem.CheckBox : DzCheckListItem.CheckBoxController, id)
151
+ listItem.selectable = true
152
+ listItem.on = obj.visible
153
+ return listItem
154
+ })
155
+ .expanded(true)
156
+ .flat(settings.flatList$)
157
+ .refresh(model.refreshObjects$)
158
+ .items(model.objects$)
159
+ .columns(['Visible', 'Name', 'Type', 'Locked'])
160
+ .text(item => [
161
+ '',
162
+ this.getDisplayName(item.value),
163
+ item.value?.type ?? '',
164
+ item.value?.locked ? 'Yes' : 'No',
165
+ ])
166
+ .data(item => item.value)
167
+ .selected(model.selectedObject$)
168
+ .filter({
169
+ keywords: model.keywords$,
170
+ field: li => li.text(0),
171
+ selectOnFilter: true,
172
+ filters: li => {
173
+ let obj = getDataItem<SceneObject>(li)
174
+ if (!obj) return true
175
+ let type = settings.filterType$.value
176
+ let matchesType = type === 'All' || obj.type === type
177
+ let matchesVisibility = !settings.showVisibleOnly$.value || obj.visible
178
+ return matchesType && matchesVisibility
179
+ },
180
+ })
181
+ .contextMenu((_, li) => {
182
+ let obj = getDataItem<SceneObject>(li)
183
+ let label = obj?.name ?? 'item'
184
+ let items: PopupMenuItem[] = [
185
+ {
186
+ text: `Select "${label}"`,
187
+ activated: () => { /* select in viewport */ },
188
+ },
189
+ {
190
+ text: obj?.visible ? 'Hide' : 'Show',
191
+ activated: () => {
192
+ if (obj) obj.visible = !obj.visible
193
+ model.refreshObjects$.trigger()
194
+ },
195
+ },
196
+ { text: '' }, // separator
197
+ {
198
+ text: 'Duplicate',
199
+ activated: () => { /* duplicate */ },
200
+ },
201
+ {
202
+ text: 'Delete',
203
+ activated: () => { /* delete */ },
204
+ },
205
+ ]
206
+ return add.contextMenu().items(...items).build()
207
+ })
208
+ .build((listView) => {
209
+ this.sceneListView = listView
210
+ listView.allColumnsShowFocus = true
211
+ })
212
+ }),
213
+
214
+ // Right pane: detail panel for selected object
215
+ add.group('Details').build(() => {
216
+ add.horizontal((layout) => {
217
+ layout.spacing = 4
218
+ add.label('Name:').minWidth(45)
219
+ add.edit().readOnly().value(model.detailName$)
220
+ })
221
+ add.horizontal((layout) => {
222
+ layout.spacing = 4
223
+ add.label('Type:').minWidth(45)
224
+ add.edit().readOnly().value(model.detailType$)
225
+ })
226
+ add.group('Flags').horizontal().build(() => {
227
+ add.checkbox('Visible').value(model.detailVisible$)
228
+ add.checkbox('Locked').value(model.detailLocked$)
229
+ })
230
+ const summaryLabel = add.label(model.selectedSummary$.value).wordWrap().build()
231
+ model.selectedSummary$.connect(text => { summaryLabel.text = text })
232
+ add.group('Scene Node').build(() => {
233
+ // NodeSelection lets the user pick any node from the scene.
234
+ add.nodeSelection().build()
235
+ })
236
+ })
237
+ )
238
+ .build()
239
+ }
240
+
241
+ // Tab 2: Settings
242
+
243
+ private buildSettingsTab(): void {
244
+ let add = this.add
245
+ let settings = this.model.settings
246
+
247
+ add.horizontal((layout) => {
248
+ layout.spacing = 8
249
+
250
+ // Left column
251
+ add.vertical(() => {
252
+ add.group('Display').build(() => {
253
+ add.checkbox('Flat List').value(settings.flatList$)
254
+ add.checkbox('Show Icons').value(settings.showIcons$)
255
+ add.checkbox('Visible Only').value(settings.showVisibleOnly$)
256
+ add.horizontal((layout) => {
257
+ layout.spacing = 4
258
+ add.label('Object Type:').minWidth(80)
259
+ add.combo()
260
+ .items(['All', 'Figure', 'Prop', 'Light', 'Camera', 'Group'])
261
+ .selected(settings.filterType$)
262
+ })
263
+ })
264
+
265
+ // Radio buttons - initialize from persisted quality value;
266
+ // toggled() writes back when the user picks a different option.
267
+ add.group('Render Quality').build(() => {
268
+ add.radio('Draft')
269
+ .value(settings.quality$.value === 'Draft')
270
+ .toggled(v => {
271
+ if (!v) return
272
+ settings.quality$.value = 'Draft'
273
+ settings.samples$.value = 16
274
+ settings.scale$.value = 0.5
275
+ })
276
+ add.radio('Standard')
277
+ .value(settings.quality$.value === 'Standard')
278
+ .toggled(v => {
279
+ if (!v) return
280
+ settings.quality$.value = 'Standard'
281
+ settings.samples$.value = 64
282
+ settings.scale$.value = 1.0
283
+ })
284
+ add.radio('High')
285
+ .value(settings.quality$.value === 'High')
286
+ .toggled(v => {
287
+ if (!v) return
288
+ settings.quality$.value = 'High'
289
+ settings.samples$.value = 256
290
+ settings.scale$.value = 2.0
291
+ })
292
+ })
293
+ })
294
+
295
+ // Right column
296
+ add.vertical(() => {
297
+ add.group('Render').build(() => {
298
+ add.horizontal((layout) => {
299
+ layout.spacing = 4
300
+ add.label('Samples:').minWidth(55)
301
+ add.slider('integer')
302
+ .value(settings.samples$)
303
+ .min(1).max(512)
304
+ .build()
305
+ })
306
+ add.horizontal((layout) => {
307
+ layout.spacing = 4
308
+ add.label('Scale:').minWidth(55)
309
+ add.slider('float')
310
+ .value(settings.scale$)
311
+ .min(0.1).max(5.0)
312
+ .build()
313
+ })
314
+ })
315
+
316
+ add.group('Export').build(() => {
317
+ add.horizontal((layout) => {
318
+ layout.spacing = 4
319
+ add.label('Output:').minWidth(55)
320
+ add.edit()
321
+ .value(settings.outputPath$)
322
+ .placeholder('Default output path...')
323
+ })
324
+ add.horizontal((layout) => {
325
+ layout.spacing = 4
326
+ add.label('Format:').minWidth(55)
327
+ // ComboBox - two-way bind to persisted string
328
+ add.combo()
329
+ .items(['JSON', 'XML', 'CSV'])
330
+ .selected(settings.exportFormat$)
331
+ })
332
+ add.checkbox('Auto Save').value(settings.autoSave$)
333
+ add.label('Auto Save enables notifications and switches the log level to Debug.').wordWrap().build()
334
+ })
335
+ })
336
+ })
337
+
338
+ // Color pickers - read the final color from the widget on accept
339
+ add.group('Colors').horizontal().build(() => {
340
+ add.vertical(() => {
341
+ add.label('Background:').build()
342
+ add.color().build()
343
+ })
344
+ add.vertical(() => {
345
+ add.label('Foreground:').build()
346
+ add.color().build()
347
+ })
348
+ })
349
+
350
+ add.group('Behaviour').horizontal().build(() => {
351
+ add.checkbox('Notifications').value(settings.notifications$)
352
+ add.horizontal((layout) => {
353
+ layout.spacing = 4
354
+ add.label('Log Level:')
355
+ // ComboEdit: user can pick from list OR type a custom level
356
+ add.comboEdit()
357
+ .items(['Debug', 'Info', 'Warning', 'Error'])
358
+ .changed(settings.logLevel$)
359
+ })
360
+ })
361
+ }
362
+
363
+ // Tab 3: About
364
+
365
+ private buildAboutTab(): void {
366
+ let add = this.add
367
+ let model = this.model
368
+ let settings = model.settings
369
+
370
+ add.group('About This Example').build(() => {
371
+ add.label([
372
+ 'DAZScript Framework - Showcase Dialog (Example 06)',
373
+ '',
374
+ 'Widgets demonstrated in this dialog:',
375
+ ' - Tabs with persisted active-tab index (bindInt)',
376
+ ' - Splitter with persisted position (bindString)',
377
+ ' - list.view - columns, keyword filter, custom filters,',
378
+ ' checkbox rows, context menu, refresh trigger, flat/tree toggle,',
379
+ ' expanded, data binding, selection binding',
380
+ ' - list.box - scrollable list with multi-select',
381
+ ' - GroupBox - vertical / horizontal / flat style / visible toggle',
382
+ ' - Label - wordWrap, minWidth, alignment',
383
+ ' - LineEdit - value binding, placeholder, readOnly',
384
+ ' - CheckBox, RadioButton - Observable two-way binding',
385
+ ' - Button - clicked, toggle, toolTip',
386
+ ' - Slider - integer and float with min/max',
387
+ ' - ColorPicker (DzColorWgt)',
388
+ ' - ComboBox - items + selected binding',
389
+ ' - ComboEdit - items + changed/edited binding',
390
+ ' - NodeSelection combo (scene node picker)',
391
+ ' - All settings persisted via AppSettings (DzAppSettings)',
392
+ ].join('\n')).wordWrap(true).build()
393
+ })
394
+
395
+ // Log list - populated externally; demonstrates Observable<string[]> binding
396
+ add.group('Recent Log').build(() => {
397
+ add.list.box()
398
+ .items(model.logEntries$)
399
+ .mode('single')
400
+ .build()
401
+ })
402
+
403
+ // Conditional filter row - group visible when showFilters$ is true
404
+ add.group('Optional Filters').horizontal()
405
+ .visible(settings.showFilters$)
406
+ .build(() => {
407
+ add.label('Tag:').minWidth(30)
408
+ add.comboEdit()
409
+ .items(['render', 'test', 'draft', 'final'])
410
+ .build()
411
+ })
412
+
413
+ add.horizontal((layout) => {
414
+ layout.spacing = 4
415
+ add.checkbox('Show Filters').value(settings.showFilters$)
416
+ })
417
+ }
418
+
419
+ private connectInteractions(): void {
420
+ let model = this.model
421
+ let settings = model.settings
422
+
423
+ settings.filterType$.connect(() => model.refreshObjects$.trigger())
424
+ settings.showVisibleOnly$.connect(() => model.refreshObjects$.trigger())
425
+ settings.showIcons$.connect(() => model.refreshObjects$.trigger())
426
+ settings.flatList$.connect(() => model.refreshObjects$.trigger())
427
+
428
+ settings.autoSave$.connect((enabled) => {
429
+ if (!enabled) return
430
+ settings.notifications$.value = true
431
+ settings.logLevel$.value = 'Debug'
432
+ this.addLog('Auto Save enabled; notifications and Debug logging selected.')
433
+ })
434
+
435
+ model.objects$.connect(() => this.updateSceneStats())
436
+ model.refreshObjects$.connect(() => this.updateSceneStats())
437
+
438
+ model.selectedObject$.connect(obj => {
439
+ model.selectedSummary$.value = obj
440
+ ? `${obj.name}: ${obj.type}, ${obj.visible ? 'visible' : 'hidden'}, ${obj.locked ? 'locked' : 'unlocked'}`
441
+ : 'No object selected'
442
+ })
443
+
444
+ model.detailVisible$.connect((visible) => {
445
+ let obj = model.selectedObject$.value
446
+ if (!obj || obj.visible === visible) return
447
+ obj.visible = visible
448
+ this.addLog(`${obj.name} visibility changed from the detail checkbox.`)
449
+ model.refreshObjects$.trigger()
450
+ })
451
+
452
+ model.detailLocked$.connect((locked) => {
453
+ let obj = model.selectedObject$.value
454
+ if (!obj || obj.locked === locked) return
455
+ obj.locked = locked
456
+ this.addLog(`${obj.name} lock state changed from the detail checkbox.`)
457
+ model.refreshObjects$.trigger()
458
+ })
459
+ }
460
+
461
+ private getDisplayName(obj: SceneObject | null): string {
462
+ if (!obj) return ''
463
+ if (!this.model.settings.showIcons$.value) return obj.name
464
+
465
+ const prefixByType: Record<SceneObject['type'], string> = {
466
+ Figure: '[F]',
467
+ Prop: '[P]',
468
+ Light: '[L]',
469
+ Camera: '[C]',
470
+ Group: '[G]',
471
+ }
472
+
473
+ return `${prefixByType[obj.type]} ${obj.name}`
474
+ }
475
+
476
+ private addLog(message: string): void {
477
+ let entries = this.model.logEntries$.value.slice()
478
+ entries.push(`[Info] ${message}`)
479
+ if (entries.length > 8) entries.shift()
480
+ this.model.logEntries$.value = entries
481
+ }
482
+
483
+ private updateSceneStats(): void {
484
+ let objects = this.getAllObjects()
485
+ let visible = objects.filter(obj => obj.visible).length
486
+ let locked = objects.filter(obj => obj.locked).length
487
+ this.model.sceneStats$.value = `${visible}/${objects.length} visible, ${locked} locked`
488
+ }
489
+
490
+ private getAllObjects(): SceneObject[] {
491
+ let objects: SceneObject[] = []
492
+ this.model.objects$.value.forEach(root => {
493
+ root.forEach(node => {
494
+ if (node.value) objects.push(node.value)
495
+ })
496
+ })
497
+ return objects
498
+ }
499
+
500
+ private applyCheckedRows(): void {
501
+ if (!this.sceneListView) return
502
+
503
+ this.sceneListView.getItems(DzListView.All).forEach(item => {
504
+ if (!(item as any).inherits('DzCheckListItem')) return
505
+
506
+ let obj = getDataItem<SceneObject>(item)
507
+ if (!obj) return
508
+
509
+ let checkbox = item as DzCheckListItem
510
+ obj.visible = checkbox.state === 0 && !checkbox.on
511
+ ? false
512
+ : checkbox.on
513
+ })
514
+
515
+ this.addLog('List checkbox states applied to scene objects.')
516
+ this.model.refreshObjects$.trigger()
517
+ }
518
+ }