/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using NUnit.Framework;
using Opc.Ua;
using Robotics.IntentEnabledRobot.Simulation;
namespace Opc.Ua.Robotics.Tests
{
///
/// Covers the collision model that keeps the arm out of the cell's furniture. The case
/// that matters most is a link spanning a surface while both of its ends stay clear:
/// the height test this replaces sampled joint origins only, so it passed exactly the
/// configurations that render an arm passing through its own bench.
///
[TestFixture]
[Category("Robotics")]
[Parallelizable]
public sealed class SimulatedCollisionModelTests
{
[Test]
public void LinkSpanningTheBenchIsRejectedThoughBothEndsAreAboveIt()
{
SimulatedCollisionModel model = BenchModel();
// Shoulder high, elbow below the bench top, wrist high again: every joint
// origin that the old height test sampled is above the surface except the one
// in the middle, and the links between them sweep straight through the table.
double[] throughBench = [-0.30, 0.0, 0.20, 0.0, 0.0, -0.02, 0.30, 0.0, 0.20];
Assert.That(model.IsClear(throughBench, out string hit), Is.False,
"A link that reaches below the bench between two joints must be refused.");
Assert.That(hit, Is.EqualTo("Bench"));
}
[Test]
public void LinkGrazingTheBenchSurfaceIsRejected()
{
SimulatedCollisionModel model = BenchModel();
// Both ends clear the bench, but only by less than the link's own thickness.
double[] grazing = [-0.30, 0.0, 0.01, 0.30, 0.0, 0.01, 0.30, 0.0, 0.30];
Assert.That(model.IsClear(grazing, out string hit), Is.False,
"A link is not a line: passing a hand's breadth under the surface is not " +
"clear just because the centre line is above it.");
Assert.That(hit, Is.EqualTo("Bench"));
}
[Test]
public void ArmAboveTheBenchIsClear()
{
SimulatedCollisionModel model = BenchModel();
double[] points = [0.0, 0.0, 0.40, 0.30, 0.0, 0.40, 0.38, 0.0, 0.25];
Assert.That(model.IsClear(points, out _), Is.True);
}
[Test]
public void LinkThroughABinWallIsRejected()
{
SimulatedCollisionModel model = BenchModel();
// Held clear of the bench, but crossing the east wall of the bin at a height
// the wall occupies.
double[] points = [0.38, 0.0, 0.045, 0.60, 0.0, 0.045, 0.60, 0.0, 0.20];
Assert.That(model.IsClear(points, out string hit), Is.False);
Assert.That(hit, Is.EqualTo("BinWallE"));
}
[Test]
public void ToolMayDescendIntoTheBinBetweenItsWalls()
{
SimulatedCollisionModel model = BenchModel();
// Wrist above the bin, tool point down inside it, where the parts lie.
double[] points = [0.38, 0.0, 0.40, 0.38, 0.0, 0.22, 0.38, 0.0, 0.056];
Assert.That(model.IsClear(points, out _), Is.True,
"The bin's inside is open, or the cell could never pick anything out of it.");
}
[Test]
public void ToolMayReachAPartLyingOnTheBench()
{
SimulatedCollisionModel model = BenchModel();
// The tool is slender and is meant to approach surfaces: a gripper that cannot
// come within a link's thickness of the bench cannot pick anything off it.
double[] points = [-0.20, 0.0, 0.40, -0.20, 0.0, 0.22, -0.20, 0.0, 0.021];
Assert.That(model.IsClear(points, out _), Is.True);
}
[Test]
public void ModelWithoutObstaclesAllowsEverything()
{
var model = new SimulatedCollisionModel(ArrayOf.Empty, 0.03, 0.008);
double[] points = [0.0, 0.0, -1.0, 0.0, 0.0, -2.0];
Assert.That(model.IsClear(points, out _), Is.True);
}
[Test]
public void PerSegmentRadiusKeepsAThinWristClearWhereAThickLinkWouldCollide()
{
SimulatedObstacleBox[] obstacles =
[
new("NearbyPart", 0.45, 0.04, 0.10, 0.01, 0.19, 0.21)
];
double[] points =
[
0.0, 0.0, 0.20,
0.30, 0.0, 0.20,
0.60, 0.0, 0.20
];
var perSegment = new SimulatedCollisionModel(
ArrayOf.Create(obstacles.AsSpan()),
ArrayOf.Create([0.05, 0.01]));
var uniformlyThick = new SimulatedCollisionModel(
ArrayOf.Create(obstacles.AsSpan()),
0.05,
0.05);
Assert.Multiple(() =>
{
Assert.That(perSegment.IsClear(points, out _), Is.True);
Assert.That(uniformlyThick.IsClear(points, out string hit), Is.False);
Assert.That(hit, Is.EqualTo("NearbyPart"));
});
}
[Test]
public void WorkpieceBoxAllowsSupportContactButRejectsVolumeOverlap()
{
var model = new SimulatedCollisionModel(
ArrayOf.Create(
[
new SimulatedObstacleBox("Support", 0.0, 0.0, 0.40, 0.40, -0.10, 0.0)
]),
0.02,
0.01);
Assert.Multiple(() =>
{
Assert.That(
model.IsBoxClear(0.0, 0.0, 0.05, 0.10, 0.10, 0.10, out _),
Is.True,
"Touching a support is valid contact, not penetration.");
Assert.That(
model.IsBoxClear(0.0, 0.0, 0.04, 0.10, 0.10, 0.10, out string hit),
Is.False);
Assert.That(hit, Is.EqualTo("Support"));
});
}
private static SimulatedCollisionModel BenchModel()
{
SimulatedObstacleBox[] obstacles =
[
new("Bench", 0.0, 0.0, 1.4000, 0.9000, -0.0690, 0.0000),
new("BinWallN", 0.3800, 0.1170, 0.2800, 0.0060, -0.0070, 0.0330),
new("BinWallS", 0.3800, -0.1170, 0.2800, 0.0060, -0.0070, 0.0330),
new("BinWallE", 0.5170, 0.0000, 0.0060, 0.2400, -0.0070, 0.0330),
new("BinWallW", 0.2430, 0.0000, 0.0060, 0.2400, -0.0070, 0.0330)
];
return new SimulatedCollisionModel(ArrayOf.Create(obstacles.AsSpan()), 0.030, 0.008);
}
}
}