From de5d858277b4139fb3a8c11f0b5c68d7438d25b9 Mon Sep 17 00:00:00 2001 From: connorivy <43247197+connorivy@users.noreply.github.com> Date: Fri, 29 Sep 2023 09:52:54 -0500 Subject: [PATCH] Feat(ETABS) : receive gridlines (#2969) * wip gridline receiving * creates grids if grid system is existing * code cleanup * appears to be working * small logic improvements * move create grid to separate method * adjust tolerances * added documentation * rename file * don't override existing grid system properties --------- Co-authored-by: Connor Ivy --- .../ConverterCSIShared/ConverterCSI.cs | 5 + .../ConverterCSIShared.projitems | 3 + .../Models/DatabaseTableWrapper.cs | 61 +++++ .../Models/ETABSGridLineDefinitionTable.cs | 245 ++++++++++++++++++ .../Geometry/ConvertGridLines.cs | 16 +- .../Services/ToNativeScalingService.cs | 35 +++ Objects/Objects/Other/Transform.cs | 9 + 7 files changed, 365 insertions(+), 9 deletions(-) create mode 100644 Objects/Converters/ConverterCSI/ConverterCSIShared/Models/DatabaseTableWrapper.cs create mode 100644 Objects/Converters/ConverterCSI/ConverterCSIShared/Models/ETABSGridLineDefinitionTable.cs create mode 100644 Objects/Converters/ConverterCSI/ConverterCSIShared/Services/ToNativeScalingService.cs 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. ///