Aperture.java
package com.varnernet.gerb4j;
import com.varnernet.gerb4j.render.GerberOutputTarget;
import java.awt.BasicStroke;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Base class for all Gerber aperture types.
*
* <p>Each subclass knows its own geometry and is responsible for reporting it <em>and</em> for
* rendering itself. {@link #getBounds()} returns the aperture's extent in local aperture
* coordinates (centred at the origin, before any flash translation). {@link #getStrokeWidth()}
* returns the effective pen width when this aperture is used for a linear draw (D01). {@link
* #render(Point2D, GerberOutputTarget, Polarity)} draws the aperture body at the given flash point,
* then punches any circular hole (if present) as a CLEAR overlay โ the same behaviour that was
* previously duplicated four times in {@code GerberContext.renderAperture()}.
*/
public abstract class Aperture {
private final String id;
/**
* Aperture attributes captured at AD time (snapshot of the aperture attribute dictionary).
*/
private Map<String, List<String>> attributes = Collections.emptyMap();
/**
* Constructs an aperture with the given identifier.
*
* @param id the aperture identifier (e.g. {@code "D10"})
*/
protected Aperture(final String id) {
this.id = id;
}
/**
* Returns the aperture ID.
*
* @return the aperture ID
*/
public String getId() {
return id;
}
/**
* Attach a snapshot of the aperture attribute dictionary to this aperture. Per Gerber spec ยง5.3,
* "the AD command attaches the aperture attributes at that moment in the attribute dictionary to
* it."
*
* @param attrs an immutable snapshot โ should not be modified after attachment.
*/
public void setAttributes(final Map<String, List<String>> attrs) {
this.attributes = attrs != null ? attrs : Collections.emptyMap();
}
/**
* Returns the full attribute map (unmodifiable).
*
* @return the attribute map
*/
public Map<String, List<String>> getAttributes() {
return Collections.unmodifiableMap(attributes);
}
/**
* Returns the values for a single attribute name, or {@code null} if not present.
*
* @param name the attribute name
* @return the attribute values
*/
public List<String> getAttribute(final String name) {
return attributes.getOrDefault(name, null);
}
/**
* Returns this aperture's bounding box in local aperture coordinates, centred at (0, 0). Callers
* translate by the flash point to get world-space extents.
*
* <p>Returns {@code null} for aperture types whose bounds cannot be determined without additional
* context (e.g. {@link BlockAperture}).
*
* @return the bounding box or null
*/
public abstract Rectangle2D getBounds();
/**
* Returns the effective stroke width (in Gerber units) when this aperture is used to draw a
* linear path (D01). Defaults to 0.0 (hairline).
*
* @return the stroke width
*/
public double getStrokeWidth() {
return 0.0;
}
/**
* Returns the {@link BasicStroke} cap style to use when this aperture is the pen for a D01 linear
* draw.
*
* <p>Circular and obround apertures produce round end caps; rectangle apertures produce flat
* (butt) end caps. Subclasses override as needed.
*
* @return one of {@link BasicStroke#CAP_ROUND}, {@link BasicStroke#CAP_BUTT}, or {@link
* BasicStroke#CAP_SQUARE}.
*/
public int getCapStyle() {
return BasicStroke.CAP_ROUND;
}
/**
* Returns the aperture's cross-section outline as a {@link Shape} in local coordinates (centred
* at the origin). Used for swept-region rendering of D01 draws where the exact Minkowski sum of
* the aperture and the path segment is computed and filled, rather than approximated with a
* {@link BasicStroke}.
*
* <p>Returns {@code null} (the default) when {@link BasicStroke} rendering is sufficient โ e.g.
* for {@link CircleAperture} where {@code CAP_ROUND} already produces the correct swept shape.
*
* @return a convex {@link Shape} representing the aperture cross-section, or {@code null} to fall
* back to stroke-based rendering.
*/
public Shape getApertureShape() {
return null;
}
/**
* Returns the {@link BasicStroke} join style to use when consecutive D01 line segments share a
* vertex.
*
* <p>For circular/obround apertures the overlap of two round-capped segments produces a filled
* circle at the vertex, matching {@link BasicStroke#JOIN_ROUND}. Rectangle apertures override
* this to {@link BasicStroke#JOIN_MITER}.
*
* @return one of {@link BasicStroke#JOIN_ROUND}, {@link BasicStroke#JOIN_MITER}, or {@link
* BasicStroke#JOIN_BEVEL}.
*/
public int getJoinStyle() {
return BasicStroke.JOIN_ROUND;
}
/**
* Render this aperture as a flash (D03) at {@code flashPoint}.
*
* <p>The default implementation does nothing; concrete subclasses override this to draw their
* body shape and, if applicable, punch a circular hole.
*
* @param flashPoint The world-space position at which the aperture origin is placed.
* @param target The output target that receives the drawing calls.
* @param polarity The effective polarity (DARK or CLEAR).
*/
public void render(final Point2D flashPoint, final GerberOutputTarget target, final Polarity polarity) {
// Default: nothing rendered (e.g. BlockAperture handled by the orchestrator)
}
/**
* Render this aperture as a flash (D03), additionally supplying the aperture transformation from
* the enclosing drawing operation.
*
* <p>For most aperture types this simply delegates to {@link #render(Point2D, GerberOutputTarget,
* Polarity)}. {@link BlockAperture} overrides this to recursively render its child operations
* through the injected {@code BlockRenderer} callback, passing the {@code transform} so that
* block-aperture LM/LR/LS are honoured.
*
* @param flashPoint The world-space position at which the aperture origin is placed.
* @param target The output target that receives the drawing calls.
* @param polarity The effective polarity (DARK or CLEAR).
* @param transform The aperture transform recorded with the flash operation.
*/
public void renderFlash(
final Point2D flashPoint,
final GerberOutputTarget target,
final Polarity polarity,
final ApertureTransform transform) {
render(flashPoint, target, polarity);
}
/**
* Expand {@code extents} to encompass this aperture when flashed at ({@code x}, {@code y}) with
* the given block transform.
*
* <p>The default implementation translates {@link #getBounds()} by the flash point. When {@link
* #getBounds()} returns {@code null} the flash point itself is used as a zero-size extent. {@link
* BlockAperture} overrides this to recurse into its child operations via the injected {@code
* BoundsExpander} callback.
*
* @param x Flash X in world coordinates.
* @param y Flash Y in world coordinates.
* @param transform Block-level affine transform already applied to produce ({@code x},{@code y}).
* @param extents Running [minX, maxX, minY, maxY] array updated in place.
*/
public void expandFlashBounds(final double x, final double y, final AffineTransform transform, final double[] extents) {
Rectangle2D localBounds = getBounds();
if (localBounds != null) {
extents[0] = Math.min(extents[0], x + localBounds.getMinX());
extents[1] = Math.max(extents[1], x + localBounds.getMaxX());
extents[2] = Math.min(extents[2], y + localBounds.getMinY());
extents[3] = Math.max(extents[3], y + localBounds.getMaxY());
} else {
extents[0] = Math.min(extents[0], x);
extents[1] = Math.max(extents[1], x);
extents[2] = Math.min(extents[2], y);
extents[3] = Math.max(extents[3], y);
}
}
/**
* Helper: return the inverted polarity (used when punching the hole).
*
* @param p the polarity
* @return the inverted polarity
*/
protected static Polarity invertPolarity(final Polarity p) {
return p == Polarity.DARK ? Polarity.CLEAR : Polarity.DARK;
}
}