Skip to content

Commit 1aef756

Browse files
author
Benoit Hudson
committed
Handle reparenting, under assumption of unique names.
Left to do: adding components.
1 parent 9680bc6 commit 1aef756

File tree

1 file changed

+268
-82
lines changed

1 file changed

+268
-82
lines changed

Assets/FbxExporters/FbxPrefab.cs

Lines changed: 268 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -155,86 +155,267 @@ public static FbxRepresentation Find(FbxRepresentation rep, string key) {
155155
}
156156

157157
/// <summary>
158-
/// Recursively perform the update.
158+
/// This class is responsible for figuring out what work needs to be done
159+
/// during an update. It's also responsible for actually doing the work.
159160
///
160-
/// In terms of a 3-way merge, "oldFbx" is the "base".
161-
/// "newFbx" and "prefabInstance" are "theirs" and "ours".
161+
/// Key assumption: upon importing, the names in the fbx model are unique.
162162
///
163-
/// Whether the fbx or the prefab counts as "ours" depends on who the
164-
/// user is: a 3d modeler that updated the FBX, or the game programmer
165-
/// who's updating the prefab.
166-
/// So we don't use 3-way merge terminology.
163+
/// TODO: handle conflicts. For now, we just clobber the prefab.
167164
/// </summary>
168-
public static void TreeDiff(FbxRepresentation oldFbx,
169-
Transform newFbx,
170-
Transform prefabInstance,
171-
string indent = "")
165+
public class UpdateList
172166
{
173-
// TODO: a better tree diff algorithm, e.g. Demaine et al 2009.
174-
//
175-
// For now we half-ass it: there's no possibility of handling
176-
// renaming other than delete-and-add. Demaine handles it.
177-
// There's also no possibility of nicely handling hierarchy
178-
// changes, but that's essentially impossible (NP-hard, no PTAS),
179-
// so it's not likely worth even trying unless we feel like doing
180-
// serious algorithmics.
181-
// 1. For each child in the union of the old, new, and prefab:
182-
// 7- old and new and prefab => recur.
183-
// 6- old and new and !prefab => skip (deleted by user)
184-
// 5- old and !new and prefab => delete from prefab
185-
// 4- old and !new and !prefab => skip, we happen to match
186-
// 3- !old and new and prefab => recur, we happen to match
187-
// 2- !old and new and !prefab => instantiate the new into the prefab
188-
// 1- !old and !new and prefab => skip (added by user)
189-
// 0- !old and !new and !prefab => not possible given the loop
190-
var names = FbxRepresentation.CopyKeySet(oldFbx);
191-
foreach(Transform child in newFbx) { names.Add(child.name); }
192-
foreach(Transform child in prefabInstance) { names.Add(child.name); }
193-
194-
indent += " ";
195-
foreach(var name in names) {
196-
var oldChild = FbxRepresentation.Find(oldFbx, name);
197-
var newChild = newFbx.Find(name);
198-
var prefabChild = prefabInstance.Find(name);
199-
int index = ((oldChild == null) ? 0 : 4)
200-
| ((newChild == null) ? 0 : 2)
201-
| ((prefabChild == null) ? 0 : 1);
202-
switch(index) {
203-
case 7:
204-
//Debug.Log(indent + "recur into " + name);
205-
TreeDiff(oldChild, newChild, prefabChild, indent);
206-
break;
207-
case 6:
208-
//Debug.Log(indent + "skip user-deleted " + name);
209-
break;
210-
case 5:
211-
//Debug.Log(indent + "delete " + name);
212-
GameObject.DestroyImmediate(prefabChild.gameObject);
213-
break;
214-
case 4:
215-
//Debug.Log(indent + "skip deleted in both new and prefabInstance " + name);
216-
break;
217-
case 3:
218-
//Debug.Log(indent + "accidental match; recur into " + name);
219-
TreeDiff(oldChild, newChild, prefabChild, indent);
220-
break;
221-
case 2:
222-
//Debug.Log(indent + "instantiate into prefabInstance " + name);
223-
prefabChild = GameObject.Instantiate(newChild);
224-
prefabChild.parent = prefabInstance;
225-
prefabChild.name = newChild.name;
226-
prefabChild.localPosition = newChild.localPosition;
227-
prefabChild.localRotation = newChild.localRotation;
228-
prefabChild.localScale = newChild.localScale;
229-
break;
230-
case 1:
231-
//Debug.Log(indent + "skip user-added node " + name);
232-
break;
233-
default:
234-
// This shouldn't happen.
235-
throw new System.NotImplementedException();
167+
168+
// We build up a flat list of names for the nodes of the old fbx,
169+
// the new fbx, and the prefab. We also figure out the parents.
170+
class Data {
171+
Dictionary<string, string> m_parents;
172+
173+
public Data() {
174+
m_parents = new Dictionary<string, string>();
175+
}
176+
177+
public void AddNode(string name, string parent) {
178+
m_parents.Add(name, parent);
179+
}
180+
181+
public bool HasNode(string name) {
182+
return m_parents.ContainsKey(name);
183+
}
184+
185+
public string GetParent(string name) {
186+
string parent;
187+
if (m_parents.TryGetValue(name, out parent)) {
188+
return parent;
189+
} else {
190+
return null;
191+
}
192+
}
193+
194+
public static HashSet<string> GetAllNames(params Data [] data) {
195+
var names = new HashSet<string>();
196+
foreach(var d in data) {
197+
foreach(var k in d.m_parents.Keys) {
198+
names.Add(k);
199+
}
200+
}
201+
return names;
202+
}
203+
}
204+
205+
/// <summary>
206+
/// Data for the hierarchy of the old fbx file, the new fbx file, and the prefab.
207+
/// </summary>
208+
Data m_old, m_new, m_prefab;
209+
210+
/// <summary>
211+
/// Names of all nodes -- old, new, or prefab.
212+
/// </summary>
213+
HashSet<string> m_allNames = new HashSet<string>();
214+
215+
/// <summary>
216+
/// Names of the new nodes to create in step 1.
217+
/// </summary>
218+
HashSet<string> m_nodesToCreate = new HashSet<string>();
219+
220+
/// <summary>
221+
/// Information about changes in parenting for step 2.
222+
/// Map from name of node in the prefab to name of node in prefab or newNodes.
223+
/// This is all the nodes in the prefab that need to be reparented.
224+
/// </summary>
225+
Dictionary<string,string> m_reparentings = new Dictionary<string, string>();
226+
227+
/// <summary>
228+
/// Names of the nodes to destroy in step 3.
229+
/// Actually calculated early.
230+
/// </summary>
231+
HashSet<string> m_nodesToDestroy = new HashSet<string>();
232+
233+
/// <summary>
234+
/// Components to destroy in step 4a.
235+
/// The string is the name in the prefab; we destroy the first
236+
/// component that matches the type.
237+
/// </summary>
238+
//Dictionary<string, List<System.Type>> m_componentsToDestroy = new Dictionary<string, List<System.Type>>();
239+
240+
/// <summary>
241+
/// List of components to update or create in steps 4b and 4c.
242+
/// The string is the name in the prefab.
243+
/// The component is a pointer to the component in the FBX.
244+
/// If the component doesn't exist in the prefab, we create it.
245+
/// If the component exists in the prefab, we update it.
246+
/// TODO: handle conflicts!
247+
/// </summary>
248+
//Dictionary<string, List<Component>> m_componentsToUpdate = new Dictionary<string, List<Component>>();
249+
250+
void SetupOld(FbxRepresentation oldFbx, string parent = null) {
251+
if (m_old == null) { m_old = new Data(); }
252+
foreach(var name in FbxRepresentation.CopyKeySet(oldFbx)) {
253+
m_old.AddNode(name, parent);
254+
SetupOld(FbxRepresentation.Find(oldFbx, name), name);
255+
}
256+
}
257+
258+
static void SetupFromTransform(Data data, Transform xfo, string parent) {
259+
foreach(Transform child in xfo) {
260+
data.AddNode(child.name, parent);
261+
SetupFromTransform(data, child, child.name);
262+
}
263+
}
264+
265+
void SetupNew(Transform newFbx) {
266+
m_new = new Data();
267+
SetupFromTransform(m_new, newFbx, null);
268+
}
269+
270+
void SetupPrefab(FbxPrefab prefab) {
271+
m_prefab = new Data();
272+
SetupFromTransform(m_prefab, prefab.transform, null);
273+
}
274+
275+
void ClassifyDestroyCreateNodes()
276+
{
277+
foreach(var name in m_allNames) {
278+
var isOld = m_old.HasNode(name);
279+
var isNew = m_new.HasNode(name);
280+
var isPrefab = m_prefab.HasNode(name);
281+
if (isOld && !isNew && isPrefab) {
282+
// This node got deleted in the DCC, so delete it.
283+
m_nodesToDestroy.Add(name);
284+
} else if (!isOld && isNew && !isPrefab) {
285+
// This node was created in the DCC but not in Unity, so create it.
286+
m_nodesToCreate.Add(name);
287+
}
288+
}
289+
}
290+
bool ShouldDestroy(string name) {
291+
return m_nodesToDestroy.Contains(name);
292+
}
293+
bool ShouldCreate(string name) {
294+
return m_nodesToCreate.Contains(name);
295+
}
296+
297+
/// <summary>
298+
/// Discover what needs to happen.
299+
///
300+
/// Four-step program:
301+
/// 1. Figure out what nodes exist (we have that data, just need to
302+
/// make it more convenient to query).
303+
/// 2. Figure out what nodes we need to create, and what nodes we
304+
/// need to destroy.
305+
/// 3. Figure out what nodes we need to reparent.
306+
/// todo 4. Figure out what nodes we need to update or add components to.
307+
/// </summary>
308+
public UpdateList(
309+
FbxRepresentation oldFbx,
310+
Transform newFbx,
311+
FbxPrefab prefab)
312+
{
313+
SetupOld(oldFbx);
314+
SetupNew(newFbx);
315+
SetupPrefab(prefab);
316+
m_allNames = Data.GetAllNames(m_old, m_new, m_prefab);
317+
318+
// Set up m_nodesToDestroy and m_nodesToCreate.
319+
ClassifyDestroyCreateNodes();
320+
321+
// Among prefab nodes we're not destroying, see if we need to change their parent.
322+
// Cases for the parent:
323+
// old new prefab
324+
// a a a => no action
325+
// a x a => doesn't matter (a is being destroyed)
326+
// a b a => switch to b
327+
// a b c => conflict! switch to b for now (todo!)
328+
// x a x => create, and parent to a. This is the second loop below.
329+
// x a a => no action
330+
// x a b => conflict! switch to a for now (todo!)
331+
// x x a => no action. Todo: what if a is being destroyed? conflict!
332+
foreach(var name in Data.GetAllNames(m_prefab)) {
333+
var prefabParent = m_prefab.GetParent(name);
334+
var oldParent = m_old.GetParent(name);
335+
var newParent = m_new.GetParent(name);
336+
337+
if (oldParent != newParent && prefabParent != newParent) {
338+
// Conflict in this case, we'll need to resolve it:
339+
// if (oldParent != prefabParent && !ShouldDestroy(prefabParent))
340+
341+
// But unconditionally switch to the new parent for
342+
// now, unless we're already there.
343+
m_reparentings.Add(name, newParent);
344+
}
345+
}
346+
// All new nodes need to be reparented (we didn't loop over them because we
347+
// only looped over the stuff in the prefab now).
348+
foreach(var name in m_nodesToCreate) {
349+
m_reparentings.Add(name, m_new.GetParent(name));
236350
}
237351
}
352+
353+
public bool NeedsUpdates() {
354+
return m_nodesToDestroy.Count > 0
355+
|| m_nodesToCreate.Count > 0
356+
|| m_reparentings.Count > 0
357+
// || m_componentsToDestroy.Count > 0
358+
// || m_comopnentsToUpdate.Count > 0
359+
;
360+
}
361+
362+
/// <summary>
363+
/// Then we act -- in a slightly different order:
364+
/// 1. Create all the new nodes we need to create.
365+
/// 2. Reparent as needed.
366+
/// 3. Delete the nodes that are no longer needed.
367+
/// todo 4. Update the components:
368+
/// 4a. delete components no longer used
369+
/// 4b. create new components
370+
/// 4c. update component values
371+
/// (A) and (B) are largely about meshfilter/meshrenderer,
372+
/// (C) is about transforms (and materials?)
373+
/// </summary>
374+
public void ImplementUpdates(FbxPrefab prefabInstance)
375+
{
376+
// Gather up all the nodes in the prefab.
377+
var prefabNodes = new Dictionary<string, Transform>();
378+
foreach(var node in prefabInstance.GetComponentsInChildren<Transform>()) {
379+
prefabNodes.Add(node.name, node);
380+
}
381+
382+
// Create new nodes.
383+
foreach(var name in m_nodesToCreate) {
384+
prefabNodes.Add(name, new GameObject(name).transform);
385+
Debug.Log("Created " + name);
386+
}
387+
388+
// Implement the reparenting in two phases to avoid making loops, e.g.
389+
// if we're flipping from a -> b to b -> a, we don't want to
390+
// have a->b->a in the intermediate stage.
391+
392+
// First set the parents to null.
393+
foreach(var kvp in m_reparentings) {
394+
var name = kvp.Key;
395+
prefabNodes[name].parent = null;
396+
}
397+
398+
// Then set the parents to the intended value.
399+
foreach(var kvp in m_reparentings) {
400+
var name = kvp.Key;
401+
var parent = kvp.Value;
402+
Transform parentNode;
403+
if (parent == null) {
404+
parentNode = prefabInstance.transform;
405+
} else {
406+
parentNode = prefabNodes[parent];
407+
}
408+
prefabNodes[name].parent = parentNode;
409+
Debug.Log("Parented " + name + " under " + parentNode.name);
410+
}
411+
412+
// Destroy the old nodes.
413+
foreach(var toDestroy in m_nodesToDestroy) {
414+
GameObject.DestroyImmediate(prefabNodes[toDestroy].gameObject);
415+
}
416+
417+
// TODO: update components!
418+
}
238419
}
239420

240421
/// <summary>
@@ -266,29 +447,34 @@ void CompareAndUpdate()
266447
return;
267448
}
268449

269-
// todo: only instantiate if there's a change
270-
var oldRep = GetFbxHistory();
271-
if (oldRep == null || oldRep.c == null) { oldRep = null; }
450+
// First write down what we want to do.
451+
var updates = new UpdateList(GetFbxHistory(), m_fbxModel.transform, this);
452+
453+
// If we don't need to do anything, jump out now.
454+
if (!updates.NeedsUpdates()) {
455+
return;
456+
}
272457

273-
// Instantiate the prefab and compare & update.
458+
// We want to do something, so instantiate the prefab, work on the instance, then copy back.
274459
var prefabInstance = UnityEditor.PrefabUtility.InstantiatePrefab(this.gameObject) as GameObject;
275460
if (!prefabInstance) {
276461
throw new System.Exception(string.Format("Failed to instantiate {0}; is it really a prefab?",
277462
this.gameObject));
278463
}
279-
TreeDiff(oldRep, m_fbxModel.transform, prefabInstance.transform);
464+
var fbxPrefab = prefabInstance.GetComponent<FbxPrefab>();
465+
466+
updates.ImplementUpdates(fbxPrefab);
280467

281468
// Update the representation of the history to match the new fbx.
282-
var fbxPrefab = prefabInstance.GetComponent<FbxPrefab>();
283469
var newFbxRep = FbxRepresentation.FromTransform(m_fbxModel.transform);
284470
var newFbxRepString = newFbxRep.ToJson();
285471
fbxPrefab.m_fbxHistory = newFbxRepString;
286472

287473
// Save the changes back to the prefab.
288-
UnityEditor.PrefabUtility.ReplacePrefab(prefabInstance.gameObject, this.transform);
474+
UnityEditor.PrefabUtility.ReplacePrefab(prefabInstance, this.transform);
289475

290476
// Destroy the prefabInstance.
291-
GameObject.DestroyImmediate(prefabInstance.gameObject);
477+
GameObject.DestroyImmediate(prefabInstance);
292478
}
293479

294480
/// <summary>

0 commit comments

Comments
 (0)