Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(revit): CNX-470 add support for layer render materials #243

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
using Autodesk.Revit.DB;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Utils.Operations.Receive;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Objects.Other;
using Speckle.Sdk;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Extensions;
using Speckle.Sdk.Models.GraphTraversal;

namespace Speckle.Connectors.Revit.HostApp;

/// <summary>
/// Utility class that converts and bakes materials in Revit. Expects to be a scoped dependency per unit of work.
/// </summary>
public class RevitMaterialBaker
{
private readonly IRevitConversionContextStack _contextStack;
Expand All @@ -23,10 +30,88 @@ RevitUtils revitUtils
_revitUtils = revitUtils;
}

public Dictionary<string, string> ObjectIdAndMaterialIndexMap { get; } = new();
/// <summary>
/// Checks the every atomic object has render material or not, if not it tries to find it from its layer tree and mutates
/// its render material proxy objects list with the traversal current. It will also map displayable objects' display values to their
/// respective material proxy.
/// </summary>
public void MapLayersRenderMaterials(RootObjectUnpackerResult unpackedRoot)
{
if (unpackedRoot.RenderMaterialProxies is null)
{
return;
}

foreach (var context in unpackedRoot.ObjectsToConvert)
{
if (context.Current.applicationId is null)
{
continue;
}

var targetRenderMaterialProxy = unpackedRoot.RenderMaterialProxies.FirstOrDefault(rmp =>
rmp.objects.Contains(context.Current.applicationId)
);

if (targetRenderMaterialProxy is null)
{
var layerParents = context.GetAscendants().Where(parent => parent is Layer);

var layer = layerParents.FirstOrDefault(layer =>
unpackedRoot.RenderMaterialProxies.Any(rmp => rmp.objects.Contains(layer.applicationId!))
);

if (layer is not null)
{
var layerRenderMaterialProxy = unpackedRoot.RenderMaterialProxies.First(rmp =>
rmp.objects.Contains(layer.applicationId!)
);

targetRenderMaterialProxy = layerRenderMaterialProxy;
}
}

if (targetRenderMaterialProxy is null)
{
continue; // exit fast, no proxy, we can't do much more.
}
// We mutate the existing proxy list that comes from source application. Because we do not keep track of parent-child relationship of objects in terms of render materials.
targetRenderMaterialProxy.objects.Add(context.Current.applicationId!);

// This is somewhat evil: we're unpacking here displayable elements by adding their display value to the target render material proxy.
// If the display value items do not have an application id, we will generate one.
var displayable = context.Current.TryGetDisplayValue();
if (displayable != null)
{
foreach (var @base in displayable)
{
if (@base.applicationId == null)
{
var guid = Guid.NewGuid().ToString();
@base.applicationId = guid;
targetRenderMaterialProxy.objects.Add(guid);
}
else
{
targetRenderMaterialProxy.objects.Add(@base.applicationId);
}
}
}
}
}

public void BakeMaterials(List<RenderMaterialProxy> speckleRenderMaterialProxies, string baseLayerName)
/// <summary>
/// Will bake render materials in the revit document.
/// </summary>
/// <param name="speckleRenderMaterialProxies"></param>
/// <param name="baseLayerName"></param>
/// <returns></returns>
public Dictionary<string, ElementId> BakeMaterials(
List<RenderMaterialProxy> speckleRenderMaterialProxies,
string baseLayerName
)
{
Dictionary<string, ElementId> objectIdAndMaterialIndexMap = new();
foreach (var proxy in speckleRenderMaterialProxies)
{
var speckleRenderMaterial = proxy.value;
Expand All @@ -49,13 +134,15 @@ public void BakeMaterials(List<RenderMaterialProxy> speckleRenderMaterialProxies

foreach (var objectId in proxy.objects)
{
ObjectIdAndMaterialIndexMap[objectId] = revitMaterial.Id.ToString();
objectIdAndMaterialIndexMap[objectId] = revitMaterial.Id;
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create material in Revit");
}
}

return objectIdAndMaterialIndexMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,10 @@
using Speckle.Sdk;
using Speckle.Sdk.Logging;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.GraphTraversal;

namespace Speckle.Connectors.Revit.Operations.Receive;

/// <summary>
/// Potentially consolidate all application specific IHostObjectBuilders
/// https://spockle.atlassian.net/browse/DUI3-465
/// </summary>
internal sealed class RevitHostObjectBuilder : IHostObjectBuilder, IDisposable
{
private readonly IRootToHostConverter _converter;
Expand Down Expand Up @@ -77,29 +72,23 @@ CancellationToken cancellationToken
var unpackedRoot = _rootObjectUnpacker.Unpack(rootObject);

using var activity = SpeckleActivityFactory.Start("Build");
IEnumerable<TraversalContext> objectsToConvert;

using (var _ = SpeckleActivityFactory.Start("Traverse"))
{
objectsToConvert = _traverseFunction.Traverse(rootObject).Where(obj => obj.Current is not Collection);
}

var elementIds = new List<ElementId>();

using TransactionGroup transactionGroup = new(_contextStack.Current.Document, $"Received data from {projectName}");
transactionGroup.Start();
_transactionManager.StartTransaction();

if (unpackedRoot.RenderMaterialProxies != null)
{
_materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies, baseLayerName);
foreach (var item in _materialBaker.ObjectIdAndMaterialIndexMap)
_materialBaker.MapLayersRenderMaterials(unpackedRoot);
// NOTE: do not set _contextStack.RenderMaterialProxyCache directly, things stop working. Ogu/Dim do not know why :) not a problem as we hopefully will refactor some of these hacks out.
var map = _materialBaker.BakeMaterials(unpackedRoot.RenderMaterialProxies, baseLayerName);
foreach (var kvp in map)
{
_contextStack.RenderMaterialProxyCache.ObjectIdAndMaterialIndexMap.Add(item.Key, item.Value); // Massive hack!
_contextStack.RenderMaterialProxyCache.ObjectIdAndMaterialIndexMap.Add(kvp.Key, kvp.Value);
}
}

var conversionResults = BakeObjects(objectsToConvert, onOperationProgressed, cancellationToken, out elementIds);
var conversionResults = BakeObjects(unpackedRoot.ObjectsToConvert, onOperationProgressed, cancellationToken);

using (var _ = SpeckleActivityFactory.Start("Commit"))
{
Expand Down Expand Up @@ -135,15 +124,13 @@ CancellationToken cancellationToken
private HostObjectBuilderResult BakeObjects(
IEnumerable<TraversalContext> objectsGraph,
Action<string, double?>? onOperationProgressed,
CancellationToken cancellationToken,
out List<ElementId> elemIds
CancellationToken cancellationToken
)
{
using (var _ = SpeckleActivityFactory.Start("BakeObjects"))
{
var conversionResults = new List<ReceiveConversionResult>();
var bakedObjectIds = new List<string>();
var elementIds = new List<ElementId>();

// is this a dumb idea?
var objectList = objectsGraph.ToList();
Expand Down Expand Up @@ -175,8 +162,6 @@ out List<ElementId> elemIds
conversionResults.Add(new(Status.ERROR, tc.Current, null, null, ex));
}
}

elemIds = elementIds;
return new(bakedObjectIds, conversionResults);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ public class RevitMaterialCacheSingleton
public Dictionary<string, Dictionary<string, RenderMaterialProxy>> ObjectRenderMaterialProxiesMap { get; } = new();

/// <summary>
/// // POC: The map we mutate PER RECEIVE operation, this smells a LOT! Once we have better conversion context stack that we can manage our data between connector - converter, this property must go away!
/// POC: The map we mutate PER RECEIVE operation, this smells a LOT! Once we have better conversion context stack that we can manage our data between connector - converter, this property must go away!
/// </summary>
public Dictionary<string, string> ObjectIdAndMaterialIndexMap { get; } = new();
public Dictionary<string, DB.ElementId> ObjectIdAndMaterialIndexMap { get; } = new();

/// <summary>
/// map (DB.Material id, RevitMaterial). This can be generated from converting render materials or material quantities.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ out var mappedElementId
)
)
{
materialId = ElementId.Parse(mappedElementId);
materialId = mappedElementId;
}

int i = 0;
Expand Down
Loading