TemplateAperture.java
package com.varnernet.gerb4j;
import com.varnernet.gerb4j.macro.MacroPrimitive;
import com.varnernet.gerb4j.macro.MacroPrimitiveFactory;
import com.varnernet.gerb4j.render.AreaTarget;
import com.varnernet.gerb4j.render.GerberOutputTarget;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A macro-based aperture (Gerber {@code %ADD…macroName…%}).
*
* <p>At construction time the macro's primitives are fully evaluated and parsed into typed {@link
* MacroPrimitive} objects. Subsequent calls to {@link #getBounds()} and {@link #render(Point2D,
* GerberOutputTarget, Polarity)} delegate directly to those objects — no raw string processing
* occurs at render time and no external helper class is needed.
*
* <h2>Area-based composition</h2>
*
* <p>Macro primitives are <em>not</em> rendered individually to the output target. Instead, an
* internal {@link com.varnernet.gerb4j.render.AreaTarget} intercepts every draw call and composes the primitives into a
* single {@link Area} using boolean add/subtract based on each primitive's effective polarity. The
* final composite shape is then sent to the real target as <b>one</b> {@code drawRegion()} call.
* This eliminates sub-pixel rasterisation mismatches between different shape types ({@code
* Ellipse2D} vs {@code Path2D}) that caused dark-dot artefacts at shape boundaries.
*/
public final class TemplateAperture extends Aperture {
private static final double EMPTY_APERTURE_RADIUS = 0.1;
private final String templateName;
private final List<String> parameters;
private final MacroDefinition macro;
private final Map<Integer, Double> evaluatedVariables;
/**
* Pre-parsed, fully-resolved primitives ready for bounds and rendering.
*/
private final List<MacroPrimitive> resolvedPrimitives;
// ── Constructors ──────────────────────────────────────────────────────────
/**
* Full constructor — both macro and evaluated variables known at construction. This is the
* preferred constructor; it produces a fully initialised object.
*
* @param id the aperture identifier
* @param templateName the name of the macro template
* @param parameters the list of parameter strings
* @param macro the macro definition
* @param evaluatedVariables the evaluated variable values
*/
public TemplateAperture(
final String id,
final String templateName,
final List<String> parameters,
final MacroDefinition macro,
final Map<Integer, Double> evaluatedVariables) {
super(id);
this.templateName = templateName;
this.parameters = parameters;
this.macro = macro;
this.evaluatedVariables =
evaluatedVariables != null
? Collections.unmodifiableMap(new HashMap<>(evaluatedVariables))
: Collections.emptyMap();
this.resolvedPrimitives =
MacroPrimitiveFactory.parse(macro, this.evaluatedVariables, parameters);
}
// ── Accessors ─────────────────────────────────────────────────────────────
/**
* Returns the name of the macro template.
*
* @return the template name
*/
public String getTemplateName() {
return templateName;
}
/**
* Returns the list of parameter strings.
*
* @return the parameters
*/
public List<String> getParameters() {
return parameters;
}
/**
* Returns the macro definition.
*
* @return the macro
*/
public MacroDefinition getMacro() {
return macro;
}
/**
* Returns the evaluated variable values.
*
* @return the evaluated variables
*/
public Map<Integer, Double> getEvaluatedVariables() {
return evaluatedVariables;
}
/**
* Returns the pre-parsed, resolved primitives (immutable list).
*
* @return the resolved primitives
*/
public List<MacroPrimitive> getResolvedPrimitives() {
return resolvedPrimitives;
}
// ── Geometry ──────────────────────────────────────────────────────────────
@Override
public Rectangle2D getBounds() {
Rectangle2D result = null;
for (MacroPrimitive p : resolvedPrimitives) {
Rectangle2D b = p.getBounds();
if (b != null) {
result = (result == null) ? b : result.createUnion(b);
}
}
return result;
}
// ── Rendering ─────────────────────────────────────────────────────────────
/**
* Renders this macro aperture by composing all primitives into a single {@link Area} via {@link
* AreaTarget} and emitting one {@code drawRegion()} call.
*
* <p>Each primitive's draw calls are captured by an {@link AreaTarget} which adds or subtracts
* each shape depending on whether the primitive's effective polarity matches the base polarity:
*
* <ul>
* <li>Same polarity → {@code Area.add()}
* <li>Opposite polarity → {@code Area.subtract()}
* </ul>
*/
@Override
public void render(final Point2D flashPoint, final GerberOutputTarget target, final Polarity polarity) {
if (resolvedPrimitives.isEmpty()) {
target.drawCircle(flashPoint, EMPTY_APERTURE_RADIUS, 0.0, polarity);
return;
}
AreaTarget collector = new AreaTarget(polarity);
for (MacroPrimitive p : resolvedPrimitives) {
p.render(flashPoint, collector, polarity);
}
Area result = collector.getArea();
if (!result.isEmpty()) {
target.drawRegion(new Path2D.Double(result), polarity);
}
}
}