Skip to main content

pfx_editor_viewport/
edit.rs

1use std::path::{Path, PathBuf};
2
3use glam::DVec3;
4use pfx_editor_doc::toml_edit::{DocumentMut, Item};
5use pfx_editor_doc::{Changed, DocError, Edit, Files};
6use pfx_load::scene::{EditError, PatchGroup, SceneEdit, Value};
7
8use crate::gizmo::Pose;
9
10pub const PLACES: [i32; 3] = [6, 4, 6];
11pub const STARTS_FROM: &str = "no longer holds the text this edit starts from";
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ScenePatch {
15    group: PatchGroup,
16}
17
18impl ScenePatch {
19    pub fn new(group: PatchGroup) -> ScenePatch {
20        ScenePatch { group }
21    }
22
23    pub fn onto(files: &Files, mut group: PatchGroup) -> ScenePatch {
24        for patch in &mut group.patches {
25            patch.file = keyed(files, &patch.file);
26        }
27        ScenePatch { group }
28    }
29
30    pub fn group(&self) -> &PatchGroup {
31        &self.group
32    }
33}
34
35impl From<PatchGroup> for ScenePatch {
36    fn from(group: PatchGroup) -> ScenePatch {
37        ScenePatch::new(group)
38    }
39}
40
41impl Edit for ScenePatch {
42    fn label(&self) -> &str {
43        self.group.label()
44    }
45
46    fn files(&self) -> Vec<PathBuf> {
47        let mut files: Vec<PathBuf> = Vec::new();
48        for file in self.group.files() {
49            if !files.iter().any(|known| known == file) {
50                files.push(file.to_path_buf());
51            }
52        }
53        files
54    }
55
56    fn apply(&mut self, files: &mut Files) -> Result<(), DocError> {
57        for patch in &self.group.patches {
58            match files.text(&patch.file) {
59                Some(text) if text == patch.before => {}
60                Some(_) => return Err(DocError::new(&patch.file, "", STARTS_FROM)),
61                None => {
62                    files.open(&patch.file, patch.before.clone());
63                }
64            }
65            files.set_text(&patch.file, patch.after.clone())?;
66        }
67        Ok(())
68    }
69
70    fn inverse(&self) -> Box<dyn Edit> {
71        Box::new(ScenePatch::new(self.group.inverse()))
72    }
73}
74
75pub fn round(value: f64, places: i32) -> f64 {
76    let scale = 10_f64.powi(places);
77    let rounded = (value * scale).round() / scale;
78    if rounded == 0.0 { 0.0 } else { rounded }
79}
80
81fn rounded(value: DVec3, places: i32) -> [f32; 3] {
82    value.to_array().map(|v| round(v, places) as f32)
83}
84
85pub fn scalar_scale(edit: &SceneEdit, name: &str) -> bool {
86    edit.files().any(|file| {
87        let Some(doc) = edit
88            .text(file)
89            .and_then(|text| text.parse::<DocumentMut>().ok())
90        else {
91            return false;
92        };
93        doc.get("object")
94            .and_then(Item::as_array_of_tables)
95            .and_then(|tables| {
96                tables
97                    .iter()
98                    .find(|table| table.get("name").and_then(Item::as_str) == Some(name))
99            })
100            .and_then(|table| table.get("scale"))
101            .and_then(Item::as_value)
102            .is_some_and(|value| value.as_float().is_some() || value.as_integer().is_some())
103    })
104}
105
106pub fn changes(edit: &SceneEdit, start: &Pose, pose: &Pose) -> Vec<(&'static str, Value)> {
107    let mut changes = Vec::new();
108    let [at, rotate, scale] = PLACES;
109    if rounded(start.at, at) != rounded(pose.at, at) {
110        changes.push(("at", Value::from(rounded(pose.at, at))));
111    }
112    if rounded(start.rotate, rotate) != rounded(pose.rotate, rotate) {
113        changes.push(("rotate", Value::from(rounded(pose.rotate, rotate))));
114    }
115    let after = rounded(pose.scale, scale);
116    if rounded(start.scale, scale) != after {
117        let uniform = after[0] == after[1] && after[1] == after[2];
118        let value = if uniform && scalar_scale(edit, &start.name) {
119            Value::Float(after[0])
120        } else {
121            Value::from(after)
122        };
123        changes.push(("scale", value));
124    }
125    changes
126}
127
128pub fn commit(
129    edit: &mut SceneEdit,
130    label: &str,
131    start: &Pose,
132    pose: &Pose,
133) -> Result<Option<PatchGroup>, EditError> {
134    let changes = changes(edit, start, pose);
135    if changes.is_empty() {
136        return Ok(None);
137    }
138    let target = pfx_load::scene::Target::Object(start.name.clone());
139    let mut group = edit.dry_run(|edit| {
140        for (key, value) in changes {
141            edit.set(&target, &[key], value)?;
142        }
143        Ok(())
144    })?;
145    if group.patches.is_empty() {
146        return Ok(None);
147    }
148    group.label = label.to_string();
149    Ok(Some(group))
150}
151
152pub fn keyed(files: &Files, path: &Path) -> PathBuf {
153    if files.get(path).is_some() {
154        return path.to_path_buf();
155    }
156    let Ok(real) = std::fs::canonicalize(path) else {
157        return path.to_path_buf();
158    };
159    files
160        .paths()
161        .find(|key| std::fs::canonicalize(key).is_ok_and(|key| key == real))
162        .unwrap_or(path)
163        .to_path_buf()
164}
165
166pub fn stale(edit: &SceneEdit, files: &Files) -> Vec<PathBuf> {
167    edit.files()
168        .filter(|file| {
169            files
170                .text(&keyed(files, file))
171                .is_some_and(|text| Some(text) != edit.text(file))
172        })
173        .map(Path::to_path_buf)
174        .collect()
175}
176
177pub fn save(files: &mut Files, changed: &Changed) -> Result<(), DocError> {
178    for file in &changed.files {
179        files.save(file)?;
180    }
181    Ok(())
182}