using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Data;
///
/// The size and hole count of a library drawing's package, measured once per package by the
/// server worker (the server never parses packages) and used to filter the library. Lengths
/// are inches whatever units the drawing was drawn in. A package that could not be measured
/// keeps a summary with set so it is not measured again.
///
public sealed record DrawingGeometrySummary
{
/// Raised when the measurement changes, so the worker measures every package again.
public const int CurrentVersion = 1;
public const int MaxErrorLength = 200;
/// The that measured this package.
public int Version { get; init; }
/// The longer side of the material's bounding box, in inches.
public double Length { get; init; }
/// The shorter side of the material's bounding box, in inches.
public double Width { get; init; }
/// Net material area (outline less holes), in square inches.
public double Area { get; init; }
/// Closed contours inside the outline: holes and other cutouts.
public int Holes { get; init; }
/// Why the package could not be measured; blank for a measured package.
public string Error { get; init; } = "";
public bool IsMeasured => Error.Length == 0;
/// The first invalid field, or null when the summary may be stored.
public string? GetValidationError()
{
if (Version < 1)
return "version must be 1 or greater.";
if (Error is null)
return "error must be a string.";
if (Error.Length > MaxErrorLength)
return $"error must be at most {MaxErrorLength} characters.";
if (!double.IsFinite(Length) || !double.IsFinite(Width) || !double.IsFinite(Area))
return "length, width and area must be finite.";
if (Length < 0 || Width < 0 || Area < 0 || Holes < 0)
return "length, width, area and holes must be zero or greater.";
if (Width > Length)
return "width must not exceed length.";
return null;
}
/// Records why a package could not be measured.
public static DrawingGeometrySummary Failed(string error)
{
var text = string.IsNullOrWhiteSpace(error) ? "Could not be measured." : error.Trim();
return new DrawingGeometrySummary
{
Version = CurrentVersion,
Error = text.Length > MaxErrorLength ? text[..MaxErrorLength] : text,
};
}
///
/// Measures a drawing's material (marks such as etch and rapids excluded): the bounding box
/// of every closed contour, and the closed contours inside the largest one as holes.
///
public static DrawingGeometrySummary Measure(Drawing drawing, Units units)
{
ArgumentNullException.ThrowIfNull(drawing);
if (drawing.Program is null)
return Failed("No program.");
var material = ConvertProgram.ToGeometry(drawing.Program)
.Where(entity => SpecialLayers.IsMaterial(entity.Layer));
var shapes = ShapeBuilder.GetShapes(material).Where(shape => shape.IsClosed()).ToList();
if (shapes.Count == 0)
return Failed("No closed cut geometry.");
var outline = shapes[0];
foreach (var shape in shapes)
{
if (shape.BoundingBox.Area() > outline.BoundingBox.Area())
outline = shape;
}
var outer = outline.BoundingBox;
var holes = shapes.Where(shape => shape != outline && outer.Contains(shape.BoundingBox)).ToList();
var bounds = shapes.Select(shape => shape.BoundingBox).ToList();
var width = bounds.Max(b => b.Right) - bounds.Min(b => b.Left);
var height = bounds.Max(b => b.Top) - bounds.Min(b => b.Bottom);
var area = System.Math.Abs(outline.Area()) - holes.Sum(hole => System.Math.Abs(hole.Area()));
var scale = units == Units.Millimeters ? 1 / 25.4 : 1.0;
return new DrawingGeometrySummary
{
Version = CurrentVersion,
Length = System.Math.Round(System.Math.Max(width, height) * scale, 4),
Width = System.Math.Round(System.Math.Min(width, height) * scale, 4),
Area = System.Math.Round(System.Math.Max(0, area) * scale * scale, 4),
Holes = holes.Count,
};
}
}