BlockAperture.java

package com.varnernet.gerb4j;

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

import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

/**
 * An aperture defined by a recorded sequence of drawing operations (%AB...%AB*%).
 *
 * <p>When this aperture is flashed (D03), all its stored operations are replayed at the flash
 * position (each operation's coordinate is translated by the flash X/Y).
 *
 * <p>Nested BlockApertures work automatically: a FLASH DrawingOperation inside the block that
 * targets another BlockAperture ID will recurse through the normal render path when the block is
 * replayed.
 *
 * <p>Two callbacks are injected by {@code GerberContext.addAperture()} so that {@link #renderFlash}
 * and {@link #expandFlashBounds} can participate in the polymorphic {@link Aperture} contracts
 * without a circular compile-time dependency on {@code GerberContext}.
 */
public class BlockAperture extends Aperture {

    // ── Functional-interface callbacks ────────────────────────────────────────

    /**
     * Callback used by {@link #renderFlash} to recursively render the block's child operations
     * through {@code GerberContext.renderItems()}.
     */
    @FunctionalInterface
    public interface BlockRenderer {
        /**
         * Renders the given operations with the specified offset and transform.
         *
         * @param ops       the list of operations to render
         * @param dx        the x offset
         * @param dy        the y offset
         * @param target    the output target
         * @param transform the aperture transform
         */
        void render(
                List<GerberOperation> ops,
                double dx,
                double dy,
                GerberOutputTarget target,
                ApertureTransform transform);
    }

    /**
     * Callback used by {@link #expandFlashBounds} to recursively expand bounds through {@code
     * GerberContext.expandBounds()}.
     */
    @FunctionalInterface
    public interface BoundsExpander {
        /**
         * Expands the bounds for the given operations.
         *
         * @param ops       the list of operations
         * @param dx        the x offset
         * @param dy        the y offset
         * @param transform the affine transform
         * @param extents   the extents array to update
         */
        void expand(
                List<GerberOperation> ops,
                double dx,
                double dy,
                AffineTransform transform,
                double[] extents);
    }

    // ── State ─────────────────────────────────────────────────────────────────

    /**
     * Type-safe list of operations in source order.
     */
    private final List<GerberOperation> operations;

    private BlockRenderer blockRenderer;
    private BoundsExpander boundsExpander;

    // ── Constructors ──────────────────────────────────────────────────────────

    /**
     * Creates a new block aperture with the given ID and an empty operation list.
     *
     * @param id the aperture identifier (e.g. "D10")
     */
    public BlockAperture(final String id) {
        super(id);
        this.operations = new ArrayList<>();
    }

    /**
     * Creates a new block aperture with the given ID and operation list.
     *
     * @param id         the aperture identifier (e.g. "D10")
     * @param operations the list of operations to store (defensive copy made)
     */
    public BlockAperture(final String id, final List<GerberOperation> operations) {
        super(id);
        this.operations = new ArrayList<>(operations);
    }

    // ── Callback wiring ───────────────────────────────────────────────────────

    /**
     * Inject the rendering callback (called once from {@code GerberContext.addAperture()}).
     *
     * @param renderer the callback to use for rendering operations
     */
    public void setBlockRenderer(final BlockRenderer renderer) {
        this.blockRenderer = renderer;
    }

    /**
     * Inject the bounds-expansion callback (called once from {@code GerberContext.addAperture()}).
     *
     * @param expander the callback to use for bounds expansion
     */
    public void setBoundsExpander(final BoundsExpander expander) {
        this.boundsExpander = expander;
    }

    // ── Aperture contract ─────────────────────────────────────────────────────

    /**
     * Returns a defensive copy of the operation list.
     *
     * @return a new list containing all operations
     */
    public List<GerberOperation> getOperations() {
        return new ArrayList<>(operations);
    }

    /**
     * Block aperture bounds depend on contained operations and flash context; not computable here.
     */
    @Override
    public Rectangle2D getBounds() {
        return null;
    }

    /**
     * Renders this block aperture by delegating to the injected {@link BlockRenderer}. The {@code
     * transform} from the enclosing flash operation is forwarded so that LM/LR/LS attributes on the
     * flash are honoured during recursive rendering.
     */
    @Override
    public void renderFlash(
            final Point2D flashPoint,
            final GerberOutputTarget target,
            final Polarity polarity,
            final ApertureTransform transform) {
        if (blockRenderer != null) {
            blockRenderer.render(
                    getOperations(), flashPoint.getX(), flashPoint.getY(), target, transform);
        }
    }

    /**
     * Expands {@code extents} by delegating to the injected {@link BoundsExpander}, which recurses
     * into the child operations using the aperture dictionary held by {@code GerberContext}.
     */
    @Override
    public void expandFlashBounds(
            final double x,
            final double y,
            final AffineTransform transform,
            final double[] extents) {
        if (boundsExpander != null) {
            boundsExpander.expand(getOperations(), x, y, transform, extents);
        }
    }
}