Files
speckle-sharp-connectors/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceObjectManager.cs
T
2024-07-04 11:56:34 +01:00

369 lines
12 KiB
C#

using System.DoubleNumerics;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Autocad.Operations.Send;
using Speckle.Connectors.Utils.Conversion;
using Speckle.Connectors.Utils.Instances;
using Speckle.Core.Kits;
using Speckle.Core.Logging;
using Speckle.Core.Models;
using Speckle.Core.Models.Instances;
namespace Speckle.Connectors.Autocad.HostApp;
/// <summary>
/// <inheritdoc/>
/// Expects to be a scoped dependency per send or receive operation.
/// </summary>
public class AutocadInstanceObjectManager : IInstanceObjectsManager<AutocadRootObject, List<Entity>>
{
private readonly AutocadLayerManager _autocadLayerManager;
private Dictionary<string, InstanceProxy> InstanceProxies { get; set; } = new();
private Dictionary<string, List<InstanceProxy>> InstanceProxiesByDefinitionId { get; set; } = new();
private Dictionary<string, InstanceDefinitionProxy> DefinitionProxies { get; set; } = new();
private Dictionary<string, AutocadRootObject> FlatAtomicObjects { get; set; } = new();
public AutocadInstanceObjectManager(AutocadLayerManager autocadLayerManager)
{
_autocadLayerManager = autocadLayerManager;
}
public UnpackResult<AutocadRootObject> UnpackSelection(IEnumerable<AutocadRootObject> objects)
{
using var transaction = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
foreach (var obj in objects)
{
if (obj.Root is BlockReference blockReference && !blockReference.IsDynamicBlock)
{
UnpackInstance(blockReference, 0, transaction);
}
FlatAtomicObjects[obj.ApplicationId] = obj;
}
return new(FlatAtomicObjects.Values.ToList(), InstanceProxies, DefinitionProxies.Values.ToList());
}
private void UnpackInstance(BlockReference instance, int depth, Transaction transaction)
{
var instanceIdString = instance.Handle.Value.ToString();
var definitionId = instance.BlockTableRecord;
InstanceProxies[instanceIdString] = new InstanceProxy()
{
applicationId = instanceIdString,
DefinitionId = definitionId.ToString(),
MaxDepth = depth,
Transform = GetMatrix(instance.BlockTransform.ToArray()),
Units = Application.DocumentManager.CurrentDocument.Database.Insunits.ToSpeckleString()
};
// 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).
// We need to interleave the creation of definitions and instances, as some definitions may depend on instances.
if (
!InstanceProxiesByDefinitionId.TryGetValue(
definitionId.ToString(),
out List<InstanceProxy> instanceProxiesWithSameDefinition
)
)
{
instanceProxiesWithSameDefinition = new List<InstanceProxy>();
InstanceProxiesByDefinitionId[definitionId.ToString()] = instanceProxiesWithSameDefinition;
}
// We ensure that all previous instance proxies that have the same definition are at this max depth. I kind of have a feeling this can be done more elegantly, but YOLO
foreach (var instanceProxy in instanceProxiesWithSameDefinition)
{
instanceProxy.MaxDepth = depth;
}
instanceProxiesWithSameDefinition.Add(InstanceProxies[instanceIdString]);
if (DefinitionProxies.TryGetValue(definitionId.ToString(), out InstanceDefinitionProxy value))
{
value.MaxDepth = depth;
return; // exit fast - we've parsed this one so no need to go further
}
var definition = (BlockTableRecord)transaction.GetObject(definitionId, OpenMode.ForRead);
// definition.Origin
var definitionProxy = new InstanceDefinitionProxy()
{
applicationId = definitionId.ToString(),
Objects = new(),
MaxDepth = depth,
["name"] = definition.Name,
["comments"] = definition.Comments,
["units"] = definition.Units // ? not sure needed?
};
// Go through each definition object
foreach (ObjectId id in definition)
{
var obj = transaction.GetObject(id, OpenMode.ForRead);
var handleIdString = obj.Handle.Value.ToString();
definitionProxy.Objects.Add(handleIdString);
if (obj is BlockReference blockReference && !blockReference.IsDynamicBlock)
{
UnpackInstance(blockReference, depth + 1, transaction);
}
FlatAtomicObjects[handleIdString] = new(obj, handleIdString);
}
DefinitionProxies[definitionId.ToString()] = definitionProxy;
}
public BakeResult BakeInstances(
List<(string[] layerPath, IInstanceComponent obj)> instanceComponents,
Dictionary<string, List<Entity>> applicationIdMap,
string baseLayerName,
Action<string, double?>? onOperationProgressed
)
{
var sortedInstanceComponents = instanceComponents
.OrderByDescending(x => x.obj.MaxDepth) // Sort by max depth, so we start baking from the deepest element first
.ThenBy(x => x.obj is InstanceDefinitionProxy ? 0 : 1) // Ensure we bake the deepest definition first, then any instances that depend on it
.ToList();
var definitionIdAndApplicationIdMap = new Dictionary<string, ObjectId>();
using var transaction = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
var conversionResults = new List<ReceiveConversionResult>();
var createdObjectIds = new List<string>();
var consumedObjectIds = new List<string>();
var count = 0;
foreach (var (path, instanceOrDefinition) in sortedInstanceComponents)
{
try
{
onOperationProgressed?.Invoke("Converting blocks", (double)++count / sortedInstanceComponents.Count);
if (instanceOrDefinition is InstanceDefinitionProxy { applicationId: not null } definitionProxy)
{
// TODO: create definition (block table record)
var constituentEntities = definitionProxy.Objects
.Select(id => applicationIdMap.TryGetValue(id, out List<Entity> value) ? value : null)
.Where(x => x is not null)
.SelectMany(ent => ent)
.ToList();
var record = new BlockTableRecord();
var objectIds = new ObjectIdCollection();
record.Name = baseLayerName;
if (definitionProxy["name"] is string name)
{
record.Name += name;
}
else
{
record.Name += definitionProxy.applicationId;
}
foreach (var entity in constituentEntities)
{
// record.AppendEntity(entity);
objectIds.Add(entity.ObjectId);
}
using var blockTable = (BlockTable)
transaction.GetObject(Application.DocumentManager.CurrentDocument.Database.BlockTableId, OpenMode.ForWrite);
var id = blockTable.Add(record);
record.AssumeOwnershipOf(objectIds);
definitionIdAndApplicationIdMap[definitionProxy.applicationId] = id;
transaction.AddNewlyCreatedDBObject(record, true);
var consumedEntitiesHandleValues = constituentEntities.Select(ent => ent.Handle.Value.ToString()).ToArray();
consumedObjectIds.AddRange(consumedEntitiesHandleValues);
createdObjectIds.RemoveAll(newId => consumedEntitiesHandleValues.Contains(newId));
}
else if (
instanceOrDefinition is InstanceProxy instanceProxy
&& definitionIdAndApplicationIdMap.TryGetValue(instanceProxy.DefinitionId, out ObjectId definitionId)
)
{
var matrix3d = GetMatrix3d(instanceProxy.Transform, instanceProxy.Units);
var insertionPoint = Point3d.Origin.TransformBy(matrix3d);
var modelSpaceBlockTableRecord = Application.DocumentManager.CurrentDocument.Database.GetModelSpace(
OpenMode.ForWrite
);
_autocadLayerManager.CreateLayerForReceive(path[0]);
var blockRef = new BlockReference(insertionPoint, definitionId)
{
BlockTransform = matrix3d,
Layer = path[0],
};
modelSpaceBlockTableRecord.AppendEntity(blockRef);
if (instanceProxy.applicationId != null)
{
applicationIdMap[instanceProxy.applicationId] = new List<Entity> { blockRef };
}
transaction.AddNewlyCreatedDBObject(blockRef, true);
conversionResults.Add(
new(Status.SUCCESS, instanceProxy, blockRef.Handle.Value.ToString(), "Instance (Block)")
);
createdObjectIds.Add(blockRef.Handle.Value.ToString());
}
}
catch (Exception ex) when (!ex.IsFatal())
{
conversionResults.Add(new(Status.ERROR, instanceOrDefinition as Base ?? new Base(), null, null, ex));
}
}
transaction.Commit();
return new(createdObjectIds, consumedObjectIds, conversionResults);
}
/// <summary>
/// Cleans up any previously created instances.
/// POC: This function will not be able to delete block definitions if the user creates a new one composed out of received definitions.
/// </summary>
/// <param name="namePrefix"></param>
public void PurgeInstances(string namePrefix)
{
using var transaction = Application.DocumentManager.CurrentDocument.Database.TransactionManager.StartTransaction();
var instanceDefinitionsToDelete = new Dictionary<string, BlockTableRecord>();
// Helper function that recurses through a given block table record's constituent objects and purges inner instances as required.
void TraverseAndClean(BlockTableRecord btr)
{
foreach (var objectId in btr)
{
var obj = transaction.GetObject(objectId, OpenMode.ForRead) as BlockReference;
if (obj == null)
{
continue;
}
var definition = (BlockTableRecord)transaction.GetObject(obj.BlockTableRecord, OpenMode.ForRead);
if (obj.IsErased)
{
TraverseAndClean(definition);
continue;
}
obj.UpgradeOpen();
obj.Erase();
TraverseAndClean(definition);
instanceDefinitionsToDelete[obj.BlockTableRecord.ToString()] = definition;
}
}
using var blockTable = (BlockTable)
transaction.GetObject(Application.DocumentManager.CurrentDocument.Database.BlockTableId, OpenMode.ForRead);
// deep clean definitions
foreach (var btrId in blockTable)
{
var btr = (BlockTableRecord)transaction.GetObject(btrId, OpenMode.ForRead);
if (btr.Name.Contains(namePrefix)) // POC: this is tightly coupled with a naming convention for definitions in the instance object manager
{
TraverseAndClean(btr);
instanceDefinitionsToDelete[btr.Name] = btr;
}
}
foreach (var def in instanceDefinitionsToDelete.Values)
{
def.UpgradeOpen();
def.Erase();
}
transaction.Commit();
}
private Matrix4x4 GetMatrix(double[] t)
{
return new Matrix4x4(
t[0],
t[1],
t[2],
t[3],
t[4],
t[5],
t[6],
t[7],
t[8],
t[9],
t[10],
t[11],
t[12],
t[13],
t[14],
t[15]
);
}
private Matrix3d GetMatrix3d(Matrix4x4 matrix, string units)
{
var sf = Units.GetConversionFactor(
units,
Application.DocumentManager.CurrentDocument.Database.Insunits.ToSpeckleString()
);
var scaledTransform = new[]
{
matrix.M11,
matrix.M12,
matrix.M13,
matrix.M14 * sf,
matrix.M21,
matrix.M22,
matrix.M23,
matrix.M24 * sf,
matrix.M31,
matrix.M32,
matrix.M33,
matrix.M34 * sf,
matrix.M41,
matrix.M42,
matrix.M43,
matrix.M44
};
var m3d = new Matrix3d(scaledTransform);
if (!m3d.IsScaledOrtho())
{
m3d = new Matrix3d(MakePerpendicular(m3d));
}
return m3d;
}
// https://forums.autodesk.com/t5/net/set-blocktransform-values/m-p/6452121#M49479
private static double[] MakePerpendicular(Matrix3d matrix)
{
// Get the basis vectors of the matrix
Vector3d right = new(matrix[0, 0], matrix[1, 0], matrix[2, 0]);
Vector3d up = new(matrix[0, 1], matrix[1, 1], matrix[2, 1]);
Vector3d newForward = right.CrossProduct(up).GetNormal();
Vector3d newUp = newForward.CrossProduct(right).GetNormal();
return new[]
{
right.X,
newUp.X,
newForward.X,
matrix[0, 3],
right.Y,
newUp.Y,
newForward.Y,
matrix[1, 3],
right.Z,
newUp.Z,
newForward.Z,
matrix[2, 3],
0.0,
0.0,
0.0,
matrix[3, 3],
};
}
}