diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index b12a752..d2f2012 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -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 diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java index ecb802c..4d0ea91 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -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) { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java index 3ec768a..ffaa7c3 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java @@ -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); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index 5af74e5..f39618b 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -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; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index e3ad533..7d4216c 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -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 { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java index 70a80c3..519ef70 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java @@ -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"); } }