Skip to content

Commit 0c9f1c5

Browse files
committed
Initial commit
0 parents  commit 0c9f1c5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4205
-0
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# This .gitignore file should be placed at the root of your Unity project directory
2+
#
3+
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
4+
#
5+
/[Ll]ibrary/
6+
/[Tt]emp/
7+
/[Oo]bj/
8+
/[Bb]uild/
9+
/[Bb]uilds/
10+
/[Ll]ogs/
11+
/[Uu]ser[Ss]ettings/
12+
13+
# MemoryCaptures can get excessive in size.
14+
# They also could contain extremely sensitive data
15+
/[Mm]emoryCaptures/
16+
17+
# Recordings can get excessive in size
18+
/[Rr]ecordings/
19+
20+
# Uncomment this line if you wish to ignore the asset store tools plugin
21+
# /[Aa]ssets/AssetStoreTools*
22+
23+
# Autogenerated Jetbrains Rider plugin
24+
/[Aa]ssets/Plugins/Editor/JetBrains*
25+
26+
# Visual Studio cache directory
27+
.vs/
28+
29+
# Gradle cache directory
30+
.gradle/
31+
32+
# Autogenerated VS/MD/Consulo solution and project files
33+
ExportedObj/
34+
.consulo/
35+
*.csproj
36+
*.unityproj
37+
*.sln
38+
*.suo
39+
*.tmp
40+
*.user
41+
*.userprefs
42+
*.pidb
43+
*.booproj
44+
*.svd
45+
*.pdb
46+
*.mdb
47+
*.opendb
48+
*.VC.db
49+
50+
# Unity3D generated meta files
51+
*.pidb.meta
52+
*.pdb.meta
53+
*.mdb.meta
54+
55+
# Unity3D generated file on crash reports
56+
sysinfo.txt
57+
58+
# Builds
59+
*.apk
60+
*.aab
61+
*.unitypackage
62+
*.app
63+
64+
# Crashlytics generated file
65+
crashlytics-build.properties
66+
67+
# Packed Addressables
68+
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
69+
70+
# Temporary auto-generated Android Assets
71+
/[Aa]ssets/[Ss]treamingAssets/aa.meta
72+
/[Aa]ssets/[Ss]treamingAssets/aa/*

.vsconfig

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"version": "1.0",
3+
"components": [
4+
"Microsoft.VisualStudio.Workload.ManagedGame"
5+
]
6+
}

Assets/Preco21.meta

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Preco21/Editor.meta

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Preco21/Editor/DiffHelper.cs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using System.Linq;
3+
using System.Collections.Generic;
4+
5+
namespace Preco21
6+
{
7+
// simple O(nm) diffing algorithm helper; this has a limitation where it cannot cope with multiple same keys in same level of siblings.
8+
// in order to around such a case will require LCS to find edit distance between other elements for generating diffs which is in general, more complex to implement.
9+
// however, the number of nodes in Unity's GameObject tree is generally < 1000, so this shouldn't be that problematic for general use cases.
10+
public class DiffHelper
11+
{
12+
public enum Type
13+
{
14+
NONE,
15+
INSERT,
16+
DELETE,
17+
CHANGE,
18+
}
19+
20+
public struct Node<T>
21+
{
22+
public string Key;
23+
public T Value;
24+
public List<Node<T>> Children;
25+
}
26+
27+
public struct Record<T>
28+
{
29+
public Type Type;
30+
public List<string> Path;
31+
public T Value;
32+
}
33+
34+
public static T? GetValueOrNull<T>(List<T> a, System.Predicate<T> predicate) where T : struct
35+
{
36+
var index = a.FindIndex(predicate);
37+
return index < 0 ? (T?)null : a[index];
38+
}
39+
40+
public static List<Record<T>> Compare<T>(Node<T>? a, Node<T>? b, List<string> _path = null)
41+
{
42+
var path = _path ?? new List<string>();
43+
var records = new List<Record<T>>();
44+
// insertion
45+
if (!a.HasValue && b.HasValue)
46+
{
47+
var bVal = b.GetValueOrDefault();
48+
var newPath = path.Append(bVal.Key).ToList();
49+
var recInsert = new Record<T>
50+
{
51+
Type = Type.INSERT,
52+
Path = newPath,
53+
Value = bVal.Value,
54+
};
55+
records.Add(recInsert);
56+
foreach (var node in bVal.Children)
57+
{
58+
records.AddRange(Compare<T>(null, node, newPath));
59+
}
60+
return records;
61+
}
62+
// deletion
63+
if (a.HasValue && !b.HasValue)
64+
{
65+
var aVal = a.GetValueOrDefault();
66+
var newPath = path.Append(aVal.Key).ToList();
67+
var recDelete = new Record<T>
68+
{
69+
Type = Type.DELETE,
70+
Path = newPath,
71+
Value = aVal.Value,
72+
};
73+
records.Add(recDelete);
74+
foreach (var node in aVal.Children)
75+
{
76+
records.AddRange(Compare<T>(node, null, newPath));
77+
}
78+
return records;
79+
}
80+
// changes
81+
if (a.HasValue && b.HasValue)
82+
{
83+
var aVal = a.GetValueOrDefault();
84+
var bVal = b.GetValueOrDefault();
85+
// both values are same; just using `a` for record fields
86+
var newPath = path.Append(aVal.Key).ToList();
87+
var recChange = new Record<T>
88+
{
89+
Type = Type.CHANGE,
90+
Path = newPath,
91+
Value = aVal.Value,
92+
};
93+
records.Add(recChange);
94+
var left = aVal.Children.Select<Node<T>, (Node<T>?, Node<T>?)>((nodeA) => (nodeA, GetValueOrNull(bVal.Children, (nodeB) => nodeA.Key == nodeB.Key)));
95+
var right = bVal.Children.Select<Node<T>, (Node<T>?, Node<T>?)>((nodeB) => (GetValueOrNull(aVal.Children, (nodeA) => nodeB.Key == nodeA.Key), nodeB));
96+
var combined = left.Concat(right).GroupBy((tuple) => new { tuple.Item1, tuple.Item2 }).Select((node) => node.First());
97+
foreach (var tuple in combined)
98+
{
99+
records.AddRange(Compare(tuple.Item1, tuple.Item2, newPath));
100+
}
101+
return records;
102+
}
103+
throw new Exception("invariant: at least one node must not be null");
104+
}
105+
}
106+
}

Assets/Preco21/Editor/DiffHelper.cs.meta

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using UnityEditor;
5+
using UnityEngine;
6+
using Preco21;
7+
8+
public class ObjectHierarchyDiff : EditorWindow
9+
{
10+
private static string LOG_TAG = "[ObjectHierarchyDiff]:";
11+
private enum ExtractDiffType
12+
{
13+
BASE,
14+
TARGET
15+
};
16+
17+
private GameObject baseObject = null;
18+
private GameObject targetObject = null;
19+
private ExtractDiffType extractDiffType = ExtractDiffType.BASE;
20+
private string additionPrefix = "__added";
21+
private string deletionPrefix = "__deleted";
22+
23+
[MenuItem("ObjectHierarchyDiff/ObjectHierarchyDiff")]
24+
public static void ShowWindow()
25+
{
26+
GetWindow<ObjectHierarchyDiff>().Show();
27+
}
28+
29+
void OnGUI()
30+
{
31+
GUILayout.Label("Setup", EditorStyles.boldLabel);
32+
GUILayout.BeginHorizontal();
33+
EditorGUILayout.LabelField("Base Object");
34+
baseObject = EditorGUILayout.ObjectField(baseObject, typeof(GameObject), true) as GameObject;
35+
GUILayout.EndHorizontal();
36+
GUILayout.BeginHorizontal();
37+
EditorGUILayout.LabelField("Target Object");
38+
targetObject = EditorGUILayout.ObjectField(targetObject, typeof(GameObject), true) as GameObject;
39+
GUILayout.EndHorizontal();
40+
41+
GUILayout.Space(10);
42+
43+
GUILayout.Label("Options", EditorStyles.boldLabel);
44+
extractDiffType = (ExtractDiffType)EditorGUILayout.Popup("Extract Diffs On", (int)extractDiffType, new string[] { "Base (deletion: -)", "Target (addition: +)" });
45+
46+
switch (extractDiffType)
47+
{
48+
case ExtractDiffType.BASE:
49+
deletionPrefix = EditorGUILayout.TextField("Deletion Prefix", deletionPrefix);
50+
break;
51+
case ExtractDiffType.TARGET:
52+
additionPrefix = EditorGUILayout.TextField("Addition Prefix", additionPrefix);
53+
break;
54+
default:
55+
throw new Exception("invariant: unexpected `ExtractDiffType`");
56+
}
57+
58+
GUILayout.Space(15);
59+
60+
if (GUILayout.Button("Run"))
61+
{
62+
var (valid, message) = Validate();
63+
if (valid)
64+
{
65+
Run();
66+
}
67+
else
68+
{
69+
Debug.LogError($"{LOG_TAG} invariant: {message}");
70+
}
71+
}
72+
}
73+
74+
private (bool, string) Validate()
75+
{
76+
if (baseObject == null)
77+
{
78+
return (false, "`Base Object` must be specified.");
79+
}
80+
if (targetObject == null)
81+
{
82+
return (false, "`Target Object` must be specified.");
83+
}
84+
if (extractDiffType == ExtractDiffType.BASE && deletionPrefix.Trim().Equals(""))
85+
{
86+
return (false, "`Deletion Prefix` must be specified.");
87+
}
88+
if (extractDiffType == ExtractDiffType.TARGET && additionPrefix.Trim().Equals(""))
89+
{
90+
return (false, "`Addition Prefix` must be specified.");
91+
}
92+
return (true, "");
93+
}
94+
95+
private void Run()
96+
{
97+
var leftNode = ConvertGameObjectToNode(baseObject);
98+
var rightNode = ConvertGameObjectToNode(targetObject);
99+
var diff = DiffHelper.Compare<GameObject>(leftNode, rightNode);
100+
if (diff.Count < 1)
101+
{
102+
Debug.LogWarning($"{LOG_TAG} No diffs found.");
103+
return;
104+
}
105+
DiffHelper.Node<GameObject> extractTo;
106+
List<DiffHelper.Record<GameObject>> subsetDiff;
107+
string prefix;
108+
switch (extractDiffType)
109+
{
110+
case ExtractDiffType.BASE:
111+
Debug.Log($"{LOG_TAG} Extracting Diffs on Base Object...");
112+
extractTo = leftNode;
113+
subsetDiff = diff.Where((e) => e.Type == DiffHelper.Type.DELETE).ToList();
114+
prefix = deletionPrefix;
115+
break;
116+
case ExtractDiffType.TARGET:
117+
Debug.Log($"{LOG_TAG} Extracting Diffs on Target Object...");
118+
extractTo = rightNode;
119+
subsetDiff = diff.Where((e) => e.Type == DiffHelper.Type.INSERT).ToList();
120+
prefix = additionPrefix;
121+
break;
122+
default:
123+
throw new Exception("invariant: unexpected `ExtractDiffType`");
124+
}
125+
Undo.RegisterFullObjectHierarchyUndo(extractTo.Value, $"ObjectHierarchyDiff: Run on `{extractTo.Value.name}`");
126+
void traverse(DiffHelper.Node<GameObject> node, List<string> path)
127+
{
128+
// FIXME: except root
129+
var currentPath = path.Count < 1 ? path.Append(node.Key).ToList() : path;
130+
var pathWithoutRoot = currentPath.Skip(1).ToList();
131+
foreach (var d in subsetDiff)
132+
{
133+
var diffPath = d.Path.Skip(1);
134+
if (Enumerable.SequenceEqual(pathWithoutRoot, diffPath))
135+
{
136+
node.Value.name = $"{node.Value.name}{prefix}";
137+
}
138+
}
139+
foreach (var obj in node.Children)
140+
{
141+
traverse(obj, currentPath.Append(obj.Key).ToList());
142+
}
143+
}
144+
traverse(extractTo, new List<string>());
145+
}
146+
147+
private DiffHelper.Node<GameObject> ConvertGameObjectToNode(GameObject gameObject, DiffHelper.Node<GameObject>? anchor = null)
148+
{
149+
var node = anchor ?? new DiffHelper.Node<GameObject>
150+
{
151+
Key = gameObject.name,
152+
Value = gameObject,
153+
Children = new List<DiffHelper.Node<GameObject>>(),
154+
};
155+
Transform[] children = gameObject.transform.GetComponentsInChildren<Transform>();
156+
foreach (var obj in children)
157+
{
158+
// for whatever reasons, Unity yields children nodes in awkward way, in which a node contains all the descendants
159+
// to around this, we need to check for some conditions here
160+
if (obj.gameObject == gameObject || obj.parent.gameObject != gameObject)
161+
{
162+
continue;
163+
}
164+
var newNode = new DiffHelper.Node<GameObject>
165+
{
166+
Key = obj.name,
167+
Value = obj.gameObject,
168+
Children = new List<DiffHelper.Node<GameObject>>(),
169+
};
170+
node.Children.Add(ConvertGameObjectToNode(obj.gameObject, newNode));
171+
}
172+
return node;
173+
}
174+
}

0 commit comments

Comments
 (0)