colour wooden cube

Building a Material Cube Farm

How to guides By Aug 21, 2025 No Comments

When you’ve got 200+ materials in your model and someone in the viz team asks for a preview, the last thing you want to do is manually place and assign materials one by one to a cube. But that’s exactly what we were staring down on a recent project — a clean export of every used material in the Revit model for the visualisation team, so they could prep their Datasmith material mapping ahead of time.

So, we automated it.

We needed a Revit project with:

  • One cube per material
  • The material applied via Object Styles using a subcategory
  • A clean, spatially separated grid of cubes ready for export to Datasmith

This is useful because instead of visualising materials in context (which can be too late in the process), this approach allows the visualisation team to:

  • Spot duplicated or near-identical materials early
  • Consolidate materials before they become an issue
  • Build material mapping tables ahead of time

The Macro

The macro does a full sweep of the model:

  • Finds all materials currently in use
  • For each material:
    • Generates a safe name
    • Opens a base cube family (you’ll need to preload this)
    • Creates or reuses a subcategory
    • Applies the material to that subcategory
    • Saves the family with a new name
    • Loads it back into the model
    • Places the cube at 1.5 m intervals along the X-axis
    • Assigns the original material to the subcategory in Object Styles
public void SubCatter()
{
    // Get the UI document and model Document
    UIDocument uidoc = this.ActiveUIDocument;
    Document doc = uidoc.Document;

    // Prep for the family
    Family family = null;

    // Find unused elements (includes materials)
    ISet<ElementId> unusedIds = doc.GetUnusedElements(new HashSet<ElementId>());

    // Get all the materials in the document
    ICollection<ElementId> materialIds = new FilteredElementCollector(doc)
        .OfClass(typeof(Material))
        .ToElementIds();
    if (materialIds.Count == 0)
    {
        TaskDialog.Show("Error", "No materials found in the document.");
        return;
    }

    int row = 0;
    double yStep = UnitUtils.ConvertToInternalUnits(1500, UnitTypeId.Millimeters);

    // For each material, make a new family called Cube with the material name as a suffix
    // and assign the material to the family
    // This will create a family for each material in the document
    foreach (ElementId materialId in materialIds)
    {
        Material mat = doc.GetElement(materialId) as Material;

        if (mat == null) continue;

        // Skip if the material has not been assigned to any geometry in the project
        if (unusedIds.Contains(materialId))
        {
            continue; // Skip unused materials
        }

        // Build a safe name so the new family can be saved to a file
        string safeName = Regex.Replace(mat.Name, @"[\\/:*?""<>|]", "_");
        string subcatName = safeName;
        string familyName = "Cube_" + safeName;
        string rootFolder = @"C:\temp\"; // Replace with your folder location
        string projFolder = Path.Combine(rootFolder, Path.GetFileNameWithoutExtension(doc.Title));
        if (!Directory.Exists(projFolder))
            Directory.CreateDirectory(projFolder);

        string savePath = Path.Combine(projFolder, familyName + ".rfa");

        // Find the base family
        Family baseFam = new FilteredElementCollector(doc)
            .OfClass(typeof(Family))
            .Cast<Family>()
            .FirstOrDefault(f => f.Name == "Cube");

        if (baseFam == null) { /* error */ break; }

        // Open and edit
        Document famDoc = doc.EditFamily(baseFam);

        if (famDoc == null)
        {
            TaskDialog.Show("Error", "Failed to open family for editing.");
            return;
        }

        // Create a subcategory inside the family document
        using (Transaction t = new Transaction(famDoc, "Assign Subcategory"))
        {
            t.Start();

            // Get Generic Models category (the cube is a generic model)
            Category genericModelCat = famDoc.Settings.Categories.get_Item(BuiltInCategory.OST_GenericModel);

            // Create subcategory (skip if exists)
            Category newSubcat = null;
            try
            {
                newSubcat = famDoc.Settings.Categories.NewSubcategory(genericModelCat, subcatName);
                // Customize properties (optional)
                newSubcat.LineColor = new Color(0, 255, 0); // Green lines. You don't need to do this, I was using this as testing
            }
            catch (Autodesk.Revit.Exceptions.ArgumentException)
            {
                // Subcategory exists, retrieve it
                newSubcat = genericModelCat.SubCategories.Cast<Category>()
                    .First(sc => sc.Name == subcatName);
            }

            // Assign to ALL geometry in the family
            FilteredElementCollector collector = new FilteredElementCollector(famDoc);
            var geometricElements = collector.OfClass(typeof(GenericForm)) // Extrusions, blends, sweeps
                    .Cast<GenericForm>();

            foreach (GenericForm geom in geometricElements)
            {
                geom.Subcategory = newSubcat; // Apply subcategory
            }

            t.Commit();
        }

        // Save the family document to the path C:\temp\ with the subcategory name appended as a suffix
        try
        {
            famDoc.SaveAs(savePath);
            // TaskDialog.Show("Success", "Family saved successfully to: " + savePath); // Uncomment this line if you want a dialogue to confirm. You'll need to click for every material though!
        }
        catch (Exception ex)
        {
            // TaskDialog.Show("Error", "Failed to save family: " + ex.Message);
        }

        // Close the family document
        if (famDoc.IsModified)
        {
            famDoc.Save();
        }
        famDoc.Close(false);

        // Load the family into the project
        using (Transaction tx = new Transaction(doc, "Load Family"))
        {
            tx.Start();

            Family loadedFamily;
            bool wasLoaded = doc.LoadFamily(savePath, out loadedFamily);

            // Uncomment this line if you want a dialogue to confirm. You'll need to click for every material though!
            /*
            if (wasLoaded)
                TaskDialog.Show("Success", "Family '" + loadedFamily.Name + "' loaded into project.");
            else
                TaskDialog.Show("Error", "Family failed to load or was already present.");
            */

            // Place the family instance at 0,0,0 in the project, then step 1500mm for each instance after
            if (loadedFamily != null)
            {
                FamilySymbol symbol = loadedFamily.GetFamilySymbolIds()
                    .Select(id => doc.GetElement(id) as FamilySymbol)
                    .FirstOrDefault();
                if (symbol != null && !symbol.IsActive)
                {
                    symbol.Activate();
                }
                XYZ location = new XYZ(row * yStep, 0, 0);
                FamilyInstance instance = doc.Create.NewFamilyInstance(location, symbol, Autodesk.Revit.DB.Structure.StructuralType.NonStructural);
                // TaskDialog.Show("Success", "Family instance '" + instance.Name + "' created at {location}."); // Uncomment this line if you want a dialogue to confirm. You'll need to click for every material though!

                row++;

                // And then set the material on the subcategory
                // Get the Generic Models root category
                Category genericCat = doc.Settings.Categories
                        .get_Item(BuiltInCategory.OST_GenericModel);

                // Find subcategory by name
                Category mySubcat = genericCat.SubCategories
                        .Cast<Category>()
                        .FirstOrDefault(c => c.Name == subcatName);
                // subcatName is the same safeName you used when creating/saving the family

                // Here mat.Name is the original project material looped on

                if (mySubcat != null && mat != null)
                {
                    mySubcat.Material = mat;
                }
            }

            tx.Commit();
        }
    }

    // Refresh the UI document to reflect changes
    uidoc.RefreshActiveView();
}Code language: C# (cs)

No manual fiddling. Just cubes, colour-coded and ready for export.

Requirements:

  • A preloaded base cube family named Cube (rename as needed)
  • A working folder path (update the script to suit your system)
  • Materials must be assigned to geometry to be included

Even though these cubes aren’t in context, they provide a fast, deterministic way to review what’s going into Datasmith. Better yet, and it’s scalable, it doesn’t care if you’ve got 20 materials or 300.

You could tweak this to work with nested families, assign metadata, or even generate CSVs for review. But for us, the simple cube farm was enough to speed up the process and remove a ton of manual effort.

Let me know if this is something you’d use. Always happy to share the macro if anyone wants to take it for a spin.

No Comments

Leave a comment

Your email address will not be published. Required fields are marked *