using System;
using System.Collections.Generic;
using System.Globalization;
using OpenNest.Geometry;
namespace OpenNest.Dimensions
{
///
/// A dimension resolved against a part's geometry. Extension lines run along the extents, on
/// the lines through and and through
/// and , out from the part to
/// ; the dimension line joins the last two.
///
public sealed class DimensionLayout
{
private const int MaxExtensionSteps = 64;
private const double ExtensionPrecision = 0.05;
public DimensionKind Kind { get; init; }
/// Why the dimension could not be resolved; null when it was.
public string Error { get; init; }
public bool IsValid => Error == null;
public double Value { get; init; }
public Vector ExtensionStart { get; init; }
public Vector ExtensionEnd { get; init; }
public Vector LineStart { get; init; }
public Vector LineEnd { get; init; }
/// The part's extents the dimension was measured from.
public Box Extents { get; init; }
/// The geometry behind .
public IReadOnlyList Profile { get; init; } = Array.Empty();
public string ValueText => Value.ToString("0.000", CultureInfo.InvariantCulture);
/// The value as drawn: radius and diameter carry their drafting prefix.
public string DisplayText => Kind switch
{
DimensionKind.Radius => "R" + ValueText,
DimensionKind.Diameter => "\u00D8" + ValueText,
_ => ValueText,
};
public Vector MidPoint => new((LineStart.X + LineEnd.X) / 2, (LineStart.Y + LineEnd.Y) / 2);
public bool IsRadial => DrawingDimension.IsRadialKind(Kind);
// Radius and diameter: the leader runs from LineStart (the center for a radius, the far
// side of the curve for a diameter) through ArrowPoint on the curve to LineEnd, the label.
/// Radius and diameter: the measured curve's center.
public Vector Center { get; init; }
/// Radius and diameter: the measured curve's radius.
public double Radius { get; init; }
/// Radius and diameter: the leader's direction from the center, in radians.
public double Angle { get; init; }
/// Radius and diameter: where the leader meets the curve on the label's side.
public Vector ArrowPoint { get; init; }
/// Diameter: where the leader meets the curve opposite .
public Vector FarArrowPoint { get; init; }
/// Radius and diameter on an arc: the measured arc; null for a circle.
public Arc Arc { get; init; }
///
/// Radius and diameter: the leader angle and offset that put the label at
/// , with the label at least from both the
/// curve and the center. A radius on an arc stays on the arc's sweep.
///
public (double Offset, double Angle) RadialPlacementAt(Vector point, double minimum)
{
var dx = point.X - Center.X;
var dy = point.Y - Center.Y;
var distance = System.Math.Sqrt(dx * dx + dy * dy);
var angle = distance > 0 ? System.Math.Atan2(dy, dx) : Angle;
if (Arc != null && Kind == DimensionKind.Radius)
angle = DimensionBuilder.ClampToSweep(Arc, angle);
var offset = distance - Radius;
if (offset > -minimum && offset < minimum)
offset = offset >= 0 ? minimum : -minimum;
if (offset < minimum - Radius)
offset = minimum - Radius <= -minimum ? minimum - Radius : minimum;
return (offset, Math.Angle.NormalizeRad(angle));
}
///
/// Where an extension line leaving on the dimension line first
/// meets the profile offset by , walking toward the part by the
/// distance known to be free at each step. itself when it is
/// already that close.
///
public Vector ExtensionTip(Vector foot, double clearance)
{
var horizontal = Kind == DimensionKind.OverallHorizontal;
var position = horizontal ? foot.Y : foot.X;
var center = horizontal
? (Extents.Bottom + Extents.Top) / 2
: (Extents.Left + Extents.Right) / 2;
var sign = System.Math.Sign(center - position);
var direction = horizontal ? new Vector(0, sign) : new Vector(sign, 0);
// The walk ends at the far side of the part at the latest: the line touches the part.
var limit = System.Math.Abs(center - position) + (horizontal ? Extents.Width : Extents.Length) / 2;
var travelled = 0.0;
for (var i = 0; i < MaxExtensionSteps && travelled < limit; i++)
{
var free = DistanceToProfile(foot + direction * travelled) - clearance;
if (free <= clearance * ExtensionPrecision)
break;
travelled += free;
}
return foot + direction * System.Math.Min(travelled, limit);
}
private double DistanceToProfile(Vector point)
{
var nearest = double.MaxValue;
foreach (var entity in Profile)
nearest = System.Math.Min(nearest, point.DistanceTo(entity.ClosestPointTo(point)));
return nearest;
}
///
/// The offset that puts the dimension line through , on the side of
/// the part's center the point is on and at least clear of it.
///
public double OffsetAt(Vector point, double minimum)
{
var horizontal = Kind == DimensionKind.OverallHorizontal;
var position = horizontal ? point.Y : point.X;
var low = horizontal ? Extents.Bottom : Extents.Left;
var high = horizontal ? Extents.Top : Extents.Right;
return position >= (low + high) / 2
? System.Math.Max(minimum, position - high)
: System.Math.Min(-minimum, position - low);
}
}
}