2 Commits

Author SHA1 Message Date
rudi bcfd4ba2e0 Antialiasing fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 40s
2026-08-25 23:31:14 +00:00
rudi 2584f4289f Admdraw appears 2026-08-25 22:50:12 +00:00
10 changed files with 229 additions and 117 deletions
-13
View File
@@ -1,13 +0,0 @@
# Fixed Bugs
- Complete failure when using `cp term conmode 3270` under VM:
Fixed in InputProcessor by sending raw line-mode EBCDIC character data in SSCP-LU mode instead of 3270 AID headers and 1920-byte buffer dumps, and correctly processing subsequent 3270 stream transitions upon CONMODE 3270.
- Unable to start a 2nd transfer after first completed ("A transfer is already in progress"):
Fixed in FTDft by signaling completion upon handling `TR_CLOSE_REQ` / host completion messages, and adding state reset in FileTransfer.
- IND$FILE CMS and TSO:
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
- Local keyboard input and cursor updates not rendering in terminal:
Fixed in ScreenBuffer, InputProcessor, and TerminalPanel by ensuring display snapshot and cursor address are updated synchronously on user input operations (typing, backspace, delete, cursor movement, erase) so the presentation layer immediately renders user keystrokes.
+1 -4
View File
@@ -118,10 +118,7 @@ public class TestRunner {
public static void main(String[] args) { public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors( .selectors(
selectPackage("haus.nightmare.lib3270j.graphics"), selectPackage("haus.nightmare.lib3270j")
selectPackage("haus.nightmare.lib3270j.datastream"),
selectPackage("haus.nightmare.lib3270j.screen"),
selectPackage("haus.nightmare.lib3270j.protocol")
) )
.build(); .build();
@@ -423,23 +423,19 @@ public class QueryReplyBuilder {
} }
private byte[] buildGraphics(int maxCols, int maxRows) { private byte[] buildGraphics(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{ return new byte[]{
(byte) 0x80, 0x02, (byte) 0x80, 0x02,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF), 0x00, 0x00,
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF), 0x00, (byte) 0xFC,
0x00 0x00
}; };
} }
private byte[] buildGImage(int maxCols, int maxRows) { private byte[] buildGImage(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{ return new byte[]{
0x00, 0x01, 0x00, 0x01,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF), 0x00, 0x00,
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF), 0x00, (byte) 0xFC,
0x00, 0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01, 0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0 (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
@@ -163,7 +163,7 @@ public class GocaDecoder {
public synchronized int findPickedSegment(int gx, int gy) { public synchronized int findPickedSegment(int gx, int gy) {
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) { for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
SegmentBounds sb = activeSegmentsInOrder.get(i); SegmentBounds sb = activeSegmentsInOrder.get(i);
if (sb.contains(gx, gy, 12)) { if (sb.contains(gx, gy, 25)) {
System.err.println(String.format( System.err.println(String.format(
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d", "findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
@@ -171,6 +171,15 @@ public class GocaDecoder {
return sb.segId; return sb.segId;
} }
} }
for (SegmentBounds sb : segmentBoundsMap.values()) {
if (sb.contains(gx, gy, 25)) {
System.err.println(String.format(
"findPickedSegment (fallback): goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
));
return sb.segId;
}
}
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy)); System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
return 0; return 0;
} }
@@ -251,26 +260,26 @@ public class GocaDecoder {
*/ */
private int getOrderLength(byte[] data, int idx, int end) { private int getOrderLength(byte[] data, int idx, int end) {
int order = data[idx] & 0xFF; int order = data[idx] & 0xFF;
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
order == GocaConstants.G_GEAR || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_GEIMG ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GERASE) {
return 1; return 1;
} }
// Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 91 00)
if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
}
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
// Fixed 2-byte orders: 1-byte opcode + 1-byte operand // All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS || if (order < 0x20) {
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSLT ||
order == GocaConstants.G_GSLW || order == GocaConstants.G_GSMS) {
return 2; return 2;
} }
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1) // 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 || if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCC ||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) { order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2; return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
} }
@@ -558,11 +567,16 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case 0x06:
case 0x11:
case GocaConstants.G_GSLT: { // Set Line Type (0x18) case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF; lineType = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
break; break;
} }
case 0x04:
case 0x05:
case 0x12:
case GocaConstants.G_GSLW: { // Set Line Width (0x19) case GocaConstants.G_GSLW: { // Set Line Width (0x19)
lineWidth = inputData[idx + 1] & 0xFF; lineWidth = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
@@ -612,9 +626,7 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case 0x04:
case GocaConstants.G_GSMX: case GocaConstants.G_GSMX:
case GocaConstants.G_GSFLW:
case GocaConstants.G_GSMS_SET: case GocaConstants.G_GSMS_SET:
case GocaConstants.G_GPOP: { case GocaConstants.G_GPOP: {
idx += orderLen; idx += orderLen;
@@ -795,7 +807,10 @@ public class GocaDecoder {
} }
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
plane.clear(); plane.clear();
resetDefaults(); resetAttributes();
curX = 0;
curY = 0;
activeSegmentsInOrder.clear();
idx += 2; idx += 2;
break; break;
} }
@@ -804,6 +819,51 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute
if (idx + 5 < end) {
int pLen = data[idx + 1] & 0xFF;
if (pLen >= 6) {
int flags0 = data[idx + 2] & 0xFF;
int flags1 = data[idx + 3] & 0xFF;
int startSeg = ((data[idx + 4] & 0xFF) << 8) | (data[idx + 5] & 0xFF);
int endSeg = (idx + 7 < end) ? (((data[idx + 6] & 0xFF) << 8) | (data[idx + 7] & 0xFF)) : startSeg;
logger.info(String.format("GOCA P_SCUDEF: flags0=0x%02x flags1=0x%02x startSeg=%d endSeg=%d",
flags0, flags1, startSeg, endSeg));
// Redraw all stored base segments not in dynamic range [startSeg..endSeg]
for (int segId : segmentOrderList) {
if (segId < startSeg || segId > endSeg) {
if (!chainedTargets.contains(segId)) {
byte[] segBytes = segmentStore.get(segId);
if (segBytes != null) {
int savedSegId = currentSegId;
currentSegId = segId;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
callDepth++;
decodeStreamDirect(segBytes, 0, segBytes.length);
callDepth--;
currentSegId = savedSegId;
}
}
}
}
for (int s = startSeg; s <= endSeg; s++) {
byte[] segBytes = segmentStore.get(s);
if (segBytes != null) {
int savedSegId = currentSegId;
currentSegId = s;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(s, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
callDepth++;
decodeStreamDirect(segBytes, 0, segBytes.length);
callDepth--;
currentSegId = savedSegId;
}
}
}
}
if (idx + 1 < end) { if (idx + 1 < end) {
int len = (data[idx + 1] & 0xFF) + 2; int len = (data[idx + 1] & 0xFF) + 2;
idx += len; idx += len;
@@ -1164,6 +1224,7 @@ public class GocaDecoder {
List<Double> ptsY = new ArrayList<>(); List<Double> ptsY = new ArrayList<>();
if (fromCurPos) { if (fromCurPos) {
trackPoint(curX, curY);
ptsX.add(plane.mapXDouble(curX)); ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY)); ptsY.add(plane.mapYDouble(curY));
if (inArea) { if (inArea) {
@@ -1178,6 +1239,7 @@ public class GocaDecoder {
while (pos + 4 <= end) { while (pos + 4 <= end) {
int x = readCoord(data, pos); int x = readCoord(data, pos);
int y = readCoord(data, pos + 2); int y = readCoord(data, pos + 2);
trackPoint(x, y);
ptsX.add(plane.mapXDouble(x)); ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y)); ptsY.add(plane.mapYDouble(y));
if (inArea) { if (inArea) {
@@ -8,11 +8,11 @@ public class GraphicInputBuilder {
// 56-byte template mask from IBM Host On-Demand (HODInput.java) // 56-byte template mask from IBM Host On-Demand (HODInput.java)
private static final byte[] MASK = new byte[] { private static final byte[] MASK = new byte[] {
0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40, 0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
}; };
@@ -22,45 +22,47 @@ public class GraphicInputBuilder {
* *
* @param gocaX GOCA signed X coordinate (-xMax..+xMax) * @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax) * @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3) * @param buttonOrAidCode The mouse button number (1=Pick, 2=Action) or 3270 AID code for keyboard
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID * @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down * @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down * @param isCtrl true if ctrl key was down
* @return 56-byte payload * @return 56-byte payload
*/ */
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode, public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) { boolean isMouseAction, boolean isShift, boolean isCtrl) {
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0); return buildGraphicInput(gocaX, gocaY, buttonOrAidCode, isMouseAction, isShift, isCtrl, 0, 0);
} }
/** /**
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag. * Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
*
* IMPORTANT ARCHITECTURE NOTE:
* Per IBM GA23-0059 / GDDM specifications:
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
* with a WCC 0xF7 alarm beep.
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
*
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down
* @param pickedSegId Picked GOCA segment ID (0 if none)
* @param pickTag Pick correlation tag
* @return 56-byte payload
*/ */
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode, public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl, pickedSegId, pickTag);
}
/**
* Builds the 56-byte Graphic Input Structured Field with cursor row/col, picked segment ID and correlation tag.
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl, boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) { int pickedSegId, int pickTag) {
byte[] sf = new byte[MASK.length]; byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length); System.arraycopy(MASK, 0, sf, 0, MASK.length);
// Byte 0-1: Structured Field Length (56 bytes)
sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
sf[1] = (byte) (MASK.length & 0xFF);
// Bytes 16-19: Cursor Row & Column
if (row > 0 || col > 0) {
sf[16] = (byte) (row & 0xFF);
sf[17] = (byte) (col & 0xFF);
sf[18] = (byte) (row & 0xFF);
sf[19] = (byte) (col & 0xFF);
}
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian) // Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
sf[24] = (byte) ((gocaX >> 8) & 0xFF); sf[24] = (byte) ((gocaX >> 8) & 0xFF);
sf[25] = (byte) (gocaX & 0xFF); sf[25] = (byte) (gocaX & 0xFF);
@@ -69,28 +71,29 @@ public class GraphicInputBuilder {
sf[26] = (byte) ((gocaY >> 8) & 0xFF); sf[26] = (byte) ((gocaY >> 8) & 0xFF);
sf[27] = (byte) (gocaY & 0xFF); sf[27] = (byte) (gocaY & 0xFF);
if (pickedSegId != 0) { if (isMouseAction) {
// Byte 28-31: Picked Segment ID (4 bytes big-endian) // Byte 28-31: Picked Segment ID (4 bytes big-endian)
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF); sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF); sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF); sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
sf[31] = (byte) (pickedSegId & 0xFF); sf[31] = (byte) (pickedSegId & 0xFF);
}
if (isMouseAction) { // Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
if (pickTag != 0) { sf[32] = (byte) ((pickTag >> 8) & 0xFF);
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian) sf[33] = (byte) (pickTag & 0xFF);
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
sf[33] = (byte) (pickTag & 0xFF);
}
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00); sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action sf[35] = (byte) (buttonOrAidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
} else { } else {
// Keyboard AID (Enter, PF keys) // Keyboard AID (Enter, PF keys)
sf[28] = 0x00;
sf[29] = 0x00;
sf[30] = 0x00;
sf[31] = 0x07; sf[31] = 0x07;
sf[32] = 0x00;
sf[33] = 0x07; sf[33] = 0x07;
sf[34] = (byte) 0xFF; sf[34] = (byte) 0xFF;
sf[35] = (byte) (aidCode & 0xFF); sf[35] = (byte) (buttonOrAidCode & 0xFF);
} }
return sf; return sf;
@@ -115,11 +115,6 @@ public class GraphicsPlane {
public synchronized void setScreenDimensions(int cols, int rows) { public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80; this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24; this.screenRows = rows > 0 ? rows : 24;
int targetW = this.screenCols * 9;
int targetH = this.screenRows * 12;
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
resize(targetW, targetH);
}
} }
public int getScreenCols() { public int getScreenCols() {
@@ -130,29 +125,50 @@ public class GraphicsPlane {
return screenRows; return screenRows;
} }
public int getTotalWidth() {
int cols = screenCols > 0 ? screenCols : 80;
return cols * 9;
}
public int getTotalHeight() {
int rows = screenRows > 0 ? screenRows : 24;
return rows * 12;
}
public int getXMax() {
int totalW = getTotalWidth();
return (totalW - 1) / 2 + (totalW - 1) % 2;
}
public int getYMax() {
int totalH = getTotalHeight();
return (totalH - 1) / 2;
}
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double. * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
* IBM 3179G / HOD presentation space coordinate range is [-xMax .. +xMax] (width = cols * 9).
*/ */
public double mapXDouble(double gocaX) { public double mapXDouble(double gocaX) {
int nominalWidth = screenCols * 9; int totalW = getTotalWidth();
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); int xMax = getXMax();
double nx = gocaX + xMax; double nx = gocaX + xMax;
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1); return (nx * canvasWidth) / (double) totalW;
} }
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double. * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 12).
*/ */
public double mapYDouble(double gocaY) { public double mapYDouble(double gocaY) {
int nominalHeight = screenRows * 12; int totalH = getTotalHeight();
int yMax = (nominalHeight - 1) / 2; int yMax = getYMax();
double ny = yMax - gocaY; double ny = yMax - gocaY;
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1); return (ny * canvasHeight) / (double) totalH;
} }
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X. * 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) { public int mapX(int gocaX) {
return (int) Math.round(mapXDouble((double) gocaX)); return (int) Math.round(mapXDouble((double) gocaX));
@@ -160,8 +176,6 @@ public class GraphicsPlane {
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
*/ */
public int mapY(int gocaY) { public int mapY(int gocaY) {
return (int) Math.round(mapYDouble((double) gocaY)); return (int) Math.round(mapYDouble((double) gocaY));
@@ -172,20 +186,20 @@ public class GraphicsPlane {
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels. * Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
*/ */
public int unmapX(int px) { public int unmapX(int px) {
int nominalWidth = screenCols * 9; int totalW = getTotalWidth();
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); int xMax = getXMax();
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1)); int nx = (int) Math.round((double) px * totalW / (canvasWidth > 0 ? canvasWidth : 1));
return nx - xMax; return nx - xMax;
} }
/** /**
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up). * Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up, -yMax..+yMax).
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels. * Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
*/ */
public int unmapY(int py) { public int unmapY(int py) {
int nominalHeight = screenRows * 12; int totalH = getTotalHeight();
int yMax = (nominalHeight - 1) / 2; int yMax = getYMax();
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1)); int ny = (int) Math.round((double) py * totalH / (canvasHeight > 0 ? canvasHeight : 1));
return yMax - ny; return yMax - ny;
} }
@@ -324,12 +324,27 @@ public class InputProcessor {
int gy = gocaDecoder.getGraphicCursorY(); int gy = gocaDecoder.getGraphicCursorY();
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy); int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
int pickTag = gocaDecoder.getSegmentTag(pickedSeg); int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
if (gocaDecoder.getGraphicsPlane() != null) {
int px = gocaDecoder.getGraphicsPlane().mapX(gx);
int py = gocaDecoder.getGraphicsPlane().mapY(gy);
int canvasW = gocaDecoder.getGraphicsPlane().getCanvasWidth();
int canvasH = gocaDecoder.getGraphicsPlane().getCanvasHeight();
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 43;
if (canvasH > 0) row = (py * numRows) / canvasH;
if (canvasW > 0) col = (px * cols) / canvasW;
}
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput( byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
); );
System.err.println(String.format( System.err.println(String.format(
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b", "sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
)); ));
out.write(AID_SF); out.write(AID_SF);
try { try {
@@ -218,22 +218,22 @@ public class GocaDecoderTest {
@Test @Test
public void test3179GCoordinateMapping() { public void test3179GCoordinateMapping() {
GraphicsPlane plane = new GraphicsPlane(720, 516); GraphicsPlane plane = new GraphicsPlane(1000, 750);
plane.setScreenDimensions(80, 43); plane.setScreenDimensions(80, 43);
// Screen center (0, 0) should map to canvas center (360, 257) // Screen center (0, 0) should map to canvas center
assertEquals(360, plane.mapX(0)); assertEquals(500, plane.mapX(0));
assertEquals(257, plane.mapY(0)); assertEquals(374, plane.mapY(0));
// Left edge (-360) should map to 0 // Left edge (-xMax) should map to 0
assertEquals(0, plane.mapX(-360)); assertEquals(0, plane.mapX(-plane.getXMax()));
// Right edge (+359) should map to 719 // Right edge (+xMax) should map to 1000
assertEquals(719, plane.mapX(359)); assertEquals(1000, plane.mapX(plane.getXMax()));
// Top edge (+257) should map to 0 // Top edge (+yMax) should map to 0
assertEquals(0, plane.mapY(257)); assertEquals(0, plane.mapY(plane.getYMax()));
// Bottom edge (-258) should map to 515 // Bottom edge (yMax - totalHeight) should map to 750
assertEquals(515, plane.mapY(-258)); assertEquals(750, plane.mapY(plane.getYMax() - plane.getTotalHeight()));
// Bidirectional roundtrip mapping must be exact // Bidirectional roundtrip mapping must be exact
assertEquals(0, plane.unmapX(plane.mapX(0))); assertEquals(0, plane.unmapX(plane.mapX(0)));
@@ -512,7 +512,7 @@ public class GocaDecoderTest {
@Test @Test
public void testMultiPolygonAreaFilling() { public void testMultiPolygonAreaFilling() {
GraphicsPlane plane = new GraphicsPlane(200, 200); GraphicsPlane plane = new GraphicsPlane(1000, 750);
GocaDecoder decoder = new GocaDecoder(plane); GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA) // Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
@@ -786,11 +786,18 @@ public class GocaDecoderTest {
} }
assertTrue(foundBlue, "Slide body must contain stippled blue pixels"); assertTrue(foundBlue, "Slide body must contain stippled blue pixels");
// Verify that the boundary (e.g. at (10, 50)) is drawn in White! // Verify that the boundary (e.g. at X=10) is drawn in White!
int whiteArgb = GocaConstants.GOCA_COLORS[7]; int whiteArgb = GocaConstants.GOCA_COLORS[7];
int borderX = plane.mapX(10); int borderX = plane.mapX(10);
int borderY = plane.mapY(50); boolean foundWhite = false;
int borderPixel = plane.getRgbBuffer()[borderY * plane.getCanvasWidth() + borderX]; int y1 = Math.min(plane.mapY(10), plane.mapY(90));
assertEquals(whiteArgb, borderPixel, "Slide border outline must be drawn in White"); int y2 = Math.max(plane.mapY(10), plane.mapY(90));
for (int y = y1; y <= y2; y++) {
if (plane.getRgbBuffer()[y * plane.getCanvasWidth() + borderX] == whiteArgb) {
foundWhite = true;
break;
}
}
assertTrue(foundWhite, "Slide border outline must be drawn in White");
} }
} }
@@ -138,7 +138,7 @@ public class InputProcessorTest {
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false); byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
assertEquals(56, sf.length); assertEquals(56, sf.length);
assertEquals(0x00, sf[0]); assertEquals(0x00, sf[0]);
assertEquals(0x34, sf[1]); // Length = 52 assertEquals(0x38, sf[1]); // Length = 56 bytes (0x0038)
assertEquals(0x0F, sf[2]); // SF ID = 0x0F assertEquals(0x0F, sf[2]); // SF ID = 0x0F
assertEquals(0x0F, sf[3]); // SF ID = 0x0F assertEquals(0x0F, sf[3]); // SF ID = 0x0F
@@ -153,6 +153,37 @@ public class InputProcessorTest {
assertEquals(0x07, sf[33]); assertEquals(0x07, sf[33]);
assertEquals((byte) 0xFF, sf[34]); assertEquals((byte) 0xFF, sf[34]);
assertEquals((byte) AID_ENTER, sf[35]); assertEquals((byte) AID_ENTER, sf[35]);
// Mouse Button 1 with picked segment 2 and tag 5
byte[] sfMouse = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
294, 192, 1, true, false, false, 2, 5
);
assertEquals(56, sfMouse.length);
assertEquals(0x00, sfMouse[0]);
assertEquals(0x38, sfMouse[1]);
int mx = (sfMouse[24] << 8) | (sfMouse[25] & 0xFF);
int my = (sfMouse[26] << 8) | (sfMouse[27] & 0xFF);
assertEquals(294, (short) mx);
assertEquals(192, (short) my);
int segId = ((sfMouse[28] & 0xFF) << 24) | ((sfMouse[29] & 0xFF) << 16) |
((sfMouse[30] & 0xFF) << 8) | (sfMouse[31] & 0xFF);
assertEquals(2, segId);
int tag = ((sfMouse[32] & 0xFF) << 8) | (sfMouse[33] & 0xFF);
assertEquals(5, tag);
assertEquals(0x00, sfMouse[34]);
assertEquals(0x01, sfMouse[35]); // Button 1
// Mouse Button 2 (Action) with no segment (0)
byte[] sfMouse2 = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
0, 0, 2, true, true, false, 0, 0
);
int segId2 = ((sfMouse2[28] & 0xFF) << 24) | ((sfMouse2[29] & 0xFF) << 16) |
((sfMouse2[30] & 0xFF) << 8) | (sfMouse2[31] & 0xFF);
assertEquals(0, segId2);
int tag2 = ((sfMouse2[32] & 0xFF) << 8) | (sfMouse2[33] & 0xFF);
assertEquals(0, tag2);
assertEquals((byte) 0x80, sfMouse2[34]); // Shift modifier
assertEquals(0x02, sfMouse2[35]); // Button 2
} }
@Test @Test
View File