/// /// <summary>
        /// Create a new (auto generated) model group with default values
        /// </summary>
        /// <param name="name">The name of the new group</param>
        /// <param name="undoBehavior">Whether or not to register undo for this action</param>
        /// <returns>The new model</returns>
        public static ModelGroup CreateNewDefaultModelGroup(string name, UndoBehavior undoBehavior = UndoBehavior.withUndo)
        {
            ModelGroup group   = null;
            var        groupGO = new GameObject();

            group               = groupGO.AddComponent <ModelGroup>();
            group.name          = name;
            group.groupName     = group.name;
            group.autoGenerated = true;

            // Add LEGOModelGroupAsset component.
            groupGO.AddComponent <LEGOModelGroupAsset>();

            if (undoBehavior == UndoBehavior.withUndo)
            {
                Undo.RegisterCreatedObjectUndo(group.gameObject, "Creating new model group");
            }
            return(group);
        }
        public static void ProcessModelGroup(ModelGroup group, ref Vector2Int vertCount, ref Vector2Int triCount, ref Vector2Int meshCount, ref Vector2Int boxColliderCount)
        {
            bool collapseMesh = true;
            bool collapseCols = group.importSettings.colliders;

            // Keep track of how many of the removable meshes are left for each part.
            var partMeshCount = new Dictionary <Part, int>();
            // Keep track of how many of the removable surface meshes (shell, colourChangeSurface) are left for each non-legacy part.
            var partSurfaceMeshCount = new Dictionary <Part, int>();

            // TODO: Move orphaned colliders to group root

            var progress = 0;

            if (collapseMesh)
            {
                //    Debug.Log("Collapsing Mesh for "+group.name);

                // Optimization settings
                bool doSort = group.optimizations.HasFlag(ModelGroup.Optimizations.SortFrontToBack);
                bool canRemoveTubesAndPins        = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveTubesAndPins);
                bool canRemoveKnobs               = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveKnobs);
                bool canRemoveCompletelyInvisible = group.optimizations.HasFlag(ModelGroup.Optimizations.RemoveInvisible);
                bool cullBackfaces = group.optimizations.HasFlag(ModelGroup.Optimizations.BackfaceCulling);

                bool randomizeNormals = group.randomizeNormals;

                // Split into different meshes depending on whether they have knobs or are transparent.
                // 0 = No knobs = Fast track to avoid normal mapping when it isn't required
                // 1 = Knobs = Slow track needs normal mapping
                // 2 = Transparent No Knobs = Fast track to avoid normal mapping when it isn't required.
                // 3 = Transparent Knobs = Slow track since we will not render transparent bricks during visibility tests.

                MeshHelper[] meshHelpers = new MeshHelper[] { new MeshHelper(), new MeshHelper(), new MeshHelper(), new MeshHelper() };

                Matrix4x4 groupMatrix    = group.transform.localToWorldMatrix;
                Matrix4x4 groupMatrixInv = groupMatrix.inverse;

                // Collect all parts.
                var parts = group.GetComponentsInChildren <Part>();

                // Collect relevant part mesh renderers along with mesh type information and a randomized rotation for each part.
                List <PartMeshRenderer> mrs = new List <PartMeshRenderer>();

                foreach (var part in parts)
                {
                    if (progress++ % 200 == 0)
                    {
                        EditorUtility.DisplayProgressBar("Processing", "Collecting part renderers.", ((float)progress / parts.Length) * 0.05f);
                    }

                    partMeshCount[part] = 0;

                    if (part.legacy)
                    {
                        // Legacy parts only have unstructured meshes.
                        foreach (var renderer in part.GetComponentsInChildren <MeshRenderer>(true))
                        {
                            var partMeshRenderer = new PartMeshRenderer()
                            {
                                part       = part,
                                rendererer = renderer,
                                type       = MeshType.Legacy
                            };
                            mrs.Add(partMeshRenderer);
                            partMeshCount[part]++;
                        }
                    }
                    else
                    {
                        partSurfaceMeshCount[part] = 0;

                        foreach (var renderer in part.GetComponentsInChildren <MeshRenderer>(true))
                        {
                            var parentName = renderer.transform.parent.name;

                            // First check if it is a decoration surface since we will not include them in the processing.
                            if (parentName == "DecorationSurfaces")
                            {
                                continue;
                            }

                            var partMeshRenderer = new PartMeshRenderer()
                            {
                                part       = part,
                                rendererer = renderer.GetComponent <MeshRenderer>()
                            };
                            if (parentName == "Knobs") // Is it a knob?
                            {
                                partMeshRenderer.type = MeshType.Knob;
                            }
                            else if (parentName == "Tubes") // Is it a tube?
                            {
                                partMeshRenderer.type = MeshType.Tube;
                            }
                            else if (parentName == "ColourChangeSurfaces") // Is it a colour change surface?
                            {
                                partMeshRenderer.type = MeshType.ColourChange;
                                partSurfaceMeshCount[part]++;
                            }
                            else // It must be the shell.
                            {
                                partMeshRenderer.type = MeshType.Shell;
                                partSurfaceMeshCount[part]++;
                            }

                            mrs.Add(partMeshRenderer);
                            partMeshCount[part]++;
                        }
                    }
                }

                Vector3 camPos = Vector3.zero;
                Vector3 camDir = Vector3.zero;

                if (group.views.Count == 0 && Camera.main)
                {
                    camPos = Camera.main.transform.position;
                    camDir = Camera.main.transform.forward;
                }
                else if (group.views.Count > 0)
                {
                    camPos = group.views[0].position;
                    camDir = group.views[0].rotation * Vector3.forward;
                }
                else
                {
                    doSort = false;
                    Debug.LogError("No views specified for front-to-back geometry sorting. Disabling!");
                }

                if (doSort)
                {
                    Vector3 sortDir;
                    if (group.views.Count == 0)
                    {
                        sortDir = camDir;
                    }
                    else
                    {
                        sortDir = group.views[0].rotation * Vector3.forward;
                    }

                    mrs.Sort(delegate(PartMeshRenderer a, PartMeshRenderer b)
                    {
                        float dA = Vector3.Dot(a.rendererer.bounds.center, sortDir);
                        float dB = Vector3.Dot(b.rendererer.bounds.center, sortDir);
                        return(dA.CompareTo(dB));
                    });
                }


                // Multiple passes
                // - Collect all meshes
                // - Remove original meshfilters and renderers
                // - Determine which Optimization level can be used (for legacy meshes only) or if mesh can be discarded completely
                // - Remove backfaces
                // - Build combined mesh

                // Collect meshes
                List <MeshInstance> instances = new List <MeshInstance>();

                for (int i = 0; i < mrs.Count; ++i)
                {
                    if (i % 200 == 0)
                    {
                        EditorUtility.DisplayProgressBar("Processing", "Collecting part meshes.", 0.05f + (float)i / mrs.Count * 0.05f);
                    }

                    MeshFilter mf = mrs[i].rendererer.GetComponent <MeshFilter>();
                    if (mf)
                    {
                        if (mrs[i].rendererer.enabled && mrs[i].rendererer.gameObject.activeInHierarchy)
                        {
                            Mesh source = mf.sharedMesh;
                            if (source)
                            {
                                meshCount.x++;
                                triCount.x  += (int)source.GetIndexCount(0) / 3;
                                vertCount.x += source.vertexCount;

                                Material material = mrs[i].rendererer.sharedMaterial;

                                MeshInstance c = new MeshInstance();
                                c.mesh        = source;
                                c.matrix      = groupMatrixInv * mf.transform.localToWorldMatrix;
                                c.worldMatrix = mf.transform.localToWorldMatrix;
                                c.material    = material;
                                c.transparent = material && material.color.a < 1.0;
                                c.part        = mrs[i].part;
                                c.type        = mrs[i].type;
                                c.up          = mf.transform.up;
                                c.bounds      = mrs[i].rendererer.bounds;
                                instances.Add(c);
                            }
                        }
                        Object.DestroyImmediate(mf);
                    }
                    Object.DestroyImmediate(mrs[i].rendererer);
                }

                List <Vector3>   viewPositions      = new List <Vector3>();
                List <Vector3>   viewDirections     = new List <Vector3>();
                List <Matrix4x4> viewMatrices       = new List <Matrix4x4>();
                List <Matrix4x4> projectionMatrices = new List <Matrix4x4>();
                List <Plane[]>   viewFrustums       = new List <Plane[]>();

                if (group.views.Count == 0 && Camera.main)
                {
                    viewMatrices.Add(Camera.main.worldToCameraMatrix);

                    if (Camera.main.orthographic)
                    {
                        viewDirections.Add(camDir);
                    }
                    else
                    {
                        viewPositions.Add(camPos);
                    }

                    viewFrustums.Add(GeometryUtility.CalculateFrustumPlanes(Camera.main));

                    projectionMatrices.Add(Camera.main.projectionMatrix);
                }
                else if (group.views.Count > 0)
                {
                    for (int i = 0; i < group.views.Count; ++i)
                    {
                        var view       = group.views[i];
                        var viewMatrix = Matrix4x4.TRS(view.position, view.rotation, Vector3.one).inverse;

                        // Invert Z for metal/openGL
                        if (SystemInfo.usesReversedZBuffer)
                        {
                            viewMatrix.SetRow(2, -viewMatrix.GetRow(2));
                        }

                        viewMatrices.Add(viewMatrix);

                        Matrix4x4 projectionMatrix;
                        Plane[]   frustumPlanes;
                        if (view.perspective)
                        {
                            projectionMatrix = Matrix4x4.Perspective(
                                view.fov,
                                view.aspect,
                                view.minRange,
                                view.maxRange
                                );

                            viewPositions.Add(view.position);

                            frustumPlanes = MathUtils.GetFrustumPlanesPerspective(view.position, view.rotation, view.fov, view.aspect, view.minRange, view.maxRange);
                        }
                        else
                        {
                            projectionMatrix = Matrix4x4.Ortho(
                                -view.size * view.aspect,
                                view.size * view.aspect,
                                -view.size,
                                view.size,
                                view.minRange,
                                view.maxRange
                                );

                            viewDirections.Add(view.rotation * Vector3.forward);

                            frustumPlanes = MathUtils.GetFrustumPlanesOrtho(view.position, view.rotation, view.size, view.aspect, view.minRange, view.maxRange);
                        }

                        projectionMatrices.Add(projectionMatrix);

                        viewFrustums.Add(frustumPlanes);
                    }
                }
                else
                {
                    if (canRemoveTubesAndPins || canRemoveKnobs || canRemoveCompletelyInvisible || cullBackfaces)
                    {
                        Debug.LogError("No views specified for backface culling and geometry removal. Disabling!");
                        canRemoveTubesAndPins        = false;
                        canRemoveKnobs               = false;
                        canRemoveCompletelyInvisible = false;
                        cullBackfaces = false;
                    }
                }

                if (instances.Count > 16777215)
                {
                    Debug.LogError($"Group {group.groupName} contains too many meshes. Some meshes will not be optimized correctly. Please split the group into multiple groups.");
                }

                AnalyzeMeshes(viewMatrices, projectionMatrices, viewPositions, viewDirections, viewFrustums, instances, partMeshCount, partSurfaceMeshCount, canRemoveTubesAndPins, canRemoveKnobs, canRemoveCompletelyInvisible, group.importSettings.lod);

                // Combine instances to a single mesh
                for (int i = 0; i < instances.Count; ++i)
                {
                    if (i % 200 == 0)
                    {
                        EditorUtility.DisplayProgressBar("Processing", "Combining part meshes.", 0.2f + (float)i / instances.Count * 0.05f);
                    }

                    Mesh source = instances[i].mesh;
                    if (source == null)
                    {
                        continue;
                    }

                    Matrix4x4 matrix   = instances[i].matrix;
                    Material  material = instances[i].material;
                    int       subMesh  = instances[i].transparent ? (instances[i].type == MeshType.Knob ? 3 : 2) : instances[i].pixelKnobCount >= knobNormalMapThreshold ? 1 : 0;

                    MeshHelper mh = new MeshHelper(source);
                    mh.Transform(matrix);

                    // Cull backfaces
                    if (cullBackfaces)
                    {
                        for (int t = 0; t < mh.triangles.Count; t += 3)
                        {
                            int     v0     = mh.triangles[t];
                            int     v1     = mh.triangles[t + 1];
                            int     v2     = mh.triangles[t + 2];
                            Vector3 triCen = groupMatrix.MultiplyPoint((mh.vertices[v0] + mh.vertices[v1] + mh.vertices[v2]) / 3);
                            Vector3 triNor = groupMatrix.MultiplyVector(mh.normals[v0] + mh.normals[v1] + mh.normals[v2]);//.normalized;

                            bool anyInView = false;
                            for (int v = 0; v < viewPositions.Count; ++v)
                            {
                                anyInView |= (Vector3.Dot(viewPositions[v] - triCen, triNor) >= 0);
                            }
                            for (int v = 0; v < viewDirections.Count; ++v)
                            {
                                anyInView |= (Vector3.Dot(viewDirections[v], triNor) <= 0);
                            }
                            if (!anyInView)
                            {
                                mh.triangles[t]     = -1;
                                mh.triangles[t + 1] = -1;
                                mh.triangles[t + 2] = -1;
                            }
                        }
                        mh.triangles.RemoveAll((obj) => obj < 0);

                        mh.RemoveUnusedVertices();
                    }

                    // culled completely?
                    if (mh.vertices.Count == 0)
                    {
                        partMeshCount[instances[i].part]--;
                        continue;
                    }

                    meshCount.y++;
                    triCount.y  += mh.triangles.Count / 3;
                    vertCount.y += mh.vertices.Count;

                    // Store color in vertices
                    mh.SetColor(material);

                    // Randomize normals
                    if (randomizeNormals)
                    {
                        mh.AddNormalNoise();
                    }

                    meshHelpers[subMesh].Combine(mh);

                    mh = null;
                }

                EditorUtility.DisplayProgressBar("Processing", "Building new meshes.", 0.3f);

                for (int i = 0; i < meshHelpers.Length; ++i)
                {
                    if (meshHelpers[i].vertices.Count > 0)
                    {
                        GameObject target = group.gameObject;
                        if (i > 0)
                        {
                            target = new GameObject(group.name + "_subMesh" + i);
                            Undo.RegisterCreatedObjectUndo(target, "Create sub mesh");
                            target.transform.SetParent(group.transform, false);
                        }

                        MeshFilter   mf = Undo.AddComponent <MeshFilter>(target);
                        MeshRenderer mr = Undo.AddComponent <MeshRenderer>(target);

                        Mesh m = new Mesh();
                        meshHelpers[i].ToMesh(m, group.importSettings.lightmapped);

                        // Need tangents?
                        if (i > 0)
                        {
                            m.RecalculateTangents();
                        }

                        // Make static.
                        target.isStatic = true;
                        if (group.importSettings.lightmapped)
                        {
                            mr.receiveGI = ReceiveGI.Lightmaps;
                        }
                        else
                        {
                            mr.receiveGI = ReceiveGI.LightProbes;
                        }

                        mf.sharedMesh = m;
                        switch (i)
                        {
                        case 0:
                        {
                            PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized.asset");
                            mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor.mat");
                            break;
                        }

                        case 1:
                        {
                            PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_NormalMap.asset");
                            mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_NormalMap.mat");
                            break;
                        }

                        case 2:
                        {
                            PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_Transparent.asset");
                            mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_Transparent.mat");
                            break;
                        }

                        case 3:
                        {
                            PartUtility.StoreOptimizedMesh(m, group.parentName + "_" + group.groupName + "_Optimized_Transparent_NormalMap.asset");
                            mr.sharedMaterial = AssetDatabase.LoadAssetAtPath <Material>("Packages/com.unity.lego.modelimporter/Materials/LEGO_VertexColor_Transparent_NormalMap.mat");
                            break;
                        }
                        }
                    }
                }
            }

            // Remove decoration surfaces for non-legacy parts that have no surface meshes left.
            progress = 0;
            foreach (var entry in partSurfaceMeshCount)
            {
                if (progress++ % 200 == 0)
                {
                    EditorUtility.DisplayProgressBar("Processing", "Removing unneeded decorations.", 0.3f + (float)progress / partSurfaceMeshCount.Count * 0.25f);
                }

                if (!entry.Key.legacy && entry.Value == 0)
                {
                    var decorationSurfaces = entry.Key.transform.Find("DecorationSurfaces");
                    if (decorationSurfaces)
                    {
                        Undo.DestroyObjectImmediate(decorationSurfaces.gameObject);
                    }
                }
            }

            // Remove parts that have no meshes left.
            progress = 0;
            foreach (var entry in partMeshCount)
            {
                if (progress++ % 200 == 0)
                {
                    EditorUtility.DisplayProgressBar("Processing", "Removing empty parts.", 0.55f + (float)progress / partMeshCount.Count * 0.25f);
                }

                if (entry.Value == 0)
                {
                    entry.Key.brick.parts.Remove(entry.Key);

                    // If no parts are left, remove the brick.
                    if (entry.Key.brick.parts.Count == 0)
                    {
                        Undo.DestroyObjectImmediate(entry.Key.brick.gameObject);
                    }
                    else
                    {
                        Undo.DestroyObjectImmediate(entry.Key.gameObject);
                    }
                }
            }

            // Collapse remaining box colliders.
            if (collapseCols)
            {
                // FIXME Move to proper constant.
                var colliderSizeBias = 0.02f;
                var epsilon          = 0.001f;

                var allColliders = group.GetComponentsInChildren <BoxCollider>();
                // Filter out colliders that are part of connectivity features.
                var partColliders = allColliders.Where((c) => c.gameObject.layer != LayerMask.NameToLayer(Connection.connectivityReceptorLayerName) && c.gameObject.layer != LayerMask.NameToLayer(Connection.connectivityConnectorLayerName)).ToArray();
                boxColliderCount.x = partColliders.Length;
                boxColliderCount.y = partColliders.Length;
                bool[] colDeleted       = new bool[partColliders.Length];
                var    collapseHappened = true;
                var    iterationCount   = 0;
                while (collapseHappened)
                {
                    collapseHappened = false;
                    iterationCount++;
                    for (var i = 0; i < partColliders.Length; ++i)
                    {
                        if (i % 200 == 0)
                        {
                            EditorUtility.DisplayProgressBar("Processing", "Collapsing colliders - iteration " + iterationCount + ".", 0.8f + (float)i / partColliders.Length * 0.05f);
                        }

                        if (colDeleted[i])
                        {
                            continue;
                        }

                        for (var j = i + 1; j < partColliders.Length; ++j)
                        {
                            if (colDeleted[j])
                            {
                                continue;
                            }

                            var colliderA = partColliders[i];
                            var colliderB = partColliders[j];

                            // Check that spaces match up.
                            var colliderBRotationInALocalSpace = Quaternion.Inverse(colliderA.transform.rotation) * colliderB.transform.rotation;
                            var euler = colliderBRotationInALocalSpace.eulerAngles;
                            if (Mathf.Abs(Mathf.Round(euler.x / 90.0f) - euler.x / 90.0f) < epsilon && Mathf.Abs(Mathf.Round(euler.y / 90.0f) - euler.y / 90.0f) < epsilon && Mathf.Abs(Mathf.Round(euler.z / 90.0f) - euler.z / 90.0f) < epsilon)
                            {
                                // Check that centers match up.
                                var colliderBCenterInColliderALocalSpace = colliderA.transform.InverseTransformPoint(colliderB.transform.TransformPoint(colliderB.center));

                                var centerDiff            = colliderBCenterInColliderALocalSpace - colliderA.center;
                                var axisToMatch           = centerDiff.MajorAxis();
                                var centerProjectedToAxis = centerDiff.SnapToMajorAxis() * centerDiff.magnitude;
                                if ((centerDiff - centerProjectedToAxis).sqrMagnitude < 0.01f)
                                {
                                    //Debug.Log(GetGameObjectPath(colliderA.gameObject) + colliderA.transform.GetSiblingIndex() + "\n" + GetGameObjectPath(colliderB.gameObject) + colliderB.transform.GetSiblingIndex());
                                    //Debug.Log("Matched centers on " + axisToMatch);
                                    // Check that size match up.
                                    var colliderBSizeInColliderALocalSpace = colliderA.transform.InverseTransformVector(colliderB.transform.TransformVector(colliderB.size)).Abs();
                                    //Debug.Log("Collider b size in collider a local space: " + colliderBSizeInColliderALocalSpace);

                                    var otherAxis1 = (axisToMatch + 1) % 3;
                                    var otherAxis2 = (axisToMatch + 2) % 3;
                                    if (Mathf.Abs(centerDiff.magnitude - (colliderA.size[axisToMatch] + colliderBSizeInColliderALocalSpace[axisToMatch] + 2.0f * colliderSizeBias) * 0.5f) < epsilon)
                                    {
                                        //Debug.Log("Matched on axis length");
                                        if (Mathf.Abs(colliderA.size[otherAxis1] - colliderBSizeInColliderALocalSpace[otherAxis1]) < epsilon && Mathf.Abs(colliderA.size[otherAxis2] - colliderBSizeInColliderALocalSpace[otherAxis2]) < epsilon)
                                        {
                                            //Debug.Log("Matched other axes");
                                            // Merge collider B into collider A.
                                            colliderA.center += centerDiff.normalized * (colliderBSizeInColliderALocalSpace[axisToMatch] + colliderSizeBias) * 0.5f;
                                            var newSize = colliderA.size;
                                            newSize[axisToMatch] += colliderBSizeInColliderALocalSpace[axisToMatch] + colliderSizeBias;
                                            colliderA.size        = newSize;

                                            // Update part and destroy collider game object. Empty parent Colliders game objects will be removed during part clean-up.
                                            var part = colliderB.GetComponentInParent <Part>();
                                            part.colliders.Remove(colliderB);
                                            Undo.DestroyObjectImmediate(colliderB.gameObject);

                                            colDeleted[j] = true;

                                            boxColliderCount.y--;

                                            // Note that a change was made, and run over colliders again when done.
                                            collapseHappened = true;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (collapseMesh || collapseCols)
            {
                // Collect all remaining parts and clean them up.
                progress = 0;
                var parts = group.GetComponentsInChildren <Part>();
                foreach (var part in parts)
                {
                    if (progress++ % 200 == 0)
                    {
                        EditorUtility.DisplayProgressBar("Processing", "Cleaning up remaining parts.", 0.85f + (float)progress / parts.Length * 0.15f);
                    }
                    CleanupPartGeometryTransforms(part);
                }
            }

            EditorUtility.ClearProgressBar();
        }
        /// <summary>
        /// Recompute the model and model group hierarchy.
        /// Some rules:
        /// 1. A brick will always be in a model group, which will always be in a model.
        /// 2. Empty models and model groups are destroyed.
        /// 3. Bricks can not contain bricks, model groups can not contain model groups and models can not contain models. So they are flattened.
        /// </summary>
        /// <param name="bricks">The bricks that we need to check the hierarchy for</param>
        /// <param name="alignRotation">Whether or not we also align rotation when we recompute pivot</param>
        /// <param name="undoBehavior">Whether or not to register undo for the recomputation of hierarchy</param>
        public static void RecomputeHierarchy(IEnumerable <Brick> bricks, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo)
        {
            // Group numbers are numbers in parentheses
            // So group 1 with three bricks is shown as (1, 1, 1)
            // In case the group is part of a prefab it is suffixed with a p like (1p, 1p)
            // In case one of the bricks in a group is an override it is noted with a + as in (1p, 1p+)

            // Cases for splitting:
            // (1, 1) -> (1) (1)
            // (1, 1) (2) -> (1) (1, 2) -> (1) (2, 2)

            // Cases for unpacking:
            // (1p) (2p) -> (1, 2p) -> (2p+, 2p)
            // (1p, 1p+) (2p) -> (1, 1, 2p) -> (2p+, 2p+, 2p)
            // (1p, 1p) -> (1p) (1p) -> (1) (1)

            // Only set parent:
            // (1) (2) -> (1, 2) -> (2, 2)
            // (1p, 1p+) (2p) -> (1p) (1p+, 2p) -> (1p) (2p+, 2p)
            // (1p, 1p+) (2) -> (1p) (1p+, 2) -> (1p) (2, 2)

            if (PrefabStageUtility.GetCurrentPrefabStage() != null)
            {
                var rootObject = PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot;
                var brick      = rootObject.GetComponent <Brick>();
                if (brick)
                {
                    return;
                }
            }

            // First we flatten models
            var modelsToCheck = new HashSet <Model>();
            var groupsToCheck = new HashSet <ModelGroup>();
            var bricksToCheck = new HashSet <Brick>();

            foreach (var brick in bricks)
            {
                var bricksInParent = brick.GetComponentsInParent <Brick>(true);
                if (bricksInParent.Length > 1)
                {
                    bricksToCheck.Add(brick);
                }
                var modelsInParent = brick.GetComponentsInParent <Model>(true);
                if (modelsInParent.Length > 1)
                {
                    modelsToCheck.UnionWith(modelsInParent);
                }
                var groupsInParent = brick.GetComponentsInParent <ModelGroup>(true);
                if (groupsInParent.Length > 1)
                {
                    groupsToCheck.UnionWith(groupsInParent);
                }
            }

            foreach (var model in modelsToCheck)
            {
                var modelsInModel = model.GetComponentsInChildren <Model>(true);
                foreach (var inModel in modelsInModel)
                {
                    if (inModel == model)
                    {
                        continue;
                    }

                    var groupsInModel = inModel.GetComponentsInChildren <ModelGroup>(true);
                    foreach (var group in groupsInModel)
                    {
                        SetParent(group.transform, model.transform, undoBehavior);
                    }
                }
            }

            // Now flatten groups
            foreach (var group in groupsToCheck)
            {
                var groupsInGroup = group.GetComponentsInChildren <ModelGroup>(true);
                foreach (var inGroup in groupsInGroup)
                {
                    if (inGroup == group)
                    {
                        continue;
                    }

                    var bricksInGroup = inGroup.GetComponentsInChildren <Brick>(true);
                    foreach (var brick in bricksInGroup)
                    {
                        SetParent(brick.transform, group.transform, undoBehavior);
                    }
                }
            }

            // Now flatten bricks
            foreach (var brick in bricksToCheck)
            {
                var group = GetGroupInParent(brick.transform);
                if (group)
                {
                    SetParent(brick.transform, group.transform, undoBehavior);
                }
            }

            var connectedClusters = new List <HashSet <Brick> >();

            // Collect all connected brick lists
            foreach (var brick in bricks)
            {
                if (connectedClusters.Any(x => x.Contains(brick)))
                {
                    continue;
                }

                if (!brick.HasConnectivity())
                {
                    var group = GetGroupInParent(brick.transform);
                    if (!group)
                    {
                        connectedClusters.Add(new HashSet <Brick> {
                            brick
                        });
                    }
                    else
                    {
                        var model = GetModelInParent(brick.transform);
                        if (!model)
                        {
                            var bricksInGroup = group.GetComponentsInChildren <Brick>(true);
                            var connected     = new HashSet <Brick>();
                            connected.Add(brick);
                            connectedClusters.Add(connected);
                        }
                    }
                }
                else
                {
                    var connected = brick.GetConnectedBricks();
                    connected.Add(brick);
                    connectedClusters.Add(connected);
                }
            }

            // Now find all groups for each cluster
            var groupsPerCluster = new List <(HashSet <Brick>, HashSet <ModelGroup>, HashSet <Brick>)>();

            foreach (var cluster in connectedClusters)
            {
                if (cluster.Count == 0)
                {
                    continue;
                }

                var bricksNotInGroup = new HashSet <Brick>();
                var groups           = new HashSet <ModelGroup>();
                foreach (var brick in cluster)
                {
                    var group = GetGroupInParent(brick.transform);
                    if (group)
                    {
                        groups.Add(group);
                    }
                    else
                    {
                        bricksNotInGroup.Add(brick);
                    }
                }
                groupsPerCluster.Add((cluster, groups, bricksNotInGroup));
            }

            // Sorting makes sure we merge before we split. Merging will make it easier to see what we need to split later.
            groupsPerCluster = groupsPerCluster.OrderByDescending(x => x.Item2.Count).ToList();

            // Check through each of these groups in the cluster
            foreach (var groupPerCluster in groupsPerCluster)
            {
                var cluster    = groupPerCluster.Item1;
                var groups     = groupPerCluster.Item2;
                var notInGroup = groupPerCluster.Item3;

                // If the cluster has more than one group, we need to merge them
                if (groups.Count > 1)
                {
                    // Merge some groups
                    ModelGroup largestGroup     = null;
                    int        largestGroupSize = 0;
                    foreach (var group in groups)
                    {
                        var bricksInGroup   = group.GetComponentsInChildren <Brick>(true);
                        var bricksInCluster = bricksInGroup.Count(x => cluster.Contains(x));
                        if (bricksInCluster >= largestGroupSize)
                        {
                            largestGroup     = group;
                            largestGroupSize = bricksInCluster;
                        }
                    }

                    foreach (var brick in cluster)
                    {
                        if (brick.transform.parent == largestGroup.transform)
                        {
                            continue;
                        }

                        if (IsPartOfPrefab(brick))
                        {
                            UnpackPrefab(brick, undoBehavior);
                        }
                        SetParent(brick.transform, largestGroup.transform, undoBehavior);
                    }

                    RecomputePivot(largestGroup, alignRotation, undoBehavior);
                    var modelGO = largestGroup.transform.parent;
                    var model   = modelGO.GetComponent <Model>();
                    if (model)
                    {
                        RecomputePivot(model, alignRotation, undoBehavior);
                    }
                }
                else if (groups.Count == 1) // In case the cluster only has one group, we need to check if the group contains bricks not in this cluster
                {
                    var group = groups.FirstOrDefault();
                    if (!group)
                    {
                        continue;
                    }

                    var bricksInGroup    = group.GetComponentsInChildren <Brick>(true);
                    var clustersForGroup = new HashSet <HashSet <Brick> >();

                    // If this group contains more than one cluster, split
                    foreach (var brick in bricksInGroup)
                    {
                        if (!clustersForGroup.Any(x => x.Contains(brick)))
                        {
                            if (brick.HasConnectivity())
                            {
                                var connected = brick.GetConnectedBricks();
                                connected.Add(brick);
                                clustersForGroup.Add(connected);
                            }
                        }
                    }

                    // Only split if we found multiple clusters in group
                    if (clustersForGroup.Count > 1)
                    {
                        // Get the model for the group
                        var model = GetModelInParent(group.transform);

                        // Find all prefabs we need to unpack by looking through the clusters in the group
                        foreach (var clusterInGroup in clustersForGroup)
                        {
                            // Look through each brick in the cluster
                            foreach (var brick in clusterInGroup)
                            {
                                // First check if there is a brick in this cluster that is part of a prefab and not an override
                                // If there is, then check if there is another cluster containing a prefab that is not an override
                                // In that case, we have to unpack, because we are changing the parents of gameobjects in a prefab
                                if (IsPartOfPrefab(brick))
                                {
                                    if (clustersForGroup.Any(clust => clust != clusterInGroup && clust.Any(obj => !PrefabUtility.IsAddedGameObjectOverride(obj.gameObject))))
                                    {
                                        UnpackPrefab(brick, undoBehavior);
                                        break;
                                    }
                                }
                            }
                        }

                        // Optimization: Check for largest group containing only a single cluster and keep that group intact
                        HashSet <Brick> largestGroupBricks   = null;
                        ModelGroup      largestGroup         = null;
                        int             largestGroupSize     = 0;
                        var             sharedParentClusters = new HashSet <ModelGroup>();

                        foreach (var clusterInGroup in clustersForGroup)
                        {
                            var parent     = clusterInGroup.First().transform.parent;
                            var modelGroup = parent.GetComponent <ModelGroup>();
                            if (!modelGroup)
                            {
                                continue;
                            }

                            var skip = false;
                            foreach (var brick in clusterInGroup)
                            {
                                if (brick.transform.parent != parent)
                                {
                                    skip = true;
                                    break;
                                }
                            }
                            if (skip)
                            {
                                continue;
                            }

                            if (largestGroupSize < clusterInGroup.Count())
                            {
                                largestGroupSize   = clusterInGroup.Count();
                                largestGroupBricks = clusterInGroup;
                                largestGroup       = modelGroup;
                            }
                        }

                        if (largestGroupBricks != null)
                        {
                            clustersForGroup.Remove(largestGroupBricks);
                        }

                        foreach (var clusterInGroup in clustersForGroup)
                        {
                            var newObject = new GameObject();
                            var newGroup  = newObject.AddComponent <ModelGroup>();

                            // Add LEGOModelGroupAsset component.
                            newObject.AddComponent <LEGOModelGroupAsset>();

                            if (undoBehavior == UndoBehavior.withUndo)
                            {
                                Undo.RegisterCreatedObjectUndo(newObject, "Created new group");
                            }

                            newGroup.transform.position = group.transform.position;

                            if (model)
                            {
                                SetParent(newGroup.transform, model.transform, undoBehavior);
                            }

                            newGroup.name             = group.groupName;
                            newGroup.groupName        = group.groupName;
                            newGroup.parentName       = group.parentName;
                            newGroup.optimizations    = group.optimizations;
                            newGroup.randomizeNormals = group.randomizeNormals;
                            foreach (var view in group.views)
                            {
                                newGroup.views.Add(new CullingCameraConfig()
                                {
                                    name        = view.name,
                                    perspective = view.perspective,
                                    position    = view.position,
                                    rotation    = view.rotation,
                                    fov         = view.fov,
                                    size        = view.size,
                                    minRange    = view.minRange,
                                    maxRange    = view.maxRange,
                                    aspect      = view.aspect
                                });
                            }
                            newGroup.autoGenerated = true;

                            foreach (var brick in clusterInGroup)
                            {
                                SetParent(brick.transform, newGroup.transform, undoBehavior);
                            }
                            RecomputePivot(newGroup, alignRotation, undoBehavior);
                        }

                        if (model)
                        {
                            RecomputePivot(model, alignRotation, undoBehavior);
                        }
                    }
                    else if (notInGroup.Count > 0)
                    {
                        foreach (var brick in notInGroup)
                        {
                            if (IsPartOfPrefab(brick))
                            {
                                UnpackPrefab(brick, undoBehavior);
                            }
                            SetParent(brick.transform, group.transform, undoBehavior);
                        }
                    }
                    else
                    {
                        RecomputePivot(group, alignRotation, undoBehavior);

                        var   modelGO        = group.transform.parent;
                        Model model          = null;
                        bool  createNewModel = (PrefabStageUtility.GetCurrentPrefabStage() != null && modelGO && !modelGO.GetComponent <Model>()) ||
                                               (PrefabStageUtility.GetCurrentPrefabStage() == null && (!modelGO || !modelGO.GetComponent <Model>()));
                        if (createNewModel)
                        {
                            model = CreateNewDefaultModel(group.name, undoBehavior);
                            if (modelGO != null)
                            {
                                if (PrefabStageUtility.GetCurrentPrefabStage() == null && PrefabUtility.IsPartOfAnyPrefab(modelGO))
                                {
                                    UnpackPrefab(modelGO.gameObject, undoBehavior);
                                }
                                SetParent(model.transform, modelGO.transform, undoBehavior);
                            }
                            SetParent(group.transform, model.transform, undoBehavior);
                            EditorGUIUtility.PingObject(group.gameObject);
                        }
                        else
                        {
                            model = modelGO.GetComponent <Model>();
                        }

                        if (model)
                        {
                            RecomputePivot(model, alignRotation, undoBehavior);
                        }
                    }
                }
                else // No groups.
                {
                    var   name  = cluster.FirstOrDefault()?.name;
                    Model model = null;

                    if (PrefabStageUtility.GetCurrentPrefabStage() != null)
                    {
                        var rootObject = PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot;
                        model = rootObject.GetComponent <Model>();
                        if (!model)
                        {
                            model = CreateNewDefaultModel(name);
                            SetParent(model.transform, rootObject.transform, undoBehavior);
                        }
                    }
                    else
                    {
                        model = CreateNewDefaultModel(name);
                    }

                    ModelGroup newGroup = CreateNewDefaultModelGroup(name);

                    SetParent(newGroup.transform, model.transform, undoBehavior);
                    var bounds = BrickBuildingUtility.ComputeBounds(cluster, Matrix4x4.identity);
                    model.transform.position = new Vector3(bounds.center.x, bounds.min.y, bounds.center.z);

                    Transform originalParent = null;
                    foreach (var brick in cluster)
                    {
                        if (!originalParent)
                        {
                            originalParent = brick.transform.parent;
                        }
                        if (brick.transform.parent != originalParent)
                        {
                            originalParent = null;
                            break;
                        }
                    }

                    if (originalParent)
                    {
                        SetParent(model.transform, originalParent, undoBehavior);
                    }

                    foreach (var brick in cluster)
                    {
                        SetParent(brick.transform, newGroup.transform, undoBehavior);
                        EditorGUIUtility.PingObject(brick.gameObject);
                    }
                }
            }

            var modelsInScene = StageUtility.GetCurrentStageHandle().FindComponentsOfType <Model>();

            foreach (var model in modelsInScene)
            {
                var children = model.GetComponentsInChildren <Brick>(true);
                if (children.Length == 0)
                {
                    if (undoBehavior == UndoBehavior.withUndo)
                    {
                        Undo.DestroyObjectImmediate(model.gameObject);
                    }
                    else
                    {
                        Object.DestroyImmediate(model.gameObject);
                    }
                }
            }

            var groupsInScene = StageUtility.GetCurrentStageHandle().FindComponentsOfType <ModelGroup>();

            foreach (var group in groupsInScene)
            {
                var children = group.GetComponentsInChildren <Brick>(true);
                if (children.Length == 0)
                {
                    if (undoBehavior == UndoBehavior.withUndo)
                    {
                        Undo.DestroyObjectImmediate(group.gameObject);
                    }
                    else
                    {
                        Object.DestroyImmediate(group.gameObject);
                    }
                }
            }
        }
 /// <summary>
 /// Recompute pivot for a model group according to the pivot type set on the model
 /// </summary>
 /// <param name="model">The model group to recompute pivot for</param>
 /// <param name="alignRotation">Whether or not to align the rotation according to the bricks in the model group</param>
 /// <param name="undoBehavior">Whether or not to register undo for this action</param>
 public static void RecomputePivot(ModelGroup group, bool alignRotation = true, UndoBehavior undoBehavior = UndoBehavior.withUndo)
 {
     RecomputePivot(group.transform, Model.Pivot.BottomCenter, alignRotation, undoBehavior);
 }
Beispiel #5
0
        public static void ReimportModelGroup(LXFMLDoc lxfml, ModelGroup group, ModelGroupImportSettings importSettings, bool detectConnectivity = false)
        {
            // Assign the new group import settings to the group.
            group.importSettings = importSettings;

            // We assume that the group can be found, so reimport it.
            if (group.processed)
            {
                // Remove all processed meshes.
                var renderers = group.GetComponentsInChildren <MeshRenderer>();
                foreach (var renderer in renderers)
                {
                    // FIXME Destroy the mesh? Prevents undo..
                    var filter = renderer.GetComponent <MeshFilter>();
                    //Undo.DestroyObjectImmediate(filter.sharedMesh);

                    if (renderer.GetComponent <ModelGroup>() == null)
                    {
                        // Destroy submesh game objects entirely.
                        Undo.DestroyObjectImmediate(renderer.gameObject);
                    }
                    else
                    {
                        // Destroy mesh related components on group game object.
                        Object.DestroyImmediate(filter);
                        Object.DestroyImmediate(renderer);
                    }
                }
            }

            // FIXME Check if bricks are referenced.
            // FIXME Check if bricks have custom components attached.

            // Remove group bricks.
            var existingBricks = group.GetComponentsInChildren <Brick>();

            foreach (var brick in existingBricks)
            {
                Undo.DestroyObjectImmediate(brick.gameObject);
            }

            var groupLightMapped = group.importSettings.isStatic && group.importSettings.lightmapped;

            SetStaticAndGIParams(group.gameObject, group.importSettings.isStatic, groupLightMapped);

            // Move group to origo to ensure that bricks are instantiated in the correct positions.
            var originalGroupLocalPosition = group.transform.localPosition;
            var originalGroupLocalRotation = group.transform.localRotation;
            var originalGroupLocalScale    = group.transform.localScale;
            var originalGroupParent        = group.transform.parent;
            var originalGroupSiblingIndex  = group.transform.GetSiblingIndex();

            group.transform.SetParent(null);
            group.transform.localPosition = Vector3.zero;
            group.transform.localRotation = Quaternion.identity;
            group.transform.localScale    = Vector3.one;

            // Create dictionary with just this group.
            var modelGroupImportSettingsDictionary = new DictionaryIntToModelGroupImportSettings();

            modelGroupImportSettingsDictionary.Add(group.number, group.importSettings);

            // Instantiate group bricks.
            var resultBricks = new Dictionary <int, Brick>(lxfml.bricks.Count);

            InstantiateModelBricks(lxfml, modelGroupImportSettingsDictionary, ref resultBricks, group.number);

            // Assign bricks to group.
            foreach (var brick in resultBricks.Values)
            {
                brick.transform.SetParent(group.transform);
            }
            ModelGroupUtility.RecomputePivot(group, false, ModelGroupUtility.UndoBehavior.withoutUndo);

            // Move group back to original location.
            group.transform.SetParent(originalGroupParent);
            group.transform.SetSiblingIndex(originalGroupSiblingIndex);
            group.transform.localPosition = originalGroupLocalPosition;
            group.transform.localRotation = originalGroupLocalRotation;
            group.transform.localScale    = originalGroupLocalScale;

            /*if (group.processed)
             * {
             *  // Process the group again.
             *  // FIXME Is this even a good idea?
             *  if (group.type == GroupType.Environment || group.type == GroupType.Static)
             *  {
             *      Vector2Int vertCount = Vector2Int.zero;
             *      Vector2Int triCount = Vector2Int.zero;
             *      Vector2Int meshCount = Vector2Int.zero;
             *      Vector2Int colliderCount = Vector2Int.zero;
             *      ModelProcessor.ProcessModelGroup(group, ref vertCount, ref triCount, ref meshCount, ref colliderCount);
             *
             *      Debug.Log($"Process result (before/after):\nVerts {vertCount.x}/{vertCount.y}, tris {triCount.x}/{triCount.y}, meshes {meshCount.x}/{meshCount.y}, colliders {colliderCount.x}/{colliderCount.y}");
             *  }
             * }*/

            if (detectConnectivity && group.importSettings.connectivity)
            {
                var sceneBricks = new HashSet <Brick>(StageUtility.GetCurrentStageHandle().FindComponentsOfType <Brick>());
                DetectConnectivity(new HashSet <Brick>(resultBricks.Values), sceneBricks);
            }

            EditorUtility.ClearProgressBar();
        }
        public static void ReimportModel(Model model, string newReimportPath = null)
        {
            var absoluteFilePath = model.absoluteFilePath;
            var relativeFilePath = model.relativeFilePath;

            // Update the file path
            if (newReimportPath != null)
            {
                absoluteFilePath = newReimportPath;
                relativeFilePath = PathUtils.GetRelativePath(Directory.GetCurrentDirectory(), newReimportPath);
            }

            lxfml = ReadFileLogic(relativeFilePath);
            if (lxfml == null)
            {
                lxfml = ReadFileLogic(absoluteFilePath);
            }
            if (lxfml != null)
            {
                pivot = model.pivot;

                // Store the new reimport path so it can be applied to the model if reimport is successful.
                filePath = newReimportPath;

                importSettings.Clear();

                // Make sure we are compatible with models from before ModelGroupImportSettings by adding default import settings.
                if (model.importSettings.Keys.Count == 0)
                {
                    foreach (var group in lxfml.groups)
                    {
                        importSettings.Add(group.number, new ModelGroupImportSettings());
                    }
                }
                else
                {
                    foreach (var entry in model.importSettings)
                    {
                        importSettings.Add(entry.Key, entry.Value);
                    }
                }

                foreach (var group in lxfml.groups)
                {
                    if (!importSettings.ContainsKey(group.number))
                    {
                        importSettings.Add(group.number, new ModelGroupImportSettings());
                    }
                }

                // Check if groups match up with the file.
                // FIXME Next version could include option to match groups up manually.
                // FIXME Check on group name? We do not have direct access to the groups from the model.
                var groupsMatch = true;
                foreach (var entry in importSettings)
                {
                    if (entry.Key >= lxfml.groups.Length)
                    {
                        Debug.LogWarning("Group " + entry.Key + " does not match up with file.");
                        groupsMatch = false;
                    }
                }

                if (!groupsMatch)
                {
                    EditorUtility.DisplayDialog("Reimport failed", "Model groups do not match up with groups in file. Check log for details", "Ok");
                    return;
                }

                ImportModel.model = model;
                group             = null;

                GetWindow <ImportModel>(true, "LEGO Model Importer");
            }
            else
            {
                ShowReadError();
            }
        }
Beispiel #7
0
        protected virtual void OnEnable()
        {
            modelGroup           = (ModelGroup)target;
            processedProp        = serializedObject.FindProperty("processed");
            optimizationsProp    = serializedObject.FindProperty("optimizations");
            randomizeNormalsProp = serializedObject.FindProperty("randomizeNormals");
            //imperfectionsProp = serializedObject.FindProperty("imperfections");
            viewsProp = serializedObject.FindProperty("views");

            // Collect views from other model groups.
            otherGroupViews.Clear();
            var groups = GameObject.FindObjectsOfType <ModelGroup>();

            foreach (var otherGroup in groups)
            {
                if (otherGroup != modelGroup)
                {
                    foreach (var view in otherGroup.views)
                    {
                        if (!otherGroupViews.ContainsKey(otherGroup))
                        {
                            otherGroupViews.Add(otherGroup, new List <CullingCameraConfig>());
                        }
                        otherGroupViews[otherGroup].Add(view);
                    }
                }
            }
            // Collect views from light sources.
            lightViews.Clear();
            var    lights                 = GameObject.FindObjectsOfType <Light>();
            Bounds groupBounds            = new Bounds();
            bool   hasComputedGroupBounds = false;

            foreach (var light in lights)
            {
                switch (light.type)
                {
                case LightType.Spot:
                {
                    CullingCameraConfig view = new CullingCameraConfig()
                    {
                        name        = light.name,
                        perspective = true,
                        position    = light.transform.position,
                        rotation    = light.transform.rotation,
                        fov         = light.spotAngle,
                        minRange    = light.shadowNearPlane,
                        maxRange    = light.range,
                        aspect      = 1.0f
                    };
                    lightViews.Add(light, view);
                    break;
                }

                case LightType.Directional:
                {
                    // Need to compute group bounds.
                    if (!hasComputedGroupBounds)
                    {
                        var renderers = modelGroup.transform.GetComponentsInChildren <MeshRenderer>();
                        if (renderers.Length > 0)
                        {
                            groupBounds = new Bounds(renderers[0].bounds.center, renderers[0].bounds.size);

                            foreach (var renderer in renderers)
                            {
                                groupBounds.Encapsulate(renderer.bounds);
                            }
                        }

                        hasComputedGroupBounds = true;
                    }

                    // Find bounds' corners in light space.
                    var corners = new List <Vector3>()
                    {
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, -1, -1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, -1, 1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, 1, -1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(-1, 1, 1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, -1, -1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, -1, 1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, 1, -1), groupBounds.extents)),
                        light.transform.InverseTransformPoint(groupBounds.center + Vector3.Scale(new Vector3(1, 1, 1), groupBounds.extents))
                    };

                    // Find min and max range for corners.
                    var min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
                    var max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
                    foreach (var corner in corners)
                    {
                        min = Vector3.Min(min, corner);
                        max = Vector3.Max(max, corner);
                    }

                    // Get size.
                    Vector2 size = new Vector2(Mathf.Max(Mathf.Abs(min.x), Mathf.Abs(max.x)), Mathf.Max(Mathf.Abs(min.y), Mathf.Abs(max.y)));

                    CullingCameraConfig view = new CullingCameraConfig()
                    {
                        name        = light.name,
                        perspective = false,
                        position    = light.transform.position,
                        rotation    = light.transform.rotation,
                        size        = size.y,
                        minRange    = min.z,
                        maxRange    = max.z,
                        aspect      = size.x / size.y
                    };
                    lightViews.Add(light, view);
                    break;
                }
                }
            }
            // Collect view from cameras.
            cameraViews.Clear();
            var cameras = GameObject.FindObjectsOfType <Camera>();

            foreach (var camera in cameras)
            {
                CullingCameraConfig view = new CullingCameraConfig()
                {
                    name        = camera.name,
                    perspective = !camera.orthographic,
                    position    = camera.transform.position,
                    rotation    = camera.transform.rotation,
                    size        = camera.orthographicSize,
                    fov         = camera.fieldOfView,
                    aspect      = camera.aspect,
                    minRange    = camera.nearClipPlane,
                    maxRange    = camera.farClipPlane
                };
                cameraViews.Add(camera, view);
            }
        }
        public static void ReimportModelGroup(LXFMLDoc lxfml, ModelGroup group, ModelGroupImportSettings importSettings, bool detectConnectivity = false)
        {
            // Assign the new group import settings to the group.
            group.importSettings = importSettings;

            // We assume that the group can be found, so reimport it.
            if (group.processed)
            {
                // Remove all processed meshes.
                var renderers = group.GetComponentsInChildren <MeshRenderer>();
                foreach (var renderer in renderers)
                {
                    // FIXME Destroy the mesh? Prevents undo..
                    var filter = renderer.GetComponent <MeshFilter>();
                    //Undo.DestroyObjectImmediate(filter.sharedMesh);

                    if (renderer.GetComponent <ModelGroup>() == null)
                    {
                        // Destroy submesh game objects entirely.
                        Undo.DestroyObjectImmediate(renderer.gameObject);
                    }
                    else
                    {
                        // Destroy mesh related components on group game object.
                        Object.DestroyImmediate(filter);
                        Object.DestroyImmediate(renderer);
                    }
                }
            }

            // FIXME Check if bricks are referenced.
            // FIXME Check if bricks have custom components attached.

            // Remove group bricks.
            var existingBricks = group.GetComponentsInChildren <Brick>();

            foreach (var brick in existingBricks)
            {
                Undo.DestroyObjectImmediate(brick.gameObject);
            }

            var groupLightMapped = group.importSettings.isStatic && group.importSettings.lightmapped;

            SetStaticAndGIParams(group.gameObject, group.importSettings.isStatic, groupLightMapped);

            // Move group to origo to ensure that bricks are instantiated in the correct positions.
            var originalGroupParent       = group.transform.parent;
            var originalGroupSiblingIndex = group.transform.GetSiblingIndex();

            group.transform.parent        = null;
            group.transform.localPosition = Vector3.zero;
            group.transform.localRotation = Quaternion.identity;
            group.transform.localScale    = Vector3.one;

            // Create dictionary with just this group.
            var modelGroupImportSettingsDictionary = new DictionaryIntToModelGroupImportSettings();

            modelGroupImportSettingsDictionary.Add(group.number, group.importSettings);

            // Instantiate group bricks.
            var resultBricks = new Dictionary <int, Brick>(lxfml.bricks.Count);

            InstantiateModelBricks(lxfml, modelGroupImportSettingsDictionary, ref resultBricks, group.number);

            // Assign bricks to group.
            foreach (var brick in resultBricks.Values)
            {
                brick.transform.SetParent(group.transform);
            }

            // Set parent of group back to original.
            group.transform.parent = originalGroupParent;
            group.transform.SetSiblingIndex(originalGroupSiblingIndex);

            if (detectConnectivity && group.importSettings.connectivity)
            {
                var sceneBricks = new HashSet <Brick>(StageUtility.GetCurrentStageHandle().FindComponentsOfType <Brick>());
                DetectConnectivity(new HashSet <Brick>(resultBricks.Values), sceneBricks);
            }

            EditorUtility.ClearProgressBar();
        }