From c5ece4c478d2c771476830f235925eb299fdabf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 17:19:03 +0200 Subject: [PATCH 1/4] Use actual connection order for triangle extraction Agent-Logs-Url: https://github.com/chrxh/alien/sessions/3fc550f8-0c1d-45f8-91e1-1367a8dc132a Co-authored-by: chrxh <73127001+chrxh@users.noreply.github.com> --- source/EngineKernels/GeometryKernels.cu | 81 +++++++++--- source/EngineTests/GeometryTests.cpp | 163 ++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 15 deletions(-) diff --git a/source/EngineKernels/GeometryKernels.cu b/source/EngineKernels/GeometryKernels.cu index 17446edd8b..6b115f5fc2 100644 --- a/source/EngineKernels/GeometryKernels.cu +++ b/source/EngineKernels/GeometryKernels.cu @@ -124,6 +124,50 @@ namespace return pos.x >= context.visibleTopLeft.x - context.cullingMargin && pos.x <= context.visibleBottomRight.x + context.cullingMargin && pos.y >= context.visibleTopLeft.y - context.cullingMargin && pos.y <= context.visibleBottomRight.y + context.cullingMargin; } + + __device__ __inline__ float getActualConnectionAngle(SimulationData const& data, Object* object, int connectionIndex) + { + auto displacement = data.objectMap.getCorrectedDirection(object->connections[connectionIndex].object->pos - object->pos); + return Math::angleOfVector(displacement); + } + + __device__ __inline__ void + getSortedConnectionIndicesByActualAngle(SimulationData const& data, Object* object, int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]) + { + for (int i = 0; i < object->numConnections; ++i) { + sortedConnectionIndices[i] = i; + } + for (int i = 1; i < object->numConnections; ++i) { + auto currentConnectionIndex = sortedConnectionIndices[i]; + auto currentAngle = getActualConnectionAngle(data, object, currentConnectionIndex); + auto currentObjectId = object->connections[currentConnectionIndex].object->id; + + auto insertIndex = i; + while (insertIndex > 0) { + auto previousConnectionIndex = sortedConnectionIndices[insertIndex - 1]; + auto previousAngle = getActualConnectionAngle(data, object, previousConnectionIndex); + auto previousObjectId = object->connections[previousConnectionIndex].object->id; + if (previousAngle < currentAngle || (fabsf(previousAngle - currentAngle) < NEAR_ZERO && previousObjectId < currentObjectId)) { + break; + } + sortedConnectionIndices[insertIndex] = previousConnectionIndex; + --insertIndex; + } + sortedConnectionIndices[insertIndex] = currentConnectionIndex; + } + } + + __device__ __inline__ int + getSortedConnectionPosition(SimulationData const& data, Object* object, Object* connectedObject, int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]) + { + getSortedConnectionIndicesByActualAngle(data, object, sortedConnectionIndices); + for (int sortedIndex = 0; sortedIndex < object->numConnections; ++sortedIndex) { + if (object->connections[sortedConnectionIndices[sortedIndex]].object == connectedObject) { + return sortedIndex; + } + } + return 0; + } } __global__ void cudaExtractObjectData(SimulationData data, ObjectVertexData* objectData, uint64_t* numObjects, GeometryExtractionContext context) @@ -267,31 +311,38 @@ __global__ void cudaExtractTriangleIndices(SimulationData data, unsigned int* tr if (object->numConnections <= 1) { continue; } - bool first = true; - int backIndices[MAX_OBJECT_CONNECTIONS]; - for (int i = 0, numConnections = object->numConnections; i < numConnections + 1; ++i) { - auto connectionIndex = i % numConnections; + int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; + getSortedConnectionIndicesByActualAngle(data, object, sortedConnectionIndices); + for (int sortedIndex = 0, numConnections = object->numConnections; sortedIndex < numConnections; ++sortedIndex) { + auto connectionIndex = sortedConnectionIndices[sortedIndex]; + auto prevIndex = sortedConnectionIndices[(sortedIndex + numConnections - 1) % numConnections]; auto const& connectedObject = object->connections[connectionIndex].object; - auto backIndex = connectedObject->getConnectionIndex(object); - backIndices[connectionIndex] = backIndex; - if (first) { - first = false; - continue; - } - auto prevIndex = (connectionIndex + numConnections - 1) % numConnections; auto const& prevConnectedObject = object->connections[prevIndex].object; - auto prevBackIndex = backIndices[prevIndex]; + + int connectedSortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; + auto backIndex = getSortedConnectionPosition(data, connectedObject, object, connectedSortedConnectionIndices); + int prevConnectedSortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; + auto prevBackIndex = getSortedConnectionPosition(data, prevConnectedObject, object, prevConnectedSortedConnectionIndices); // Triangle? - if (prevConnectedObject->getConnectedObject(prevBackIndex - 1) == connectedObject) { + if (prevConnectedObject + ->connections + [prevConnectedSortedConnectionIndices[(prevBackIndex + prevConnectedObject->numConnections - 1) % prevConnectedObject->numConnections]] + .object + == connectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id) { addTriangle(object, object->tempValue1.as_uint64, prevConnectedObject, connectedObject); } } // Rectangle? - auto fourthCellCandidate1 = connectedObject->getConnectedObject(backIndex + 1); - auto fourthCellCandidate2 = prevConnectedObject->getConnectedObject(prevBackIndex - 1); + auto fourthCellCandidate1 = + connectedObject->connections[connectedSortedConnectionIndices[(backIndex + 1) % connectedObject->numConnections]].object; + auto fourthCellCandidate2 = + prevConnectedObject + ->connections + [prevConnectedSortedConnectionIndices[(prevBackIndex + prevConnectedObject->numConnections - 1) % prevConnectedObject->numConnections]] + .object; if (fourthCellCandidate2 == fourthCellCandidate1 && fourthCellCandidate1 != object && fourthCellCandidate2 != object && connectedObject != prevConnectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id && object->id < fourthCellCandidate2->id) { diff --git a/source/EngineTests/GeometryTests.cpp b/source/EngineTests/GeometryTests.cpp index 63520d6426..4a0f3b77d9 100644 --- a/source/EngineTests/GeometryTests.cpp +++ b/source/EngineTests/GeometryTests.cpp @@ -5,6 +5,9 @@ #include +#include +#include +#include #include #include #include @@ -35,6 +38,78 @@ class GeometryTests : public IntegrationTestFramework } protected: + GLuint createShader(GLenum type, std::string_view source) + { + auto shader = glCreateShader(type); + auto* sourcePtr = source.data(); + auto sourceLength = static_cast(source.size()); + glShaderSource(shader, 1, &sourcePtr, &sourceLength); + glCompileShader(shader); + return shader; + } + + GLuint createTriangleShaderProgram() + { + auto vertexShader = createShader(GL_VERTEX_SHADER, Shaders::TriangleVS); + auto geometryShader = createShader(GL_GEOMETRY_SHADER, Shaders::TriangleGS); + auto fragmentShader = createShader(GL_FRAGMENT_SHADER, Shaders::TriangleFS); + + auto program = glCreateProgram(); + glAttachShader(program, vertexShader); + glAttachShader(program, geometryShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + + glDeleteShader(vertexShader); + glDeleteShader(geometryShader); + glDeleteShader(fragmentShader); + + return program; + } + + void setupTriangleVao(GeometryBuffers const& geometryBuffers) + { + glBindVertexArray(geometryBuffers->getVaoForTriangles()); + glBindBuffer(GL_ARRAY_BUFFER, geometryBuffers->getVboForObjects()); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)0); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + glVertexAttribIPointer(2, 1, GL_INT, sizeof(ObjectVertexData), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)(6 * sizeof(float) + sizeof(int))); + glEnableVertexAttribArray(3); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, geometryBuffers->getEboForTriangles()); + } + + int renderTrianglePixels(GeometryBuffers const& geometryBuffers) + { + setupTriangleVao(geometryBuffers); + + auto program = createTriangleShaderProgram(); + glUseProgram(program); + glUniform1f(glGetUniformLocation(program, "zoom"), 50.0f); + glUniform2f(glGetUniformLocation(program, "worldSize"), 1000.0f, 1000.0f); + glUniform2f(glGetUniformLocation(program, "rectUpperLeft"), 99.5f, 99.5f); + glUniform2f(glGetUniformLocation(program, "viewportSize"), 100.0f, 100.0f); + + glViewport(0, 0, 100, 100); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glDrawElements(GL_TRIANGLES, toInt(geometryBuffers->getNumObjects().triangleIndices), GL_UNSIGNED_INT, 0); + glDisable(GL_DEPTH_TEST); + glFinish(); + + std::vector pixels(100 * 100 * 3); + glReadPixels(0, 0, 100, 100, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); + glDeleteProgram(program); + + return std::count_if(pixels.begin(), pixels.end(), [](unsigned char value) { return value > 0; }); + } + GLFWwindow* _window = nullptr; }; @@ -216,6 +291,94 @@ TEST_F(GeometryTests, copyBuffers_triangle) EXPECT_EQ(6u, triangles.size()); } +TEST_F(GeometryTests, copyBuffers_triangleWithZeroReferenceAngle) +{ + auto data = Desc().addCreature({ + ObjectDesc().id(1).pos({100.0f, 100.0f}), + ObjectDesc().id(2).pos({101.0f, 100.0f}), + ObjectDesc().id(3).pos({100.5f, 100.866f}), + }); + data.addConnection(1, 2); + data.addConnection(2, 3); + data.addConnection(3, 1); + data.getConnectionRef(1, 2)._angleFromPrevious = 0.0f; + data.getConnectionRef(1, 3)._angleFromPrevious = 360.0f; + + _simulationFacade->setSimulationData(data); + auto geometryBuffers = _GeometryBuffers::create(); + RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; + + _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); + + auto numObjects = geometryBuffers->getNumObjects(); + EXPECT_EQ(3u, numObjects.objects); + EXPECT_EQ(6u, numObjects.lineIndices); + EXPECT_EQ(6u, numObjects.triangleIndices); + + auto triangles = geometryBuffers->getTriangleIndices(); + EXPECT_EQ(6u, triangles.size()); +} + +TEST_F(GeometryTests, renderTriangleWithZeroReferenceAngle) +{ + auto data = Desc().addCreature({ + ObjectDesc().id(1).pos({100.0f, 100.0f}), + ObjectDesc().id(2).pos({101.0f, 100.0f}), + ObjectDesc().id(3).pos({100.5f, 100.866f}), + }); + data.addConnection(1, 2); + data.addConnection(2, 3); + data.addConnection(3, 1); + data.getConnectionRef(1, 2)._angleFromPrevious = 0.0f; + data.getConnectionRef(1, 3)._angleFromPrevious = 360.0f; + + _simulationFacade->setSimulationData(data); + auto geometryBuffers = _GeometryBuffers::create(); + RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; + + _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); + + EXPECT_GT(renderTrianglePixels(geometryBuffers), 0); +} + +TEST_F(GeometryTests, renderTriangleFanWithZeroReferenceAnglesUsesActualConnectionOrder) +{ + auto createTriangleFan = [] { + auto result = Desc().addCreature({ + ObjectDesc().id(1).pos({100.0f, 100.0f}), + ObjectDesc().id(2).pos({99.0f, 100.0f}), + ObjectDesc().id(3).pos({100.0f, 101.0f}), + ObjectDesc().id(4).pos({101.0f, 100.0f}), + }); + result.addConnection(1, 2); + result.addConnection(1, 3); + result.addConnection(1, 4); + result.addConnection(2, 3); + result.addConnection(3, 4); + return result; + }; + + auto referenceData = createTriangleFan(); + _simulationFacade->setSimulationData(referenceData); + auto referenceGeometryBuffers = _GeometryBuffers::create(); + RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; + _simulationFacade->tryCopyBuffersFromCudaToOpenGL(referenceGeometryBuffers, visibleWorldRect); + auto const referencePixels = renderTrianglePixels(referenceGeometryBuffers); + + auto zeroAngleData = createTriangleFan(); + auto& centerConnections = zeroAngleData.getObjectRef(1)._connections; + std::swap(centerConnections.at(1), centerConnections.at(2)); + centerConnections.at(0)._angleFromPrevious = 180.0f; + centerConnections.at(1)._angleFromPrevious = 180.0f; + centerConnections.at(2)._angleFromPrevious = 0.0f; + + _simulationFacade->setSimulationData(zeroAngleData); + auto geometryBuffers = _GeometryBuffers::create(); + _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); + + EXPECT_EQ(referencePixels, renderTrianglePixels(geometryBuffers)); +} + TEST_F(GeometryTests, copyBuffers_quad) { auto data = Desc().addCreature({ From 178dab3b27899398cefc7f420afc4ff0a59aac8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 17:31:50 +0200 Subject: [PATCH 2/4] Remove added zero-angle geometry tests Agent-Logs-Url: https://github.com/chrxh/alien/sessions/3fc550f8-0c1d-45f8-91e1-1367a8dc132a Co-authored-by: chrxh <73127001+chrxh@users.noreply.github.com> --- source/EngineTests/GeometryTests.cpp | 163 --------------------------- 1 file changed, 163 deletions(-) diff --git a/source/EngineTests/GeometryTests.cpp b/source/EngineTests/GeometryTests.cpp index 4a0f3b77d9..63520d6426 100644 --- a/source/EngineTests/GeometryTests.cpp +++ b/source/EngineTests/GeometryTests.cpp @@ -5,9 +5,6 @@ #include -#include -#include -#include #include #include #include @@ -38,78 +35,6 @@ class GeometryTests : public IntegrationTestFramework } protected: - GLuint createShader(GLenum type, std::string_view source) - { - auto shader = glCreateShader(type); - auto* sourcePtr = source.data(); - auto sourceLength = static_cast(source.size()); - glShaderSource(shader, 1, &sourcePtr, &sourceLength); - glCompileShader(shader); - return shader; - } - - GLuint createTriangleShaderProgram() - { - auto vertexShader = createShader(GL_VERTEX_SHADER, Shaders::TriangleVS); - auto geometryShader = createShader(GL_GEOMETRY_SHADER, Shaders::TriangleGS); - auto fragmentShader = createShader(GL_FRAGMENT_SHADER, Shaders::TriangleFS); - - auto program = glCreateProgram(); - glAttachShader(program, vertexShader); - glAttachShader(program, geometryShader); - glAttachShader(program, fragmentShader); - glLinkProgram(program); - - glDeleteShader(vertexShader); - glDeleteShader(geometryShader); - glDeleteShader(fragmentShader); - - return program; - } - - void setupTriangleVao(GeometryBuffers const& geometryBuffers) - { - glBindVertexArray(geometryBuffers->getVaoForTriangles()); - glBindBuffer(GL_ARRAY_BUFFER, geometryBuffers->getVboForObjects()); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - glVertexAttribIPointer(2, 1, GL_INT, sizeof(ObjectVertexData), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); - glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(ObjectVertexData), (void*)(6 * sizeof(float) + sizeof(int))); - glEnableVertexAttribArray(3); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, geometryBuffers->getEboForTriangles()); - } - - int renderTrianglePixels(GeometryBuffers const& geometryBuffers) - { - setupTriangleVao(geometryBuffers); - - auto program = createTriangleShaderProgram(); - glUseProgram(program); - glUniform1f(glGetUniformLocation(program, "zoom"), 50.0f); - glUniform2f(glGetUniformLocation(program, "worldSize"), 1000.0f, 1000.0f); - glUniform2f(glGetUniformLocation(program, "rectUpperLeft"), 99.5f, 99.5f); - glUniform2f(glGetUniformLocation(program, "viewportSize"), 100.0f, 100.0f); - - glViewport(0, 0, 100, 100); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LESS); - glDrawElements(GL_TRIANGLES, toInt(geometryBuffers->getNumObjects().triangleIndices), GL_UNSIGNED_INT, 0); - glDisable(GL_DEPTH_TEST); - glFinish(); - - std::vector pixels(100 * 100 * 3); - glReadPixels(0, 0, 100, 100, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); - glDeleteProgram(program); - - return std::count_if(pixels.begin(), pixels.end(), [](unsigned char value) { return value > 0; }); - } - GLFWwindow* _window = nullptr; }; @@ -291,94 +216,6 @@ TEST_F(GeometryTests, copyBuffers_triangle) EXPECT_EQ(6u, triangles.size()); } -TEST_F(GeometryTests, copyBuffers_triangleWithZeroReferenceAngle) -{ - auto data = Desc().addCreature({ - ObjectDesc().id(1).pos({100.0f, 100.0f}), - ObjectDesc().id(2).pos({101.0f, 100.0f}), - ObjectDesc().id(3).pos({100.5f, 100.866f}), - }); - data.addConnection(1, 2); - data.addConnection(2, 3); - data.addConnection(3, 1); - data.getConnectionRef(1, 2)._angleFromPrevious = 0.0f; - data.getConnectionRef(1, 3)._angleFromPrevious = 360.0f; - - _simulationFacade->setSimulationData(data); - auto geometryBuffers = _GeometryBuffers::create(); - RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; - - _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); - - auto numObjects = geometryBuffers->getNumObjects(); - EXPECT_EQ(3u, numObjects.objects); - EXPECT_EQ(6u, numObjects.lineIndices); - EXPECT_EQ(6u, numObjects.triangleIndices); - - auto triangles = geometryBuffers->getTriangleIndices(); - EXPECT_EQ(6u, triangles.size()); -} - -TEST_F(GeometryTests, renderTriangleWithZeroReferenceAngle) -{ - auto data = Desc().addCreature({ - ObjectDesc().id(1).pos({100.0f, 100.0f}), - ObjectDesc().id(2).pos({101.0f, 100.0f}), - ObjectDesc().id(3).pos({100.5f, 100.866f}), - }); - data.addConnection(1, 2); - data.addConnection(2, 3); - data.addConnection(3, 1); - data.getConnectionRef(1, 2)._angleFromPrevious = 0.0f; - data.getConnectionRef(1, 3)._angleFromPrevious = 360.0f; - - _simulationFacade->setSimulationData(data); - auto geometryBuffers = _GeometryBuffers::create(); - RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; - - _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); - - EXPECT_GT(renderTrianglePixels(geometryBuffers), 0); -} - -TEST_F(GeometryTests, renderTriangleFanWithZeroReferenceAnglesUsesActualConnectionOrder) -{ - auto createTriangleFan = [] { - auto result = Desc().addCreature({ - ObjectDesc().id(1).pos({100.0f, 100.0f}), - ObjectDesc().id(2).pos({99.0f, 100.0f}), - ObjectDesc().id(3).pos({100.0f, 101.0f}), - ObjectDesc().id(4).pos({101.0f, 100.0f}), - }); - result.addConnection(1, 2); - result.addConnection(1, 3); - result.addConnection(1, 4); - result.addConnection(2, 3); - result.addConnection(3, 4); - return result; - }; - - auto referenceData = createTriangleFan(); - _simulationFacade->setSimulationData(referenceData); - auto referenceGeometryBuffers = _GeometryBuffers::create(); - RealRect visibleWorldRect{{0, 0}, {1000, 1000}}; - _simulationFacade->tryCopyBuffersFromCudaToOpenGL(referenceGeometryBuffers, visibleWorldRect); - auto const referencePixels = renderTrianglePixels(referenceGeometryBuffers); - - auto zeroAngleData = createTriangleFan(); - auto& centerConnections = zeroAngleData.getObjectRef(1)._connections; - std::swap(centerConnections.at(1), centerConnections.at(2)); - centerConnections.at(0)._angleFromPrevious = 180.0f; - centerConnections.at(1)._angleFromPrevious = 180.0f; - centerConnections.at(2)._angleFromPrevious = 0.0f; - - _simulationFacade->setSimulationData(zeroAngleData); - auto geometryBuffers = _GeometryBuffers::create(); - _simulationFacade->tryCopyBuffersFromCudaToOpenGL(geometryBuffers, visibleWorldRect); - - EXPECT_EQ(referencePixels, renderTrianglePixels(geometryBuffers)); -} - TEST_F(GeometryTests, copyBuffers_quad) { auto data = Desc().addCreature({ From 5f158e947f46abdb94e9417c85e172f5204a2e11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 21 May 2026 17:45:00 +0200 Subject: [PATCH 3/4] Handle zero-angle triangle extraction edgecase Agent-Logs-Url: https://github.com/chrxh/alien/sessions/4d630b49-4f05-4c79-ab00-b8fa4cf652dd Co-authored-by: chrxh <73127001+chrxh@users.noreply.github.com> --- source/EngineKernels/GeometryKernels.cu | 85 +++++++++---------------- 1 file changed, 30 insertions(+), 55 deletions(-) diff --git a/source/EngineKernels/GeometryKernels.cu b/source/EngineKernels/GeometryKernels.cu index 6b115f5fc2..37980cfe23 100644 --- a/source/EngineKernels/GeometryKernels.cu +++ b/source/EngineKernels/GeometryKernels.cu @@ -125,48 +125,29 @@ namespace && pos.y >= context.visibleTopLeft.y - context.cullingMargin && pos.y <= context.visibleBottomRight.y + context.cullingMargin; } - __device__ __inline__ float getActualConnectionAngle(SimulationData const& data, Object* object, int connectionIndex) + __device__ __inline__ int getPreviousConnectionIndexWithNonZeroAngle(Object* object, int connectionIndex) { - auto displacement = data.objectMap.getCorrectedDirection(object->connections[connectionIndex].object->pos - object->pos); - return Math::angleOfVector(displacement); - } - - __device__ __inline__ void - getSortedConnectionIndicesByActualAngle(SimulationData const& data, Object* object, int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]) - { - for (int i = 0; i < object->numConnections; ++i) { - sortedConnectionIndices[i] = i; - } - for (int i = 1; i < object->numConnections; ++i) { - auto currentConnectionIndex = sortedConnectionIndices[i]; - auto currentAngle = getActualConnectionAngle(data, object, currentConnectionIndex); - auto currentObjectId = object->connections[currentConnectionIndex].object->id; - - auto insertIndex = i; - while (insertIndex > 0) { - auto previousConnectionIndex = sortedConnectionIndices[insertIndex - 1]; - auto previousAngle = getActualConnectionAngle(data, object, previousConnectionIndex); - auto previousObjectId = object->connections[previousConnectionIndex].object->id; - if (previousAngle < currentAngle || (fabsf(previousAngle - currentAngle) < NEAR_ZERO && previousObjectId < currentObjectId)) { - break; - } - sortedConnectionIndices[insertIndex] = previousConnectionIndex; - --insertIndex; + auto result = (connectionIndex + object->numConnections - 1) % object->numConnections; + for (int i = 0; i < object->numConnections - 1; ++i) { + auto angleToCurrent = object->connections[(result + 1) % object->numConnections].angleFromPrevious; + if (angleToCurrent >= NEAR_ZERO) { + return result; } - sortedConnectionIndices[insertIndex] = currentConnectionIndex; + result = (result + object->numConnections - 1) % object->numConnections; } + return connectionIndex; } - __device__ __inline__ int - getSortedConnectionPosition(SimulationData const& data, Object* object, Object* connectedObject, int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]) + __device__ __inline__ int getNextConnectionIndexWithNonZeroAngle(Object* object, int connectionIndex) { - getSortedConnectionIndicesByActualAngle(data, object, sortedConnectionIndices); - for (int sortedIndex = 0; sortedIndex < object->numConnections; ++sortedIndex) { - if (object->connections[sortedConnectionIndices[sortedIndex]].object == connectedObject) { - return sortedIndex; + auto result = (connectionIndex + 1) % object->numConnections; + for (int i = 0; i < object->numConnections - 1; ++i) { + if (object->connections[result].angleFromPrevious >= NEAR_ZERO) { + return result; } + result = (result + 1) % object->numConnections; } - return 0; + return connectionIndex; } } @@ -311,38 +292,32 @@ __global__ void cudaExtractTriangleIndices(SimulationData data, unsigned int* tr if (object->numConnections <= 1) { continue; } - int sortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; - getSortedConnectionIndicesByActualAngle(data, object, sortedConnectionIndices); - for (int sortedIndex = 0, numConnections = object->numConnections; sortedIndex < numConnections; ++sortedIndex) { - auto connectionIndex = sortedConnectionIndices[sortedIndex]; - auto prevIndex = sortedConnectionIndices[(sortedIndex + numConnections - 1) % numConnections]; + for (int connectionIndex = 0, numConnections = object->numConnections; connectionIndex < numConnections; ++connectionIndex) { + auto prevIndex = getPreviousConnectionIndexWithNonZeroAngle(object, connectionIndex); + if (prevIndex == connectionIndex) { + continue; + } auto const& connectedObject = object->connections[connectionIndex].object; auto const& prevConnectedObject = object->connections[prevIndex].object; - int connectedSortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; - auto backIndex = getSortedConnectionPosition(data, connectedObject, object, connectedSortedConnectionIndices); - int prevConnectedSortedConnectionIndices[MAX_OBJECT_CONNECTIONS]; - auto prevBackIndex = getSortedConnectionPosition(data, prevConnectedObject, object, prevConnectedSortedConnectionIndices); + auto backIndex = connectedObject->getConnectionIndex(object); + auto prevBackIndex = prevConnectedObject->getConnectionIndex(object); + auto connectedNextIndex = getNextConnectionIndexWithNonZeroAngle(connectedObject, backIndex); + auto prevConnectedPrevIndex = getPreviousConnectionIndexWithNonZeroAngle(prevConnectedObject, prevBackIndex); + if (connectedNextIndex == backIndex || prevConnectedPrevIndex == prevBackIndex) { + continue; + } // Triangle? - if (prevConnectedObject - ->connections - [prevConnectedSortedConnectionIndices[(prevBackIndex + prevConnectedObject->numConnections - 1) % prevConnectedObject->numConnections]] - .object - == connectedObject) { + if (prevConnectedObject->connections[prevConnectedPrevIndex].object == connectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id) { addTriangle(object, object->tempValue1.as_uint64, prevConnectedObject, connectedObject); } } // Rectangle? - auto fourthCellCandidate1 = - connectedObject->connections[connectedSortedConnectionIndices[(backIndex + 1) % connectedObject->numConnections]].object; - auto fourthCellCandidate2 = - prevConnectedObject - ->connections - [prevConnectedSortedConnectionIndices[(prevBackIndex + prevConnectedObject->numConnections - 1) % prevConnectedObject->numConnections]] - .object; + auto fourthCellCandidate1 = connectedObject->connections[connectedNextIndex].object; + auto fourthCellCandidate2 = prevConnectedObject->connections[prevConnectedPrevIndex].object; if (fourthCellCandidate2 == fourthCellCandidate1 && fourthCellCandidate1 != object && fourthCellCandidate2 != object && connectedObject != prevConnectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id && object->id < fourthCellCandidate2->id) { From 9f1c67d232b5969932acb3e016daf792cc502560 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:33:24 +0200 Subject: [PATCH 4/4] Fix zero-angle absolute connection insertion Co-authored-by: chrxh <73127001+chrxh@users.noreply.github.com> --- source/EngineImpl/EngineWorker.cpp | 11 ++++ source/EngineImpl/EngineWorker.h | 1 + source/EngineImpl/SimulationCudaFacade.cu | 13 +++++ source/EngineImpl/SimulationCudaFacade.cuh | 1 + source/EngineImpl/SimulationFacadeImpl.cpp | 10 ++++ source/EngineImpl/SimulationFacadeImpl.h | 2 + source/EngineImpl/TestKernelsService.cu | 12 +++++ source/EngineImpl/TestKernelsService.cuh | 8 +++ source/EngineInterface/SimulationFacade.h | 2 + source/EngineKernels/GeometryKernels.cu | 52 +++++-------------- .../ObjectConnectionProcessor.cuh | 9 +++- source/EngineKernels/TestKernels.cu | 29 +++++++++++ source/EngineKernels/TestKernels.cuh | 7 +++ source/EngineTests/ObjectConnectionTests.cpp | 35 +++++++++++++ 14 files changed, 151 insertions(+), 41 deletions(-) diff --git a/source/EngineImpl/EngineWorker.cpp b/source/EngineImpl/EngineWorker.cpp index 6d6396e781..642494bcc4 100644 --- a/source/EngineImpl/EngineWorker.cpp +++ b/source/EngineImpl/EngineWorker.cpp @@ -472,6 +472,17 @@ void EngineWorker::testOnly_createConnection(uint64_t objectId1, uint64_t object _simulationCudaFacade->testOnly_createConnection(objectId1, objectId2); } +void EngineWorker::testOnly_createConnectionWithAbsAngle( + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2) +{ + EngineWorkerGuard access(this); + _simulationCudaFacade->testOnly_createConnectionWithAbsAngle(objectId1, objectId2, desiredDistance, desiredAbsAngle1, desiredAbsAngle2); +} + void EngineWorker::testOnly_cleanupAfterTimestep() { EngineWorkerGuard access(this); diff --git a/source/EngineImpl/EngineWorker.h b/source/EngineImpl/EngineWorker.h index dd673a975e..79d83d4dc3 100644 --- a/source/EngineImpl/EngineWorker.h +++ b/source/EngineImpl/EngineWorker.h @@ -120,6 +120,7 @@ class EngineWorker // Only for tests void testOnly_mutate(uint64_t objectId); void testOnly_createConnection(uint64_t objectId1, uint64_t objectId2); + void testOnly_createConnectionWithAbsAngle(uint64_t objectId1, uint64_t objectId2, float desiredDistance, float desiredAbsAngle1, float desiredAbsAngle2); void testOnly_cleanupAfterTimestep(); void testOnly_cleanupAfterDataManipulation(); void testOnly_resizeArrays(ArraySizesForGpuEntities const& sizeDelta); diff --git a/source/EngineImpl/SimulationCudaFacade.cu b/source/EngineImpl/SimulationCudaFacade.cu index 3b9d15e4ab..c828b30bfa 100644 --- a/source/EngineImpl/SimulationCudaFacade.cu +++ b/source/EngineImpl/SimulationCudaFacade.cu @@ -595,6 +595,19 @@ void _SimulationCudaFacade::testOnly_createConnection(uint64_t objectId1, uint64 syncAndCheck(); } +void _SimulationCudaFacade::testOnly_createConnectionWithAbsAngle( + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2) +{ + checkAndProcessSimulationParameterChanges(); + TestKernelsService::get().testOnly_createConnectionWithAbsAngle( + _settings.cudaSettings, getSimulationDataPtrCopy(), objectId1, objectId2, desiredDistance, desiredAbsAngle1, desiredAbsAngle2); + syncAndCheck(); +} + void _SimulationCudaFacade::testOnly_cleanupAfterTimestep() { checkAndProcessSimulationParameterChanges(); diff --git a/source/EngineImpl/SimulationCudaFacade.cuh b/source/EngineImpl/SimulationCudaFacade.cuh index da0fcd493a..e8b5646bf0 100644 --- a/source/EngineImpl/SimulationCudaFacade.cuh +++ b/source/EngineImpl/SimulationCudaFacade.cuh @@ -106,6 +106,7 @@ public: // Only for tests void testOnly_mutate(uint64_t objectId); void testOnly_createConnection(uint64_t objectId1, uint64_t objectId2); + void testOnly_createConnectionWithAbsAngle(uint64_t objectId1, uint64_t objectId2, float desiredDistance, float desiredAbsAngle1, float desiredAbsAngle2); void testOnly_cleanupAfterTimestep(); void testOnly_cleanupAfterDataManipulation(); void testOnly_resizeArrays(ArraySizesForGpuEntities const& sizeDelta); diff --git a/source/EngineImpl/SimulationFacadeImpl.cpp b/source/EngineImpl/SimulationFacadeImpl.cpp index d55b86de92..3aa36e15b1 100644 --- a/source/EngineImpl/SimulationFacadeImpl.cpp +++ b/source/EngineImpl/SimulationFacadeImpl.cpp @@ -387,6 +387,16 @@ void _SimulationFacadeImpl::testOnly_createConnection(uint64_t objectId1, uint64 _worker.testOnly_createConnection(objectId1, objectId2); } +void _SimulationFacadeImpl::testOnly_createConnectionWithAbsAngle( + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2) +{ + _worker.testOnly_createConnectionWithAbsAngle(objectId1, objectId2, desiredDistance, desiredAbsAngle1, desiredAbsAngle2); +} + void _SimulationFacadeImpl::testOnly_cleanupAfterTimestep() { _worker.testOnly_cleanupAfterTimestep(); diff --git a/source/EngineImpl/SimulationFacadeImpl.h b/source/EngineImpl/SimulationFacadeImpl.h index ec495c0b66..e53322372e 100644 --- a/source/EngineImpl/SimulationFacadeImpl.h +++ b/source/EngineImpl/SimulationFacadeImpl.h @@ -108,6 +108,8 @@ class _SimulationFacadeImpl : public _SimulationFacade // for tests only void testOnly_mutate(uint64_t objectId) override; void testOnly_createConnection(uint64_t objectId1, uint64_t objectId2) override; + void testOnly_createConnectionWithAbsAngle(uint64_t objectId1, uint64_t objectId2, float desiredDistance, float desiredAbsAngle1, float desiredAbsAngle2) + override; void testOnly_cleanupAfterTimestep() override; void testOnly_cleanupAfterDataManipulation() override; void testOnly_resizeArrays(ArraySizesForGpuEntities const& sizeDelta) override; diff --git a/source/EngineImpl/TestKernelsService.cu b/source/EngineImpl/TestKernelsService.cu index c6b8ecc433..95400f17ba 100644 --- a/source/EngineImpl/TestKernelsService.cu +++ b/source/EngineImpl/TestKernelsService.cu @@ -25,6 +25,18 @@ void TestKernelsService::testOnly_createConnection(CudaSettings const& gpuSettin KERNEL_CALL_1_1(cudaTestCreateConnection, data, objectId1, objectId2); } +void TestKernelsService::testOnly_createConnectionWithAbsAngle( + CudaSettings const& gpuSettings, + SimulationData const& data, + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2) +{ + KERNEL_CALL_1_1(cudaTestCreateConnectionWithAbsAngle, data, objectId1, objectId2, desiredDistance, desiredAbsAngle1, desiredAbsAngle2); +} + bool TestKernelsService::testOnly_isDataValid(CudaSettings const& gpuSettings, SimulationData const& data) { setValueToDevice(_cudaBoolResult, true); diff --git a/source/EngineImpl/TestKernelsService.cuh b/source/EngineImpl/TestKernelsService.cuh index 798286677c..a7ae8d934f 100644 --- a/source/EngineImpl/TestKernelsService.cuh +++ b/source/EngineImpl/TestKernelsService.cuh @@ -16,6 +16,14 @@ public: void testOnly_mutate(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId); void testOnly_createConnection(CudaSettings const& gpuSettings, SimulationData const& data, uint64_t objectId1, uint64_t objectId2); + void testOnly_createConnectionWithAbsAngle( + CudaSettings const& gpuSettings, + SimulationData const& data, + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2); bool testOnly_isDataValid(CudaSettings const& gpuSettings, SimulationData const& data); private: diff --git a/source/EngineInterface/SimulationFacade.h b/source/EngineInterface/SimulationFacade.h index 13c40a68c0..904d0e2149 100644 --- a/source/EngineInterface/SimulationFacade.h +++ b/source/EngineInterface/SimulationFacade.h @@ -122,6 +122,8 @@ class _SimulationFacade //**************** virtual void testOnly_mutate(uint64_t objectId) = 0; virtual void testOnly_createConnection(uint64_t objectId1, uint64_t objectId2) = 0; + virtual void + testOnly_createConnectionWithAbsAngle(uint64_t objectId1, uint64_t objectId2, float desiredDistance, float desiredAbsAngle1, float desiredAbsAngle2) = 0; virtual void testOnly_cleanupAfterTimestep() = 0; virtual void testOnly_cleanupAfterDataManipulation() = 0; virtual void testOnly_resizeArrays(ArraySizesForGpuEntities const& sizeDelta) = 0; diff --git a/source/EngineKernels/GeometryKernels.cu b/source/EngineKernels/GeometryKernels.cu index 37980cfe23..17446edd8b 100644 --- a/source/EngineKernels/GeometryKernels.cu +++ b/source/EngineKernels/GeometryKernels.cu @@ -124,31 +124,6 @@ namespace return pos.x >= context.visibleTopLeft.x - context.cullingMargin && pos.x <= context.visibleBottomRight.x + context.cullingMargin && pos.y >= context.visibleTopLeft.y - context.cullingMargin && pos.y <= context.visibleBottomRight.y + context.cullingMargin; } - - __device__ __inline__ int getPreviousConnectionIndexWithNonZeroAngle(Object* object, int connectionIndex) - { - auto result = (connectionIndex + object->numConnections - 1) % object->numConnections; - for (int i = 0; i < object->numConnections - 1; ++i) { - auto angleToCurrent = object->connections[(result + 1) % object->numConnections].angleFromPrevious; - if (angleToCurrent >= NEAR_ZERO) { - return result; - } - result = (result + object->numConnections - 1) % object->numConnections; - } - return connectionIndex; - } - - __device__ __inline__ int getNextConnectionIndexWithNonZeroAngle(Object* object, int connectionIndex) - { - auto result = (connectionIndex + 1) % object->numConnections; - for (int i = 0; i < object->numConnections - 1; ++i) { - if (object->connections[result].angleFromPrevious >= NEAR_ZERO) { - return result; - } - result = (result + 1) % object->numConnections; - } - return connectionIndex; - } } __global__ void cudaExtractObjectData(SimulationData data, ObjectVertexData* objectData, uint64_t* numObjects, GeometryExtractionContext context) @@ -292,32 +267,31 @@ __global__ void cudaExtractTriangleIndices(SimulationData data, unsigned int* tr if (object->numConnections <= 1) { continue; } - for (int connectionIndex = 0, numConnections = object->numConnections; connectionIndex < numConnections; ++connectionIndex) { - auto prevIndex = getPreviousConnectionIndexWithNonZeroAngle(object, connectionIndex); - if (prevIndex == connectionIndex) { - continue; - } + bool first = true; + int backIndices[MAX_OBJECT_CONNECTIONS]; + for (int i = 0, numConnections = object->numConnections; i < numConnections + 1; ++i) { + auto connectionIndex = i % numConnections; auto const& connectedObject = object->connections[connectionIndex].object; - auto const& prevConnectedObject = object->connections[prevIndex].object; - auto backIndex = connectedObject->getConnectionIndex(object); - auto prevBackIndex = prevConnectedObject->getConnectionIndex(object); - auto connectedNextIndex = getNextConnectionIndexWithNonZeroAngle(connectedObject, backIndex); - auto prevConnectedPrevIndex = getPreviousConnectionIndexWithNonZeroAngle(prevConnectedObject, prevBackIndex); - if (connectedNextIndex == backIndex || prevConnectedPrevIndex == prevBackIndex) { + backIndices[connectionIndex] = backIndex; + if (first) { + first = false; continue; } + auto prevIndex = (connectionIndex + numConnections - 1) % numConnections; + auto const& prevConnectedObject = object->connections[prevIndex].object; + auto prevBackIndex = backIndices[prevIndex]; // Triangle? - if (prevConnectedObject->connections[prevConnectedPrevIndex].object == connectedObject) { + if (prevConnectedObject->getConnectedObject(prevBackIndex - 1) == connectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id) { addTriangle(object, object->tempValue1.as_uint64, prevConnectedObject, connectedObject); } } // Rectangle? - auto fourthCellCandidate1 = connectedObject->connections[connectedNextIndex].object; - auto fourthCellCandidate2 = prevConnectedObject->connections[prevConnectedPrevIndex].object; + auto fourthCellCandidate1 = connectedObject->getConnectedObject(backIndex + 1); + auto fourthCellCandidate2 = prevConnectedObject->getConnectedObject(prevBackIndex - 1); if (fourthCellCandidate2 == fourthCellCandidate1 && fourthCellCandidate1 != object && fourthCellCandidate2 != object && connectedObject != prevConnectedObject) { if (object->id < connectedObject->id && object->id < prevConnectedObject->id && object->id < fourthCellCandidate2->id) { diff --git a/source/EngineKernels/ObjectConnectionProcessor.cuh b/source/EngineKernels/ObjectConnectionProcessor.cuh index 69c4343c44..e1fec97569 100644 --- a/source/EngineKernels/ObjectConnectionProcessor.cuh +++ b/source/EngineKernels/ObjectConnectionProcessor.cuh @@ -435,14 +435,19 @@ ObjectConnectionProcessor::tryAddConnectionWithAbsAngle_oneWay(Object* object1, auto insertIndex = 0; auto summedAngle = 0.0f; desiredAbsAngle = Math::getNormalizedAngle(desiredAbsAngle, 0.0f); + if (desiredAbsAngle < NEAR_ZERO) { + desiredAbsAngle = 360.0f; + } for (int i = 1; i <= n; ++i) { auto const& angleFromPrevious = object1->getConnection(i).angleFromPrevious; + auto nextSummedAngle = summedAngle + angleFromPrevious; - if (desiredAbsAngle >= summedAngle - NEAR_ZERO && desiredAbsAngle < summedAngle + angleFromPrevious) { + if (desiredAbsAngle > summedAngle + NEAR_ZERO && desiredAbsAngle <= nextSummedAngle + NEAR_ZERO) { insertIndex = i; + desiredAbsAngle = min(desiredAbsAngle, nextSummedAngle); break; } - summedAngle += angleFromPrevious; + summedAngle = nextSummedAngle; } DEVICE_CHECK(insertIndex > 0); diff --git a/source/EngineKernels/TestKernels.cu b/source/EngineKernels/TestKernels.cu index 4fa95167b3..4e66d3991f 100644 --- a/source/EngineKernels/TestKernels.cu +++ b/source/EngineKernels/TestKernels.cu @@ -71,6 +71,35 @@ __global__ void cudaTestCreateConnection(SimulationData data, uint64_t objectId1 } } +__global__ void cudaTestCreateConnectionWithAbsAngle( + SimulationData data, + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2) +{ + DEVICE_CHECK(blockDim.x == 1 && gridDim.x == 1); + + auto& objects = data.entities.objects; + auto partition = calcSystemThreadPartition(objects.getNumEntries()); + Object* object1 = nullptr; + Object* object2 = nullptr; + for (int index = partition.startIndex; index <= partition.endIndex; index += partition.step) { + auto& object = objects.at(index); + if (object->id == objectId1) { + object1 = object; + } + if (object->id == objectId2) { + object2 = object; + } + } + + if (object1 != nullptr && object2 != nullptr) { + ObjectConnectionProcessor::tryAddConnectionWithAbsAngle(data, object1, object2, desiredDistance, desiredAbsAngle1, desiredAbsAngle2); + } +} + namespace { __device__ bool isEnergyValid(float energy) diff --git a/source/EngineKernels/TestKernels.cuh b/source/EngineKernels/TestKernels.cuh index 6196f013e2..b980330e57 100644 --- a/source/EngineKernels/TestKernels.cuh +++ b/source/EngineKernels/TestKernels.cuh @@ -7,4 +7,11 @@ __global__ void cudaTestMutate(SimulationData data, uint64_t objectId); __global__ void cudaTestCreateConnection(SimulationData data, uint64_t objectId1, uint64_t objectId2); +__global__ void cudaTestCreateConnectionWithAbsAngle( + SimulationData data, + uint64_t objectId1, + uint64_t objectId2, + float desiredDistance, + float desiredAbsAngle1, + float desiredAbsAngle2); __global__ void cudaTestIsDataValid(SimulationData data, bool* result); diff --git a/source/EngineTests/ObjectConnectionTests.cpp b/source/EngineTests/ObjectConnectionTests.cpp index 794d618b2f..eb7a430cd4 100644 --- a/source/EngineTests/ObjectConnectionTests.cpp +++ b/source/EngineTests/ObjectConnectionTests.cpp @@ -149,3 +149,38 @@ TEST_F(ObjectConnectionTests, addThirdConnection2) EXPECT_TRUE(approxCompare(1.0f, connection3._distance)); EXPECT_TRUE(approxCompare(90.0f, connection3._angleFromPrevious)); } + +TEST_F(ObjectConnectionTests, addConnectionWithZeroAbsAngleInsertsBeforeReferenceConnection) +{ + auto data = Desc().objects({ + ObjectDesc().id(1).pos({0, 0}).type(SolidDesc()), + ObjectDesc().id(2).pos({1, 0}).type(SolidDesc()), + ObjectDesc().id(3).pos({0, 1}).type(SolidDesc()), + ObjectDesc().id(4).pos({-1, 0}).type(SolidDesc()), + }); + data.addConnection(1, 2); + data.addConnection(1, 3); + _simulationFacade->setSimulationData(data); + _simulationFacade->testOnly_createConnectionWithAbsAngle(1, 4, 1.0f, 0.0f, 0.0f); + + auto actualData = _simulationFacade->getSimulationData(); + ASSERT_EQ(4, actualData._objects.size()); + + auto object = actualData.getObjectRef(1); + ASSERT_EQ(3, object._connections.size()); + + auto connection1 = object._connections.at(0); + EXPECT_EQ(2, connection1._objectId); + EXPECT_TRUE(approxCompare(1.0f, connection1._distance)); + EXPECT_TRUE(approxCompare(0.0f, connection1._angleFromPrevious)); + + auto connection2 = object._connections.at(1); + EXPECT_EQ(3, connection2._objectId); + EXPECT_TRUE(approxCompare(1.0f, connection2._distance)); + EXPECT_TRUE(approxCompare(90.0f, connection2._angleFromPrevious)); + + auto connection3 = object._connections.at(2); + EXPECT_EQ(4, connection3._objectId); + EXPECT_TRUE(approxCompare(1.0f, connection3._distance)); + EXPECT_TRUE(approxCompare(270.0f, connection3._angleFromPrevious)); +}