WindowFoilParameters.java

package org.fresnel.optics;

import java.util.List;

/**
 * Parameters for a window-foil layout: a rectangular sheet ({@code sheetWidthMm} ×
 * {@code sheetHeightMm}) tiled with hexagonal macro cells on a flat-top hex grid.
 *
 * <p>Each macro cell uses the same {@code subDiameterMm}, {@code subPitchMm} and
 * {@code wavelengthNm}, but can have its own focal length and target offset (for
 * artistic projection patterns). If only one cell-spec is supplied it is reused
 * for every cell.
 *
 * <p>The cell flat-to-flat distance is {@code √3·macroRadiusMm}; the tiling pitch
 * is exactly that so the cells share edges (lückenlos / gap-less). Cells whose
 * centre lies inside the rectangle are rendered (truncated at the sheet edge).
 *
 * @param sheetWidthMm   sheet width, mm
 * @param sheetHeightMm  sheet height, mm
 * @param macroRadiusMm  circumscribed radius of each hex macro cell, mm
 * @param subDiameterMm  sub-element diameter, mm
 * @param subPitchMm     sub-element pitch, mm
 * @param wavelengthNm   design wavelength, nm
 * @param dpi            printer resolution
 * @param maskType       binary amplitude or greyscale phase
 * @param polarity       mask polarity
 * @param cellSpecs      per-cell specifications (focal length + target offset).
 *                       If empty, an on-axis 1 m focus is used everywhere; if size 1
 *                       the spec is reused for every cell; otherwise iterated cyclically
 *                       in row-major cell order.
 * @param drawCropMarks  if true, draw thin crop marks on the sheet corners and at the
 *                       top of each macro cell to aid alignment after cutting
 */
public record WindowFoilParameters(
        double sheetWidthMm,
        double sheetHeightMm,
        double macroRadiusMm,
        double subDiameterMm,
        double subPitchMm,
        double wavelengthNm,
        double dpi,
        MaskType maskType,
        Polarity polarity,
        List<CellSpec> cellSpecs,
        boolean drawCropMarks
) {

    /** Per-cell focal-length and target-offset specification. */
    public record CellSpec(
            double focalLengthMm,
            double targetOffsetXmm,
            double targetOffsetYmm
    ) {
        public CellSpec {
            if (focalLengthMm <= 0) throw new IllegalArgumentException("focalLengthMm must be > 0");
        }
        public static CellSpec onAxis(double focalLengthMm) {
            return new CellSpec(focalLengthMm, 0.0, 0.0);
        }
    }

    public WindowFoilParameters {
        if (sheetWidthMm <= 0) throw new IllegalArgumentException("sheetWidthMm must be > 0");
        if (sheetHeightMm <= 0) throw new IllegalArgumentException("sheetHeightMm must be > 0");
        if (macroRadiusMm <= 0) throw new IllegalArgumentException("macroRadiusMm must be > 0");
        if (subDiameterMm <= 0) throw new IllegalArgumentException("subDiameterMm must be > 0");
        if (subPitchMm < subDiameterMm)
            throw new IllegalArgumentException("subPitchMm must be ≥ subDiameterMm");
        if (subDiameterMm > 2.0 * macroRadiusMm)
            throw new IllegalArgumentException("subDiameterMm must be ≤ 2·macroRadiusMm");
        if (wavelengthNm <= 0) throw new IllegalArgumentException("wavelengthNm must be > 0");
        if (dpi <= 0) throw new IllegalArgumentException("dpi must be > 0");
        if (maskType == null) throw new IllegalArgumentException("maskType must not be null");
        if (polarity == null) throw new IllegalArgumentException("polarity must not be null");
        cellSpecs = cellSpecs == null ? List.of() : List.copyOf(cellSpecs);
    }

    /** Default cell-spec to use when none supplied: on-axis 1 m focus. */
    public CellSpec defaultCellSpec() { return CellSpec.onAxis(1000.0); }

    /** Spec for the {@code i}-th cell (0-based, row-major) — supports empty/single/list. */
    public CellSpec specForCell(int i) {
        if (cellSpecs.isEmpty()) return defaultCellSpec();
        return cellSpecs.get(i % cellSpecs.size());
    }
}