From 40ebd40fe2e3a2d0d2857995e0aaa4d3a4f56f40 Mon Sep 17 00:00:00 2001 From: Rudi Date: Fri, 28 Aug 2026 13:36:02 -0400 Subject: [PATCH] Phase 5 --- .../nightmare/lib3270j/graphics/FillArea.java | 224 +++++++++++++ .../lib3270j/graphics/FilletPts.java | 109 +++++++ .../graphics/GddmCoordinateTransform.java | 8 + .../lib3270j/graphics/GocaDecoder.java | 208 +++++++++++- .../lib3270j/graphics/GraphicsPlane.java | 115 ++++--- .../graphics/ProgramSymbolManager.java | 36 +++ .../lib3270j/graphics/ProgramSymbolSet.java | 4 + .../lib3270j/graphics/VectorSymbolData.java | 59 ++++ .../lib3270j/graphics/FillAreaTest.java | 99 ++++++ .../lib3270j/graphics/FilletPtsTest.java | 71 +++++ .../graphics/GocaDecoderPhase5Test.java | 296 ++++++++++++++++++ 11 files changed, 1180 insertions(+), 49 deletions(-) create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java create mode 100644 lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FilletPtsTest.java create mode 100644 lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderPhase5Test.java diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java new file mode 100644 index 0000000..696f370 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FillArea.java @@ -0,0 +1,224 @@ +package haus.nightmare.lib3270j.graphics; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Encapsulates edge table representation and scanline rasterization for GOCA filled areas. + * Matches IBM Host On-Demand (HODArea.java, FillArea.java) area processing. + * + *

Supports even-odd multi-polygon subpath rasterization, all 17 standard IBM GOCA fill patterns (0-16), + * custom Programmed Symbol pattern sets (LCID >= 0x40), and background mix modes (BMX_LEAVE / BMX_OVER). + */ +public class FillArea { + + /** + * Internal representation of a directed polygon edge for scanline intersection. + */ + public static class Edge { + public final double x1, y1; + public final double x2, y2; + + public Edge(double x1, double y1, double x2, double y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + } + + private final List edges = new ArrayList<>(); + private final List subpathsX = new ArrayList<>(); + private final List subpathsY = new ArrayList<>(); + + public FillArea() {} + + /** + * Adds a single directed edge to the edge table. + */ + public synchronized void addEdge(double x1, double y1, double x2, double y2) { + if (Math.abs(y1 - y2) > 1e-6) { + edges.add(new Edge(x1, y1, x2, y2)); + } + } + + /** + * Adds an integer directed edge to the edge table. + */ + public synchronized void addEdge(int x1, int y1, int x2, int y2) { + addEdge((double) x1, (double) y1, (double) x2, (double) y2); + } + + /** + * Adds a complete closed or open polygon subpath. + */ + public synchronized void addPolygon(double[] px, double[] py, int numPoints) { + if (px == null || py == null || numPoints < 2) return; + int n = Math.min(numPoints, Math.min(px.length, py.length)); + double[] sx = new double[n]; + double[] sy = new double[n]; + System.arraycopy(px, 0, sx, 0, n); + System.arraycopy(py, 0, sy, 0, n); + subpathsX.add(sx); + subpathsY.add(sy); + + for (int i = 0; i < n - 1; i++) { + addEdge(px[i], py[i], px[i + 1], py[i + 1]); + } + if (n >= 3 && (Math.abs(px[0] - px[n - 1]) > 1e-6 || Math.abs(py[0] - py[n - 1]) > 1e-6)) { + addEdge(px[n - 1], py[n - 1], px[0], py[0]); + } + } + + /** + * Adds an integer polygon subpath. + */ + public synchronized void addPolygon(int[] px, int[] py, int numPoints) { + if (px == null || py == null || numPoints < 2) return; + int n = Math.min(numPoints, Math.min(px.length, py.length)); + double[] dpx = new double[n]; + double[] dpy = new double[n]; + for (int i = 0; i < n; i++) { + dpx[i] = px[i]; + dpy[i] = py[i]; + } + addPolygon(dpx, dpy, n); + } + + public synchronized boolean isEmpty() { + return edges.isEmpty() && subpathsX.isEmpty(); + } + + public synchronized int getEdgeCount() { + return edges.size(); + } + + public synchronized int getSubpathCount() { + return subpathsX.size(); + } + + public synchronized void clear() { + edges.clear(); + subpathsX.clear(); + subpathsY.clear(); + } + + /** + * Rasterizes and fills the accumulated area polygons on the target GraphicsPlane. + */ + public synchronized void fill(GraphicsPlane plane, int fillColorArgb, int patternSet, int pattern, + boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, + int bgMix, int bgColorArgb, ProgramSymbolManager psm) { + if (plane == null) return; + if (edges.isEmpty() && subpathsX.isEmpty()) return; + + int canvasW = plane.getCanvasWidth(); + int canvasH = plane.getCanvasHeight(); + if (canvasW <= 0 || canvasH <= 0) return; + + int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; + int bg = bgColorArgb; + + // Background mix / transparency rule for Black fills: + // BMX_LEAVE / 0 or 2 / MIX_DEFAULT: Transparent black + // BMX_OVER / 1 or 5: Opaque background overpaint + boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && + (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); + + if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { + double minY = Double.MAX_VALUE; + double maxY = Double.MIN_VALUE; + + for (Edge e : edges) { + if (e.y1 < minY) minY = e.y1; + if (e.y2 < minY) minY = e.y2; + if (e.y1 > maxY) maxY = e.y1; + if (e.y2 > maxY) maxY = e.y2; + } + + int iMinY = Math.max(0, (int) Math.floor(minY)); + int iMaxY = Math.min(canvasH - 1, (int) Math.ceil(maxY)); + + List nodeX = new ArrayList<>(); + byte[] patRows = null; + ProgramSymbolSet.SymbolSlot psSlot = null; + + if (patternSet >= 0x40 && psm != null) { + psSlot = psm.getSymbol(patternSet, pattern); + } + if (psSlot == null) { + if (pattern >= 0 && pattern < GraphicsPlane.PATTERN_DATA.length) { + patRows = GraphicsPlane.PATTERN_DATA[pattern]; + } else { + patRows = GraphicsPlane.PATTERN_DATA[0]; + } + } + + for (int y = iMinY; y <= iMaxY; y++) { + nodeX.clear(); + double scanY = y + 0.5; + + for (Edge e : edges) { + if ((e.y1 < scanY && e.y2 >= scanY) || (e.y2 < scanY && e.y1 >= scanY)) { + double x = e.x1 + (scanY - e.y1) / (e.y2 - e.y1) * (e.x2 - e.x1); + nodeX.add(x); + } + } + + Collections.sort(nodeX); + + for (int i = 0; i < nodeX.size(); i += 2) { + if (i + 1 >= nodeX.size()) break; + int leftX = Math.max(0, (int) Math.round(nodeX.get(i))); + int rightX = Math.min(canvasW - 1, (int) Math.round(nodeX.get(i + 1))); + + for (int x = leftX; x <= rightX; x++) { + if (psSlot != null) { + int psW = psSlot.getWidth(); + int psH = psSlot.getHeight(); + int psX = (psW > 0) ? (x % psW) : 0; + int psY = (psH > 0) ? (y % psH) : 0; + byte[] psPix = psSlot.getPixelData(); + int pIdx = psY * psW + psX; + boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); + if (bit) { + plane.setPixel(x, y, fill); + } else if (bgMix == GocaConstants.MIX_OVER) { + plane.setPixel(x, y, bg); + } + } else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { + plane.setPixel(x, y, fill); + } else if (patRows != null) { + int b = patRows[y & 7] & 0xFF; + if (((b >> (7 - (x & 7))) & 1) != 0) { + plane.setPixel(x, y, fill); + } else if (bgMix == GocaConstants.MIX_OVER) { + plane.setPixel(x, y, bg); + } + } + } + } + } + } + + // Draw boundary outlines if enabled + if (drawBoundary && boundaryColorArgb != 0) { + for (int s = 0; s < subpathsX.size(); s++) { + double[] px = subpathsX.get(s); + double[] py = subpathsY.get(s); + int pLen = px.length; + if (pLen >= 2) { + for (int i = 0; i < pLen - 1; i++) { + plane.drawLine(px[i], py[i], px[i + 1], py[i + 1], + boundaryColorArgb, lineType, lineWidth); + } + if (pLen >= 3 && (Math.abs(px[0] - px[pLen - 1]) > 1e-6 || Math.abs(py[0] - py[pLen - 1]) > 1e-6)) { + plane.drawLine(px[pLen - 1], py[pLen - 1], px[0], py[0], + boundaryColorArgb, lineType, lineWidth); + } + } + } + } + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java new file mode 100644 index 0000000..05c4935 --- /dev/null +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/FilletPts.java @@ -0,0 +1,109 @@ +package haus.nightmare.lib3270j.graphics; + +import java.util.ArrayList; +import java.util.List; + +/** + * Calculates intermediate points for IBM GOCA rational quadratic spline fillets. + * Matches IBM Host On-Demand (HODFillet.java, FilletPts.java) curve interpolation. + * + *

A GOCA fillet curve passes from the initial point P_0 to the terminal point P_{N-1}, + * bending tangentially toward each intermediate control point P_i. + */ +public class FilletPts { + + public static final int DEFAULT_STEPS_PER_SEGMENT = 24; + + /** + * Calculates interpolated spline vertices across control points using double precision. + * + * @param px Array of X control coordinates + * @param py Array of Y control coordinates + * @param numPoints Number of control points (starting at index 0) + * @param stepsPerSegment Number of interpolation steps per segment + * @return 2D array of coordinates: result[0] = x coordinates, result[1] = y coordinates + */ + public static double[][] calculate(double[] px, double[] py, int numPoints, int stepsPerSegment) { + if (px == null || py == null || numPoints <= 0) { + return new double[][] { new double[0], new double[0] }; + } + + int n = Math.min(numPoints, Math.min(px.length, py.length)); + if (n == 1) { + return new double[][] { new double[] { px[0] }, new double[] { py[0] } }; + } + + if (n == 2) { + return new double[][] { + new double[] { px[0], px[1] }, + new double[] { py[0], py[1] } + }; + } + + int steps = Math.max(4, stepsPerSegment > 0 ? stepsPerSegment : DEFAULT_STEPS_PER_SEGMENT); + List outX = new ArrayList<>(); + List outY = new ArrayList<>(); + + outX.add(px[0]); + outY.add(py[0]); + + for (int i = 0; i < n - 1; i++) { + double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0; + double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0; + double p1x = px[i]; + double p1y = py[i]; + double p2x = (i == n - 2) ? px[n - 1] : (px[i] + px[i + 1]) / 2.0; + double p2y = (i == n - 2) ? py[n - 1] : (py[i] + py[i + 1]) / 2.0; + + for (int s = 1; s <= steps; s++) { + double t = (double) s / (double) 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; + outX.add(bx); + outY.add(by); + } + } + + int total = outX.size(); + double[] resX = new double[total]; + double[] resY = new double[total]; + for (int i = 0; i < total; i++) { + resX[i] = outX.get(i); + resY[i] = outY.get(i); + } + + return new double[][] { resX, resY }; + } + + /** + * Calculates interpolated spline vertices across control points using integer coordinates. + * + * @param px Array of X control coordinates + * @param py Array of Y control coordinates + * @param numPoints Number of control points + * @param stepsPerSegment Number of interpolation steps per segment + * @return 2D array of coordinates: result[0] = x coordinates, result[1] = y coordinates + */ + public static int[][] calculate(int[] px, int[] py, int numPoints, int stepsPerSegment) { + if (px == null || py == null || numPoints <= 0) { + return new int[][] { new int[0], new int[0] }; + } + int n = Math.min(numPoints, Math.min(px.length, py.length)); + double[] dpx = new double[n]; + double[] dpy = new double[n]; + for (int i = 0; i < n; i++) { + dpx[i] = px[i]; + dpy[i] = py[i]; + } + double[][] res = calculate(dpx, dpy, n, stepsPerSegment); + int total = res[0].length; + int[] rx = new int[total]; + int[] ry = new int[total]; + for (int i = 0; i < total; i++) { + rx[i] = (int) Math.round(res[0][i]); + ry[i] = (int) Math.round(res[1][i]); + } + return new int[][] { rx, ry }; + } +} diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java index 487e26f..a9e8b26 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GddmCoordinateTransform.java @@ -125,6 +125,14 @@ public class GddmCoordinateTransform { return defaultCharHeight; } + /** + * Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py). + * Matches IBM Host On-Demand (HODTransform.calculate(int, int)). + */ + public Point calculate(int gx, int gy) { + return gocaToBase(gx, gy); + } + /** * Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py). */ 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 44f9519..7a08e5a 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -345,6 +345,76 @@ public class GocaDecoder { } } + /** + * Decodes a stream of GOCA drawing orders (matching IBM Host On-Demand HODDecoder.decodeGOCA). + */ + public synchronized void decodeGoca(byte[] data, int offset, int length) { + decodeStream(data, offset, length); + } + + /** + * Decodes a character stream of GOCA drawing orders. + */ + public synchronized void decodeGoca(char[] data, int offset, int length) { + if (data == null || length <= 0 || offset < 0 || offset + length > data.length) return; + byte[] b = new byte[length]; + for (int i = 0; i < length; i++) { + b[i] = (byte) (data[offset + i] & 0xFF); + } + decodeGoca(b, 0, length); + } + + /** + * Processes a discrete GOCA segment. + */ + public synchronized void processSegment(byte[] data, int offset, int length) { + decodeStream(data, offset, length); + } + + /** + * Processes a GOCA segment with char data and start/end offset array. + */ + public synchronized void processSegment(char[] data, int[] offsets) { + if (data == null || offsets == null || offsets.length < 2) return; + int off = offsets[0]; + int len = offsets[1] - offsets[0]; + decodeGoca(data, off, len); + } + + /** + * Executes a stored procedure segment by segment ID, traversing chained next segments. + */ + public synchronized void procedureSegment(int segId) { + if (segId == 0) return; + byte[] segData = segmentStore.get(segId); + if (segData != null) { + int savedSeg = currentSegId; + currentSegId = segId; + SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new); + activeSegmentsInOrder.remove(sb); + activeSegmentsInOrder.add(sb); + callDepth++; + decodeStreamDirect(segData, 0, segData.length); + callDepth--; + currentSegId = savedSeg; + + // Execute chained segments + Integer nextId = segmentChainMap.get(segId); + if (nextId != null && nextId != 0 && callDepth < 16) { + procedureSegment(nextId); + } + } else { + logger.warning("procedureSegment: Segment not found in store: " + segId); + } + } + + /** + * Executes a procedure segment from raw data buffer. + */ + public synchronized void procedureSegment(byte[] procData, int offset, int length) { + decodeStream(procData, offset, length); + } + /** * Decodes a stream of GOCA drawing orders. */ @@ -433,6 +503,7 @@ public class GocaDecoder { } case GocaConstants.G_ENDSEGM: { // End Segment (0x71) logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId)); + int finishedSegId = currentSegId; if (currentSegId != 0) { SegmentBounds sb = segmentBoundsMap.get(currentSegId); if (sb != null) { @@ -444,6 +515,23 @@ public class GocaDecoder { } currentSegId = 0; idx += orderLen; + if (finishedSegId != 0 && callDepth < 16) { + Integer nextId = segmentChainMap.get(finishedSegId); + if (nextId != null && nextId != 0) { + byte[] nextSeg = segmentStore.get(nextId); + if (nextSeg != null) { + logger.info("Executing chained segment nextId=" + nextId); + currentSegId = nextId; + SegmentBounds targetSb = segmentBoundsMap.computeIfAbsent(nextId, SegmentBounds::new); + activeSegmentsInOrder.remove(targetSb); + activeSegmentsInOrder.add(targetSb); + callDepth++; + decodeStreamDirect(nextSeg, 0, nextSeg.length); + callDepth--; + currentSegId = 0; + } + } + } break; } case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39) @@ -1187,17 +1275,17 @@ public class GocaDecoder { List ptsX = new ArrayList<>(); List ptsY = new ArrayList<>(); + List gocaPtsX = new ArrayList<>(); + List gocaPtsY = new ArrayList<>(); if (fromCurPos) { trackPoint(curX, curY); ptsX.add(plane.mapXDouble(curX)); ptsY.add(plane.mapYDouble(curY)); - if (inArea) { - if (currentPolyPts == 0) { - addAreaLineStart(curX, curY); - } else { - addAreaPoint(curX, curY); - } + gocaPtsX.add(curX); + gocaPtsY.add(curY); + if (inArea && currentPolyPts == 0) { + addAreaLineStart(curX, curY); } } @@ -1207,12 +1295,10 @@ public class GocaDecoder { trackPoint(x, y); ptsX.add(plane.mapXDouble(x)); ptsY.add(plane.mapYDouble(y)); - if (inArea) { - if (ptsX.size() == 1 && currentPolyPts == 0) { - addAreaLineStart(x, y); - } else { - addAreaPoint(x, y); - } + gocaPtsX.add(x); + gocaPtsY.add(y); + if (inArea && ptsX.size() == 1 && currentPolyPts == 0) { + addAreaLineStart(x, y); } curX = x; curY = y; @@ -1227,7 +1313,22 @@ public class GocaDecoder { px[i] = ptsX.get(i); py[i] = ptsY.get(i); } - plane.drawFillet(px, py, n, curColor, lineType, lineWidth); + + if (inArea) { + int gn = gocaPtsX.size(); + int[] gx = new int[gn]; + int[] gy = new int[gn]; + for (int i = 0; i < gn; i++) { + gx[i] = gocaPtsX.get(i); + gy[i] = gocaPtsY.get(i); + } + int[][] curve = FilletPts.calculate(gx, gy, gn, 16); + for (int i = 1; i < curve[0].length; i++) { + addAreaPoint(curve[0][i], curve[1][i]); + } + } else { + plane.drawFillet(px, py, n, curColor, lineType, lineWidth); + } } } @@ -1330,6 +1431,87 @@ public class GocaDecoder { curY = startY; } + /** + * Draws a transformed character string (matching HODDecoder.drawGCS). + */ + public synchronized void drawGcs(double x, double y, String text, int color, double cw, double ch, int dir, double angle) { + if (plane != null && text != null && !text.isEmpty()) { + if (charPrecision == GocaConstants.CP_STROKE) { + plane.drawVectorText(x, y, text, color, cw, ch, dir, angle); + } else { + plane.drawText(x, y, text, color, cw, ch, dir, angle); + } + } + } + + /** + * Draws an EBCDIC byte buffer as a transformed character string. + */ + public synchronized void drawGcs(byte[] ebcdicData, int offset, int length, int color, double cw, double ch, int dir, double angle) { + if (ebcdicData == null || length <= 0 || offset < 0 || offset + length > ebcdicData.length) return; + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = EbcdicTranslator.ebcdicToAscii(ebcdicData[offset + i]); + } + drawGcs(plane.mapXDouble(curX), plane.mapYDouble(curY), new String(chars), color, cw, ch, dir, angle); + } + + /** + * Draws a single stroked vector symbol character. + */ + public synchronized void drawHodVss(int charCode, double x, double y, double cw, double ch, int color) { + if (plane != null) { + plane.drawHodVss(charCode, x, y, cw, ch, color); + } + } + + /** + * Builds and returns the Vector Symbol Set glyph index. + */ + public static int[] buildVssIndex() { + return VectorSymbolData.buildVssIndex(); + } + + /** + * Sets the graphics cursor shape (crosshair, box, etc.). + */ + public synchronized void setHodCursorShape(int shape) { + if (plane != null) { + plane.setHodCursorShape(shape); + } + } + + /** + * Attaches the graphics cursor at specific coordinates. + */ + public synchronized void attachGraphicCursor(int x, int y) { + setGraphicsCursorActive(true); + setGraphicCursorPosition(x, y); + if (plane != null) { + plane.attachGraphicCursor(x, y); + } + } + + /** + * Detaches the graphics cursor. + */ + public synchronized void detachGraphicCursor() { + setGraphicsCursorActive(false); + if (plane != null) { + plane.detachGraphicCursor(); + } + } + + /** + * Erases the graphics presentation space and resets drawing attributes. + */ + public synchronized void eraseGraphicsPlane() { + if (plane != null) { + plane.eraseGraphicsPlane(); + } + resetDefaults(); + } + private int readCoord(byte[] data, int off) { return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 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 1b576dd..111371e 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -46,6 +46,13 @@ public class GraphicsPlane { private long updateCount = 0; private ProgramSymbolManager programSymbolManager; + private int currentPattern = GocaConstants.PT_SOLID; + private int currentPatternSet = 0; + private boolean graphicCursorAttached = false; + private int graphicCursorX = 0; + private int graphicCursorY = 0; + private int hodCursorShape = 0; + public void setProgramSymbolManager(ProgramSymbolManager psm) { this.programSymbolManager = psm; } @@ -54,6 +61,65 @@ public class GraphicsPlane { return programSymbolManager; } + public synchronized void setPattern(int pattern) { + if (pattern >= 0 && pattern < PATTERN_DATA.length) { + this.currentPattern = pattern; + } else { + this.currentPattern = GocaConstants.PT_SOLID; + } + } + + public synchronized int getPattern() { + return currentPattern; + } + + public synchronized void setPatternSet(int patternSet) { + this.currentPatternSet = patternSet; + } + + public synchronized int getPatternSet() { + return currentPatternSet; + } + + public synchronized void eraseGraphicsPlane() { + clear(); + } + + public synchronized void attachGraphicCursor(int x, int y) { + this.graphicCursorAttached = true; + this.graphicCursorX = x; + this.graphicCursorY = y; + } + + public synchronized void detachGraphicCursor() { + this.graphicCursorAttached = false; + } + + public synchronized boolean isGraphicCursorAttached() { + return graphicCursorAttached; + } + + public synchronized int getGraphicCursorX() { + return graphicCursorX; + } + + public synchronized int getGraphicCursorY() { + return graphicCursorY; + } + + public synchronized void setGraphicCursorPosition(int x, int y) { + this.graphicCursorX = x; + this.graphicCursorY = y; + } + + public synchronized void setHodCursorShape(int shape) { + this.hodCursorShape = shape; + } + + public synchronized int getHodCursorShape() { + return hodCursorShape; + } + public GraphicsPlane(int width, int height) { this.canvasWidth = Math.max(1, width); this.canvasHeight = Math.max(1, height); @@ -518,27 +584,12 @@ public class GraphicsPlane { return; } - double prevX = px[0]; - double prevY = py[0]; + double[][] spline = FilletPts.calculate(px, py, numPoints, 30); + double[] sx = spline[0]; + double[] sy = spline[1]; - for (int i = 0; i < numPoints - 1; i++) { - double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0; - double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0; - double p1x = px[i]; - double p1y = py[i]; - 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 = 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; - drawLine(prevX, prevY, bx, by, colorArgb, lineType, lineWidth); - prevX = bx; - prevY = by; - } + for (int i = 0; i < sx.length - 1; i++) { + drawLine(sx[i], sy[i], sx[i + 1], sy[i + 1], colorArgb, lineType, lineWidth); } hasContent = true; updateCount++; @@ -555,6 +606,13 @@ public class GraphicsPlane { drawFillet(dpx, dpy, numPoints, colorArgb, lineType, lineWidth); } + /** + * Draws a single stroked vector character from the IBM Vector Symbol Set (VSS). + */ + public synchronized void drawHodVss(int charCode, double x, double y, double cw, double ch, int color) { + drawVssChar(x, y, (char) charCode, color, cw, ch); + } + /** * Fills a closed polygon area with a solid color or hatching pattern. */ @@ -781,22 +839,7 @@ public class GraphicsPlane { drawMarker((double) x, (double) y, markerType, size, colorArgb); } - private static final int[] VSS_OFFSETS = new int[256]; - static { - Arrays.fill(VSS_OFFSETS, -1); - int sym = VectorSymbolData.VSS_SYMBOL_START; // 33 - if (sym < 256) { - VSS_OFFSETS[sym] = 0; - } - for (int i = 0; i < VectorSymbolData.vss_data.length; i++) { - if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF - sym++; - if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) { - VSS_OFFSETS[sym] = i + 1; - } - } - } - } + private static final int[] VSS_OFFSETS = VectorSymbolData.buildVssIndex(); @FunctionalInterface public interface TextRenderer { diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java index 675ea6e..e29bd29 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolManager.java @@ -45,6 +45,11 @@ public class ProgramSymbolManager { } } + public ProgramSymbolManager(int defaultWidth, int defaultHeight) { + this(); + setDefaultCellDimensions(defaultWidth, defaultHeight); + } + /** * Resets all symbol sets. */ @@ -58,6 +63,37 @@ public class ProgramSymbolManager { } } + /** + * Clears a specific symbol set by LCID. + */ + public synchronized void clearSymbolSet(int lcid) { + if (lcid <= 0 || lcid >= 256) return; + lcidMap[lcid] = null; + for (ProgramSymbolSet s : sets) { + if (s != null && s.getLcid() == lcid) { + s.clear(); + s.setLcid(0); + } + } + for (ProgramSymbolSet s : stagingSets) { + if (s != null && s.getLcid() == lcid) { + s.clear(); + s.setLcid(0); + } + } + } + + /** + * Clears an individual symbol glyph within a symbol set. + */ + public synchronized void clearSlot(int lcid, int codePoint) { + ProgramSymbolSet set = getSymbolSet(lcid); + if (set != null) { + int index = (codePoint >= 0x40) ? (codePoint - 0x40) : codePoint; + set.clearSlot(index); + } + } + /** * Commits all staged multi-plane symbol sets to the active presentation sets atomically. */ diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java index 95cf444..8d43184 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/ProgramSymbolSet.java @@ -94,6 +94,10 @@ public class ProgramSymbolSet { return isTriplePlane; } + public boolean isLoaded() { + return pixelData != null && pixelData.length > 0; + } + /** * Returns an image scaled directly to target dimensions (cellWidth x cellHeight). * Enables unscaled 1:1 hardware blitting in Java2D. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/VectorSymbolData.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/VectorSymbolData.java index 4bd93a2..23b3a4d 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/VectorSymbolData.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/VectorSymbolData.java @@ -17,5 +17,64 @@ public final class VectorSymbolData { public static final int MARKER_WIDTH = 24; public static final int MARKER_HEIGHT = 32; static char[] marker_data = new char[]{'\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '\u00ff', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00ff', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0012', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\f', '\u0000', '\n', '\u0000', '\f', '\u0000', '\u0016', '\u00c1', '\b', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\u0012', '\u0000', '\u0010', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\f', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0010', '\u0000', '\f', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u0006', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\u0016', '\u0000', '\u0012', '\u0000', '\n', '\u0000', '\u0006', '\u0000', '\n', '`', '\u0000', '\u00ff', 'h', '\u00c0', '\u00c1', '\u0014', '\u0000', '\u000b', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u0011', '\u0000', '\u000b', '\u0000', '\u000f', '`', '\u0000', '\u00ff', '\u00c5', '\u0018', '\u0000', '\t', '\u0000', '\u0010', '\u0000', '\t', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\u0013', '\u0000', '\u000f', '\u0000', '\r', '\u0000', '\t', '\u0000', '\r', '\u0000', '\t', '\u0000', '\u0010', '\u00ff'}; + + private static final int[] VSS_INDEX = buildVssIndex(); + + /** + * Builds and returns a 256-element offset lookup table for fast VSS character glyph addressing. + */ + public static int[] buildVssIndex() { + int[] offsets = new int[256]; + java.util.Arrays.fill(offsets, -1); + int sym = VSS_SYMBOL_START; + if (sym < 256) { + offsets[sym] = 0; + } + for (int i = 0; i < vss_data.length; i++) { + if (vss_data[i] == END_DEFAULT) { + sym++; + if (sym < 256 && i + 1 < vss_data.length) { + offsets[sym] = i + 1; + } + } + } + return offsets; + } + + /** + * Builds and returns an offset lookup table for standard GOCA markers. + */ + public static int[] buildMarkerIndex() { + int[] offsets = new int[32]; + java.util.Arrays.fill(offsets, -1); + int m = 1; + if (m < 32) { + offsets[m] = 0; + } + for (int i = 0; i < marker_data.length; i++) { + if (marker_data[i] == END_DEFAULT) { + m++; + if (m < 32 && i + 1 < marker_data.length) { + offsets[m] = i + 1; + } + } + } + return offsets; + } + + public static int getVssOffset(int charCode) { + if (charCode >= 0 && charCode < 256) { + return VSS_INDEX[charCode]; + } + return -1; + } + + public static char[] getVssData() { + return vss_data; + } + + public static char[] getMarkerData() { + return marker_data; + } } diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java new file mode 100644 index 0000000..a5d2905 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FillAreaTest.java @@ -0,0 +1,99 @@ +package haus.nightmare.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class FillAreaTest { + + @Test + public void testEmptyAndEdgeAddition() { + FillArea area = new FillArea(); + assertTrue(area.isEmpty()); + assertEquals(0, area.getEdgeCount()); + + area.addEdge(10.0, 10.0, 50.0, 50.0); + assertFalse(area.isEmpty()); + assertEquals(1, area.getEdgeCount()); + + area.clear(); + assertTrue(area.isEmpty()); + assertEquals(0, area.getEdgeCount()); + } + + @Test + public void testAddPolygonAndSubpaths() { + FillArea area = new FillArea(); + int[] px = new int[] { 10, 50, 50, 10 }; + int[] py = new int[] { 10, 10, 50, 50 }; + area.addPolygon(px, py, 4); + + assertEquals(1, area.getSubpathCount()); + // 2 non-horizontal directed edges for a closed 4-sided polygon (horizontal edges skipped in active edge table) + assertEquals(2, area.getEdgeCount()); + } + + @Test + public void testRasterizationSolidAndHatch() { + GraphicsPlane plane = new GraphicsPlane(100, 100); + FillArea area = new FillArea(); + + // Add a 40x40 box at (20,20) to (60,60) + area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4); + + int red = 0xFFFF0000; + area.fill(plane, red, 0, GocaConstants.PT_SOLID, true, red, + GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_DEFAULT, 0xFF000000, null); + + assertTrue(plane.hasContent()); + int[] buffer = plane.getRgbBuffer(); + int redCount = 0; + for (int p : buffer) { + if (p == red) redCount++; + } + // Interior of 40x40 box should have ~1600 red pixels + assertTrue(redCount > 1200, "Expected filled red pixels in solid polygon"); + + // Clear plane and test Pattern 5 (Checkerboard) + plane.clear(); + area.fill(plane, red, 0, 5, true, red, + GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_DEFAULT, 0xFF000000, null); + + assertTrue(plane.hasContent()); + int patCount = 0; + int zeroCount = 0; + for (int y = 25; y <= 55; y++) { + for (int x = 25; x <= 55; x++) { + int p = buffer[y * 100 + x]; + if (p == red) patCount++; + else if (p == 0) zeroCount++; + } + } + assertTrue(patCount > 0, "Expected patterned red pixels"); + assertTrue(zeroCount > 0, "Expected transparent gaps in checkerboard pattern under BMX_LEAVE"); + } + + @Test + public void testBackgroundMixOverpaint() { + GraphicsPlane plane = new GraphicsPlane(100, 100); + FillArea area = new FillArea(); + area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4); + + int red = 0xFFFF0000; + int blue = 0xFF0000FF; // Background overpaint color + area.fill(plane, red, 0, 5, false, 0, + GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.MIX_OVER, blue, null); + + int[] buffer = plane.getRgbBuffer(); + int redCount = 0; + int blueCount = 0; + for (int y = 25; y <= 55; y++) { + for (int x = 25; x <= 55; x++) { + int p = buffer[y * 100 + x]; + if (p == red) redCount++; + else if (p == blue) blueCount++; + } + } + assertTrue(redCount > 0, "Expected pattern foreground red pixels"); + assertTrue(blueCount > 0, "Expected pattern background blue pixels under MIX_OVER"); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FilletPtsTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FilletPtsTest.java new file mode 100644 index 0000000..a94348f --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/FilletPtsTest.java @@ -0,0 +1,71 @@ +package haus.nightmare.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class FilletPtsTest { + + @Test + public void testEmptyAndSinglePointInputs() { + double[][] empty = FilletPts.calculate((double[]) null, (double[]) null, 0, 10); + assertEquals(0, empty[0].length); + assertEquals(0, empty[1].length); + + double[][] single = FilletPts.calculate(new double[] { 10.0 }, new double[] { 20.0 }, 1, 10); + assertEquals(1, single[0].length); + assertEquals(10.0, single[0][0], 1e-6); + assertEquals(20.0, single[1][0], 1e-6); + } + + @Test + public void testTwoPointsStraightLine() { + double[] px = new double[] { 10.0, 50.0 }; + double[] py = new double[] { 20.0, 80.0 }; + double[][] res = FilletPts.calculate(px, py, 2, 10); + + assertEquals(2, res[0].length); + assertEquals(10.0, res[0][0], 1e-6); + assertEquals(20.0, res[1][0], 1e-6); + assertEquals(50.0, res[0][1], 1e-6); + assertEquals(80.0, res[1][1], 1e-6); + } + + @Test + public void testThreePointsQuadraticSpline() { + // Control points: (0, 0) -> (50, 100) -> (100, 0) + double[] px = new double[] { 0.0, 50.0, 100.0 }; + double[] py = new double[] { 0.0, 100.0, 0.0 }; + int steps = 10; + double[][] res = FilletPts.calculate(px, py, 3, steps); + + // Start point must be (0, 0) + assertEquals(0.0, res[0][0], 1e-6); + assertEquals(0.0, res[1][0], 1e-6); + + // End point must be (100, 0) + int last = res[0].length - 1; + assertEquals(100.0, res[0][last], 1e-6); + assertEquals(0.0, res[1][last], 1e-6); + + // Curve must be symmetric and positive Y in between + assertTrue(res[0].length > 15); + for (int i = 1; i < last; i++) { + assertTrue(res[1][i] > 0.0, "Y coordinate along upward curve should be positive"); + assertTrue(res[0][i] >= 0.0 && res[0][i] <= 100.0); + } + } + + @Test + public void testIntegerOverload() { + int[] px = new int[] { 10, 50, 90 }; + int[] py = new int[] { 10, 100, 10 }; + int[][] res = FilletPts.calculate(px, py, 3, 8); + + assertEquals(10, res[0][0]); + assertEquals(10, res[1][0]); + int last = res[0].length - 1; + assertEquals(90, res[0][last]); + assertEquals(10, res[1][last]); + assertTrue(res[0].length > 10); + } +} diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderPhase5Test.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderPhase5Test.java new file mode 100644 index 0000000..53cff95 --- /dev/null +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderPhase5Test.java @@ -0,0 +1,296 @@ +package haus.nightmare.lib3270j.graphics; + +import org.junit.jupiter.api.Test; +import java.io.ByteArrayOutputStream; +import java.awt.Point; + +import static org.junit.jupiter.api.Assertions.*; + +public class GocaDecoderPhase5Test { + + @Test + public void testDecodeGocaByteAndCharOverloads() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red + out.write(GocaConstants.G_GLINE); out.write(0x08); + out.write(0x00); out.write(10); out.write(0x00); out.write(10); + out.write(0x00); out.write(50); out.write(0x00); out.write(50); + + byte[] stream = out.toByteArray(); + decoder.decodeGoca(stream, 0, stream.length); + assertTrue(plane.hasContent()); + + // Test char overload + char[] charStream = new char[stream.length]; + for (int i = 0; i < stream.length; i++) { + charStream[i] = (char) (stream[i] & 0xFF); + } + plane.clear(); + assertFalse(plane.hasContent()); + decoder.decodeGoca(charStream, 0, charStream.length); + assertTrue(plane.hasContent()); + } + + @Test + public void testProcessSegmentAndProcedureSegment() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // Define Segment 5 + ByteArrayOutputStream s5 = new ByteArrayOutputStream(); + s5.write(GocaConstants.G_BEGSEGM); + s5.write(0x0C); + s5.write(0x00); s5.write(0x00); s5.write(0x00); s5.write(0x05); // Seg ID 5 + s5.write(0x00); s5.write(0x00); s5.write(0x00); s5.write(0x00); + s5.write(0x00); s5.write(0x00); s5.write(0x00); s5.write(0x00); + s5.write(GocaConstants.G_GSCOL); s5.write(0x04); // Green + s5.write(GocaConstants.G_GLINE); s5.write(0x08); + s5.write(0x00); s5.write(outCoord(20)); + s5.write(0x00); s5.write(outCoord(20)); + s5.write(0x00); s5.write(outCoord(80)); + s5.write(0x00); s5.write(outCoord(80)); + s5.write(GocaConstants.G_ENDSEGM); s5.write(0x00); + + byte[] seg5Bytes = s5.toByteArray(); + decoder.processSegment(seg5Bytes, 0, seg5Bytes.length); + + assertTrue(plane.hasContent()); + plane.clear(); + assertFalse(plane.hasContent()); + + // Execute via procedureSegment(5) + decoder.procedureSegment(5); + assertTrue(plane.hasContent()); + } + + @Test + public void testChainedSegmentsExecution() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + // Segment 10 chains to Segment 11 + ByteArrayOutputStream s10 = new ByteArrayOutputStream(); + s10.write(GocaConstants.G_BEGSEGM); + s10.write(0x0C); + s10.write(0x00); s10.write(0x00); s10.write(0x00); s10.write(0x0A); // Seg 10 + s10.write(0x00); s10.write(0x00); + s10.write(0x00); s10.write(0x00); // flags + s10.write(0x00); s10.write(0x00); s10.write(0x00); s10.write(0x0B); // Next Seg ID = 11! + s10.write(GocaConstants.G_GSCOL); s10.write(0x01); // Blue + s10.write(GocaConstants.G_GLINE); s10.write(0x08); + s10.write(0x00); s10.write(10); s10.write(0x00); s10.write(10); + s10.write(0x00); s10.write(30); s10.write(0x00); s10.write(30); + s10.write(GocaConstants.G_ENDSEGM); s10.write(0x00); + + // Segment 11 draws a line in Yellow (Color 6) + ByteArrayOutputStream s11 = new ByteArrayOutputStream(); + s11.write(GocaConstants.G_BEGSEGM); + s11.write(0x0C); + s11.write(0x00); s11.write(0x00); s11.write(0x00); s11.write(0x0B); // Seg 11 + s11.write(0x00); s11.write(0x00); + s11.write(0x00); s11.write(0x00); + s11.write(0x00); s11.write(0x00); s11.write(0x00); s11.write(0x00); + s11.write(GocaConstants.G_GSCOL); s11.write(0x06); // Yellow + s11.write(GocaConstants.G_GLINE); s11.write(0x08); + s11.write(0x00); s11.write(40); s11.write(0x00); s11.write(40); + s11.write(0x00); s11.write(70); s11.write(0x00); s11.write(70); + s11.write(GocaConstants.G_ENDSEGM); s11.write(0x00); + + // Store Segment 11 first + decoder.decodeGoca(s11.toByteArray(), 0, s11.toByteArray().length); + plane.clear(); + assertFalse(plane.hasContent()); + + // Execute Segment 10; upon ENDSEGM, Segment 11 should be automatically chained! + decoder.decodeGoca(s10.toByteArray(), 0, s10.toByteArray().length); + assertTrue(plane.hasContent()); + + // Verify both Blue (Seg 10) and Yellow (Seg 11) pixels exist + int blueArgb = GocaConstants.GOCA_COLORS[1]; + int yellowArgb = GocaConstants.GOCA_COLORS[6]; + boolean foundBlue = false; + boolean foundYellow = false; + for (int p : plane.getRgbBuffer()) { + if (p == blueArgb) foundBlue = true; + if (p == yellowArgb) foundYellow = true; + } + assertTrue(foundBlue, "Expected Blue pixel from Segment 10"); + assertTrue(foundYellow, "Expected Yellow pixel from chained Segment 11"); + } + + @Test + public void testEllipticArcConjugateParameters() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Arc Parameters (P=20, Q=0, R=0, S=10) + out.write(GocaConstants.G_GSAP); + out.write(0x08); + out.write(0x00); out.write(20); // P = 20 + out.write(0x00); out.write(0); // Q = 0 + out.write(0x00); out.write(0); // R = 0 + out.write(0x00); out.write(10); // S = 10 + + // Full Arc Absolute at (100, 100), Multiplier = 1.0 (0x01, 0x00) + out.write(GocaConstants.G_GFARC); + out.write(0x06); // 4 bytes center + 2 bytes multiplier + out.write(0x00); out.write(100); // Center X = 100 + out.write(0x00); out.write(100); // Center Y = 100 + out.write(0x01); out.write(0x00); // Multiplier 1.0 + + byte[] stream = out.toByteArray(); + decoder.decodeGoca(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + } + + @Test + public void testFilletDrawingAndAreaAccumulation() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Set Color Pink (3) + out.write(GocaConstants.G_GSCOL); out.write(0x03); + // Fillet Absolute (0,0) -> (50, 80) -> (100, 0) + out.write(GocaConstants.G_GFLT); + out.write(0x0C); // 3 points * 4 bytes = 12 bytes + out.write(0x00); out.write(0); out.write(0x00); out.write(0); + out.write(0x00); out.write(50); out.write(0x00); out.write(80); + out.write(0x00); out.write(100);out.write(0x00); out.write(0); + + byte[] stream = out.toByteArray(); + decoder.decodeGoca(stream, 0, stream.length); + + assertTrue(plane.hasContent()); + } + + @Test + public void testGraphicsCursorAndErase() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + assertFalse(decoder.isGraphicsCursorActive()); + decoder.attachGraphicCursor(50, 75); + assertTrue(decoder.isGraphicsCursorActive()); + assertEquals(50, decoder.getGraphicCursorX()); + assertEquals(75, decoder.getGraphicCursorY()); + assertTrue(plane.isGraphicCursorAttached()); + + decoder.setHodCursorShape(2); + assertEquals(2, plane.getHodCursorShape()); + + decoder.detachGraphicCursor(); + assertFalse(decoder.isGraphicsCursorActive()); + assertFalse(plane.isGraphicCursorAttached()); + + plane.setPixel(10, 10, 0xFFFFFFFF); + assertTrue(plane.hasContent()); + decoder.eraseGraphicsPlane(); + assertFalse(plane.hasContent()); + } + + @Test + public void testGddmCoordinateTransformCalculate() { + GddmCoordinateTransform transform = new GddmCoordinateTransform(80, 24, 9, 16); + Point p1 = transform.calculate(0, 0); + Point p2 = transform.gocaToBase(0, 0); + assertEquals(p2, p1); + assertEquals(360, p1.x); + assertEquals(191, p1.y); + } + + @Test + public void testProgramSymbolManagerSingleAndTriplePlane() { + ProgramSymbolManager psm = new ProgramSymbolManager(9, 16); + + // LoadPS Format 1 Single-Plane (Flags=0x01, LCID=0x41, Start=0x40, RWS=0x02) + // 9x16 char slice: 18 bytes per symbol + byte[] loadps1 = new byte[4 + 18]; + loadps1[0] = 0x01; // Format 1 + loadps1[1] = 0x41; // LCID 0x41 + loadps1[2] = 0x40; // Start codepoint + loadps1[3] = 0x02; // RWS slot 2 + for (int i = 0; i < 18; i++) { + loadps1[4 + i] = (byte) 0xAA; + } + psm.loadps(loadps1); + + ProgramSymbolSet set = psm.getSymbolSet(0x41); + assertNotNull(set); + ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x41, 0x40); + assertNotNull(slot); + assertTrue(slot.isLoaded()); + + // Test clearSlot and clearSymbolSet + psm.clearSlot(0x41, 0x40); + assertNull(psm.getSymbol(0x41, 0x40)); + + psm.clearSymbolSet(0x41); + assertNull(psm.getSymbolSet(0x41)); + + // LoadPS Format 1 Triple-Plane (Flags=0x01, LCID=0x42, Start=0x40, RWS=0x04) + // 3 planes * 18 bytes = 54 bytes + byte[] loadps3Plane = new byte[4 + 54]; + loadps3Plane[0] = 0x01; // Format 1 + loadps3Plane[1] = 0x42; // LCID 0x42 + loadps3Plane[2] = 0x40; // Start codepoint + loadps3Plane[3] = 0x04; // RWS slot 4 (Triple Plane) + for (int i = 0; i < 54; i++) { + loadps3Plane[4 + i] = (byte) 0xFF; + } + psm.loadps(loadps3Plane); + ProgramSymbolSet tripleSet = psm.getSymbolSet(0x42); + assertNotNull(tripleSet); + assertTrue(tripleSet.isTriplePlane()); + } + + @Test + public void testVectorSymbolDataIndexes() { + int[] vssIdx = VectorSymbolData.buildVssIndex(); + assertEquals(256, vssIdx.length); + assertTrue(vssIdx[VectorSymbolData.VSS_SYMBOL_START] >= 0); + assertTrue(VectorSymbolData.getVssOffset(65) >= 0); // 'A' + + int[] markerIdx = VectorSymbolData.buildMarkerIndex(); + assertEquals(32, markerIdx.length); + assertTrue(markerIdx[1] >= 0); + } + + @Test + public void testGraphicsPlanePatternAndVssDrawing() { + GraphicsPlane plane = new GraphicsPlane(300, 200); + plane.setPattern(5); + assertEquals(5, plane.getPattern()); + + plane.setPatternSet(0x40); + assertEquals(0x40, plane.getPatternSet()); + + // Draw VSS glyph 'A' (code 65) + plane.drawHodVss(65, 50.0, 50.0, 16.0, 24.0, 0xFF00FF00); + assertTrue(plane.hasContent()); + } + + @Test + public void testGraphicInputBuilderAllOverloads() { + byte[] sf1 = GraphicInputBuilder.buildGraphicInput(100, 200, 1, true, false, false); + assertEquals(56, sf1.length); + assertEquals(0x00, sf1[0]); + assertEquals(0x34, sf1[1]); + assertEquals(100, ((sf1[24] & 0xFF) << 8) | (sf1[25] & 0xFF)); + assertEquals(200, ((sf1[26] & 0xFF) << 8) | (sf1[27] & 0xFF)); + + byte[] sf2 = GraphicInputBuilder.buildGraphicInput(50, 75, 5, 10, 0xF1, false, true, false, 2, 42); + assertEquals(56, sf2.length); + assertEquals(0x00, sf2[0]); + assertEquals(0x34, sf2[1]); + } + + private static byte outCoord(int val) { + return (byte) (val & 0xFF); + } +}