1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use pfx_editor_doc::{Changed, Disk, Files, History};
5use pfx_editor_toml::{Diagnostic, Source};
6use pfx_editor_viewport::{Event, ScenePatch, Viewport};
7use pfx_load::scene::{Patch, PatchGroup, Scene, SceneEdit, Value};
8use pfx_play::{Edit, Pending, PlaySession, Tunables};
9
10use crate::check::{SceneCheck, worded};
11use crate::inspect::{self, Change, Raw, Row};
12use crate::level::{Level, labelled};
13use crate::log::Log;
14use crate::outline::{Item, Outline};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Play {
18 Stopped,
19 Playing,
20 Paused,
21}
22
23#[derive(Clone, Debug, PartialEq)]
24pub enum Request {
25 Play,
26 Pause,
27 Resume,
28 Step,
29 Stop,
30 Record,
31 Open(PathBuf),
32 Keep,
33 World(Change),
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Group {
38 Begin,
39 Keep,
40 End,
41}
42
43#[derive(Clone, Debug, Default, PartialEq)]
44pub struct Layout {
45 pub outline: BTreeMap<Item, egui::Rect>,
46 pub rows: BTreeMap<String, egui::Rect>,
47 pub buttons: BTreeMap<&'static str, egui::Rect>,
48 pub viewport: Option<egui::Rect>,
49 pub source: Option<egui::Rect>,
50 pub version: Option<egui::Rect>,
51}
52
53impl Layout {
54 pub fn find(&self, name: &str) -> Option<[f32; 2]> {
55 let centre = |rect: &egui::Rect| [rect.center().x, rect.center().y];
56 if let Some(rest) = name.strip_prefix("row:") {
57 return self.rows.get(rest).map(centre);
58 }
59 if let Some(rest) = name.strip_prefix("button:") {
60 return self.buttons.get(rest).map(centre);
61 }
62 for (prefix, rect) in [("viewport", self.viewport), ("source", self.source)] {
63 let Some(rest) = name.strip_prefix(prefix) else {
64 continue;
65 };
66 let rect = rect?;
67 let rest = rest.trim_start_matches(':');
68 if rest.is_empty() {
69 return Some(centre(&rect));
70 }
71 let (x, y) = rest.split_once(',')?;
72 let (x, y) = (x.parse::<f32>().ok()?, y.parse::<f32>().ok()?);
73 return Some([
74 rect.left() + rect.width() * x,
75 rect.top() + rect.height() * y,
76 ]);
77 }
78 self.outline
79 .iter()
80 .find(|(item, _)| item.key() == name)
81 .map(|(_, rect)| centre(rect))
82 }
83}
84
85#[derive(Clone, Debug, PartialEq)]
86struct Held {
87 label: String,
88 start: BTreeMap<PathBuf, String>,
89}
90
91pub struct Editor {
92 pub edit: SceneEdit,
93 pub files: Files,
94 pub history: History,
95 pub viewport: Box<Viewport>,
96 pub sources: BTreeMap<PathBuf, Source>,
97 pub problems: Vec<Diagnostic>,
98 pub outline: Outline,
99 pub raw: Raw,
100 pub rows: Vec<Row>,
101 pub selection: Option<Item>,
102 pub trace: bool,
103 pub trace_samples: u32,
104 pub play: Play,
105 pub log: Log,
106 pub app: String,
107 pub pgpu: Option<String>,
108 pub muted: bool,
109 pub sound_available: bool,
110 pub sound: Option<String>,
111 pub screen: egui::Rect,
112 pub texture: Option<egui::TextureId>,
113 pub revision: u64,
114 pub layout: Layout,
115 pub texts: BTreeMap<String, String>,
116 pub saves: Vec<PathBuf>,
117 pub outside: bool,
118 pub project: Option<pfx_project::Project>,
119 pub tunables: Tunables,
120 pub live: BTreeMap<String, Value>,
121 pub level: Level,
122 tunables_error: Option<String>,
123 held: Option<Held>,
124 requests: Vec<Request>,
125}
126
127pub fn frame_aspect(
128 project: &pfx_project::Project,
129 device: pfx_gpu::screens::Device,
130) -> Result<Option<f32>, String> {
131 let Some(table) = project.screen() else {
132 return Ok(None);
133 };
134 let policy = pfx_gpu::screens::ScreenPolicy::from_table(table)
135 .map_err(|error| format!("project.toml: [authoring.pfx.screen]: {error}"))?;
136 Ok(policy
137 .aspects(device)
138 .first()
139 .map(|aspect| aspect.ratio() as f32))
140}
141
142pub fn version_line(name: &str, version: &str) -> String {
143 format!("{name} v{}", version.trim_start_matches('v'))
144}
145
146fn absolute(path: &Path) -> PathBuf {
147 std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
148}
149
150impl Editor {
151 pub fn open(path: impl AsRef<Path>) -> Result<Editor, String> {
152 let path = absolute(path.as_ref());
153 let edit = SceneEdit::open(&path).map_err(|error| error.to_string())?;
154 let mut files = Files::new();
155 for file in edit.files() {
156 files.load(file).map_err(|error| error.to_string())?;
157 }
158 let viewport = Box::new(Viewport::open("pfx edit viewport", &path));
159 let mut editor = Editor {
160 outline: Outline::default(),
161 raw: Raw::default(),
162 rows: Vec::new(),
163 edit,
164 files,
165 history: History::default(),
166 viewport,
167 sources: BTreeMap::new(),
168 problems: Vec::new(),
169 selection: None,
170 trace: false,
171 trace_samples: 0,
172 play: Play::Stopped,
173 log: Log::default(),
174 app: version_line("pfx", env!("CARGO_PKG_VERSION")),
175 pgpu: None,
176 muted: false,
177 sound_available: false,
178 sound: None,
179 screen: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1.0, 1.0)),
180 texture: None,
181 revision: 0,
182 layout: Layout::default(),
183 texts: BTreeMap::new(),
184 saves: Vec::new(),
185 outside: false,
186 project: None,
187 tunables: Tunables::default(),
188 live: BTreeMap::new(),
189 level: Level::new(),
190 tunables_error: None,
191 held: None,
192 requests: Vec::new(),
193 };
194 editor.sync_files();
195 editor.refresh();
196 editor.revision = 0;
197 let name = editor.name();
198 editor.log.info(format!(
199 "opened {name}: {} objects, {} materials, {} lights",
200 editor.scene().objects.len(),
201 editor.scene().library.len(),
202 editor.scene().lights.len() + editor.scene().emitters.len()
203 ));
204 let root = editor.path().to_path_buf();
205 for problem in editor.problems.clone() {
206 editor.log.warn(worded(&problem, &root));
207 }
208 Ok(editor)
209 }
210
211 pub fn scene(&self) -> &Scene {
212 self.edit.scene()
213 }
214
215 pub fn path(&self) -> &Path {
216 self.edit.path()
217 }
218
219 pub fn project_root(&self) -> &Path {
220 self.edit.project().root()
221 }
222
223 pub fn name(&self) -> String {
224 self.path()
225 .file_name()
226 .map(|name| name.to_string_lossy().into_owned())
227 .unwrap_or_default()
228 }
229
230 pub fn files(&self) -> Vec<PathBuf> {
231 let mut files: Vec<PathBuf> = self.edit.files().map(Path::to_path_buf).collect();
232 if let Some(file) = Tunables::find(self.project_root())
233 && !files.contains(&file)
234 {
235 files.push(file);
236 }
237 files
238 }
239
240 fn load_tunables(&mut self) {
241 let loaded = match Tunables::find(self.project_root()) {
242 Some(file) => match self.files.text(&file) {
243 Some(text) => Tunables::parse(text, &file),
244 None => Tunables::open(&file),
245 },
246 None => Ok(Tunables::default()),
247 };
248 match loaded {
249 Ok(tunables) => {
250 self.tunables = tunables;
251 self.tunables_error = None;
252 }
253 Err(error) => {
254 if self.tunables_error.as_ref() != Some(&error) {
255 self.log.error(format!("tunables: {error}"));
256 }
257 self.tunables_error = Some(error);
258 }
259 }
260 }
261
262 fn rows_of(&self, item: &Item) -> Vec<Row> {
263 let mut rows = inspect::rows(self.edit.scene(), &self.raw, &self.tunables, item);
264 inspect::overlay(&mut rows, &self.live);
265 rows
266 }
267
268 pub fn refresh(&mut self) {
269 self.load_tunables();
270 self.outline = Outline::of(self.edit.scene(), &self.files(), &self.tunables);
271 if let Some(project) = &self.project {
272 self.outline.add_project(project, self.edit.path());
273 }
274 let layers = self.layers();
275 self.outline.add_tiles(&layers);
276 let scene = self.edit.scene();
277 self.level
278 .picked
279 .retain(|name| scene.object(name).is_some());
280 self.raw = Raw::of(&self.edit);
281 self.problems = SceneCheck::new(self.edit.project()).all();
282 if let Some(item) = &self.selection
283 && !item.exists(self.edit.scene())
284 {
285 self.selection = None;
286 }
287 self.reload_rows();
288 self.texts.clear();
289 self.revision += 1;
290 }
291
292 pub fn select(&mut self, item: Option<Item>) {
293 if self.selection == item {
294 return;
295 }
296 self.end_group();
297 self.selection = item;
298 self.level.picked.clear();
299 if let Some(Item::Asset(asset)) = &self.selection
300 && crate::level::placeable(asset)
301 {
302 self.level.stamp = Some(asset.clone());
303 }
304 if let Some(Item::Layer(layer)) = &self.selection {
305 self.level.brush.layer = Some(layer.clone());
306 }
307 let object = self.selected_object().map(str::to_string);
308 self.viewport.select(object.as_deref());
309 self.reload_rows();
310 self.texts.clear();
311 }
312
313 fn reload_rows(&mut self) {
314 self.rows = match &self.selection {
315 Some(item) => self.rows_of(item),
316 None => Vec::new(),
317 };
318 }
319
320 pub fn open_scene(&mut self, scene: PathBuf) {
321 let request = Request::Open(scene);
322 if matches!(&request, Request::Open(scene) if scene == self.path())
323 || self.requests.contains(&request)
324 {
325 return;
326 }
327 self.end_group();
328 self.request(request);
329 }
330
331 pub fn set_project(&mut self, project: pfx_project::Project) {
332 let scenes = project.scenes().len();
333 self.log.info(format!(
334 "project {}: {scenes} {}",
335 project.name(),
336 if scenes == 1 { "scene" } else { "scenes" }
337 ));
338 self.project = Some(project);
339 self.frame_project();
340 self.refresh();
341 }
342
343 pub fn frame_project(&mut self) {
344 let device = pfx_gpu::screens::detect(&pfx_gpu::screens::SystemSource).device;
345 let aspect = match self
346 .project
347 .as_ref()
348 .map(|project| frame_aspect(project, device))
349 {
350 Some(Ok(aspect)) => aspect,
351 Some(Err(error)) => {
352 self.log.error(error);
353 None
354 }
355 None => None,
356 };
357 self.viewport.set_frame_aspect(aspect);
358 }
359
360 pub fn selected_object(&self) -> Option<&str> {
361 match &self.selection {
362 Some(Item::Object(name)) => Some(name),
363 _ => None,
364 }
365 }
366
367 pub fn editable(&self) -> bool {
368 self.play == Play::Stopped
369 }
370
371 pub fn dry(&mut self, change: &Change) -> Result<PatchGroup, String> {
372 if change.tunable().is_some() {
373 let label = format!("set {}", change.describe());
374 let patch = change
375 .live(&self.tunables)
376 .and_then(|edit| self.tunables_patch(&[edit], &label));
377 return match patch {
378 Ok(patches) => Ok(PatchGroup { label, patches }),
379 Err(error) => {
380 self.log.warn(error.clone());
381 Err(error)
382 }
383 };
384 }
385 let change = change.filed(self.edit.scene());
386 self.edit
387 .dry_run(|edit| change.apply(edit))
388 .map_err(|error| {
389 self.log.edit_error(&error);
390 error.to_string()
391 })
392 }
393
394 fn tunables_patch(&self, edits: &[Edit], label: &str) -> Result<Vec<Patch>, String> {
395 let Some(file) = self.tunables.file().map(Path::to_path_buf) else {
396 return Err("this project has no tunables".to_string());
397 };
398 let before = match self.files.text(&file) {
399 Some(text) => text.to_string(),
400 None => std::fs::read_to_string(&file)
401 .map_err(|error| format!("{}: {error}", file.display()))?,
402 };
403 let mut after = before.clone();
404 for edit in edits {
405 if let Edit::Tunable { name, value } = edit {
406 let value = self.tunables.check(name, *value)?;
407 after = Tunables::written(&after, name, value)
408 .map_err(|error| format!("{}: {error}", file.display()))?;
409 }
410 }
411 Ok(if after == before {
412 Vec::new()
413 } else {
414 vec![Patch {
415 label: label.to_string(),
416 file,
417 before,
418 after,
419 }]
420 })
421 }
422
423 pub fn play_edit(&mut self, session: &mut PlaySession, change: &Change) -> bool {
424 let applied = change
425 .live(&self.tunables)
426 .and_then(|edit| session.edit(edit));
427 match applied {
428 Ok(()) => {
429 if let Some(value) = &change.value {
430 self.live.insert(change.describe(), value.clone());
431 }
432 inspect::overlay(&mut self.rows, &self.live);
433 true
434 }
435 Err(error) => {
436 self.log.warn(format!("play mode: {error}"));
437 false
438 }
439 }
440 }
441
442 pub fn stopped(&mut self, keep: Option<&Pending>) {
443 self.play = Play::Stopped;
444 self.live.clear();
445 if let Some(pending) = keep.filter(|pending| !pending.is_empty()) {
446 self.keep(pending);
447 }
448 self.reload_rows();
449 }
450
451 pub fn keep(&mut self, pending: &Pending) {
452 self.reloaded();
453 let label = format!(
454 "keep {} play {}",
455 pending.len(),
456 if pending.len() == 1 { "edit" } else { "edits" }
457 );
458 let scene: Vec<_> = pending.scene().collect();
459 let mut group = if scene.is_empty() {
460 PatchGroup::default()
461 } else {
462 let found = self.edit.dry_run(|edit| {
463 for (target, path, value) in &scene {
464 let path: Vec<&str> = path.iter().map(String::as_str).collect();
465 edit.set(target, &path, (*value).clone())?;
466 }
467 Ok(())
468 });
469 match found {
470 Ok(group) => group,
471 Err(error) => {
472 self.log.edit_error(&error);
473 return;
474 }
475 }
476 };
477 let tunables: Vec<Edit> = pending
478 .iter()
479 .filter(|edit| matches!(edit, Edit::Tunable { .. }))
480 .cloned()
481 .collect();
482 if !tunables.is_empty() {
483 match self.tunables_patch(&tunables, &label) {
484 Ok(patches) => group.patches.extend(patches),
485 Err(error) => {
486 self.log.error(format!("keep: {error}"));
487 return;
488 }
489 }
490 }
491 let group = labelled(group, label);
492 if group.is_empty() {
493 self.log
494 .info("keep: the play edits match the files already");
495 return;
496 }
497 self.land(group);
498 }
499
500 pub fn change(&mut self, change: Change, group: Group) {
501 if !self.editable() {
502 self.request(Request::World(change));
503 return;
504 }
505 if group == Group::Begin && self.held.is_none() {
506 self.held = Some(Held {
507 label: change.describe(),
508 start: self
509 .files
510 .paths()
511 .filter_map(|path| {
512 Some((path.to_path_buf(), self.files.text(path)?.to_string()))
513 })
514 .collect(),
515 });
516 }
517 if let Ok(found) = self.dry(&change)
518 && !found.is_empty()
519 {
520 if self.held.is_some() {
521 self.step(found);
522 } else {
523 self.land(found);
524 }
525 }
526 if group == Group::End {
527 self.end_group();
528 }
529 }
530
531 fn step(&mut self, group: PatchGroup) {
532 let label = group.label.clone();
533 let mut patch = ScenePatch::new(group);
534 let files: Vec<PathBuf> = pfx_editor_doc::Edit::files(&patch);
535 match pfx_editor_doc::Edit::apply(&mut patch, &mut self.files) {
536 Ok(()) => self.wrote(&Changed {
537 label,
538 files,
539 caret: None,
540 }),
541 Err(error) => self.log.error(error.to_string()),
542 }
543 }
544
545 pub fn land(&mut self, group: PatchGroup) {
546 match self.history.apply(&mut self.files, ScenePatch::new(group)) {
547 Ok(changed) if !changed.is_empty() => {
548 self.log.info(changed.label.clone());
549 self.wrote(&changed);
550 }
551 Ok(_) => {}
552 Err(error) => self.log.error(error.to_string()),
553 }
554 }
555
556 pub fn end_group(&mut self) {
557 let Some(held) = self.held.take() else {
558 return;
559 };
560 let patches: Vec<Patch> = held
561 .start
562 .iter()
563 .filter_map(|(file, before)| {
564 let after = self.files.text(file)?;
565 (after != before).then(|| Patch {
566 label: held.label.clone(),
567 file: file.clone(),
568 before: before.clone(),
569 after: after.to_string(),
570 })
571 })
572 .collect();
573 if patches.is_empty() {
574 return;
575 }
576 for patch in &patches {
577 let _ = self.files.set_text(&patch.file, patch.before.clone());
578 }
579 let group = PatchGroup {
580 label: held.label,
581 patches,
582 };
583 match self.history.apply(&mut self.files, ScenePatch::new(group)) {
584 Ok(changed) => self.log.info(changed.label),
585 Err(error) => self.log.error(error.to_string()),
586 }
587 }
588
589 pub fn grouping(&self) -> bool {
590 self.held.is_some()
591 }
592
593 pub fn save(&mut self, changed: &Changed) -> bool {
594 let mut saved = true;
595 for file in &changed.files {
596 match self.files.save(file) {
597 Ok(_) => self.saves.push(file.clone()),
598 Err(error) => {
599 self.log.error(error.to_string());
600 saved = false;
601 }
602 }
603 }
604 saved
605 }
606
607 pub fn wrote(&mut self, changed: &Changed) {
608 if !self.save(changed) {
609 return;
610 }
611 self.caught_up();
612 }
613
614 fn caught_up(&mut self) {
615 if let Err(error) = self.edit.reload() {
616 self.log.scene_error(&error);
617 }
618 self.sync_files();
619 self.refresh();
620 self.viewport.check();
621 self.events();
622 }
623
624 fn sync_files(&mut self) {
625 let wanted = self.files();
626 for file in &wanted {
627 if self.files.get(file).is_none()
628 && let Err(error) = self.files.load(file)
629 {
630 self.log.error(error.to_string());
631 }
632 }
633 let gone: Vec<PathBuf> = self
634 .files
635 .paths()
636 .filter(|path| !wanted.iter().any(|file| file == path))
637 .map(Path::to_path_buf)
638 .collect();
639 for path in gone {
640 self.files.close(&path);
641 self.sources.remove(&path);
642 }
643 }
644
645 pub fn undo(&mut self) {
646 self.step_history(true);
647 }
648
649 pub fn redo(&mut self) {
650 self.step_history(false);
651 }
652
653 fn step_history(&mut self, undo: bool) {
654 if !self.editable() {
655 return;
656 }
657 self.end_group();
658 let (word, stepped) = if undo {
659 ("undo", self.history.undo(&mut self.files))
660 } else {
661 ("redo", self.history.redo(&mut self.files))
662 };
663 match stepped {
664 Ok(Some(changed)) => {
665 self.log.info(format!("{word} {}", changed.label));
666 self.wrote(&changed);
667 }
668 Ok(None) => self.log.info(format!("nothing to {word}")),
669 Err(error) => self.log.error(error.to_string()),
670 }
671 }
672
673 pub fn typed(&mut self, changed: Option<Changed>) {
674 if let Some(changed) = changed.filter(|changed| !changed.is_empty()) {
675 self.log.info(changed.label.clone());
676 self.wrote(&changed);
677 }
678 }
679
680 pub fn reloaded(&mut self) {
681 if self.held.is_some() {
682 return;
683 }
684 let mut outside = false;
685 let paths: Vec<PathBuf> = self.files.paths().map(Path::to_path_buf).collect();
686 for path in paths {
687 match self.files.disk(&path) {
688 Ok(Disk::Outside(text)) => {
689 outside = true;
690 if let Err(error) = self.files.reload(&path, text) {
691 self.log.error(error.to_string());
692 }
693 }
694 Ok(_) => {}
695 Err(error) => self.log.error(error.to_string()),
696 }
697 }
698 let changed = self
699 .edit
700 .files()
701 .any(|file| std::fs::read_to_string(file).ok().as_deref() != self.edit.text(file));
702 if !outside && !changed {
703 return;
704 }
705 if outside {
706 let dropped = self.history.prune(&self.files);
707 if dropped > 0 {
708 self.log.warn(format!(
709 "the files changed outside the editor; {dropped} undo steps no longer apply and were dropped"
710 ));
711 } else {
712 self.log
713 .info("reloaded the files changed outside the editor");
714 }
715 }
716 if let Err(error) = self.edit.reload() {
717 self.log.scene_error(&error);
718 return;
719 }
720 self.sync_files();
721 self.refresh();
722 }
723
724 pub fn events(&mut self) {
725 for event in self.viewport.take_events() {
726 match event {
727 Event::Picked(found) => {
728 if self.level.shift {
729 if let Some(found) = found {
730 self.pick_more(&found.name);
731 }
732 continue;
733 }
734 let item = found.map(|selection| Item::Object(selection.name));
735 if item != self.selection {
736 self.select(item);
737 }
738 }
739 Event::Edited(changed) => {
740 self.saves.extend(changed.files.iter().cloned());
741 self.log.info(changed.label.clone());
742 self.caught_up();
743 }
744 Event::Refused(error) => self.log.warn(error),
745 Event::Reloaded => self.reloaded(),
746 Event::Broken(error) => self.log.error(error),
747 }
748 }
749 }
750
751 pub fn source(&mut self, file: &Path) -> &mut Source {
752 self.sources
753 .entry(file.to_path_buf())
754 .or_insert_with(|| Source::new(("pfx edit source", file), file).toolbar(false))
755 }
756
757 pub fn toggle_view(&mut self) {
758 match self.viewport.look() {
759 pfx_editor_viewport::Look::Scene => {
760 if let Some(eye) = self.viewport.eye() {
761 self.viewport.set_eye(eye);
762 }
763 }
764 pfx_editor_viewport::Look::Orbit(_) => self.viewport.look_through_scene(),
765 }
766 }
767
768 pub fn focus(&mut self) {
769 self.viewport.frame_selection();
770 }
771
772 pub fn request(&mut self, request: Request) {
773 if request == Request::Record && self.requests.contains(&request) {
774 return;
775 }
776 self.requests.push(request);
777 }
778
779 pub fn take_requests(&mut self) -> Vec<Request> {
780 std::mem::take(&mut self.requests)
781 }
782
783 pub fn step_selection(&mut self, by: isize) {
784 let next = self.outline.step(self.selection.as_ref(), by);
785 self.select(next);
786 }
787
788 pub fn play_pressed(&mut self) {
789 match self.play {
790 Play::Stopped => {
791 self.end_group();
792 self.request(Request::Play);
793 }
794 Play::Paused => self.request(Request::Resume),
795 Play::Playing => self.request(Request::Pause),
796 }
797 }
798}