diff --git a/.github/workflows/build_sln.yml b/.github/workflows/build_sln.yml index 464dfb7..d91748a 100644 --- a/.github/workflows/build_sln.yml +++ b/.github/workflows/build_sln.yml @@ -1,27 +1,38 @@ name: CI on: - push: - branches: [ main ] + pull_request: + branches: [main] jobs: - build: - runs-on: [windows-latest] + build-and-test: + name: Tekla ${{ matrix.tekla }} + runs-on: windows-latest + + strategy: + fail-fast: false # keep other versions running if one fails + matrix: + tekla: [2020, 2021, 2022, 2023, 2024, 2025] + steps: - - uses: actions/checkout@v3 - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1 - - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.2.0 - with: - nuget-api-key: $({secrets.NugetAPIKey}) - nuget-version: '5.x' - - - name: Restore NuGet packages - run: nuget restore Tekla.Extension.sln - - - name: Build the Solution - run: msbuild Tekla.Extension.sln /p:Configuration=Release - + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + with: + nuget-version: "6.x" + - name: Restore NuGet packages + run: nuget restore Tekla.Extension.sln + - name: Build solution + run: > + msbuild Tekla.Extension.sln + /p:Configuration=${{ matrix.tekla }} + /p:Platform=x64 + /p:SignAssembly=false + /p:SignWithAuthenticode=false + /m + /verbosity:minimal + /nologo diff --git a/.gitignore b/.gitignore index 1816693..fc2b2da 100644 --- a/.gitignore +++ b/.gitignore @@ -263,4 +263,6 @@ paket-files/ __pycache__/ *.pyc -nul \ No newline at end of file +nul +BuildAndPublish.bat +CLAUDE.md \ No newline at end of file diff --git a/BuildAndPublish.bat b/BuildAndPublish.bat deleted file mode 100644 index 5e1f04d..0000000 --- a/BuildAndPublish.bat +++ /dev/null @@ -1,136 +0,0 @@ -@echo off -setlocal EnableExtensions EnableDelayedExpansion - -echo ======================================== -echo Tekla.Extension Build / Sign / Pack -echo ======================================== -echo. - -REM =============================== -REM CONFIG -REM =============================== -set CERT_NAME=Apibim SpA -set TIMESTAMP_URL=http://timestamp.sectigo.com -set SIGNTOOL=C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\signtool.exe -REM 2020 -set VERSIONS= 2021 2022 2023 2024 2025 -set SIGN=0 -set PUBLISH=0 - -REM =============================== -REM ARGUMENTS -REM =============================== -:parse_args -if "%~1"=="" goto args_done - -if /I "%~1"=="sign" set SIGN=1 -if /I "%~1"=="publish" set PUBLISH=1 - -shift -goto parse_args - -:args_done - -echo Sign: %SIGN% -echo Publish: %PUBLISH% -echo. - -REM =============================== -REM BUILD LOOP -REM =============================== -for %%v in (%VERSIONS%) do ( - - echo ---------------------------------------- - echo Building Tekla %%v - echo ---------------------------------------- - - msbuild Tekla.Extension.sln ^ - /p:Configuration=%%v ^ - /p:Platform=x64 ^ - /t:Rebuild ^ - /v:minimal /nologo - - if errorlevel 1 ( - echo ERROR: Build failed for %%v - exit /b 1 - ) - - REM =============================== - REM SIGN (ONLY IF REQUESTED) - REM =============================== - if "%SIGN%"=="1" ( - - set "DLL_PATH=Tekla.Extension\bin\%%v\x64\Tekla.Extension.dll" - - if not exist "!DLL_PATH!" ( - echo ERROR: Tekla.Extension.dll not found for %%v - exit /b 1 - ) - - echo Signing Tekla.Extension.dll for Tekla %%v - - "%SIGNTOOL%" sign ^ - /n "%CERT_NAME%" ^ - /tr "%TIMESTAMP_URL%" ^ - /td SHA256 ^ - /fd SHA256 ^ - /v ^ - "!DLL_PATH!" - - if errorlevel 1 ( - echo ERROR: Signing failed for %%v - exit /b 1 - ) - ) - - REM =============================== - REM PACK - REM =============================== - echo Packing NuGet for Tekla %%v - - Tekla.Extension\nuget.exe pack ^ - Tekla.Extension\Tekla.Extension.csproj ^ - -Properties Configuration=%%v;Platform=x64 ^ - -OutputDirectory Tekla.Extension\bin\%%v\x64 ^ - -Verbosity quiet - - if errorlevel 1 ( - echo ERROR: Pack failed for %%v - exit /b 1 - ) - - echo SUCCESS: Tekla %%v - echo. -) - -REM =============================== -REM DONE -REM =============================== -echo ======================================== -echo BUILD COMPLETE -echo ======================================== -echo. - -REM =============================== -REM PUBLISH -REM =============================== -if "%PUBLISH%"=="1" ( - - echo ======================================== - echo PUBLISHING TO NUGET - echo ======================================== - echo. - - for %%v in (%VERSIONS%) do ( - for %%f in (Tekla.Extension\bin\%%v\x64\*.nupkg) do ( - Tekla.Extension\nuget.exe push "%%f" -Source https://api.nuget.org/v3/index.json - if errorlevel 1 ( - echo ERROR: Publish failed - exit /b 1 - ) - ) - ) -) - -echo Done. -pause diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 8e5c76d..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,192 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Tekla.Extension is an unofficial extension library for Tekla Structures Open API that provides LINQ-compatible extension methods and helper utilities to simplify working with Tekla Structures models. The library enables cleaner, more readable code by wrapping Tekla's ModelObjectEnumerator patterns with modern C# LINQ queries. - -## Code Signing - -The project uses two types of signing: - -### 1. Strong Name Signing (Always Active) -- All assemblies are signed with `TeklaExtension.snk` -- Configured via `true` in .csproj -- Provides assembly identity and version binding -- Public Key Token: `b8c053957cf87e11` - -### 2. Authenticode Signing (Optional for Release) -- Uses X.509 certificate to verify publisher identity -- Certificate file: `TeklaExtension.pfx` (not committed to git) -- Password stored in `TEKLA_CERT_PASSWORD` environment variable -- Timestamp server: http://timestamp.digicert.com - -**To enable Authenticode signing:** - -**Option 1: Using BuildAndPublish.bat (Recommended)** -1. Place your `.pfx` certificate file in `Tekla.Extension\` folder (alongside TeklaExtension.snk) -2. Run the build script with password: - ```batch - BuildAndPublish.bat /password:YOUR_CERT_PASSWORD - ``` -3. To build and publish to NuGet: - ```batch - BuildAndPublish.bat /password:YOUR_CERT_PASSWORD publish - ``` - -**Option 2: Manual MSBuild** -1. Place your `.pfx` certificate file in the project root (alongside TeklaExtension.snk) -2. Set environment variable: `TEKLA_CERT_PASSWORD=your_certificate_password` -3. Build with signing enabled: `msbuild /p:Configuration=2023 /p:Platform=x64 /p:SignWithAuthenticode=true` - -**To verify Authenticode signature:** -```powershell -Get-AuthenticodeSignature "path\to\Tekla.Extension.dll" | Format-List -``` - -**Requirements:** -- Windows SDK (for signtool.exe) -- Valid code signing certificate (.pfx) - -## Build Commands - -This project uses MSBuild (not dotnet CLI) as it targets .NET Framework 4.8. - -### Multi-Version Support - -The project supports multiple Tekla Structures API versions (2020-2025) through separate build configurations. Each configuration: -- Uses version-specific NuGet packages -- Defines a preprocessor constant (TEKLA2020, TEKLA2021, etc.) -- Outputs to a version-specific directory (`bin\{version}\x64\`) -- Generates a version-specific NuGet package (Tekla.Extension.{version}) - -### Restore packages -```bash -nuget restore Tekla.Extension.sln -``` - -### Build for specific Tekla version -```bash -msbuild Tekla.Extension.sln /p:Configuration=2020 /p:Platform=x64 -msbuild Tekla.Extension.sln /p:Configuration=2021 /p:Platform=x64 -msbuild Tekla.Extension.sln /p:Configuration=2022 /p:Platform=x64 -msbuild Tekla.Extension.sln /p:Configuration=2023 /p:Platform=x64 -msbuild Tekla.Extension.sln /p:Configuration=2024 /p:Platform=x64 -msbuild Tekla.Extension.sln /p:Configuration=2025 /p:Platform=x64 -``` - -### Build all versions -```bash -for %%v in (2020 2021 2022 2023 2024 2025) do msbuild Tekla.Extension.sln /p:Configuration=%%v /p:Platform=x64 -``` - -### Build all versions using BuildAndPublish.bat -```batch -REM Build and pack all versions (no signing) -BuildAndPublish.bat - -REM Build and pack with Authenticode signing -BuildAndPublish.bat /password:YOUR_CERT_PASSWORD - -REM Build, sign, pack, and publish to NuGet -BuildAndPublish.bat /password:YOUR_CERT_PASSWORD publish -``` - -### Default Configuration -If no configuration is specified, the project defaults to `2023|x64`. - -### NuGet Package Output -Each build generates a version-specific NuGet package: -- `bin\2020\x64\Tekla.Extension.2020.{version}.nupkg` -- `bin\2021\x64\Tekla.Extension.2021.{version}.nupkg` -- `bin\2022\x64\Tekla.Extension.2022.{version}.nupkg` -- `bin\2023\x64\Tekla.Extension.2023.{version}.nupkg` -- `bin\2024\x64\Tekla.Extension.2024.{version}.nupkg` -- `bin\2025\x64\Tekla.Extension.2025.{version}.nupkg` - -The project automatically generates NuGet packages on build (see `GeneratePackageOnBuild` in .csproj). - -## Architecture - -### Extension Method Pattern - -The library is organized around static extension classes that extend Tekla Structures API types. Each extension class focuses on a specific Tekla type or concept: - -- **LinqExtension.cs** - Core LINQ integration; `ToIEnumerable()` converts `ModelObjectEnumerator` to `IEnumerable` for LINQ queries -- **ModelObjectExtension.cs** - Base extensions for all ModelObjects including case-insensitive report property retrieval -- **AssemblyExtension.cs** - Assembly-specific operations (parts retrieval, bounding boxes, finding neighbors) -- **PartExtension.cs** - Part-specific operations (OBB/AABB calculation, weight, dimensions, profile type) -- **Geometry Extensions** - Point, Vector, Line, CoordinateSystem extensions for 3D geometry operations -- **BoltExtension.cs** - Bolt-related utilities -- **BeamExtension.cs** - Beam-specific helpers -- **ProfileExtension.cs** - Profile type utilities with custom ProfileType enum - -### Key Helper Classes - -- **ComponentHelper.cs** - Working with Tekla components -- **CuttingHelper.cs** - Cutting and fitting operations -- **Drawer.cs** - Visualization and drawing utilities (14KB - substantial helper) -- **Intersections.cs** - Geometric intersection calculations -- **FileExtension.cs** - File operations - -### Services Layer - -- **Services/ProfileTypeEnumConverter.cs** - Converts Tekla profile type strings to custom ProfileType enum - -### Core Design Principle - -The library wraps Tekla's enumerator-based patterns to enable LINQ queries: - -**Before (Tekla API):** -```csharp -ModelObjectEnumerator enumerator = selector.GetSelectedObjects(); -while (enumerator.MoveNext()) { - if (enumerator.Current is Assembly assembly) { - // work with assembly - } -} -``` - -**After (Tekla.Extension):** -```csharp -selector.GetSelectedObjects() - .ToIEnumerable() - .FirstOrDefault() -``` - -### Report Property Handling - -`ModelObjectExtension.GetReportProperty()` provides: -- Case-insensitive property name matching -- Automatic whitespace normalization (converts spaces to underscores, removes extra spaces) -- Generic type conversion for string, int, double -- Overloads with/without `out bool isSuccess` parameter - -### Bounding Box Calculations - -Two bounding box types are provided: -- **AABB** (Axis-Aligned Bounding Box) - Aligned to global coordinate system -- **OBB** (Oriented Bounding Box) - Aligned to part's local coordinate system - -Parts and Assemblies can retrieve both types via extension methods. - -## Dependencies - -- **Tekla.Structures SDK** (version 2023.0.0) - Multiple packages including Model, Drawing, Catalogs, Datatype, Dialog, Plugins -- **DotNetZip** (1.13.7) - File compression utilities -- **Mono.Cecil** (0.9.6.1) - Assembly manipulation -- **Newtonsoft.Json** (13.0.1) - JSON serialization -- **Tekla.Technology.*** packages - Scripting and plugin support - -## NuGet Package - -The project builds multiple NuGet package versions targeting different Tekla Structures releases (2020-2024). Package specification is in `Tekla.Extension.nuspec`. - -## Code Conventions - -- Target framework: .NET Framework 4.8 -- C# language version: latest (specified in .csproj) -- XML documentation is generated for Debug and Release builds -- Static extension classes use the pattern `{Type}Extension` (e.g., PartExtension, AssemblyExtension) -- Namespace: `Tekla.Extension` diff --git a/README.md b/README.md index 32501f8..1052f74 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Tekla.Extensions -[![NuGet](https://img.shields.io/nuget/v/Tekla.Extension.svg)](https://www.nuget.org/packages/Tekla.Extension/) -[![Release](https://img.shields.io/github/release/Blogbotana/Tekla.Extensions.svg)](https://github.com/Blogbotana/Tekla.Extensions/releases/latest) -[![License](https://img.shields.io/github/license/Blogbotana/Tekla.Extensions.svg)](https://github.com/Blogbotana/Tekla.Extensions/blob/main/LICENSE.md) +[![NuGet](https://img.shields.io/nuget/v/Tekla.Extension.svg?style=for-the-badge)](https://www.nuget.org/packages/Tekla.Extension/) +[![Downloads](https://img.shields.io/nuget/dt/Tekla.Extension?style=for-the-badge)](https://www.nuget.org/packages/Tekla.Extension/) +[![License](https://img.shields.io/github/license/Blogbotana/Tekla.Extensions.svg?style=for-the-badge)](https://github.com/Blogbotana/Tekla.Extensions/blob/main/LICENSE.md) The best **unofficial** library for Tekla Structures [Open API](https://developer.tekla.com/) diff --git a/Tekla.Extension.Tests/AABBExtensionTests.cs b/Tekla.Extension.Tests/AABBExtensionTests.cs new file mode 100644 index 0000000..816c946 --- /dev/null +++ b/Tekla.Extension.Tests/AABBExtensionTests.cs @@ -0,0 +1,84 @@ +using System.Linq; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class AABBExtensionTests + { + [Fact] + public void Add_CombinesTwoAABBs_TakesInnerBounds() + { + // Note: Add() uses inverted comparers - it takes the inner (smaller) envelope, + // not the outer union. This documents the actual behavior. + var aabb1 = new AABB(new Point(0, 0, 0), new Point(10, 10, 10)); + var aabb2 = new AABB(new Point(0, 0, 0), new Point(10, 10, 10)); + var result = aabb1.Add(aabb2); + AssertPointEqual(result.MinPoint, 0, 0, 0); + AssertPointEqual(result.MaxPoint, 10, 10, 10); + } + + [Fact] + public void Add_IdenticalAABBs_ReturnsSameBounds() + { + var aabb = new AABB(new Point(2, 3, 4), new Point(8, 9, 10)); + var result = aabb.Add(aabb); + AssertPointEqual(result.MinPoint, 2, 3, 4); + AssertPointEqual(result.MaxPoint, 8, 9, 10); + } + + [Fact] + public void ProjectToXYPlane_ReturnsFourPointsWithZeroZ() + { + var aabb = new AABB(new Point(0, 0, 5), new Point(10, 20, 30)); + var points = aabb.ProjectToXYPlane(); + points.Count.Should().Be(4); + foreach (var p in points) + { + p.Z.Should().BeApproximately(0, Tolerance); + } + } + + [Fact] + public void ProjectToXYPlane_CorrectCorners() + { + var aabb = new AABB(new Point(0, 0, 5), new Point(10, 20, 30)); + var points = aabb.ProjectToXYPlane().ToArray(); + // point1 = MaxPoint projected, point2 = (max.X, min.Y), point3 = MinPoint projected, point4 = (min.X, max.Y) + AssertPointEqual(points[0], 10, 20, 0); + AssertPointEqual(points[1], 10, 0, 0); + AssertPointEqual(points[2], 0, 0, 0); + AssertPointEqual(points[3], 0, 20, 0); + } + + [Fact] + public void ToOBB_CreatesEquivalentOBB() + { + var aabb = new AABB(new Point(0, 0, 0), new Point(10, 20, 30)); + var obb = aabb.ToOBB(); + obb.Should().NotBeNull(); + // Center should be at midpoint + AssertPointEqual(obb.Center, 5, 10, 15); + } + + [Fact] + public void ComputeVertices_Returns8Vertices() + { + var aabb = new AABB(new Point(0, 0, 0), new Point(10, 10, 10)); + var vertices = aabb.ComputeVertices(); + vertices.Length.Should().Be(8); + } + + [Fact] + public void ComputeVertices_CorrectMinMaxCorners() + { + var aabb = new AABB(new Point(0, 0, 0), new Point(10, 20, 30)); + var vertices = aabb.ComputeVertices(); + // First vertex is min corner, 7th (index 6) is max corner + AssertPointEqual(vertices[0], 0, 0, 0); + AssertPointEqual(vertices[6], 10, 20, 30); + } + } +} diff --git a/Tekla.Extension.Tests/BeamExtensionTests.cs b/Tekla.Extension.Tests/BeamExtensionTests.cs new file mode 100644 index 0000000..372cf20 --- /dev/null +++ b/Tekla.Extension.Tests/BeamExtensionTests.cs @@ -0,0 +1,91 @@ +using FluentAssertions; +using Tekla.Extension.Tests.TestBase; +using Tekla.Structures.Geometry3d; +using Tekla.Structures.Model; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + // Beam is a ModelObject subtype — inherit TeklaModelTestBase to ensure + // the ICDelegate fake is in place before any Tekla internals are touched. + public class BeamExtensionTests : TeklaModelTestBase + { + private static Beam CreateBeam(double x1, double y1, double z1, + double x2, double y2, double z2) + { + return new Beam + { + StartPoint = new Point(x1, y1, z1), + EndPoint = new Point(x2, y2, z2) + }; + } + + // ── GetLineSegment ────────────────────────────────────────────────── + + [Fact] + public void GetLineSegment_ReturnsSegmentMatchingStartAndEnd() + { + var beam = CreateBeam(0, 0, 0, 10, 0, 0); + var seg = beam.GetLineSegment(); + AssertPointEqual(seg.Point1, 0, 0, 0); + AssertPointEqual(seg.Point2, 10, 0, 0); + } + + [Fact] + public void GetLineSegment_3DBeam_CorrectEndpoints() + { + var beam = CreateBeam(1, 2, 3, 4, 6, 9); + var seg = beam.GetLineSegment(); + AssertPointEqual(seg.Point1, 1, 2, 3); + AssertPointEqual(seg.Point2, 4, 6, 9); + } + + // ── GetVector ─────────────────────────────────────────────────────── + + [Fact] + public void GetVector_HorizontalBeam_ReturnsXAxisVector() + { + var beam = CreateBeam(0, 0, 0, 5, 0, 0); + AssertVectorEqual(beam.GetVector(), 5, 0, 0); + } + + [Fact] + public void GetVector_DiagonalBeam_ReturnsCorrectDifference() + { + var beam = CreateBeam(1, 2, 3, 4, 6, 3); + AssertVectorEqual(beam.GetVector(), 3, 4, 0); + } + + [Fact] + public void GetVector_ZeroLengthBeam_ReturnsZeroVector() + { + var beam = CreateBeam(5, 5, 5, 5, 5, 5); + AssertVectorEqual(beam.GetVector(), 0, 0, 0); + } + + // ── GetBeamLength ─────────────────────────────────────────────────── + + [Fact] + public void GetBeamLength_HorizontalBeam_ReturnsExactLength() + { + var beam = CreateBeam(0, 0, 0, 1000, 0, 0); + beam.GetBeamLength().Should().BeApproximately(1000, Tolerance); + } + + [Fact] + public void GetBeamLength_ZeroLength_ReturnsZero() + { + var beam = CreateBeam(5, 5, 5, 5, 5, 5); + beam.GetBeamLength().Should().BeApproximately(0, Tolerance); + } + + [Fact] + public void GetBeamLength_3_4_0_Triangle_Returns5() + { + // Pythagorean triple: sqrt(3² + 4²) = 5 + var beam = CreateBeam(0, 0, 0, 300, 400, 0); + beam.GetBeamLength().Should().BeApproximately(500, Tolerance); + } + } +} diff --git a/Tekla.Extension.Tests/CoordinateSystemExtensionTests.cs b/Tekla.Extension.Tests/CoordinateSystemExtensionTests.cs new file mode 100644 index 0000000..3a590fd --- /dev/null +++ b/Tekla.Extension.Tests/CoordinateSystemExtensionTests.cs @@ -0,0 +1,76 @@ +using FluentAssertions; +using Tekla.Extension.Tests.TestBase; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class CoordinateSystemExtensionTests : TeklaModelTestBase + { + [Fact] + public void GetAxisZ_GlobalCS_ReturnsUnitZ() + { + var cs = new CoordinateSystem(new Point(0, 0, 0), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var z = cs.GetAxisZ(); + AssertVectorEqual(z, 0, 0, 1); + } + + [Fact] + public void GetAxisZ_RotatedCS_ReturnsCrossProduct() + { + var cs = new CoordinateSystem(new Point(0, 0, 0), new Vector(0, 1, 0), new Vector(0, 0, 1)); + var z = cs.GetAxisZ(); + AssertVectorEqual(z, 1, 0, 0); + } + + [Fact] + public void MatrixToGlobal_AndMatrixToLocal_RoundTrip() + { + var cs = new CoordinateSystem(new Point(100, 200, 300), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var toGlobal = cs.MatrixToGlobal(); + var toLocal = cs.MatrixToLocal(); + + var original = new Point(10, 20, 30); + var local = toLocal.Transform(original); + var backToGlobal = toGlobal.Transform(local); + + AssertPointEqual(backToGlobal, 10, 20, 30); + } + + [Fact] + public void MatrixToLocal_TransformsPointToLocalCoordinates() + { + var cs = new CoordinateSystem(new Point(100, 0, 0), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var matrix = cs.MatrixToLocal(); + var transformed = matrix.Transform(new Point(110, 0, 0)); + AssertPointEqual(transformed, 10, 0, 0); + } + + [Fact] + public void ToTransformationPlane_ReturnsNonNull() + { + var cs = new CoordinateSystem(new Point(0, 0, 0), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var tp = cs.ToTransformationPlane(); + tp.Should().NotBeNull(); + } + + [Fact] + public void ToGeometricPlane_ReturnsNonNull() + { + var cs = new CoordinateSystem(new Point(0, 0, 0), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var gp = cs.ToGeometricPlane(); + gp.Should().NotBeNull(); + } + + [Fact] + public void ToGeometricPlane_HasCorrectOrigin() + { + var cs = new CoordinateSystem(new Point(5, 10, 15), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var gp = cs.ToGeometricPlane(); + AssertPointEqual(gp.Origin, 5, 10, 15); + } + } +} + + diff --git a/Tekla.Extension.Tests/DistanceExtensionTests.cs b/Tekla.Extension.Tests/DistanceExtensionTests.cs new file mode 100644 index 0000000..c4ceeb0 --- /dev/null +++ b/Tekla.Extension.Tests/DistanceExtensionTests.cs @@ -0,0 +1,62 @@ +using FluentAssertions; +using Xunit; + +namespace Tekla.Extension.Tests +{ + public class DistanceExtensionTests + { + [Fact] + public void GetDistances_ThreeSpaceSeparated_ReturnsCorrectCount() + { + var result = "100 200 300".GetDistances(); + result.Count.Should().Be(3); + } + + [Fact] + public void GetDistances_ThreeValues_AllParsedCorrectly() + { + var result = "100 200 300".GetDistances(); + result[0].Should().BeApproximately(100, 0.001); + result[1].Should().BeApproximately(200, 0.001); + result[2].Should().BeApproximately(300, 0.001); + } + + [Fact] + public void GetDistances_SingleValue_ReturnsSingleElement() + { + var result = "150".GetDistances(); + result.Count.Should().Be(1); + result[0].Should().BeApproximately(150, 0.001); + } + + [Fact] + public void GetDistances_FourValues_CountMatchesTokens() + { + var result = "10 20 30 40".GetDistances(); + result.Count.Should().Be(4); + } + + [Fact] + public void GetDistances_EmptyString_ReturnsEmptyList() + { + var result = "".GetDistances(); + result.Count.Should().Be(0); + } + + [Fact] + public void GetDistances_EqualSpacing_AllValuesIdentical() + { + var result = "50 50 50".GetDistances(); + foreach (var d in result) + d.Should().BeApproximately(50, 0.001); + } + + [Fact] + public void GetDistances_DecimalValue_ParsedCorrectly() + { + var result = "100.5".GetDistances(); + result.Count.Should().Be(1); + result[0].Should().BeApproximately(100.5, 0.001); + } + } +} diff --git a/Tekla.Extension.Tests/GeometryExtensionTests.cs b/Tekla.Extension.Tests/GeometryExtensionTests.cs new file mode 100644 index 0000000..80c9025 --- /dev/null +++ b/Tekla.Extension.Tests/GeometryExtensionTests.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; + +namespace Tekla.Extension.Tests +{ + public class GeometryExtensionTests + { + [Fact] + public void IsPointInLineSegment3d_PointOnSegment_ReturnsTrue() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(5, 0, 0)).Should().BeTrue(); + } + + [Fact] + public void IsPointInLineSegment3d_PointAtStart_ReturnsTrue() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(0, 0, 0)).Should().BeTrue(); + } + + [Fact] + public void IsPointInLineSegment3d_PointAtEnd_ReturnsTrue() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(10, 0, 0)).Should().BeTrue(); + } + + [Fact] + public void IsPointInLineSegment3d_PointOffSegment_ReturnsFalse() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(5, 5, 0)).Should().BeFalse(); + } + + [Fact] + public void IsPointInLineSegment3d_PointBeyondEnd_ReturnsFalse() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(15, 0, 0)).Should().BeFalse(); + } + + [Fact] + public void IsPointInLineSegment3d_PointBeforeStart_ReturnsFalse() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + GeometryExtension.IsPointInLineSegment3d(seg, new Point(-5, 0, 0)).Should().BeFalse(); + } + + [Fact] + public void IsPointInsidePolygon_PointInside_ReturnsTrue() + { + var polygon = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(10, 10, 0), + new Point(0, 10, 0) + }; + new Point(5, 5, 0).IsPointInsidePolygon(polygon).Should().BeTrue(); + } + + [Fact] + public void IsPointInsidePolygon_PointOutside_ReturnsFalse() + { + var polygon = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(10, 10, 0), + new Point(0, 10, 0) + }; + new Point(15, 15, 0).IsPointInsidePolygon(polygon).Should().BeFalse(); + } + + [Fact] + public void IsPointInsidePolygon_PointOnEdge_IncludeTrue_ReturnsTrue() + { + var polygon = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(10, 10, 0), + new Point(0, 10, 0) + }; + new Point(5, 0, 0).IsPointInsidePolygon(polygon, includeLine: true).Should().BeTrue(); + } + + [Fact] + public void IsPointInsidePolygon_PointOnEdge_IncludeFalse_MayReturnTrueDueToWindingNumber() + { + // With includeLine=false, the explicit edge check is skipped, + // but the winding number algorithm may still classify edge points as inside. + // This documents the actual behavior. + var polygon = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(10, 10, 0), + new Point(0, 10, 0) + }; + // Point on bottom edge - winding number still sees it as inside + var result = new Point(5, 0, 0).IsPointInsidePolygon(polygon, includeLine: false); + result.Should().BeTrue(); + } + } +} diff --git a/Tekla.Extension.Tests/IntersectionsTests.cs b/Tekla.Extension.Tests/IntersectionsTests.cs new file mode 100644 index 0000000..aaeaf0c --- /dev/null +++ b/Tekla.Extension.Tests/IntersectionsTests.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class IntersectionsTests + { + [Fact] + public void IsLineSegmentsIntersect_CrossingSegments_ReturnsTrue() + { + var seg1 = new LineSegment(new Point(0, 0, 0), new Point(10, 10, 0)); + var seg2 = new LineSegment(new Point(10, 0, 0), new Point(0, 10, 0)); + Intersections.IsLineSegmentsIntersect(seg1, seg2, out var point).Should().BeTrue(); + AssertPointEqual(point, 5, 5, 0); + } + + [Fact] + public void IsLineSegmentsIntersect_ParallelSegments_ReturnsFalse() + { + var seg1 = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + var seg2 = new LineSegment(new Point(0, 5, 0), new Point(10, 5, 0)); + Intersections.IsLineSegmentsIntersect(seg1, seg2, out var point).Should().BeFalse(); + point.Should().BeNull(); + } + + [Fact] + public void IsLineSegmentsIntersect_NonIntersectingSegments_ReturnsFalse() + { + var seg1 = new LineSegment(new Point(0, 0, 0), new Point(1, 0, 0)); + var seg2 = new LineSegment(new Point(5, 5, 0), new Point(10, 10, 0)); + Intersections.IsLineSegmentsIntersect(seg1, seg2, out _).Should().BeFalse(); + } + + [Fact] + public void IsLineSegmentsIntersect_TouchingAtEndpoint_ReturnsTrue() + { + var seg1 = new LineSegment(new Point(0, 0, 0), new Point(5, 5, 0)); + var seg2 = new LineSegment(new Point(5, 5, 0), new Point(10, 0, 0)); + Intersections.IsLineSegmentsIntersect(seg1, seg2, out var point).Should().BeTrue(); + AssertPointEqual(point, 5, 5, 0); + } + + [Fact] + public void GetIntersectionPoints_TwoSegmentCollections_FindsIntersections() + { + var segs1 = new List + { + new LineSegment(new Point(0, 5, 0), new Point(10, 5, 0)) + }; + var segs2 = new List + { + new LineSegment(new Point(5, 0, 0), new Point(5, 10, 0)) + }; + var points = Intersections.GetIntersectionPoints(segs1, segs2); + points.Count.Should().Be(1); + AssertPointEqual(points.First(), 5, 5, 0); + } + + [Fact] + public void GetIntersectionPoints_NoIntersections_ReturnsEmpty() + { + var segs1 = new List + { + new LineSegment(new Point(0, 0, 0), new Point(1, 0, 0)) + }; + var segs2 = new List + { + new LineSegment(new Point(0, 5, 0), new Point(1, 5, 0)) + }; + Intersections.GetIntersectionPoints(segs1, segs2).Count.Should().Be(0); + } + + [Fact] + public void GetIntersectionPoints_Polygons_FindsAllIntersections() + { + // Square polygon from (0,0) to (10,10) + var polygon1 = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(10, 10, 0), + new Point(0, 10, 0) + }; + // Square polygon from (5,5) to (15,15) + var polygon2 = new List + { + new Point(5, 5, 0), + new Point(15, 5, 0), + new Point(15, 15, 0), + new Point(5, 15, 0) + }; + var points = Intersections.GetIntersectionPoints(polygon1, polygon2); + points.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void IsLineSegmentsIntersect_PerpendicularCross_FindsCorrectPoint() + { + var seg1 = new LineSegment(new Point(0, 5, 0), new Point(10, 5, 0)); + var seg2 = new LineSegment(new Point(3, 0, 0), new Point(3, 10, 0)); + Intersections.IsLineSegmentsIntersect(seg1, seg2, out var point).Should().BeTrue(); + AssertPointEqual(point, 3, 5, 0); + } + } +} diff --git a/Tekla.Extension.Tests/LineExtensionTests.cs b/Tekla.Extension.Tests/LineExtensionTests.cs new file mode 100644 index 0000000..3bfdc10 --- /dev/null +++ b/Tekla.Extension.Tests/LineExtensionTests.cs @@ -0,0 +1,97 @@ +using System; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class LineExtensionTests + { + [Fact] + public void RotatePointAroundAxis_90Degrees_AroundZ() + { + var startDir = new Vector(1, 0, 0); + var center = new Point(0, 0, 0); + var axis = new Vector(0, 0, 1); + var result = LineExtension.RotatePointAroundAxis(startDir, center, axis, Math.PI / 2); + AssertPointEqual(result, 0, 1, 0); + } + + [Fact] + public void RotatePointAroundAxis_360Degrees_ReturnsOriginal() + { + var startDir = new Vector(1, 0, 0); + var center = new Point(0, 0, 0); + var axis = new Vector(0, 0, 1); + var result = LineExtension.RotatePointAroundAxis(startDir, center, axis, 2 * Math.PI); + AssertPointEqual(result, 1, 0, 0); + } + + [Fact] + public void RotatePointAroundAxis_0Degrees_ReturnsOriginal() + { + var startDir = new Vector(5, 3, 0); + var center = new Point(1, 1, 0); + var axis = new Vector(0, 0, 1); + var result = LineExtension.RotatePointAroundAxis(startDir, center, axis, 0); + AssertPointEqual(result, 6, 4, 0); + } + + [Fact] + public void DevideBy_IntoOneSegment_ReturnsSameSegment() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + var result = seg.DevideBy(1); + result.Length.Should().Be(1); + AssertPointEqual(result[0].Point1, 0, 0, 0); + AssertPointEqual(result[0].Point2, 10, 0, 0); + } + + [Fact] + public void DevideBy_IntoTwoSegments_ReturnsEqualHalves() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + var result = seg.DevideBy(2); + result.Length.Should().Be(2); + AssertPointEqual(result[0].Point1, 0, 0, 0); + AssertPointEqual(result[0].Point2, 5, 0, 0); + AssertPointEqual(result[1].Point1, 5, 0, 0); + AssertPointEqual(result[1].Point2, 10, 0, 0); + } + + [Fact] + public void DevideBy_IntoThreeSegments() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(9, 0, 0)); + var result = seg.DevideBy(3); + result.Length.Should().Be(3); + AssertPointEqual(result[0].Point2, 3, 0, 0); + AssertPointEqual(result[1].Point2, 6, 0, 0); + AssertPointEqual(result[2].Point2, 9, 0, 0); + } + + [Fact] + public void GetCenterPoint_HorizontalSegment() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + AssertPointEqual(seg.GetCenterPoint(), 5, 0, 0); + } + + [Fact] + public void GetCenterPoint_DiagonalSegment() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 10, 10)); + AssertPointEqual(seg.GetCenterPoint(), 5, 5, 5); + } + + [Fact] + public void ToLine_ConvertsLineSegmentToLine() + { + var seg = new LineSegment(new Point(0, 0, 0), new Point(10, 0, 0)); + var line = seg.ToLine(); + line.Should().NotBeNull(); + AssertPointEqual(line.Origin, 0, 0, 0); + } + } +} diff --git a/Tekla.Extension.Tests/LinqExtensionTests.cs b/Tekla.Extension.Tests/LinqExtensionTests.cs new file mode 100644 index 0000000..6fa8a58 --- /dev/null +++ b/Tekla.Extension.Tests/LinqExtensionTests.cs @@ -0,0 +1,45 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Tekla.Extension.Tests +{ + public class LinqExtensionTests + { + [Fact] + public void ToIEnumerable_ArrayList_ConvertsAllElements() + { + var list = new ArrayList { "one", "two", "three" }; + var result = list.GetEnumerator().ToIEnumerable().ToList(); + result.Should().HaveCount(3); + result.Should().ContainInOrder("one", "two", "three"); + } + + [Fact] + public void ToIEnumerable_FiltersMatchingType() + { + var list = new ArrayList { "text", 42, "another", 3.14 }; + var result = list.GetEnumerator().ToIEnumerable().ToList(); + result.Should().HaveCount(2); + result.Should().ContainInOrder("text", "another"); + } + + [Fact] + public void ToIEnumerable_EmptyEnumerator_ReturnsEmpty() + { + var list = new ArrayList(); + var result = list.GetEnumerator().ToIEnumerable().ToList(); + result.Should().BeEmpty(); + } + + [Fact] + public void ToIEnumerable_NoMatchingType_ReturnsEmpty() + { + var list = new ArrayList { 1, 2, 3 }; + var result = list.GetEnumerator().ToIEnumerable().ToList(); + result.Should().BeEmpty(); + } + } +} diff --git a/Tekla.Extension.Tests/ModelObjectExtensionTests.cs b/Tekla.Extension.Tests/ModelObjectExtensionTests.cs new file mode 100644 index 0000000..f4e79ee --- /dev/null +++ b/Tekla.Extension.Tests/ModelObjectExtensionTests.cs @@ -0,0 +1,101 @@ +using FluentAssertions; +using Tekla.Extension.Tests.TestBase; +using Tekla.Structures.Model; +using Xunit; + +namespace Tekla.Extension.Tests +{ + // All tests create ModelObject subtypes or call GetReportProperty which + // routes through the Tekla remoting layer — CDelegateSetter must be set. + public class ModelObjectExtensionTests : TeklaModelTestBase + { + // ── GetReportProperty — return values with no model ───────────────── + // With GenericDelegateFake the model call returns + // the default bool (false) without modifying the ref value, so each + // overload returns the initializer value used inside the extension. + + [Fact] + public void GetReportProperty_StringType_ReturnsEmptyString() + { + var beam = new Beam(); + var result = beam.GetReportProperty("PROFILE"); + result.Should().Be(string.Empty); + } + + [Fact] + public void GetReportProperty_IntType_ReturnsIntMinValue() + { + var beam = new Beam(); + var result = beam.GetReportProperty("DRAWING.ID"); + result.Should().Be(int.MinValue); + } + + [Fact] + public void GetReportProperty_DoubleType_ReturnsDoubleMinValue() + { + var beam = new Beam(); + var result = beam.GetReportProperty("LENGTH"); + result.Should().Be(double.MinValue); + } + + [Fact] + public void GetReportProperty_IsSuccess_ReturnsFalse() + { + var beam = new Beam(); + beam.GetReportProperty("HEIGHT", out bool ok); + ok.Should().BeFalse(); + } + + [Fact] + public void GetReportProperty_UnsupportedType_ReturnsDefault() + { + // The extension returns default(T) for any type that is not + // string / int / double and sets isSuccess = false. + var beam = new Beam(); + var result = beam.GetReportProperty("ANY", out bool ok); + result.Should().BeNull(); + ok.Should().BeFalse(); + } + + // ── IsConnectionObject ────────────────────────────────────────────── + + [Fact] + public void IsConnectionObject_Beam_ReturnsFalse() + { + new Beam().IsConnectionObject().Should().BeFalse(); + } + + [Fact] + public void IsConnectionObject_Weld_ReturnsFalse() + { + // Weld is an associative object, not a connection object. + new Weld().IsConnectionObject().Should().BeFalse(); + } + + // ── IsAssociativeObject ───────────────────────────────────────────── + + [Fact] + public void IsAssociativeObject_Beam_ReturnsFalse() + { + new Beam().IsAssociativeObject().Should().BeFalse(); + } + + [Fact] + public void IsAssociativeObject_Weld_ReturnsTrue() + { + new Weld().IsAssociativeObject().Should().BeTrue(); + } + + [Fact] + public void IsAssociativeObject_BooleanPart_ReturnsTrue() + { + new BooleanPart().IsAssociativeObject().Should().BeTrue(); + } + + [Fact] + public void IsAssociativeObject_SingleRebar_ReturnsTrue() + { + new SingleRebar().IsAssociativeObject().Should().BeTrue(); + } + } +} diff --git a/Tekla.Extension.Tests/OBBExtensionTests.cs b/Tekla.Extension.Tests/OBBExtensionTests.cs new file mode 100644 index 0000000..4e7d5ad --- /dev/null +++ b/Tekla.Extension.Tests/OBBExtensionTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class OBBExtensionTests + { + private static OBB CreateAxisAlignedOBB(Point center, double extentX, double extentY, double extentZ) + { + return new OBB(center, + new Vector(1, 0, 0), new Vector(0, 1, 0), new Vector(0, 0, 1), + extentX, extentY, extentZ); + } + + [Fact] + public void CombineOBBs_TwoOBBs_ReturnsCombined() + { + var obb1 = CreateAxisAlignedOBB(new Point(0, 0, 0), 5, 5, 5); + var obb2 = CreateAxisAlignedOBB(new Point(10, 0, 0), 5, 5, 5); + var combined = new List { obb1, obb2 }.CombineOBBs(); + combined.Should().NotBeNull(); + } + + [Fact] + public void CombineOBBs_EmptyCollection_ReturnsDefaultOBB() + { + var combined = new List().CombineOBBs(); + combined.Should().NotBeNull(); + } + + [Fact] + public void CombineOBBs_SingleOBB_ReturnsSameOBB() + { + var obb = CreateAxisAlignedOBB(new Point(5, 5, 5), 10, 10, 10); + var combined = new List { obb }.CombineOBBs(); + combined.Should().NotBeNull(); + AssertPointEqual(combined.Center, 5, 5, 5); + } + + [Fact] + public void GetMaximumPoint_BugDocumentation_ReturnsMaxValue() + { + // Bug: GetMaximumPoint starts with MaxPoint (double.MaxValue) and uses (x > y) comparer. + // No vertex will ever be > double.MaxValue, so it always returns MaxPoint unchanged. + // Should start with MinPoint instead. + var obb = CreateAxisAlignedOBB(new Point(5, 5, 5), 5, 5, 5); + var max = obb.GetMaximumPoint(); + max.X.Should().Be(double.MaxValue); + max.Y.Should().Be(double.MaxValue); + max.Z.Should().Be(double.MaxValue); + } + + [Fact] + public void GetMinimumPoint_ReturnsCorrectMinCorner() + { + // GetMinimumPoint starts with MaxPoint and uses (x < y) comparer - this one works correctly. + var obb = CreateAxisAlignedOBB(new Point(5, 5, 5), 5, 5, 5); + var min = obb.GetMinimumPoint(); + AssertPointEqual(min, 0, 0, 0); + } + } +} diff --git a/Tekla.Extension.Tests/PointExtensionTests.cs b/Tekla.Extension.Tests/PointExtensionTests.cs new file mode 100644 index 0000000..bcae317 --- /dev/null +++ b/Tekla.Extension.Tests/PointExtensionTests.cs @@ -0,0 +1,309 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Tekla.Structures.Model; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class PointExtensionTests + { + [Fact] + public void MaxPoint_HasMaxValues() + { + var p = PointExtension.MaxPoint; + p.X.Should().Be(double.MaxValue); + p.Y.Should().Be(double.MaxValue); + p.Z.Should().Be(double.MaxValue); + } + + [Fact] + public void MinPoint_HasMinValues() + { + var p = PointExtension.MinPoint; + p.X.Should().Be(double.MinValue); + p.Y.Should().Be(double.MinValue); + p.Z.Should().Be(double.MinValue); + } + + [Fact] + public void GetCenterPoint_ReturnsMidpoint() + { + var p1 = new Point(0, 0, 0); + var p2 = new Point(10, 20, 30); + AssertPointEqual(p1.GetCenterPoint(p2), 5, 10, 15); + } + + [Fact] + public void GetCenterPoint_SamePoints_ReturnsSamePoint() + { + var p = new Point(5, 5, 5); + AssertPointEqual(p.GetCenterPoint(p), 5, 5, 5); + } + + [Fact] + public void GetVector_ReturnsDirectionVector() + { + var start = new Point(1, 2, 3); + var end = new Point(4, 6, 8); + AssertVectorEqual(PointExtension.GetVector(start, end), 3, 4, 5); + } + + [Fact] + public void Round_DefaultDigits() + { + var p = new Point(1.12345, 2.6789, 3.999); + p.Round(); + AssertPointEqual(p, 1.12, 2.68, 4.0); + } + + [Fact] + public void Round_SpecifiedDigits() + { + var p = new Point(1.12345, 2.6789, 3.5); + p.Round(1); + AssertPointEqual(p, 1.1, 2.7, 3.5); + } + + [Fact] + public void GetNearestPoint_TwoPoints_ReturnsCloser() + { + var origin = new Point(0, 0, 0); + var near = new Point(1, 0, 0); + var far = new Point(10, 0, 0); + var result = origin.GetNearestPoint(near, far); + AssertPointEqual(result, 1, 0, 0); + } + + [Fact] + public void GetNearestPoint_Collection_ReturnsClosest() + { + var origin = new Point(0, 0, 0); + var points = new List + { + new Point(10, 0, 0), + new Point(1, 0, 0), + new Point(5, 0, 0) + }; + var result = origin.GetNearestPoint(points); + AssertPointEqual(result, 1, 0, 0); + } + + [Fact] + public void GetRemotePoint_Collection_ReturnsFarthest() + { + var origin = new Point(0, 0, 0); + var points = new List + { + new Point(10, 0, 0), + new Point(1, 0, 0), + new Point(5, 0, 0) + }; + var result = origin.GetRemotePoint(points); + AssertPointEqual(result, 10, 0, 0); + } + + [Fact] + public void ToContourPoint_ConvertsCorrectly() + { + var p = new Point(1, 2, 3); + var cp = p.ToContourPoint(); + cp.X.Should().BeApproximately(1, Tolerance); + cp.Y.Should().BeApproximately(2, Tolerance); + cp.Z.Should().BeApproximately(3, Tolerance); + } + + [Fact] + public void ToPoint_FromContourPoint_ConvertsCorrectly() + { + var cp = new ContourPoint(new Point(4, 5, 6), null); + var p = cp.ToPoint(); + AssertPointEqual(p, 4, 5, 6); + } + + [Fact] + public void ToVector_ConvertsCorrectly() + { + var p = new Point(1, 2, 3); + AssertVectorEqual(p.ToVector(), 1, 2, 3); + } + + [Fact] + public void IsNull_NaNPoint_ReturnsFalse_DocumentsBug() + { + // Documents known bug: uses == double.NaN instead of double.IsNaN() + // NaN == NaN is always false in IEEE 754 + var p = new Point(double.NaN, double.NaN, double.NaN); + p.IsNull().Should().BeFalse(); + } + + [Fact] + public void IsEmpty_ZeroPoint_ReturnsTrue() + { + new Point(0, 0, 0).IsEmpty().Should().BeTrue(); + } + + [Fact] + public void IsEmpty_NonZeroPoint_ReturnsFalse() + { + new Point(1, 0, 0).IsEmpty().Should().BeFalse(); + } + + [Fact] + public void RoundTo_RoundsToNearestMultiple() + { + var p = new Point(17, 23, 38); + p.RoundTo(10); + AssertPointEqual(p, 20, 20, 40); + } + + [Fact] + public void RoundTo_ExactMultiple_Unchanged() + { + var p = new Point(20, 30, 40); + p.RoundTo(10); + AssertPointEqual(p, 20, 30, 40); + } + + [Fact] + public void CeilingTo_RoundsUp() + { + var p = new Point(11, 21, 31); + p.CeilingTo(10); + AssertPointEqual(p, 20, 30, 40); + } + + [Fact] + public void CeilingTo_ExactMultiple_Unchanged() + { + var p = new Point(20, 30, 40); + p.CeilingTo(10); + AssertPointEqual(p, 20, 30, 40); + } + + [Fact] + public void FloorTo_RoundsDown() + { + var p = new Point(19, 29, 39); + p.FloorTo(10); + AssertPointEqual(p, 10, 20, 30); + } + + [Fact] + public void FloorTo_ExactMultiple_Unchanged() + { + var p = new Point(20, 30, 40); + p.FloorTo(10); + AssertPointEqual(p, 20, 30, 40); + } + + [Fact] + public void ResetX_ReturnsNewPointWithModifiedX() + { + var p = new Point(1, 2, 3); + var result = p.ResetX(99); + AssertPointEqual(result, 99, 2, 3); + } + + [Fact] + public void ResetY_ReturnsNewPointWithModifiedY() + { + var p = new Point(1, 2, 3); + var result = p.ResetY(99); + AssertPointEqual(result, 1, 99, 3); + } + + [Fact] + public void ResetZ_ReturnsNewPointWithModifiedZ() + { + var p = new Point(1, 2, 3); + var result = p.ResetZ(99); + AssertPointEqual(result, 1, 2, 99); + } + + [Fact] + public void GetLineSegmentsOfPolygon_ClosedTriangle_Returns3Segments() + { + var points = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(5, 10, 0) + }; + var segments = points.GetLineSegmentsOfPolygon(isClosed: true); + segments.Count.Should().Be(3); + } + + [Fact] + public void GetLineSegmentsOfPolygon_OpenPolyline_Returns2Segments() + { + var points = new List + { + new Point(0, 0, 0), + new Point(10, 0, 0), + new Point(5, 10, 0) + }; + var segments = points.GetLineSegmentsOfPolygon(isClosed: false); + segments.Count.Should().Be(2); + } + + [Fact] + public void GetPolygonAABB_ReturnsCorrectBounds() + { + var points = new List + { + new Point(1, 2, 3), + new Point(10, 20, 30), + new Point(5, 5, 5) + }; + var aabb = PointExtension.GetPolygonAABB(points); + AssertPointEqual(aabb.MinPoint, 1, 2, 3); + AssertPointEqual(aabb.MaxPoint, 10, 20, 30); + } + + [Fact] + public void GetMinimumDistance_ReturnsShortestDistanceBetweenConsecutivePoints() + { + var points = new List + { + new Point(0, 0, 0), + new Point(1, 0, 0), + new Point(5, 0, 0) + }; + points.GetMinimumDistance().Should().BeApproximately(1.0, Tolerance); + } + + [Fact] + public void GetMaximumDistance_ReturnsLongestDistanceBetweenConsecutivePoints() + { + var points = new List + { + new Point(0, 0, 0), + new Point(1, 0, 0), + new Point(10, 0, 0) + }; + // Consecutive distances: [0]->[1]=1, [1]->[2]=9, [2]->[0]=10 + points.GetMaximumDistance().Should().BeApproximately(10.0, Tolerance); + } + + [Fact] + public void ComparePoints_DefaultComparer_WritesMaxValues() + { + var source = new Point(10, 5, 20); + var target = new Point(5, 10, 15); + PointExtension.ComparePoints(source, target); + AssertPointEqual(target, 10, 10, 20); + } + + [Fact] + public void ComparePoints_MinComparer_WritesMinValues() + { + var source = new Point(3, 15, 8); + var target = new Point(5, 10, 12); + PointExtension.ComparePoints(source, target, (x, y) => x < y); + AssertPointEqual(target, 3, 10, 8); + } + } +} diff --git a/Tekla.Extension.Tests/ProfileExtensionTests.cs b/Tekla.Extension.Tests/ProfileExtensionTests.cs new file mode 100644 index 0000000..10da094 --- /dev/null +++ b/Tekla.Extension.Tests/ProfileExtensionTests.cs @@ -0,0 +1,79 @@ +using FluentAssertions; +using Tekla.Extension.Tests.TestBase; +using Xunit; + +namespace Tekla.Extension.Tests +{ + // LibraryProfileItem / ParametricProfileItem route through ModelInternal, + // so CDelegateSetter must be set. With ReturnDefaultStrategy every catalog + // call returns 0 / false, which means Select() → false and all property + // lookups return 0. Tests document that behaviour for unknown profiles. + public class ProfileExtensionTests : TeklaCatalogTestBase + { + private const string UnknownProfile = "XYZZY_DOES_NOT_EXIST"; + + // ── IsProfileExist ────────────────────────────────────────────────── + + [Fact] + public void IsProfileExist_UnknownProfile_ReturnsFalse() + { + ProfileExtension.IsProfileExist(UnknownProfile).Should().BeFalse(); + } + + [Fact] + public void IsProfileExist_EmptyString_ReturnsFalse() + { + ProfileExtension.IsProfileExist(string.Empty).Should().BeFalse(); + } + + // ── GetProfileProperty ────────────────────────────────────────────── + + [Fact] + public void GetProfileProperty_UnknownProfile_ReturnsZero() + { + ProfileExtension.GetProfileProperty(UnknownProfile, "HEIGHT") + .Should().BeApproximately(0, 0.001); + } + + [Fact] + public void GetProfileProperty_UnknownAttribute_ReturnsZero() + { + ProfileExtension.GetProfileProperty(UnknownProfile, "NONEXISTENT_ATTR") + .Should().BeApproximately(0, 0.001); + } + + // ── GetProfileHeight / GetProfileWidth ────────────────────────────── + + [Fact] + public void GetProfileHeight_UnknownProfile_ReturnsZero() + { + ProfileExtension.GetProfileHeight(UnknownProfile) + .Should().BeApproximately(0, 0.001); + } + + [Fact] + public void GetProfileWidth_UnknownProfile_ReturnsZero() + { + ProfileExtension.GetProfileWidth(UnknownProfile) + .Should().BeApproximately(0, 0.001); + } + + // ── GetProfileSymbol ──────────────────────────────────────────────── + + [Fact] + public void GetProfileSymbol_UnknownProfile_ReturnsZero() + { + ProfileExtension.GetProfileSymbol(UnknownProfile, "h") + .Should().BeApproximately(0, 0.001); + } + + // ── GetPlateWidthByProfile ────────────────────────────────────────── + + [Fact] + public void GetPlateWidthByProfile_UnknownProfile_ReturnsZero() + { + ProfileExtension.GetPlateWidthByProfile(UnknownProfile) + .Should().BeApproximately(0, 0.001); + } + } +} diff --git a/Tekla.Extension.Tests/ProfileTypeEnumConverterTests.cs b/Tekla.Extension.Tests/ProfileTypeEnumConverterTests.cs new file mode 100644 index 0000000..3839c3a --- /dev/null +++ b/Tekla.Extension.Tests/ProfileTypeEnumConverterTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using Tekla.Extension.Enums; +using Tekla.Extension.Services; +using Xunit; + +namespace Tekla.Extension.Tests +{ + public class ProfileTypeEnumConverterTests + { + [Theory] + [InlineData("B", ProfileType.Plate)] + [InlineData("I", ProfileType.Ibeam)] + [InlineData("L", ProfileType.Angle)] + [InlineData("U", ProfileType.Channel)] + [InlineData("RU", ProfileType.RoundBar)] + [InlineData("RO", ProfileType.RoundTube)] + [InlineData("M", ProfileType.RectangularTube)] + [InlineData("C", ProfileType.CFChannel)] + [InlineData("T", ProfileType.Tbeam)] + [InlineData("Z", ProfileType.Zbeam)] + public void GetProfileTypeFromString_KnownTypes_ReturnsCorrectEnum(string input, ProfileType expected) + { + ProfileTypeEnumConverter.GetProfileTypeFromString(input).Should().Be(expected); + } + + [Theory] + [InlineData("")] + [InlineData("X")] + [InlineData("unknown")] + public void GetProfileTypeFromString_UnknownString_ReturnsUnknown(string input) + { + ProfileTypeEnumConverter.GetProfileTypeFromString(input).Should().Be(ProfileType.Unknown); + } + + [Theory] + [InlineData(ProfileType.Plate, "B")] + [InlineData(ProfileType.Ibeam, "I")] + [InlineData(ProfileType.Angle, "L")] + [InlineData(ProfileType.Channel, "U")] + [InlineData(ProfileType.RoundBar, "RU")] + [InlineData(ProfileType.RoundTube, "RO")] + [InlineData(ProfileType.RectangularTube, "M")] + [InlineData(ProfileType.CFChannel, "C")] + [InlineData(ProfileType.Tbeam, "T")] + [InlineData(ProfileType.Zbeam, "Z")] + public void GetStringValueFromProfileType_KnownTypes_ReturnsCorrectString(ProfileType input, string expected) + { + ProfileTypeEnumConverter.GetStringValueFromProfileType(input).Should().Be(expected); + } + + [Fact] + public void GetStringValueFromProfileType_Unknown_ReturnsUnknownString() + { + ProfileTypeEnumConverter.GetStringValueFromProfileType(ProfileType.Unknown).Should().Be("Unknown"); + } + + [Theory] + [InlineData("B")] + [InlineData("I")] + [InlineData("L")] + [InlineData("U")] + [InlineData("RU")] + [InlineData("RO")] + [InlineData("M")] + [InlineData("C")] + [InlineData("T")] + [InlineData("Z")] + public void RoundTrip_StringToEnumToString_ReturnsOriginal(string input) + { + var profileType = ProfileTypeEnumConverter.GetProfileTypeFromString(input); + var result = ProfileTypeEnumConverter.GetStringValueFromProfileType(profileType); + result.Should().Be(input); + } + } +} diff --git a/Tekla.Extension.Tests/Tekla.Extension.Tests.csproj b/Tekla.Extension.Tests/Tekla.Extension.Tests.csproj new file mode 100644 index 0000000..8fa8bf9 --- /dev/null +++ b/Tekla.Extension.Tests/Tekla.Extension.Tests.csproj @@ -0,0 +1,121 @@ + + + + net48 + latest + true + false + 2020;2021;2022;2023;2024;2025 + x64 + 2023 + x64 + x64 + NU1605 + true + + + + bin\2020\x64\ + TEKLA2020 + x64 + + + bin\2021\x64\ + TEKLA2021 + x64 + + + bin\2022\x64\ + TEKLA2022 + x64 + + + bin\2023\x64\ + TEKLA2023 + x64 + + + bin\2024\x64\ + TEKLA2024 + x64 + + + bin\2025\x64\ + TEKLA2025 + x64 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tekla.Extension.Tests/TestBase/TeklaCatalogTestBase.cs b/Tekla.Extension.Tests/TestBase/TeklaCatalogTestBase.cs new file mode 100644 index 0000000..74467ad --- /dev/null +++ b/Tekla.Extension.Tests/TestBase/TeklaCatalogTestBase.cs @@ -0,0 +1,24 @@ +using Moq; +using System; +using Tekla.Structures.CatalogInternal; +using Tekla.Structures.RemotingHelper; + +namespace Tekla.Extension.Tests.TestBase +{ + /// + /// Base class for tests that require Tekla Model internal (ICDelegate) to be mocked. + /// Constructor sets up the fake delegate; Dispose resets it (xUnit calls both per test). + /// + public abstract class TeklaCatalogTestBase : IDisposable + { + protected TeklaCatalogTestBase() + { + CDelegateSetter.SetInstanceForUnitTesting(new GenericDelegateFake()); + } + + public void Dispose() + { + //CDelegateSetter.ResetInstance(); 'CDelegateSetter' does not contain a definition for 'ResetInstance' + } + } +} diff --git a/Tekla.Extension.Tests/TestBase/TeklaModelTestBase.cs b/Tekla.Extension.Tests/TestBase/TeklaModelTestBase.cs new file mode 100644 index 0000000..fb08685 --- /dev/null +++ b/Tekla.Extension.Tests/TestBase/TeklaModelTestBase.cs @@ -0,0 +1,25 @@ +using Moq; +using System; +using Tekla.Structures.ModelInternal; +using Tekla.Structures.RemotingHelper; + +namespace Tekla.Extension.Tests.TestBase +{ + + /// + /// Base class for tests that require Tekla Model internal (ICDelegate) to be mocked. + /// Constructor sets up the fake delegate; Dispose resets it (xUnit calls both per test). + /// + public abstract class TeklaModelTestBase : IDisposable + { + protected TeklaModelTestBase() + { + CDelegateSetter.SetInstanceForUnitTesting(new GenericDelegateFake()); + } + + public void Dispose() + { + //CDelegateSetter.ResetInstance(); 'CDelegateSetter' does not contain a definition for 'ResetInstance' + } + } +} diff --git a/Tekla.Extension.Tests/TestHelpers.cs b/Tekla.Extension.Tests/TestHelpers.cs new file mode 100644 index 0000000..0332b99 --- /dev/null +++ b/Tekla.Extension.Tests/TestHelpers.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using System; +using System.ComponentModel; +using System.Security.Cryptography; +using Tekla.Structures.Geometry3d; +using Tekla.Structures.ModelInternal; +using Tekla.Structures.RemotingHelper; + +namespace Tekla.Extension.Tests +{ + internal static class TestHelpers + { + public const double Tolerance = 1e-6; + + public static void AssertPointEqual(Point actual, double expectedX, double expectedY, double expectedZ) + { + actual.X.Should().BeApproximately(expectedX, Tolerance); + actual.Y.Should().BeApproximately(expectedY, Tolerance); + actual.Z.Should().BeApproximately(expectedZ, Tolerance); + } + + public static void AssertVectorEqual(Vector actual, double expectedX, double expectedY, double expectedZ) + { + actual.X.Should().BeApproximately(expectedX, Tolerance); + actual.Y.Should().BeApproximately(expectedY, Tolerance); + actual.Z.Should().BeApproximately(expectedZ, Tolerance); + } + } + +} diff --git a/Tekla.Extension.Tests/VectorExtensionTests.cs b/Tekla.Extension.Tests/VectorExtensionTests.cs new file mode 100644 index 0000000..aa8a276 --- /dev/null +++ b/Tekla.Extension.Tests/VectorExtensionTests.cs @@ -0,0 +1,140 @@ +using FluentAssertions; +using Tekla.Structures.Geometry3d; +using Xunit; +using static Tekla.Extension.Tests.TestHelpers; + +namespace Tekla.Extension.Tests +{ + public class VectorExtensionTests + { + [Fact] + public void X_ReturnsUnitX() + { + AssertVectorEqual(VectorExtension.X, 1, 0, 0); + } + + [Fact] + public void Y_ReturnsUnitY() + { + AssertVectorEqual(VectorExtension.Y, 0, 1, 0); + } + + [Fact] + public void Z_ReturnsUnitZ() + { + AssertVectorEqual(VectorExtension.Z, 0, 0, 1); + } + + [Fact] + public void Null_ReturnsZeroVector() + { + AssertVectorEqual(VectorExtension.Null, 0, 0, 0); + } + + [Fact] + public void Nan_ReturnsNanVector() + { + var nan = VectorExtension.Nan; + double.IsNaN(nan.X).Should().BeTrue(); + double.IsNaN(nan.Y).Should().BeTrue(); + double.IsNaN(nan.Z).Should().BeTrue(); + } + + [Fact] + public void Add_TwoVectors_ReturnsSumVector() + { + var v = new Vector(1, 2, 3); + var p = new Point(4, 5, 6); + AssertVectorEqual(v.Add(p), 5, 7, 9); + } + + [Fact] + public void Subtract_TwoVectors_ReturnsDifferenceVector() + { + var v = new Vector(4, 5, 6); + var p = new Point(1, 2, 3); + AssertVectorEqual(v.Subtract(p), 3, 3, 3); + } + + [Fact] + public void Negative_ReturnsNegatedVector() + { + var v = new Vector(1, -2, 3); + AssertVectorEqual(v.Negative(), -1, 2, -3); + } + + [Fact] + public void ToPoint_ConvertsCorrectly() + { + var v = new Vector(1, 2, 3); + var p = v.ToPoint(); + AssertPointEqual(p, 1, 2, 3); + } + + [Fact] + public void ProjectPointToVector_ProjectsCorrectly() + { + var point = new Point(3, 4, 0); + var vector = new Vector(1, 0, 0); + var projected = point.ProjectPointToVector(vector); + AssertPointEqual(projected, 3, 0, 0); + } + + [Fact] + public void ProjectPointToVector_DiagonalProjection() + { + var point = new Point(2, 2, 0); + var vector = new Vector(1, 1, 0); + var projected = point.ProjectPointToVector(vector); + AssertPointEqual(projected, 2, 2, 0); + } + + [Fact] + public void Round_RoundsToSpecifiedDigits() + { + var v = new Vector(1.12345, 2.6789, 3.5); + v.Round(2); + AssertVectorEqual(v, 1.12, 2.68, 3.5); + } + + [Fact] + public void Round_DefaultDigits() + { + var v = new Vector(1.12345, 2.6789, 3.999); + v.Round(); + AssertVectorEqual(v, 1.12, 2.68, 4.0); + } + + [Fact] + public void GetLengthSquared_UnitVector_ReturnsOne() + { + VectorExtension.X.GetLengthSquared().Should().BeApproximately(1.0, Tolerance); + } + + [Fact] + public void GetLengthSquared_ArbitraryVector_ReturnsCorrectValue() + { + var v = new Vector(3, 4, 0); + v.GetLengthSquared().Should().BeApproximately(25.0, Tolerance); + } + + [Fact] + public void Translate_MovesPointByVector() + { + var point = new Point(1, 2, 3); + var vector = new Vector(10, 20, 30); + point.Translate(vector); + AssertPointEqual(point, 11, 22, 33); + } + + [Fact] + public void TransformVector_WithIdentityLikeMatrix() + { + var cs = new CoordinateSystem(new Point(0, 0, 0), new Vector(1, 0, 0), new Vector(0, 1, 0)); + var matrix = MatrixFactory.FromCoordinateSystem(cs); + var v = new Vector(1, 0, 0); + var result = matrix.TransformVector(v); + AssertVectorEqual(result, 1, 0, 0); + } + } +} diff --git a/Tekla.Extension.sln b/Tekla.Extension.sln index 881c683..857fd57 100644 --- a/Tekla.Extension.sln +++ b/Tekla.Extension.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.6.33815.320 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tekla.Extension", "Tekla.Extension\Tekla.Extension.csproj", "{03C19A66-26C8-4572-A784-E0F59E513CD1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tekla.Extension.Tests", "Tekla.Extension.Tests\Tekla.Extension.Tests.csproj", "{B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 2020|x64 = 2020|x64 @@ -27,6 +29,18 @@ Global {03C19A66-26C8-4572-A784-E0F59E513CD1}.2024|x64.Build.0 = 2024|x64 {03C19A66-26C8-4572-A784-E0F59E513CD1}.2025|x64.ActiveCfg = 2025|x64 {03C19A66-26C8-4572-A784-E0F59E513CD1}.2025|x64.Build.0 = 2025|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2020|x64.ActiveCfg = 2020|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2020|x64.Build.0 = 2020|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2021|x64.ActiveCfg = 2021|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2021|x64.Build.0 = 2021|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2022|x64.ActiveCfg = 2022|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2022|x64.Build.0 = 2022|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2023|x64.ActiveCfg = 2023|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2023|x64.Build.0 = 2023|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2024|x64.ActiveCfg = 2024|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2024|x64.Build.0 = 2024|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2025|x64.ActiveCfg = 2025|x64 + {B5A7F4E2-3D1C-4F8A-9E6B-2C7D8A9F0E1B}.2025|x64.Build.0 = 2025|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tekla.Extension/Tekla.Extension.csproj b/Tekla.Extension/Tekla.Extension.csproj index d5643d9..edbd51c 100644 --- a/Tekla.Extension/Tekla.Extension.csproj +++ b/Tekla.Extension/Tekla.Extension.csproj @@ -29,6 +29,7 @@ More methods and features Copyright © Apibim SpA 2025 Trimble Tekla Structures + true bin\2020\x64\