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);
+ }
+}