This commit is contained in:
@@ -423,23 +423,19 @@ public class QueryReplyBuilder {
|
||||
}
|
||||
|
||||
private byte[] buildGraphics(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildGImage(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
0x00, 0x01,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00, 0x00,
|
||||
0x00, (byte) 0xFC,
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
|
||||
@@ -163,7 +163,7 @@ public class GocaDecoder {
|
||||
public synchronized int findPickedSegment(int gx, int gy) {
|
||||
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
|
||||
SegmentBounds sb = activeSegmentsInOrder.get(i);
|
||||
if (sb.contains(gx, gy, 12)) {
|
||||
if (sb.contains(gx, gy, 25)) {
|
||||
System.err.println(String.format(
|
||||
"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
|
||||
@@ -171,6 +171,15 @@ public class GocaDecoder {
|
||||
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));
|
||||
return 0;
|
||||
}
|
||||
@@ -798,7 +807,10 @@ public class GocaDecoder {
|
||||
}
|
||||
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
|
||||
plane.clear();
|
||||
resetDefaults();
|
||||
resetAttributes();
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
activeSegmentsInOrder.clear();
|
||||
idx += 2;
|
||||
break;
|
||||
}
|
||||
@@ -816,6 +828,26 @@ public class GocaDecoder {
|
||||
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) {
|
||||
|
||||
@@ -35,31 +35,19 @@ public class GraphicInputBuilder {
|
||||
|
||||
/**
|
||||
* 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 0-1: Total structured field length (0x0038 = 56 bytes).
|
||||
* - 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 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).
|
||||
* - Byte 34: Modifier flags (Shift = 0x80, Ctrl = 0x40, 0xFF for keyboard AID).
|
||||
* - Byte 35: Button ID (0x01 = Button 1 / Pick, 0x02 = Button 2 / Action) or 3270 AID code.
|
||||
*
|
||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||
* @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 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 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,
|
||||
int pickedSegId, int pickTag) {
|
||||
byte[] sf = new byte[MASK.length];
|
||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||
|
||||
@@ -67,6 +55,14 @@ public class GraphicInputBuilder {
|
||||
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)
|
||||
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
|
||||
sf[25] = (byte) (gocaX & 0xFF);
|
||||
|
||||
@@ -115,11 +115,6 @@ public class GraphicsPlane {
|
||||
public synchronized void setScreenDimensions(int cols, int rows) {
|
||||
this.screenCols = cols > 0 ? cols : 80;
|
||||
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() {
|
||||
@@ -130,29 +125,50 @@ public class GraphicsPlane {
|
||||
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.
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-xMax .. +xMax] (width = cols * 9).
|
||||
*/
|
||||
public double mapXDouble(double gocaX) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||
int totalW = getTotalWidth();
|
||||
int xMax = getXMax();
|
||||
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.
|
||||
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 12).
|
||||
*/
|
||||
public double mapYDouble(double gocaY) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
int yMax = (nominalHeight - 1) / 2;
|
||||
int totalH = getTotalHeight();
|
||||
int yMax = getYMax();
|
||||
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.
|
||||
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
|
||||
*/
|
||||
public int mapX(int 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).
|
||||
* 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) {
|
||||
return (int) Math.round(mapYDouble((double) gocaY));
|
||||
@@ -172,20 +186,20 @@ public class GraphicsPlane {
|
||||
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapX(int px) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0);
|
||||
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1));
|
||||
int totalW = getTotalWidth();
|
||||
int xMax = getXMax();
|
||||
int nx = (int) Math.round((double) px * totalW / (canvasWidth > 0 ? canvasWidth : 1));
|
||||
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.
|
||||
*/
|
||||
public int unmapY(int py) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
int yMax = (nominalHeight - 1) / 2;
|
||||
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1));
|
||||
int totalH = getTotalHeight();
|
||||
int yMax = getYMax();
|
||||
int ny = (int) Math.round((double) py * totalH / (canvasHeight > 0 ? canvasHeight : 1));
|
||||
return yMax - ny;
|
||||
}
|
||||
|
||||
|
||||
@@ -324,12 +324,27 @@ public class InputProcessor {
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
||||
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(
|
||||
gx, gy, button, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
);
|
||||
System.err.println(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
));
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
|
||||
@@ -218,22 +218,22 @@ public class GocaDecoderTest {
|
||||
|
||||
@Test
|
||||
public void test3179GCoordinateMapping() {
|
||||
GraphicsPlane plane = new GraphicsPlane(720, 516);
|
||||
GraphicsPlane plane = new GraphicsPlane(1000, 750);
|
||||
plane.setScreenDimensions(80, 43);
|
||||
|
||||
// Screen center (0, 0) should map to canvas center (360, 257)
|
||||
assertEquals(360, plane.mapX(0));
|
||||
assertEquals(257, plane.mapY(0));
|
||||
// Screen center (0, 0) should map to canvas center
|
||||
assertEquals(500, plane.mapX(0));
|
||||
assertEquals(374, plane.mapY(0));
|
||||
|
||||
// Left edge (-360) should map to 0
|
||||
assertEquals(0, plane.mapX(-360));
|
||||
// Right edge (+359) should map to 719
|
||||
assertEquals(719, plane.mapX(359));
|
||||
// Left edge (-xMax) should map to 0
|
||||
assertEquals(0, plane.mapX(-plane.getXMax()));
|
||||
// Right edge (+xMax) should map to 1000
|
||||
assertEquals(1000, plane.mapX(plane.getXMax()));
|
||||
|
||||
// Top edge (+257) should map to 0
|
||||
assertEquals(0, plane.mapY(257));
|
||||
// Bottom edge (-258) should map to 515
|
||||
assertEquals(515, plane.mapY(-258));
|
||||
// Top edge (+yMax) should map to 0
|
||||
assertEquals(0, plane.mapY(plane.getYMax()));
|
||||
// Bottom edge (yMax - totalHeight) should map to 750
|
||||
assertEquals(750, plane.mapY(plane.getYMax() - plane.getTotalHeight()));
|
||||
|
||||
// Bidirectional roundtrip mapping must be exact
|
||||
assertEquals(0, plane.unmapX(plane.mapX(0)));
|
||||
@@ -512,7 +512,7 @@ public class GocaDecoderTest {
|
||||
|
||||
@Test
|
||||
public void testMultiPolygonAreaFilling() {
|
||||
GraphicsPlane plane = new GraphicsPlane(200, 200);
|
||||
GraphicsPlane plane = new GraphicsPlane(1000, 750);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// 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");
|
||||
|
||||
// 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 borderX = plane.mapX(10);
|
||||
int borderY = plane.mapY(50);
|
||||
int borderPixel = plane.getRgbBuffer()[borderY * plane.getCanvasWidth() + borderX];
|
||||
assertEquals(whiteArgb, borderPixel, "Slide border outline must be drawn in White");
|
||||
boolean foundWhite = false;
|
||||
int y1 = Math.min(plane.mapY(10), plane.mapY(90));
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user