AbstractGerberTarget.java
package com.varnernet.gerb4j.render;
import com.varnernet.gerb4j.Aperture;
import com.varnernet.gerb4j.Polarity;
import com.varnernet.gerb4j.QuadrantMode;
import java.awt.Shape;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Abstract base class for all {@link GerberOutputTarget} implementations.
*
* <h2>Responsibilities</h2>
*
* <ul>
* <li><b>Geometry algorithms</b> — {@link #convexHull} and {@link #getShapeVertices} are shared
* by every rendering target that uses the swept-region Minkowski-sum approach for
* non-circular apertures. They live here, in the hierarchy, so the relationship between the
* algorithm and its consumers is expressed structurally.
* <li><b>Aperture dispatch</b> — {@link #drawPath} and {@link #drawArc} are {@code final}
* template methods that select between the stroked (circular aperture) and swept-region
* (non-circular aperture) strategies. This rule is identical for every target and must not
* diverge between implementations.
* </ul>
*
* <h2>Template method pattern</h2>
*
* <p>Subclasses implement four protected rendering hooks:
*
* <ul>
* <li>{@link #renderPathStroked} — circular aperture D01 via BasicStroke
* <li>{@link #renderPathSweptRegion} — non-circular aperture D01 via the Minkowski sweep
* <li>{@link #renderArcStroked} — circular aperture arc via Arc2D
* <li>{@link #renderArcSweptRegion} — non-circular aperture arc via the Minkowski sweep
* approximation
* </ul>
*/
public abstract class AbstractGerberTarget implements GerberOutputTarget {
/**
* Default constructor for subclasses.
*/
protected AbstractGerberTarget() {
}
// ── Aperture dispatch (final template methods) ────────────────────────────
/**
* Dispatches to stroked or swept-region rendering based on whether the aperture has an explicit
* cross-section shape.
*
* <p>{@code final} — the dispatch rule must be identical for every target.
*
* @param path the path to draw
* @param aperture the aperture to use
* @param polarity the polarity
*/
@Override
public final void drawPath(final Path2D.Double path, final Aperture aperture, final Polarity polarity) {
Shape apertureShape = aperture.getApertureShape();
if (apertureShape != null) {
renderPathSweptRegion(path, apertureShape, polarity);
} else {
renderPathStroked(path, aperture, polarity);
}
}
/**
* Dispatches to stroked or swept-region arc rendering.
*
* <p>{@code final} — the dispatch rule must be identical for every target.
*
* @param startPoint the start point of the arc
* @param endPoint the end point of the arc
* @param centerOffset the center offset
* @param aperture the aperture to use
* @param polarity the polarity
* @param clockwise whether the arc is clockwise
* @param quadrantMode the quadrant mode
*/
@Override
public final void drawArc(
final Point2D startPoint,
final Point2D endPoint,
final Point2D centerOffset,
final Aperture aperture,
final Polarity polarity,
final boolean clockwise,
final QuadrantMode quadrantMode) {
Shape apertureShape = aperture.getApertureShape();
if (apertureShape != null) {
renderArcSweptRegion(
startPoint, endPoint, centerOffset, apertureShape, polarity, clockwise, quadrantMode);
} else {
renderArcStroked(
startPoint, endPoint, centerOffset, aperture, polarity, clockwise, quadrantMode);
}
}
// ── Abstract rendering hooks ──────────────────────────────────────────────
/**
* Render a linear D01 path with a circular aperture (stroke-based). Called when {@link
* Aperture#getApertureShape()} returns {@code null}.
*
* @param path the path to render
* @param aperture the aperture
* @param polarity the polarity
*/
protected abstract void renderPathStroked(
Path2D.Double path, Aperture aperture, Polarity polarity);
/**
* Render a linear D01 path with a non-circular aperture (swept region). Called when {@link
* Aperture#getApertureShape()} returns a cross-section shape.
*
* @param path the path to render
* @param apertureShape aperture cross-section in local coordinates, centred at the origin
* @param polarity the polarity
*/
protected abstract void renderPathSweptRegion(
Path2D.Double path, Shape apertureShape, Polarity polarity);
/**
* Render an arc D01 with a circular aperture (stroke-based). Called when {@link
* Aperture#getApertureShape()} returns {@code null}.
*
* @param startPoint the start point
* @param endPoint the end point
* @param centerOffset the center offset
* @param aperture the aperture
* @param polarity the polarity
* @param clockwise whether clockwise
* @param quadrantMode the quadrant mode
*/
protected abstract void renderArcStroked(
Point2D startPoint,
Point2D endPoint,
Point2D centerOffset,
Aperture aperture,
Polarity polarity,
boolean clockwise,
QuadrantMode quadrantMode);
/**
* Render an arc D01 with a non-circular aperture (swept region). Called when {@link
* Aperture#getApertureShape()} returns a cross-section shape.
*
* @param startPoint the start point
* @param endPoint the end point
* @param centerOffset the center offset
* @param apertureShape the aperture shape
* @param polarity the polarity
* @param clockwise whether clockwise
* @param quadrantMode the quadrant mode
*/
protected abstract void renderArcSweptRegion(
Point2D startPoint,
Point2D endPoint,
Point2D centerOffset,
Shape apertureShape,
Polarity polarity,
boolean clockwise,
QuadrantMode quadrantMode);
// ── Geometry algorithms ───────────────────────────────────────────────────
/**
* Extracts the boundary vertices of a {@link Shape}, flattening any curves to line segments with
* the given flatness tolerance.
*
* <p>Used by both {@link Graphics2DTarget} and {@link AreaTarget} to obtain the discrete vertex
* list of an aperture cross-section before computing a swept convex hull.
*
* @param shape the shape whose outline vertices are needed
* @param flatness maximum allowed curve-flattening deviation, in shape units
* @return ordered list of {@code [x, y]} pairs along the shape boundary
*/
protected static List<double[]> getShapeVertices(final Shape shape, final double flatness) {
List<double[]> vertices = new ArrayList<>();
PathIterator pi = shape.getPathIterator(null, flatness);
double[] coords = new double[6];
while (!pi.isDone()) {
int type = pi.currentSegment(coords);
if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) {
vertices.add(new double[]{coords[0], coords[1]});
}
pi.next();
}
return vertices;
}
/**
* Computes the convex hull of a set of 2D points using Andrew's monotone chain algorithm and
* returns it as a filled {@link Path2D.Double}.
*
* <p>Used to compute the Minkowski sum of an aperture cross-section with a line segment: the hull
* of the aperture vertices translated to both endpoints of the segment.
*
* @param points array of {@code [x, y]} pairs in any order
* @return a closed {@link Path2D.Double} tracing the convex hull
*/
protected static Path2D.Double convexHull(final double[][] points) {
int n = points.length;
if (n < 2) {
Path2D.Double p = new Path2D.Double();
if (n == 1) {
p.moveTo(points[0][0], points[0][1]);
}
p.closePath();
return p;
}
// Sort by x, break ties by y
Arrays.sort(
points,
(a, b) -> {
int cmp = Double.compare(a[0], b[0]);
return cmp != 0 ? cmp : Double.compare(a[1], b[1]);
});
double[][] hull = new double[2 * n][2];
int k = 0;
// Lower hull
for (int i = 0; i < n; i++) {
while (k >= 2 && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) {
k--;
}
hull[k++] = points[i];
}
// Upper hull
int lower = k + 1;
for (int i = n - 2; i >= 0; i--) {
while (k >= lower && cross(hull[k - 2], hull[k - 1], points[i]) <= 0) {
k--;
}
hull[k++] = points[i];
}
// k-1 because the last point equals the first
Path2D.Double path = new Path2D.Double();
path.moveTo(hull[0][0], hull[0][1]);
for (int i = 1; i < k - 1; i++) {
path.lineTo(hull[i][0], hull[i][1]);
}
path.closePath();
return path;
}
/**
* 2D cross product of vectors OA and OB.
*
* @param o the origin point
* @param a the first point
* @param b the second point
* @return the cross product value
*/
private static double cross(final double[] o, final double[] a, final double[] b) {
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}
}