Files
speckle-sharp-connectors/Connectors/Autocad/Speckle.Connectors.AutocadShared/HostApp/AutocadInstanceBaker.cs
T
Jedd Morgan 7860c44f4e feat(api)!: Implement new packfile based sends via SendPipline (aka DuckDB changes) (#1277)
* Dim/quack lets go (#1275)

* Add model ingestion to sharp connectors

* correct ingestion message

* Progress

* grasshopper

* GH exception messages

* fix GH

* file names

* revit file name

* grasshopper file names

* etabs file names

* delete tests

* tekla maybe

* ingestion  scope

* bad boolean logic

* Longer TimeSpan

* wip upload pipe

* 10s

* passthrough ingestion id

* happy hack time: prevent ingestion completion

this is handled server-side in the processing logic.

* add packfile send endpoint detection and routing

Route to SendViaPackfile when the server supports the upload-signing
endpoint (POST probe, 404 = unsupported) and a continuous traversal
builder is registered.

* Adds Continuous Traversal Builder

Introduces a Continuous Traversal Builder to manage the conversion and processing of Revit elements within a Send Pipeline.

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* feat(api): DI Refactor for Duck DB + Gergo's API endpoint changes (#1282)

* Di

* undo accidental change

* Feat (duck): dui ingestion updates post upload (#1295)

* Pass optional ingestion id to DUI

* Make ingestion id null for the SendViaIngestion, see the note :)

* feat!: Duckdev progress reporitng (#1296)

* Di

* throwaway from laptop

* Progress reporting

* Use matching logger

* Revit and revert rhino unpacker progress

* more revertion

* make pr even cleaner

* and this one

* fix build issues with other connectors

* SDK nuget (#1299)

* Bump to 3.14.0-alpha.2

* Feat(duck): grasshopper (#1297)

* Duck x Grasshopper - who would win?

* Fix registration for new builder

* missing imports

* return version id grasshopper

* Align sync resource to sync

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Bump SDK

* feat(importer): rhino file importer changes for packfile (#1301)

* rhino importer changes

* correct deps

* Bump SDK

* Fix build issues

* ditto

* Fix build issue

* Lower standards

* Fix build

* feat: duck for acad, civil, navis, tekla, etabs (#1300)

* duck: acad, civil, etabs, tekla, navis and bump channels to 10.0.0

* notes

* fix conflicts

* more conflicts

* Ready for testing

* fix(duck): Fix send caching (#1302)

* potential fix

* undo-rhino chnages

* fix xml comment

* amended comment

* revit

* Fix build

* Aligned converting message

* fix: reoccurring object references

* Bump sdk and resolve merge conflict issues

* Merge pull request #1317 from specklesystems/jrm/importer-tracing

feat(otel): Tracing and OTEL changes for Rhino importer

* Fix revit linked model progress (#1312)

* Revert otel packages

* bump SDK

* Trace unpacking groups

* Align trace context nullability with app

* Disable send caching in Navisworks

* comments

* Update FileimportPayload.cs

* fix using directive

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Fix merge conflicts

---------

Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sebastian Witt <sebastian.witt@rwth-aachen.de>
2026-04-08 10:07:56 +01:00

298 lines
10 KiB
C#

using System.Diagnostics.CodeAnalysis;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Autocad.HostApp.Extensions;
using Speckle.Connectors.Common.Conversion;
using Speckle.Connectors.Common.Instances;
using Speckle.Converters.Autocad;
using Speckle.Converters.Common;
using Speckle.DoubleNumerics;
using Speckle.Sdk;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Dependencies;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.Instances;
using Speckle.Sdk.Pipelines.Progress;
using AutocadColor = Autodesk.AutoCAD.Colors.Color;
namespace Speckle.Connectors.Autocad.HostApp;
/// <summary>
/// Expects to be a scoped dependency receive operation.
/// </summary>
public class AutocadInstanceBaker : IInstanceBaker<IReadOnlyCollection<Entity>>
{
private readonly AutocadLayerBaker _layerBaker;
private readonly IAutocadColorBaker _colorBaker;
private readonly IAutocadMaterialBaker _materialBaker;
private readonly AutocadContext _autocadContext;
private readonly ILogger<AutocadInstanceBaker> _logger;
private readonly IConverterSettingsStore<AutocadConversionSettings> _converterSettings;
public AutocadInstanceBaker(
AutocadLayerBaker layerBaker,
IAutocadColorBaker colorBaker,
IAutocadMaterialBaker materialBaker,
AutocadContext autocadContext,
ILogger<AutocadInstanceBaker> logger,
IConverterSettingsStore<AutocadConversionSettings> converterSettings
)
{
_layerBaker = layerBaker;
_colorBaker = colorBaker;
_materialBaker = materialBaker;
_autocadContext = autocadContext;
_logger = logger;
_converterSettings = converterSettings;
}
[SuppressMessage("Maintainability", "CA1506:Avoid excessive class coupling")]
public BakeResult BakeInstances(
ICollection<(Collection[] collectionPath, IInstanceComponent obj)> instanceComponents,
Dictionary<string, IReadOnlyCollection<Entity>> applicationIdMap,
string baseLayerName,
IProgress<CardProgress> 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 HashSet<ReceiveConversionResult>();
var createdObjectIds = new HashSet<string>();
var consumedObjectIds = new HashSet<string>();
var count = 0;
foreach (var (collectionPath, instanceOrDefinition) in sortedInstanceComponents)
{
try
{
onOperationProgressed.Report(new("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 IReadOnlyCollection<Entity>? value) ? value : null
)
.Where(x => x is not null)
.SelectMany(ent => ent!)
.ToList();
var record = new BlockTableRecord();
var objectIds = new ObjectIdCollection();
// We're expecting to have Name prop always for definitions. If there is an edge case, ask to Dim or Ogu
record.Name = _autocadContext.RemoveInvalidChars(
$"{definitionProxy.name}-({definitionProxy.applicationId})-{baseLayerName}"
);
foreach (var entity in constituentEntities)
{
objectIds.Add(entity.ObjectId);
}
if (constituentEntities.Count == 0)
{
throw new ConversionException("No objects found to create instance definition.");
}
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.GetSpeckleApplicationId()).ToArray();
consumedObjectIds.UnionWith(consumedEntitiesHandleValues);
createdObjectIds.RemoveWhere(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
);
// POC: collectionPath for instances should be an array of size 1, because we are flattening collections on traversal
string layerName = _layerBaker.CreateLayerForReceive(collectionPath, baseLayerName);
// get color and material if any
string instanceId = instanceProxy.applicationId ?? instanceProxy.id.NotNull();
AutocadColor? objColor = _colorBaker.ObjectColorsIdMap.TryGetValue(instanceId, out AutocadColor? color)
? color
: null;
ObjectId objMaterial = _materialBaker.ObjectMaterialsIdMap.TryGetValue(instanceId, out ObjectId matId)
? matId
: ObjectId.Null;
BlockReference blockRef = new(insertionPoint, definitionId) { BlockTransform = matrix3d, Layer = layerName, };
if (objColor is not null)
{
blockRef.Color = objColor;
}
if (objMaterial != ObjectId.Null)
{
blockRef.MaterialId = objMaterial;
}
modelSpaceBlockTableRecord.AppendEntity(blockRef);
applicationIdMap[instanceId] = new List<Entity> { blockRef };
transaction.AddNewlyCreatedDBObject(blockRef, true);
conversionResults.Add(
new(Status.SUCCESS, instanceProxy, blockRef.GetSpeckleApplicationId(), "Instance (Block)")
);
createdObjectIds.Add(blockRef.GetSpeckleApplicationId());
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Failed to create an instance from proxy");
conversionResults.Add(new(Status.ERROR, instanceOrDefinition as Base ?? new Base(), null, null, ex));
}
}
transaction.Commit();
return new(createdObjectIds.Freeze(), consumedObjectIds.Freeze(), conversionResults.Freeze());
}
/// <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)
{
namePrefix = _autocadContext.RemoveInvalidChars(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 Matrix3d GetMatrix3d(Matrix4x4 matrix, string units)
{
var sf = Units.GetConversionFactor(units, _converterSettings.Current.SpeckleUnits);
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 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
[
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]
];
}
}