hgame 0.26.4

CG production management structs, e.g. of assets, personnels, progress, etc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
use super::*;
#[cfg(feature = "gui")]
use crate::link::*;
#[cfg(all(feature = "image_processing", feature = "gui"))]
use crate::ImageSpec;

use crossbeam_channel::Sender;
use glob::Paths;
use mkutil::finder::*;
use std::ffi::OsStr;

#[cfg(feature = "image_processing")]
const MAX_READ_IMAGES_TIMES: u8 = 3;
const SEND_ERR: &str = "Failed to send InPlaceSeqDisplay via channel";

#[derive(Debug, Clone)]
#[cfg(feature = "image_processing")]
/// Embedded images with a slider that allows scrubbing for individual images.
pub(super) struct ImagesScrub {
    /// Usage, or creation type, of images, used for slider prefix.
    typ: SequenceType,

    #[cfg(all(feature = "image_processing", feature = "gui"))]
    /// Parsing images can fail, so we're storing `Option<ImageSpec>`.
    /// This vector mirrors a to-be-given vector of image paths.
    pub(super) img_specs: Option<Vec<Option<ImageSpec>>>,

    /// Index of the `Vec<Option<ImageSpec>>` plus one, used as position on a slider.
    pos: usize,
}

#[cfg(feature = "image_processing")]
impl Default for ImagesScrub {
    /// IMPORTANT: `Self::pos` must be defaulted to 1.
    fn default() -> Self {
        Self {
            typ: Default::default(),
            #[cfg(all(feature = "image_processing", feature = "gui"))]
            img_specs: None,
            pos: 1,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct InPlaceSequence {
    /// Usually folder name, used for section display.
    name: String,

    /// The directory hosting the image sequence.
    location_dir: Option<PathBuf>,

    /// Paths of every image in the sequence.
    pub(super) img_paths: Option<Vec<PathBuf>>,

    #[cfg(feature = "image_processing")]
    pub(super) scrub: ImagesScrub,

    /// Used to switch from info message to error message after a  
    /// certain number of times having looked into the `Self::location_dir` for images.
    times_processed: u8,
}

impl PartialEq for InPlaceSequence {
    fn eq(&self, other: &Self) -> bool {
        self.img_paths == other.img_paths
    }
}

impl InPlaceSequence {
    /// `Unglobbed` means `Self::img_paths` will still `is_none`,
    /// since we'll only glob for images upon request.
    fn unglobbed<P>(parent_dir: PathBuf, name: String, intermediate_dirs: Option<P>) -> Self
    where
        P: AsRef<Path>,
    {
        Self {
            location_dir: match intermediate_dirs {
                Some(intermediate) => Some(parent_dir.join(intermediate)),
                None => Some(parent_dir),
            },
            name,
            ..Default::default()
        }
    }

    fn with_img_paths(mut self, img_paths: Option<Vec<PathBuf>>) -> Self {
        self.img_paths = img_paths;
        self
    }

    #[cfg(feature = "image_processing")]
    fn sequence_type(mut self, typ: Option<&SequenceType>) -> Self {
        if let Some(typ) = typ {
            self.scrub.typ = typ.clone();
        };
        self
    }

    fn read_location_dir(&mut self) {
        // keeping track of how many times we have read the folder
        self.times_processed += 1;

        if self.img_paths.is_some() {
            // NOTE: this aims to init only once, but empty folder will escape
            // this condition and inits multiple times.
            return;
        };

        if let Some(location_dir) = &self.location_dir {
            self.img_paths = list_images(location_dir, true).ok();
        };
    }

    /// Intended for `InPlaceSeqDisplay::NotEmbedded(_)`, as external player
    /// like DJV only expects a path to the first image in the sequence.
    fn drain_all_except_first_image(&mut self) {
        self.read_location_dir();

        if let Some(images) = self.img_paths.as_mut() {
            if let Ordering::Greater = images.len().cmp(&1) {
                images.drain(1..);
            };
        };
    }

    #[cfg(all(feature = "image_processing", feature = "gui"))]
    /// Generates `RetainedImage`s from `Self::img_paths`.
    pub(super) fn img_specs_mut(&mut self) {
        self.scrub.img_specs = self.img_paths.as_ref().map(|paths| {
            paths
                .iter()
                .map(|i| ImageSpec::from_retained_image(i))
                .collect()
        });
    }

    #[cfg(all(feature = "image_processing", feature = "gui"))]
    /// Makes `TextureId`s from `Self::img_paths` which are the results of reading
    /// the `Self::location_dir`.
    fn img_specs_mut_from_location_dir(&mut self) {
        self.read_location_dir();
        self.img_specs_mut();
    }
}

#[cfg(feature = "gui")]
impl InPlaceSequence {
    #[cfg(feature = "image_processing")]
    fn show_make_texture_ids_button(&mut self, ui: &mut egui::Ui) {
        if ui
            .button("🔳 Read Images")
            .on_hover_text(format!("Load images for {}", self.name))
            .clicked()
        {
            self.img_specs_mut_from_location_dir();
        };
    }

    /// A "Peek" button that when clicked drains all image paths except the first one.
    fn show_peek_button(&mut self, ui: &mut egui::Ui) {
        let needs_peeking = self.img_paths.is_none();

        if ui
            .add_enabled(needs_peeking, egui::Button::new("👓 Peek"))
            .on_hover_text("Prepare for external image player")
            .clicked()
        {
            self.drain_all_except_first_image();
        };
    }

    #[cfg(feature = "image_processing")]
    pub(super) fn show_img_embed_and_pos_slider(
        &mut self,
        ui: &mut egui::Ui,
        thumb_width: f32,
    ) -> Option<egui::Response> {
        let Self {
            location_dir: _,
            name: _,
            img_paths,
            scrub,
            times_processed,
        } = self;

        match scrub.img_specs.as_mut() {
            None => None,
            Some(specs) => {
                // SAFETY: `Self::img_paths` should already `is_some` in this block,
                // since `Self::embed::img_specs` can only be initialized via `Self::make_texture_ids`
                let path_spec_pairs: Vec<(&PathBuf, &ImageSpec)> = img_paths
                    .as_ref()
                    .unwrap()
                    .iter()
                    .zip(specs.iter())
                    .filter(|(_, spec)| spec.is_some())
                    .map(|(path, spec)| (path, spec.as_ref().unwrap()))
                    .collect();

                let seq_len = path_spec_pairs.len();

                match seq_len.cmp(&0) {
                    Ordering::Greater => {
                        let mut response: Option<egui::Response> =
                            // slider should be added first, as heights of images will change
                            if let Ordering::Greater = seq_len.cmp(&1) {
                                // only shows the slider if len > 1
                                Some(
                                    ui.add(egui::Slider::new(&mut scrub.pos, 1..=seq_len)
                                    .prefix(scrub.typ.as_ref())
                                    .suffix(format!("/{}", seq_len))
                                    .integer())
                                )
                            } else {
                                None
                            };

                        if let Some((path, img)) = path_spec_pairs.get(scrub.pos - 1) {
                            let img_response =
                                ui.add(ImageLink::with_retained_image_actual_height(
                                    ui.ctx(),
                                    img.inner(),
                                    Some(path),
                                    thumb_width,
                                ));
                            if response.is_none() {
                                // for case of a single image and no slider
                                response = Some(img_response);
                            };
                        };

                        response
                    }
                    _ => Some(show_loading_hint(ui, times_processed)),
                }
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
/// Essentially a series of `egui::ImageButton`s whose "position"
/// -- the current index -- is controlled by a `egui::Slider`.
pub enum InPlaceSeqDisplay {
    Embedded(InPlaceSequence),

    /// For large image sequences, `Self::make_texture_ids` takes very long to process,
    /// in such case users should view the sequences with external players only.
    NotEmbedded(InPlaceSequence),
}

impl InPlaceSeqDisplay {
    pub(super) fn inner(&self) -> &InPlaceSequence {
        match self {
            Self::Embedded(inner) | Self::NotEmbedded(inner) => inner,
        }
    }

    pub(super) fn inner_mut(&mut self) -> &mut InPlaceSequence {
        match self {
            Self::Embedded(inner) | Self::NotEmbedded(inner) => inner,
        }
    }

    pub(super) fn with_img_paths(self, img_paths: Option<Vec<PathBuf>>) -> Self {
        match self {
            Self::Embedded(inner) => Self::Embedded(inner.with_img_paths(img_paths)),
            Self::NotEmbedded(inner) => Self::NotEmbedded(inner.with_img_paths(img_paths)),
        }
    }

    fn name(&self) -> &String {
        &self.inner().name
    }

    /// Iterates over the given `glob::Paths` and read sub-directories -- with optional
    /// intermediate directories -- for [`InPlaceSeqDisplay`], but does NOT list any images,
    /// and finally sends result over the given channel.
    pub fn glob_over_paths(
        paths: Paths,
        intermediate_dirs: Option<PathBuf>,
        should_embed: bool,
        _typ: Option<SequenceType>,
        tx: Sender<GlobbedSequences>,
    ) {
        std::thread::spawn(move || {
            let mut sequences: Vec<Self> = vec![];

            // filters from `GlobResult`s
            let dir_paths = paths
                .into_iter()
                .filter_map(|p| p.ok())
                // avoids `.DS_Store` files on MacOS or some `desktop.ini` files on Windows
                // before subsequent `Path::read_dir`
                .filter(|p| p.is_dir())
                // avoids empty folders
                .filter(|p| {
                    p.read_dir()
                        .expect(&format!("Failed to read dir: {}", p.display()))
                        .next()
                        .is_some()
                });

            for p in dir_paths {
                let name = p
                    .file_name()
                    .unwrap_or(OsStr::new("😷 broken folder name"))
                    .to_string_lossy()
                    .to_string();

                #[allow(unused_mut)]
                // NOTE: we accept the cost of re-matching in each loop in favor of the
                // simpler handling for the `InPlaceSeqDisplay::name`.
                let mut seq = InPlaceSequence::unglobbed(p, name, intermediate_dirs.as_ref());

                #[cfg(feature = "image_processing")]
                {
                    seq = seq.sequence_type(_typ.as_ref());
                }

                // shadowing
                let seq = match should_embed {
                    true => InPlaceSeqDisplay::Embedded(seq),
                    false => InPlaceSeqDisplay::NotEmbedded(seq),
                };

                sequences.push(seq);
            }

            match sequences.is_empty() {
                false => {
                    if let Err(e) = tx.send(GlobbedSequences::ok(sequences)) {
                        error!("{}: {}", SEND_ERR, e);
                    };
                }
                true => {
                    if let Err(e) = tx.send(GlobbedSequences::empty()) {
                        error!("{}: {}", SEND_ERR, e);
                    };
                }
            }
        });
    }
}

#[cfg(feature = "gui")]
impl InPlaceSeqDisplay {
    /// Shows a row of "Read Images" or "Peek" buttons depending on whether
    /// it's embed and non-embed.
    pub fn show_load_image_buttons(&mut self, ui: &mut egui::Ui) -> Option<egui::Response> {
        ui.horizontal(|ui| {
            // reads `ImageSpec`s or drains after peeking
            match self {
                Self::Embedded(_seq) => {
                    #[cfg(feature = "image_processing")]
                    // only showing "Read Images" button for embedded type
                    _seq.show_make_texture_ids_button(ui);
                }
                Self::NotEmbedded(seq) => {
                    // SAFETY: `Self::img_paths` can still be `is_none` by this point.
                    seq.show_peek_button(ui);
                }
            };

            // open DJV
            let external_player_response = self.inner().img_paths.as_ref().map(|paths| {
                ui.add(TurntableLink::with_player("🎥 djv", || {
                    if let Err(e) = open_djv(paths) {
                        error!("Failed to open with DJV: {}", e);
                    };
                }))
            });

            // open folder 🗁 📂
            if let Some(location_dir) = &self.inner().location_dir {
                ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
                    ui.add(FinderLink::with_url(Some("📁"), location_dir));
                });
            };

            external_player_response
        })
        .inner
    }

    /// Returns different `egui::Response` depending on whether it's embedded or non-embedded.
    fn ui(&mut self, ui: &mut egui::Ui, _thumb_width: f32) -> Option<egui::Response> {
        let external_player_response = self.show_load_image_buttons(ui);

        match self {
            InPlaceSeqDisplay::NotEmbedded(_) => {
                // we return `Response` from the external player button
                external_player_response
            }
            InPlaceSeqDisplay::Embedded(_) => {
                #[cfg(feature = "image_processing")]
                return {
                    // we're embedding images, and return `Response` from the image pos slider
                    self.inner_mut()
                        .show_img_embed_and_pos_slider(ui, _thumb_width)
                };

                #[cfg(not(feature = "image_processing"))]
                None
            }
        }
    }

    pub fn collapsing_header_ui(
        &mut self,
        ui: &mut egui::Ui,
        thumb_width: f32,
    ) -> Option<Option<egui::Response>> {
        egui::CollapsingHeader::new(self.name())
            .default_open(false)
            .show(ui, |ui| self.ui(ui, thumb_width))
            .body_returned
    }
}

// -------------------------------------------------------------------------------
#[derive(Debug)]
/// Group of image sequences that are located in a same parent directory.
/// Not using `anyhow::Error` so that we can send glob job to another thread.
pub struct GlobbedSequences(pub Result<Vec<InPlaceSeqDisplay>, String>);

impl GlobbedSequences {
    pub fn uninitialized() -> Self {
        Self(Err("⚠ Uninitialized".to_owned()))
    }

    fn ok(sequences: Vec<InPlaceSeqDisplay>) -> Self {
        Self(Ok(sequences))
    }

    /// Holds error which reports that no `GlobbedSequences` constructed successfully.
    fn empty() -> Self {
        Self(Err(
            "Folder is empty, or inaccessible, or doesn't exist".to_owned()
        ))
    }
}

impl From<&anyhow::Error> for GlobbedSequences {
    fn from(error: &anyhow::Error) -> Self {
        Self(Err(format!("{}", error)))
    }
}

// -------------------------------------------------------------------------------
#[allow(dead_code)]
/// Tries DJV2 first, then tries DJV1.
fn open_djv(sequence: &[PathBuf]) -> AnyResult<()> {
    let cfg = ClientCfgCel::path_config().as_ref()?;
    // passes the first file
    if !sequence.is_empty() {
        let seq_start = sequence.get(0).context("Failed to get first image path")?;

        // tries multiple versions of DJV
        try_execute_file(&cfg.exe().djv(), seq_start).map_err(|e| anyhow!("{}", e))
    } else {
        Err(anyhow!("Image sequence is empty"))
    }
}

#[cfg(all(feature = "image_processing", feature = "gui"))]
fn show_loading_hint(ui: &mut egui::Ui, times_processed: &u8) -> egui::Response {
    ui.monospace(match times_processed.cmp(&MAX_READ_IMAGES_TIMES) {
        Ordering::Less => {
            // "...still loading, please wait a moment then press \"🔳 Read Images\" again..."
            "Unable to load images"
        }
        _ => "😷 Folder contains no images",
    })
}