Antialiasing and fill fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s

This commit is contained in:
2026-08-25 15:48:36 +00:00
parent dbe0cdaa89
commit 36b93df986
7 changed files with 760 additions and 148 deletions
@@ -1,6 +1,7 @@
package org.pubvm.j3270.ui;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.graphics.GocaConstants;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.screen.ScreenBuffer;
@@ -905,6 +906,63 @@ public class TerminalPanel extends JPanel {
public void setClient(Telnet3270Client client) {
this.client = client;
if (client != null) {
setupGraphicsPlaneRenderer();
updateCellSize();
}
}
private void setupGraphicsPlaneRenderer() {
if (client != null && client.getGraphicsPlane() != null) {
client.getGraphicsPlane().setTextRenderer((plane, x, y, text, colorArgb, cw, ch, dir, angle) -> {
int[] rgb = plane.getRgbBuffer();
int pw = plane.getCanvasWidth();
int ph = plane.getCanvasHeight();
if (rgb == null || pw <= 0 || ph <= 0) return;
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(
pw, ph, java.awt.image.BufferedImage.TYPE_INT_ARGB
);
img.setRGB(0, 0, pw, ph, rgb, 0, pw);
Graphics2D g2 = img.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
int fontSize = (int) Math.round(ch * 0.95);
if (fontSize < 10) fontSize = 10;
Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize);
g2.setFont(f);
FontMetrics fm = g2.getFontMetrics();
int ascent = fm.getAscent();
g2.setColor(new Color(colorArgb, true));
double curX = x;
double curY = y;
for (int i = 0; i < text.length(); i++) {
String s = text.substring(i, i + 1);
int charW = fm.stringWidth(s);
int drawX = (int) Math.round(curX + Math.max(0, (cw - charW) / 2.0));
int drawY = (int) Math.round(curY + ascent + Math.max(0, (ch - fm.getHeight()) / 2.0));
g2.drawString(s, drawX, drawY);
switch (dir) {
case GocaConstants.CD_TB: curY += ch; break;
case GocaConstants.CD_RL: curX -= cw; break;
case GocaConstants.CD_BT: curY -= ch; break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX += cw;
break;
}
}
g2.dispose();
img.getRGB(0, 0, pw, ph, rgb, 0, pw);
});
}
}
@Override
@@ -931,9 +989,12 @@ public class TerminalPanel extends JPanel {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// Clear entire panel with background color
g2.setColor(bgColor);
@@ -962,7 +1023,11 @@ public class TerminalPanel extends JPanel {
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
lastGraphicsUpdateCount = currentUpdateCount;
}
g2.drawImage(cachedGraphicsImage, ox, oy, gridW, gridH, null);
if (gWidth == gridW && gHeight == gridH) {
g2.drawImage(cachedGraphicsImage, ox, oy, null);
} else {
g2.drawImage(cachedGraphicsImage, ox, oy, gridW, gridH, null);
}
}
}
@@ -105,6 +105,12 @@ public final class GocaConstants {
public static final int LW_NORMAL = 1;
public static final int LW_THICK = 2;
// Character Precision (G_GSCC / 0x3B)
public static final int CP_DEFAULT = 0;
public static final int CP_STRING = 1;
public static final int CP_CHAR = 2;
public static final int CP_STROKE = 3;
// Fill Patterns (GSPT)
public static final int PT_DEFAULT = 0;
public static final int PT_D1 = 1;
@@ -34,6 +34,7 @@ public class GocaDecoder {
private int charWidth = 9;
private int charHeight = 16;
private int charSet = 0;
private int charPrecision = GocaConstants.CP_STRING;
private int arcParamP = 1;
private int arcParamQ = 0;
private int arcParamR = 0;
@@ -47,6 +48,8 @@ public class GocaDecoder {
private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>();
private final List<Integer> areaPolygons = new ArrayList<>();
private int currentPolyPts = 0;
// Image accumulation
private boolean inImage = false;
@@ -212,6 +215,7 @@ public class GocaDecoder {
charDir = GocaConstants.CD_LR;
charAngle = 0.0;
charSet = 0;
charPrecision = GocaConstants.CP_STRING;
inArea = false;
areaDrawBoundary = true;
areaFill = true;
@@ -226,24 +230,30 @@ public class GocaDecoder {
/**
* Determines the total byte length of a GOCA drawing order starting at data[idx].
*
* IMPORTANT ARCHITECTURE NOTE:
* GOCA orders follow IBM GA23-0059 architecture rules:
* 1. 1-byte standalone orders (NOP, etc.) -> length 1.
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG) -> 1 or 2 bytes (with 0x00 trailing byte).
* 3. Fixed 1-byte immediate operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 4. Orders with opcode >= 0x20 (including GCALL 0x2A, GSCS 0x38, GSCD 0x3A, GSPT 0x28, GSMT 0x29, GBAR 0x68,
* GLINE 0xC1, GARC 0xC6, GCHST 0xC3, etc.) are self-defining with a 1-byte length field data[idx+1],
* making total length = payloadLen + 2.
* Never hardcode orders >= 0x20 to 2 bytes, as that desynchronizes the GOCA order stream.
* IMPORTANT ARCHITECTURE & PARSING SAFETY NOTE:
* GOCA orders follow IBM GA23-0059 and Host On-Demand (HOD) architecture rules:
* 1. 1-byte standalone orders (NOP, ERASE, etc.) -> length 1.
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> 1 or 2 bytes (with 0x00 trailing byte).
* 3. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 4. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39,
* GSPT 0x28, GSMT 0x29, GSMS_SET 0x3C) can be transmitted either as:
* - Short 2-byte form: [opcode] [value] (e.g. GBAR with flags 0x80 -> '68 80')
* - Self-defining 3-byte form: [opcode] [length=0x01] [value] (e.g. '68 01 80')
* CRITICAL: NEVER allow 1-byte attribute orders (like GBAR 0x68 or GSCC 0x3B) to fall through to the
* variable-length formula `(data[idx + 1] & 0xFF) + 2`. If GBAR flags (e.g. 0x80 for boundary) are read as
* a length field, the decoder will skip 130 bytes, corrupting and skipping all subsequent drawing orders!
* 5. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.)
* have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2.
*/
private int getOrderLength(byte[] data, int idx, int end) {
int order = data[idx] & 0xFF;
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00) {
return 1;
}
if (order == GocaConstants.G_GEAR ||
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP ||
order == GocaConstants.G_GERASE) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
}
if (idx + 1 >= end) {
@@ -259,7 +269,8 @@ public class GocaDecoder {
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GBAR) {
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
}
if (order == GocaConstants.G_GCALL) {
@@ -578,10 +589,15 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case GocaConstants.G_GSCC: { // Set Character Precision (0x3B)
charPrecision = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
if (charPrecision == 0) charPrecision = GocaConstants.CP_STRING;
idx += orderLen;
break;
}
case 0x04:
case GocaConstants.G_GSMX:
case GocaConstants.G_GSFLW:
case GocaConstants.G_GSCC:
case GocaConstants.G_GSMS_SET:
case GocaConstants.G_GPOP: {
idx += orderLen;
@@ -594,10 +610,8 @@ public class GocaDecoder {
}
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0 || (flags == 0);
boolean fill = (flags == 0) || (flags & 0x40) != 0 ||
(pattern >= 1 && pattern <= 14);
beginArea(drawBoundary, fill);
boolean drawBoundary = (flags & 0x80) != 0 || (flags & 0x40) != 0 || (flags == 0);
beginArea(drawBoundary);
idx += orderLen;
break;
}
@@ -814,22 +828,31 @@ public class GocaDecoder {
}
}
private void beginArea(boolean drawBoundary, boolean fill) {
private void beginArea(boolean drawBoundary) {
this.inArea = true;
this.areaDrawBoundary = drawBoundary;
this.areaFill = fill;
this.areaFill = true;
this.fillColor = this.curColor;
this.areaPointsX.clear();
this.areaPointsY.clear();
this.areaPolygons.clear();
this.currentPolyPts = 0;
}
private void endArea() {
if (!inArea || areaPointsX.size() < 3) {
if (!inArea) return;
if (currentPolyPts > 0) {
areaPolygons.add(currentPolyPts);
currentPolyPts = 0;
}
if (areaPointsX.size() < 3 || areaPolygons.isEmpty()) {
inArea = false;
areaPointsX.clear();
areaPointsY.clear();
areaPolygons.clear();
return;
}
int n = areaPointsX.size();
int[] px = new int[n];
int[] py = new int[n];
@@ -838,21 +861,49 @@ public class GocaDecoder {
py[i] = plane.mapY(areaPointsY.get(i));
}
plane.fillArea(px, py, n, fillColor, areaFill ? pattern : GocaConstants.PT_EMPTY,
areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
int numPolys = areaPolygons.size();
int[] polyCounts = new int[numPolys];
for (int i = 0; i < numPolys; i++) {
polyCounts[i] = areaPolygons.get(i);
}
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
inArea = false;
areaPointsX.clear();
areaPointsY.clear();
areaPolygons.clear();
}
private void addAreaLineStart(int startX, int startY) {
if (!inArea) return;
int sz = areaPointsX.size();
if (sz > 0 && currentPolyPts > 0) {
int lastX = areaPointsX.get(sz - 1);
int lastY = areaPointsY.get(sz - 1);
if (lastX != startX || lastY != startY) {
areaPolygons.add(currentPolyPts);
currentPolyPts = 0;
}
}
if (currentPolyPts == 0) {
areaPointsX.add(startX);
areaPointsY.add(startY);
currentPolyPts++;
}
}
private void addAreaPoint(int x, int y) {
if (inArea) {
int sz = areaPointsX.size();
if (sz == 0 || areaPointsX.get(sz - 1) != x || areaPointsY.get(sz - 1) != y) {
areaPointsX.add(x);
areaPointsY.add(y);
if (!inArea) return;
int sz = areaPointsX.size();
if (sz > 0 && currentPolyPts > 0) {
if (areaPointsX.get(sz - 1) == x && areaPointsY.get(sz - 1) == y) {
return;
}
}
areaPointsX.add(x);
areaPointsY.add(y);
currentPolyPts++;
}
private void beginImage(int x, int y, int w, int h) {
@@ -897,7 +948,7 @@ public class GocaDecoder {
trackPoint(startX, startY);
if (inArea) {
addAreaPoint(startX, startY);
addAreaLineStart(startX, startY);
}
while (pos + 4 <= end) {
@@ -910,8 +961,8 @@ public class GocaDecoder {
if (inArea) {
addAreaPoint(nextX, nextY);
} else {
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
plane.mapX(nextX), plane.mapY(nextY),
plane.drawLine(plane.mapXDouble(startX), plane.mapYDouble(startY),
plane.mapXDouble(nextX), plane.mapYDouble(nextY),
curColor, lineType, lineWidth);
}
@@ -939,7 +990,7 @@ public class GocaDecoder {
trackPoint(startX, startY);
if (inArea) {
addAreaPoint(startX, startY);
addAreaLineStart(startX, startY);
}
while (pos + 2 <= end) {
@@ -955,8 +1006,8 @@ public class GocaDecoder {
if (inArea) {
addAreaPoint(nextX, nextY);
} else {
plane.drawLine(plane.mapX(startX), plane.mapY(startY),
plane.mapX(nextX), plane.mapY(nextY),
plane.drawLine(plane.mapXDouble(startX), plane.mapYDouble(startY),
plane.mapXDouble(nextX), plane.mapYDouble(nextY),
curColor, lineType, lineWidth);
}
@@ -1011,11 +1062,13 @@ public class GocaDecoder {
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
// Map to pixel space
int rx = Math.abs(plane.mapX((int) Math.round(semiAxis1)) - plane.mapX(0));
int ry = Math.abs(plane.mapY(0) - plane.mapY((int) Math.round(semiAxis2)));
if (rx <= 0) rx = Math.max(1, (int) Math.round(semiAxis1));
if (ry <= 0) ry = Math.max(1, (int) Math.round(semiAxis2));
// Map to pixel space with double precision
double cx = plane.mapXDouble(centerX);
double cy = plane.mapYDouble(centerY);
double rx = Math.abs(plane.mapXDouble(semiAxis1) - plane.mapXDouble(0));
double ry = Math.abs(plane.mapYDouble(0) - plane.mapYDouble(semiAxis2));
if (rx <= 0.0) rx = Math.max(1.0, semiAxis1);
if (ry <= 0.0) ry = Math.max(1.0, semiAxis2);
// For partial arcs, determine start angle and sweep angle
double startAngleDeg = 0.0;
@@ -1037,14 +1090,14 @@ public class GocaDecoder {
int sweepFrac = data[pos + 1] & 0xFF;
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
} else {
// No sweep data — if we have a current point, start there and sweep full circle
if (curX != centerX || curY != centerY) {
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
double startRad = Math.atan2(plane.mapYDouble(centerY) - plane.mapYDouble(curY), plane.mapXDouble(curX) - plane.mapXDouble(centerX));
startAngleDeg = Math.toDegrees(startRad);
if (startAngleDeg < 0) startAngleDeg += 360.0;
}
@@ -1055,11 +1108,7 @@ public class GocaDecoder {
trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2));
trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2));
System.out.println("processArc: center=(" + centerX + "," + centerY + ") cur=(" + curX + "," + curY
+ ") rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
+ " isFull=" + isFull);
plane.drawArc(plane.mapX(centerX), plane.mapY(centerY), rx, ry, startAngleDeg, sweepAngleDeg,
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull);
curX = centerX;
@@ -1070,19 +1119,19 @@ public class GocaDecoder {
int pos = off;
int end = off + len;
List<Integer> ptsX = new ArrayList<>();
List<Integer> ptsY = new ArrayList<>();
List<Double> ptsX = new ArrayList<>();
List<Double> ptsY = new ArrayList<>();
if (fromCurPos) {
ptsX.add(plane.mapX(curX));
ptsY.add(plane.mapY(curY));
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
}
while (pos + 4 <= end) {
int x = readCoord(data, pos);
int y = readCoord(data, pos + 2);
ptsX.add(plane.mapX(x));
ptsY.add(plane.mapY(y));
ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y));
curX = x;
curY = y;
pos += 4;
@@ -1090,8 +1139,8 @@ public class GocaDecoder {
if (ptsX.size() >= 2) {
int n = ptsX.size();
int[] px = new int[n];
int[] py = new int[n];
double[] px = new double[n];
double[] py = new double[n];
for (int i = 0; i < n; i++) {
px[i] = ptsX.get(i);
py[i] = ptsY.get(i);
@@ -1106,14 +1155,14 @@ public class GocaDecoder {
if (fromCurPos) {
trackPoint(curX, curY);
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
plane.drawMarker(plane.mapXDouble(curX), plane.mapYDouble(curY), markerType, markerSize, markerColor);
}
while (pos + 4 <= end) {
int x = readCoord(data, pos);
int y = readCoord(data, pos + 2);
trackPoint(x, y);
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
plane.drawMarker(plane.mapXDouble(x), plane.mapYDouble(y), markerType, markerSize, markerColor);
curX = x;
curY = y;
pos += 4;
@@ -1137,8 +1186,8 @@ public class GocaDecoder {
if (textLen <= 0) return;
// IBM 3279 vector graphics base cell is 9x12
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10;
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14;
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14.0;
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
int totalH = (charHeight > 0 ? charHeight : 14);
@@ -1149,20 +1198,24 @@ public class GocaDecoder {
if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) {
int code = data[pos + i] & 0xFF;
int px = plane.mapX(startX);
int py = plane.mapY(startY) - ch;
double px = plane.mapXDouble(startX);
double py = plane.mapYDouble(startY) - ch;
ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
if (slot != null) {
int[] rgb = slot.getRgbPixels(curColor, 0);
int symW = slot.getWidth();
int symH = slot.getHeight();
for (int dy = 0; dy < ch; dy++) {
int sy = (dy * symH) / ch;
for (int dx = 0; dx < cw; dx++) {
int sx = (dx * symW) / cw;
int ipx = (int) Math.round(px);
int ipy = (int) Math.round(py);
int icw = (int) Math.round(cw);
int ich = (int) Math.round(ch);
for (int dy = 0; dy < ich; dy++) {
int sy = (dy * symH) / ich;
for (int dx = 0; dx < icw; dx++) {
int sx = (dx * symW) / icw;
int pixelArgb = rgb[sy * symW + sx];
if ((pixelArgb >>> 24) != 0) {
plane.setPixel(px + dx, py + dy, pixelArgb);
plane.setPixel(ipx + dx, ipy + dy, pixelArgb);
}
}
}
@@ -1183,8 +1236,13 @@ public class GocaDecoder {
}
String text = new String(chars);
plane.drawVectorText(plane.mapX(startX), plane.mapY(startY) - ch, text,
curColor, cw, ch, charDir, charAngle);
if (charPrecision == GocaConstants.CP_STROKE) {
plane.drawVectorText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text,
curColor, cw, ch, charDir, charAngle);
} else {
plane.drawText(plane.mapXDouble(startX), plane.mapYDouble(startY) - ch, text,
curColor, cw, ch, charDir, charAngle);
}
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
curY = startY;
@@ -121,15 +121,32 @@ public class GraphicsPlane {
return screenRows;
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
*/
public double mapXDouble(double gocaX) {
int nominalWidth = screenCols * 9;
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
double nx = gocaX + xMax;
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1);
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
*/
public double mapYDouble(double gocaY) {
int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2;
double ny = yMax - gocaY;
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1);
}
/**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
*/
public int mapX(int gocaX) {
int nominalWidth = screenCols * 9;
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
int nx = gocaX + xMax;
return (int) Math.round((double) nx * canvasWidth / nominalWidth);
return (int) Math.round(mapXDouble((double) gocaX));
}
/**
@@ -138,10 +155,7 @@ public class GraphicsPlane {
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
*/
public int mapY(int gocaY) {
int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2;
int ny = yMax - gocaY;
return (int) Math.round((double) ny * canvasHeight / nominalHeight);
return (int) Math.round(mapYDouble((double) gocaY));
}
/**
@@ -167,20 +181,163 @@ public class GraphicsPlane {
}
/**
* Safely plots a pixel at (x, y).
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending.
*/
public synchronized void setPixel(int x, int y, int colorArgb) {
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
rgbBuffer[y * canvasWidth + x] = colorArgb;
int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return;
int idx = y * canvasWidth + x;
if (srcA == 255) {
rgbBuffer[idx] = colorArgb;
} else {
int dst = rgbBuffer[idx];
int dstA = (dst >>> 24) & 0xFF;
if (dstA == 0) {
rgbBuffer[idx] = colorArgb;
} else {
int srcR = (colorArgb >>> 16) & 0xFF;
int srcG = (colorArgb >>> 8) & 0xFF;
int srcB = colorArgb & 0xFF;
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int invSrcA = 255 - srcA;
int outA = srcA + (dstA * invSrcA + 127) / 255;
int outR = (srcR * srcA + dstR * invSrcA + 127) / 255;
int outG = (srcG * srcA + dstG * invSrcA + 127) / 255;
int outB = (srcB * srcA + dstB * invSrcA + 127) / 255;
rgbBuffer[idx] = ((outA & 0xFF) << 24) | ((outR & 0xFF) << 16) | ((outG & 0xFF) << 8) | (outB & 0xFF);
}
}
hasContent = true;
updateCount++;
updateCount++;
}
}
/**
* Draws an absolute or relative line using Bresenham's algorithm with line styles and widths.
* Plots a pixel with fractional alpha coverage (0.0 to 1.0) for anti-aliasing.
*/
public synchronized void setPixelCoverage(int x, int y, int colorRgb, double coverage) {
if (coverage <= 0.0) return;
int alpha = (int) Math.round(coverage * 255.0);
if (alpha > 255) alpha = 255;
if (alpha <= 0) return;
setPixel(x, y, (alpha << 24) | (colorRgb & 0x00FFFFFF));
}
/**
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates.
*/
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF;
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
(int) Math.round(x1), (int) Math.round(y1),
(0xFF << 24) | color, lineType, lineWidth);
return;
}
// Special case: single point or zero-length line
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
return;
}
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
if (steep) {
double tmp = x0; x0 = y0; y0 = tmp;
tmp = x1; x1 = y1; y1 = tmp;
}
if (x0 > x1) {
double tmp = x0; x0 = x1; x1 = tmp;
tmp = y0; y0 = y1; y1 = tmp;
}
double dx = x1 - x0;
double dy = y1 - y0;
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
// First endpoint
double xend = Math.round(x0);
double yend = y0 + gradient * (xend - x0);
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
int xpxl1 = (int) xend;
int ypxl1 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else {
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
double intery = yend + gradient;
// Second endpoint
xend = Math.round(x1);
yend = y1 + gradient * (xend - x1);
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
int xpxl2 = (int) xend;
int ypxl2 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else {
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
// Main anti-aliased stepping loop
if (steep) {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
plotPixelWu(y + 1, x, color, frac, lineWidth);
intery += gradient;
}
} else {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
plotPixelWu(x, y + 1, color, frac, lineWidth);
intery += gradient;
}
}
hasContent = true;
updateCount++;
}
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK) {
setPixelCoverage(x, y, colorRgb, 1.0);
setPixelCoverage(x + 1, y, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x, y + 1, colorRgb, Math.min(1.0, brightness));
setPixelCoverage(x + 1, y + 1, colorRgb, Math.min(1.0, brightness * 0.7));
} else {
// Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
setPixelCoverage(x, y, colorRgb, b);
}
}
/**
* Draws an absolute or relative line using anti-aliasing for smooth vectors.
*/
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, colorArgb, lineType, lineWidth);
}
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
@@ -255,25 +412,25 @@ public class GraphicsPlane {
}
/**
* Draws a full or partial arc / ellipse.
* Draws a full or partial arc / ellipse with sub-pixel double precision and anti-aliasing.
*/
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
public synchronized void drawArc(double cx, double cy, double rx, double ry, double startAngleDeg, double sweepAngleDeg,
int colorArgb, int lineType, int lineWidth, boolean isFull) {
if (rx <= 0) rx = 1;
if (ry <= 0) ry = 1;
if (rx <= 0) rx = 1.0;
if (ry <= 0) ry = 1.0;
int numSteps = Math.max(24, Math.max(rx, ry) * 4);
int numSteps = (int) Math.max(36, Math.max(rx, ry) * 6);
double startRad = Math.toRadians(startAngleDeg);
double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
double stepRad = sweepRad / numSteps;
int prevX = (int) Math.round(cx + rx * Math.cos(startRad));
int prevY = (int) Math.round(cy - ry * Math.sin(startRad));
double prevX = cx + rx * Math.cos(startRad);
double prevY = cy - ry * Math.sin(startRad);
for (int i = 1; i <= numSteps; i++) {
double angle = startRad + i * stepRad;
int nextX = (int) Math.round(cx + rx * Math.cos(angle));
int nextY = (int) Math.round(cy - ry * Math.sin(angle));
double nextX = cx + rx * Math.cos(angle);
double nextY = cy - ry * Math.sin(angle);
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
prevX = nextX;
prevY = nextY;
@@ -282,10 +439,15 @@ public class GraphicsPlane {
updateCount++;
}
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
int colorArgb, int lineType, int lineWidth, boolean isFull) {
drawArc((double) cx, (double) cy, (double) rx, (double) ry, startAngleDeg, sweepAngleDeg, colorArgb, lineType, lineWidth, isFull);
}
/**
* Draws a Fillet (spline / curve approximation across control points).
* Draws a Fillet (spline / curve approximation across control points) with sub-pixel precision.
*/
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
public synchronized void drawFillet(double[] px, double[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
if (px == null || py == null || numPoints < 2) return;
if (numPoints == 2) {
@@ -293,8 +455,8 @@ public class GraphicsPlane {
return;
}
int prevX = px[0];
int prevY = py[0];
double prevX = px[0];
double prevY = py[0];
for (int i = 0; i < numPoints - 1; i++) {
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
@@ -304,23 +466,32 @@ public class GraphicsPlane {
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
int steps = 20;
int steps = 30;
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double oneMinusT = 1.0 - t;
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
int nextX = (int) Math.round(bx);
int nextY = (int) Math.round(by);
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
prevX = nextX;
prevY = nextY;
drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth);
prevX = bx;
prevY = by;
}
}
hasContent = true;
updateCount++;
}
public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
if (px == null || py == null || numPoints < 2) return;
double[] dpx = new double[numPoints];
double[] dpy = new double[numPoints];
for (int i = 0; i < numPoints; i++) {
dpx[i] = px[i];
dpy[i] = py[i];
}
drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth);
}
/**
* Fills a closed polygon area with a solid color or hatching pattern.
*/
@@ -335,13 +506,24 @@ public class GraphicsPlane {
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, null, 1, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
}
/**
* Fills an area containing one or more closed polygon subpaths with a solid color or hatching pattern,
* using the even-odd fill rule across all subpath contours.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) {
// Find polygon vertical bounds
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
for (int i = 1; i < numPoints; i++) {
@@ -356,13 +538,25 @@ public class GraphicsPlane {
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
int j = numPoints - 1;
for (int i = 0; i < numPoints; i++) {
if ((py[i] < y && py[j] >= y) || (py[j] < y && py[i] >= y)) {
int x = px[i] + (int) Math.round((double) (y - py[i]) / (py[j] - py[i]) * (px[j] - px[i]));
nodeX.add(x);
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
if (pLen >= 3) {
int j = pLen - 1;
for (int i = 0; i < pLen; i++) {
int yi = py[offset + i];
int yj = py[offset + j];
int xi = px[offset + i];
int xj = px[offset + j];
if ((yi < y && yj >= y) || (yj < y && yi >= y)) {
int x = xi + (int) Math.round((double) (y - yi) / (yj - yi) * (xj - xi));
nodeX.add(x);
}
j = i;
}
}
j = i;
offset += pLen;
}
Collections.sort(nodeX);
@@ -373,7 +567,7 @@ public class GraphicsPlane {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern >= 16) {
if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
@@ -389,9 +583,23 @@ public class GraphicsPlane {
}
if (drawBoundary && boundaryColorArgb != 0) {
for (int i = 0; i < numPoints; i++) {
int next = (i + 1) % numPoints;
drawLine(px[i], py[i], px[next], py[next], boundaryColorArgb, lineType, lineWidth);
int offset = 0;
int polyCount = (polyCounts != null && numPolys > 0) ? numPolys : 1;
for (int p = 0; p < polyCount; p++) {
int pLen = (polyCounts != null && p < polyCounts.length) ? polyCounts[p] : numPoints;
if (pLen >= 2) {
for (int i = 0; i < pLen - 1; i++) {
drawLine((double) px[offset + i], (double) py[offset + i],
(double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, lineWidth);
}
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, lineWidth);
}
}
offset += pLen;
}
}
hasContent = true;
@@ -399,11 +607,11 @@ public class GraphicsPlane {
}
/**
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
*/
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int s = Math.max(3, size > 0 ? size : 5);
double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
switch (markerType) {
case GocaConstants.MK_CROSS: // x
@@ -429,8 +637,8 @@ public class GraphicsPlane {
break;
case GocaConstants.MK_6STAR: // 6-point star
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2, y - s, x + s / 2, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2, y + s, x + s / 2, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2.0, y - s, x + s / 2.0, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s / 2.0, y + s, x + s / 2.0, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_8STAR: // 8-point star
drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
@@ -439,21 +647,17 @@ public class GraphicsPlane {
drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break;
case GocaConstants.MK_SDIAMOND: // solid diamond
fillArea(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4,
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
fillArea(new int[]{(int) Math.round(x), (int) Math.round(x + s), (int) Math.round(x), (int) Math.round(x - s)},
new int[]{(int) Math.round(y - s), (int) Math.round(y), (int) Math.round(y + s), (int) Math.round(y)},
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break;
case GocaConstants.MK_SSQUARE: // solid square
fillArea(new int[]{x - s, x + s, x + s, x - s}, new int[]{y - s, y - s, y + s, y + s}, 4,
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
fillArea(new int[]{(int) Math.round(x - s), (int) Math.round(x + s), (int) Math.round(x + s), (int) Math.round(x - s)},
new int[]{(int) Math.round(y - s), (int) Math.round(y - s), (int) Math.round(y + s), (int) Math.round(y + s)},
4, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break;
case GocaConstants.MK_DOT: // dot
for (int dy = -2; dy <= 2; dy++) {
for (int dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy <= 4) {
setPixel(x + dx, y + dy, color);
}
}
}
drawArc(x, y, 2.0, 2.0, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
break;
case GocaConstants.MK_CIRCLE: // circle
default:
@@ -464,6 +668,10 @@ public class GraphicsPlane {
updateCount++;
}
public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
drawMarker((double) x, (double) y, markerType, size, colorArgb);
}
private static final int[] VSS_OFFSETS = new int[256];
static {
Arrays.fill(VSS_OFFSETS, -1);
@@ -481,18 +689,54 @@ public class GraphicsPlane {
}
}
@FunctionalInterface
public interface TextRenderer {
void drawText(GraphicsPlane plane, double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle);
}
private TextRenderer textRenderer;
public void setTextRenderer(TextRenderer renderer) {
this.textRenderer = renderer;
}
public TextRenderer getTextRenderer() {
return this.textRenderer;
}
/**
* Draws stroked vector text using IBM Vector Symbol Set (VSS).
* Draws character text (using pluggable TextRenderer or fallback vector font).
*/
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
public synchronized void drawText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return;
if (textRenderer != null) {
textRenderer.drawText(this, x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
hasContent = true;
updateCount++;
} else {
drawVectorText(x, y, text, colorArgb, cellWidth, cellHeight, dir, angle);
}
}
public synchronized void drawText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
}
/**
* Draws stroked vector text using IBM Vector Symbol Set (VSS) with sub-pixel anti-aliasing.
*/
public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int curX = x;
int curY = y;
int cw = cellWidth > 0 ? cellWidth : 12;
int ch = cellHeight > 0 ? cellHeight : 20;
double curX = x;
double curY = y;
double cw = cellWidth > 0 ? cellWidth : 12.0;
double ch = cellHeight > 0 ? cellHeight : 20.0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
@@ -513,7 +757,12 @@ public class GraphicsPlane {
updateCount++;
}
private void drawVssChar(int x, int y, char c, int color, int cw, int ch) {
public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) {
drawVectorText((double) x, (double) y, text, colorArgb, (double) cellWidth, (double) cellHeight, dir, angle);
}
private void drawVssChar(double x, double y, char c, int color, double cw, double ch) {
int code = (int) c;
if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
return;
@@ -532,20 +781,33 @@ public class GraphicsPlane {
int dataPtr = ptr + 2;
if (numPoints >= 2) {
int prevVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
int prevVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
int prevPx = x + (int) Math.round(((double) prevVx / VectorSymbolData.VSS_WIDTH) * cw);
int prevPy = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - prevVy) / VectorSymbolData.VSS_HEIGHT) * ch);
double[] px = new double[numPoints];
double[] py = new double[numPoints];
int[] ipx = new int[numPoints];
int[] ipy = new int[numPoints];
for (int p = 1; p < numPoints; p++) {
for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
int px = x + (int) Math.round(((double) vx / VectorSymbolData.VSS_WIDTH) * cw);
int py = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch);
px[p] = x + ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
py[p] = y + ((double) (VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch;
ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]);
}
drawLine(prevPx, prevPy, px, py, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
prevPx = px;
prevPy = py;
// If contour is closed (e.g. bold character loop), fill with solid color
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
if (isClosed) {
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
}
for (int p = 0; p < numPoints - 1; p++) {
drawLine(px[p], py[p], px[p + 1], py[p + 1], color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
}
}
ptr += 2 + byteLen;
@@ -74,7 +74,7 @@ public class DataStreamProcessorTest {
input.setLastAid(AID_ENTER);
java.util.concurrent.atomic.AtomicReference<byte[]> sentData = new java.util.concurrent.atomic.AtomicReference<>();
processor.setOutputCallback(sentData::set);
processor.setOutputSender(sentData::set);
byte[] rbRecord = new byte[] { (byte) CMD_RB };
processor.processRecord(rbRecord, 0, rbRecord.length, true);
@@ -335,4 +335,224 @@ public class GocaDecoderTest {
assertEquals(0x40, decoder.getCharSet());
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
}
@Test
public void testAntialiasedLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int red = 0xFFFF0000;
// Draw a diagonal line with Xiaolin Wu anti-aliasing
plane.drawLine(10.0, 10.0, 50.0, 30.0, red, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int y = 0; y < 100; y++) {
for (int x = 0; x < 100; x++) {
int pixel = buffer[y * 100 + x];
if (pixel != 0) {
nonZeroPixels++;
int alpha = (pixel >>> 24) & 0xFF;
int r = (pixel >>> 16) & 0xFF;
assertEquals(255, r, "Red channel must be preserved");
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
}
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage");
}
@Test
public void testSubpixelArcAndAlphaBlending() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
int green = 0xFF00FF00;
plane.drawArc(50.0, 50.0, 30.0, 30.0, 0.0, 360.0, green, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 50, "Expected arc pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha");
}
@Test
public void testVectorTextAntialiasedDrawing() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
plane.clear();
int yellow = 0xFFFFFF00;
plane.drawVectorText(10.0, 10.0, "8% 1985 TAX", yellow, 12.0, 20.0, GocaConstants.CD_LR, 0.0);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false;
int nonZeroPixels = 0;
for (int p : buffer) {
if (p != 0) {
nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) {
hasIntermediateAlpha = true;
}
}
}
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha");
}
@Test
public void testTextRendererPrecisionSwitching() {
GraphicsPlane plane = new GraphicsPlane(200, 100);
GocaDecoder decoder = new GocaDecoder(plane);
final boolean[] textRendererCalled = new boolean[1];
plane.setTextRenderer((p, x, y, text, color, cw, ch, dir, angle) -> {
textRendererCalled[0] = true;
});
// Test String Precision (Default): GCHST (0xC3) with "TAX"
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GCHST);
out.write(0x07); // length = 7 (4 bytes pos + 3 bytes text)
out.write(0x00); out.write(0x00); // x = 0
out.write(0x00); out.write(0x00); // y = 0
out.write(0xE3); // 'T' in EBCDIC
out.write(0xC1); // 'A' in EBCDIC
out.write(0xE7); // 'X' in EBCDIC
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(textRendererCalled[0], "Expected pluggable TextRenderer to be called for String precision");
// Now set Stroke precision (G_GSCC with 3):
textRendererCalled[0] = false;
ByteArrayOutputStream outStroke = new ByteArrayOutputStream();
outStroke.write(GocaConstants.G_GSCC);
outStroke.write(0x01);
outStroke.write(GocaConstants.CP_STROKE); // 3
outStroke.write(GocaConstants.G_GCHST);
outStroke.write(0x07);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0x00); outStroke.write(0x00);
outStroke.write(0xE3); outStroke.write(0xC1); outStroke.write(0xE7);
byte[] strokeStream = outStroke.toByteArray();
decoder.decodeStream(strokeStream, 0, strokeStream.length);
assertFalse(textRendererCalled[0], "Expected drawVectorText (not textRenderer) when precision is CP_STROKE");
assertTrue(plane.hasContent());
}
@Test
public void testThickLineRendering() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
plane.clear();
plane.drawLine(10.0, 10.0, 50.0, 50.0, 0xFF00FF00, GocaConstants.LT_SOLID, GocaConstants.LW_THICK);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 60, "Expected thick line to occupy more pixels than standard 1px line");
}
@Test
public void testShortFormAttributeOrderParsing() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form with 0x80 flag (boundary=true, fill=false),
// followed immediately by GLINE (0xC1 len=12 for 3 points) and GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GBAR);
out.write(0x80); // Short 2-byte form with 0x80 flag! Must NOT be treated as length=128!
out.write(GocaConstants.G_GLINE);
out.write(0x0C); // 3 points = 12 bytes
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x00);
out.write(0x00); out.write(0x32);
out.write(0x00); out.write(0x32);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent(), "Expected GLINE and GEAR inside GBAR short-form 0x80 to be decoded properly");
}
@Test
public void testMultiPolygonAreaFilling() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
// Polygon 1 (e.g. Letter 'T' bar): (10,10) to (30,10) to (30,20) to (10,20) to (10,10)
// Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20)
// GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GBAR);
out.write(0x80);
// Polygon 1
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(20);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
// Polygon 2 (disconnected start -> triggers new subpath)
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points = 20 bytes
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(20);
out.write(0x00); out.write(22); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(40);
out.write(0x00); out.write(18); out.write(0x00); out.write(20);
out.write(GocaConstants.G_GEAR);
out.write(0x00);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer();
int nonZero = 0;
for (int p : buffer) {
if (p != 0) nonZero++;
}
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
}
}
@@ -3,6 +3,7 @@ package org.lib3270j.input;
import org.junit.jupiter.api.Test;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.telnet.TelnetFSM;
import static org.junit.jupiter.api.Assertions.*;
@@ -184,7 +185,7 @@ public class InputProcessorTest {
// Let's verify DataStreamProcessor ReadModified behavior with the same buffer
DataStreamProcessor dsp = new DataStreamProcessor(screen, translator);
dsp.setInputProcessor(input);
dsp.setOutputCallback(sent::set);
dsp.setOutputSender(sent::set);
byte[] rmRecord = new byte[] { (byte) CMD_RM };
dsp.processRecord(rmRecord, 0, rmRecord.length, true);