VectorLineMacroPrimitive.java

package com.varnernet.gerb4j.macro;

import com.varnernet.gerb4j.Polarity;
import com.varnernet.gerb4j.render.GerberOutputTarget;

import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

/**
 * Gerber macro primitive type 20 — Vector Line. Format: {@code
 * 20,exposure,width,startX,startY,endX,endY,rotation}
 *
 * <p>Per spec §4.5.1.5, primitive 20 is a <em>filled rectangle</em> with flat (square) ends — not a
 * stroked path with round caps. The implementation computes the four rotated corners of the
 * rectangle and delegates to {@link GerberOutputTarget#drawRegion}, exactly as {@link
 * CenterLineMacroPrimitive} does for primitive 21.
 */
final class VectorLineMacroPrimitive extends AbstractMacroPrimitive {

    private static final int CORNERS = 4;

    private final double width;
    private final double startX;
    private final double startY;
    private final double endX;
    private final double endY;
    private final double rotation;

    VectorLineMacroPrimitive(
            final boolean exposed,
            final double width,
            final double startX,
            final double startY,
            final double endX,
            final double endY,
            final double rotation) {
        super(exposed);
        this.width = width;
        this.startX = startX;
        this.startY = startY;
        this.endX = endX;
        this.endY = endY;
        this.rotation = rotation;
    }

    @Override
    public Rectangle2D getBounds() {
        double[] s = rotatePoint(startX, startY, rotation);
        double[] e = rotatePoint(endX, endY, rotation);
        double half = width / 2.0;
        double minX = Math.min(s[0], e[0]) - half;
        double maxX = Math.max(s[0], e[0]) + half;
        double minY = Math.min(s[1], e[1]) - half;
        double maxY = Math.max(s[1], e[1]) + half;
        return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
    }

    @Override
    public void render(final Point2D flashPoint, final GerberOutputTarget target, final Polarity polarity) {
        // Compute the perpendicular unit vector scaled to half-width.
        // This gives the offset from the line axis to each long edge.
        double dx = endX - startX;
        double dy = endY - startY;
        double len = Math.sqrt(dx * dx + dy * dy);

        // Avoid degenerate zero-length lines
        if (len == 0.0) {
            return;
        }

        double half = width / 2.0;
        // Perpendicular: rotate (dx,dy) by 90° CCW → (-dy, dx), then normalise and scale
        double px = -dy / len * half;
        double py = dx / len * half;

        // Four corners in local (pre-rotation) macro space
        double[][] local = {
                {startX + px, startY + py}, // start-left
                {endX + px, endY + py}, // end-left
                {endX - px, endY - py}, // end-right
                {startX - px, startY - py}, // start-right
        };

        // Apply macro rotation, then translate by flash point
        Path2D.Double path = new Path2D.Double();
        for (int i = 0; i < CORNERS; i++) {
            double[] rot = rotatePoint(local[i][0], local[i][1], rotation);
            double wx = flashPoint.getX() + rot[0];
            double wy = flashPoint.getY() + rot[1];
            if (i == 0) {
                path.moveTo(wx, wy);
            } else {
                path.lineTo(wx, wy);
            }
        }
        path.closePath();

        target.drawRegion(path, effectivePolarity(polarity));
    }
}