435 lines
17 KiB
Java
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 >= 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|