1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, Value as Toml};
6
7use crate::migrate::add_ids;
8use crate::paths::slashed;
9use crate::project::Project;
10use crate::types::{FileKind, Material};
11use crate::{Diagnostic, Id, Scene};
12
13#[derive(Clone, Debug, PartialEq)]
14pub enum Value {
15 Bool(bool),
16 Int(i64),
17 Float(f32),
18 Text(String),
19 Array(Vec<Value>),
20 Table(Vec<(String, Value)>),
21}
22
23impl From<bool> for Value {
24 fn from(value: bool) -> Self {
25 Self::Bool(value)
26 }
27}
28
29impl From<i64> for Value {
30 fn from(value: i64) -> Self {
31 Self::Int(value)
32 }
33}
34
35impl From<i32> for Value {
36 fn from(value: i32) -> Self {
37 Self::Int(value.into())
38 }
39}
40
41impl From<u32> for Value {
42 fn from(value: u32) -> Self {
43 Self::Int(value.into())
44 }
45}
46
47impl From<f32> for Value {
48 fn from(value: f32) -> Self {
49 Self::Float(value)
50 }
51}
52
53impl From<&str> for Value {
54 fn from(value: &str) -> Self {
55 Self::Text(value.to_string())
56 }
57}
58
59impl From<String> for Value {
60 fn from(value: String) -> Self {
61 Self::Text(value)
62 }
63}
64
65impl From<Id> for Value {
66 fn from(value: Id) -> Self {
67 Self::Text(value.to_string())
68 }
69}
70
71impl From<Vec<Value>> for Value {
72 fn from(value: Vec<Value>) -> Self {
73 Self::Array(value)
74 }
75}
76
77impl<const N: usize> From<[f32; N]> for Value {
78 fn from(value: [f32; N]) -> Self {
79 Self::Array(value.into_iter().map(Self::Float).collect())
80 }
81}
82
83fn float_text(value: f32) -> String {
84 let mut text = format!("{value}");
85 if !text.contains(['.', 'e', 'E']) {
86 text.push_str(".0");
87 }
88 text
89}
90
91impl Value {
92 fn toml(&self) -> Result<Toml, String> {
93 Ok(match self {
94 Value::Bool(value) => Toml::from(*value),
95 Value::Int(value) => Toml::from(*value),
96 Value::Float(value) => {
97 if !value.is_finite() {
98 return Err("a number is not finite".to_string());
99 }
100 float_text(*value)
101 .parse::<Toml>()
102 .map_err(|error| error.to_string())?
103 }
104 Value::Text(value) => Toml::from(value.as_str()),
105 Value::Array(items) => {
106 let mut array = Array::new();
107 for item in items {
108 array.push(item.toml()?);
109 }
110 Toml::Array(array)
111 }
112 Value::Table(fields) => {
113 let mut table = InlineTable::new();
114 for (key, value) in fields {
115 table.insert(key, value.toml()?);
116 }
117 Toml::InlineTable(table)
118 }
119 })
120 }
121
122 fn tables(&self) -> bool {
123 match self {
124 Value::Array(items) => {
125 !items.is_empty() && items.iter().all(|item| matches!(item, Value::Table(_)))
126 }
127 _ => false,
128 }
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub enum Target {
134 Scene,
135 Object(String),
136 Mesh(String),
137 Node { mesh: String, node: String },
138 Material(String),
139 Light(String),
140 Emitter(String),
141 Mover(String),
142 Content(String),
143 Text(String),
144 Sound(String),
145 Rig(String),
146 Tiles(String),
147 Sun,
148 Sky,
149 Haze,
150 Camera,
151 Finish,
152 Trace,
153 Plates,
154 Physics,
155}
156
157impl fmt::Display for Target {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 Target::Scene => write!(f, "scene"),
161 Target::Object(name) => write!(f, "object {name}"),
162 Target::Mesh(name) => write!(f, "mesh {name}"),
163 Target::Node { mesh, node } => write!(f, "mesh {mesh} node {node}"),
164 Target::Material(name) => write!(f, "material {name}"),
165 Target::Light(name) => write!(f, "light {name}"),
166 Target::Emitter(name) => write!(f, "emitter {name}"),
167 Target::Mover(name) => write!(f, "mover {name}"),
168 Target::Content(name) => write!(f, "content {name}"),
169 Target::Text(name) => write!(f, "text {name}"),
170 Target::Sound(name) => write!(f, "sound {name}"),
171 Target::Rig(name) => write!(f, "rig {name}"),
172 Target::Tiles(name) => write!(f, "tile layer {name}"),
173 Target::Sun => write!(f, "sun"),
174 Target::Sky => write!(f, "sky"),
175 Target::Haze => write!(f, "haze"),
176 Target::Camera => write!(f, "camera"),
177 Target::Finish => write!(f, "finish"),
178 Target::Trace => write!(f, "trace"),
179 Target::Plates => write!(f, "plates"),
180 Target::Physics => write!(f, "physics"),
181 }
182 }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub enum Kind {
187 Object,
188 Mesh,
189 Material,
190 Light,
191 Emitter,
192 Mover,
193 Content,
194 Text,
195 Sound,
196 Rig,
197 Tiles,
198}
199
200const ORDER_OBJECT: &[&str] = &[
201 "id",
202 "name",
203 "mesh",
204 "prefab",
205 "at",
206 "rotate",
207 "scale",
208 "parent",
209 "material",
210 "materials",
211 "shadow",
212 "two_sided",
213 "hidden",
214 "clip",
215 "content",
216 "pick",
217 "alpha_cutoff",
218 "face_camera",
219 "dynamic",
220 "body",
221 "character",
222 "trigger",
223 "animation",
224 "set",
225 "authoring",
226];
227const ORDER_LIGHT: &[&str] = &[
228 "id",
229 "name",
230 "position",
231 "color",
232 "intensity",
233 "radius",
234 "range",
235 "shadow",
236 "authoring",
237];
238const ORDER_EMITTER: &[&str] = &[
239 "id",
240 "name",
241 "position",
242 "radius",
243 "color",
244 "intensity",
245 "authoring",
246];
247const ORDER_MOVER: &[&str] = &[
248 "id",
249 "name",
250 "objects",
251 "kind",
252 "pivot",
253 "axis",
254 "travel",
255 "period",
256 "motion",
257 "offset",
258 "clip",
259 "authoring",
260];
261const ORDER_MESH: &[&str] = &["id", "file", "node", "nodes", "authoring"];
262const ORDER_CONTENT: &[&str] = &["id", "image", "authoring"];
263const ORDER_TEXT: &[&str] = &[
264 "id",
265 "text",
266 "font",
267 "size",
268 "color",
269 "at",
270 "rotate",
271 "lit",
272 "dynamic",
273 "authoring",
274];
275const ORDER_SOUND: &[&str] = &[
276 "id",
277 "file",
278 "volume",
279 "pan",
280 "loop",
281 "play",
282 "object",
283 "authoring",
284];
285const ORDER_TILES: &[&str] = &[
286 "id",
287 "name",
288 "cell",
289 "origin",
290 "plane",
291 "palette",
292 "rows",
293 "cells",
294 "authoring",
295];
296const ORDER_RIG: &[&str] = &[
297 "id",
298 "name",
299 "kind",
300 "target",
301 "offset",
302 "dead_zone",
303 "look_ahead",
304 "damping",
305 "bounds",
306 "orthographic",
307 "snap",
308 "sensitivity",
309 "invert",
310 "smoothing",
311 "pitch",
312 "head_bob",
313 "distance",
314 "collide",
315 "authoring",
316];
317const ORDER_MATERIAL: &[&str] = &[
318 "id",
319 "family",
320 "base",
321 "roughness",
322 "metalness",
323 "specular",
324 "clearcoat",
325 "clearcoat_roughness",
326 "sheen",
327 "transmission",
328 "ior",
329 "dispersion",
330 "thickness",
331 "subsurface",
332 "subsurface_tint",
333 "absorption",
334 "thin_film",
335 "thin_film_ior",
336 "thin_film_amount",
337 "emission",
338 "fresnel_power",
339 "normal",
340 "maps",
341 "content",
342 "content_layer",
343 "layers",
344 "ageing",
345 "authoring",
346];
347
348impl Kind {
349 fn target(self, name: &str) -> Target {
350 let name = name.to_string();
351 match self {
352 Kind::Object => Target::Object(name),
353 Kind::Mesh => Target::Mesh(name),
354 Kind::Material => Target::Material(name),
355 Kind::Light => Target::Light(name),
356 Kind::Emitter => Target::Emitter(name),
357 Kind::Mover => Target::Mover(name),
358 Kind::Content => Target::Content(name),
359 Kind::Text => Target::Text(name),
360 Kind::Sound => Target::Sound(name),
361 Kind::Rig => Target::Rig(name),
362 Kind::Tiles => Target::Tiles(name),
363 }
364 }
365
366 fn list(self) -> Option<&'static str> {
367 match self {
368 Kind::Object => Some("object"),
369 Kind::Light => Some("light"),
370 Kind::Emitter => Some("emitter"),
371 Kind::Mover => Some("mover"),
372 Kind::Rig => Some("rig"),
373 Kind::Tiles => Some("tiles"),
374 _ => None,
375 }
376 }
377
378 fn map(self) -> Option<&'static str> {
379 match self {
380 Kind::Mesh => Some("mesh"),
381 Kind::Material => Some("materials"),
382 Kind::Content => Some("content"),
383 Kind::Text => Some("text"),
384 Kind::Sound => Some("sound"),
385 _ => None,
386 }
387 }
388
389 fn order(self) -> &'static [&'static str] {
390 match self {
391 Kind::Object => ORDER_OBJECT,
392 Kind::Mesh => ORDER_MESH,
393 Kind::Material => ORDER_MATERIAL,
394 Kind::Light => ORDER_LIGHT,
395 Kind::Emitter => ORDER_EMITTER,
396 Kind::Mover => ORDER_MOVER,
397 Kind::Content => ORDER_CONTENT,
398 Kind::Text => ORDER_TEXT,
399 Kind::Sound => ORDER_SOUND,
400 Kind::Rig => ORDER_RIG,
401 Kind::Tiles => ORDER_TILES,
402 }
403 }
404}
405
406#[derive(Clone, Debug, Default, PartialEq, Eq)]
407pub struct EditError {
408 pub file: PathBuf,
409 pub key: String,
410 pub line: Option<u32>,
411 pub code: &'static str,
412 pub message: String,
413 pub diagnostics: Vec<Diagnostic>,
414}
415
416impl fmt::Display for EditError {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 match self.line {
419 Some(line) => write!(
420 f,
421 "{}:{line}: {}: {}",
422 self.file.display(),
423 self.key,
424 self.message
425 ),
426 None => write!(f, "{}: {}: {}", self.file.display(), self.key, self.message),
427 }
428 }
429}
430
431impl std::error::Error for EditError {}
432
433#[derive(Clone, Debug, PartialEq, Eq)]
434pub struct Patch {
435 pub label: String,
436 pub file: PathBuf,
437 pub before: String,
438 pub after: String,
439}
440
441impl Patch {
442 pub fn label(&self) -> &str {
443 &self.label
444 }
445
446 pub fn file(&self) -> &Path {
447 &self.file
448 }
449
450 pub fn inverse(&self) -> Self {
451 Self {
452 label: self.label.clone(),
453 file: self.file.clone(),
454 before: self.after.clone(),
455 after: self.before.clone(),
456 }
457 }
458}
459
460#[derive(Clone, Debug, Default, PartialEq, Eq)]
461pub struct PatchGroup {
462 pub label: String,
463 pub patches: Vec<Patch>,
464}
465
466impl PatchGroup {
467 pub fn label(&self) -> &str {
468 &self.label
469 }
470
471 pub fn files(&self) -> impl Iterator<Item = &Path> {
472 self.patches.iter().map(Patch::file)
473 }
474
475 pub fn is_empty(&self) -> bool {
476 self.patches.is_empty()
477 }
478
479 pub fn after(&self) -> BTreeMap<PathBuf, String> {
480 self.patches
481 .iter()
482 .map(|patch| (patch.file.clone(), patch.after.clone()))
483 .collect()
484 }
485
486 pub fn inverse(&self) -> Self {
487 Self {
488 label: self.label.clone(),
489 patches: self.patches.iter().rev().map(Patch::inverse).collect(),
490 }
491 }
492
493 pub fn write(&self) -> Result<(), EditError> {
494 let fail = |file: &Path, message: String| EditError {
495 file: file.to_path_buf(),
496 key: self.label.clone(),
497 line: None,
498 code: crate::code::BAD_VALUE,
499 message,
500 diagnostics: Vec::new(),
501 };
502 let mut running: BTreeMap<&Path, (&str, &str)> = BTreeMap::new();
503 for patch in &self.patches {
504 let current = match running.get(patch.file.as_path()) {
505 Some((_, after)) => Some(after.to_string()),
506 None => std::fs::read_to_string(&patch.file).ok(),
507 };
508 if current.as_deref() != Some(patch.before.as_str()) {
509 return Err(fail(
510 &patch.file,
511 "no longer holds the text this edit starts from".to_string(),
512 ));
513 }
514 let first = running
515 .get(patch.file.as_path())
516 .map_or(patch.before.as_str(), |(first, _)| *first);
517 running.insert(&patch.file, (first, &patch.after));
518 }
519 let mut written: Vec<(&Path, &str)> = Vec::new();
520 for (file, (before, after)) in &running {
521 if let Err(error) = write(file, after) {
522 for (done, text) in written {
523 let _ = write(done, text);
524 }
525 return Err(fail(file, format!("cannot write: {error}")));
526 }
527 written.push((file, before));
528 }
529 Ok(())
530 }
531
532 fn merge(&mut self, other: &PatchGroup) {
533 if self.label.is_empty() {
534 self.label = other.label.clone();
535 }
536 for patch in &other.patches {
537 match self.patches.iter_mut().find(|own| own.file == patch.file) {
538 Some(own) => own.after = patch.after.clone(),
539 None => self.patches.push(patch.clone()),
540 }
541 }
542 }
543
544 fn settle(&mut self) {
545 self.patches.retain(|patch| patch.before != patch.after);
546 }
547}
548
549#[derive(Clone, Copy, PartialEq, Eq)]
550enum Role {
551 Scene,
552 Library,
553}
554
555#[derive(Clone)]
556struct File {
557 text: String,
558 doc: DocumentMut,
559 role: Role,
560 kind: FileKind,
561}
562
563#[derive(Clone)]
564struct Tree {
565 order: Vec<PathBuf>,
566 files: BTreeMap<PathBuf, File>,
567}
568
569fn strings(doc: &DocumentMut, key: &str) -> Vec<String> {
570 doc.get(key)
571 .and_then(Item::as_array)
572 .map(|array| {
573 array
574 .iter()
575 .filter_map(|value| value.as_str().map(str::to_string))
576 .collect()
577 })
578 .unwrap_or_default()
579}
580
581fn rooted(path: &str) -> Option<PathBuf> {
582 crate::paths::rooted(path).ok().map(PathBuf::from)
583}
584
585struct Refusal {
586 file: PathBuf,
587 code: &'static str,
588 message: String,
589}
590
591impl Refusal {
592 fn new(file: &Path, message: impl Into<String>) -> Self {
593 Self {
594 file: file.to_path_buf(),
595 code: crate::code::BAD_VALUE,
596 message: message.into(),
597 }
598 }
599
600 fn nested(&self) -> bool {
601 self.code == crate::code::INCLUDE_CYCLE || self.code == crate::code::INCLUDE_DEPTH
602 }
603
604 fn error(self, root: &Path, key: &str) -> EditError {
605 EditError {
606 file: root.join(&self.file),
607 key: key.to_string(),
608 line: None,
609 code: self.code,
610 message: self.message,
611 diagnostics: Vec::new(),
612 }
613 }
614}
615
616struct Visit {
617 path: PathBuf,
618 file: File,
619 includes: std::vec::IntoIter<String>,
620}
621
622impl Tree {
623 fn gather(
624 root: &Path,
625 file: &Path,
626 kind: FileKind,
627 overlay: &BTreeMap<PathBuf, String>,
628 ) -> Result<Self, Refusal> {
629 let mut tree = Self {
630 order: Vec::new(),
631 files: BTreeMap::new(),
632 };
633 let mut chain = vec![Self::visit(root, file, Role::Scene, kind, overlay)?];
634 while let Some(visit) = chain.last_mut() {
635 let Some(include) = visit.includes.next() else {
636 let Some(visit) = chain.pop() else { break };
637 tree.settle(root, visit, kind, overlay)?;
638 continue;
639 };
640 let Some(path) = rooted(&include) else {
641 continue;
642 };
643 let names = |from: usize| -> String {
644 let names: Vec<String> = chain[from..]
645 .iter()
646 .map(|visit| slashed(&visit.path))
647 .chain([slashed(&path)])
648 .collect();
649 crate::project::trail(&names)
650 };
651 let last = &chain[chain.len() - 1].path;
652 if let Some(start) = chain.iter().position(|visit| visit.path == path) {
653 return Err(Refusal {
654 file: last.clone(),
655 code: crate::code::INCLUDE_CYCLE,
656 message: format!(
657 "the {} includes itself through {include}: {}",
658 kind.name(),
659 names(start)
660 ),
661 });
662 }
663 if tree.files.contains_key(&path) {
664 continue;
665 }
666 if chain.len() > crate::MAX_DEPTH {
667 return Err(Refusal {
668 file: last.clone(),
669 code: crate::code::INCLUDE_DEPTH,
670 message: format!(
671 "including {include} nests the {}'s includes more than {} deep: {}",
672 kind.name(),
673 crate::MAX_DEPTH,
674 names(0)
675 ),
676 });
677 }
678 let visit = Self::visit(root, &path, Role::Scene, kind, overlay)?;
679 chain.push(visit);
680 }
681 Ok(tree)
682 }
683
684 fn visit(
685 root: &Path,
686 path: &Path,
687 role: Role,
688 kind: FileKind,
689 overlay: &BTreeMap<PathBuf, String>,
690 ) -> Result<Visit, Refusal> {
691 let file = Self::read(root, path, role, kind, overlay)?;
692 let includes = if role == Role::Scene {
693 strings(&file.doc, "include")
694 } else {
695 Vec::new()
696 };
697 Ok(Visit {
698 path: path.to_path_buf(),
699 file,
700 includes: includes.into_iter(),
701 })
702 }
703
704 fn settle(
705 &mut self,
706 root: &Path,
707 visit: Visit,
708 kind: FileKind,
709 overlay: &BTreeMap<PathBuf, String>,
710 ) -> Result<(), Refusal> {
711 let libraries = if visit.file.role == Role::Scene {
712 strings(&visit.file.doc, "materials")
713 } else {
714 Vec::new()
715 };
716 self.order.push(visit.path.clone());
717 self.files.insert(visit.path, visit.file);
718 for library in libraries {
719 let Some(library) = rooted(&library) else {
720 continue;
721 };
722 if self.files.contains_key(&library) {
723 continue;
724 }
725 let file = Self::read(root, &library, Role::Library, kind, overlay)?;
726 self.order.push(library.clone());
727 self.files.insert(library, file);
728 }
729 Ok(())
730 }
731
732 fn read(
733 root: &Path,
734 path: &Path,
735 role: Role,
736 kind: FileKind,
737 overlay: &BTreeMap<PathBuf, String>,
738 ) -> Result<File, Refusal> {
739 let text = match overlay.get(path) {
740 Some(text) => text.clone(),
741 None => std::fs::read_to_string(root.join(path)).map_err(|error| {
742 Refusal::new(path, format!("cannot read {}: {error}", path.display()))
743 })?,
744 };
745 let doc: DocumentMut = text
746 .parse()
747 .map_err(|error: toml_edit::TomlError| Refusal::new(path, error.message().trim()))?;
748 if doc.to_string() != text {
749 return Err(Refusal::new(
750 path,
751 "cannot be written back byte for byte, so it is not edited",
752 ));
753 }
754 if doc.get("format").and_then(Item::as_integer).unwrap_or(0) != i64::from(crate::FORMAT) {
755 return Err(Refusal::new(
756 path,
757 "is not format 1; migrate it before editing it",
758 ));
759 }
760 let kind = if role == Role::Library {
761 FileKind::Materials
762 } else {
763 kind
764 };
765 Ok(File {
766 text,
767 doc,
768 role,
769 kind,
770 })
771 }
772}
773
774enum Container<'a> {
775 Table(&'a mut Table),
776 Inline(&'a mut InlineTable),
777 Array(&'a mut Array),
778 Aot(&'a mut ArrayOfTables),
779}
780
781fn enter_item(item: &mut Item) -> Option<Container<'_>> {
782 match item {
783 Item::Table(table) => Some(Container::Table(table)),
784 Item::ArrayOfTables(array) => Some(Container::Aot(array)),
785 Item::Value(value) => enter_value(value),
786 Item::None => None,
787 }
788}
789
790fn enter_value(value: &mut Toml) -> Option<Container<'_>> {
791 match value {
792 Toml::InlineTable(table) => Some(Container::Inline(table)),
793 Toml::Array(array) => Some(Container::Array(array)),
794 _ => None,
795 }
796}
797
798impl<'a> Container<'a> {
799 fn child(self, segment: &str, create: Option<&str>) -> Option<Container<'a>> {
800 match self {
801 Container::Table(table) => {
802 if !table.contains_key(segment) {
803 let prefix = create?;
804 let mut made = Table::new();
805 made.set_implicit(true);
806 made.decor_mut().set_prefix(prefix);
807 table.insert(segment, Item::Table(made));
808 }
809 enter_item(table.get_mut(segment)?)
810 }
811 Container::Inline(table) => {
812 if !table.contains_key(segment) {
813 create?;
814 table.insert(segment, Toml::InlineTable(InlineTable::new()));
815 }
816 enter_value(table.get_mut(segment)?)
817 }
818 Container::Array(array) => enter_value(array.get_mut(segment.parse().ok()?)?),
819 Container::Aot(array) => Some(Container::Table(array.get_mut(segment.parse().ok()?)?)),
820 }
821 }
822}
823
824fn walk<'a>(
825 doc: &'a mut DocumentMut,
826 segments: &[String],
827 create: Option<&str>,
828) -> Option<Container<'a>> {
829 let mut at = Container::Table(doc.as_table_mut());
830 for segment in segments {
831 at = at.child(segment, create)?;
832 }
833 Some(at)
834}
835
836fn same(old: &Toml, new: &Value) -> bool {
837 match (old, new) {
838 (Toml::Boolean(a), Value::Bool(b)) => a.value() == b,
839 (Toml::String(a), Value::Text(b)) => a.value() == b,
840 (Toml::Integer(a), Value::Int(b)) => a.value() == b,
841 (Toml::Integer(a), Value::Float(b)) => *a.value() as f64 == f64::from(*b),
842 (Toml::Float(a), Value::Float(b)) => *a.value() as f32 == *b,
843 (Toml::Float(a), Value::Int(b)) => *a.value() == *b as f64,
844 (Toml::Array(a), Value::Array(b)) => {
845 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| same(x, y))
846 }
847 (Toml::InlineTable(a), Value::Table(b)) => {
848 a.len() == b.len()
849 && b.iter()
850 .all(|(key, value)| a.get(key).is_some_and(|old| same(old, value)))
851 }
852 _ => false,
853 }
854}
855
856fn merge(old: &mut Toml, new: &Value) -> Result<(), String> {
857 if same(old, new) {
858 return Ok(());
859 }
860 match (&mut *old, new) {
861 (Toml::Array(a), Value::Array(b)) if a.len() == b.len() => {
862 for (x, y) in a.iter_mut().zip(b) {
863 merge(x, y)?;
864 }
865 Ok(())
866 }
867 (Toml::InlineTable(a), Value::Table(b)) => {
868 let gone: Vec<String> = a
869 .iter()
870 .map(|(key, _)| key.to_string())
871 .filter(|key| !b.iter().any(|(name, _)| name == key))
872 .collect();
873 for key in gone {
874 a.remove(&key);
875 }
876 for (key, value) in b {
877 match a.get_mut(key) {
878 Some(old) => merge(old, value)?,
879 None => {
880 a.insert(key, value.toml()?);
881 }
882 }
883 }
884 Ok(())
885 }
886 _ => {
887 let decor = old.decor().clone();
888 let mut made = new.toml()?;
889 *made.decor_mut() = decor;
890 *old = made;
891 Ok(())
892 }
893 }
894}
895
896fn table_of(value: &Value) -> Result<Table, String> {
897 let Value::Table(fields) = value else {
898 return Err("an entry of a table list is a table".to_string());
899 };
900 let mut table = Table::new();
901 table.decor_mut().set_prefix("\n");
902 for (key, value) in fields {
903 table.insert(key, build(value)?);
904 }
905 Ok(table)
906}
907
908fn build(value: &Value) -> Result<Item, String> {
909 if value.tables() {
910 let Value::Array(items) = value else {
911 return Err("not a list".to_string());
912 };
913 let mut array = ArrayOfTables::new();
914 for item in items {
915 array.push(table_of(item)?);
916 }
917 return Ok(Item::ArrayOfTables(array));
918 }
919 Ok(Item::Value(value.toml()?))
920}
921
922fn index(key: &str) -> Result<usize, String> {
923 key.parse()
924 .map_err(|_| format!("{key} is not a list place"))
925}
926
927fn put(at: Container<'_>, key: &str, value: &Value) -> Result<(), String> {
928 match at {
929 Container::Table(table) => {
930 match table.get_mut(key) {
931 Some(Item::Value(old)) => merge(old, value)?,
932 Some(old) => *old = build(value)?,
933 None => {
934 table.insert(key, build(value)?);
935 }
936 }
937 Ok(())
938 }
939 Container::Inline(table) => {
940 match table.get_mut(key) {
941 Some(old) => merge(old, value)?,
942 None => {
943 table.insert(key, value.toml()?);
944 }
945 }
946 Ok(())
947 }
948 Container::Array(array) => {
949 let place = index(key)?;
950 if let Some(old) = array.get_mut(place) {
951 merge(old, value)?;
952 Ok(())
953 } else if place == array.len() {
954 array.push(value.toml()?);
955 Ok(())
956 } else {
957 Err(format!("the list has {} entries, not {place}", array.len()))
958 }
959 }
960 Container::Aot(array) => {
961 let place = index(key)?;
962 let table = table_of(value)?;
963 if place < array.len() {
964 array.replace(place, table);
965 Ok(())
966 } else if place == array.len() {
967 array.push(table);
968 Ok(())
969 } else {
970 Err(format!("the list has {} entries, not {place}", array.len()))
971 }
972 }
973 }
974}
975
976fn append(at: Container<'_>, key: &str, value: &Value) -> Result<(), String> {
977 let list = |entry: Value| Value::Array(vec![entry]);
978 match at {
979 Container::Table(table) => match table.get_mut(key) {
980 Some(item) => push_item(item, value),
981 None => {
982 table.insert(key, build(&list(value.clone()))?);
983 Ok(())
984 }
985 },
986 Container::Inline(table) => match table.get_mut(key) {
987 Some(Toml::Array(array)) => {
988 array.push(value.toml()?);
989 Ok(())
990 }
991 Some(_) => Err(format!("{key} is not a list")),
992 None => {
993 table.insert(key, list(value.clone()).toml()?);
994 Ok(())
995 }
996 },
997 _ => Err(format!("{key} is not in a table")),
998 }
999}
1000
1001fn push_item(item: &mut Item, value: &Value) -> Result<(), String> {
1002 match item {
1003 Item::ArrayOfTables(array) => {
1004 array.push(table_of(value)?);
1005 Ok(())
1006 }
1007 Item::Value(Toml::Array(array)) => {
1008 array.push(value.toml()?);
1009 Ok(())
1010 }
1011 _ => Err("is not a list".to_string()),
1012 }
1013}
1014
1015fn drop_key(at: Container<'_>, key: &str) -> Result<bool, String> {
1016 match at {
1017 Container::Table(table) => Ok(table.remove(key).is_some()),
1018 Container::Inline(table) => Ok(table.remove(key).is_some()),
1019 Container::Array(array) => {
1020 let place = index(key)?;
1021 if place < array.len() {
1022 array.remove(place);
1023 Ok(true)
1024 } else {
1025 Ok(false)
1026 }
1027 }
1028 Container::Aot(array) => {
1029 let place = index(key)?;
1030 if place < array.len() {
1031 array.remove(place);
1032 Ok(true)
1033 } else {
1034 Ok(false)
1035 }
1036 }
1037 }
1038}
1039
1040fn named(doc: &DocumentMut, key: &str, reference: &str) -> Option<usize> {
1041 let array = doc.get(key)?.as_array_of_tables()?;
1042 let field =
1043 |table: &Table, field: &str| table.get(field).and_then(Item::as_str) == Some(reference);
1044 array
1045 .iter()
1046 .position(|table| field(table, "id"))
1047 .or_else(|| array.iter().position(|table| field(table, "name")))
1048}
1049
1050fn keyed(doc: &DocumentMut, key: &str, reference: &str) -> Option<String> {
1051 let table = doc.get(key).and_then(Item::as_table_like)?;
1052 if table.contains_key(reference) {
1053 return Some(reference.to_string());
1054 }
1055 table
1056 .iter()
1057 .find(|(_, item)| {
1058 item.as_table_like()
1059 .and_then(|entry| entry.get("id"))
1060 .and_then(Item::as_str)
1061 == Some(reference)
1062 })
1063 .map(|(name, _)| name.to_string())
1064}
1065
1066fn path_of(doc: &DocumentMut, target: &Target) -> Option<Vec<String>> {
1067 let section = |key: &str| doc.contains_key(key).then(|| vec![key.to_string()]);
1068 let list = |key: &str, name: &str| {
1069 named(doc, key, name).map(|place| vec![key.to_string(), place.to_string()])
1070 };
1071 let map = |key: &str, name: &str| keyed(doc, key, name).map(|name| vec![key.to_string(), name]);
1072 match target {
1073 Target::Scene => Some(Vec::new()),
1074 Target::Object(name) => list("object", name),
1075 Target::Light(name) => list("light", name),
1076 Target::Emitter(name) => list("emitter", name),
1077 Target::Mover(name) => list("mover", name),
1078 Target::Rig(name) => list("rig", name),
1079 Target::Tiles(name) => list("tiles", name),
1080 Target::Mesh(name) => map("mesh", name),
1081 Target::Content(name) => map("content", name),
1082 Target::Text(name) => map("text", name),
1083 Target::Sound(name) => map("sound", name),
1084 Target::Material(name) => map("materials", name),
1085 Target::Node { mesh, node } => map("mesh", mesh).map(|mut path| {
1086 path.push("nodes".to_string());
1087 path.push(node.clone());
1088 path
1089 }),
1090 Target::Sun => section("sun"),
1091 Target::Sky => section("sky"),
1092 Target::Haze => section("haze"),
1093 Target::Camera => section("camera"),
1094 Target::Finish => section("finish"),
1095 Target::Trace => section("trace"),
1096 Target::Plates => section("plates"),
1097 Target::Physics => section("physics"),
1098 }
1099}
1100
1101fn section_key(target: &Target) -> Option<&'static str> {
1102 match target {
1103 Target::Sun => Some("sun"),
1104 Target::Sky => Some("sky"),
1105 Target::Haze => Some("haze"),
1106 Target::Camera => Some("camera"),
1107 Target::Finish => Some("finish"),
1108 Target::Trace => Some("trace"),
1109 Target::Plates => Some("plates"),
1110 Target::Physics => Some("physics"),
1111 _ => None,
1112 }
1113}
1114
1115struct Draft<'a> {
1116 tree: &'a Tree,
1117 root: &'a Path,
1118 work: BTreeMap<PathBuf, DocumentMut>,
1119}
1120
1121impl<'a> Draft<'a> {
1122 fn new(tree: &'a Tree, root: &'a Path) -> Self {
1123 Self {
1124 tree,
1125 root,
1126 work: BTreeMap::new(),
1127 }
1128 }
1129
1130 fn doc(&self, file: &Path) -> &DocumentMut {
1131 self.work
1132 .get(file)
1133 .unwrap_or_else(|| &self.tree.files[file].doc)
1134 }
1135
1136 fn doc_mut(&mut self, file: &Path) -> &mut DocumentMut {
1137 let tree = self.tree;
1138 self.work
1139 .entry(file.to_path_buf())
1140 .or_insert_with(|| tree.files[file].doc.clone())
1141 }
1142
1143 fn files(&self, role: Role) -> Vec<PathBuf> {
1144 self.tree
1145 .order
1146 .iter()
1147 .filter(|file| self.tree.files[*file].role == role)
1148 .cloned()
1149 .collect()
1150 }
1151
1152 fn locate(
1153 &self,
1154 target: &Target,
1155 first: Option<&str>,
1156 ) -> Result<(PathBuf, Vec<String>), Refusal> {
1157 if let Some(name) = reference_of(target)
1158 && name.contains('/')
1159 {
1160 return Err(Refusal::new(
1161 self.root,
1162 format!(
1163 "{target} is placed by a prefab; set it through its placement's [object.set]"
1164 ),
1165 ));
1166 }
1167 if *target == Target::Scene {
1168 if first == Some("fallback") {
1169 for file in self.files(Role::Scene) {
1170 if self.doc(&file).contains_key("fallback") {
1171 return Ok((file, Vec::new()));
1172 }
1173 }
1174 }
1175 return Ok((self.root.to_path_buf(), Vec::new()));
1176 }
1177 let role = if matches!(target, Target::Material(_)) {
1178 Role::Library
1179 } else {
1180 Role::Scene
1181 };
1182 for file in self.files(role) {
1183 if let Some(path) = path_of(self.doc(&file), target) {
1184 return Ok((file, path));
1185 }
1186 }
1187 if let Some(key) = section_key(target) {
1188 return Ok((self.root.to_path_buf(), vec![key.to_string()]));
1189 }
1190 if let Target::Node { mesh, node } = target {
1191 let (file, mut path) = self.locate(&Target::Mesh(mesh.clone()), None)?;
1192 path.push("nodes".to_string());
1193 path.push(node.clone());
1194 return Ok((file, path));
1195 }
1196 Err(Refusal::new(
1197 self.root,
1198 format!("the scene has no {target}"),
1199 ))
1200 }
1201
1202 fn at(&mut self, file: &Path, path: &[String], create: bool) -> Result<Container<'_>, Refusal> {
1203 let wanted = path.join(".");
1204 let made = if create { Some("\n") } else { None };
1205 walk(self.doc_mut(file), path, made)
1206 .ok_or_else(|| Refusal::new(file, format!("has no {wanted}")))
1207 }
1208
1209 fn patches(self, label: &str, root: &Path, taken: &BTreeSet<Id>) -> Vec<Patch> {
1210 let mut taken = taken.clone();
1211 let mut patches = Vec::new();
1212 for (file, mut doc) in self.work {
1213 let before = self.tree.files[&file].text.clone();
1214 if doc.to_string() == before {
1215 continue;
1216 }
1217 add_ids(
1218 &mut doc,
1219 &slashed(&file),
1220 self.tree.files[&file].kind,
1221 &mut taken,
1222 );
1223 let after = doc.to_string();
1224 patches.push(Patch {
1225 label: label.to_string(),
1226 file: root.join(&file),
1227 before,
1228 after,
1229 });
1230 }
1231 patches
1232 }
1233}
1234
1235fn reference_of(target: &Target) -> Option<&str> {
1236 match target {
1237 Target::Object(name)
1238 | Target::Mesh(name)
1239 | Target::Light(name)
1240 | Target::Emitter(name)
1241 | Target::Mover(name)
1242 | Target::Content(name)
1243 | Target::Text(name)
1244 | Target::Sound(name)
1245 | Target::Rig(name)
1246 | Target::Tiles(name) => Some(name),
1247 Target::Node { mesh, .. } => Some(mesh),
1248 _ => None,
1249 }
1250}
1251
1252fn clear_positions(table: &mut Table) {
1253 table.set_position(None);
1254 for (_, item) in table.iter_mut() {
1255 match item {
1256 Item::Table(inner) => clear_positions(inner),
1257 Item::ArrayOfTables(array) => {
1258 for inner in array.iter_mut() {
1259 clear_positions(inner);
1260 }
1261 }
1262 _ => {}
1263 }
1264 }
1265}
1266
1267fn swap(value: &mut Toml, old: &str, new: &str) -> bool {
1268 if value.as_str() != Some(old) {
1269 return false;
1270 }
1271 let decor = value.decor().clone();
1272 let mut made = Toml::from(new);
1273 *made.decor_mut() = decor;
1274 *value = made;
1275 true
1276}
1277
1278fn ordered(kind: Kind, fields: Vec<(String, Value)>) -> Vec<(String, Value)> {
1279 let order = kind.order();
1280 let mut known: Vec<(usize, (String, Value))> = Vec::new();
1281 let mut rest = Vec::new();
1282 for field in fields {
1283 match order.iter().position(|key| *key == field.0) {
1284 Some(place) => known.push((place, field)),
1285 None => rest.push(field),
1286 }
1287 }
1288 known.sort_by_key(|(place, _)| *place);
1289 known
1290 .into_iter()
1291 .map(|(_, field)| field)
1292 .chain(rest)
1293 .collect()
1294}
1295
1296pub struct SceneEdit {
1297 root: PathBuf,
1298 file: PathBuf,
1299 kind: FileKind,
1300 project: Project,
1301 tree: Tree,
1302 scene: Scene,
1303 undo: Vec<PatchGroup>,
1304 redo: Vec<PatchGroup>,
1305 open: Option<PatchGroup>,
1306 dry: Option<PatchGroup>,
1307}
1308
1309pub(crate) fn first_error(diagnostics: &[Diagnostic], root: &Path, key: &str) -> EditError {
1310 let diagnostic = diagnostics
1311 .iter()
1312 .find(|diagnostic| diagnostic.is_error())
1313 .or_else(|| diagnostics.first());
1314 match diagnostic {
1315 Some(diagnostic) => EditError {
1316 file: root.join(&diagnostic.file),
1317 key: key.to_string(),
1318 line: Some(diagnostic.line),
1319 code: diagnostic.code,
1320 message: diagnostic.message.clone(),
1321 diagnostics: diagnostics.to_vec(),
1322 },
1323 None => EditError {
1324 file: root.to_path_buf(),
1325 key: key.to_string(),
1326 line: None,
1327 code: crate::code::BAD_VALUE,
1328 message: "the scene does not load".to_string(),
1329 diagnostics: Vec::new(),
1330 },
1331 }
1332}
1333
1334fn gather(
1335 project: &Project,
1336 file: &Path,
1337 kind: FileKind,
1338 overlay: &BTreeMap<PathBuf, String>,
1339 key: &str,
1340) -> Result<Tree, EditError> {
1341 let root = project.root();
1342 Tree::gather(root, file, kind, overlay).map_err(|refusal| {
1343 if refusal.nested()
1344 && let Err(found) = project.scene_with(file, overlay.clone())
1345 {
1346 return first_error(&found, root, key);
1347 }
1348 refusal.error(root, key)
1349 })
1350}
1351
1352impl SceneEdit {
1353 pub fn open(path: impl AsRef<Path>) -> Result<Self, EditError> {
1354 let path = path.as_ref();
1355 let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
1356 let root = Project::root_of(&absolute);
1357 let project = Project::open(&root).map_err(|diagnostic| EditError {
1358 file: absolute.clone(),
1359 key: String::new(),
1360 line: None,
1361 code: diagnostic.code,
1362 message: diagnostic.message.clone(),
1363 diagnostics: vec![diagnostic],
1364 })?;
1365 let root = project.root().to_path_buf();
1366 let file = project.relative(&absolute);
1367 let kind = match FileKind::of(&file) {
1368 Some(FileKind::Prefab) => FileKind::Prefab,
1369 _ => FileKind::Scene,
1370 };
1371 let tree = gather(&project, &file, kind, &BTreeMap::new(), "")?;
1372 let scene = project
1373 .scene(&file)
1374 .map_err(|diagnostics| first_error(&diagnostics, &root, ""))?;
1375 Ok(Self {
1376 root,
1377 file,
1378 kind,
1379 project,
1380 tree,
1381 scene,
1382 undo: Vec::new(),
1383 redo: Vec::new(),
1384 open: None,
1385 dry: None,
1386 })
1387 }
1388
1389 pub fn path(&self) -> PathBuf {
1390 self.root.join(&self.file)
1391 }
1392
1393 pub fn root(&self) -> &Path {
1394 &self.root
1395 }
1396
1397 pub fn scene(&self) -> &Scene {
1398 &self.scene
1399 }
1400
1401 pub fn project(&self) -> &Project {
1402 &self.project
1403 }
1404
1405 pub fn files(&self) -> impl Iterator<Item = PathBuf> + '_ {
1406 self.tree.order.iter().map(|file| self.root.join(file))
1407 }
1408
1409 pub fn text(&self, file: impl AsRef<Path>) -> Option<&str> {
1410 let file = self.project.relative(file.as_ref());
1411 self.tree.files.get(&file).map(|file| file.text.as_str())
1412 }
1413
1414 pub fn reload(&mut self) -> Result<(), EditError> {
1415 if self.dry.is_some() {
1416 return Err(self.refuse("reload", "a dry run is open"));
1417 }
1418 let fresh = Self::open(self.path())?;
1419 *self = fresh;
1420 Ok(())
1421 }
1422
1423 pub fn can_undo(&self) -> bool {
1424 !self.undo.is_empty()
1425 }
1426
1427 pub fn can_redo(&self) -> bool {
1428 !self.redo.is_empty()
1429 }
1430
1431 pub fn undo_label(&self) -> Option<&str> {
1432 self.undo.last().map(PatchGroup::label)
1433 }
1434
1435 pub fn redo_label(&self) -> Option<&str> {
1436 self.redo.last().map(PatchGroup::label)
1437 }
1438
1439 fn refuse(&self, key: &str, message: &str) -> EditError {
1440 EditError {
1441 file: self.path(),
1442 key: key.to_string(),
1443 line: None,
1444 code: crate::code::BAD_VALUE,
1445 message: message.to_string(),
1446 diagnostics: Vec::new(),
1447 }
1448 }
1449
1450 pub fn begin(&mut self, label: &str) -> Result<(), EditError> {
1451 if self.dry.is_some() {
1452 return Err(self.refuse(label, "a dry run takes no group"));
1453 }
1454 if self.open.is_some() {
1455 return Err(self.refuse(label, "a group of edits is open already"));
1456 }
1457 self.open = Some(PatchGroup {
1458 label: label.to_string(),
1459 patches: Vec::new(),
1460 });
1461 Ok(())
1462 }
1463
1464 pub fn end(&mut self) -> Option<PatchGroup> {
1465 let mut group = self.open.take()?;
1466 group.settle();
1467 if group.is_empty() {
1468 return None;
1469 }
1470 self.redo.clear();
1471 self.undo.push(group.clone());
1472 Some(group)
1473 }
1474
1475 pub fn cancel(&mut self) -> Result<(), EditError> {
1476 let Some(mut group) = self.open.take() else {
1477 return Ok(());
1478 };
1479 group.settle();
1480 let inverse = group.inverse();
1481 let label = group.label.clone();
1482 if let Err(error) = self.land(&inverse.patches, &label) {
1483 self.open = Some(group);
1484 return Err(error);
1485 }
1486 Ok(())
1487 }
1488
1489 pub fn apply(&mut self, group: &PatchGroup) -> Result<(), EditError> {
1490 if self.dry.is_some() {
1491 return Err(self.refuse(&group.label, "a dry run applies nothing"));
1492 }
1493 self.land(&group.patches, &group.label)
1494 }
1495
1496 pub fn dry_run<T>(
1497 &mut self,
1498 edits: impl FnOnce(&mut Self) -> Result<T, EditError>,
1499 ) -> Result<PatchGroup, EditError> {
1500 if self.dry.is_some() || self.open.is_some() {
1501 return Err(self.refuse("dry run", "a group of edits is open"));
1502 }
1503 let project = self.project.clone();
1504 let tree = self.tree.clone();
1505 self.dry = Some(PatchGroup::default());
1506 let outcome = edits(self);
1507 let group = self.dry.take();
1508 self.project = project;
1509 self.tree = tree;
1510 outcome?;
1511 let mut group = group.unwrap_or_default();
1512 group.settle();
1513 if !group.is_empty() {
1514 let running = self.running(&group.patches);
1515 self.verify(&running, &group.label)?;
1516 }
1517 Ok(group)
1518 }
1519
1520 pub fn undo(&mut self) -> Result<Option<PatchGroup>, EditError> {
1521 if self.open.is_some() || self.dry.is_some() {
1522 return Err(self.refuse("undo", "a group of edits is open"));
1523 }
1524 let Some(group) = self.undo.pop() else {
1525 return Ok(None);
1526 };
1527 let inverse = group.inverse();
1528 if let Err(error) = self.land(&inverse.patches, &inverse.label) {
1529 self.undo.push(group);
1530 return Err(error);
1531 }
1532 self.redo.push(group);
1533 Ok(Some(inverse))
1534 }
1535
1536 pub fn redo(&mut self) -> Result<Option<PatchGroup>, EditError> {
1537 if self.open.is_some() || self.dry.is_some() {
1538 return Err(self.refuse("redo", "a group of edits is open"));
1539 }
1540 let Some(group) = self.redo.pop() else {
1541 return Ok(None);
1542 };
1543 if let Err(error) = self.land(&group.patches, &group.label) {
1544 self.redo.push(group);
1545 return Err(error);
1546 }
1547 self.undo.push(group.clone());
1548 Ok(Some(group))
1549 }
1550
1551 fn land(&mut self, patches: &[Patch], key: &str) -> Result<(), EditError> {
1552 let fail = |file: &Path, line: Option<u32>, message: String| EditError {
1553 file: file.to_path_buf(),
1554 key: key.to_string(),
1555 line,
1556 code: crate::code::BAD_VALUE,
1557 message,
1558 diagnostics: Vec::new(),
1559 };
1560 let mut running: BTreeMap<PathBuf, String> = BTreeMap::new();
1561 for patch in patches {
1562 let relative = self.project.relative(&patch.file);
1563 let current = match running.get(&relative) {
1564 Some(text) => text.as_str(),
1565 None => match self.tree.files.get(&relative) {
1566 Some(file) => file.text.as_str(),
1567 None => {
1568 return Err(fail(
1569 &patch.file,
1570 None,
1571 "is not a file of this scene".to_string(),
1572 ));
1573 }
1574 },
1575 };
1576 if current != patch.before {
1577 return Err(fail(
1578 &patch.file,
1579 None,
1580 "no longer holds the text this edit starts from".to_string(),
1581 ));
1582 }
1583 running.insert(relative, patch.after.clone());
1584 }
1585 for file in running.keys() {
1586 let held = self.tree.files.get(file).map(|file| file.text.as_str());
1587 match std::fs::read_to_string(self.root.join(file)) {
1588 Ok(disk) if Some(disk.as_str()) == held => {}
1589 _ => {
1590 return Err(fail(
1591 &self.root.join(file),
1592 None,
1593 "changed on disk since it was read; reload the scene".to_string(),
1594 ));
1595 }
1596 }
1597 }
1598 let (scene, tree) = self.verify(&running, key)?;
1599 let mut written: Vec<&PathBuf> = Vec::new();
1600 for (file, text) in &running {
1601 if let Err(error) = write(&self.root.join(file), text) {
1602 for done in written {
1603 let _ = write(&self.root.join(done), &self.tree.files[done].text);
1604 }
1605 return Err(fail(
1606 &self.root.join(file),
1607 None,
1608 format!("cannot write: {error}"),
1609 ));
1610 }
1611 written.push(file);
1612 }
1613 for (file, text) in &running {
1614 self.project.update(file, text);
1615 }
1616 self.tree = tree;
1617 self.scene = scene;
1618 Ok(())
1619 }
1620
1621 fn running(&self, patches: &[Patch]) -> BTreeMap<PathBuf, String> {
1622 patches
1623 .iter()
1624 .map(|patch| (self.project.relative(&patch.file), patch.after.clone()))
1625 .collect()
1626 }
1627
1628 fn verify(
1629 &self,
1630 running: &BTreeMap<PathBuf, String>,
1631 key: &str,
1632 ) -> Result<(Scene, Tree), EditError> {
1633 let scene = self
1634 .project
1635 .scene_with(&self.file, running.clone())
1636 .map_err(|diagnostics| first_error(&diagnostics, &self.root, key))?;
1637 let tree = gather(&self.project, &self.file, self.kind, running, key)?;
1638 Ok((scene, tree))
1639 }
1640
1641 fn hold(&mut self, group: &PatchGroup, key: &str) -> Result<(), EditError> {
1642 let running = self.running(&group.patches);
1643 let mut held: BTreeMap<PathBuf, String> = self
1644 .tree
1645 .files
1646 .iter()
1647 .map(|(file, held)| (file.clone(), held.text.clone()))
1648 .collect();
1649 held.extend(running.clone());
1650 self.tree = gather(&self.project, &self.file, self.kind, &held, key)?;
1651 for (file, text) in &running {
1652 self.project.update(file, text);
1653 }
1654 if let Some(dry) = &mut self.dry {
1655 dry.merge(group);
1656 }
1657 Ok(())
1658 }
1659
1660 fn record(&mut self, group: &PatchGroup) {
1661 match &mut self.open {
1662 Some(open) => open.merge(group),
1663 None => {
1664 self.redo.clear();
1665 self.undo.push(group.clone());
1666 }
1667 }
1668 }
1669
1670 fn run(
1671 &mut self,
1672 label: String,
1673 key: String,
1674 change: impl FnOnce(&mut Draft<'_>) -> Result<(), Refusal>,
1675 ) -> Result<PatchGroup, EditError> {
1676 let overlay: BTreeMap<PathBuf, String> = BTreeMap::new();
1677 let taken = self.project.ids(&overlay);
1678 let patches = {
1679 let mut draft = Draft::new(&self.tree, &self.file);
1680 change(&mut draft).map_err(|refusal| refusal.error(&self.root, &key))?;
1681 draft.patches(&label, &self.root, &taken)
1682 };
1683 let group = PatchGroup { label, patches };
1684 if group.is_empty() {
1685 return Ok(group);
1686 }
1687 if self.dry.is_some() {
1688 self.hold(&group, &key)?;
1689 return Ok(group);
1690 }
1691 self.land(&group.patches, &key)?;
1692 self.record(&group);
1693 Ok(group)
1694 }
1695
1696 pub fn set(
1697 &mut self,
1698 target: &Target,
1699 path: &[&str],
1700 value: impl Into<Value>,
1701 ) -> Result<PatchGroup, EditError> {
1702 let value = value.into();
1703 let key = format!("{target} {}", path.join("."));
1704 let target = target.clone();
1705 let path: Vec<String> = path.iter().map(|segment| segment.to_string()).collect();
1706 self.run(format!("set {key}"), key, move |draft| {
1707 let Some((last, parents)) = path.split_last() else {
1708 return Err(Refusal::new(draft.root, "an edit names a key"));
1709 };
1710 let (file, mut base) = draft.locate(&target, path.first().map(String::as_str))?;
1711 base.extend(parents.iter().cloned());
1712 let at = draft.at(&file, &base, true)?;
1713 put(at, last, &value).map_err(|message| Refusal::new(&file, message))
1714 })
1715 }
1716
1717 pub fn unset(&mut self, target: &Target, path: &[&str]) -> Result<PatchGroup, EditError> {
1718 let key = format!("{target} {}", path.join("."));
1719 let target = target.clone();
1720 let path: Vec<String> = path.iter().map(|segment| segment.to_string()).collect();
1721 self.run(format!("unset {key}"), key, move |draft| {
1722 let Some((last, parents)) = path.split_last() else {
1723 return Err(Refusal::new(draft.root, "an edit names a key"));
1724 };
1725 let (file, mut base) = draft.locate(&target, path.first().map(String::as_str))?;
1726 base.extend(parents.iter().cloned());
1727 let Ok(at) = draft.at(&file, &base, false) else {
1728 return Ok(());
1729 };
1730 drop_key(at, last)
1731 .map(|_| ())
1732 .map_err(|message| Refusal::new(&file, message))
1733 })
1734 }
1735
1736 pub fn push(
1737 &mut self,
1738 target: &Target,
1739 path: &[&str],
1740 value: impl Into<Value>,
1741 ) -> Result<PatchGroup, EditError> {
1742 let value = value.into();
1743 let key = format!("{target} {}", path.join("."));
1744 let target = target.clone();
1745 let path: Vec<String> = path.iter().map(|segment| segment.to_string()).collect();
1746 self.run(format!("add to {key}"), key, move |draft| {
1747 let Some((last, parents)) = path.split_last() else {
1748 return Err(Refusal::new(draft.root, "an edit names a key"));
1749 };
1750 let (file, mut base) = draft.locate(&target, path.first().map(String::as_str))?;
1751 base.extend(parents.iter().cloned());
1752 let at = draft.at(&file, &base, true)?;
1753 append(at, last, &value).map_err(|message| Refusal::new(&file, message))
1754 })
1755 }
1756
1757 pub fn remove(&mut self, target: &Target) -> Result<PatchGroup, EditError> {
1758 let key = target.to_string();
1759 let target = target.clone();
1760 self.run(format!("remove {key}"), key, move |draft| {
1761 let (file, path) = draft.locate(&target, None)?;
1762 let Some((last, parents)) = path.split_last() else {
1763 return Err(Refusal::new(&file, "the scene itself is not removed"));
1764 };
1765 let at = draft.at(&file, parents, false)?;
1766 drop_key(at, last).map_err(|message| Refusal::new(&file, message))?;
1767 if let [list] = parents {
1768 let doc = draft.doc_mut(&file);
1769 let empty = doc
1770 .get(list)
1771 .and_then(Item::as_array_of_tables)
1772 .is_some_and(ArrayOfTables::is_empty);
1773 if empty {
1774 doc.remove(list);
1775 }
1776 }
1777 Ok(())
1778 })
1779 }
1780
1781 fn file_for(&self, file: Option<&Path>, role: Role) -> Result<PathBuf, EditError> {
1782 match file {
1783 Some(file) => {
1784 let relative = self.project.relative(file);
1785 let beside = Path::new(&crate::paths::folder(&self.file)).join(file);
1786 [relative, beside]
1787 .into_iter()
1788 .find(|path| {
1789 self.tree
1790 .files
1791 .get(path)
1792 .is_some_and(|held| held.role == role)
1793 })
1794 .ok_or_else(|| {
1795 self.refuse(&file.display().to_string(), "is not a file of this scene")
1796 })
1797 }
1798 None => match role {
1799 Role::Scene => Ok(self.file.clone()),
1800 Role::Library => self
1801 .tree
1802 .order
1803 .iter()
1804 .find(|path| self.tree.files[*path].role == Role::Library)
1805 .cloned()
1806 .ok_or_else(|| self.refuse("material", "the scene names no library")),
1807 },
1808 }
1809 }
1810
1811 pub fn add(
1812 &mut self,
1813 kind: Kind,
1814 id: Id,
1815 name: &str,
1816 fields: &[(&str, Value)],
1817 file: Option<&Path>,
1818 ) -> Result<PatchGroup, EditError> {
1819 let key = kind.target(name).to_string();
1820 let role = if kind == Kind::Material {
1821 Role::Library
1822 } else {
1823 Role::Scene
1824 };
1825 let file = self.file_for(file, role)?;
1826 let taken = self.project.ids(&BTreeMap::new());
1827 if taken.contains(&id) {
1828 return Err(self.refuse(&key, &format!("id {id} is taken in this project")));
1829 }
1830 let name = name.to_string();
1831 let mut all: Vec<(String, Value)> = vec![("id".to_string(), Value::from(id))];
1832 if kind.list().is_some() {
1833 all.push(("name".to_string(), Value::from(name.as_str())));
1834 }
1835 all.extend(
1836 fields
1837 .iter()
1838 .filter(|(key, _)| *key != "id" && *key != "name")
1839 .map(|(key, value)| (key.to_string(), value.clone())),
1840 );
1841 let fields = ordered(kind, all);
1842 self.run(format!("add {key}"), key, move |draft| {
1843 let mut table = Table::new();
1844 table.decor_mut().set_prefix("\n");
1845 for (field, value) in &fields {
1846 let item = build(value).map_err(|message| Refusal::new(&file, message))?;
1847 table.insert(field, item);
1848 }
1849 let doc = draft.doc_mut(&file);
1850 if let Some(list) = kind.list() {
1851 if !doc.contains_key(list) {
1852 doc.insert(list, Item::ArrayOfTables(ArrayOfTables::new()));
1853 }
1854 match doc.get_mut(list).and_then(Item::as_array_of_tables_mut) {
1855 Some(array) => array.push(table),
1856 None => return Err(Refusal::new(&file, format!("{list} is not a table list"))),
1857 }
1858 } else if let Some(map) = kind.map() {
1859 let Some(parents) = walk(doc, &[map.to_string()], Some("\n")) else {
1860 return Err(Refusal::new(&file, format!("{map} is not a table")));
1861 };
1862 match parents {
1863 Container::Table(parent) => {
1864 if parent.contains_key(&name) {
1865 return Err(Refusal::new(&file, format!("{name} exists already")));
1866 }
1867 parent.insert(&name, Item::Table(table));
1868 }
1869 _ => return Err(Refusal::new(&file, format!("{map} is not a table"))),
1870 }
1871 }
1872 Ok(())
1873 })
1874 }
1875
1876 pub fn add_material(
1877 &mut self,
1878 name: &str,
1879 id: Id,
1880 material: &Material,
1881 file: Option<&Path>,
1882 ) -> Result<PatchGroup, EditError> {
1883 let key = format!("material {name}");
1884 let material = Material {
1885 id: Some(id),
1886 ..material.clone()
1887 };
1888 let full =
1889 toml::to_string(&material).map_err(|error| self.refuse(&key, &error.to_string()))?;
1890 let plain = toml::Table::try_from(Material::default()).unwrap_or_default();
1891 let mut doc: DocumentMut = full
1892 .parse()
1893 .map_err(|error: toml_edit::TomlError| self.refuse(&key, error.message()))?;
1894 let keep: BTreeSet<String> = material.to_table().keys().cloned().collect();
1895 let drop: Vec<String> = doc
1896 .as_table()
1897 .iter()
1898 .map(|(key, _)| key.to_string())
1899 .filter(|key| !keep.contains(key) && plain.contains_key(key))
1900 .collect();
1901 for key in drop {
1902 doc.remove(&key);
1903 }
1904 let taken = self.project.ids(&BTreeMap::new());
1905 if taken.contains(&id) {
1906 return Err(self.refuse(&key, &format!("id {id} is taken in this project")));
1907 }
1908 let file = self.file_for(file, Role::Library)?;
1909 let name = name.to_string();
1910 self.run(format!("add {key}"), key, move |draft| {
1911 let mut table = doc.as_table().clone();
1912 clear_positions(&mut table);
1913 table.decor_mut().set_prefix("\n");
1914 table.set_implicit(false);
1915 let target = draft.doc_mut(&file);
1916 let Some(Container::Table(parent)) =
1917 walk(target, &["materials".to_string()], Some("\n"))
1918 else {
1919 return Err(Refusal::new(&file, "materials is not a table"));
1920 };
1921 if parent.contains_key(&name) {
1922 return Err(Refusal::new(&file, format!("{name} exists already")));
1923 }
1924 parent.insert(&name, Item::Table(table));
1925 Ok(())
1926 })
1927 }
1928
1929 pub fn duplicate_object(
1930 &mut self,
1931 name: &str,
1932 new: &str,
1933 id: Id,
1934 ) -> Result<PatchGroup, EditError> {
1935 let key = format!("object {name}");
1936 let taken = self.project.ids(&BTreeMap::new());
1937 if taken.contains(&id) {
1938 return Err(self.refuse(&key, &format!("id {id} is taken in this project")));
1939 }
1940 let (name, new) = (name.to_string(), new.to_string());
1941 self.run(format!("duplicate {key} as {new}"), key, move |draft| {
1942 let (file, path) = draft.locate(&Target::Object(name.clone()), None)?;
1943 let place: usize = path[1].parse().unwrap_or(0);
1944 let doc = draft.doc_mut(&file);
1945 let Some(array) = doc.get_mut("object").and_then(Item::as_array_of_tables_mut) else {
1946 return Err(Refusal::new(&file, "object is not a table list"));
1947 };
1948 let Some(original) = array.get(place) else {
1949 return Err(Refusal::new(
1950 &file,
1951 format!("the scene has no object {name}"),
1952 ));
1953 };
1954 let mut copy = original.clone();
1955 clear_positions(&mut copy);
1956 copy.decor_mut().set_prefix("\n");
1957 copy.insert("name", Item::Value(Toml::from(new.as_str())));
1958 if let Some(Item::Value(value)) = copy.get_mut("name") {
1959 *value.decor_mut() = toml_edit::Decor::default();
1960 }
1961 match copy.get_mut("id") {
1962 Some(Item::Value(value)) => {
1963 let decor = value.decor().clone();
1964 let mut made = Toml::from(id.to_string());
1965 *made.decor_mut() = decor;
1966 *value = made;
1967 }
1968 _ => {
1969 copy.insert("id", toml_edit::value(id.to_string()));
1970 }
1971 }
1972 copy.remove("pick");
1973 array.insert(place + 1, copy);
1974 Ok(())
1975 })
1976 }
1977
1978 pub fn rename_object(&mut self, name: &str, new: &str) -> Result<PatchGroup, EditError> {
1979 let key = format!("object {name}");
1980 let (name, new) = (name.to_string(), new.to_string());
1981 self.run(format!("rename {key} to {new}"), key, move |draft| {
1982 let mut found = false;
1983 for file in draft.files(Role::Scene) {
1984 let doc = draft.doc_mut(&file);
1985 if let Some(array) = doc.get_mut("object").and_then(Item::as_array_of_tables_mut) {
1986 for table in array.iter_mut() {
1987 for field in ["name", "parent"] {
1988 if let Some(Item::Value(value)) = table.get_mut(field)
1989 && swap(value, &name, &new)
1990 && field == "name"
1991 {
1992 found = true;
1993 }
1994 }
1995 }
1996 }
1997 if let Some(array) = doc.get_mut("mover").and_then(Item::as_array_of_tables_mut) {
1998 for table in array.iter_mut() {
1999 if let Some(Item::Value(Toml::Array(objects))) = table.get_mut("objects") {
2000 for value in objects.iter_mut() {
2001 swap(value, &name, &new);
2002 }
2003 }
2004 }
2005 }
2006 if let Some(array) = doc.get_mut("rig").and_then(Item::as_array_of_tables_mut) {
2007 for table in array.iter_mut() {
2008 if let Some(Item::Value(value)) = table.get_mut("target") {
2009 swap(value, &name, &new);
2010 }
2011 }
2012 }
2013 if let Some(sounds) = doc.get_mut("sound").and_then(Item::as_table_like_mut) {
2014 for (_, sound) in sounds.iter_mut() {
2015 if let Some(Item::Value(value)) = sound
2016 .as_table_like_mut()
2017 .and_then(|sound| sound.get_mut("object"))
2018 {
2019 swap(value, &name, &new);
2020 }
2021 }
2022 }
2023 }
2024 if found {
2025 Ok(())
2026 } else {
2027 Err(Refusal::new(
2028 draft.root,
2029 format!("the scene has no object {name}"),
2030 ))
2031 }
2032 })
2033 }
2034
2035 pub fn set_at(&mut self, object: &str, at: [f32; 3]) -> Result<PatchGroup, EditError> {
2036 self.set(&Target::Object(object.to_string()), &["at"], at)
2037 }
2038
2039 pub fn set_rotate(&mut self, object: &str, rotate: [f32; 3]) -> Result<PatchGroup, EditError> {
2040 self.set(&Target::Object(object.to_string()), &["rotate"], rotate)
2041 }
2042
2043 pub fn set_scale(
2044 &mut self,
2045 object: &str,
2046 scale: impl Into<Value>,
2047 ) -> Result<PatchGroup, EditError> {
2048 self.set(&Target::Object(object.to_string()), &["scale"], scale)
2049 }
2050
2051 pub fn set_object_material(
2052 &mut self,
2053 object: &str,
2054 material: Option<&str>,
2055 ) -> Result<PatchGroup, EditError> {
2056 let target = Target::Object(object.to_string());
2057 match material {
2058 Some(material) => self.set(&target, &["material"], material),
2059 None => self.unset(&target, &["material"]),
2060 }
2061 }
2062
2063 pub fn set_object_materials(
2064 &mut self,
2065 object: &str,
2066 materials: &[(&str, &str)],
2067 ) -> Result<PatchGroup, EditError> {
2068 let target = Target::Object(object.to_string());
2069 if materials.is_empty() {
2070 return self.unset(&target, &["materials"]);
2071 }
2072 let table = materials
2073 .iter()
2074 .map(|(node, material)| (node.to_string(), Value::from(*material)))
2075 .collect();
2076 self.set(&target, &["materials"], Value::Table(table))
2077 }
2078
2079 pub fn set_shadow(&mut self, object: &str, shadow: &str) -> Result<PatchGroup, EditError> {
2080 self.set(&Target::Object(object.to_string()), &["shadow"], shadow)
2081 }
2082
2083 pub fn set_two_sided(&mut self, object: &str, on: bool) -> Result<PatchGroup, EditError> {
2084 self.set(&Target::Object(object.to_string()), &["two_sided"], on)
2085 }
2086
2087 pub fn set_hidden(&mut self, object: &str, on: bool) -> Result<PatchGroup, EditError> {
2088 self.set(&Target::Object(object.to_string()), &["hidden"], on)
2089 }
2090
2091 pub fn set_clip(&mut self, object: &str, planes: &[[f32; 4]]) -> Result<PatchGroup, EditError> {
2092 let target = Target::Object(object.to_string());
2093 if planes.is_empty() {
2094 return self.unset(&target, &["clip"]);
2095 }
2096 let planes: Vec<Value> = planes.iter().map(|plane| Value::from(*plane)).collect();
2097 self.set(&target, &["clip"], Value::Array(planes))
2098 }
2099
2100 pub fn set_parent(
2101 &mut self,
2102 object: &str,
2103 parent: Option<&str>,
2104 ) -> Result<PatchGroup, EditError> {
2105 let target = Target::Object(object.to_string());
2106 match parent {
2107 Some(parent) => self.set(&target, &["parent"], parent),
2108 None => self.unset(&target, &["parent"]),
2109 }
2110 }
2111
2112 pub fn add_mesh(
2113 &mut self,
2114 id: Id,
2115 name: &str,
2116 file: &str,
2117 node: Option<&str>,
2118 ) -> Result<PatchGroup, EditError> {
2119 let mut fields = vec![("file", Value::from(file))];
2120 if let Some(node) = node {
2121 fields.push(("node", Value::from(node)));
2122 }
2123 self.add(Kind::Mesh, id, name, &fields, None)
2124 }
2125
2126 pub fn set_node(
2127 &mut self,
2128 mesh: &str,
2129 node: &str,
2130 key: &str,
2131 value: impl Into<Value>,
2132 ) -> Result<PatchGroup, EditError> {
2133 let target = Target::Node {
2134 mesh: mesh.to_string(),
2135 node: node.to_string(),
2136 };
2137 self.set(&target, &[key], value)
2138 }
2139
2140 pub fn set_material(
2141 &mut self,
2142 name: &str,
2143 path: &[&str],
2144 value: impl Into<Value>,
2145 ) -> Result<PatchGroup, EditError> {
2146 self.set(&Target::Material(name.to_string()), path, value)
2147 }
2148}
2149
2150fn write(file: &Path, text: &str) -> std::io::Result<()> {
2151 let name = file
2152 .file_name()
2153 .map(|name| name.to_string_lossy().into_owned())
2154 .unwrap_or_default();
2155 let temporary = file.with_file_name(format!(".{name}.edit"));
2156 std::fs::write(&temporary, text)?;
2157 std::fs::rename(&temporary, file).inspect_err(|_| {
2158 let _ = std::fs::remove_file(&temporary);
2159 })
2160}