Phase 5
This commit is contained in:
@@ -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 >= 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user