Files
OpenNest/OpenNest.Gpu/PartBitmap.cs
T
ajandClaude Opus 5.5 0186ccfbff feat(nest): keep parts out of tabbed cutouts
A tabbed cutout's slug stays attached while the sheet is cut, but nesting
saw a closed hole and could place parts inside it. Nesting and layout paths
now read material through NestingMaterial.ForNesting, which drops closed
contours with tab spans so the slug counts as part. That covers part
outlines and collision lines, the cutout router, fill expansion, best-fit,
NestLayoutCheck and the GPU bitmaps. Area, overlap diagnostics and cutting
keep the hole, and programs without tabs read exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:14:22 -04:00

295 lines
9.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Gpu
{
public class PartBitmap
{
public int[] Cells { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public double CellSize { get; set; }
public double OriginX { get; set; }
public double OriginY { get; set; }
public const double DefaultCellSize = 0.05;
public static PartBitmap FromDrawing(
Drawing drawing,
double cellSize = DefaultCellSize,
double spacingDilation = 0
)
{
var polygons = GetClosedPolygons(drawing);
return Rasterize(polygons, cellSize, spacingDilation);
}
public static PartBitmap FromDrawingRotated(
Drawing drawing,
double rotation,
double cellSize = DefaultCellSize,
double spacingDilation = 0
)
{
var polygons = GetClosedPolygons(drawing);
if (!rotation.IsEqualTo(0))
{
foreach (var poly in polygons)
poly.Rotate(rotation);
}
return Rasterize(polygons, cellSize, spacingDilation);
}
/// <summary>
/// Rasterizes a Part that was created with Part.CreateAtOrigin.
/// Extracts polygons from the Part's program and offsets by Location,
/// guaranteeing the bitmap matches the exact coordinate space used
/// by the CPU PairEvaluator.
/// </summary>
public static PartBitmap FromPart(Part part, double cellSize = DefaultCellSize)
{
var entities = ConvertProgram
.ToGeometry(part.Program)
.ForNesting();
var shapes = ShapeBuilder.GetShapes(entities);
var polygons = new List<Polygon>();
foreach (var shape in shapes)
{
if (!shape.IsClosed())
continue;
var polygon = shape.ToPolygonWithTolerance(0.05);
polygon.Close();
polygon.Offset(part.Location);
polygons.Add(polygon);
}
return Rasterize(polygons, cellSize, 0);
}
private static PartBitmap Rasterize(
List<Polygon> polygons,
double cellSize,
double spacingDilation
)
{
if (polygons.Count == 0)
return new PartBitmap
{
Cells = Array.Empty<int>(),
Width = 0,
Height = 0,
CellSize = cellSize,
};
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
foreach (var poly in polygons)
{
poly.UpdateBounds();
var bb = poly.BoundingBox;
if (bb.Left < minX)
minX = bb.Left;
if (bb.Bottom < minY)
minY = bb.Bottom;
if (bb.Right > maxX)
maxX = bb.Right;
if (bb.Top > maxY)
maxY = bb.Top;
}
minX -= spacingDilation;
minY -= spacingDilation;
maxX += spacingDilation;
maxY += spacingDilation;
var width = (int)System.Math.Ceiling((maxX - minX) / cellSize);
var height = (int)System.Math.Ceiling((maxY - minY) / cellSize);
if (width <= 0 || height <= 0)
return new PartBitmap
{
Cells = Array.Empty<int>(),
Width = 0,
Height = 0,
CellSize = cellSize,
};
var cells = new int[width * height];
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var px = minX + (x + 0.5) * cellSize;
var py = minY + (y + 0.5) * cellSize;
var pt = new Vector(px, py);
foreach (var poly in polygons)
{
if (poly.ContainsPoint(pt))
{
cells[y * width + x] = 1;
break;
}
}
}
}
var dilationCells = (int)System.Math.Ceiling(spacingDilation / cellSize);
if (dilationCells > 0)
Dilate(cells, width, height, dilationCells);
return new PartBitmap
{
Cells = cells,
Width = width,
Height = height,
CellSize = cellSize,
OriginX = minX,
OriginY = minY,
};
}
private static List<Polygon> GetClosedPolygons(Drawing drawing)
{
var entities = ConvertProgram
.ToGeometry(drawing.Program)
.ForNesting();
var shapes = ShapeBuilder.GetShapes(entities);
var polygons = new List<Polygon>();
foreach (var shape in shapes)
{
if (!shape.IsClosed())
continue;
var polygon = shape.ToPolygonWithTolerance(0.05);
polygon.Close();
polygons.Add(polygon);
}
return polygons;
}
/// <summary>
/// Blits two locally-rasterized bitmaps into a shared world-space grid.
/// B is placed at the given world-space offset. Returns two aligned int[]
/// arrays of identical dimensions with no fractional offset math.
/// </summary>
public static (int[] cellsA, int[] cellsB, int width, int height) BlitPair(
PartBitmap bitmapA,
PartBitmap bitmapB,
double offsetX,
double offsetY
)
{
var cellSize = bitmapA.CellSize;
// B's world-space origin when placed at the offset
var bWorldOriginX = bitmapB.OriginX + offsetX;
var bWorldOriginY = bitmapB.OriginY + offsetY;
// Combined world-space bounds
var combinedMinX = System.Math.Min(bitmapA.OriginX, bWorldOriginX);
var combinedMinY = System.Math.Min(bitmapA.OriginY, bWorldOriginY);
var combinedMaxX = System.Math.Max(
bitmapA.OriginX + bitmapA.Width * cellSize,
bWorldOriginX + bitmapB.Width * cellSize
);
var combinedMaxY = System.Math.Max(
bitmapA.OriginY + bitmapA.Height * cellSize,
bWorldOriginY + bitmapB.Height * cellSize
);
var sharedWidth = (int)System.Math.Ceiling((combinedMaxX - combinedMinX) / cellSize);
var sharedHeight = (int)System.Math.Ceiling((combinedMaxY - combinedMinY) / cellSize);
if (sharedWidth <= 0 || sharedHeight <= 0)
return (Array.Empty<int>(), Array.Empty<int>(), 0, 0);
var cellsA = new int[sharedWidth * sharedHeight];
var cellsB = new int[sharedWidth * sharedHeight];
// Integer cell offsets for A within the shared grid
var aOffX = (int)System.Math.Round((bitmapA.OriginX - combinedMinX) / cellSize);
var aOffY = (int)System.Math.Round((bitmapA.OriginY - combinedMinY) / cellSize);
// Integer cell offsets for B within the shared grid
var bOffX = (int)System.Math.Round((bWorldOriginX - combinedMinX) / cellSize);
var bOffY = (int)System.Math.Round((bWorldOriginY - combinedMinY) / cellSize);
// Blit A into the shared grid
for (var y = 0; y < bitmapA.Height; y++)
{
for (var x = 0; x < bitmapA.Width; x++)
{
if (bitmapA.Cells[y * bitmapA.Width + x] == 1)
{
var sx = aOffX + x;
var sy = aOffY + y;
if (sx >= 0 && sx < sharedWidth && sy >= 0 && sy < sharedHeight)
cellsA[sy * sharedWidth + sx] = 1;
}
}
}
// Blit B into the shared grid
for (var y = 0; y < bitmapB.Height; y++)
{
for (var x = 0; x < bitmapB.Width; x++)
{
if (bitmapB.Cells[y * bitmapB.Width + x] == 1)
{
var sx = bOffX + x;
var sy = bOffY + y;
if (sx >= 0 && sx < sharedWidth && sy >= 0 && sy < sharedHeight)
cellsB[sy * sharedWidth + sx] = 1;
}
}
}
return (cellsA, cellsB, sharedWidth, sharedHeight);
}
private static void Dilate(int[] cells, int width, int height, int radius)
{
var source = (int[])cells.Clone();
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
if (source[y * width + x] != 1)
continue;
for (var dy = -radius; dy <= radius; dy++)
{
for (var dx = -radius; dx <= radius; dx++)
{
var nx = x + dx;
var ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
cells[ny * width + nx] = 1;
}
}
}
}
}
}
}