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]);
    }
}