fix(autocad): CNX-318 better color inheritance for blocks (#167)

* updates definition object colors and refactors speckle application id retrieval

* adds definition object color inehritance and speckle app id extensions

* minor build fixes

* adds source to color proxy and color proxy id

* Update AutocadColorManager.cs

* Update AutocadColorManager.cs
This commit is contained in:
Claire Kuang
2024-08-20 13:39:21 +02:00
committed by GitHub
parent dbc359f0e4
commit 8a0dfca204
13 changed files with 210 additions and 128 deletions
@@ -33,9 +33,9 @@ public class AutocadBasicConnectorBinding : IBasicConnectorBinding
public string GetConnectorVersion() => typeof(AutocadBasicConnectorBinding).Assembly.GetVersion();
public string GetSourceApplicationName() => Speckle.Connectors.Utils.Connector.Slug;
public string GetSourceApplicationName() => Utils.Connector.Slug;
public string GetSourceApplicationVersion() => Speckle.Connectors.Utils.Connector.VersionString;
public string GetSourceApplicationVersion() => Utils.Connector.VersionString;
public Account[] GetAccounts() => AccountManager.GetAccounts().ToArray();
@@ -1,5 +1,6 @@
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
@@ -79,9 +80,8 @@ public class AutocadSelectionBinding : ISelectionBinding
continue;
}
var handleString = dbObject.Handle.Value.ToString();
objectTypes.Add(dbObject.GetType().Name);
objs.Add(handleString);
objs.Add(dbObject.GetSpeckleApplicationId());
}
tr.Commit();
@@ -107,7 +107,7 @@ public sealed class AutocadSendBinding : ISendBinding
private void OnChangeChangedObjectIds(DBObject dBObject)
{
ChangedObjectIds[dBObject.Handle.Value.ToString()] = 1;
ChangedObjectIds[dBObject.GetSpeckleApplicationId()] = 1;
_idleManager.SubscribeToIdle(nameof(AutocadSendBinding), RunExpirationChecks);
}
@@ -2,6 +2,7 @@ using Autodesk.AutoCAD.Colors;
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Autocad.Operations.Send;
using Speckle.Sdk.Models.Instances;
using Speckle.Sdk.Models.Proxies;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
@@ -15,93 +16,171 @@ public class AutocadColorManager
// POC: Will be addressed to move it into AutocadContext!
private Document Doc => Application.DocumentManager.MdiActiveDocument;
/// <summary>
/// For receive operations
/// </summary>
public Dictionary<string, AutocadColor> ObjectColorsIdMap { get; } = new();
private ColorProxy ConvertColorToColorProxy(AutocadColor color, string id)
/// <summary>
/// For send operations
/// </summary>
private Dictionary<string, ColorProxy> ColorProxies { get; } = new();
private readonly Dictionary<string, AutocadColor> _layerColorDict = new(); // keeps track of layer colors for object inheritance
private readonly Dictionary<string, string> _objectsByLayerDict = new(); // keeps track of ids for all objects that inherited their color by layer
/// <summary>
/// Processes an object's color and adds the object id to a color proxy in <see cref="ColorProxies"/> if object color is set ByAci, ByColor, or ByBlock.
/// Otherwise, stores the object id and color in a corresponding ByLayer dictionary for further processing block definitions after all objects are converted.
/// From testing, a definition object will inherit its layer's color if by layer, otherwise it will inherit the instance color settings (which we are sending with the instance).
/// Skips processing ByPen for now, because I don't understand what this means.
/// </summary>
/// <param name="objectId"></param>
/// <param name="color"></param>
private void ProcessObjectColor(string objectId, AutocadColor color, string? layerId = null)
{
switch (color.ColorMethod)
{
case ColorMethod.ByAci:
case ColorMethod.ByColor:
case ColorMethod.ByBlock:
AddObjectIdToColorProxy(objectId, color);
break;
case ColorMethod.ByLayer:
if (layerId != null)
{
#if NET8_0
_objectsByLayerDict.TryAdd(objectId, layerId);
#else
if (!_objectsByLayerDict.ContainsKey(objectId))
{
_objectsByLayerDict.Add(objectId, layerId);
}
#endif
}
break;
case ColorMethod.ByPen: // POC: no idea what this means
break;
}
}
private void AddObjectIdToColorProxy(string objectId, AutocadColor color)
{
string colorId = color.GetSpeckleApplicationId();
if (ColorProxies.TryGetValue(colorId, out ColorProxy? proxy))
{
proxy.objects.Add(objectId);
}
else
{
ColorProxy newColor = ConvertColorToColorProxy(color);
newColor.objects.Add(objectId);
ColorProxies[colorId] = newColor;
}
}
private ColorProxy ConvertColorToColorProxy(AutocadColor color)
{
int argb = color.ColorValue.ToArgb();
string name = color.ColorNameForDisplay;
string id = color.GetSpeckleApplicationId();
ColorProxy colorProxy = new(argb, id, name) { objects = new() };
// INFO: this index is an Autocad internal index for set rgb values
// https://gohtx.com/acadcolors.php
// add the color source as well for receiving in other apps
// POC: in order to support full fidelity color support across autocad and rhino, we need to keep track of the color source property. Not sure if this is the best place to keep track of the source, vs on a ColorSourceProxy or as a property on the atomic object.
colorProxy["source"] = color.IsByBlock
? "block"
: color.IsByLayer
? "layer"
: "object";
// set additional properties if by aci or by block
// ByBlock colors for some reason do not have their color value set to the correct color (white): instead it's a near-black
// ByACI is an Autocad internal index for set rgb values, which effects name presentation, see: https://gohtx.com/acadcolors.php
if (color.IsByAci)
{
colorProxy["autocadColorIndex"] = (int)color.ColorIndex;
}
else if (color.IsByBlock)
{
colorProxy.value = -1;
}
return colorProxy;
}
/// <summary>
/// Iterates through a given set of autocad objects and collects their colors. Note: expects objects to be "atomic", and extracted out of their instances already.
/// Processes colors for definition objects that had their colors inherited. This method is in place primarily to process complex color inheritance in blocks.
/// </summary>
/// <param name="unpackedAutocadRootObjects"></param>
/// <param name="layers"></param>
/// <returns></returns>
/// <remarks>
/// We are **always setting the color** (treating it as ColorMethod.ByColor) for definition objects with color "ByLayer" because this overrides instance color, to guarantee they look correct in the viewer and when receiving.
/// </remarks>
public void ProcessDefinitionObjects(List<InstanceDefinitionProxy> definitions)
{
// process all definition objects, while removing process objects from the by block color dict as necessary
foreach (InstanceDefinitionProxy definition in definitions)
{
foreach (string objectId in definition.objects)
{
if (_objectsByLayerDict.TryGetValue(objectId, out string? layerId))
{
if (_layerColorDict.TryGetValue(layerId, out AutocadColor? layerColor))
{
AddObjectIdToColorProxy(objectId, layerColor);
}
}
}
}
}
/// <summary>
/// Iterates through a given set of autocad objects, layers, and definitions to collect atomic object colors.
/// </summary>
/// <param name="unpackedAutocadRootObjects">atomic root objects, including definition objects</param>
/// <param name="layers">layers used by atomic objects</param>
/// <param name="definitions">definitions used by instances in atomic objects</param>
/// <returns></returns>
/// <remarks>
/// Due to complications in color inheritance for blocks, we are processing block definition object colors last.
/// </remarks>
public List<ColorProxy> UnpackColors(
List<AutocadRootObject> unpackedAutocadRootObjects,
List<LayerTableRecord> layers
List<LayerTableRecord> layers,
List<InstanceDefinitionProxy> definitions
)
{
Dictionary<string, ColorProxy> colorProxies = new();
// Stage 1: unpack colors from objects
foreach (AutocadRootObject rootObj in unpackedAutocadRootObjects)
{
Entity entity = rootObj.Root;
// skip any objects that inherit their colors
if (!entity.Color.IsByAci && !entity.Color.IsByColor)
{
continue;
}
// assumes color names are unique
string colorId = entity.Color.ColorNameForDisplay;
if (colorProxies.TryGetValue(colorId, out ColorProxy? value))
{
value.objects.Add(rootObj.ApplicationId);
}
else
{
ColorProxy newColor = ConvertColorToColorProxy(entity.Color, colorId);
newColor.objects.Add(rootObj.ApplicationId);
colorProxies[colorId] = newColor;
}
ProcessObjectColor(rootObj.ApplicationId, entity.Color, entity.LayerId.ToString());
}
// Stage 2: make sure we collect layer colors as well
foreach (LayerTableRecord layer in layers)
{
// assumes color names are unique
string colorId = layer.Color.ColorNameForDisplay;
string layerId = layer.GetSpeckleApplicationId(); // Do not use handle directly, see note in the 'GetSpeckleApplicationId' method
if (colorProxies.TryGetValue(colorId, out ColorProxy? value))
{
value.objects.Add(layerId);
}
else
{
ColorProxy newColor = ConvertColorToColorProxy(layer.Color, colorId);
newColor.objects.Add(layerId);
colorProxies[colorId] = newColor;
}
ProcessObjectColor(layer.GetSpeckleApplicationId(), layer.Color);
_layerColorDict.Add(layer.Id.ToString(), layer.Color);
}
return colorProxies.Values.ToList();
// Stage 3: process definition objects that inherited their colors
ProcessDefinitionObjects(definitions);
return ColorProxies.Values.ToList();
}
public AutocadColor ConvertColorProxyToColor(ColorProxy colorProxy)
{
AutocadColor color = colorProxy["autocadColorIndex"] is long index
// if source = block, return a default ByBlock color
if (colorProxy["source"] is string source && source == "block")
{
return AutocadColor.FromColorIndex(ColorMethod.ByBlock, 0);
}
return colorProxy["autocadColorIndex"] is long index
? AutocadColor.FromColorIndex(ColorMethod.ByAci, (short)index)
: AutocadColor.FromColor(System.Drawing.Color.FromArgb(colorProxy.value));
return color;
}
/// <summary>
@@ -115,6 +194,13 @@ public class AutocadColorManager
foreach (ColorProxy colorProxy in colorProxies)
{
onOperationProgressed?.Invoke("Converting colors", (double)++count / colorProxies.Count);
// skip any colors with source = layer, since object color default source is by layer
if (colorProxy["source"] is string source && source == "layer")
{
continue;
}
foreach (string objectId in colorProxy.objects)
{
AutocadColor convertedColor = ConvertColorProxyToColor(colorProxy);
@@ -1,4 +1,5 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Autocad.Operations.Send;
using Speckle.Connectors.Utils.Conversion;
using Speckle.Sdk;
@@ -39,7 +40,7 @@ public class AutocadGroupManager
{
continue;
}
var groupAppId = group.Handle.ToString();
var groupAppId = group.GetSpeckleApplicationId();
if (groupProxies.TryGetValue(groupAppId, out GroupProxy? groupProxy))
{
groupProxy.objects.Add(applicationId);
@@ -65,27 +65,27 @@ public class AutocadInstanceObjectManager : IInstanceUnpacker<AutocadRootObject>
private void UnpackInstance(BlockReference instance, int depth, Transaction transaction)
{
var instanceIdString = instance.Handle.Value.ToString();
string instanceId = instance.GetSpeckleApplicationId();
// If this instance has a reference to an anonymous block, it means it's spawned from a dynamic block. Anonymous blocks are
// used to represent specific "instances" of dynamic ones.
// We do not want to send the full dynamic block definition, but its current "instance", as such here we're making sure we
// take up the anon block table reference definition (if it exists). If it's not an instance of a dynamic block, we're
// using the normal def reference.
var hasAnonymousBlockTableRecordDefinition = instance.AnonymousBlockTableRecord != ObjectId.Null;
var definitionId = hasAnonymousBlockTableRecordDefinition
ObjectId definitionId = !instance.AnonymousBlockTableRecord.IsNull
? instance.AnonymousBlockTableRecord
: instance.BlockTableRecord;
InstanceProxy instanceProxy =
new()
{
applicationId = instanceIdString,
applicationId = instanceId,
definitionId = definitionId.ToString(),
maxDepth = depth,
transform = GetMatrix(instance.BlockTransform.ToArray()),
units = _unitsConverter.ConvertOrThrow(Application.DocumentManager.CurrentDocument.Database.Insunits)
};
_instanceObjectsManager.AddInstanceProxy(instanceIdString, instanceProxy);
_instanceObjectsManager.AddInstanceProxy(instanceId, instanceProxy);
// For each block instance that has the same definition, we need to keep track of the "maximum depth" at which is found.
// This will enable on receive to create them in the correct order (descending by max depth, interleaved definitions and instances).
@@ -113,7 +113,7 @@ public class AutocadInstanceObjectManager : IInstanceUnpacker<AutocadRootObject>
}
}
instanceProxiesWithSameDefinition.Add(_instanceObjectsManager.GetInstanceProxy(instanceIdString));
instanceProxiesWithSameDefinition.Add(_instanceObjectsManager.GetInstanceProxy(instanceId));
if (
_instanceObjectsManager.TryGetInstanceDefinitionProxy(definitionId.ToString(), out InstanceDefinitionProxy value)
@@ -134,29 +134,30 @@ public class AutocadInstanceObjectManager : IInstanceUnpacker<AutocadRootObject>
applicationId = definitionId.ToString(),
objects = new(),
maxDepth = depth,
name = hasAnonymousBlockTableRecordDefinition ? "Dynamic instance " + definitionId : definition.Name,
name = !instance.AnonymousBlockTableRecord.IsNull ? "Dynamic instance " + definitionId : definition.Name,
["comments"] = definition.Comments
};
// Go through each definition object
foreach (ObjectId id in definition)
{
var obj = transaction.GetObject(id, OpenMode.ForRead);
Entity obj = (Entity)transaction.GetObject(id, OpenMode.ForRead);
// In the case of dynamic blocks, this prevents sending objects that are not visibile in its current state.
if (obj is Entity { Visible: false })
if (!obj.Visible)
{
continue;
}
var handleIdString = obj.Handle.Value.ToString();
definitionProxy.objects.Add(handleIdString);
string appId = obj.GetSpeckleApplicationId();
definitionProxy.objects.Add(appId);
if (obj is BlockReference blockReference)
{
UnpackInstance(blockReference, depth + 1, transaction);
}
_instanceObjectsManager.AddAtomicObject(handleIdString, new((Entity)obj, handleIdString));
_instanceObjectsManager.AddAtomicObject(appId, new(obj, appId));
}
_instanceObjectsManager.AddDefinitionProxy(definitionId.ToString(), definitionProxy);
@@ -220,7 +221,7 @@ public class AutocadInstanceObjectManager : IInstanceUnpacker<AutocadRootObject>
definitionIdAndApplicationIdMap[definitionProxy.applicationId] = id;
transaction.AddNewlyCreatedDBObject(record, true);
var consumedEntitiesHandleValues = constituentEntities.Select(ent => ent.Handle.Value.ToString()).ToArray();
var consumedEntitiesHandleValues = constituentEntities.Select(ent => ent.GetSpeckleApplicationId()).ToArray();
consumedObjectIds.AddRange(consumedEntitiesHandleValues);
createdObjectIds.RemoveAll(newId => consumedEntitiesHandleValues.Contains(newId));
}
@@ -272,9 +273,9 @@ public class AutocadInstanceObjectManager : IInstanceUnpacker<AutocadRootObject>
transaction.AddNewlyCreatedDBObject(blockRef, true);
conversionResults.Add(
new(Status.SUCCESS, instanceProxy, blockRef.Handle.Value.ToString(), "Instance (Block)")
new(Status.SUCCESS, instanceProxy, blockRef.GetSpeckleApplicationId(), "Instance (Block)")
);
createdObjectIds.Add(blockRef.Handle.Value.ToString());
createdObjectIds.Add(blockRef.GetSpeckleApplicationId());
}
}
catch (Exception ex) when (!ex.IsFatal())
@@ -76,7 +76,7 @@ public class AutocadMaterialManager
if (transaction.GetObject(entity.MaterialId, OpenMode.ForRead) is Material material)
{
string materialId = material.Handle.ToString();
string materialId = material.GetSpeckleApplicationId();
if (materialProxies.TryGetValue(materialId, out RenderMaterialProxy? value))
{
value.objects.Add(rootObj.ApplicationId);
@@ -95,7 +95,7 @@ public class AutocadMaterialManager
{
if (transaction.GetObject(layer.MaterialId, OpenMode.ForRead) is Material material)
{
string materialId = material.Handle.ToString();
string materialId = material.GetSpeckleApplicationId();
string layerId = layer.GetSpeckleApplicationId(); // Do not use handle directly, see note in the 'GetSpeckleApplicationId' method
if (materialProxies.TryGetValue(materialId, out RenderMaterialProxy? value))
{
@@ -1,39 +0,0 @@
using Autodesk.AutoCAD.DatabaseServices;
using Speckle.Sdk;
using Speckle.Sdk.Common;
namespace Speckle.Connectors.Autocad.HostApp.Extensions;
public static class AcadUnitsExtension
{
public static string ToSpeckleString(this UnitsValue units)
{
switch (units)
{
case UnitsValue.Millimeters:
return Units.Millimeters;
case UnitsValue.Centimeters:
return Units.Centimeters;
case UnitsValue.Meters:
return Units.Meters;
case UnitsValue.Kilometers:
return Units.Kilometers;
case UnitsValue.Inches:
case UnitsValue.USSurveyInch:
return Units.Inches;
case UnitsValue.Feet:
case UnitsValue.USSurveyFeet:
return Units.Feet;
case UnitsValue.Yards:
case UnitsValue.USSurveyYard:
return Units.Yards;
case UnitsValue.Miles:
case UnitsValue.USSurveyMile:
return Units.Miles;
case UnitsValue.Undefined:
return Units.None;
default:
throw new SpeckleException($"The Unit System \"{units}\" is unsupported.");
}
}
}
@@ -1,14 +0,0 @@
using Autodesk.AutoCAD.DatabaseServices;
namespace Speckle.Connectors.Autocad.HostApp.Extensions;
public static class LayerTableRecordExtensions
{
/// <summary>
/// Layers and geometries can have same application ids.....
/// We should prevent it for sketchup converter. Because when it happens "objects_to_bake" definition
/// is changing on the way if it happens.
/// </summary>
public static string GetSpeckleApplicationId(this LayerTableRecord layerTableRecord) =>
$"layer_{layerTableRecord.Handle}";
}
@@ -0,0 +1,44 @@
using Autodesk.AutoCAD.DatabaseServices;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.HostApp.Extensions;
public static class SpeckleApplicationIdExtensions
{
/// <summary>
/// Retrieves the Speckle object application id
/// </summary>
public static string GetSpeckleApplicationId(this Entity entity) => entity.Handle.Value.ToString();
/// <summary>
/// Retrieves the Speckle object application id
/// </summary>
public static string GetSpeckleApplicationId(this DBObject dbObj) => dbObj.Handle.Value.ToString();
/// <summary>
/// Layers and geometries can have same application ids.....
/// We should prevent it for sketchup converter. Because when it happens "objects_to_bake" definition
/// is changing on the way if it happens.
/// </summary>
public static string GetSpeckleApplicationId(this LayerTableRecord layerTableRecord) =>
$"layer_{layerTableRecord.Handle.Value}";
/// <summary>
/// Retrieves a unique material Speckle object application id.
/// </summary>
/// <remarks> Unconfirmed, but materials and geometries may have same application ids.</remarks>
public static string GetSpeckleApplicationId(this Material material) => $"material_{material.Handle.Value}";
/// <summary>
/// Retrieves a unique color Speckle object application id from the rgb value and color source.
/// </summary>
/// <remarks> Uses the rgb value since color names are not unique </remarks>
public static string GetSpeckleApplicationId(this AutocadColor color) =>
$"color_{color.ColorValue}_{(color.IsByBlock ? "block" : color.IsByLayer ? "layer" : "object")}";
/// <summary>
/// Retrieves a unique group Speckle object application id.
/// </summary>
/// <remarks>Unconfirmed, but groups and geometries may have same application ids.</remarks>
public static string GetSpeckleApplicationId(this Group group) => $"group_{group.Handle.Value}";
}
@@ -156,12 +156,12 @@ public class AutocadHostObjectBuilder : IHostObjectBuilder
convertedObjects.Select(e => new ReceiveConversionResult(
Status.SUCCESS,
atomicObject,
e.Handle.Value.ToString(),
e.GetSpeckleApplicationId(),
e.GetType().ToString()
))
);
bakedObjectIds.AddRange(convertedObjects.Select(e => e.Handle.Value.ToString()));
bakedObjectIds.AddRange(convertedObjects.Select(e => e.GetSpeckleApplicationId()));
}
catch (Exception ex) when (!ex.IsFatal())
{
@@ -149,7 +149,11 @@ public class AutocadRootObjectBuilder : IRootObjectBuilder<AutocadRootObject>
modelWithLayers["renderMaterialProxies"] = materialProxies;
// set colors
List<ColorProxy> colorProxies = _colorManager.UnpackColors(atomicObjects, usedAcadLayers);
List<ColorProxy> colorProxies = _colorManager.UnpackColors(
atomicObjects,
usedAcadLayers,
instanceDefinitionProxies
);
modelWithLayers["colorProxies"] = colorProxies;
return new RootObjectBuilderResult(modelWithLayers, results);
@@ -26,12 +26,11 @@
<Compile Include="$(MSBuildThisFileDirectory)HostApp\AutocadIdleManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\AutocadColorManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\AutocadLayerManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\AcadUnitsExtension.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\DatabaseExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\DocumentExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\EditorExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\EntityExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\LayerTableRecordExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\Extensions\SpeckleApplicationIdExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)HostApp\TransactionContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Receive\AutocadHostObjectBuilder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Operations\Send\AutocadRootObject.cs" />