hydrate_model/editor/
edit_context.rs

1use hydrate_data::json_storage::RestoreAssetFromStorageImpl;
2use hydrate_data::{
3    CanonicalPathReference, OrderedSet, PathReference, PathReferenceNamespaceResolver,
4    PropertiesBundle, SingleObject,
5};
6use hydrate_pipeline::{DynEditContext, HydrateProjectConfiguration};
7use std::path::{Path, PathBuf};
8use uuid::Uuid;
9
10use crate::editor::undo::{UndoContext, UndoStack};
11use crate::{
12    AssetId, AssetLocation, AssetName, BuildInfo, DataSet, DataSetAssetInfo, DataSetDiff,
13    DataSetResult, EditContextKey, EndContextBehavior, HashMap, HashSet, ImportInfo, NullOverride,
14    OverrideBehavior, SchemaFingerprint, SchemaNamedType, SchemaRecord, SchemaSet, Value,
15};
16
17//TODO: Delete unused property data when path ancestor is null or in replace mode
18
19//TODO: Should we make a struct that refs the schema/data? We could have transactions and databases
20// return the temp struct with refs and move all the functions to that
21
22//TODO: Read-only sources? For things like network cache. Could only sync files we edit and overlay
23// files source over net cache source, etc.
24
25// Editor Context
26// - Used to edit assets in isolation (for example, a node graph)
27// - Expected that edited assets won't be modified by anything else
28//   - Might get away with applying diffs, but may result in unintuitive behavior
29// - Expected that non-edited assets *may* be modified, but not in a way that is incompatible with the edited assets
30//   - Or, make a copy of non-edited assets
31// - Maybe end-user needs to decide if they want to read new/old data?
32//
33// Undo Context
34// - Used to demarcate changes that should conceptually be treated as a single operation
35// - An undo context is labeled with a string. Within the context, multiple edits can be made
36// - An undo context *may* be left in a "resumable" state. If we make modifications with an undo context
37//   of the same name, we append to that undo context
38// - If an edit is made with an undo context that doesn't match an existing "resumable" undo context,
39//   we commit that context and carry on with the operation under a new undo context
40// - A wrapper around dataset that understands undo contexts will produce a queue of finished undo
41//   contexts, which contain revert/apply diffs
42// - These undo contexts can be pushed onto a single global queue or a per-document queue
43
44pub struct EditContext {
45    project_config: HydrateProjectConfiguration,
46    schema_set: SchemaSet,
47    pub(super) data_set: DataSet,
48    undo_context: UndoContext,
49}
50
51impl PathReferenceNamespaceResolver for EditContext {
52    fn namespace_root(
53        &self,
54        namespace: &str,
55    ) -> Option<PathBuf> {
56        self.project_config.namespace_root(namespace)
57    }
58
59    fn simplify_path(
60        &self,
61        path: &Path,
62    ) -> Option<(String, PathBuf)> {
63        self.project_config.simplify_path(path)
64    }
65}
66
67impl RestoreAssetFromStorageImpl for EditContext {
68    fn restore_asset(
69        &mut self,
70        asset_id: AssetId,
71        asset_name: AssetName,
72        asset_location: AssetLocation,
73        import_info: Option<ImportInfo>,
74        build_info: BuildInfo,
75        prototype: Option<AssetId>,
76        schema: SchemaFingerprint,
77        properties: HashMap<String, Value>,
78        property_null_overrides: HashMap<String, NullOverride>,
79        properties_in_replace_mode: HashSet<String>,
80        dynamic_collection_entries: HashMap<String, OrderedSet<Uuid>>,
81    ) -> DataSetResult<()> {
82        self.restore_asset(
83            asset_id,
84            asset_name,
85            asset_location,
86            import_info,
87            build_info,
88            prototype,
89            schema,
90            properties,
91            property_null_overrides,
92            properties_in_replace_mode,
93            dynamic_collection_entries,
94        )
95    }
96
97    fn namespace_resolver(&self) -> &dyn PathReferenceNamespaceResolver {
98        self
99    }
100}
101
102impl DynEditContext for EditContext {
103    fn data_set(&self) -> &DataSet {
104        &self.data_set
105    }
106
107    fn schema_set(&self) -> &SchemaSet {
108        &self.schema_set
109    }
110}
111
112impl EditContext {
113    // Call after adding a new asset
114    fn track_new_asset(
115        &mut self,
116        asset_id: AssetId,
117    ) {
118        if self.undo_context.has_open_context() {
119            // If an undo context is open, we use the diff for change tracking
120            self.undo_context.track_new_asset(asset_id);
121        }
122    }
123
124    // Call before editing or deleting an asset
125    fn track_existing_asset(
126        &mut self,
127        asset_id: AssetId,
128    ) -> DataSetResult<()> {
129        if self.undo_context.has_open_context() {
130            // If an undo is open, we use the diff for change tracking
131            self.undo_context
132                .track_existing_asset(&mut self.data_set, asset_id)?;
133        }
134
135        Ok(())
136    }
137
138    pub fn apply_diff(
139        &mut self,
140        diff: &DataSetDiff,
141    ) -> DataSetResult<()> {
142        diff.apply(&mut self.data_set, &self.schema_set)?;
143        Ok(())
144    }
145
146    pub fn new(
147        project_config: &HydrateProjectConfiguration,
148        edit_context_key: EditContextKey,
149        schema_set: SchemaSet,
150        undo_stack: &UndoStack,
151    ) -> Self {
152        EditContext {
153            project_config: project_config.clone(),
154            schema_set,
155            data_set: Default::default(),
156            undo_context: UndoContext::new(undo_stack, edit_context_key),
157        }
158    }
159
160    pub fn new_with_data(
161        project_config: &HydrateProjectConfiguration,
162        edit_context_key: EditContextKey,
163        schema_set: SchemaSet,
164        undo_stack: &UndoStack,
165    ) -> Self {
166        EditContext {
167            project_config: project_config.clone(),
168            schema_set,
169            data_set: Default::default(),
170            undo_context: UndoContext::new(undo_stack, edit_context_key),
171        }
172    }
173
174    pub fn with_undo_context<F: FnOnce(&mut Self) -> EndContextBehavior>(
175        &mut self,
176        name: &'static str,
177        f: F,
178    ) {
179        self.undo_context.begin_context(&self.data_set, name);
180        let end_context_behavior = (f)(self);
181        self.undo_context
182            .end_context(&self.data_set, end_context_behavior);
183    }
184
185    pub fn commit_pending_undo_context(&mut self) {
186        self.undo_context.commit_context(&mut self.data_set);
187    }
188
189    pub fn cancel_pending_undo_context(&mut self) -> DataSetResult<()> {
190        self.undo_context.cancel_context(&mut self.data_set)
191    }
192
193    // pub fn apply_diff(&mut self, diff: &DataSetDiff) {
194    //     diff.apply(&mut self.data_set);
195    // }
196
197    //
198    // Schema-related functions
199    //
200    pub fn schema_set(&self) -> &SchemaSet {
201        &self.schema_set
202    }
203
204    pub fn schemas(&self) -> &HashMap<SchemaFingerprint, SchemaNamedType> {
205        &self.schema_set.schemas()
206    }
207
208    //
209    // Data-related functions
210    //
211    pub fn data_set(&self) -> &DataSet {
212        &self.data_set
213    }
214
215    // pub fn data_set_mut(&mut self) -> &mut DataSet {
216    //     &mut self.data_set
217    // }
218
219    pub fn assets(&self) -> &HashMap<AssetId, DataSetAssetInfo> {
220        self.data_set.assets()
221    }
222
223    pub fn has_asset(
224        &self,
225        asset_id: AssetId,
226    ) -> bool {
227        self.assets().contains_key(&asset_id)
228    }
229
230    pub fn new_asset_with_id(
231        &mut self,
232        asset_id: AssetId,
233        asset_name: &AssetName,
234        asset_location: &AssetLocation,
235        schema: &SchemaRecord,
236    ) -> DataSetResult<()> {
237        self.data_set.new_asset_with_id(
238            asset_id,
239            asset_name.clone(),
240            asset_location.clone(),
241            schema,
242        )?;
243        self.track_new_asset(asset_id);
244        Ok(())
245    }
246
247    pub fn new_asset(
248        &mut self,
249        asset_name: &AssetName,
250        asset_location: &AssetLocation,
251        schema: &SchemaRecord,
252    ) -> AssetId {
253        let asset_id = self
254            .data_set
255            .new_asset(asset_name.clone(), asset_location.clone(), schema);
256        self.track_new_asset(asset_id);
257        asset_id
258    }
259
260    pub fn new_asset_from_prototype(
261        &mut self,
262        asset_name: &AssetName,
263        asset_location: &AssetLocation,
264        prototype: AssetId,
265    ) -> DataSetResult<AssetId> {
266        let asset_id = self.data_set.new_asset_from_prototype(
267            asset_name.clone(),
268            asset_location.clone(),
269            prototype,
270        )?;
271        self.track_new_asset(asset_id);
272        Ok(asset_id)
273    }
274
275    pub fn init_from_single_object(
276        &mut self,
277        asset_id: AssetId,
278        asset_name: AssetName,
279        asset_location: AssetLocation,
280        single_object: &SingleObject,
281    ) -> DataSetResult<()> {
282        self.track_new_asset(asset_id);
283        self.data_set.new_asset_with_id(
284            asset_id,
285            asset_name,
286            asset_location,
287            single_object.schema(),
288        )?;
289        self.data_set
290            .copy_from_single_object(asset_id, single_object)
291    }
292
293    pub fn restore_assets_from(
294        &mut self,
295        data_set: DataSet,
296    ) -> DataSetResult<()> {
297        for (k, v) in data_set.take_assets() {
298            self.restore_asset(
299                k,
300                v.asset_name().clone(),
301                v.asset_location().clone(),
302                v.import_info().clone().clone(),
303                v.build_info().clone(),
304                v.prototype(),
305                v.schema().fingerprint(),
306                v.properties().clone(),
307                v.property_null_overrides().clone(),
308                v.properties_in_replace_mode().clone(),
309                v.dynamic_collection_entries().clone(),
310            )?;
311        }
312
313        Ok(())
314    }
315
316    pub(crate) fn restore_asset(
317        &mut self,
318        asset_id: AssetId,
319        asset_name: AssetName,
320        asset_location: AssetLocation,
321        import_info: Option<ImportInfo>,
322        build_info: BuildInfo,
323        prototype: Option<AssetId>,
324        schema: SchemaFingerprint,
325        properties: HashMap<String, Value>,
326        property_null_overrides: HashMap<String, NullOverride>,
327        properties_in_replace_mode: HashSet<String>,
328        dynamic_collection_entries: HashMap<String, OrderedSet<Uuid>>,
329    ) -> DataSetResult<()> {
330        self.track_new_asset(asset_id);
331        self.data_set.restore_asset(
332            asset_id,
333            asset_name,
334            asset_location,
335            import_info,
336            build_info,
337            &self.schema_set,
338            prototype,
339            schema,
340            properties,
341            property_null_overrides,
342            properties_in_replace_mode,
343            dynamic_collection_entries,
344        )
345    }
346
347    pub fn duplicate_asset(
348        &mut self,
349        asset_id: AssetId,
350    ) -> DataSetResult<AssetId> {
351        let new_asset_id = self.data_set.duplicate_asset(asset_id, &self.schema_set)?;
352        self.track_new_asset(new_asset_id);
353        Ok(new_asset_id)
354    }
355
356    pub fn delete_asset(
357        &mut self,
358        asset_id: AssetId,
359    ) -> DataSetResult<()> {
360        self.track_existing_asset(asset_id)?;
361        self.data_set.delete_asset(asset_id)
362    }
363
364    pub fn set_asset_location(
365        &mut self,
366        asset_id: AssetId,
367        new_location: AssetLocation,
368    ) -> DataSetResult<()> {
369        self.track_existing_asset(asset_id)?;
370        self.data_set.set_asset_location(asset_id, new_location)?;
371        // Again so that we track the new location too
372        self.track_existing_asset(asset_id)?;
373        Ok(())
374    }
375
376    pub fn set_import_info(
377        &mut self,
378        asset_id: AssetId,
379        import_info: ImportInfo,
380    ) -> DataSetResult<()> {
381        self.data_set.set_import_info(asset_id, import_info)?;
382        self.track_existing_asset(asset_id)?;
383        Ok(())
384    }
385
386    pub fn asset_name(
387        &self,
388        asset_id: AssetId,
389    ) -> DataSetResult<&AssetName> {
390        self.data_set.asset_name(asset_id)
391    }
392
393    pub fn asset_name_or_id_string(
394        &self,
395        asset_id: AssetId,
396    ) -> DataSetResult<String> {
397        let asset_name = self.data_set.asset_name(asset_id)?;
398        Ok(if let Some(name) = asset_name.as_string() {
399            name.to_string()
400        } else {
401            asset_id.as_uuid().to_string()
402        })
403    }
404
405    pub fn set_asset_name(
406        &mut self,
407        asset_id: AssetId,
408        asset_name: AssetName,
409    ) -> DataSetResult<()> {
410        self.track_existing_asset(asset_id)?;
411        self.data_set.set_asset_name(asset_id, asset_name)
412    }
413
414    pub fn asset_location(
415        &self,
416        asset_id: AssetId,
417    ) -> Option<AssetLocation> {
418        self.data_set.asset_location(asset_id)
419    }
420
421    pub fn asset_location_chain(
422        &self,
423        asset_id: AssetId,
424    ) -> DataSetResult<Vec<AssetLocation>> {
425        self.data_set.asset_location_chain(asset_id)
426    }
427
428    pub fn import_info(
429        &self,
430        asset_id: AssetId,
431    ) -> Option<&ImportInfo> {
432        self.data_set.import_info(asset_id)
433    }
434
435    pub fn resolve_path_reference<P: Into<PathReference>>(
436        &self,
437        asset_id: AssetId,
438        path: P,
439    ) -> DataSetResult<Option<AssetId>> {
440        self.data_set.resolve_path_reference(asset_id, path)
441    }
442
443    pub fn resolve_canonical_path_reference(
444        &self,
445        asset_id: AssetId,
446        canonical_path: &CanonicalPathReference,
447    ) -> DataSetResult<Option<AssetId>> {
448        self.data_set
449            .resolve_canonical_path_reference(asset_id, canonical_path)
450    }
451
452    pub fn resolve_all_path_reference_overrides(
453        &self,
454        asset_id: AssetId,
455    ) -> DataSetResult<HashMap<CanonicalPathReference, AssetId>> {
456        self.data_set.resolve_all_path_reference_overrides(asset_id)
457    }
458
459    pub fn get_all_path_reference_overrides(
460        &mut self,
461        asset_id: AssetId,
462    ) -> Option<&HashMap<CanonicalPathReference, AssetId>> {
463        self.data_set.get_all_path_reference_overrides(asset_id)
464    }
465
466    pub fn set_path_reference_override(
467        &mut self,
468        asset_id: AssetId,
469        path: CanonicalPathReference,
470        referenced_asset_id: AssetId,
471    ) -> DataSetResult<()> {
472        self.track_existing_asset(asset_id)?;
473        self.data_set
474            .set_path_reference_override(asset_id, path, referenced_asset_id)
475    }
476
477    pub fn asset_prototype(
478        &self,
479        asset_id: AssetId,
480    ) -> Option<AssetId> {
481        self.data_set.asset_prototype(asset_id)
482    }
483
484    pub fn asset_schema(
485        &self,
486        asset_id: AssetId,
487    ) -> Option<&SchemaRecord> {
488        self.data_set.asset_schema(asset_id)
489    }
490
491    pub fn get_null_override(
492        &self,
493        asset_id: AssetId,
494        path: impl AsRef<str>,
495    ) -> DataSetResult<NullOverride> {
496        self.data_set
497            .get_null_override(&self.schema_set, asset_id, path)
498    }
499
500    pub fn set_null_override(
501        &mut self,
502        asset_id: AssetId,
503        path: impl AsRef<str>,
504        null_override: NullOverride,
505    ) -> DataSetResult<()> {
506        self.track_existing_asset(asset_id)?;
507        self.data_set
508            .set_null_override(&self.schema_set, asset_id, path, null_override)
509    }
510
511    pub fn resolve_null_override(
512        &self,
513        asset_id: AssetId,
514        path: impl AsRef<str>,
515    ) -> DataSetResult<NullOverride> {
516        self.data_set
517            .resolve_null_override(&self.schema_set, asset_id, path)
518    }
519
520    pub fn has_property_override(
521        &self,
522        asset_id: AssetId,
523        path: impl AsRef<str>,
524    ) -> DataSetResult<bool> {
525        self.data_set.has_property_override(asset_id, path)
526    }
527
528    // Just gets if this asset has a property without checking prototype chain for fallback or returning a default
529    // Returning none means it is not overridden
530    pub fn get_property_override(
531        &self,
532        asset_id: AssetId,
533        path: impl AsRef<str>,
534    ) -> DataSetResult<Option<&Value>> {
535        self.data_set.get_property_override(asset_id, path)
536    }
537
538    // Just sets a property on this asset, making it overridden, or replacing the existing override
539    pub fn set_property_override(
540        &mut self,
541        asset_id: AssetId,
542        path: impl AsRef<str>,
543        value: Option<Value>,
544    ) -> DataSetResult<Option<Value>> {
545        self.track_existing_asset(asset_id)?;
546        self.data_set
547            .set_property_override(&self.schema_set, asset_id, path, value)
548    }
549
550    pub fn apply_property_override_to_prototype(
551        &mut self,
552        asset_id: AssetId,
553        path: impl AsRef<str>,
554    ) -> DataSetResult<()> {
555        self.track_existing_asset(asset_id)?;
556        if let Some(prototype) = self.asset_prototype(asset_id) {
557            self.track_existing_asset(prototype)?;
558        }
559
560        self.data_set
561            .apply_property_override_to_prototype(&self.schema_set, asset_id, path)
562    }
563
564    pub fn resolve_property(
565        &self,
566        asset_id: AssetId,
567        path: impl AsRef<str>,
568    ) -> DataSetResult<&Value> {
569        self.data_set
570            .resolve_property(&self.schema_set, asset_id, path)
571    }
572
573    pub fn get_dynamic_array_entries(
574        &self,
575        asset_id: AssetId,
576        path: impl AsRef<str>,
577    ) -> DataSetResult<std::slice::Iter<Uuid>> {
578        self.data_set
579            .get_dynamic_array_entries(&self.schema_set, asset_id, path)
580    }
581
582    pub fn get_map_entries(
583        &self,
584        asset_id: AssetId,
585        path: impl AsRef<str>,
586    ) -> DataSetResult<std::slice::Iter<Uuid>> {
587        self.data_set
588            .get_map_entries(&self.schema_set, asset_id, path)
589    }
590
591    pub fn add_dynamic_array_entry(
592        &mut self,
593        asset_id: AssetId,
594        path: impl AsRef<str>,
595    ) -> DataSetResult<Uuid> {
596        self.track_existing_asset(asset_id)?;
597        self.data_set
598            .add_dynamic_array_entry(&self.schema_set, asset_id, path)
599    }
600
601    pub fn add_map_entry(
602        &mut self,
603        asset_id: AssetId,
604        path: impl AsRef<str>,
605    ) -> DataSetResult<Uuid> {
606        self.track_existing_asset(asset_id)?;
607        self.data_set
608            .add_map_entry(&self.schema_set, asset_id, path)
609    }
610
611    pub fn insert_dynamic_array_entry(
612        &mut self,
613        asset_id: AssetId,
614        path: impl AsRef<str>,
615        index: usize,
616        entry_uuid: Uuid,
617    ) -> DataSetResult<()> {
618        self.track_existing_asset(asset_id)?;
619        self.data_set.insert_dynamic_array_entry(
620            &self.schema_set,
621            asset_id,
622            path,
623            index,
624            entry_uuid,
625        )
626    }
627
628    pub fn remove_dynamic_array_entry(
629        &mut self,
630        asset_id: AssetId,
631        path: impl AsRef<str>,
632        element_id: Uuid,
633    ) -> DataSetResult<bool> {
634        self.track_existing_asset(asset_id)?;
635        self.data_set
636            .remove_dynamic_array_entry(&self.schema_set, asset_id, path, element_id)
637    }
638
639    pub fn remove_map_entry(
640        &mut self,
641        asset_id: AssetId,
642        path: impl AsRef<str>,
643        element_id: Uuid,
644    ) -> DataSetResult<bool> {
645        self.track_existing_asset(asset_id)?;
646        self.data_set
647            .remove_map_entry(&self.schema_set, asset_id, path, element_id)
648    }
649
650    pub fn resolve_dynamic_array_entries(
651        &self,
652        asset_id: AssetId,
653        path: impl AsRef<str>,
654    ) -> DataSetResult<Box<[Uuid]>> {
655        self.data_set
656            .resolve_dynamic_array_entries(&self.schema_set, asset_id, path)
657    }
658
659    pub fn resolve_map_entries(
660        &self,
661        asset_id: AssetId,
662        path: impl AsRef<str>,
663    ) -> DataSetResult<Box<[Uuid]>> {
664        self.data_set
665            .resolve_map_entries(&self.schema_set, asset_id, path)
666    }
667
668    pub fn get_override_behavior(
669        &self,
670        asset_id: AssetId,
671        path: impl AsRef<str>,
672    ) -> DataSetResult<OverrideBehavior> {
673        self.data_set
674            .get_override_behavior(&self.schema_set, asset_id, path)
675    }
676
677    pub fn set_override_behavior(
678        &mut self,
679        asset_id: AssetId,
680        path: impl AsRef<str>,
681        behavior: OverrideBehavior,
682    ) -> DataSetResult<()> {
683        self.track_existing_asset(asset_id)?;
684        self.data_set
685            .set_override_behavior(&self.schema_set, asset_id, path, behavior)
686    }
687
688    pub fn read_properties_bundle(
689        &self,
690        schema_set: &SchemaSet,
691        asset_id: AssetId,
692        path: impl AsRef<str>,
693    ) -> DataSetResult<PropertiesBundle> {
694        self.data_set
695            .read_properties_bundle(schema_set, asset_id, path)
696    }
697
698    pub fn write_properties_bundle(
699        &mut self,
700        schema_set: &SchemaSet,
701        asset_id: AssetId,
702        path: impl AsRef<str>,
703        properties_bundle: &PropertiesBundle,
704    ) -> DataSetResult<()> {
705        self.track_existing_asset(asset_id)?;
706        self.data_set
707            .write_properties_bundle(schema_set, asset_id, path, properties_bundle)
708    }
709}