diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSI.cs b/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSI.cs
index f9053b043c..52bb2ac2ee 100644
--- a/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSI.cs
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSI.cs
@@ -1,4 +1,5 @@
using CSiAPIv1;
+using Objects.BuiltElements;
using Objects.Structural.Analysis;
using Objects.Structural.CSI.Analysis;
using Objects.Structural.CSI.Geometry;
@@ -112,6 +113,7 @@ public bool CanConvertToNative(Base @object)
case Load _:
//case Geometry.Line line:
case Node _:
+ case GridLine _:
//case Model o:
//case Property property:
@@ -211,6 +213,9 @@ public object ConvertToNative(Base @object)
case BuiltElements.Column o:
CurveBasedElementToNative(o, o.baseLine, ref appObj);
break;
+ case GridLine o:
+ GridLineToNative(o);
+ break;
#endregion
default:
appObj.Update(
diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSIShared.projitems b/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSIShared.projitems
index 2c47d7b4cf..f0f03983ad 100644
--- a/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSIShared.projitems
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/ConverterCSIShared.projitems
@@ -16,6 +16,8 @@
+
+
@@ -55,5 +57,6 @@
+
\ No newline at end of file
diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/DatabaseTableWrapper.cs b/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/DatabaseTableWrapper.cs
new file mode 100644
index 0000000000..b95f3a624c
--- /dev/null
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/DatabaseTableWrapper.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using ConverterCSIShared.Services;
+using CSiAPIv1;
+
+namespace ConverterCSIShared.Models
+{
+ internal abstract class DatabaseTableWrapper
+ {
+ public abstract string TableKey { get; }
+ protected readonly cSapModel cSapModel;
+ protected readonly ToNativeScalingService toNativeScalingService;
+ private int tableVersion;
+ protected string[] fieldKeysIncluded;
+ protected int numRecords;
+ private readonly List tableData;
+
+ protected DatabaseTableWrapper(cSapModel cSapModel, ToNativeScalingService toNativeScalingService)
+ {
+ this.cSapModel = cSapModel;
+ this.toNativeScalingService = toNativeScalingService;
+ this.tableData = new List(GetTableData());
+ }
+ private string[] GetTableData()
+ {
+ var tableData = Array.Empty();
+ cSapModel.DatabaseTables.GetTableForEditingArray(
+ TableKey,
+ "this param does nothing",
+ ref tableVersion,
+ ref fieldKeysIncluded,
+ ref numRecords,
+ ref tableData
+ );
+ return tableData;
+ }
+
+ protected void AddRow(params string[] arguments)
+ {
+ if (arguments.Length != fieldKeysIncluded.Length)
+ {
+ throw new ArgumentException($"Method {nameof(AddRow)} was passed an array of length {arguments.Length}, but was expecting an array of length {fieldKeysIncluded.Length}");
+ }
+ tableData.AddRange(arguments);
+ numRecords++;
+ }
+
+ public void ApplyEditedTables()
+ {
+ var tableDataArray = tableData.ToArray();
+ cSapModel.DatabaseTables.SetTableForEditingArray(TableKey, ref tableVersion, ref fieldKeysIncluded, numRecords, ref tableDataArray);
+
+ int numFatalErrors = 0;
+ int numWarnMsgs = 0;
+ int numInfoMsgs = 0;
+ int numErrorMsgs = 0;
+ string importLog = "";
+ cSapModel.DatabaseTables.ApplyEditedTables(false, ref numFatalErrors, ref numErrorMsgs, ref numWarnMsgs, ref numInfoMsgs, ref importLog);
+ }
+ }
+}
diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/ETABSGridLineDefinitionTable.cs b/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/ETABSGridLineDefinitionTable.cs
new file mode 100644
index 0000000000..36ee8566e9
--- /dev/null
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/Models/ETABSGridLineDefinitionTable.cs
@@ -0,0 +1,245 @@
+#nullable enable
+using System;
+using ConverterCSIShared.Services;
+using CSiAPIv1;
+using Objects.BuiltElements;
+using Objects.Geometry;
+using Objects.Other;
+using Speckle.Core.Logging;
+
+namespace ConverterCSIShared.Models
+{
+ ///
+ /// Encapsulates the logic dealing with creating and manipulating gridlines via the interactive database in ETABS
+ ///
+ internal class ETABSGridLineDefinitionTable : DatabaseTableWrapper
+ {
+ private const double gridTolerance = .001; // .05 degrees as radians
+ public override string TableKey => "Grid Definitions - Grid Lines";
+ public static string?[] DefaultRow => new string?[] {
+ null, // Name : Grid System name
+ null, // LineType
+ null, // Id : Grid name
+ null, // Ordinate : Offset in the positive direction
+ null, // Angle : clockwise offset in degrees for polar (cylindrical) coordinates
+ null, // X1
+ null, // Y1
+ null, // X2
+ null, // Y2
+ "Start", // BubbleLoc
+ "Yes", // Visible
+ };
+ public ETABSGridLineDefinitionTable(cSapModel cSapModel, ToNativeScalingService toNativeScalingService)
+ : base(cSapModel, toNativeScalingService) { }
+
+ public const string XGridLineType = "X (Cartesian)";
+ public const string YGridLineType = "Y (Cartesian)";
+ public const string RGridLineType = "R (Cylindrical)";
+ public const string TGridLineType = "T (Cylindrical)";
+
+ ///
+ /// Add a gridline that is represented by a straight line to the ETABS document
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void AddCartesian(
+ string gridSystemName,
+ string gridLineType,
+ string gridName,
+ double location,
+ string visible = "Yes"
+ )
+ {
+ if (gridLineType != XGridLineType && gridLineType != YGridLineType)
+ {
+ throw new ArgumentException($"Argument gridLineType must be either {XGridLineType} or {YGridLineType}");
+ }
+ var newRow = new string?[fieldKeysIncluded.Length];
+ for (var index = 0; index < fieldKeysIncluded.Length; index++)
+ {
+ newRow[index] = fieldKeysIncluded[index] switch
+ {
+ "Name" => gridSystemName,
+ "LineType" => gridLineType,
+ "ID" => gridName,
+ "Ordinate" => location.ToString(),
+ "Visible" => visible,
+ _ => DefaultRow[index],
+ };
+ }
+
+ AddRow(newRow);
+ }
+
+ ///
+ /// Add a gridline that is represented by a straight line to the ETABS document
+ ///
+ ///
+ ///
+ ///
+ public void AddCartesian(GridLine gridLine)
+ {
+ if (gridLine.baseLine is not Line line)
+ {
+ throw new ArgumentException("Non line based gridlines are not supported");
+ }
+
+ var gridRotation = GetAngleOffsetFromGlobalCoordinateSystem(line);
+
+ var gridSystem = GetExistingGridSystem(gridRotation) ?? CreateGridSystem(gridRotation);
+ var transform = GetTransformFromGridSystem(gridSystem);
+ _ = line.TransformTo(transform.Inverse(), out Line transformedLine);
+
+ var newUx = Math.Abs(transformedLine.start.x - transformedLine.end.x);
+ var newUy = Math.Abs(transformedLine.start.y - transformedLine.end.y);
+
+ string lineType;
+ double gridLineOffset;
+ if (newUx < gridTolerance)
+ {
+ lineType = XGridLineType;
+ gridLineOffset = toNativeScalingService
+ .ScaleLength(transformedLine.start.x, transformedLine.units ?? transformedLine.start.units);
+ }
+ else if (newUy < gridTolerance)
+ {
+ lineType = YGridLineType;
+ gridLineOffset = toNativeScalingService
+ .ScaleLength(transformedLine.start.y, transformedLine.units ?? transformedLine.start.units);
+ }
+ else
+ {
+ throw new SpeckleException($"Error in transforming line from global coordinates to grid system with rotation {gridSystem.Rotation} and x,y offsets {gridSystem.XOrigin}, {gridSystem.YOrigin}");
+ }
+
+ AddCartesian(gridSystem.Name, lineType, gridLine.label, gridLineOffset);
+ }
+
+ ///
+ /// Returns the rotation counter-clockwise from from the global x axis in radians
+ ///
+ ///
+ ///
+ private static double GetAngleOffsetFromGlobalCoordinateSystem(Line line)
+ {
+ var ux = line.start.x - line.end.x;
+ var uy = line.start.y - line.end.y;
+
+ if (Math.Abs(ux) < gridTolerance)
+ {
+ return Math.PI / 2;
+ }
+ return Math.Atan(uy / ux);
+ }
+
+ ///
+ /// Find a GridSystem in the CSi model whose local x axis is either parallel or perpendicular to the provided
+ /// grid angle.
+ ///
+ /// Rotation counter-clockwise from the global x axis in radians
+ ///
+ private GridSystemRepresentation? GetExistingGridSystem(double gridRotation)
+ {
+ var numGridSys = 0;
+ var gridSysNames = Array.Empty();
+ this.cSapModel.GridSys.GetNameList(ref numGridSys, ref gridSysNames);
+
+ var xOrigin = 0.0;
+ var yOrigin = 0.0;
+ var rotationDeg = 0.0;
+ foreach (var gridSysName in gridSysNames)
+ {
+ var success = cSapModel.GridSys.GetGridSys(gridSysName, ref xOrigin, ref yOrigin, ref rotationDeg);
+ if (success != 0)
+ {
+ // something went wrong. This is not necessarily unexpected or bad
+ continue;
+ }
+
+ var rotationRad = rotationDeg * Math.PI / 180;
+ var combinedRotationsNormalized = Math.Abs((rotationRad - gridRotation) / (Math.PI / 2));
+ var combinedRotationsRemainder = combinedRotationsNormalized - Math.Floor(combinedRotationsNormalized);
+
+ if (combinedRotationsRemainder < gridTolerance || combinedRotationsRemainder > 1 - gridTolerance)
+ {
+ return new GridSystemRepresentation(gridSysName, xOrigin, yOrigin, rotationRad);
+ }
+ }
+ // could not find compatible existing grid system
+ return null;
+ }
+
+ private GridSystemRepresentation CreateGridSystem(double gridRotation)
+ {
+ var systemName = GetUniqueGridSystemName();
+ _ = cSapModel.GridSys.SetGridSys(systemName, 0, 0, gridRotation * 180 / Math.PI);
+
+ // when a grid system is created, it doesn't show up unless it has at least one grid in each direction
+ AddCartesian(systemName, XGridLineType, "Default0", 0, "No");
+ AddCartesian(systemName, YGridLineType, "Default1", 0, "No");
+ return new GridSystemRepresentation(systemName, 0, 0, gridRotation);
+ }
+
+ ///
+ /// Returns a unique name to be used for a new speckle-created grid system.
+ ///
+ ///
+ private string GetUniqueGridSystemName()
+ {
+ var numGridSys = 0;
+ var gridSysNames = Array.Empty();
+ this.cSapModel.GridSys.GetNameList(ref numGridSys, ref gridSysNames);
+
+ var numberOfGridSystems = 0;
+ var gridSystemNamePrefix = "SpeckleGridSystem";
+ foreach (var gridSysName in gridSysNames)
+ {
+ // test if this grid system is one that we already created. If it is, then we need to adjust our
+ // numberOfGridSystems so that if we do end up creating a new one, it doesn't override an existing one.
+ if (!gridSysName.StartsWith(gridSystemNamePrefix))
+ {
+ continue;
+ }
+
+ if (int.TryParse(gridSysName.Replace(gridSystemNamePrefix, ""), out int gridSysNum))
+ {
+ numberOfGridSystems = Math.Max(numberOfGridSystems, gridSysNum + 1);
+ }
+ }
+ return $"{gridSystemNamePrefix}{numberOfGridSystems}"; ;
+ }
+
+ private static Transform GetTransformFromGridSystem(GridSystemRepresentation sys)
+ {
+ return new Transform(
+ new double[]
+ {
+ Math.Cos(sys.Rotation), -Math.Sin(sys.Rotation), 0, sys.XOrigin,
+ Math.Sin(sys.Rotation), Math.Cos(sys.Rotation), 0, sys.YOrigin,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1
+ }
+ );
+ }
+ }
+
+ public class GridSystemRepresentation
+ {
+ public GridSystemRepresentation(string name, double xOrigin, double yOrigin, double rotation)
+ {
+ Name = name;
+ XOrigin = xOrigin;
+ YOrigin = yOrigin;
+ Rotation = rotation;
+ }
+
+ public string Name { get; }
+ public double XOrigin { get; set; }
+ public double YOrigin { get; set; }
+ public double Rotation { get; set; }
+ }
+}
diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/Partial Classes/Geometry/ConvertGridLines.cs b/Objects/Converters/ConverterCSI/ConverterCSIShared/Partial Classes/Geometry/ConvertGridLines.cs
index d1d022975f..b11fd91f98 100644
--- a/Objects/Converters/ConverterCSI/ConverterCSIShared/Partial Classes/Geometry/ConvertGridLines.cs
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/Partial Classes/Geometry/ConvertGridLines.cs
@@ -1,22 +1,20 @@
-using System;
+using System;
using System.Collections.Generic;
using Objects.Geometry;
-using Objects.Structural.Geometry;
-using Objects.Structural.Analysis;
-using Speckle.Core.Models;
using Objects.Structural.CSI.Geometry;
-using Objects.Structural.CSI.Properties;
using Objects.BuiltElements;
-using System.Linq;
-using CSiAPIv1;
+using ConverterCSIShared.Models;
namespace Objects.Converter.CSI
{
public partial class ConverterCSI
{
- public void gridLinesToNative(CSIGridLines gridlines)
+ private ETABSGridLineDefinitionTable gridLineDefinitionTable;
+ private ETABSGridLineDefinitionTable GridLineDefinitionTable => gridLineDefinitionTable ??= new(Model, new(Model));
+ public void GridLineToNative(GridLine gridline)
{
- throw new NotSupportedException();
+ GridLineDefinitionTable.AddCartesian(gridline);
+ GridLineDefinitionTable.ApplyEditedTables();
}
public CSIGridLines gridLinesToSpeckle(string name)
diff --git a/Objects/Converters/ConverterCSI/ConverterCSIShared/Services/ToNativeScalingService.cs b/Objects/Converters/ConverterCSI/ConverterCSIShared/Services/ToNativeScalingService.cs
new file mode 100644
index 0000000000..21951aeaa6
--- /dev/null
+++ b/Objects/Converters/ConverterCSI/ConverterCSIShared/Services/ToNativeScalingService.cs
@@ -0,0 +1,35 @@
+#nullable enable
+using System;
+using CSiAPIv1;
+using Speckle.Core.Kits;
+
+namespace ConverterCSIShared.Services
+{
+ public class ToNativeScalingService
+ {
+ public ToNativeScalingService(cSapModel cSapModel)
+ {
+ var unitsArray = cSapModel.GetPresentUnits().ToString().Split('_');
+
+ ForceUnits = unitsArray[0];
+ LengthUnits = unitsArray[1];
+ TempuratureUnits = unitsArray[2];
+ }
+
+ public string LengthUnits { get; private set; }
+ public string ForceUnits { get; private set; }
+ public string TempuratureUnits { get; private set; }
+
+ ///
+ /// Scales a value from a length unit of a specified power to the native length unit to the same power
+ ///
+ ///
+ ///
+ ///
+ ///
+ public double ScaleLength(double value, string speckleUnits, int power = 1)
+ {
+ return value * Math.Pow(Units.GetConversionFactor(speckleUnits, LengthUnits), power);
+ }
+ }
+}
diff --git a/Objects/Objects/Other/Transform.cs b/Objects/Objects/Other/Transform.cs
index 3ca5938e07..2a1b130365 100644
--- a/Objects/Objects/Other/Transform.cs
+++ b/Objects/Objects/Other/Transform.cs
@@ -214,6 +214,15 @@ public double[] ConvertToUnits(string newUnits)
};
}
+ public Transform Inverse()
+ {
+ if (Matrix4x4.Invert(matrix, out var transformed))
+ {
+ return new Transform(transformed);
+ }
+ throw new SpeckleException("Could not create inverse transform");
+ }
+
///
/// Returns the matrix that results from multiplying two matrices together.
///