Skip to main content

pfx_editor/
outline.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use pfx_load::scene::tiles::{Tiles, cell_of};
5use pfx_load::scene::{Scene, Target};
6use pfx_play::Tunables;
7use pfx_project::Project;
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum Item {
11    Object(String),
12    Mesh(String),
13    Node { mesh: String, node: String },
14    Light(String),
15    Emitter(String),
16    Material(String),
17    Sound(String),
18    Tunables(String),
19    File(PathBuf),
20    Scene(PathBuf),
21    Folder(PathBuf),
22    Asset(PathBuf),
23    Layer(String),
24    Sun,
25    Sky,
26    Haze,
27    Camera,
28    Finish,
29}
30
31impl Item {
32    pub fn target(&self) -> Option<Target> {
33        Some(match self {
34            Item::Object(name) => Target::Object(name.clone()),
35            Item::Mesh(name) => Target::Mesh(name.clone()),
36            Item::Node { mesh, node } => Target::Node {
37                mesh: mesh.clone(),
38                node: node.clone(),
39            },
40            Item::Light(name) => Target::Light(name.clone()),
41            Item::Emitter(name) => Target::Emitter(name.clone()),
42            Item::Material(name) => Target::Material(name.clone()),
43            Item::Sound(name) => Target::Sound(name.clone()),
44            Item::Sun => Target::Sun,
45            Item::Sky => Target::Sky,
46            Item::Haze => Target::Haze,
47            Item::Camera => Target::Camera,
48            Item::Finish => Target::Finish,
49            Item::File(_)
50            | Item::Tunables(_)
51            | Item::Scene(_)
52            | Item::Folder(_)
53            | Item::Asset(_)
54            | Item::Layer(_) => return None,
55        })
56    }
57
58    pub fn kind(&self) -> &'static str {
59        match self {
60            Item::Object(_) => "object",
61            Item::Mesh(_) => "mesh",
62            Item::Node { .. } => "node",
63            Item::Light(_) => "light",
64            Item::Emitter(_) => "emitter",
65            Item::Material(_) => "material",
66            Item::Sound(_) => "sound",
67            Item::Tunables(_) => "tunables",
68            Item::File(_) => "file",
69            Item::Scene(_) => "scene",
70            Item::Folder(_) => "folder",
71            Item::Asset(_) => "asset",
72            Item::Layer(_) => "layer",
73            Item::Sun => "sun",
74            Item::Sky => "sky",
75            Item::Haze => "haze",
76            Item::Camera => "camera",
77            Item::Finish => "finish",
78        }
79    }
80
81    pub fn name(&self) -> String {
82        match self {
83            Item::Object(name)
84            | Item::Mesh(name)
85            | Item::Light(name)
86            | Item::Emitter(name)
87            | Item::Material(name)
88            | Item::Sound(name)
89            | Item::Tunables(name)
90            | Item::Layer(name) => name.clone(),
91            Item::Node { mesh, node } => format!("{mesh}/{node}"),
92            Item::File(path) | Item::Scene(path) | Item::Folder(path) | Item::Asset(path) => path
93                .file_name()
94                .map(|name| name.to_string_lossy().into_owned())
95                .unwrap_or_default(),
96            Item::Sun | Item::Sky | Item::Haze | Item::Camera | Item::Finish => String::new(),
97        }
98    }
99
100    pub fn key(&self) -> String {
101        let name = self.name();
102        if name.is_empty() {
103            self.kind().to_string()
104        } else {
105            format!("{}:{name}", self.kind())
106        }
107    }
108
109    pub fn exists(&self, scene: &Scene) -> bool {
110        match self {
111            Item::Object(name) => scene.object(name).is_some(),
112            Item::Mesh(name) => scene.meshes.contains_key(name),
113            Item::Node { mesh, node } => scene
114                .meshes
115                .get(mesh)
116                .is_some_and(|found| found.parts.iter().any(|part| &part.node == node)),
117            Item::Light(name) => scene.lights.contains_key(name),
118            Item::Emitter(name) => scene.emitters.contains_key(name),
119            Item::Material(name) => scene.library.contains(name),
120            Item::Sound(name) => scene.sounds.contains_key(name),
121            Item::Tunables(_) => true,
122            Item::File(path) => scene.files.contains_key(path),
123            Item::Scene(_) | Item::Folder(_) | Item::Asset(_) | Item::Layer(_) => true,
124            Item::Sun | Item::Sky | Item::Haze | Item::Camera | Item::Finish => true,
125        }
126    }
127}
128
129#[derive(Clone, Debug, PartialEq)]
130pub struct Node {
131    pub item: Item,
132    pub label: String,
133    pub children: Vec<Node>,
134}
135
136#[derive(Clone, Debug, PartialEq)]
137pub struct Section {
138    pub title: &'static str,
139    pub nodes: Vec<Node>,
140}
141
142#[derive(Clone, Debug, Default, PartialEq)]
143pub struct Outline {
144    pub sections: Vec<Section>,
145}
146
147fn leaf(item: Item, label: impl Into<String>) -> Node {
148    Node {
149        item,
150        label: label.into(),
151        children: Vec::new(),
152    }
153}
154
155fn object_tree(scene: &Scene) -> Vec<Node> {
156    let mut children: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
157    let mut roots = Vec::new();
158    for object in &scene.objects {
159        match &object.parent {
160            Some(parent) if scene.object(parent).is_some() => {
161                children.entry(parent).or_default().push(&object.name)
162            }
163            _ => roots.push(object.name.as_str()),
164        }
165    }
166    fn grow(name: &str, children: &BTreeMap<&str, Vec<&str>>) -> Node {
167        Node {
168            item: Item::Object(name.to_string()),
169            label: name.to_string(),
170            children: children
171                .get(name)
172                .map(|names| names.iter().map(|child| grow(child, children)).collect())
173                .unwrap_or_default(),
174        }
175    }
176    roots
177        .into_iter()
178        .map(|name| grow(name, &children))
179        .collect()
180}
181
182fn mesh_nodes(scene: &Scene) -> Vec<Node> {
183    scene
184        .meshes
185        .iter()
186        .map(|(name, mesh)| {
187            let mut nodes: Vec<&str> = Vec::new();
188            for part in &mesh.parts {
189                if !nodes.contains(&part.node.as_str()) {
190                    nodes.push(&part.node);
191                }
192            }
193            Node {
194                item: Item::Mesh(name.clone()),
195                label: name.clone(),
196                children: nodes
197                    .into_iter()
198                    .map(|node| {
199                        leaf(
200                            Item::Node {
201                                mesh: name.clone(),
202                                node: node.to_string(),
203                            },
204                            node,
205                        )
206                    })
207                    .collect(),
208            }
209        })
210        .collect()
211}
212
213pub fn relative(root: &Path, file: &Path) -> String {
214    let base = root.parent().unwrap_or(Path::new(""));
215    file.strip_prefix(base)
216        .unwrap_or(file)
217        .to_string_lossy()
218        .replace('\\', "/")
219}
220
221impl Outline {
222    pub fn of(scene: &Scene, files: &[PathBuf], tunables: &Tunables) -> Outline {
223        let lights = scene
224            .lights
225            .keys()
226            .map(|name| leaf(Item::Light(name.clone()), name))
227            .chain(
228                scene
229                    .emitters
230                    .keys()
231                    .map(|name| leaf(Item::Emitter(name.clone()), name)),
232            )
233            .collect();
234        let materials = scene
235            .library
236            .names()
237            .map(|name| leaf(Item::Material(name.to_string()), name))
238            .collect();
239        let world = vec![
240            leaf(Item::Sun, "sun"),
241            leaf(Item::Sky, "sky"),
242            leaf(Item::Haze, "haze"),
243            leaf(Item::Camera, "camera"),
244            leaf(Item::Finish, "finish"),
245        ];
246        let files = files
247            .iter()
248            .map(|file| leaf(Item::File(file.clone()), relative(&scene.path, file)))
249            .collect();
250        let sounds: Vec<Node> = scene
251            .sounds
252            .keys()
253            .map(|name| leaf(Item::Sound(name.clone()), name))
254            .collect();
255        let groups: Vec<Node> = tunables
256            .groups()
257            .into_iter()
258            .map(|(group, _)| {
259                let label = if group.is_empty() { "general" } else { group };
260                leaf(Item::Tunables(group.to_string()), label)
261            })
262            .collect();
263        let mut outline = Outline {
264            sections: vec![
265                Section {
266                    title: "objects",
267                    nodes: object_tree(scene),
268                },
269                Section {
270                    title: "meshes",
271                    nodes: mesh_nodes(scene),
272                },
273                Section {
274                    title: "lights",
275                    nodes: lights,
276                },
277                Section {
278                    title: "materials",
279                    nodes: materials,
280                },
281                Section {
282                    title: "world",
283                    nodes: world,
284                },
285                Section {
286                    title: "files",
287                    nodes: files,
288                },
289            ],
290        };
291        for (after, title, nodes) in [("lights", "sounds", sounds), ("world", "tunables", groups)] {
292            if nodes.is_empty() {
293                continue;
294            }
295            let at = outline
296                .sections
297                .iter()
298                .position(|section| section.title == after)
299                .map_or(outline.sections.len(), |at| at + 1);
300            outline.sections.insert(at, Section { title, nodes });
301        }
302        outline
303    }
304
305    pub fn add_project(&mut self, project: &Project, open: &Path) {
306        let scenes = project
307            .scenes()
308            .into_iter()
309            .map(|scene| {
310                let mut label = project.relative(&scene);
311                if scene == open {
312                    label.push_str("  ·  open");
313                }
314                leaf(Item::Scene(scene), label)
315            })
316            .collect();
317        let content = project
318            .folders()
319            .into_iter()
320            .map(|folder| Node {
321                label: match project.relative(&folder.path) {
322                    relative if relative.is_empty() => ".".to_string(),
323                    relative => relative,
324                },
325                children: folder
326                    .files
327                    .iter()
328                    .map(|file| {
329                        let name = file
330                            .file_name()
331                            .map(|name| name.to_string_lossy().into_owned())
332                            .unwrap_or_default();
333                        leaf(Item::Asset(file.clone()), name)
334                    })
335                    .collect(),
336                item: Item::Folder(folder.path),
337            })
338            .collect();
339        self.sections.splice(
340            0..0,
341            [
342                Section {
343                    title: "scenes",
344                    nodes: scenes,
345                },
346                Section {
347                    title: "content",
348                    nodes: content,
349                },
350            ],
351        );
352    }
353
354    pub fn add_tiles(&mut self, tiles: &[Tiles]) {
355        let layers: Vec<&str> = tiles.iter().map(|layer| layer.name.as_str()).collect();
356        if let Some(objects) = self
357            .sections
358            .iter_mut()
359            .find(|section| section.title == "objects")
360        {
361            objects.nodes.retain(|node| match &node.item {
362                Item::Object(name) => {
363                    cell_of(name).is_none_or(|(layer, _)| !layers.contains(&layer.as_str()))
364                }
365                _ => true,
366            });
367        }
368        if tiles.is_empty() {
369            return;
370        }
371        let nodes = tiles
372            .iter()
373            .map(|layer| {
374                let count = layer.cells().len();
375                leaf(
376                    Item::Layer(layer.name.clone()),
377                    format!(
378                        "{}  ·  {count} {}",
379                        layer.name,
380                        if count == 1 { "cell" } else { "cells" }
381                    ),
382                )
383            })
384            .collect();
385        let at = self
386            .sections
387            .iter()
388            .position(|section| section.title == "objects")
389            .map_or(0, |at| at + 1);
390        self.sections.insert(
391            at,
392            Section {
393                title: "tiles",
394                nodes,
395            },
396        );
397    }
398
399    pub fn items(&self) -> Vec<&Item> {
400        fn walk<'a>(nodes: &'a [Node], into: &mut Vec<&'a Item>) {
401            for node in nodes {
402                into.push(&node.item);
403                walk(&node.children, into);
404            }
405        }
406        let mut items = Vec::new();
407        for section in &self.sections {
408            walk(&section.nodes, &mut items);
409        }
410        items
411    }
412
413    pub fn contains(&self, item: &Item) -> bool {
414        self.items().contains(&item)
415    }
416
417    pub fn section(&self, title: &str) -> Option<&Section> {
418        self.sections.iter().find(|section| section.title == title)
419    }
420
421    pub fn path_to(&self, item: &Item) -> Vec<Item> {
422        fn find(nodes: &[Node], item: &Item, trail: &mut Vec<Item>) -> bool {
423            for node in nodes {
424                trail.push(node.item.clone());
425                if &node.item == item || find(&node.children, item, trail) {
426                    return true;
427                }
428                trail.pop();
429            }
430            false
431        }
432        for section in &self.sections {
433            let mut trail = Vec::new();
434            if find(&section.nodes, item, &mut trail) {
435                return trail;
436            }
437        }
438        Vec::new()
439    }
440
441    pub fn step(&self, from: Option<&Item>, by: isize) -> Option<Item> {
442        let items = self.items();
443        if items.is_empty() {
444            return None;
445        }
446        let at = from
447            .and_then(|item| items.iter().position(|candidate| *candidate == item))
448            .map(|at| at as isize + by)
449            .unwrap_or(if by < 0 { items.len() as isize - 1 } else { 0 });
450        let at = at.clamp(0, items.len() as isize - 1) as usize;
451        Some(items[at].clone())
452    }
453}