diff --git a/VisualPinball.Engine/Math/DragPointData.cs b/VisualPinball.Engine/Math/DragPointData.cs
index 3259ac84a..93f68a019 100644
--- a/VisualPinball.Engine/Math/DragPointData.cs
+++ b/VisualPinball.Engine/Math/DragPointData.cs
@@ -87,6 +87,22 @@ public DragPointData Lerp(DragPointData dp, float pos)
};
}
+ public DragPointData Clone()
+ {
+ return new DragPointData(Center) {
+ PosZ = PosZ,
+ IsSmooth = IsSmooth,
+ IsSlingshot = IsSlingshot,
+ HasAutoTexture = HasAutoTexture,
+ TextureCoord = TextureCoord,
+ IsLocked = IsLocked,
+ EditorLayer = EditorLayer,
+ EditorLayerName = EditorLayerName,
+ EditorLayerVisibility = EditorLayerVisibility,
+ CalcHeight = CalcHeight,
+ };
+ }
+
#region BIFF
static DragPointData()
diff --git a/VisualPinball.Unity/Assets/Presets.meta b/VisualPinball.Unity/Assets/Presets.meta
new file mode 100644
index 000000000..0d4105838
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 74b6f483aa6bd6c49bc05dca5f2c6750
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam.meta
new file mode 100644
index 000000000..0c740dbdc
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 81635ffcc11fea249b3073b401a78d1f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset
new file mode 100644
index 000000000..59c6e7a72
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Close-Up Less
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.0339
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.0386
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.0339
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset.meta
new file mode 100644
index 000000000..b33c080cb
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up Less.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: dcf08986d96fede4795f309017d50707
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset
new file mode 100644
index 000000000..54528ab60
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Close-Up
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.0212
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.0235
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.0212
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset.meta
new file mode 100644
index 000000000..04be5c50f
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Close-Up.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 227bb9df5028f714d9dd6dd47a69f1af
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset
new file mode 100644
index 000000000..992f677e2
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Default
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.06
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.0762
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.06
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset.meta
new file mode 100644
index 000000000..49ba504ee
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Default.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 42ff1390796e56943a02edf674c5cc7e
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset
new file mode 100644
index 000000000..52be2341f
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Flipper
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.09444
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.12913
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.13054
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset.meta
new file mode 100644
index 000000000..dae18e241
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Flipper.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 414d13c4ce29de24ba6b563c75651aaa
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset
new file mode 100644
index 000000000..031399549
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Long Distance
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.1946
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.2167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.1946
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset.meta
new file mode 100644
index 000000000..9368b08c1
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Long Distance.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 134d53ae37a2565439b8e853b641959f
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset
new file mode 100644
index 000000000..820eafe67
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset
@@ -0,0 +1,75 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!181963792 &2655988077585873504
+Preset:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Longest Distance
+ m_TargetType:
+ m_NativeTypeID: 4
+ m_ManagedTypePPtr: {fileID: 0}
+ m_ManagedTypeFallback:
+ m_Properties:
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.x
+ value: 0.27781588
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.y
+ value: 0.36497167
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.z
+ value: -0.1150751
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalRotation.w
+ value: 0.8811196
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.x
+ value: -0.5269
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.y
+ value: 0.5089
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalPosition.z
+ value: -0.4841
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.x
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.y
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalScale.z
+ value: 1
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_ConstrainProportionsScale
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_RootOrder
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 35
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 45
+ objectReference: {fileID: 0}
+ - target: {fileID: 0}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ m_ExcludedProperties: []
diff --git a/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset.meta b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset.meta
new file mode 100644
index 000000000..c45a7b76b
--- /dev/null
+++ b/VisualPinball.Unity/Assets/Presets/Asset Thumbcam/Longest Distance.preset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0d2c84dbec7fd3e4b86d0f8cd6adec09
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2655988077585873504
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
index 7f446caf8..9fa539b1e 100644
--- a/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
+++ b/VisualPinball.Unity/Documentation~/creators-guide/toc.yml
@@ -72,6 +72,15 @@
- name: Import into Unity
href: xref:tutorial_backglass_3
+ - name: Make a 3D Scan Game-Ready
+ href: xref:tutorial_3d_scan
+ items:
+ - name: Clean Up the Mesh
+ href: xref:tutorial_3d_scan_1
+ - name: Retopologize
+ href: xref:tutorial_3d_scan_2
+ - name: Bake Texture Maps
+ href: xref:tutorial_3d_scan_3
- name: Manual
href: manual/manual.md
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/1-clean-up.md b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/1-clean-up.md
new file mode 100644
index 000000000..8c23ea34b
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/1-clean-up.md
@@ -0,0 +1,56 @@
+---
+uid: tutorial_3d_scan_1
+title: Clean Up Your 3D Scan
+description: Get rid of unwanted geometry, and use a reasonable poly count.
+---
+
+# Clean Up
+
+Open Blender and import your model.
+
+## Reposition
+
+The first thing you'll see is that it's probably not correctly sized and positioned. Using the `R` (rotate) and `G` (grab) keys, place your object at the origin.
+
+
+
+Be sure to apply everything by hitting `Ctrl+A` in the scene view and choose *All Transforms*.
+
+## Remove Unwanted Geometry
+
+
+
+As you can see, our scan has a bunch of artifacts that aren't actually part of the owl. We're going to focus on the bottom part.
+
+1. Zoom in on the part you want to remove.
+2. Toggle *X-Ray*, so you can select occluded geometry, and hit `TAB` to enter Edit Mode.
+3. In Edit Mode, hit `1` and select the vertices you want to remove.
+4. Hit `X` and select *Delete Vertices*
+5. Press `2` for edge selection, and select the boundary of the hole you've just created. Hit `F` to create a face and `Ctrl+T` to triangulate it.
+6. From Edit Mode, change into *Sculpt Mode*, select the *Smooth* tool, adjust the radius, and smoothen up the hole you've filled.
+
+
+Steps 1 to 6.
+
+Repeat this for all the other parts that are unwanted in your scan.
+
+> [!note]
+> If you have free floating geometry that you want to quickly erase, do this:
+> 1. In Edit Mode, select a vertex of the part you want to keep.
+> 2. Hit `Ctrl+L` to select all linked vertices
+> 3. Hit `Ctrl+I` to invert the selection.
+> 4. Hit `X` and select *Delete Vertices* to delete all loose parts.
+
+## Decimate the Mesh
+
+This step is optional, but 1.8mio triangles is hard to handle, so we'll decimate the mesh to 10% so we have something more light to work with. 180k triangles is still plenty enough, even later when we use it to bake in the details.
+
+1. Go back to object mode by hitting `TAB`.
+2. Go to the *Modifier* panel on the right hand side.
+3. Add a *Decimate* modifier.
+4. Set the *Ratio* to 0.1 (Blender will probably freeze for a few seconds).
+5. From the drop down, select *Apply*.
+
+Finally, export the mesh as *Wavefront (.obj)* and name it `hi-poly.obj`.
+
+Now that we have a good hi-poly mesh, let's continue with the next step, [retopology](xref:tutorial_3d_scan_2).
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/2-mesh-retopology.md b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/2-mesh-retopology.md
new file mode 100644
index 000000000..7607fc94c
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/2-mesh-retopology.md
@@ -0,0 +1,82 @@
+---
+uid: tutorial_3d_scan_2
+title: Mesh Retopology
+description: Create a new topology using Instant Meshes and RetopoFlow3
+---
+
+# Mesh Retopology
+
+For this step we'll need two additional tools:
+
+- [Instant Meshes](https://github.com/wjakob/instant-meshes) (a standalone tool)
+- [RetopoFlow 3](https://github.com/CGCookie/retopoflow) (a Blender plug-in)
+
+Both programs are free and open source.
+
+## Retopowhat?
+
+From [blender.org](https://docs.blender.org/manual/en/latest/modeling/meshes/retopology.html):
+
+> Retopology is the process of simplifying the topology of a mesh to make it cleaner and easier to work with. Retopology is need for mangled topology resulting from sculpting or generated topology, for example from a 3D scan.
+
+You're probably not going to rig and animate your playfield toy. However, retopology gives you the possibility to put in more geometry where it's most visible, adding more details to your mesh for the same price (=poly count).
+
+Our approach here is to let *Instant Meshes* handle most of the heavy work, and manually add more details where appropriate.
+
+If you want to dig deeper into topology, here is an extensive video on the topic:
+
+> [!Video https://www.youtube.com/embed/6Kt0gW3_kio]
+
+## Base Topology
+
+Open up *Instant Meshes* and load `hi-poly.obj`. Now you need to decide the number of vertices you're aiming for. Since we're going to add more geometry later, we can aim pretty low. In our case, we're going for a thousand vertices.
+
+Set *Target vertex count* to 1K, then click on *Solve* under *Orientation field*. This gives you hints how the geometry will be oriented.
+
+
+
+Now, if you're doing this the first time, you probably have no idea what to look for. In general, what you're aiming for is an orientation field that is well aligned with the geometry. For example, we would like to have an orientation along each of the crawls, so we can more easily add details without having to move too much vertices around.
+
+Use the *comb* tool to draw new lines. Each time you add a new line, the program updates the orientation field to accommodate for the new orientation you're setting.
+
+
+
+In our example, we've added new orientations to the crawls, the wings and the face. We didn't go into too much detail for the face, since we're re-doing that one manually later anyway.
+
+Now, click on *Solve* under *Position field*. This gives you a rough idea how the polygons will be aligned. If necessary, re-comb and solve again.
+
+
+
+Click on *Export Mesh* and *Extract Mesh*. This shows you the mesh we're going fine-tune in Blender.
+
+
+
+Click on *Save* and export it as `low-poly-base.obj`.
+
+## Fine-Tune
+
+Go back to Blender, and import `low-poly-base.obj`. Select it, and choose *Object -> Shade Smooth*.
+
+
+
+If you haven't yet, install the [RetopoFlow 3](https://github.com/CGCookie/retopoflow) plugin. We won't go into too much detail on how to use RetopoFlow, but this tutorial should teach you the basics:
+
+> [!Video https://www.youtube.com/embed/X8kQiccJetw]
+
+In order to get your imported model into RetopoFlow, select the mesh, enter edit mode, click on the *RetopoFlow* button, and select *Start RetopoFlow*.
+
+
+
+Now, re-topo the parts that need more details. In our example, we've completely removed and remodeled the head, added more geometry to the feet and some to the wings. We've also aligned some vertices to better match the model.
+
+
+
+> [!note]
+> #### Quads vs Tris
+> You're probably heard about the debate whether to use purely quads or triangles in your geometry. [Here](http://wedesignvirtual.com/quads-vs-tris-in-3d-modeling/) is a pretty good overview about the pros and cons of each.
+>
+> In short, since we're not purists and are working on game assets, some triangles are fine.
+
+So you got your mesh. Spend some time fine-tuning it. Rotate, and check if there are any important angles where the silhouette looks jagged. Add more geometry if needed, but also keep in mind the distance from which your toy will be typically rendered (probably no one will zoom-in fully on your model during game play).
+
+Now let's [bring back the details of the original geometry](xref:tutorial_3d_scan_3).
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/3-bake-texture-maps.md b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/3-bake-texture-maps.md
new file mode 100644
index 000000000..c1ff43018
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/3-bake-texture-maps.md
@@ -0,0 +1,60 @@
+---
+uid: tutorial_3d_scan_3
+title: Bake Texture Maps
+description: Create a normal map and a diffusion map.
+---
+
+# Texture Baking
+
+In this last part, we'll use the original mesh to bake the details of the geometry into a normal map. We'll also project the original texture (diffuse map) onto our new model.
+
+## UV-Unwrap
+
+But first, we need to UV-unwrap our low-poly mesh. Change to the *UV Editing* workspace in Blender, select the low-poly model, `TAB` into Edit Mode, hit `A` to select all, and choose *UV -> Smart UV Project*.
+
+
+
+## Set Up Material
+
+Before we bake anything, we'll set up the material. Go to the *Shading* workspace and assign a new material to the low-poly mesh. Then, set up the two maps we're going to bake. In the node editor:
+
+1. Hit `Shift+A`, choose *Texture -> Image Texture*.
+2. With the image texture selected, hit *Shift+D* to duplicate
+3. On the first texture, click *New*, name it "Diffuse", and set the size to the same size as the texture of your original 3D scan.
+4. On the second texture, also click *New*, name it "Normals", same size, but enable *32-bit Float*.
+5. Hit `Shift+A` again, and add a *Normal Map* node.
+6. Connect the "Diffuse" texture's *Color* to the principled BSDF node's *Base Color* input.
+7. Connect the "Normal" texture's *Color* to the normal map's *Color* input.
+6. Connect the normal map's *Normal* output to the BSDF node's *Normal* input.
+
+Your graph should now look like this:
+
+
+
+## Bake Normal Map
+
+In the Outliner, make sure both the high-poly and the low-poly mesh are visible. Also, make sure that the low-poly object doesn't have any modifiers added (RetopoFlow might have added a *Mirror* and *Displace* modifier). Select in this order, while holding `Ctrl`:
+
+1. The high-poly mesh
+2. The low-poly mesh
+
+Go to *Render Properties* and make sure *Cycles* is selected as the render engine. Then, scroll down to the *Bake* section.
+
+- Set *Bake Type* to *Normal*
+- Check *Selected to Active*
+- In the node editor, select the "Normals" node.
+- Hit *Bake*.
+
+Depending on the size of your mesh, you'll also need to set *Extrusion* and *Max Ray Distance*. Values on 3mm and 5mm respectively worked for this example.
+
+
+
+## Bake Diffuse Map
+
+Now, select the "Diffuse" node in the node editor. Change *Bake Type* to *Diffuse*, uncheck *Direct* and *Indirect*, and hit *Bake* again.
+
+
+
+That's it! Hide the high-poly mesh, and you've got your game-ready model!
+
+
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/before-after.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/before-after.jpg
new file mode 100644
index 000000000..dc0d60b3e
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/before-after.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/cleanup-steps.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/cleanup-steps.jpg
new file mode 100644
index 000000000..3efb05c59
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/cleanup-steps.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/diffuse-map.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/diffuse-map.jpg
new file mode 100644
index 000000000..f89765168
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/diffuse-map.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/final-result.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/final-result.jpg
new file mode 100644
index 000000000..59ee98cf1
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/final-result.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/geometry-to-clean.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/geometry-to-clean.jpg
new file mode 100644
index 000000000..c7732ff56
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/geometry-to-clean.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-combed.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-combed.jpg
new file mode 100644
index 000000000..fd115fff1
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-combed.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-mesh.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-mesh.jpg
new file mode 100644
index 000000000..b9a3943b4
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-mesh.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-no-adjustments.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-no-adjustments.jpg
new file mode 100644
index 000000000..fed2be39a
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-no-adjustments.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-positions.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-positions.jpg
new file mode 100644
index 000000000..677f205bd
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/im-positions.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/index.md b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/index.md
new file mode 100644
index 000000000..9c85de4e6
--- /dev/null
+++ b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/index.md
@@ -0,0 +1,42 @@
+---
+uid: tutorial_3d_scan
+title: Make a 3D Scan Game-Ready
+description: How to clean-up, retopologize and bake in the details, using Blender.
+---
+
+# Make a 3D Scan Game-Ready
+
+In this tutorial we describe how to convert a 3D scan of toy into something that can be used as a game asset. It's not necessarily linked to VPE or even Unity, but there are many ways of doing it, and this flow has been working great so far.
+
+## Overview
+
+In general, there are multiple characteristics of a 3D scan that make it unsuitable for importing directly into a game engine:
+
+- There is often noise, i.e. elements that aren't part of the actual object.
+- Depending on the method of the 3D scan, the poly count of the model can be extremely high.
+- The topology of the scanned mesh often isn't ideal.
+
+This tutorial addresses all of those issues, by explaining how to:
+
+- Clean up the mesh
+- Reduce the poly count
+- Retopologize the geometry
+- Bake the lost geometry details into a normal map
+
+We'll be using an owl toy that you can download [here](https://vpuniverse.com/files/file/11638-magic-girl-owl/) (props to Dazz for ordering and scanning the model).
+
+
+Left: Original scan, 1.86 mio triangles, right: Game-ready conversion, 1,400 triangles.
+
+
+## Prerequisites
+
+- The 3D model you're converting.
+- You should have a intermediate level in Blender.
+
+## Workflow
+
+1. [Clean up the mesh](xref:tutorial_3d_scan_1)
+2. [Retopologize the mesh](xref:tutorial_3d_scan_2)
+3. [Bake a normal map and a diffusion map](xref:tutorial_3d_scan_3)
+
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/low-poly-imported.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/low-poly-imported.jpg
new file mode 100644
index 000000000..44fbbf16c
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/low-poly-imported.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/node-view-material.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/node-view-material.jpg
new file mode 100644
index 000000000..169016eaf
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/node-view-material.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/normal-map.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/normal-map.jpg
new file mode 100644
index 000000000..b8d2f8b4e
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/normal-map.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/positioned.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/positioned.jpg
new file mode 100644
index 000000000..8520081d4
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/positioned.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-done.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-done.jpg
new file mode 100644
index 000000000..82c25fc73
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-done.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-start.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-start.jpg
new file mode 100644
index 000000000..0d620d12d
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/retopoflow-start.jpg differ
diff --git a/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/uv-unwrap.jpg b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/uv-unwrap.jpg
new file mode 100644
index 000000000..dd6bf0643
Binary files /dev/null and b/VisualPinball.Unity/Documentation~/creators-guide/tutorials/make-a-3d-scan-game-ready/uv-unwrap.jpg differ
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs
index 52aac9cac..1040ce0a7 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.cs
@@ -39,27 +39,29 @@ public partial class AssetBrowser : EditorWindow, IDragHandler
[NonSerialized]
public List Libraries;
+ public IEnumerable SelectedAssets => _selectedResults.Select(r => r.Asset);
+
[SerializeField]
private List _selectedLibraries;
[NonSerialized]
- private List _assets;
-
- [NonSerialized]
- private readonly HashSet _previewLoadingAssets = new();
+ private List _assetResults;
[NonSerialized]
public AssetQuery Query;
- private AssetResult LastSelectedAsset {
- set => _detailsElement.Asset = value;
+ public const string ThumbPath = "Packages/org.visualpinball.unity.assetlibrary/Editor/Thumbnails~";
+ public const int ThumbSize = 256;
+
+ private AssetResult LastSelectedResult {
+ set => _detailsElement.Asset = value?.Asset;
}
- private AssetResult _firstSelectedAsset;
- private readonly HashSet _selectedAssets = new();
+ private AssetResult _firstSelectedResult;
+ private readonly HashSet _selectedResults = new();
- private readonly Dictionary _elementByAsset = new();
- private readonly Dictionary _assetsByElement = new();
+ private readonly Dictionary _elementByAsset = new();
+ private readonly Dictionary _resultByElement = new();
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@@ -77,6 +79,7 @@ public static void ShowWindow()
private void Refresh()
{
+ _statusLabel.text = "Loading Assets...";
RefreshLibraries();
RefreshCategories();
RefreshAssets();
@@ -122,13 +125,13 @@ private void OnLibraryChanged(object sender, EventArgs e)
{
RefreshLibraries();
RefreshCategories();
- _detailsElement.UpdateDetails();
}
public void FilterByAttribute(string attributeKey, string value, bool remove = false)
{
var queryString = attributeKey.Contains(" ") ? $"\"{attributeKey}\":" : $"{attributeKey}:";
- queryString += value.Contains(" ") ? $"\"{value}\"" : value;
+ var queryValue = AssetQuery.ValueToQuery(value);
+ queryString += value.Contains(" ") ? $"\"{queryValue}\"" : queryValue;
if (remove) {
_queryInput.value = _queryInput.value.Replace(queryString, "").Trim();
@@ -140,12 +143,16 @@ public void FilterByAttribute(string attributeKey, string value, bool remove = f
public void FilterByTag(string tag, bool remove = false)
{
- if (remove) {
- _queryInput.value = _queryInput.value.Replace($"[{tag}]", "").Trim();
+ _queryInput.value = remove
+ ? _queryInput.value.Replace($"[{tag}]", "").Trim()
+ : $"{_queryInput.value} [{tag}]".Trim();
+ }
- } else {
- _queryInput.value = $"{_queryInput.value} [{tag}]".Trim();
- }
+ public void FilterByQuality(AssetQuality quality, bool remove = false)
+ {
+ _queryInput.value = remove
+ ? _queryInput.value.Replace($"({quality.ToString()})", "").Trim()
+ : $"{_queryInput.value} ({quality.ToString()})".Trim();
}
@@ -161,63 +168,153 @@ private void RefreshAssets()
private void OnQueryUpdated(object sender, AssetQueryResult e)
{
- UpdateQueryResults(e.Rows);
+ UpdateQueryResults(e.Rows, e.DurationMs);
+ _categoryView.UpdateCategoryTags();
}
- private void UpdateQueryResults(List assets)
+ private void UpdateQueryResults(List results, long duration)
{
- _statusLabel.text = $"Found {assets.Count} asset" + (assets.Count == 1 ? "" : "s") + ".";
- _assets = assets;
+ _assetResults = results;
_gridContent.Clear();
_elementByAsset.Clear();
- _assetsByElement.Clear();
- _selectedAssets.Clear();
- _previewLoadingAssets.Clear();
+ _resultByElement.Clear();
+ _selectedResults.Clear();
- LastSelectedAsset = null;
- foreach (var row in assets) {
+ LastSelectedResult = null;
+ foreach (var row in results) {
var element = NewItem(row);
_elementByAsset[row.Asset] = element;
- _assetsByElement[element] = row;
+ _resultByElement[element] = row;
_gridContent.Add(_elementByAsset[row.Asset]);
- if (row.IsLoadingAssetPreview) {
- _previewLoadingAssets.Add(row);
- }
}
- if (!assets.Contains(_firstSelectedAsset)) {
- _firstSelectedAsset = null;
+ _gridContent.MarkDirtyRepaint(); // todo doesn't work, scrolling is still screwed.
+
+ if (!results.Contains(_firstSelectedResult)) {
+ _firstSelectedResult = null;
} else {
- SelectOnly(_firstSelectedAsset);
+ SelectOnly(_firstSelectedResult);
}
+
+ _statusLabel.text = $"Found {results.Count} asset" + (results.Count == 1 ? "" : "s") + $" in {duration}ms.";
}
private void AddAssetContextMenu(ContextualMenuPopulateEvent evt)
{
- if (evt.target is VisualElement ve && _assetsByElement.ContainsKey(ve)) {
- var clickedAsset = _assetsByElement[ve];
- var lib = _assetsByElement[ve].Library;
- if (!lib.IsLocked) {
- evt.menu.AppendAction("Remove from Library", _ => {
- if (!_selectedAssets.Contains(clickedAsset)) {
- _selectedAssets.Add(clickedAsset);
- ToggleSelectionClass(_elementByAsset[clickedAsset.Asset]);
+ if (evt.target is not VisualElement ve || !_resultByElement.ContainsKey(ve)) {
+ return;
+ }
+
+ var clickedAsset = _resultByElement[ve];
+ var lib = _resultByElement[ve].Asset.Library;
+ if (lib.IsLocked) {
+ return;
+ }
+
+ // lib is not locked, and asset is known.
+ evt.menu.AppendAction("Remove from Library", _ => {
+ if (!_selectedResults.Contains(clickedAsset)) {
+ _selectedResults.Add(clickedAsset);
+ ToggleSelectionClass(_elementByAsset[clickedAsset.Asset]);
+ }
+ var numRemovedAssets = 0;
+ foreach (var assetResult in _selectedResults.Where(a => !a.Asset.Library.IsLocked).ToList()) {
+ _selectedResults.Remove(assetResult);
+ assetResult.Asset.Library.RemoveAsset(assetResult.Asset);
+ if (_thumbCache.ContainsKey(assetResult.Asset.GUID)) {
+ DestroyImmediate(_thumbCache[assetResult.Asset.GUID]);
+ _thumbCache.Remove(assetResult.Asset.GUID);
+ }
+ numRemovedAssets++;
+ }
+
+ RefreshCategories();
+ RefreshAssets();
+ _statusLabel.text = $"Removed {numRemovedAssets} assets from library.";
+ });
+
+ if (_selectedResults.Count > 1) {
+ var srcAsset = clickedAsset.Asset;
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction("Add Attributes to Selected", _ => {
+ var destAssets = OtherSelected(clickedAsset).ToList();
+ foreach (var destAsset in destAssets) {
+ foreach (var attr in srcAsset.Attributes) {
+ destAsset.AddAttribute(attr.Key, attr.Value);
+ }
+ destAsset.Save();
+ }
+ EditorUtility.DisplayDialog("Add Attributes to Selected", $"Added {srcAsset.Attributes.Count} attributes to {destAssets.Count} other assets.", "OK");
+
+ });
+ evt.menu.AppendAction("Replace Attributes in Selected", _ => {
+ var destAssets = OtherSelected(clickedAsset).ToList();
+ foreach (var destAsset in destAssets) {
+ foreach (var attr in srcAsset.Attributes) {
+ destAsset.ReplaceAttribute(attr.Key, attr.Value);
+ }
+ destAsset.Save();
+ }
+ EditorUtility.DisplayDialog("Replace Attributes to Selected", $"Replaced {srcAsset.Attributes.Count} attributes in {destAssets.Count} other assets.", "OK");
+ });
+
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction("Add Tags to Selected", _ => {
+ var destAssets = OtherSelected(clickedAsset).ToList();
+ foreach (var destAsset in destAssets) {
+ foreach (var tag in srcAsset.Tags) {
+ destAsset.AddTag(tag.TagName);
+ }
+ destAsset.Save();
+ }
+ EditorUtility.DisplayDialog("Add Tags to Selected", $"Added {srcAsset.Tags.Count} tags to {destAssets.Count} other assets.", "OK");
+ });
+
+ evt.menu.AppendAction("Replace Tags in Selected", _ => {
+ var destAssets = OtherSelected(clickedAsset).ToList();
+ foreach (var destAsset in destAssets) {
+ destAsset.Tags.Clear();
+ foreach (var tag in srcAsset.Tags) {
+ destAsset.AddTag(tag.TagName);
+ }
+ destAsset.Save();
+ }
+ EditorUtility.DisplayDialog("Replace Tags in Selected", $"Replaced {srcAsset.Tags.Count} tags in {destAssets.Count} other assets.", "OK");
+ });
+
+ evt.menu.AppendSeparator();
+ evt.menu.AppendAction("Copy All to Selected", _ => {
+ var destAssets = OtherSelected(clickedAsset).ToList();
+ foreach (var destAsset in destAssets) {
+ if (string.IsNullOrEmpty(destAsset.Description)) {
+ destAsset.Description = srcAsset.Description;
}
- var numRemovedAssets = 0;
- foreach (var asset in _selectedAssets.Where(a => !a.Library.IsLocked).ToList()) {
- _selectedAssets.Remove(asset);
- asset.Library.RemoveAsset(asset.Asset);
- numRemovedAssets++;
+ foreach (var tag in srcAsset.Tags) {
+ destAsset.AddTag(tag.TagName);
+ }
+ foreach (var attr in srcAsset.Attributes) {
+ destAsset.AddAttribute(attr.Key, attr.Value);
+ }
+ foreach (var link in srcAsset.Links.Where(link => destAsset.Links.FirstOrDefault(l => l.Name == link.Name) != null)) {
+ destAsset.Links.Add(new AssetLink(link.Name, link.Url));
}
- RefreshCategories();
- RefreshAssets();
- _statusLabel.text = $"Removed {numRemovedAssets} assets from library.";
- });
- }
+ destAsset.Scale = srcAsset.Scale;
+ destAsset.Quality = srcAsset.Quality;
+ destAsset.ThumbCameraPreset = srcAsset.ThumbCameraPreset;
+
+ destAsset.Save();
+ }
+ EditorUtility.DisplayDialog("Copy All to Selected", $"Copied data of to {destAssets.Count} other assets.", "OK");
+ });
}
}
+ private IEnumerable OtherSelected(AssetResult src)
+ {
+ return _selectedResults.Where(a => !a.Asset.Library.IsLocked && a.Asset.GUID != src.Asset.GUID).Select(ar => ar.Asset);
+ }
+
private void OnEmptyClicked(PointerUpEvent evt)
{
SelectNone();
@@ -229,13 +326,13 @@ private void OnAssetClicked(IMouseEvent evt, VisualElement element)
if (evt.button != 0) {
return;
}
- var clickedAsset = _assetsByElement[element];
+ var clickedAsset = _resultByElement[element];
// no modifier pressed
if (!evt.shiftKey && !evt.ctrlKey) {
// already selected?
- if (_selectedAssets.Contains(clickedAsset)) {
- if (_selectedAssets.Count != 1) {
+ if (_selectedResults.Contains(clickedAsset)) {
+ if (_selectedResults.Count != 1) {
SelectOnly(clickedAsset);
} // if count is 1, and user clicks on it, do nothing.
} else {
@@ -246,7 +343,7 @@ private void OnAssetClicked(IMouseEvent evt, VisualElement element)
// only CTRL pressed
if (!evt.shiftKey && evt.ctrlKey) {
// already selected?
- if (_selectedAssets.Contains(clickedAsset)) {
+ if (_selectedResults.Contains(clickedAsset)) {
UnSelect(clickedAsset);
} else {
Select(clickedAsset);
@@ -255,9 +352,9 @@ private void OnAssetClicked(IMouseEvent evt, VisualElement element)
// only SHIFT pressed
if (evt.shiftKey && !evt.ctrlKey) {
- var startIndex = _firstSelectedAsset != null ? _assets.IndexOf(_firstSelectedAsset) : 0;
- var endIndex = _assets.IndexOf(clickedAsset);
- LastSelectedAsset = clickedAsset;
+ var startIndex = _firstSelectedResult != null ? _assetResults.IndexOf(_firstSelectedResult) : 0;
+ var endIndex = _assetResults.IndexOf(clickedAsset);
+ LastSelectedResult = clickedAsset;
SelectRange(startIndex, endIndex);
}
@@ -268,7 +365,7 @@ private void OnAssetClicked(IMouseEvent evt, VisualElement element)
}
}
- public void OnCategoriesUpdated(Dictionary> categories) => Query.Filter(categories);
+ public void OnCategoriesUpdated(Dictionary> categories) => Query.Filter(categories);
private void OnSearchQueryChanged(ChangeEvent evt) => Query.Search(evt.newValue);
private void OnLibraryToggled(AssetLibrary lib, bool enabled)
{
@@ -287,7 +384,7 @@ public AssetLibrary GetLibraryByPath(string pathToCheck)
});
}
- public void AddAssets(IEnumerable paths, Func getCategory)
+ public void AddAssets(IEnumerable paths, Func getCategory)
{
var numAdded = 0;
var numUpdated = 0;
@@ -332,15 +429,15 @@ private void SelectRange(int start, int end)
if (start > end) {
(start, end) = (end, start);
}
- for (var i = 0; i < _assets.Count; i++) {
- var asset = _assets[i];
+ for (var i = 0; i < _assetResults.Count; i++) {
+ var asset = _assetResults[i];
if (i >= start && i <= end) {
- if (!_selectedAssets.Contains(asset)) {
- _selectedAssets.Add(asset);
+ if (!_selectedResults.Contains(asset)) {
+ _selectedResults.Add(asset);
ToggleSelectionClass(_elementByAsset[asset.Asset]);
}
- } else if (_selectedAssets.Contains(asset)) {
- _selectedAssets.Remove(asset);
+ } else if (_selectedResults.Contains(asset)) {
+ _selectedResults.Remove(asset);
ToggleSelectionClass(_elementByAsset[asset.Asset]);
}
}
@@ -348,47 +445,47 @@ private void SelectRange(int start, int end)
private void SelectNone()
{
- foreach (var selectedAsset in _selectedAssets) {
+ foreach (var selectedAsset in _selectedResults) {
ToggleSelectionClass(_elementByAsset[selectedAsset.Asset]);
}
- _selectedAssets.Clear();
- _firstSelectedAsset = null;
- LastSelectedAsset = null;
+ _selectedResults.Clear();
+ _firstSelectedResult = null;
+ LastSelectedResult = null;
}
- private void SelectOnly(AssetResult asset)
+ private void SelectOnly(AssetResult result)
{
var wasAlreadySelected = false;
- foreach (var selectedAsset in _selectedAssets) {
- if (selectedAsset != asset) {
+ foreach (var selectedAsset in _selectedResults) {
+ if (selectedAsset != result) {
ToggleSelectionClass(_elementByAsset[selectedAsset.Asset]);
} else {
wasAlreadySelected = true;
}
}
- _selectedAssets.Clear();
- _selectedAssets.Add(asset);
+ _selectedResults.Clear();
+ _selectedResults.Add(result);
if (!wasAlreadySelected) {
- ToggleSelectionClass(_elementByAsset[asset.Asset]);
+ ToggleSelectionClass(_elementByAsset[result.Asset]);
}
- _firstSelectedAsset = asset;
- LastSelectedAsset = asset;
+ _firstSelectedResult = result;
+ LastSelectedResult = result;
}
- private void UnSelect(AssetResult asset)
+ private void UnSelect(AssetResult result)
{
- _selectedAssets.Remove(asset);
- ToggleSelectionClass(_elementByAsset[asset.Asset]);
- _firstSelectedAsset = _selectedAssets.Count > 0 ? _selectedAssets.FirstOrDefault() : null;
- LastSelectedAsset = _selectedAssets.Count > 0 ? _selectedAssets.LastOrDefault() : null;
+ _selectedResults.Remove(result);
+ ToggleSelectionClass(_elementByAsset[result.Asset]);
+ _firstSelectedResult = _selectedResults.Count > 0 ? _selectedResults.FirstOrDefault() : null;
+ LastSelectedResult = _selectedResults.Count > 0 ? _selectedResults.LastOrDefault() : null;
}
- private void Select(AssetResult asset)
+ private void Select(AssetResult result)
{
- _selectedAssets.Add(asset);
- ToggleSelectionClass(_elementByAsset[asset.Asset]);
- LastSelectedAsset = asset;
+ _selectedResults.Add(result);
+ ToggleSelectionClass(_elementByAsset[result.Asset]);
+ LastSelectedResult = result;
}
private static void ToggleSelectionClass(VisualElement element) => element.ToggleInClassList("selected");
@@ -403,6 +500,9 @@ private void Select(AssetResult asset)
public static bool IsDraggingExistingAssets => DragAndDrop.GetGenericData("assets") is HashSet;
public static bool IsDraggingNewAssets => DragAndDrop.paths is { Length: > 0 };
+ public IEnumerable NonActiveSelection => !_detailsElement.HasAsset
+ ? Array.Empty()
+ : _selectedResults.Where(sr => sr.Asset.GUID != _detailsElement.Asset.GUID);
private void OnDragEnterEvent(DragEnterEvent evt)
{
@@ -461,14 +561,15 @@ private void OnDragPerformEvent(DragPerformEvent evt)
#endregion
- private void Update()
+ public void RefreshThumb(Asset asset)
{
- foreach (var asset in _previewLoadingAssets) {
- if (_elementByAsset.ContainsKey(asset.Asset)) {
- asset.RefreshPreviewImage(_elementByAsset[asset.Asset]);
- }
+ if (_thumbCache.ContainsKey(asset.GUID)) {
+ DestroyImmediate(_thumbCache[asset.GUID]);
+ _thumbCache.Remove(asset.GUID);
+ }
+ if (_elementByAsset.ContainsKey(asset)) {
+ LoadThumb(_elementByAsset[asset], asset);
}
- _previewLoadingAssets.RemoveWhere(asset => !asset.IsLoadingAssetPreview);
}
private void OnThumbSizeChanged(ChangeEvent evt)
@@ -480,19 +581,19 @@ private void OnThumbSizeChanged(ChangeEvent evt)
}
}
- public void AttachData(AssetResult clickedAsset)
+ public void AttachData(AssetResult clickedResult)
{
- if (!_selectedAssets.Contains(clickedAsset)) {
- _selectedAssets.Add(clickedAsset);
+ if (!_selectedResults.Contains(clickedResult)) {
+ _selectedResults.Add(clickedResult);
}
- DragAndDrop.objectReferences = _selectedAssets.Select(result => result.Asset.Object).ToArray();
- StartDraggingAssets(_selectedAssets);
+ DragAndDrop.objectReferences = _selectedResults.Select(result => result.Asset.Object).ToArray();
+ StartDraggingAssets(_selectedResults);
}
}
public interface IDragHandler
{
- void AttachData(AssetResult clickedAsset);
+ void AttachData(AssetResult clickedResult);
}
}
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss
index f9ddf6429..9b0160431 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uss
@@ -24,9 +24,9 @@
-unity-text-align: middle-center;
}
-AssetDetailsElement > TemplateContainer {
+/*AssetDetails > TemplateContainer {
flex-grow: 1;
-}
+}*/
#libraryList .library-item {
margin-left: 8px;
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uxml b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uxml
index f317c4df7..4da9226de 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uxml
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser.uxml
@@ -42,13 +42,15 @@
-
+
-
+
+
+
diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs
index 5b10e4faa..1a4ea467e 100644
--- a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs
+++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowser_Init.cs
@@ -14,9 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.UIElements;
+using UnityEngine;
using UnityEngine.UIElements;
namespace VisualPinball.Unity.Editor
@@ -35,12 +37,14 @@ public partial class AssetBrowser
private VisualElement _dragErrorContainerLeft;
private Label _dragErrorLabel;
private VisualElement _dragErrorContainer;
- private AssetDetailsElement _detailsElement;
+ private AssetDetails _detailsElement;
private Label _statusLabel;
private Slider _sizeSlider;
private VisualTreeAsset _assetTree;
+ private readonly Dictionary _thumbCache = new();
+
public string DragErrorLeft {
get => _dragErrorContainerLeft.ClassListContains("hidden") ? null : _dragErrorLabelLeft.text;
set {
@@ -93,7 +97,7 @@ public void CreateGUI()
_categoryView = ui.Q();
_gridContent = ui.Q("gridContent");
- _detailsElement = ui.Q();
+ _detailsElement = ui.Q();
_statusLabel = ui.Q