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
-[](https://www.nuget.org/packages/Tekla.Extension/)
-[](https://github.com/Blogbotana/Tekla.Extensions/releases/latest)
-[](https://github.com/Blogbotana/Tekla.Extensions/blob/main/LICENSE.md)
+[](https://www.nuget.org/packages/Tekla.Extension/)
+[](https://www.nuget.org/packages/Tekla.Extension/)
+[](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