Skip to main content

pfx_scene/
edit.rs

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}