/* ========================================================================
* 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 Opc.Ua;
namespace Robotics.IntentEnabledRobot.Simulation
{
///
/// An axis-aligned solid the arm must not pass through, in the arm's base frame.
///
///
/// What the box represents, for diagnostics.
///
///
/// Centre of the box on X.
///
///
/// Centre of the box on Y.
///
///
/// Full extent on X.
///
///
/// Full extent on Y.
///
///
/// Underside of the box.
///
///
/// Top of the box.
///
///
/// How much of the link's own thickness has to stay outside this solid. A wall or a
/// post is an object the arm must not intersect, so it takes the full link radius. A
/// work surface is different: it is a half-space the arm must not go *below*, and a
/// link that comes right down to the table is doing its job - a gripper has to reach a
/// part lying on it. Inflating the bench by a link radius refuses exactly the poses the
/// cell exists to make, because with the tool vertical this arm's wrist stacks along
/// the tool axis and J4 ends up 22 mm above the surface when the tool is 56 mm above
/// it. Zero here means "not below", which is what a work surface actually asks for.
///
public readonly record struct SimulatedObstacleBox(
string Name,
double CentreX,
double CentreY,
double SizeX,
double SizeY,
double MinZ,
double MaxZ,
double Clearance = double.NaN);
///
/// Decides whether an arm configuration puts any part of the arm inside the cell's
/// furniture.
///
///
///
/// This replaces a test that compared each joint origin against a single
/// horizontal plane. That test passes a configuration whose links pass clean through
/// the bench, because the point where a link ends can sit above the plane while the
/// length of the link between two joints dips below it - which is exactly what "the
/// arm moves through the table" looks like. It also had nothing to say about the bin
/// or the fixture, so the wrist could come to rest inside the bin and no part of the
/// simulation objected.
///
///
/// Each link is treated as a capsule between consecutive joint origins and tested
/// against axis-aligned boxes. That is a coarse model - it is not a physics engine,
/// there is no contact response and nothing here pushes back - but it answers the one
/// question the arm needs answered before it moves: would this configuration put a
/// link inside something solid.
///
///
public sealed class SimulatedCollisionModel
{
///
/// Creates a collision model over a set of obstacles.
///
///
/// The solids the arm must stay out of, in the arm's base frame.
///
///
/// How thick the arm's links are treated as being, in metres.
///
///
/// How thick the tool beyond the flange is treated as being, in metres. It is
/// slender next to a link, and unlike a link it is meant to approach surfaces
/// closely - a gripper that cannot come within a link's thickness of the bench
/// cannot pick anything up off it.
///
public SimulatedCollisionModel(
ArrayOf obstacles,
double linkRadius,
double toolRadius)
{
Obstacles = obstacles;
LinkRadius = linkRadius;
ToolRadius = toolRadius;
}
///
/// Creates a collision model with one radius per chain segment.
///
/// Fixed solids in the arm base frame.
///
/// Radius for each consecutive point pair supplied to .
///
public SimulatedCollisionModel(
ArrayOf obstacles,
ArrayOf segmentRadii)
{
if (segmentRadii.IsEmpty)
{
throw new ArgumentException(
"At least one segment radius is required.",
nameof(segmentRadii));
}
Obstacles = obstacles;
SegmentRadii = segmentRadii;
LinkRadius = segmentRadii[0];
ToolRadius = segmentRadii[^1];
}
///
/// Gets the solids the arm must stay out of.
///
public ArrayOf Obstacles { get; }
///
/// Gets the radius the arm's links are treated as having.
///
public double LinkRadius { get; }
///
/// Gets the radius the tool beyond the flange is treated as having.
///
public double ToolRadius { get; }
///
/// Gets per-segment radii when the arm provides them.
///
public ArrayOf SegmentRadii { get; }
///
/// Gets or sets the solids that move, tested alongside the fixed ones.
///
///
/// The furniture is fixed, but the workpieces are not: a stack built on the fixture
/// is as solid as the fixture under it, and an arm that reaches through it looks
/// exactly as wrong as one reaching through the bench. The host owns where the parts
/// are, so it republishes them here when it moves one - and leaves out whatever the
/// gripper is carrying, since a part travelling with the tool cannot be an obstacle
/// to it.
///
public ArrayOf MovingObstacles { get; set; }
///
/// Gets whether a chain of joint origins, plus the tool point beyond the last one,
/// stays clear of every obstacle.
///
///
/// Successive points along the arm, each as three coordinates in the arm's base
/// frame: the joint origins followed by the tool centre point.
///
///
/// The first obstacle the arm intersects, when the result is false.
///
public bool IsClear(ReadOnlySpan points, out string hit)
{
hit = string.Empty;
if (points.Length < 6 || (Obstacles.Count == 0 && MovingObstacles.Count == 0))
{
return true;
}
ReadOnlySpan boxes = Obstacles.Span;
ReadOnlySpan moving = MovingObstacles.Span;
int lastSegment = points.Length - 6;
for (int segment = 0; segment <= lastSegment; segment += 3)
{
int segmentIndex = segment / 3;
double radius = segmentIndex < SegmentRadii.Count
? SegmentRadii[segmentIndex]
: segment == lastSegment
? ToolRadius
: LinkRadius;
double ax = points[segment];
double ay = points[segment + 1];
double az = points[segment + 2];
double bx = points[segment + 3];
double by = points[segment + 4];
double bz = points[segment + 5];
if (!IsSegmentClear(boxes, ax, ay, az, bx, by, bz, radius, out hit) ||
!IsSegmentClear(moving, ax, ay, az, bx, by, bz, radius, out hit))
{
return false;
}
}
return true;
}
///
/// Gets whether an axis-aligned workpiece stays clear of every obstacle.
///
///
public bool IsBoxClear(
double centreX,
double centreY,
double centreZ,
double sizeX,
double sizeY,
double sizeZ,
out string hit)
{
if (sizeX < 0.0 || sizeY < 0.0 || sizeZ < 0.0)
{
throw new ArgumentOutOfRangeException(
nameof(sizeX),
"Workpiece dimensions must be non-negative.");
}
if (!IsBoxClear(
Obstacles.Span,
centreX,
centreY,
centreZ,
sizeX,
sizeY,
sizeZ,
out hit))
{
return false;
}
return IsBoxClear(
MovingObstacles.Span,
centreX,
centreY,
centreZ,
sizeX,
sizeY,
sizeZ,
out hit);
}
private static bool IsBoxClear(
ReadOnlySpan boxes,
double centreX,
double centreY,
double centreZ,
double sizeX,
double sizeY,
double sizeZ,
out string hit)
{
hit = string.Empty;
double minX = centreX - (sizeX * 0.5);
double maxX = centreX + (sizeX * 0.5);
double minY = centreY - (sizeY * 0.5);
double maxY = centreY + (sizeY * 0.5);
double minZ = centreZ - (sizeZ * 0.5);
double maxZ = centreZ + (sizeZ * 0.5);
for (int ii = 0; ii < boxes.Length; ii++)
{
SimulatedObstacleBox box = boxes[ii];
double boxMinX = box.CentreX - (box.SizeX * 0.5);
double boxMaxX = box.CentreX + (box.SizeX * 0.5);
double boxMinY = box.CentreY - (box.SizeY * 0.5);
double boxMaxY = box.CentreY + (box.SizeY * 0.5);
if (maxX > boxMinX + ContactToleranceMetres &&
minX < boxMaxX - ContactToleranceMetres &&
maxY > boxMinY + ContactToleranceMetres &&
minY < boxMaxY - ContactToleranceMetres &&
maxZ > box.MinZ + ContactToleranceMetres &&
minZ < box.MaxZ - ContactToleranceMetres)
{
hit = box.Name;
return false;
}
}
return true;
}
///
/// Gets whether one link stays clear of a set of solids.
///
private static bool IsSegmentClear(
ReadOnlySpan boxes,
double ax, double ay, double az,
double bx, double by, double bz,
double radius,
out string hit)
{
hit = string.Empty;
for (int ii = 0; ii < boxes.Length; ii++)
{
double clearance = double.IsNaN(boxes[ii].Clearance) ? radius : boxes[ii].Clearance;
if (IntersectsSegment(boxes[ii], ax, ay, az, bx, by, bz, clearance))
{
hit = boxes[ii].Name;
return false;
}
}
return true;
}
///
/// Gets whether a capsule of around the segment from A to
/// B reaches inside a box.
///
///
/// The segment is sampled rather than solved analytically. Sampling is enough here
/// because the boxes are large next to the sample spacing, and it keeps the test
/// short enough to run for every candidate solution and every step of a path.
///
private static bool IntersectsSegment(
SimulatedObstacleBox box,
double ax, double ay, double az,
double bx, double by, double bz,
double radius)
{
double minX = box.CentreX - (box.SizeX * 0.5) - radius;
double maxX = box.CentreX + (box.SizeX * 0.5) + radius;
double minY = box.CentreY - (box.SizeY * 0.5) - radius;
double maxY = box.CentreY + (box.SizeY * 0.5) + radius;
double minZ = box.MinZ - radius;
double maxZ = box.MaxZ + radius;
// A segment can cross a box while both ends sit outside it, so walk the
// segment: testing only the endpoints is the mistake this class exists to fix.
double length = Norm(bx - ax, by - ay, bz - az);
int samples = Math.Max(2, (int)Math.Ceiling(length / SampleSpacingMetres) + 1);
for (int ii = 0; ii <= samples; ii++)
{
double t = (double)ii / samples;
double x = ax + ((bx - ax) * t);
double y = ay + ((by - ay) * t);
double z = az + ((bz - az) * t);
if (x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ)
{
return true;
}
}
return false;
}
private static double Norm(double x, double y, double z)
{
return Math.Sqrt((x * x) + (y * y) + (z * z));
}
///
/// Fine enough to catch a link clipping the corner of a bin wall, coarse enough that
/// a whole configuration costs a few hundred comparisons.
///
private const double SampleSpacingMetres = 0.02;
private const double ContactToleranceMetres = 1e-6;
}
}