MacroPrimitiveFactory.java
package com.varnernet.gerb4j.macro;
import com.varnernet.gerb4j.MacroDefinition;
import com.varnernet.gerb4j.MacroExpressionEvaluator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* Parses the raw primitive strings stored in a {@link MacroDefinition} into typed {@link
* MacroPrimitive} objects given a fully-evaluated variable map.
*
* <p>This is the single implementation of macro-primitive parsing. It replaces the parallel logic
* that previously existed in both {@code MacroApertureRenderer} (for rendering) and {@code
* MacroPrimitiveParser} (for bounds). Each resulting {@link MacroPrimitive} knows its own bounds
* <em>and</em> how to render itself.
*
* <p>Call {@link #parse(MacroDefinition, Map, List)} once at aperture-instantiation time (when the
* AD command is processed) and store the result in the {@link
* com.varnernet.gerb4j.TemplateAperture}.
*/
public final class MacroPrimitiveFactory {
private static final Logger LOG = Logger.getLogger(MacroPrimitiveFactory.class.getName());
private static final int THERMAL_PRIMITIVE_MIN_LENGTH = 7;
private static final int CIRCLE_PRIMITIVE_MIN_LENGTH = 5;
private static final int VECTOR_LINE_PRIMITIVE_MIN_LENGTH = 8;
private static final int CENTER_LINE_PRIMITIVE_MIN_LENGTH = 7;
private static final int OUTLINE_PRIMITIVE_MIN_LENGTH = 6;
private static final int POLYGON_PRIMITIVE_MIN_LENGTH = 7;
private MacroPrimitiveFactory() {
}
/**
* Parse all primitives in {@code macro} using the supplied variable values.
*
* @param macro The macro definition (holds raw primitive strings).
* @param evaluatedVars Map of variable number → evaluated value (may be empty).
* @param parameters Raw parameter strings from the AD command (used as fallback when a variable
* is not in {@code evaluatedVars}).
* @return An unmodifiable list of resolved {@link MacroPrimitive} objects, in source order.
* Comment primitives (type 0) are silently skipped.
*/
public static List<MacroPrimitive> parse(
final MacroDefinition macro, final Map<Integer, Double> evaluatedVars, final List<String> parameters) {
if (macro == null) {
return Collections.emptyList();
}
Map<Integer, Double> vars = evaluatedVars != null ? evaluatedVars : Collections.emptyMap();
List<String> params = parameters != null ? parameters : Collections.emptyList();
List<MacroPrimitive> result = new ArrayList<>();
for (String raw : macro.getPrimitives()) {
try {
MacroPrimitive p = parsePrimitive(raw, vars, params);
if (p != null) {
result.add(p);
}
} catch (Exception e) {
LOG.warning("[MacroPrimitiveFactory] Skipping malformed primitive \""
+ raw + "\": " + e.getMessage());
}
}
return Collections.unmodifiableList(result);
}
// ── Primitive dispatch ─────────────────────────────────────────────────────
private static MacroPrimitive parsePrimitive(
final String raw, final Map<Integer, Double> vars, final List<String> params) {
String rawInput = raw;
if (rawInput.endsWith("*")) {
rawInput = rawInput.substring(0, rawInput.length() - 1);
}
String[] parts = rawInput.split(",");
if (parts.length < 2) {
return null;
}
String type = parts[0].trim();
return switch (type) {
case "0" -> null; // comment primitive — skip
case "1" -> parseCircle(parts, vars, params);
case "20" -> parseVectorLine(parts, vars, params);
case "21" -> parseCenterLine(parts, vars, params);
case "4" -> parseOutline(parts, vars, params);
case "5" -> parsePolygon(parts, vars, params);
case "7" -> parseThermal(parts, vars, params);
default -> null;
};
}
// ── Per-type parsers ───────────────────────────────────────────────────────
/**
* Type 1: {@code 1,exposure,diameter,centerX,centerY[,rotation]}.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed circle macro primitive
*/
private static MacroPrimitive parseCircle(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < CIRCLE_PRIMITIVE_MIN_LENGTH) {
return null;
}
boolean exposed = val(p[1], vars, params) != 0.0;
double diameter = val(p[2], vars, params);
double centerX = val(p[3], vars, params);
double centerY = val(p[4], vars, params);
double rotation = p.length >= 6 ? val(p[5], vars, params) : 0.0;
return new CircleMacroPrimitive(exposed, diameter, centerX, centerY, rotation);
}
/**
* Type 20: {@code 20,exposure,width,startX,startY,endX,endY,rotation}.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed vector line macro primitive
*/
private static MacroPrimitive parseVectorLine(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < VECTOR_LINE_PRIMITIVE_MIN_LENGTH) {
return null;
}
boolean exposed = val(p[1], vars, params) != 0.0;
double width = val(p[2], vars, params);
double startX = val(p[3], vars, params);
double startY = val(p[4], vars, params);
double endX = val(p[5], vars, params);
double endY = val(p[6], vars, params);
double rotation = val(p[7], vars, params);
return new VectorLineMacroPrimitive(exposed, width, startX, startY, endX, endY, rotation);
}
/**
* Type 21: {@code 21,exposure,width,height,centerX,centerY,rotation}.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed center line macro primitive
*/
private static MacroPrimitive parseCenterLine(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < CENTER_LINE_PRIMITIVE_MIN_LENGTH) {
return null;
}
boolean exposed = val(p[1], vars, params) != 0.0;
double width = val(p[2], vars, params);
double height = val(p[3], vars, params);
double centerX = val(p[4], vars, params);
double centerY = val(p[5], vars, params);
double rotation = val(p[6], vars, params);
return new CenterLineMacroPrimitive(exposed, width, height, centerX, centerY, rotation);
}
/**
* Type 4: {@code 4,exposure,n,x1,y1,x2,y2,...,xn+1,yn+1,rotation}. The n+1ᵗʰ coordinate pair
* closes the outline back to the first vertex; the last element is always the rotation angle.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed outline macro primitive
*/
private static MacroPrimitive parseOutline(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < OUTLINE_PRIMITIVE_MIN_LENGTH) {
return null;
}
boolean exposed = val(p[1], vars, params) != 0.0;
int numVertices = (int) val(p[2], vars, params);
double rotation = val(p[p.length - 1], vars, params);
// n+1 coordinate pairs start at index 3, each pair is (x, y)
int maxVertices = numVertices + 1;
double[] xs = new double[maxVertices];
double[] ys = new double[maxVertices];
for (int i = 0; i < maxVertices; i++) {
int xi = 3 + i * 2;
int yi = 4 + i * 2;
// Leave room for the trailing rotation field
if (xi >= p.length - 1 || yi >= p.length - 1) {
break;
}
xs[i] = val(p[xi], vars, params);
ys[i] = val(p[yi], vars, params);
}
return new OutlineMacroPrimitive(exposed, xs, ys, rotation);
}
/**
* Type 5: {@code 5,exposure,numVertices,centerX,centerY,diameter,rotation}.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed polygon macro primitive
*/
private static MacroPrimitive parsePolygon(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < POLYGON_PRIMITIVE_MIN_LENGTH) {
return null;
}
boolean exposed = val(p[1], vars, params) != 0.0;
int numVertices = (int) val(p[2], vars, params);
double centerX = val(p[3], vars, params);
double centerY = val(p[4], vars, params);
double diameter = val(p[5], vars, params);
double rotation = val(p[6], vars, params);
return new PolygonMacroPrimitive(exposed, numVertices, centerX, centerY, diameter, rotation);
}
/**
* Type 7: {@code 7,centerX,centerY,outerDiameter,innerDiameter,gapThickness,rotation}.
*
* @param p the primitive parameters
* @param vars the variable values
* @param params the macro parameters
* @return the parsed thermal macro primitive
*/
private static MacroPrimitive parseThermal(
final String[] p, final Map<Integer, Double> vars, final List<String> params) {
if (p.length < THERMAL_PRIMITIVE_MIN_LENGTH) {
return null;
}
double centerX = val(p[1], vars, params);
double centerY = val(p[2], vars, params);
double outerDia = val(p[3], vars, params);
double innerDia = val(p[4], vars, params);
double gapThick = val(p[5], vars, params);
double rotation = val(p[6], vars, params);
return new ThermalMacroPrimitive(centerX, centerY, outerDia, innerDia, gapThick, rotation);
}
// ── Expression evaluation ──────────────────────────────────────────────────
/**
* Evaluate a macro expression string to a double value.
*
* <p>This method parses and evaluates arithmetic expressions in Gerber macro definitions,
* supporting variables ($1, $2, etc.), basic arithmetic (+, -, *, /), and parentheses.
*
* <p>This is the single canonical implementation, replacing the duplicate {@code parseValue()}
* methods that existed in {@code MacroApertureRenderer} and {@code MacroPrimitiveParser}.
*
* @param expr the expression string to evaluate
* @param vars the variable map for $N references
* @param params the parameter list for $1, $2, etc.
* @return the evaluated double value
*/
static double val(final String expr, final Map<Integer, Double> vars, final List<String> params) {
String exprInput = expr.trim();
// Fast path: plain decimal literal
try {
return Double.parseDouble(exprInput);
} catch (NumberFormatException ignored) {
}
// Full expression evaluation with variable substitution
return MacroExpressionEvaluator.DEFAULT.evaluateWithVariables(exprInput, params, vars);
}
}