Compare commits

..

4 Commits

Author SHA1 Message Date
Oğuzhan Koral 3424de9130 Merge pull request #1032 from specklesystems/dev
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Update dev to main
2025-08-18 11:33:50 +01:00
Jedd Morgan f1f17eea3d Merge pull request #1021 from specklesystems/jrm/dev-to-main
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
Dev -> Main
2025-08-07 15:16:38 +01:00
Jedd Morgan 642607acad Merge branch 'main' into jrm/dev-to-main 2025-08-07 15:09:09 +01:00
Claire Kuang 2cb7211734 Merge pull request #1009 from specklesystems/dev
.NET Build and Publish / build-windows (push) Has been cancelled
.NET Build and Publish / build-linux (push) Has been cancelled
.NET Build and Publish / deploy-installers (push) Has been cancelled
dev to main
2025-07-30 12:42:01 +01:00
11 changed files with 278 additions and 794 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
file_version: ${{ steps.set-version.outputs.file_version }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -37,173 +37,91 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
protected override void SolveInstance(IGH_DataAccess da)
{
// on first iteration, discover all fields from all objects to create stable output structure
if (da.Iteration == 0)
{
HashSet<string> allFields = DiscoverAllFieldsFromInput();
if (allFields.Count > 0)
{
var requiredOutputs = CreateOutputParamsFromFieldNames(allFields);
if (OutputMismatch(requiredOutputs))
{
OnPingDocument()?.ScheduleSolution(5, _ => CreateOutputs(requiredOutputs));
return;
}
}
}
// process current object normally
object data = new();
if (!da.GetData(0, ref data))
da.GetData(0, ref data);
List<OutputParamWrapper> outputParams = new();
switch (data)
{
return;
case SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null:
// get children elements from the wrapper to override the elements prop while parsing
List<IGH_Goo> children = collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList();
outputParams = ParseSpeckleWrapper(collectionGoo.Value, children);
break;
case SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null:
// get geometries from the wrapper to override the displayvalue prop while parsing
List<IGH_Goo> display = dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList();
outputParams = ParseSpeckleWrapper(dataObjectGoo.Value, null, display);
break;
case SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null:
outputParams = ParseSpeckleWrapper(objectGoo.Value);
break;
case SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null:
outputParams = ParseSpeckleWrapper(blockInstanceGoo.Value);
break;
case SpeckleBlockDefinitionWrapperGoo blockDef:
outputParams = ParseSpeckleWrapper(blockDef.Value);
break;
case SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null:
outputParams = ParseSpeckleWrapper(materialGoo.Value);
break;
case SpecklePropertyGroupGoo propGoo:
Name = $"properties ({propGoo.Value.Count})";
outputParams = new();
foreach (var key in propGoo.Value.Keys)
{
ISpecklePropertyGoo value = propGoo.Value[key];
object? outputValue = value is SpecklePropertyGoo prop
? prop.Value
: value is SpecklePropertyGroupGoo propGroup
? propGroup
: value;
OutputParamWrapper output =
outputValue is IList
? CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.list)
: CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item);
outputParams.Add(output);
}
break;
default:
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}");
return;
}
var outputParams = DeconstructObject(data);
if (outputParams == null)
{
return;
}
// set component name based on the current object
NickName = Name;
// set output data - fill missing fields with nulls for objects that don't have all fields
SetOutputData(da, outputParams);
}
/// <summary>
/// Discovers all unique field names from all input objects by looking at volatile data directly.
/// </summary>
private HashSet<string> DiscoverAllFieldsFromInput()
{
HashSet<string> allFields = new();
foreach (var item in Params.Input[0].VolatileData.AllData(true))
if (da.Iteration == 0 && OutputMismatch(outputParams))
{
var objectOutputs = DeconstructObject(item);
if (objectOutputs != null)
OnPingDocument()
.ScheduleSolution(
5,
_ =>
{
CreateOutputs(outputParams);
}
);
}
else
{
for (int i = 0; i < outputParams.Count; i++)
{
foreach (var output in objectOutputs)
var outParam = Params.Output[i];
var outParamWrapper = outputParams[i];
switch (outParam.Access)
{
allFields.Add(output.Param.Name);
case GH_ParamAccess.item:
da.SetData(i, outParamWrapper.Value);
break;
case GH_ParamAccess.list:
da.SetDataList(i, outParamWrapper.Value as IList);
break;
}
}
}
return allFields;
}
/// <summary>
/// Creates output parameter wrappers from a set of field names, all with item access.
/// </summary>
private List<OutputParamWrapper> CreateOutputParamsFromFieldNames(HashSet<string> fieldNames) =>
fieldNames
.OrderBy(name => name)
.Select(fieldName => CreateOutputParamByKeyValue(fieldName, null, GH_ParamAccess.item))
.ToList();
/// <summary>
/// Deconstructs a single object into its constituent fields/properties.
/// </summary>
private List<OutputParamWrapper>? DeconstructObject(object data) =>
data switch
{
// get children elements from wrapper to override elements prop while parsing
SpeckleCollectionWrapperGoo collectionGoo when collectionGoo.Value != null
=> ParseSpeckleWrapper(
collectionGoo.Value,
collectionGoo.Value.Elements.Select(o => ((SpeckleWrapper)o).CreateGoo()).ToList()
),
// get geometries from wrapper to override displayValue prop while parsing
SpeckleDataObjectWrapperGoo dataObjectGoo when dataObjectGoo.Value != null
=> ParseSpeckleWrapper(
dataObjectGoo.Value,
null,
dataObjectGoo.Value.Geometries.Select(o => o.CreateGoo()).ToList()
),
SpeckleGeometryWrapperGoo objectGoo when objectGoo.Value != null => ParseSpeckleWrapper(objectGoo.Value),
SpeckleBlockInstanceWrapperGoo blockInstanceGoo when blockInstanceGoo.Value != null
=> ParseSpeckleWrapper(blockInstanceGoo.Value),
SpeckleBlockDefinitionWrapperGoo blockDef when blockDef.Value != null => ParseSpeckleWrapper(blockDef.Value),
SpeckleMaterialWrapperGoo materialGoo when materialGoo.Value != null => ParseSpeckleWrapper(materialGoo.Value),
SpecklePropertyGroupGoo propGoo when propGoo.Value != null => ParsePropertyGroup(propGoo),
_ => HandleUnsupportedType(data)
};
/// <summary>
/// Handles SpecklePropertyGroupGoo objects by extracting their key-value pairs.
/// </summary>
private List<OutputParamWrapper> ParsePropertyGroup(SpecklePropertyGroupGoo propGoo)
{
Name = $"properties ({propGoo.Value.Count})";
List<OutputParamWrapper> objectOutputs = new();
foreach (var key in propGoo.Value.Keys)
{
ISpecklePropertyGoo value = propGoo.Value[key];
object? outputValue = value switch
{
SpecklePropertyGoo prop => prop.Value,
SpecklePropertyGroupGoo propGroup => propGroup,
_ => value
};
objectOutputs.Add(CreateOutputParamByKeyValue(key, outputValue, GH_ParamAccess.item));
}
return objectOutputs;
}
/// <summary>
/// Handles unsupported object types by logging an error and returning null.
/// </summary>
private List<OutputParamWrapper>? HandleUnsupportedType(object data)
{
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, $"Type cannot be deconstructed: {data.GetType().Name}");
return null;
}
/// <summary>
/// Sets output data for the current iteration, filling missing fields with null values.
/// Uses a lookup dictionary for efficient field matching.
/// </summary>
private void SetOutputData(IGH_DataAccess da, List<OutputParamWrapper> currentOutputs)
{
if (Params.Output.Count == 0)
{
return;
}
// create a lookup for current outputs by field name
var outputLookup = currentOutputs.ToDictionary(o => o.Param.Name, o => o.Value);
// set data for each output parameter
for (int i = 0; i < Params.Output.Count; i++)
{
var outputParam = Params.Output[i];
// set the value if it exists, otherwise set null
object? value = outputLookup.TryGetValue(outputParam.Name, out var fieldValue) ? fieldValue : null;
switch (outputParam.Access)
{
case GH_ParamAccess.item:
da.SetData(i, value);
break;
case GH_ParamAccess.list:
da.SetDataList(i, value as IList ?? new List<object?>());
break;
}
}
}
private List<OutputParamWrapper> ParseSpeckleWrapper(
@@ -228,146 +146,125 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
return result;
}
// process each property of the Base object
// cycle through base props
foreach (var prop in @base.GetMembers(DynamicBaseMemberType.Instance | DynamicBaseMemberType.Dynamic))
{
// skip internal dynamic property keys
if (prop.Key == nameof(Base.DynamicPropertyKeys))
// Convert and add to corresponding output structure
var value = prop.Value;
switch (value)
{
continue;
}
case null:
result.Add(CreateOutputParamByKeyValue(prop.Key, null, GH_ParamAccess.item));
break;
var outputParam = CreateOutputParamForProperty(prop, @base, elements, displayValue);
if (outputParam != null)
{
result.Add(outputParam);
case IList list:
List<object> nativeObjects = new();
// override list value if base is a collection and this is the elements prop, since this is empty if coming from a collectionwrapper
if (@base is Collection && prop.Key == "elements" && elements != null)
{
list = elements;
}
// override list value if base is a dataobject and this is the displayvalue prop, since this is empty if coming from a dataobject wrapper
if (@base is Speckle.Objects.Data.DataObject && prop.Key == "displayValue" && displayValue != null)
{
list = displayValue;
}
foreach (var x in list)
{
switch (x)
{
case SpeckleWrapper wrapper:
nativeObjects.Add(wrapper.CreateGoo());
break;
case Base xBase:
nativeObjects.AddRange(ConvertOrCreateWrapper(xBase));
break;
default:
nativeObjects.Add(x);
break;
}
}
result.Add(CreateOutputParamByKeyValue(prop.Key, nativeObjects, GH_ParamAccess.list));
break;
case Dictionary<string, object?> dict: // this should be treated a properties dict
SpecklePropertyGroupGoo propertyGoo = new();
propertyGoo.CastFrom(dict);
result.Add(CreateOutputParamByKeyValue(prop.Key, propertyGoo, GH_ParamAccess.item));
break;
case SpeckleWrapper wrapper:
result.Add(CreateOutputParamByKeyValue(prop.Key, wrapper.CreateGoo(), GH_ParamAccess.item));
break;
case Base baseValue:
result.Add(CreateOutputParamByKeyValue(prop.Key, ConvertOrCreateWrapper(baseValue), GH_ParamAccess.list));
break;
default:
// we don't want to output dynamic property keys
if (prop.Key == nameof(Base.DynamicPropertyKeys))
{
continue;
}
result.Add(CreateOutputParamByKeyValue(prop.Key, prop.Value, GH_ParamAccess.item));
break;
}
}
return result;
}
/// <summary>
/// Creates an output parameter for a single property, handling different value types appropriately.
/// </summary>
private OutputParamWrapper CreateOutputParamForProperty(
KeyValuePair<string, object?> prop,
Base @base,
List<IGH_Goo>? elements,
List<IGH_Goo>? displayValue
) =>
prop.Value switch
{
null => CreateOutputParamByKeyValue(prop.Key, null, GH_ParamAccess.item),
IList list => CreateListOutputParam(prop.Key, list, @base, elements, displayValue),
Dictionary<string, object?> dict => CreateDictionaryOutputParam(prop.Key, dict),
SpeckleWrapper wrapper => CreateOutputParamByKeyValue(prop.Key, wrapper.CreateGoo(), GH_ParamAccess.item),
Base baseValue => CreateOutputParamByKeyValue(prop.Key, ConvertOrCreateWrapper(baseValue), GH_ParamAccess.list),
_ => CreateOutputParamByKeyValue(prop.Key, prop.Value, GH_ParamAccess.item)
};
/// <summary>
/// Creates an output parameter for list properties, with special handling for collection elements and display values.
/// </summary>
private OutputParamWrapper CreateListOutputParam(
string key,
IList list,
Base @base,
List<IGH_Goo>? elements,
List<IGH_Goo>? displayValue
)
{
// override list value for special cases
IList actualList = key switch
{
"elements" when @base is Collection && elements != null => elements,
"displayValue" when @base is Speckle.Objects.Data.DataObject && displayValue != null => displayValue,
_ => list
};
List<object> nativeObjects = new();
foreach (var item in actualList)
{
switch (item)
{
case SpeckleWrapper wrapper:
nativeObjects.Add(wrapper.CreateGoo());
break;
case Base baseItem:
nativeObjects.AddRange(ConvertOrCreateWrapper(baseItem));
break;
default:
nativeObjects.Add(item);
break;
}
}
return CreateOutputParamByKeyValue(key, nativeObjects, GH_ParamAccess.list);
}
/// <summary>
/// Creates an output parameter for dictionary properties, converting them to SpecklePropertyGroupGoo.
/// </summary>
private OutputParamWrapper CreateDictionaryOutputParam(string key, Dictionary<string, object?> dict)
{
SpecklePropertyGroupGoo propertyGoo = new();
propertyGoo.CastFrom(dict);
return CreateOutputParamByKeyValue(key, propertyGoo, GH_ParamAccess.item);
}
/// <summary>
/// Converts a Speckle Base object to host geometry or creates a wrapper if conversion fails.
/// Returns a list of SpeckleGeometryWrapperGoo objects.
/// </summary>
private List<SpeckleGeometryWrapperGoo> ConvertOrCreateWrapper(Base @base)
{
try
{
// attempt conversion to host geometry
// convert the base and create a wrapper for each result
List<(object, Base)> convertedBase = SpeckleConversionContext.Current.ConvertToHost(@base);
return convertedBase.Select(CreateGeometryWrapper).ToList();
List<SpeckleGeometryWrapperGoo> convertedWrappers = new();
foreach ((object o, Base b) in convertedBase)
{
GeometryBase? g = o as GeometryBase;
SpeckleGeometryWrapper convertedWrapper =
new()
{
Base = b,
GeometryBase = g,
Name = b["name"] as string ?? "",
Color = null,
Material = null
};
convertedWrappers.Add(new(convertedWrapper));
}
return convertedWrappers;
}
catch (ConversionException)
{
// fallback: create wrapper without conversion for objects that can't be converted
return new List<SpeckleGeometryWrapperGoo> { CreateFallbackWrapper(@base) };
// some classes, like RawEncoding, have no direct conversion or fallback value.
// when this is the case, wrap it to allow users to further expand the object.
SpeckleGeometryWrapper convertedWrapper =
new()
{
Base = @base,
GeometryBase = null,
Name = @base[Constants.NAME_PROP] as string ?? "",
Color = null,
Material = null
};
return new() { new SpeckleGeometryWrapperGoo(convertedWrapper) };
}
}
/// <summary>
/// Creates a SpeckleGeometryWrapperGoo from a converted geometry and base object pair.
/// </summary>
private SpeckleGeometryWrapperGoo CreateGeometryWrapper((object geometry, Base @base) converted)
{
SpeckleGeometryWrapper wrapper =
new()
{
Base = converted.@base,
GeometryBase = converted.geometry as GeometryBase,
Name = converted.@base["name"] as string ?? "",
Color = null,
Material = null
};
return new SpeckleGeometryWrapperGoo(wrapper);
}
/// <summary>
/// Creates a fallback wrapper for Base objects that cannot be converted to host geometry.
/// </summary>
private SpeckleGeometryWrapperGoo CreateFallbackWrapper(Base @base)
{
SpeckleGeometryWrapper wrapper =
new()
{
Base = @base,
GeometryBase = null,
Name = @base[Constants.NAME_PROP] as string ?? "",
Color = null,
Material = null
};
return new SpeckleGeometryWrapperGoo(wrapper);
}
private OutputParamWrapper CreateOutputParamByKeyValue(string key, object? value, GH_ParamAccess access)
{
Param_GenericObject param =
@@ -400,17 +297,19 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
return myParam;
}
public bool DestroyParameter(GH_ParameterSide side, int index) => side == GH_ParameterSide.Output;
public bool DestroyParameter(GH_ParameterSide side, int index)
{
return side == GH_ParameterSide.Output;
}
private void CreateOutputs(List<OutputParamWrapper> outputParams)
{
// remove all existing output parameters
// TODO: better, nicer handling of creation/removal
while (Params.Output.Count > 0)
{
Params.UnregisterOutputParameter(Params.Output[^1]);
}
// add new output parameters
foreach (var newParam in outputParams)
{
var param = new Param_GenericObject
@@ -423,15 +322,11 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
Params.RegisterOutputParam(param);
}
// notify Grasshopper of parameter changes
Params.OnParametersChanged();
VariableParameterMaintenance();
ExpireSolution(false);
}
/// <summary>
/// Determines if the current output parameter structure differs from the required structure.
/// </summary>
private bool OutputMismatch(List<OutputParamWrapper> outputParams)
{
if (Params.Output.Count != outputParams.Count)
@@ -439,10 +334,10 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
return true;
}
for (int i = 0; i < outputParams.Count; i++)
var count = 0;
foreach (var newParam in outputParams)
{
var newParam = outputParams[i];
var oldParam = Params.Output[i];
var oldParam = Params.Output[count];
if (
oldParam.NickName != newParam.Param.NickName
|| oldParam.Name != newParam.Param.Name
@@ -451,6 +346,7 @@ public class DeconstructSpeckleParam : GH_Component, IGH_VariableParameterCompon
{
return true;
}
count++;
}
return false;
@@ -1,188 +1,95 @@
using System.Diagnostics.CodeAnalysis;
using Autodesk.Revit.DB;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Services;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
namespace Speckle.Converters.RevitShared.ToHost.TopLevel;
public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<GeometryObject>>
public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<DB.GeometryObject>>
{
private readonly RevitToHostCacheSingleton _revitToHostCacheSingleton;
private readonly ScalingServiceToHost _scalingServiceToHost;
private readonly IReferencePointConverter _referencePointConverter;
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private const double PLANAR_TOLERANCE = 1e-9; // tune if needed, added to avoid numeric noise
private const bool ALLOW_VERTEX_COLOR_OVERRIDE = true; // flip to true if colors should win
private Document? _lastDoc; // if this converter instance is used across open documents, we'll want to invalidate the material cache
public MeshConverterToHost(
RevitToHostCacheSingleton revitToHostCacheSingleton,
ScalingServiceToHost scalingServiceToHost,
IReferencePointConverter referencePointConverter,
IConverterSettingsStore<RevitConversionSettings> converterSettings
IReferencePointConverter referencePointConverter
)
{
_revitToHostCacheSingleton = revitToHostCacheSingleton;
_scalingServiceToHost = scalingServiceToHost;
_referencePointConverter = referencePointConverter;
_converterSettings = converterSettings;
}
public List<GeometryObject> Convert(SOG.Mesh mesh)
public List<DB.GeometryObject> Convert(SOG.Mesh mesh)
{
const TessellatedShapeBuilderTarget TARGET = TessellatedShapeBuilderTarget.Mesh;
const TessellatedShapeBuilderFallback FALLBACK = TessellatedShapeBuilderFallback.Salvage;
TessellatedShapeBuilderTarget target = TessellatedShapeBuilderTarget.Mesh;
TessellatedShapeBuilderFallback fallback = TessellatedShapeBuilderFallback.Salvage;
using var tsb = new TessellatedShapeBuilder();
tsb.Fallback = FALLBACK;
tsb.Target = TARGET;
tsb.GraphicsStyleId = ElementId.InvalidElementId;
// tsb.OpenConnectedFaceSet(false);
using var tsb = new TessellatedShapeBuilder()
{
Fallback = fallback,
Target = target,
GraphicsStyleId = ElementId.InvalidElementId
};
tsb.OpenConnectedFaceSet(false);
var vertices = ArrayToPoints(mesh.vertices, mesh.units);
var vertColors = DecodeVertexColors(mesh.colors);
// optional default material from cache
ElementId defaultMat = ElementId.InvalidElementId;
ElementId materialId = ElementId.InvalidElementId;
if (
_revitToHostCacheSingleton.MaterialsByObjectId.TryGetValue(
mesh.applicationId ?? mesh.id.NotNull(),
out var mapped
out var mappedElementId
)
)
{
defaultMat = mapped;
materialId = mappedElementId;
}
bool hasExplicitMat = defaultMat != ElementId.InvalidElementId;
var facesByMat = new Dictionary<ElementId, List<IList<XYZ>>>();
int i = 0;
while (i < mesh.faces.Count)
{
int faceVertexCount = mesh.faces[i];
if (faceVertexCount < 3)
int n = mesh.faces[i];
if (n < 3)
{
faceVertexCount += 3;
n += 3; // 0 -> 3, 1 -> 4 to preserve backwards compatibility
}
var faceIdx = mesh.faces.GetRange(i + 1, faceVertexCount);
var points = new XYZ[faceVertexCount];
for (int k = 0; k < faceVertexCount; k++)
var points = mesh.faces.GetRange(i + 1, n).Select(x => vertices[x]).ToArray();
if (IsNonPlanarQuad(points))
{
points[k] = vertices[faceIdx[k]];
// Non-planar quads will be triangulated as it's more desirable than `TessellatedShapeBuilder.Build`'s attempt to make them planar.
// TODO consider triangulating all n > 3 polygons that are non-planar
var triPoints = new List<XYZ> { points[0], points[1], points[3] };
var face1 = new TessellatedFace(triPoints, materialId);
tsb.AddFace(face1);
triPoints = new List<XYZ> { points[1], points[2], points[3] };
var face2 = new TessellatedFace(triPoints, materialId);
tsb.AddFace(face2);
}
else
{
var face = new TessellatedFace(points, materialId);
tsb.AddFace(face);
}
var faceMaterial = FaceMat(faceIdx);
switch (faceVertexCount)
{
case 4 when IsNonPlanarQuad(points):
{
// Non-planar quads will be triangulated as it's more desirable than
// TessellatedShapeBuilder.Build's attempt to make them planar.
AddFace([points[0], points[1], points[3]], faceMaterial);
AddFace([points[1], points[2], points[3]], faceMaterial);
break;
}
case > 4 when !IsPlanarNgon(points):
{
for (int k = 1; k < faceVertexCount - 1; k++)
{
AddFace([points[0], points[k], points[k + 1]], faceMaterial);
}
break;
}
default:
{
AddFace(points, faceMaterial);
break;
}
}
i += faceVertexCount + 1;
i += n + 1;
}
var all = new List<GeometryObject>();
tsb.CloseConnectedFaceSet();
foreach (var kv in facesByMat)
{
using var perMat = new TessellatedShapeBuilder();
perMat.Fallback = FALLBACK;
perMat.Target = TARGET;
perMat.GraphicsStyleId = ElementId.InvalidElementId;
tsb.Build();
var result = tsb.GetBuildResult();
perMat.OpenConnectedFaceSet(true);
foreach (var tf in kv.Value.Select(pts => new TessellatedFace(pts, kv.Key)).Where(tf => tf.IsValidObject))
{
perMat.AddFace(tf);
}
perMat.CloseConnectedFaceSet();
perMat.Build();
all.AddRange(perMat.GetBuildResult().GetGeometricalObjects());
}
return all;
void AddFace(IList<XYZ> pts, ElementId mat)
{
if (!facesByMat.TryGetValue(mat, out var list))
{
facesByMat[mat] = list = [];
}
list.Add(pts);
}
// local helper to pick a face material from vertex colors
[SuppressMessage("ReSharper", "RedundantLogicalConditionalExpressionOperand")]
ElementId FaceMat(IList<int> idx)
{
int vCount = vertColors.Length;
var hasColors = vCount > 0;
if (!hasColors || hasExplicitMat && !ALLOW_VERTEX_COLOR_OVERRIDE)
{
return defaultMat;
}
int sr = 0,
sg = 0,
sb = 0,
c = 0;
foreach (var v in idx)
{
if ((uint)v >= (uint)vCount)
{
continue;
}
var vc = vertColors[v];
sr += vc.Red;
sg += vc.Green;
sb += vc.Blue;
c++;
}
if (c == 0)
{
return defaultMat;
}
byte r = Quant((byte)(sr / c));
byte g = Quant((byte)(sg / c));
byte b = Quant((byte)(sb / c));
return GetOrCreateMaterial(_converterSettings.Current.Document, r, g, b);
}
return result.GetGeometricalObjects().ToList();
}
private static bool IsNonPlanarQuad(IList<XYZ> points)
@@ -210,59 +117,7 @@ public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<GeometryObject
1,
1
);
return Math.Abs(matrix.GetDeterminant()) > PLANAR_TOLERANCE;
}
private static bool IsPlanarNgon(IList<XYZ> vertices)
{
int n = vertices.Count;
if (n < 4)
{
return true; // 3 points always define a plane
}
// Newells method for robust best-fit plane =>
// https://www.realtimerendering.com/resources/GraphicsGems/gemsiii/newell.c
double normalX = 0,
normalY = 0,
normalZ = 0;
for (int i = 0, j = n - 1; i < n; j = i, i++)
{
var u = vertices[i];
var v = vertices[j];
normalX += (v.Y - u.Y) * (v.Z + u.Z);
normalY += (v.Z - u.Z) * (v.X + u.X);
normalZ += (v.X - u.X) * (v.Y + u.Y);
}
var length = Math.Sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ);
if (length < 1e-12)
{
return true; // degenerate polygon; treat as planar
}
normalX /= length;
normalY /= length;
normalZ /= length;
var pointOnPlane = vertices[0];
double normalisedPlane = -(normalX * pointOnPlane.X + normalY * pointOnPlane.Y + normalZ * pointOnPlane.Z);
// max signed distance of all vertices to plane
double maxSignedDistance = 0;
for (int i = 1; i < n; i++)
{
var p = vertices[i];
double distance = normalX * p.X + normalY * p.Y + normalZ * p.Z + normalisedPlane;
maxSignedDistance = Math.Max(maxSignedDistance, Math.Abs(distance));
if (maxSignedDistance > PLANAR_TOLERANCE)
{
return false;
}
}
return true;
return matrix.GetDeterminant() != 0;
}
private XYZ[] ArrayToPoints(IList<double> arr, string units)
@@ -273,7 +128,7 @@ public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<GeometryObject
}
XYZ[] points = new XYZ[arr.Count / 3];
var fTypeId = _scalingServiceToHost.UnitsToNative(units);
var fTypeId = _scalingServiceToHost.UnitsToNative(units) ?? UnitTypeId.Meters;
for (int i = 2, k = 0; i < arr.Count; i += 3)
{
@@ -291,89 +146,4 @@ public class MeshConverterToHost : ITypedConverter<SOG.Mesh, List<GeometryObject
return points;
}
private readonly Dictionary<int, ElementId> _matCache = new();
private static Color[] DecodeVertexColors(IList<int>? argb)
{
if (argb == null)
{
return [];
}
var outArr = new Color[argb.Count];
for (int i = 0; i < argb.Count; i++)
{
uint v = unchecked((uint)argb[i]); // Speckle stores ARGB in a signed int
byte r = (byte)((v >> 16) & 0xFF);
byte g = (byte)((v >> 8) & 0xFF);
byte b = (byte)(v & 0xFF);
outArr[i] = new Color(r, g, b);
}
return outArr;
}
private static byte Quant(byte v, int step = 17)
{
int q = (int)Math.Round(v / (double)step) * step;
return (byte)Math.Max(0, Math.Min(255, q));
}
private ElementId GetOrCreateMaterial(Document doc, byte r, byte g, byte b)
{
if (!ReferenceEquals(doc, _lastDoc)) // essentially a document change check hack
{
_matCache.Clear();
_lastDoc = doc;
}
int key = (r << 16) | (g << 8) | b;
if (_matCache.TryGetValue(key, out var id))
{
return id;
}
string name = $"Speckle_DS_{r}_{g}_{b}";
Material? existing;
using (var filteredElementCollector = new FilteredElementCollector(doc))
{
filteredElementCollector.OfClass(typeof(Material)); // add the filter on the same instance
existing = filteredElementCollector
.Cast<Material>() // enumerate inside the using
.FirstOrDefault(m => string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase));
}
if (existing != null)
{
return _matCache[key] = existing.Id;
}
ElementId mid;
if (doc.IsModifiable)
{
using var st = new SubTransaction(doc);
st.Start();
mid = CreateMaterialWithColor(doc, name, r, g, b);
st.Commit();
}
else
{
using var t = new Transaction(doc, "Create DS Material");
t.Start();
mid = CreateMaterialWithColor(doc, name, r, g, b);
t.Commit();
}
return _matCache[key] = mid;
static ElementId CreateMaterialWithColor(Document doc, string name, byte r, byte g, byte b)
{
var materialId = Material.Create(doc, name);
((Material)doc.GetElement(materialId)).Color = new Color(r, g, b);
return materialId;
}
}
}
@@ -78,24 +78,6 @@ public class LocalToGlobalToDirectShapeConverter
.DirectShapeLibrary.GetDirectShapeLibrary(_converterSettings.Current.Document)
.FindDefinition(target.atomicObject.applicationId ?? target.atomicObject.id.NotNull());
result.SetShape(def);
// add snapping references for meshes and curves
foreach (var shape in def)
{
switch (shape)
{
case DB.Mesh m:
foreach (var v in m.Vertices)
{
result.AddReferencePoint(v);
}
break;
case DB.Curve c:
result.AddReferenceCurve(c);
break;
}
}
return result; // note fast exit here
}
@@ -44,27 +44,9 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
public Dictionary<string, object> Convert(DB.Element target)
{
Dictionary<string, object> quantities = new();
switch (target)
if (target.Category?.HasMaterialQuantities ?? false) //category can be null
{
case DBA.Railing railing:
// railings can have subelements including top rails, hand rails, and balusters.
// they also do *not* have any materials associated with their category.
List<DB.ElementId> railingElementIds = [railing.GetTypeId(), railing.TopRail, .. railing.GetHandRails()];
ProcessMaterialsByElementTypes(railingElementIds, quantities);
break;
default:
ProcessMaterialsByCategory(target, quantities);
break;
}
return quantities;
}
private void ProcessMaterialsByCategory(DB.Element element, Dictionary<string, object> quantities)
{
if (element.Category?.HasMaterialQuantities ?? false) //category can be null
{
foreach (DB.ElementId? matId in element.GetMaterialIds(false))
foreach (DB.ElementId? matId in target.GetMaterialIds(false))
{
if (matId is null)
{
@@ -74,18 +56,11 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
var materialQuantity = new Dictionary<string, object>();
var unitSettings = _converterSettings.Current.Document.GetUnits();
// add material props
if (TryAddMaterialPropertiesToQuantitiesDict(matId, materialQuantity, out string matName))
{
quantities[matName] = materialQuantity;
}
// add area and volume props
var areaUnitType = unitSettings.GetFormatOptions(DB.SpecTypeId.Area).GetUnitTypeId();
AddMaterialProperty(
materialQuantity,
"area",
_scalingService.Scale(element.GetMaterialArea(matId, false), areaUnitType),
_scalingService.Scale(target.GetMaterialArea(matId, false), areaUnitType),
areaUnitType
);
@@ -93,142 +68,57 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
AddMaterialProperty(
materialQuantity,
"volume",
_scalingService.Scale(element.GetMaterialVolume(matId), volumeUnitType),
_scalingService.Scale(target.GetMaterialVolume(matId), volumeUnitType),
volumeUnitType
);
}
}
}
private void ProcessMaterialsByElementTypes(List<DB.ElementId> elementIds, Dictionary<string, object> quantities)
{
Dictionary<DB.ElementId, double> matLengths = new(); // stores mat id to total length found for mat
foreach (DB.ElementId elementId in elementIds)
{
if (
_converterSettings.Current.Document.GetElement(elementId) is DB.Element element
&& _converterSettings.Current.Document.GetElement(element.GetTypeId()) is DB.ElementType elementType
)
{
DB.ElementId elementMatId = DB.ElementId.InvalidElementId;
foreach (DB.Parameter param in elementType.Parameters)
if (_converterSettings.Current.Document.GetElement(matId) is DB.Material material)
{
DB.Definition def = param.Definition;
if (param.StorageType == DB.StorageType.ElementId && def.GetDataType() == DB.SpecTypeId.Reference.Material)
{
elementMatId = param.AsElementId();
break;
}
}
materialQuantity["materialName"] = material.Name;
materialQuantity["materialCategory"] = material.MaterialCategory;
materialQuantity["materialClass"] = material.MaterialClass;
if (elementMatId != DB.ElementId.InvalidElementId)
{
// try get the length from the element
foreach (DB.Parameter eParam in element.Parameters)
// get StructuralAssetId (or try to)
DB.ElementId structuralAssetId = material.StructuralAssetId;
if (structuralAssetId != DB.ElementId.InvalidElementId)
{
DB.Definition eParamDef = eParam.Definition;
var forgeTypeId = eParamDef.GetDataType();
if (forgeTypeId == DB.SpecTypeId.Length)
StructuralAssetProperties structuralAssetProperties = _structuralAssetExtractor.TryGetProperties(
structuralAssetId
);
materialQuantity["structuralAsset"] = structuralAssetProperties.Name;
AddMaterialProperty(
materialQuantity,
"density",
structuralAssetProperties.Density,
structuralAssetProperties.DensityUnitId
);
// more reliable way of determining material type (wood/concrete/type) as it uses Revit enum
// materialClass, materialCategory etc. are user string inputs
materialQuantity["materialType"] = structuralAssetProperties.MaterialType;
// Only add compressive strength for concrete materials (used by F+E for Automate)
if (
structuralAssetProperties.MaterialType == "Concrete"
&& structuralAssetProperties.CompressiveStrength.HasValue
)
{
double length = eParam.AsDouble();
if (matLengths.TryGetValue(elementMatId, out double _))
{
matLengths[elementMatId] += length;
}
else
{
matLengths.Add(elementMatId, length);
}
AddMaterialProperty(
materialQuantity,
"compressiveStrength",
structuralAssetProperties.CompressiveStrength.Value,
structuralAssetProperties.CompressiveStrengthUnitId!
);
}
}
quantities[material.Name] = materialQuantity;
}
}
}
foreach (var entry in matLengths)
{
var materialQuantity = new Dictionary<string, object>();
var unitSettings = _converterSettings.Current.Document.GetUnits();
// add material props
if (TryAddMaterialPropertiesToQuantitiesDict(entry.Key, materialQuantity, out string matName))
{
quantities[matName] = materialQuantity;
// add length prop
var lengthUnitType = unitSettings.GetFormatOptions(DB.SpecTypeId.Length).GetUnitTypeId();
AddMaterialProperty(
materialQuantity,
"length",
_scalingService.Scale(entry.Value, lengthUnitType),
lengthUnitType
);
}
}
}
/// <summary>
/// Adds the material properties (like name, category, and class) to the material quantity dictionary
/// </summary>
/// <param name="matId">the material id</param>
/// <param name="materialQuantity"></param>
/// <param name="matName"></param>
/// <returns>true if material is found, false if not</returns>
private bool TryAddMaterialPropertiesToQuantitiesDict(
DB.ElementId matId,
Dictionary<string, object> materialQuantity,
out string matName
)
{
matName = "";
if (_converterSettings.Current.Document.GetElement(matId) is DB.Material material)
{
materialQuantity["materialName"] = material.Name;
materialQuantity["materialCategory"] = material.MaterialCategory;
materialQuantity["materialClass"] = material.MaterialClass;
// get StructuralAssetId (or try to)
DB.ElementId structuralAssetId = material.StructuralAssetId;
if (structuralAssetId != DB.ElementId.InvalidElementId)
{
StructuralAssetProperties structuralAssetProperties = _structuralAssetExtractor.TryGetProperties(
structuralAssetId
);
materialQuantity["structuralAsset"] = structuralAssetProperties.Name;
AddMaterialProperty(
materialQuantity,
"density",
structuralAssetProperties.Density,
structuralAssetProperties.DensityUnitId
);
// more reliable way of determining material type (wood/concrete/type) as it uses Revit enum
// materialClass, materialCategory etc. are user string inputs
materialQuantity["materialType"] = structuralAssetProperties.MaterialType;
// Only add compressive strength for concrete materials (used by F+E for Automate)
if (
structuralAssetProperties.MaterialType == "Concrete"
&& structuralAssetProperties.CompressiveStrength.HasValue
)
{
AddMaterialProperty(
materialQuantity,
"compressiveStrength",
structuralAssetProperties.CompressiveStrength.Value,
structuralAssetProperties.CompressiveStrengthUnitId!
);
}
}
matName = material.Name;
return true;
}
return false;
return quantities;
}
/// <summary>
@@ -239,7 +129,7 @@ public class MaterialQuantitiesToSpeckleLite : ITypedConverter<DB.Element, Dicti
/// <param name="value">The numeric value of the property</param>
/// <param name="unitId">The Forge type ID representing the units of the property</param>
/// <remarks>
/// Saves code when used repeatedly. Etabs implements an extension method to dicts (see utils folder). May be worth exploring.
/// Saves code when used repeatedbly. Etabs implements an extension method to dicts (see utils folder). May be worth exploring.
/// </remarks>
private void AddMaterialProperty(
Dictionary<string, object> materialQuantity,
@@ -1,12 +1,11 @@
using Microsoft.Extensions.Logging;
using Speckle.Importers.JobProcessor.Domain;
using Speckle.Importers.JobProcessor.Domain;
using Speckle.Importers.Rhino;
using Speckle.Sdk.Api;
using Version = Speckle.Sdk.Api.GraphQL.Models.Version;
namespace Speckle.Importers.JobProcessor.JobHandlers;
internal sealed class RhinoJobHandler(ILogger<RhinoJobHandler> logger) : IJobHandler
internal sealed class RhinoJobHandler : IJobHandler
{
public async Task<Version> ProcessJob(FileimportJob job, IClient client, CancellationToken cancellationToken)
{
@@ -32,23 +31,7 @@ internal sealed class RhinoJobHandler(ILogger<RhinoJobHandler> logger) : IJobHan
}
finally
{
try
{
await Cleanup(directory.FullName);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
logger.LogError(ex, "Failed to cleanup file");
}
Directory.Delete(directory.FullName, true);
}
}
private static async Task Cleanup(string path)
{
//Some weird cases where *something* is keeping a lock on the file, this *may* fix things...
await Task.Delay(100);
GC.Collect();
GC.WaitForPendingFinalizers();
Directory.Delete(path, true);
}
}
@@ -28,10 +28,10 @@ internal sealed class JobProcessorInstance(
{
await using var connection = await repository.SetupConnection(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Listening for jobs...");
while (true)
{
logger.LogInformation("Listening for jobs...");
FileimportJob? job = await repository.GetNextJob(connection, cancellationToken);
if (job == null)
{
@@ -94,7 +94,7 @@ internal sealed class JobProcessorInstance(
projectId = job.Payload.ProjectId,
jobId = job.Payload.BlobId,
warnings = [],
reason = string.IsNullOrEmpty(ex.Message) ? ex.GetType().ToString() : ex.Message,
reason = ex.ToString(),
result = new FileImportResult(0, 0, 0, "Rhino Importer", versionId: null)
};
await client.FileImport.FinishFileImportJob(input, cancellationToken);
@@ -1,34 +0,0 @@
using Speckle.Sdk.SQLite;
namespace Speckle.Importers.Rhino.Internal;
/// <summary>
/// Dummy implementation of <see cref="ISqLiteJsonCacheManager"/> to avoid
/// </summary>
public sealed class DummySqliteJsonCacheManager : ISqLiteJsonCacheManager
{
public void Dispose() { }
public IReadOnlyCollection<(string Id, string Json)> GetAllObjects() => [];
public void DeleteObject(string id) { }
public string? GetObject(string id) => null;
public void SaveObject(string id, string json) { }
public void UpdateObject(string id, string json) { }
public void SaveObjects(IEnumerable<(string id, string json)> items) { }
public bool HasObject(string objectId) => false;
}
public sealed class DummySqliteJsonCacheManagerFactory : ISqLiteJsonCacheManagerFactory
{
private static readonly ISqLiteJsonCacheManager s_instance = new DummySqliteJsonCacheManager();
public ISqLiteJsonCacheManager CreateForUser(string scope) => s_instance;
public ISqLiteJsonCacheManager CreateFromStream(string streamId) => s_instance;
}
@@ -1,4 +1,4 @@
using Rhino;
using Rhino;
using Speckle.Importers.Rhino.Internal.FileTypeConfig;
using Speckle.Sdk;
using Speckle.Sdk.Credentials;
@@ -34,9 +34,10 @@ internal sealed class ImporterInstance(Sender sender)
}
finally
{
//Being a bit extra defensive to ensure we're cleaning up the old doc
//Being a bit extra defensive that we're cleaning up the old doc
RhinoDoc.ActiveDoc?.Dispose();
RhinoDoc.ActiveDoc = null;
GC.Collect();
}
}
@@ -3,7 +3,6 @@ using Speckle.Connectors.Common;
using Speckle.Connectors.Common.Threading;
using Speckle.Connectors.Rhino.DependencyInjection;
using Speckle.Converters.Rhino;
using Speckle.Sdk.SQLite;
namespace Speckle.Importers.Rhino.Internal;
@@ -22,9 +21,6 @@ internal static class ServiceRegistration
// override default thread context
services.AddSingleton<IThreadContext>(new ImporterThreadContext());
// override sqlite cache, since we don't want to persist to disk any object data
services.AddTransient<ISqLiteJsonCacheManagerFactory, DummySqliteJsonCacheManagerFactory>();
return services;
}
}