This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user