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(§ion.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(§ion.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}