diff --git a/Assets/Scripts/Layers/LayerTypes/FolderLayer.cs b/Assets/Scripts/Layers/LayerTypes/FolderLayer.cs index e8bd9016..5f475662 100644 --- a/Assets/Scripts/Layers/LayerTypes/FolderLayer.cs +++ b/Assets/Scripts/Layers/LayerTypes/FolderLayer.cs @@ -1,9 +1,9 @@ -using System; +using System.Runtime.Serialization; using Netherlands3D.Twin.Projects; namespace Netherlands3D.Twin.Layers { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Folder")] public class FolderLayer : LayerData { public FolderLayer(string name) : base(name) diff --git a/Assets/Scripts/Layers/LayerTypes/LayerData.cs b/Assets/Scripts/Layers/LayerTypes/LayerData.cs index 5e9bbb25..07c03a43 100644 --- a/Assets/Scripts/Layers/LayerTypes/LayerData.cs +++ b/Assets/Scripts/Layers/LayerTypes/LayerData.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using Netherlands3D.Twin.Layers.Properties; using Netherlands3D.Twin.Projects; using Newtonsoft.Json; @@ -12,13 +13,13 @@ namespace Netherlands3D.Twin.Layers [Serializable] public class LayerData { - [SerializeField, JsonProperty] protected Guid UUID = Guid.NewGuid(); - [SerializeField, JsonProperty] protected string name; - [SerializeField, JsonProperty] protected bool activeSelf = true; - [SerializeField, JsonProperty] protected Color color = new Color(86f / 256f, 160f / 256f, 227f / 255f); - [SerializeField, JsonProperty] protected List children = new(); + [SerializeField, DataMember] protected Guid UUID = Guid.NewGuid(); + [SerializeField, DataMember] protected string name; + [SerializeField, DataMember] protected bool activeSelf = true; + [SerializeField, DataMember] protected Color color = new Color(86f / 256f, 160f / 256f, 227f / 255f); + [SerializeField, DataMember] protected List children = new(); [JsonIgnore] protected LayerData parent; //not serialized to avoid a circular reference - [SerializeField, JsonProperty] protected List layerProperties = new(); + [SerializeField, DataMember] protected List layerProperties = new(); [JsonIgnore] public RootLayer Root => ProjectData.Current.RootLayer; [JsonIgnore] public LayerData ParentLayer => parent; diff --git a/Assets/Scripts/Layers/LayerTypes/PolygonSelectionLayer.cs b/Assets/Scripts/Layers/LayerTypes/PolygonSelectionLayer.cs index 9b3b1636..099916f5 100644 --- a/Assets/Scripts/Layers/LayerTypes/PolygonSelectionLayer.cs +++ b/Assets/Scripts/Layers/LayerTypes/PolygonSelectionLayer.cs @@ -1,11 +1,10 @@ -using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using Netherlands3D.Coordinates; using Netherlands3D.SelectionTools; using Netherlands3D.Twin.FloatingOrigin; using Netherlands3D.Twin.Layers.Properties; -using Netherlands3D.Twin.Projects; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Events; @@ -19,11 +18,12 @@ public enum ShapeType Line = 2 } - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "PolygonSelection")] public class PolygonSelectionLayer : ReferencedLayerData, ILayerWithPropertyData//, ILayerWithPropertyPanels { - [JsonProperty] public List OriginalPolygon { get; private set; } - [SerializeField, JsonProperty] private ShapeType shapeType; + [DataMember] public List OriginalPolygon { get; private set; } + [DataMember] private ShapeType shapeType; + [JsonIgnore] private PolygonSelectionLayerPropertyData polygonPropertyData; [JsonIgnore] public LayerPropertyData PropertyData => polygonPropertyData; [JsonIgnore] public CompoundPolygon Polygon { get; set; } diff --git a/Assets/Scripts/Layers/LayerTypes/ReferencedLayerData.cs b/Assets/Scripts/Layers/LayerTypes/ReferencedLayerData.cs index 8bf254b8..4193c7d7 100644 --- a/Assets/Scripts/Layers/LayerTypes/ReferencedLayerData.cs +++ b/Assets/Scripts/Layers/LayerTypes/ReferencedLayerData.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.Serialization; using Netherlands3D.Twin.Layers.Properties; using Netherlands3D.Twin.Projects; using Newtonsoft.Json; @@ -7,10 +8,10 @@ namespace Netherlands3D.Twin.Layers { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Prefab")] public class ReferencedLayerData : LayerData { - [SerializeField, JsonProperty] private string prefabId; + [DataMember] private string prefabId; [JsonIgnore] public LayerGameObject Reference { get; } [JsonIgnore] public bool KeepReferenceOnDestroy { get; set; } = false; diff --git a/Assets/Scripts/Layers/LayerTypes/RootLayer.cs b/Assets/Scripts/Layers/LayerTypes/RootLayer.cs index ad864c03..4f94de9e 100644 --- a/Assets/Scripts/Layers/LayerTypes/RootLayer.cs +++ b/Assets/Scripts/Layers/LayerTypes/RootLayer.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using Netherlands3D.Twin.Projects; using Newtonsoft.Json; using UnityEngine; namespace Netherlands3D.Twin.Layers { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Root")] public class RootLayer : LayerData { [JsonIgnore] public List SelectedLayers { get; private set; } = new(); diff --git a/Assets/Scripts/Layers/Properties/PropertyData/CartesianTileSubObjectColorPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/CartesianTileSubObjectColorPropertyData.cs index f3d73934..65d10ac7 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/CartesianTileSubObjectColorPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/CartesianTileSubObjectColorPropertyData.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; +using System.Runtime.Serialization; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Events; namespace Netherlands3D.Twin.Layers.Properties { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "CartesianTileSubObjectColor")] public class CartesianTileSubObjectColorPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets { - [SerializeField, JsonProperty] private Uri data; + [DataMember] private Uri data; [JsonIgnore] public readonly UnityEvent OnDataChanged = new(); diff --git a/Assets/Scripts/Layers/Properties/PropertyData/LayerPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/LayerPropertyData.cs index 61afa727..580a0a3f 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/LayerPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/LayerPropertyData.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; using Newtonsoft.Json; using UnityEngine; @@ -11,6 +12,6 @@ public class LayerPropertyData /// Property data has a unique identifier for tracking which data belongs to this /// property; such as assets. /// - [SerializeField, JsonProperty] public Guid UUID = Guid.NewGuid(); + [DataMember] public Guid UUID = Guid.NewGuid(); } } diff --git a/Assets/Scripts/Layers/Properties/PropertyData/LayerURLPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/LayerURLPropertyData.cs index 9cde5941..8b4950dc 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/LayerURLPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/LayerURLPropertyData.cs @@ -1,14 +1,14 @@ using System; -using System.Collections; using System.Collections.Generic; -using UnityEngine; +using System.Runtime.Serialization; +using Newtonsoft.Json; namespace Netherlands3D.Twin.Layers.Properties { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Url")] public class LayerURLPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets { - public string url = ""; + [DataMember] public string url = ""; public IEnumerable GetAssets() { diff --git a/Assets/Scripts/Layers/Properties/PropertyData/OBJPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/OBJPropertyData.cs index dd112625..35afff59 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/OBJPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/OBJPropertyData.cs @@ -1,15 +1,17 @@ using System.Collections; using System; using System.Collections.Generic; +using System.Runtime.Serialization; using UnityEngine; using Newtonsoft.Json; using UnityEngine.Events; namespace Netherlands3D.Twin.Layers.Properties { + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Obj")] public class ObjPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets { - [SerializeField, JsonProperty] private Uri objFile; + [DataMember] private Uri objFile; [JsonIgnore] public readonly UnityEvent OnDataChanged = new(); diff --git a/Assets/Scripts/Layers/Properties/PropertyData/PolygonSelectionLayerPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/PolygonSelectionLayerPropertyData.cs index 29e8c011..b26d035a 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/PolygonSelectionLayerPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/PolygonSelectionLayerPropertyData.cs @@ -1,15 +1,17 @@ using System.Collections; using System.Collections.Generic; +using System.Runtime.Serialization; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Events; namespace Netherlands3D.Twin.Layers.Properties { + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "PolygonSelection")] public class PolygonSelectionLayerPropertyData : LayerPropertyData { - [SerializeField, JsonProperty] private float lineWidth = 10f; - [SerializeField, JsonProperty] private float extrusionHeight = 10f; + [DataMember] private float lineWidth = 10f; + [DataMember] private float extrusionHeight = 10f; [JsonIgnore] public readonly UnityEvent OnLineWidthChanged = new(); [JsonIgnore] public readonly UnityEvent OnExtrusionHeightChanged = new(); diff --git a/Assets/Scripts/Layers/Properties/PropertyData/Tile3DLayerPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/Tile3DLayerPropertyData.cs index a8653a02..57293502 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/Tile3DLayerPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/Tile3DLayerPropertyData.cs @@ -1,14 +1,15 @@ using System; +using System.Runtime.Serialization; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Events; namespace Netherlands3D.Twin.Layers.Properties { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "3DTiles")] public class Tile3DLayerPropertyData : LayerPropertyData { - [SerializeField, JsonProperty] private string url; + [DataMember] private string url; [JsonIgnore] public string Url diff --git a/Assets/Scripts/Layers/Properties/PropertyData/TransformLayerPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/TransformLayerPropertyData.cs index 97b75f46..9dbc33d3 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/TransformLayerPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/TransformLayerPropertyData.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.Serialization; using Netherlands3D.Coordinates; using Newtonsoft.Json; using UnityEngine; @@ -8,12 +9,12 @@ namespace Netherlands3D.Twin.Layers.Properties { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Transform")] public class TransformLayerPropertyData : LayerPropertyData { - [SerializeField, JsonProperty] private Coordinate position; - [SerializeField, JsonProperty] private Vector3 eulerRotation; - [SerializeField, JsonProperty] private Vector3 localScale; + [DataMember] private Coordinate position; + [DataMember] private Vector3 eulerRotation; + [DataMember] private Vector3 localScale; [JsonIgnore] public Coordinate Position diff --git a/Assets/Scripts/Layers/Properties/PropertyData/WindmillPropertyData.cs b/Assets/Scripts/Layers/Properties/PropertyData/WindmillPropertyData.cs index af4a9292..45527a48 100644 --- a/Assets/Scripts/Layers/Properties/PropertyData/WindmillPropertyData.cs +++ b/Assets/Scripts/Layers/Properties/PropertyData/WindmillPropertyData.cs @@ -1,15 +1,14 @@ -using System; +using System.Runtime.Serialization; using Newtonsoft.Json; -using UnityEngine; using UnityEngine.Events; namespace Netherlands3D.Twin.Layers.Properties { - [Serializable] + [DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Windmill")] public class WindmillPropertyData : LayerPropertyData { - [SerializeField, JsonProperty] private float axisHeight = 120f; - [SerializeField, JsonProperty] private float rotorDiameter = 120f; + [DataMember] private float axisHeight = 120f; + [DataMember] private float rotorDiameter = 120f; [JsonIgnore] public readonly UnityEvent OnRotorDiameterChanged = new(); [JsonIgnore] public readonly UnityEvent OnAxisHeightChanged = new(); diff --git a/Assets/Scripts/Projects/DataContractSerializationBinder.cs b/Assets/Scripts/Projects/DataContractSerializationBinder.cs new file mode 100644 index 00000000..1909270a --- /dev/null +++ b/Assets/Scripts/Projects/DataContractSerializationBinder.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json.Serialization; + +namespace Netherlands3D.Twin.Projects +{ + /// + /// For Netherlands3D, we want to make sure that changes in the structure of the code won't affect how types + /// are linked in the serialized file. By default, JSON.net will include a type reference using the Assembly name + /// and Class name (including namespace) in the serialized JSON; but this will break if we were to move classes to + /// another assembly (such as packaging a building block or functionality) or when we need to change the namespace + /// of a class during refactoring + /// + /// This Serialization Binder will ensure that if a DataContract attribute is present with a Data object -which is + /// recommended for Netherlands3D objects- that the name and namespace with that attribute is used to populate the + /// `$type` field in the JSON output. This will detach the name of the data object with the name of the class and + /// give more freedom to change the internals of a building block or functionanality. + /// + /// This serialization binder will act as a decorator (https://refactoring.guru/design-patterns/decorator) around + /// another SerializationBinder. When there is no DataContract attribute defined, the decorated Serialization Binder + /// is invoked. Generally you provide the DefaultSerializationBinder by Newtonsoft, so that regular classes (such + /// as the Unity color, Vector3 or others) still correctly serialize. + /// + public class DataContractSerializationBinder: ISerializationBinder + { + private readonly ISerializationBinder decoratedSerializationBinder; + private IDictionary KnownTypes { get; set; } = new Dictionary(); + + public DataContractSerializationBinder(ISerializationBinder decoratedSerializationBinder) + { + this.decoratedSerializationBinder = decoratedSerializationBinder; + IndexAliasesForWellDefinedDataObjects(); + } + + public Type BindToType(string assemblyName, string typeName) + { + var typeCodeAndType = KnownTypes.FirstOrDefault(t => t.Key == typeName); + if (typeCodeAndType.Equals(default(KeyValuePair))) + { + return decoratedSerializationBinder.BindToType(assemblyName, typeName); + } + + return typeCodeAndType.Value; + } + + public void BindToName(Type serializedType, out string assemblyName, out string typeName) + { + var typeCodeAndType = KnownTypes.FirstOrDefault(kv => kv.Value == serializedType); + if (typeCodeAndType.Equals(default(KeyValuePair))) + { + decoratedSerializationBinder.BindToName(serializedType, out assemblyName, out typeName); + return; + } + + var typeCode = typeCodeAndType.Key; + + assemblyName = null; + typeName = typeCode; + } + + private void IndexAliasesForWellDefinedDataObjects() + { + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + IndexAllTypesWithADataContract(assembly); + } + } + + private void IndexAllTypesWithADataContract(Assembly assembly) + { + var types = assembly.GetTypes().Where(t => t.IsDefined(typeof(DataContractAttribute))); + foreach (var type in types) + { + IndexTypeWithDataContract(type); + } + } + + private void IndexTypeWithDataContract(Type type) + { + var attribute = Attribute.GetCustomAttribute(type, typeof(DataContractAttribute)) as DataContractAttribute; + if (attribute == null) return; + + KnownTypes.TryAdd(ExtractTypeAlias(type, attribute), type); + } + + private static string ExtractTypeAlias(Type type, DataContractAttribute attribute) + { + // By default, we assume DataContract doesn't have additional info defined and we use the Type's + // full name as a type code + string alias = type.FullName ?? type.Name; + + // If there is no name associated with the DataContract, that's OK and we return the type's name. + if (string.IsNullOrEmpty(attribute.Name)) return alias; + + // if DataContract does have a name defined; we use that so that we type-map the data and prevent future + // issues when changing the location or namespace of a serialized class. + alias = attribute.Name; + + if (string.IsNullOrEmpty(attribute.Namespace)) return alias; + + // It would be even better if the DataContract has a vendor specific namespace, to prevent naming clashes + return $"{attribute.Namespace}/{alias}"; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Projects/DataContractSerializationBinder.cs.meta b/Assets/Scripts/Projects/DataContractSerializationBinder.cs.meta new file mode 100644 index 00000000..73273fa1 --- /dev/null +++ b/Assets/Scripts/Projects/DataContractSerializationBinder.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c381a7560e6043258456e9e7773a7de7 +timeCreated: 1727099441 \ No newline at end of file diff --git a/Assets/Scripts/Projects/ProjectDataStore.cs b/Assets/Scripts/Projects/ProjectDataStore.cs index ae2bcb82..8dd70702 100644 --- a/Assets/Scripts/Projects/ProjectDataStore.cs +++ b/Assets/Scripts/Projects/ProjectDataStore.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using Netherlands3D.Twin.Layers; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using UnityEditor; using UnityEngine; @@ -25,7 +26,8 @@ public class ProjectDataStore : ScriptableObject private readonly JsonSerializerSettings serializerSettings = new() { TypeNameHandling = TypeNameHandling.Auto, - Formatting = Formatting.Indented + Formatting = Formatting.Indented, + SerializationBinder = new DataContractSerializationBinder(new DefaultSerializationBinder()) }; [SerializeField] private string DefaultFileName = "NL3D_Project_";