This commit is contained in:
2026-08-28 13:36:02 -04:00
parent f310d39fc1
commit 40ebd40fe2
11 changed files with 1180 additions and 49 deletions
@@ -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.
*
* <p>Supports even-odd multi-polygon subpath rasterization, all 17 standard IBM GOCA fill patterns (0-16),
* custom Programmed Symbol pattern sets (LCID &gt;= 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<Edge> edges = new ArrayList<>();
private final List<double[]> subpathsX = new ArrayList<>();
private final List<double[]> 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<Double> 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);
}
}
}
}
}
}
@@ -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.
*
* <p>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<Double> outX = new ArrayList<>();
List<Double> 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 };
}
}
@@ -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).
*/
@@ -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<Double> ptsX = new ArrayList<>();
List<Double> ptsY = new ArrayList<>();
List<Integer> gocaPtsX = new ArrayList<>();
List<Integer> gocaPtsY = new ArrayList<>();
if (fromCurPos) {
trackPoint(curX, curY);
ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY));
if (inArea) {
if (currentPolyPts == 0) {
gocaPtsX.add(curX);
gocaPtsY.add(curY);
if (inArea && currentPolyPts == 0) {
addAreaLineStart(curX, curY);
} else {
addAreaPoint(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) {
gocaPtsX.add(x);
gocaPtsY.add(y);
if (inArea && ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
} else {
addAreaPoint(x, y);
}
}
curX = x;
curY = y;
@@ -1227,9 +1313,24 @@ public class GocaDecoder {
px[i] = ptsX.get(i);
py[i] = ptsY.get(i);
}
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);
}
}
}
private void processMarker(byte[] data, int off, int len, boolean fromCurPos) {
int pos = off;
@@ -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));
}
@@ -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 {
@@ -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.
*/
@@ -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.
@@ -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;
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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);
}
}