Files
j3270/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java
T
rudi cbb310b889
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m22s
GOCA fixing
2026-09-04 14:34:01 -04:00

435 lines
17 KiB
Java

package haus.nightmare.lib3270j.graphics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Encapsulates edge table representation and scanline rasterization for GOCA filled areas.
* Matches IBM Host On-Demand (HODArea.java, FillArea.java) area processing.
*
* <p>Supports even-odd multi-polygon subpath rasterization, all 17 standard IBM GOCA fill patterns (0-16),
* custom Programmed Symbol pattern sets (LCID &gt;= 0x40), and background mix modes (BMX_LEAVE / BMX_OVER).
*/
public class FillArea {
/**
* Internal representation of a directed polygon edge for scanline intersection.
*/
public static class Edge {
public final double x1, y1;
public final double x2, y2;
public final int direction; // +1 if y1 < y2 (upward), -1 if y1 > y2 (downward)
public Edge(double x1, double y1, double x2, double y2) {
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.direction = (y2 > y1) ? 1 : -1;
}
}
private int fillRule = GocaConstants.FILL_RULE_EVEN_ODD;
private final List<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> subpathsY = new ArrayList<>();
private int fillColor = -1;
private boolean fillModeOR = false;
private boolean solidFill = true;
private int[] pixelPattern;
private int patternWidth = 8;
private int patternHeight = 8;
public FillArea() {}
public FillArea(int fillRule) {
this.fillRule = fillRule;
}
/**
* IBM Host On-Demand multi-polygon constructor.
*/
public FillArea(int[] px, int[] py, int[] polyCounts, int numPolys, java.awt.Color color) {
this();
if (color != null) {
this.fillColor = color.getRGB();
}
if (px != null && py != null && polyCounts != null) {
int offset = 0;
for (int i = 0; i < numPolys && i < polyCounts.length; i++) {
int count = polyCounts[i];
if (count >= 2 && offset + count <= px.length && offset + count <= py.length) {
int[] sx = new int[count];
int[] sy = new int[count];
System.arraycopy(px, offset, sx, 0, count);
System.arraycopy(py, offset, sy, 0, count);
addPolygon(sx, sy, count);
}
offset += count;
}
}
}
public synchronized java.awt.Rectangle getBounds() {
if (edges.isEmpty()) {
return new java.awt.Rectangle(0, 0, 0, 0);
}
double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
for (Edge e : edges) {
minX = Math.min(minX, Math.min(e.x1, e.x2));
minY = Math.min(minY, Math.min(e.y1, e.y2));
maxX = Math.max(maxX, Math.max(e.x1, e.x2));
maxY = Math.max(maxY, Math.max(e.y1, e.y2));
}
int x = (int) Math.floor(minX);
int y = (int) Math.floor(minY);
int w = (int) Math.ceil(maxX) - x + 1;
int h = (int) Math.ceil(maxY) - y + 1;
return new java.awt.Rectangle(x, y, Math.max(0, w), Math.max(0, h));
}
public synchronized void setFillModeOR() {
this.fillModeOR = true;
}
public synchronized boolean isFillModeOR() {
return this.fillModeOR;
}
public synchronized void set8x8Pattern(byte[] pat) {
this.solidFill = false;
this.patternWidth = 8;
this.patternHeight = 8;
this.pixelPattern = new int[64];
if (pat != null) {
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (j < pat.length) {
this.pixelPattern[i + j * 8] = ((pat[j] >> (7 - i)) & 1) == 1 ? this.fillColor : 0;
}
}
}
}
}
public synchronized void setRGBPattern(int[] pat, int w, int h) {
if (w <= 0 || h <= 0 || pat == null) return;
this.solidFill = false;
this.patternWidth = w;
this.patternHeight = h;
this.pixelPattern = pat;
}
public synchronized java.awt.Image getImage() {
java.awt.Rectangle b = getBounds();
if (b.width <= 0 || b.height <= 0) {
return new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_ARGB);
}
GraphicsPlane tempPlane = new GraphicsPlane(b.x + b.width, b.y + b.height);
fill(tempPlane, fillColor, 0, solidFill ? GocaConstants.PT_SOLID : 0, false, 0, 0, 1, 0, 0, null);
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(b.width, b.height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics g = img.getGraphics();
g.drawImage(tempPlane.getImage(), -b.x, -b.y, null);
g.dispose();
return img;
}
public synchronized void dispose() {
clear();
this.pixelPattern = null;
}
public synchronized void setFillRule(int fillRule) {
this.fillRule = fillRule;
}
public synchronized int getFillRule() {
return fillRule;
}
/**
* Adds a single directed edge to the edge table.
*/
public synchronized void addEdge(double x1, double y1, double x2, double y2) {
if (Math.abs(y1 - y2) > 1e-6) {
edges.add(new Edge(x1, y1, x2, y2));
}
}
/**
* Adds an integer directed edge to the edge table.
*/
public synchronized void addEdge(int x1, int y1, int x2, int y2) {
addEdge((double) x1, (double) y1, (double) x2, (double) y2);
}
/**
* Adds a complete closed or open polygon subpath.
*/
public synchronized void addPolygon(double[] px, double[] py, int numPoints) {
if (px == null || py == null || numPoints < 2) return;
int n = Math.min(numPoints, Math.min(px.length, py.length));
double[] sx = new double[n];
double[] sy = new double[n];
System.arraycopy(px, 0, sx, 0, n);
System.arraycopy(py, 0, sy, 0, n);
subpathsX.add(sx);
subpathsY.add(sy);
for (int i = 0; i < n - 1; i++) {
addEdge(px[i], py[i], px[i + 1], py[i + 1]);
}
if (n >= 3 && (Math.abs(px[0] - px[n - 1]) > 1e-6 || Math.abs(py[0] - py[n - 1]) > 1e-6)) {
addEdge(px[n - 1], py[n - 1], px[0], py[0]);
}
}
/**
* Adds an integer polygon subpath.
*/
public synchronized void addPolygon(int[] px, int[] py, int numPoints) {
if (px == null || py == null || numPoints < 2) return;
int n = Math.min(numPoints, Math.min(px.length, py.length));
double[] dpx = new double[n];
double[] dpy = new double[n];
for (int i = 0; i < n; i++) {
dpx[i] = px[i];
dpy[i] = py[i];
}
addPolygon(dpx, dpy, n);
}
public synchronized boolean isEmpty() {
return edges.isEmpty() && subpathsX.isEmpty();
}
public synchronized int getEdgeCount() {
return edges.size();
}
public synchronized int getSubpathCount() {
return subpathsX.size();
}
public synchronized void clear() {
edges.clear();
subpathsX.clear();
subpathsY.clear();
}
/**
* Rasterizes and fills a direct polygon on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
clear();
addPolygon(px, py, numPoints);
fill(plane, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, null);
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, ProgramSymbolManager psm) {
fill(plane, fillColorArgb, patternSet, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb, this.fillRule, psm);
}
private static class NodeIntersection implements Comparable<NodeIntersection> {
final double x;
final int dir;
NodeIntersection(double x, int dir) {
this.x = x;
this.dir = dir;
}
@Override
public int compareTo(NodeIntersection other) {
return Double.compare(this.x, other.x);
}
}
private void plotFillPixel(GraphicsPlane plane, int x, int y, int colorArgb, boolean isMixOr) {
if (isMixOr) {
int dst = plane.getPixel(x, y);
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int srcR = (colorArgb >>> 16) & 0xFF;
int srcG = (colorArgb >>> 8) & 0xFF;
int srcB = colorArgb & 0xFF;
int outR = Math.min(255, dstR | srcR);
int outG = Math.min(255, dstG | srcG);
int outB = Math.min(255, dstB | srcB);
int outA = Math.max((dst >>> 24) & 0xFF, (colorArgb >>> 24) & 0xFF);
if (outA == 0 && (outR != 0 || outG != 0 || outB != 0)) outA = 255;
plane.setPixelDirect(x, y, (outA << 24) | (outR << 16) | (outG << 8) | outB);
} else {
plane.setPixel(x, y, colorArgb);
}
}
/**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
*/
public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb, int fillRule, ProgramSymbolManager psm) {
if (plane == null) return;
if (edges.isEmpty() && subpathsX.isEmpty()) return;
int canvasW = plane.getCanvasWidth();
int canvasH = plane.getCanvasHeight();
if (canvasW <= 0 || canvasH <= 0) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1);
boolean isMixOr = fillModeOR || (plane.getMixMode() == GocaConstants.MIX_OR && (getBounds().width >= 5 && getBounds().height >= 5 && getBounds().width * getBounds().height >= 100));
if (pattern != GocaConstants.PT_EMPTY) {
double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE;
for (Edge e : edges) {
if (e.y1 < minY) minY = e.y1;
if (e.y2 < minY) minY = e.y2;
if (e.y1 > maxY) maxY = e.y1;
if (e.y2 > maxY) maxY = e.y2;
}
int iMinY = Math.max(0, (int) Math.floor(minY));
int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY));
List<NodeIntersection> intersections = new ArrayList<>();
byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && psm != null) {
psSlot = psm.getSymbol(patternSet, pattern);
}
if (psSlot == null) {
if (pattern >= 0 && pattern < GraphicsPlane.PATTERN_DATA.length) {
patRows = GraphicsPlane.PATTERN_DATA[pattern];
} else {
patRows = GraphicsPlane.PATTERN_DATA[0];
}
}
for (int y = iMinY; y <= iMaxY; y++) {
intersections.clear();
double scanY = y + 0.5;
for (Edge e : edges) {
if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) {
double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1);
intersections.add(new NodeIntersection(x, e.direction));
}
}
Collections.sort(intersections);
if (fillRule == GocaConstants.FILL_RULE_WINDING) {
// Non-Zero Winding Rule: evaluate winding count
int winding = 0;
for (int i = 0; i < intersections.size() - 1; i++) {
winding += intersections.get(i).dir;
if (winding != 0) {
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plotFillPixel(plane, x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plotFillPixel(plane, x, y, bg, isMixOr);
}
}
}
}
}
} else {
// Even-Odd / Alternate Rule
for (int i = 0; i < intersections.size(); i += 2) {
if (i + 1 >= intersections.size()) break;
int leftX = Math.max(0, (int) Math.round(intersections.get(i).x));
int rightX = Math.min(canvasW - 1, (int) Math.round(intersections.get(i + 1).x));
for (int x = leftX; x <= rightX; x++) {
if (psSlot != null) {
int psW = psSlot.getWidth();
int psH = psSlot.getHeight();
int psX = (psW > 0) ? (x % psW) : 0;
int psY = (psH > 0) ? (y % psH) : 0;
byte[] psPix = psSlot.getPixelData();
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plotFillPixel(plane, x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plotFillPixel(plane, x, y, bg, isMixOr);
}
}
}
}
}
}
}
// Draw boundary outlines if enabled
if (drawBoundary && boundaryColorArgb != 0) {
for (int s = 0; s < subpathsX.size(); s++) {
double[] px = subpathsX.get(s);
double[] py = subpathsY.get(s);
int pLen = px.length;
if (pLen >= 2) {
for (int i = 0; i < pLen - 1; i++) {
plane.drawLine(px[i], py[i], px[i + 1], py[i + 1],
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (Math.abs(px[0] - px[pLen - 1]) > 1e-6 || Math.abs(py[0] - py[pLen - 1]) > 1e-6)) {
plane.drawLine(px[pLen - 1], py[pLen - 1], px[0], py[0],
boundaryColorArgb, lineType, lineWidth);
}
}
}
}
}
}