Skip to main content

pfx_load/scene/
edit.rs

1#![allow(clippy::result_large_err)]
2
3use std::collections::BTreeMap;
4use std::path::{Path, PathBuf};
5
6use pfx_scene::{Id, Project, code};
7
8pub use pfx_scene::{EditError, Kind, Patch, PatchGroup, Target, Value};
9
10use super::{Scene, SceneError, build};
11
12type Writer = pfx_scene::SceneEdit;
13type Op = Box<dyn Fn(&mut Writer) -> Result<PatchGroup, EditError>>;
14
15fn edit_error(error: SceneError, key: &str) -> EditError {
16    EditError {
17        file: error.file,
18        key: key.to_string(),
19        line: error.line.map(|line| line as u32),
20        code: code::BAD_VALUE,
21        message: error.message,
22        diagnostics: Vec::new(),
23    }
24}
25
26pub(super) fn scene_error(root: &Path, error: EditError) -> SceneError {
27    if error.diagnostics.is_empty() {
28        return SceneError::new(
29            &error.file,
30            error.line.map(|line| line as usize),
31            error.message,
32        );
33    }
34    build::refused(root, &error.diagnostics)
35}
36
37fn merge_group(group: &mut PatchGroup, other: &PatchGroup) {
38    for patch in &other.patches {
39        match group.patches.iter_mut().find(|own| own.file == patch.file) {
40            Some(own) => own.after = patch.after.clone(),
41            None => group.patches.push(patch.clone()),
42        }
43    }
44}
45
46fn settle(group: &mut PatchGroup) {
47    group.patches.retain(|patch| patch.before != patch.after);
48}
49
50fn since(previous: &PatchGroup, now: &PatchGroup, label: &str) -> PatchGroup {
51    let patches = now
52        .patches
53        .iter()
54        .filter_map(|patch| {
55            let before = previous
56                .patches
57                .iter()
58                .find(|old| old.file == patch.file)
59                .map_or_else(|| patch.before.clone(), |old| old.after.clone());
60            (before != patch.after).then(|| Patch {
61                label: label.to_string(),
62                file: patch.file.clone(),
63                before,
64                after: patch.after.clone(),
65            })
66        })
67        .collect();
68    PatchGroup {
69        label: label.to_string(),
70        patches,
71    }
72}
73
74#[derive(Default)]
75struct Dry {
76    ops: Vec<Op>,
77    group: PatchGroup,
78}
79
80pub struct SceneEdit {
81    path: PathBuf,
82    root: PathBuf,
83    writer: Writer,
84    files: Vec<PathBuf>,
85    scene: Scene,
86    undo: Vec<PatchGroup>,
87    redo: Vec<PatchGroup>,
88    open: Option<PatchGroup>,
89    dry: Option<Dry>,
90}
91
92impl SceneEdit {
93    pub fn open(path: impl AsRef<Path>) -> Result<Self, SceneError> {
94        let path = path.as_ref().to_path_buf();
95        let root = build::project_root(&path);
96        let writer = Writer::open(&path).map_err(|error| scene_error(&root, error))?;
97        let scene = build::build(&path, &root, writer.scene(), None, &BTreeMap::new())?;
98        let mut edit = Self {
99            path,
100            root,
101            writer,
102            files: Vec::new(),
103            scene,
104            undo: Vec::new(),
105            redo: Vec::new(),
106            open: None,
107            dry: None,
108        };
109        edit.follow();
110        Ok(edit)
111    }
112
113    fn follow(&mut self) {
114        let root = self.writer.root().to_path_buf();
115        self.files = self
116            .writer
117            .files()
118            .map(|file| {
119                file.strip_prefix(&root)
120                    .map(|relative| self.root.join(relative))
121                    .unwrap_or(file)
122            })
123            .collect();
124    }
125
126    pub fn path(&self) -> &Path {
127        &self.path
128    }
129
130    pub fn scene(&self) -> &Scene {
131        &self.scene
132    }
133
134    pub fn resolved(&self) -> &pfx_scene::Scene {
135        self.writer.scene()
136    }
137
138    pub fn project(&self) -> &Project {
139        self.writer.project()
140    }
141
142    pub fn files(&self) -> impl Iterator<Item = &Path> {
143        self.files.iter().map(PathBuf::as_path)
144    }
145
146    pub fn text(&self, file: impl AsRef<Path>) -> Option<&str> {
147        let file = file.as_ref();
148        self.writer.text(build::absolute(file))
149    }
150
151    pub fn reload(&mut self) -> Result<(), SceneError> {
152        *self = Self::open(&self.path)?;
153        Ok(())
154    }
155
156    pub fn can_undo(&self) -> bool {
157        !self.undo.is_empty()
158    }
159
160    pub fn can_redo(&self) -> bool {
161        !self.redo.is_empty()
162    }
163
164    pub fn undo_label(&self) -> Option<&str> {
165        self.undo.last().map(PatchGroup::label)
166    }
167
168    pub fn redo_label(&self) -> Option<&str> {
169        self.redo.last().map(PatchGroup::label)
170    }
171
172    fn refuse(&self, key: &str, message: &str) -> EditError {
173        EditError {
174            file: self.writer.path(),
175            key: key.to_string(),
176            line: None,
177            code: code::BAD_VALUE,
178            message: message.to_string(),
179            diagnostics: Vec::new(),
180        }
181    }
182
183    pub fn begin(&mut self, label: &str) -> Result<(), EditError> {
184        if self.dry.is_some() {
185            return Err(self.refuse(label, "a dry run takes no group"));
186        }
187        if self.open.is_some() {
188            return Err(self.refuse(label, "a group of edits is open already"));
189        }
190        self.open = Some(PatchGroup {
191            label: label.to_string(),
192            patches: Vec::new(),
193        });
194        Ok(())
195    }
196
197    pub fn end(&mut self) -> Option<PatchGroup> {
198        let mut group = self.open.take()?;
199        settle(&mut group);
200        if group.is_empty() {
201            return None;
202        }
203        self.redo.clear();
204        self.undo.push(group.clone());
205        Some(group)
206    }
207
208    pub fn cancel(&mut self) -> Result<(), EditError> {
209        let Some(mut group) = self.open.take() else {
210            return Ok(());
211        };
212        settle(&mut group);
213        let inverse = group.inverse();
214        let label = group.label.clone();
215        if let Err(error) = self.land(&inverse, &label) {
216            self.open = Some(group);
217            return Err(error);
218        }
219        Ok(())
220    }
221
222    pub fn apply(&mut self, group: &PatchGroup) -> Result<(), EditError> {
223        if self.dry.is_some() {
224            return Err(self.refuse(&group.label, "a dry run applies nothing"));
225        }
226        self.land(group, &group.label)
227    }
228
229    pub fn undo(&mut self) -> Result<Option<PatchGroup>, EditError> {
230        if self.open.is_some() || self.dry.is_some() {
231            return Err(self.refuse("undo", "a group of edits is open"));
232        }
233        let Some(group) = self.undo.pop() else {
234            return Ok(None);
235        };
236        let inverse = group.inverse();
237        if let Err(error) = self.land(&inverse, &inverse.label) {
238            self.undo.push(group);
239            return Err(error);
240        }
241        self.redo.push(group);
242        Ok(Some(inverse))
243    }
244
245    pub fn redo(&mut self) -> Result<Option<PatchGroup>, EditError> {
246        if self.open.is_some() || self.dry.is_some() {
247            return Err(self.refuse("redo", "a group of edits is open"));
248        }
249        let Some(group) = self.redo.pop() else {
250            return Ok(None);
251        };
252        if let Err(error) = self.land(&group, &group.label) {
253            self.redo.push(group);
254            return Err(error);
255        }
256        self.undo.push(group.clone());
257        Ok(Some(group))
258    }
259
260    pub fn dry_run<T>(
261        &mut self,
262        edits: impl FnOnce(&mut Self) -> Result<T, EditError>,
263    ) -> Result<PatchGroup, EditError> {
264        if self.dry.is_some() || self.open.is_some() {
265            return Err(self.refuse("dry run", "a group of edits is open"));
266        }
267        self.dry = Some(Dry::default());
268        let outcome = edits(self);
269        let dry = self.dry.take().unwrap_or_default();
270        outcome?;
271        let mut group = dry.group;
272        settle(&mut group);
273        if !group.is_empty() {
274            self.check(&group, &group.label)?;
275        }
276        Ok(group)
277    }
278
279    fn check(&self, group: &PatchGroup, key: &str) -> Result<Scene, EditError> {
280        let resolved = self
281            .writer
282            .project()
283            .scene_with(&self.writer.path(), group.after())
284            .map_err(|found| edit_error(build::refused(&self.root, &found), key))?;
285        let root = self.writer.root();
286        let overlay = group
287            .after()
288            .into_iter()
289            .map(|(file, text)| {
290                let relative = file
291                    .strip_prefix(root)
292                    .map(Path::to_path_buf)
293                    .unwrap_or(file);
294                (relative, text)
295            })
296            .collect();
297        build::build(&self.path, &self.root, &resolved, None, &overlay)
298            .map_err(|error| edit_error(error, key))
299    }
300
301    fn land(&mut self, group: &PatchGroup, key: &str) -> Result<(), EditError> {
302        let scene = self.check(group, key)?;
303        self.writer.apply(group).map_err(|error| EditError {
304            key: key.to_string(),
305            ..error
306        })?;
307        self.scene = scene;
308        self.follow();
309        Ok(())
310    }
311
312    fn record(&mut self, group: &PatchGroup) {
313        match &mut self.open {
314            Some(open) => merge_group(open, group),
315            None => {
316                self.redo.clear();
317                self.undo.push(group.clone());
318            }
319        }
320    }
321
322    fn change(&mut self, key: String, label: String, op: Op) -> Result<PatchGroup, EditError> {
323        let Self { writer, dry, .. } = self;
324        if let Some(dry) = dry {
325            let group = writer
326                .dry_run(|writer| {
327                    for done in &dry.ops {
328                        done(writer)?;
329                    }
330                    op(writer)
331                })
332                .map_err(|error| EditError {
333                    key: key.clone(),
334                    ..error
335                })?;
336            let own = since(&dry.group, &group, &label);
337            dry.ops.push(op);
338            if dry.group.label.is_empty() {
339                dry.group.label = group.label.clone();
340            }
341            merge_group(&mut dry.group, &own);
342            return Ok(own);
343        }
344        let group = writer
345            .dry_run(|writer| op(writer))
346            .map_err(|error| EditError {
347                key: key.clone(),
348                ..error
349            })?;
350        if group.is_empty() {
351            return Ok(group);
352        }
353        self.land(&group, &key)?;
354        self.record(&group);
355        Ok(group)
356    }
357
358    fn at_path(
359        &mut self,
360        verb: &str,
361        target: &Target,
362        path: &[&str],
363        apply: impl Fn(&mut Writer, &Target, &[&str]) -> Result<PatchGroup, EditError> + 'static,
364    ) -> Result<PatchGroup, EditError> {
365        let target = target.clone();
366        let path: Vec<String> = path.iter().map(|segment| segment.to_string()).collect();
367        let key = format!("{target} {}", path.join("."));
368        self.change(
369            key.clone(),
370            format!("{verb} {key}"),
371            Box::new(move |writer| {
372                let path: Vec<&str> = path.iter().map(String::as_str).collect();
373                apply(writer, &target, &path)
374            }),
375        )
376    }
377
378    pub fn set(
379        &mut self,
380        target: &Target,
381        path: &[&str],
382        value: impl Into<Value>,
383    ) -> Result<PatchGroup, EditError> {
384        let value = value.into();
385        self.at_path("set", target, path, move |writer, target, path| {
386            writer.set(target, path, value.clone())
387        })
388    }
389
390    pub fn unset(&mut self, target: &Target, path: &[&str]) -> Result<PatchGroup, EditError> {
391        self.at_path("unset", target, path, |writer, target, path| {
392            writer.unset(target, path)
393        })
394    }
395
396    pub fn push(
397        &mut self,
398        target: &Target,
399        path: &[&str],
400        value: impl Into<Value>,
401    ) -> Result<PatchGroup, EditError> {
402        let value = value.into();
403        self.at_path("add to", target, path, move |writer, target, path| {
404            writer.push(target, path, value.clone())
405        })
406    }
407
408    pub fn remove(&mut self, target: &Target) -> Result<PatchGroup, EditError> {
409        let target = target.clone();
410        let key = target.to_string();
411        self.change(
412            key.clone(),
413            format!("remove {key}"),
414            Box::new(move |writer| writer.remove(&target)),
415        )
416    }
417
418    pub fn add(
419        &mut self,
420        kind: Kind,
421        id: Id,
422        name: &str,
423        fields: &[(&str, Value)],
424        file: Option<&Path>,
425    ) -> Result<PatchGroup, EditError> {
426        let name = name.to_string();
427        let fields: Vec<(String, Value)> = fields
428            .iter()
429            .map(|(key, value)| (key.to_string(), value.clone()))
430            .collect();
431        let file = file.map(Path::to_path_buf);
432        let key = format!("{} {name}", format!("{kind:?}").to_lowercase());
433        self.change(
434            key.clone(),
435            format!("add {key}"),
436            Box::new(move |writer| {
437                let fields: Vec<(&str, Value)> = fields
438                    .iter()
439                    .map(|(key, value)| (key.as_str(), value.clone()))
440                    .collect();
441                writer.add(kind, id, &name, &fields, file.as_deref())
442            }),
443        )
444    }
445
446    pub fn add_material(
447        &mut self,
448        name: &str,
449        id: Id,
450        material: &pfx_materials::Material,
451        file: Option<&Path>,
452    ) -> Result<PatchGroup, EditError> {
453        let key = format!("material {name}");
454        let text = material
455            .to_toml()
456            .map_err(|message| self.refuse(&key, &message))?;
457        let typed: pfx_scene::types::Material =
458            toml::from_str(&text).map_err(|error| self.refuse(&key, error.message()))?;
459        let name = name.to_string();
460        let file = file.map(Path::to_path_buf);
461        self.change(
462            key.clone(),
463            format!("add {key}"),
464            Box::new(move |writer| writer.add_material(&name, id, &typed, file.as_deref())),
465        )
466    }
467
468    pub fn duplicate_object(
469        &mut self,
470        name: &str,
471        new: &str,
472        id: Id,
473    ) -> Result<PatchGroup, EditError> {
474        let (name, new) = (name.to_string(), new.to_string());
475        self.change(
476            format!("object {name}"),
477            format!("duplicate object {name} as {new}"),
478            Box::new(move |writer| writer.duplicate_object(&name, &new, id)),
479        )
480    }
481
482    pub fn rename_object(&mut self, name: &str, new: &str) -> Result<PatchGroup, EditError> {
483        let (name, new) = (name.to_string(), new.to_string());
484        self.change(
485            format!("object {name}"),
486            format!("rename object {name} to {new}"),
487            Box::new(move |writer| writer.rename_object(&name, &new)),
488        )
489    }
490
491    pub fn set_at(&mut self, object: &str, at: [f32; 3]) -> Result<PatchGroup, EditError> {
492        self.set(&Target::Object(object.to_string()), &["at"], at)
493    }
494
495    pub fn set_rotate(&mut self, object: &str, rotate: [f32; 3]) -> Result<PatchGroup, EditError> {
496        self.set(&Target::Object(object.to_string()), &["rotate"], rotate)
497    }
498
499    pub fn set_scale(
500        &mut self,
501        object: &str,
502        scale: impl Into<Value>,
503    ) -> Result<PatchGroup, EditError> {
504        self.set(&Target::Object(object.to_string()), &["scale"], scale)
505    }
506
507    pub fn set_object_material(
508        &mut self,
509        object: &str,
510        material: Option<&str>,
511    ) -> Result<PatchGroup, EditError> {
512        let target = Target::Object(object.to_string());
513        match material {
514            Some(material) => self.set(&target, &["material"], material),
515            None => self.unset(&target, &["material"]),
516        }
517    }
518
519    pub fn set_object_materials(
520        &mut self,
521        object: &str,
522        materials: &[(&str, &str)],
523    ) -> Result<PatchGroup, EditError> {
524        let target = Target::Object(object.to_string());
525        if materials.is_empty() {
526            return self.unset(&target, &["materials"]);
527        }
528        let table = materials
529            .iter()
530            .map(|(node, material)| (node.to_string(), Value::from(*material)))
531            .collect();
532        self.set(&target, &["materials"], Value::Table(table))
533    }
534
535    pub fn set_shadow(&mut self, object: &str, shadow: &str) -> Result<PatchGroup, EditError> {
536        self.set(&Target::Object(object.to_string()), &["shadow"], shadow)
537    }
538
539    pub fn set_two_sided(&mut self, object: &str, on: bool) -> Result<PatchGroup, EditError> {
540        self.set(&Target::Object(object.to_string()), &["two_sided"], on)
541    }
542
543    pub fn set_hidden(&mut self, object: &str, on: bool) -> Result<PatchGroup, EditError> {
544        self.set(&Target::Object(object.to_string()), &["hidden"], on)
545    }
546
547    pub fn set_clip(&mut self, object: &str, planes: &[[f32; 4]]) -> Result<PatchGroup, EditError> {
548        let target = Target::Object(object.to_string());
549        if planes.is_empty() {
550            return self.unset(&target, &["clip"]);
551        }
552        let planes: Vec<Value> = planes.iter().map(|plane| Value::from(*plane)).collect();
553        self.set(&target, &["clip"], Value::Array(planes))
554    }
555
556    pub fn set_parent(
557        &mut self,
558        object: &str,
559        parent: Option<&str>,
560    ) -> Result<PatchGroup, EditError> {
561        let target = Target::Object(object.to_string());
562        match parent {
563            Some(parent) => self.set(&target, &["parent"], parent),
564            None => self.unset(&target, &["parent"]),
565        }
566    }
567
568    pub fn add_mesh(
569        &mut self,
570        id: Id,
571        name: &str,
572        file: &str,
573        node: Option<&str>,
574    ) -> Result<PatchGroup, EditError> {
575        let mut fields = vec![("file", Value::from(file))];
576        if let Some(node) = node {
577            fields.push(("node", Value::from(node)));
578        }
579        self.add(Kind::Mesh, id, name, &fields, None)
580    }
581
582    pub fn set_node(
583        &mut self,
584        mesh: &str,
585        node: &str,
586        key: &str,
587        value: impl Into<Value>,
588    ) -> Result<PatchGroup, EditError> {
589        let target = Target::Node {
590            mesh: mesh.to_string(),
591            node: node.to_string(),
592        };
593        self.set(&target, &[key], value)
594    }
595
596    pub fn set_material(
597        &mut self,
598        name: &str,
599        path: &[&str],
600        value: impl Into<Value>,
601    ) -> Result<PatchGroup, EditError> {
602        self.set(&Target::Material(name.to_string()), path, value)
603    }
604}
605
606#[cfg(test)]
607#[path = "edit_tests.rs"]
608mod tests;