Skip to main content

pfx_editor/
editor.rs

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}