DxfExporter.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;
/**
* DXF (AutoCAD Drawing Interchange File) export for fabrication tools.
*
* <p>Emits a minimal AutoCAD R12 ASCII DXF containing only an {@code ENTITIES}
* section. Most CAM software (LightBurn, RDWorks, Inkscape, QCAD, LibreCAD,
* EagleCAD, KiCad) accepts this minimal form. Coordinates are in millimetres,
* matching the rest of the optics core.
*
* <p>For an on-axis circular zone plate the boundary of every Fresnel zone is
* emitted as a full {@code CIRCLE} entity (group code 0/CIRCLE) with centre at
* the origin and radius {@code r_n = √(n·λ·f)}. The aperture-clipping circle
* is also emitted as the outermost circle. This is the most useful form for
* laser cutters and pen plotters, which only ever follow path outlines and do
* not fill regions.
*
* <p>For arbitrary masks (off-axis, hex, foil, hologram) there is no concise
* vector representation; clients should use SVG or PDF in those cases.
*/
public final class DxfExporter {
private DxfExporter() {}
/**
* Write zone-boundary circles of an on-axis zone plate as DXF.
*
* <p>Off-axis offsets in {@code p} are honoured by translating the centre.
* The {@code maskType} and {@code polarity} fields do not affect the output:
* a CAM operator typically chooses cut/engrave layers from the resulting
* geometry separately.
*/
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;
double cx = p.targetOffsetXmm();
double cy = p.targetOffsetYmm();
// Number of zones n_max = R² / (λ·f)
int nMax = (int) Math.floor((R * R) / (lambdaMm * f));
try (Writer w = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
writeHeader(w);
// Aperture-clipping outer circle.
writeCircle(w, cx, cy, R);
// Zone boundaries n = 1..nMax (skip any that exceed the aperture).
for (int n = 1; n <= nMax; n++) {
double rn = Math.sqrt(n * lambdaMm * f);
if (rn > R) break;
writeCircle(w, cx, cy, rn);
}
writeFooter(w);
}
}
public static byte[] toDxfBytes(SingleZonePlateParameters p) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeZonePlate(p, baos);
return baos.toByteArray();
}
private static void writeHeader(Writer w) throws IOException {
// Minimal DXF: just an ENTITIES section. AutoCAD R12-compatible.
w.write("0\nSECTION\n2\nENTITIES\n");
}
private static void writeFooter(Writer w) throws IOException {
w.write("0\nENDSEC\n0\nEOF\n");
}
private static void writeCircle(Writer w, double cx, double cy, double r) throws IOException {
// Group codes: 0=entity, 8=layer, 10/20/30=centre x/y/z, 40=radius.
w.write(String.format(Locale.ROOT,
"0\nCIRCLE\n8\n0\n10\n%.6f\n20\n%.6f\n30\n0.0\n40\n%.6f\n",
cx, cy, r));
}
}