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;