GerberExporter.java
package org.fresnel.optics;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/**
* Gerber RS-274X export for PCB-style fabrication of binary zone plates.
*
* <p>Emits a single-layer Gerber file using a circular aperture flashed at the
* origin with alternating layer polarity ({@code %LPD*%} / {@code %LPC*%}) to
* paint each Fresnel zone. The "ON" zones become copper (dark), the "OFF"
* zones become absence-of-copper (clear). A photoplotter or PCB house can use
* the file directly.
*
* <p>Conventions:
* <ul>
* <li>Units: millimetres ({@code %MOMM*%}).</li>
* <li>Coordinate format: 4 integer / 6 fractional digits, leading zeros omitted
* ({@code %FSLAX46Y46*%}).</li>
* <li>One {@code C} (circle) aperture per zone radius.</li>
* <li>Single flash {@code D03} at {@code 0,0} per aperture; layer polarity is
* toggled before each flash so successive disks paint the alternating
* Fresnel rings.</li>
* </ul>
*
* <p>For arbitrary masks (off-axis, hex, foil, hologram) this exporter is not
* applicable — use SVG or PDF instead.
*/
public final class GerberExporter {
private GerberExporter() {}
/** Write an on-axis circular zone plate as a single-layer RS-274X Gerber file. */
public static void writeZonePlate(SingleZonePlateParameters p, OutputStream out) throws IOException {
double D = p.apertureDiameterMm();
double f = p.focalLengthMm();
double lambdaMm = Units.nmToMm(p.wavelengthNm());
double R = D / 2.0;
boolean positive = p.polarity() == Polarity.POSITIVE;
int nMax = (int) Math.floor((R * R) / (lambdaMm * f));
try (Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
// Header: format spec, units, generator comment.
w.write("G04 Generated by Fresnel Zone Plate Designer*\n");
w.write("%FSLAX46Y46*%\n");
w.write("%MOMM*%\n");
// Aperture macro not needed — use plain circle apertures.
// Define apertures: D10 is the aperture-clipping disk; D11..D(10+nMax)
// are one circle per zone radius. Aperture index 10+n is therefore zone n.
w.write(String.format(Locale.ROOT,
"%%ADD10C,%.6f*%%\n", 2.0 * R));
for (int n = 1; n <= nMax; n++) {
double rn = Math.sqrt(n * lambdaMm * f);
if (rn > R) rn = R;
w.write(String.format(Locale.ROOT,
"%%ADD%dC,%.6f*%%\n", 10 + n, 2.0 * rn));
}
// The outermost disk is the "background". For positive polarity the
// OFF colour is dark (copper present outside the ON zones) and ON zones
// clear it; for negative polarity it is reversed.
//
// Strategy: paint outer disk with the OFF colour (LPD if OFF=dark),
// then for each zone n from largest to smallest, alternate polarity so
// successive disks "carve" the rings.
//
// Zone n is bounded by r_{n-1} and r_n; ON for odd n in positive polarity.
// Painting from largest to smallest with toggling polarity yields the
// correct alternating annuli regardless of starting polarity.
// Start with the OFF colour for the clipping disk.
String offPolarity = positive ? "D" : "C"; // dark vs clear
w.write("%LP" + offPolarity + "*%\n");
w.write("D10*\n");
w.write("X0Y0D03*\n");
// Now paint zones nMax..1 alternating polarity.
// Zone nMax has the OPPOSITE polarity of the background if nMax is odd
// (positive polarity, ON for odd n) — but because we flip on every step,
// we just pick the correct starting polarity for n=nMax.
// ON for odd n iff positive polarity; OFF for even n.
for (int n = nMax; n >= 1; n--) {
boolean on = ((n % 2) == 1) == positive;
String pol = on ? "D" : "C";
w.write("%LP" + pol + "*%\n");
w.write(String.format(Locale.ROOT, "D%d*\n", 10 + n));
w.write("X0Y0D03*\n");
}
// End-of-file.
w.write("M02*\n");
}
}
public static byte[] toGerberBytes(SingleZonePlateParameters p) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeZonePlate(p, baos);
return baos.toByteArray();
}
}