AreaTarget.java
package com.varnernet.gerb4j.render;
import com.varnernet.gerb4j.Aperture;
import com.varnernet.gerb4j.ApertureTransform;
import com.varnernet.gerb4j.ArcGeometry;
import com.varnernet.gerb4j.Mirror;
import com.varnernet.gerb4j.Polarity;
import com.varnernet.gerb4j.QuadrantMode;
import java.awt.BasicStroke;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link GerberOutputTarget} that composes all Gerber operations into a single {@link Area} in
* <em>Gerber coordinate space</em> (Y-up, no pixel scale, no axis flip).
*
* <p>Extends {@link AbstractGerberTarget} which provides the geometry algorithms ({@link
* #convexHull}, {@link #getShapeVertices}) and the {@link #drawPath}/ {@link #drawArc} dispatch
* logic. This class is responsible only for the Area-specific concern: composing shapes via boolean
* add/subtract.
*
* <p>DARK polarity operations add material ({@code Area.add()}); CLEAR polarity operations subtract
* material ({@code Area.subtract()}).
*
* <p>A list of individual {@link PolarizedShape} objects is also maintained so that CAM consumers
* can reason about discrete features without having to decompose the fused {@link Area}.
*/
public class AreaTarget extends AbstractGerberTarget {
/**
* Single fused shape: the boolean combination of all operations.
*/
private final Area area = new Area();
/**
* Individual shapes with polarity metadata, for CAM consumers.
*/
private final List<PolarizedShape> shapes = new ArrayList<>();
/**
* The polarity that adds material (normally {@link Polarity#DARK}).
*/
private final Polarity basePolarity;
/**
* Current aperture transform, or {@code null} for identity.
*/
private AffineTransform currentTransform = null;
// ── Constructors ──────────────────────────────────────────────────────────
/**
* Constructs an {@code AreaTarget} with {@link Polarity#DARK} as the base.
*/
public AreaTarget() {
this(Polarity.DARK);
}
/**
* Constructs an {@code AreaTarget} with an explicit base polarity.
*
* @param basePolarity the polarity that adds material ({@code Area.add()}); the opposite polarity
* will subtract.
*/
public AreaTarget(final Polarity basePolarity) {
this.basePolarity = basePolarity;
}
// ── Results ───────────────────────────────────────────────────────────────
/**
* Returns a defensive copy of the composite area of all operations rendered so far.
*
* @return the composite area
*/
public Area getArea() {
return (Area) area.clone();
}
/**
* Returns an unmodifiable view of all individual {@link PolarizedShape} objects in emission
* order.
*
* @return the list of shapes
*/
public List<PolarizedShape> getShapes() {
return Collections.unmodifiableList(shapes);
}
// ── Core composition ──────────────────────────────────────────────────────
/**
* Applies the current aperture transform to {@code shape}, records it in the {@link
* PolarizedShape} list, and boolean-adds or subtracts it from the composite area.
*
* @param shape the shape to compose
* @param polarity the polarity of the shape
*/
private void compose(final Shape shape, final Polarity polarity) {
Shape transformed =
(currentTransform != null) ? currentTransform.createTransformedShape(shape) : shape;
shapes.add(new PolarizedShape(transformed, polarity));
if (polarity == basePolarity) {
area.add(new Area(transformed));
} else {
area.subtract(new Area(transformed));
}
}
// ── Flash operations (D03) ────────────────────────────────────────────────
@Override
public void drawCircle(
final Point2D center, final double diameter, final double rotationDegrees, final Polarity polarity) {
double r = diameter / 2.0;
double cx = center.getX();
double cy = center.getY();
Shape circle = new Ellipse2D.Double(cx - r, cy - r, diameter, diameter);
if (rotationDegrees != 0) {
circle =
AffineTransform.getRotateInstance(Math.toRadians(rotationDegrees), cx, cy)
.createTransformedShape(circle);
}
compose(circle, polarity);
}
@Override
public void drawRectangle(
final Point2D center, final double width, final double height, final double rotationDegrees, final Polarity polarity) {
double cx = center.getX();
double cy = center.getY();
Shape rect = new Rectangle2D.Double(cx - width / 2.0, cy - height / 2.0, width, height);
if (rotationDegrees != 0) {
rect =
AffineTransform.getRotateInstance(Math.toRadians(rotationDegrees), cx, cy)
.createTransformedShape(rect);
}
compose(rect, polarity);
}
@Override
public void drawObround(
final Point2D center, final double width, final double height, final double rotationDegrees, final Polarity polarity) {
double cx = center.getX();
double cy = center.getY();
double cornerDiameter = Math.min(width, height);
Shape obround =
new RoundRectangle2D.Double(
cx - width / 2.0, cy - height / 2.0, width, height, cornerDiameter, cornerDiameter);
if (rotationDegrees != 0) {
obround =
AffineTransform.getRotateInstance(Math.toRadians(rotationDegrees), cx, cy)
.createTransformedShape(obround);
}
compose(obround, polarity);
}
@Override
public void drawPolygon(
final Point2D center, final int numVertices, final double diameter, final double rotationDegrees, final Polarity polarity) {
double radius = diameter / 2.0;
double startAngle = Math.toRadians(rotationDegrees);
double cx = center.getX();
double cy = center.getY();
Path2D.Double path = new Path2D.Double();
for (int i = 0; i < numVertices; i++) {
double angle = startAngle + 2.0 * Math.PI * i / numVertices;
double x = cx + radius * Math.cos(angle);
double y = cy + radius * Math.sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.closePath();
compose(path, polarity);
}
// ── Linear draw hooks (called by AbstractGerberTarget.drawPath) ───────────
@Override
protected void renderPathStroked(final Path2D.Double path, final Aperture aperture, final Polarity polarity) {
float strokeWidth = (float) aperture.getStrokeWidth();
BasicStroke stroke =
new BasicStroke(
strokeWidth > 0 ? strokeWidth : 0.001f,
aperture.getCapStyle(),
aperture.getJoinStyle());
compose(stroke.createStrokedShape(path), polarity);
}
@Override
protected void renderPathSweptRegion(final Path2D.Double path, final Shape apertureShape, final Polarity polarity) {
List<double[]> vertices = getShapeVertices(apertureShape, 0.01);
PathIterator it = path.getPathIterator(null);
double[] coords = new double[6];
double lastX = 0;
double lastY = 0;
while (!it.isDone()) {
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO) {
lastX = coords[0];
lastY = coords[1];
} else if (type == PathIterator.SEG_LINETO) {
compose(sweptSegmentHull(vertices, lastX, lastY, coords[0], coords[1]), polarity);
lastX = coords[0];
lastY = coords[1];
}
it.next();
}
}
// ── Arc hooks (called by AbstractGerberTarget.drawArc) ────────────────────
@Override
protected void renderArcStroked(
final Point2D startPoint,
final Point2D endPoint,
final Point2D centerOffset,
final Aperture aperture,
final Polarity polarity,
final boolean clockwise,
final QuadrantMode quadrantMode) {
ArcGeometry geom = new ArcGeometry(startPoint, endPoint, centerOffset, clockwise, quadrantMode);
Arc2D.Double arc =
new Arc2D.Double(
geom.centerX - geom.radius,
geom.centerY - geom.radius,
2.0 * geom.radius,
2.0 * geom.radius,
Math.toDegrees(-geom.startAngle),
Math.toDegrees(-geom.angularExtent),
Arc2D.OPEN);
float strokeWidth = (float) aperture.getStrokeWidth();
BasicStroke stroke =
new BasicStroke(
strokeWidth > 0 ? strokeWidth : 0.001f,
aperture.getCapStyle(),
aperture.getJoinStyle());
compose(stroke.createStrokedShape(arc), polarity);
}
@Override
protected void renderArcSweptRegion(
final Point2D startPoint,
final Point2D endPoint,
final Point2D centerOffset,
final Shape apertureShape,
final Polarity polarity,
final boolean clockwise,
final QuadrantMode quadrantMode) {
ArcGeometry geom = new ArcGeometry(startPoint, endPoint, centerOffset, clockwise, quadrantMode);
List<double[]> vertices = getShapeVertices(apertureShape, 0.01);
int segments = geom.approximationSegments();
double angleStep = geom.angularExtent / segments;
double prevX = startPoint.getX();
double prevY = startPoint.getY();
for (int i = 1; i <= segments; i++) {
double angle = geom.startAngle + i * angleStep;
double curX = geom.centerX + geom.radius * Math.cos(angle);
double curY = geom.centerY + geom.radius * Math.sin(angle);
compose(sweptSegmentHull(vertices, prevX, prevY, curX, curY), polarity);
prevX = curX;
prevY = curY;
}
}
// ── Region (G36/G37) ──────────────────────────────────────────────────────
@Override
public void drawRegion(final Path2D.Double region, final Polarity polarity) {
compose(region, polarity);
}
// ── Aperture transforms (LM/LR/LS) ───────────────────────────────────────
@Override
public void setTransformation(final Double rotationDegrees, final Double scaleFactor, final Mirror mirroring) {
ApertureTransform t = new ApertureTransform(mirroring, rotationDegrees, scaleFactor);
currentTransform = t.toAffineTransform();
}
@Override
public void clearTransformation() {
currentTransform = null;
}
// ── Private geometry helpers ──────────────────────────────────────────────
/**
* Builds the convex hull of the aperture shape translated to both endpoints — Minkowski sum in
* Gerber coordinate units.
*
* @param apertureVertices the vertices of the aperture shape
* @param x1 the x coordinate of the first endpoint
* @param y1 the y coordinate of the first endpoint
* @param x2 the x coordinate of the second endpoint
* @param y2 the y coordinate of the second endpoint
* @return the convex hull path
*/
private static Path2D.Double sweptSegmentHull(
final List<double[]> apertureVertices, final double x1, final double y1, final double x2, final double y2) {
int n = apertureVertices.size();
double[][] points = new double[2 * n][2];
for (int i = 0; i < n; i++) {
double ax = apertureVertices.get(i)[0];
double ay = apertureVertices.get(i)[1];
points[i][0] = ax + x1;
points[i][1] = ay + y1;
points[n + i][0] = ax + x2;
points[n + i][1] = ay + y2;
}
return convexHull(points);
}
}