Skip to content

Commit

Permalink
feat(rhino): DUI3-142 adds render materials to objects on send and re…
Browse files Browse the repository at this point in the history
…ceive (#63)

* adds rhino material table to send commit

* adds render materials on send

* adds render materials on receive

* Update RhinoHostObjectBuilder.cs

* fixes dictionary trygetvalue errors

* Update RhinoMaterialManager.cs
  • Loading branch information
clairekuang authored Jul 23, 2024
1 parent f4e7214 commit b07a61a
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,6 @@ public void Load(SpeckleContainerBuilder builder)
builder.AddScoped<RhinoInstanceObjectsManager>();
builder.AddScoped<RhinoGroupManager>();
builder.AddScoped<RhinoLayerManager>();
builder.AddScoped<RhinoMaterialManager>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using Rhino;
using Rhino.Render;
using Speckle.Connectors.Utils.Conversion;
using Speckle.Core.Kits;
using Speckle.Core.Logging;
using BakeResult = Speckle.Connectors.Utils.RenderMaterials.BakeResult;
using Material = Rhino.DocObjects.Material;
using RenderMaterial = Rhino.Render.RenderMaterial;
using SpeckleRenderMaterial = Objects.Other.RenderMaterial;

namespace Speckle.Connectors.Rhino7.HostApp;

/// <summary>
/// Utility class managing layer creation and/or extraction from rhino. Expects to be a scoped dependency per send or receive operation.
/// </summary>
public class RhinoMaterialManager
{
/// <summary>
/// A dictionary of (material index, material guid)
/// </summary>
private readonly Dictionary<string, SpeckleRenderMaterial> _renderMaterialCache = new();

/// <summary>
/// Creates a Speckle Render Material from the provided Rhino material
/// </summary>
/// <param name="material"></param>
/// <returns>The existing Speckle Render Material if this material has been previously created, or the newly created Speckle Render Material</returns>
public SpeckleRenderMaterial CreateSpeckleRenderMaterial(Material material)
{
string materialId = material.Id.ToString();
if (_renderMaterialCache.TryGetValue(materialId, out SpeckleRenderMaterial existingMaterial))
{
return existingMaterial;
}

// get physically based render material
Material pbMaterial = material;
if (!material.IsPhysicallyBased)
{
pbMaterial = new();
pbMaterial.CopyFrom(material);
pbMaterial.ToPhysicallyBased();
}

using RenderMaterial rm = RenderMaterial.FromMaterial(pbMaterial, null);
Rhino.DocObjects.PhysicallyBasedMaterial pbRenderMaterial = rm.ConvertToPhysicallyBased(
RenderTexture.TextureGeneration.Allow
);

string renderMaterialName = material.Name ?? "default"; // default rhino material has no name
System.Drawing.Color diffuse = pbRenderMaterial.BaseColor.AsSystemColor();
System.Drawing.Color emissive = pbRenderMaterial.Emission.AsSystemColor();
double opacity = pbRenderMaterial.Opacity;

SpeckleRenderMaterial speckleRenderMaterial =
new(pbRenderMaterial.Opacity, pbRenderMaterial.Metallic, pbRenderMaterial.Roughness, diffuse, emissive)
{
name = renderMaterialName,
applicationId = materialId
};

return speckleRenderMaterial;
}

/// <summary>
/// Determines if a Speckle Render Material has already been created from the input Rhino material
/// </summary>
/// <param name="material"></param>
/// <returns>True if yes, False if no Speckle Render material has been created from the input material</returns>
public bool Contains(Material material)
{
return _renderMaterialCache.TryGetValue(material.Id.ToString(), out SpeckleRenderMaterial _);
}

public BakeResult BakeMaterials(
List<SpeckleRenderMaterial> speckleRenderMaterials,
string baseLayerName,
Action<string, double?>? onOperationProgressed
)
{
var doc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around

// Keeps track of the incoming SpeckleRenderMaterial application Id and the index of the corresponding Rhino Material in the doc material table
Dictionary<string, int> materialIdAndIndexMap = new();

int count = 0;
List<ReceiveConversionResult> conversionResults = new();
foreach (SpeckleRenderMaterial speckleRenderMaterial in speckleRenderMaterials)
{
onOperationProgressed?.Invoke("Converting render materials", (double)++count / speckleRenderMaterials.Count);
try
{
// POC: Currently we're relying on the render material name for identification if it's coming from speckle and from which model; could we do something else?
string matName = $"{speckleRenderMaterial.name}-({speckleRenderMaterial.applicationId})-{baseLayerName}";
Color diffuse = Color.FromArgb(speckleRenderMaterial.diffuse);
Color emissive = Color.FromArgb(speckleRenderMaterial.emissive);
double transparency = 1 - speckleRenderMaterial.opacity;

Material rhinoMaterial =
new()
{
Name = matName,
DiffuseColor = diffuse,
EmissionColor = emissive,
Transparency = transparency
};

int matIndex = doc.Materials.Add(rhinoMaterial);

// POC: check on matIndex -1, means we haven't created anything - this is most likely an recoverable error at this stage
if (matIndex == -1)
{
throw new ConversionException("Failed to add a material to the document.");
}

if (speckleRenderMaterial.applicationId != null)
{
materialIdAndIndexMap[speckleRenderMaterial.applicationId] = matIndex;
}

conversionResults.Add(new(Status.SUCCESS, speckleRenderMaterial, matName, "Material"));
}
catch (Exception ex) when (!ex.IsFatal())
{
conversionResults.Add(new(Status.ERROR, speckleRenderMaterial, null, null, ex));
}
}

return new(materialIdAndIndexMap, conversionResults);
}

/// <summary>
/// Removes all materials with a name starting with <paramref name="namePrefix"/> from the active document
/// </summary>
/// <param name="namePrefix"></param>
public void PurgeMaterials(string namePrefix)
{
var currentDoc = RhinoDoc.ActiveDoc; // POC: too much right now to interface around
foreach (Material material in currentDoc.Materials)
{
if (!material.IsDeleted && material.Name.Contains(namePrefix))
{
currentDoc.Materials.Delete(material);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Speckle.Core.Models.Collections;
using Speckle.Core.Models.GraphTraversal;
using Speckle.Core.Models.Instances;
using SpeckleRenderMaterial = Objects.Other.RenderMaterial;

namespace Speckle.Connectors.Rhino7.Operations.Receive;

Expand All @@ -21,23 +22,25 @@ public class RhinoHostObjectBuilder : IHostObjectBuilder
private readonly IRootToHostConverter _converter;
private readonly IConversionContextStack<RhinoDoc, UnitSystem> _contextStack;
private readonly GraphTraversal _traverseFunction;

private readonly RhinoInstanceObjectsManager _instanceObjectsManager;
private readonly RhinoLayerManager _layerManager;
private readonly RhinoMaterialManager _materialManager;

public RhinoHostObjectBuilder(
IRootToHostConverter converter,
IConversionContextStack<RhinoDoc, UnitSystem> contextStack,
GraphTraversal traverseFunction,
RhinoLayerManager layerManager,
RhinoInstanceObjectsManager instanceObjectsManager
RhinoInstanceObjectsManager instanceObjectsManager,
RhinoMaterialManager materialManager
)
{
_converter = converter;
_contextStack = contextStack;
_traverseFunction = traverseFunction;
_layerManager = layerManager;
_instanceObjectsManager = instanceObjectsManager;
_materialManager = materialManager;
}

public HostObjectBuilderResult Build(
Expand All @@ -61,10 +64,15 @@ CancellationToken cancellationToken

var groupProxies = (rootObject["groupProxies"] as List<object>)?.Cast<GroupProxy>().ToList();

List<SpeckleRenderMaterial>? renderMaterials = (rootObject["renderMaterials"] as List<object>)
?.Cast<SpeckleRenderMaterial>()
.ToList();

var conversionResults = BakeObjects(
objectsToConvert,
instanceDefinitionProxies,
groupProxies,
renderMaterials,
baseLayerName,
onOperationProgressed
);
Expand All @@ -78,24 +86,22 @@ private HostObjectBuilderResult BakeObjects(
IEnumerable<TraversalContext> objectsGraph,
List<InstanceDefinitionProxy>? instanceDefinitionProxies,
List<GroupProxy>? groupProxies,
List<SpeckleRenderMaterial>? renderMaterials,
string baseLayerName,
Action<string, double?>? onOperationProgressed
)
{
RhinoDoc doc = _contextStack.Current.Document;
var rootLayerIndex = _contextStack.Current.Document.Layers.Find(Guid.Empty, baseLayerName, RhinoMath.UnsetIntIndex);

// Remove all previously received layers and render materials from the document
int rootLayerIndex = _contextStack.Current.Document.Layers.Find(Guid.Empty, baseLayerName, RhinoMath.UnsetIntIndex);
PreReceiveDeepClean(baseLayerName, rootLayerIndex);
_layerManager.CreateBaseLayer(baseLayerName);

_layerManager.CreateBaseLayer(baseLayerName);
using var noDraw = new DisableRedrawScope(doc.Views);

var conversionResults = new List<ReceiveConversionResult>();
var bakedObjectIds = new List<string>();

var instanceComponents = new List<(Collection[] collectionPath, IInstanceComponent obj)>();

// POC: these are not captured by traversal, so we need to re-add them here
var instanceComponents = new List<(Collection[] collectionPath, IInstanceComponent obj)>();
if (instanceDefinitionProxies != null && instanceDefinitionProxies.Count > 0)
{
var transformed = instanceDefinitionProxies.Select(proxy =>
Expand All @@ -104,7 +110,7 @@ private HostObjectBuilderResult BakeObjects(
instanceComponents.AddRange(transformed);
}

var atomicObjects = new List<(Collection[] collectionPath, Base obj)>();
List<(Collection[] collectionPath, Base obj)> atomicObjects = new();

// Split up the instances from the non-instances
foreach (TraversalContext tc in objectsGraph)
Expand All @@ -121,18 +127,39 @@ private HostObjectBuilderResult BakeObjects(
}
}

List<ReceiveConversionResult> conversionResults = new();

// Stage 0: Convert render materials
Dictionary<string, int> materialsIdMap = new();
if (renderMaterials != null && renderMaterials.Count > 0)
{
(materialsIdMap, List<ReceiveConversionResult> materialsConversionResults) = _materialManager.BakeMaterials(
renderMaterials,
baseLayerName,
onOperationProgressed
);
conversionResults.AddRange(materialsConversionResults); // add instance conversion results to our list
}

// Stage 1: Convert atomic objects
// Note: this can become encapsulated later in an "atomic object baker" of sorts, if needed.
var bakedObjectIds = new List<string>();
var applicationIdMap = new Dictionary<string, List<string>>(); // used in converting blocks in stage 2. keeps track of original app id => resulting new app ids post baking
var count = 0;
foreach (var (path, obj) in atomicObjects)
{
onOperationProgressed?.Invoke("Converting objects", (double)++count / atomicObjects.Count);
try
{
var layerIndex = _layerManager.GetAndCreateLayerFromPath(path, baseLayerName);
int layerIndex = _layerManager.GetAndCreateLayerFromPath(path, baseLayerName);
// TODO: need to add render materials to layers
int materialIndex = obj["renderMaterialId"] is string renderMaterialId
? materialsIdMap.TryGetValue(renderMaterialId, out materialIndex)
? materialIndex
: 0
: 0;
var result = _converter.Convert(obj);
var conversionIds = HandleConversionResult(result, obj, layerIndex).ToList();
var conversionIds = HandleConversionResult(result, obj, layerIndex, materialIndex).ToList();
foreach (var r in conversionIds)
{
conversionResults.Add(new(Status.SUCCESS, obj, r, result.GetType().ToString()));
Expand Down Expand Up @@ -182,6 +209,7 @@ private HostObjectBuilderResult BakeObjects(
private void PreReceiveDeepClean(string baseLayerName, int rootLayerIndex)
{
_instanceObjectsManager.PurgeInstances(baseLayerName);
_materialManager.PurgeMaterials(baseLayerName);

var doc = _contextStack.Current.Document;
// Cleans up any previously received objects
Expand All @@ -204,21 +232,35 @@ private void PreReceiveDeepClean(string baseLayerName, int rootLayerIndex)
}
}

private IReadOnlyList<string> HandleConversionResult(object conversionResult, Base originalObject, int layerIndex)
private IReadOnlyList<string> HandleConversionResult(
object conversionResult,
Base originalObject,
int layerIndex,
int materialIndex
)
{
var doc = _contextStack.Current.Document;
List<string> newObjectIds = new();
switch (conversionResult)
{
case IEnumerable<GeometryBase> list:
{
Group group = BakeObjectsAsGroup(originalObject.id, list, layerIndex);
Group group = BakeObjectsAsGroup(originalObject.id, list, layerIndex, materialIndex);
newObjectIds.Add(group.Id.ToString());
break;
}
case GeometryBase newObject:
{
var newObjectGuid = doc.Objects.Add(newObject, new ObjectAttributes { LayerIndex = layerIndex });
Guid newObjectGuid = doc.Objects.Add(
newObject,
new ObjectAttributes
{
LayerIndex = layerIndex,
MaterialIndex = materialIndex,
MaterialSource =
materialIndex == 0 ? ObjectMaterialSource.MaterialFromLayer : ObjectMaterialSource.MaterialFromObject
}
);
newObjectIds.Add(newObjectGuid.ToString());
break;
}
Expand All @@ -231,10 +273,21 @@ private IReadOnlyList<string> HandleConversionResult(object conversionResult, Ba
return newObjectIds;
}

private Group BakeObjectsAsGroup(string groupName, IEnumerable<GeometryBase> list, int layerIndex)
private Group BakeObjectsAsGroup(string groupName, IEnumerable<GeometryBase> list, int layerIndex, int materialIndex)
{
var doc = _contextStack.Current.Document;
var objectIds = list.Select(obj => doc.Objects.Add(obj, new ObjectAttributes { LayerIndex = layerIndex }));
var objectIds = list.Select(obj =>
doc.Objects.Add(
obj,
new ObjectAttributes
{
LayerIndex = layerIndex,
MaterialIndex = materialIndex,
MaterialSource =
materialIndex == 0 ? ObjectMaterialSource.MaterialFromLayer : ObjectMaterialSource.MaterialFromObject
}
)
);
var groupIndex = _contextStack.Current.Document.Groups.Add(groupName, objectIds);
var group = _contextStack.Current.Document.Groups.FindIndex(groupIndex);
return group;
Expand Down
Loading

0 comments on commit b07a61a

Please sign in to comment.