pfx_editor_viewport/
edit.rs1use 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}