Admops fix
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m1s

This commit is contained in:
2026-08-25 22:22:20 +00:00
parent 7eebd2107c
commit 3526e682a1
7 changed files with 445 additions and 38 deletions
+60
View File
@@ -87,3 +87,63 @@ echo "=== Build Complete ==="
echo "Executable JAR created: $BUILD_DIR/j3270.jar" echo "Executable JAR created: $BUILD_DIR/j3270.jar"
ls -lh "$BUILD_DIR/j3270.jar" ls -lh "$BUILD_DIR/j3270.jar"
# 5. Compile and run tests if requested
if [ "$1" = "test" ] || [ "$1" = "check" ]; then
echo "=== Running Tests ==="
TEST_BUILD_DIR="$BUILD_DIR/test-classes"
mkdir -p "$TEST_BUILD_DIR"
JUNIT_CP=$(find "$HOME/.gradle/caches/modules-2/files-2.1/" -name "*.jar" 2>/dev/null | grep -E "junit|opentest|apiguardian" | tr "\n" ":" || true)
FULL_CP="$BUILD_DIR/lib3270j:$BUILD_DIR/j3270:$JUNIT_CP"
find "$SCRIPT_DIR/lib3270j/src/test/java" -name "*.java" > "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
if [ -d "$SCRIPT_DIR/j3270/src/test/java" ]; then
find "$SCRIPT_DIR/j3270/src/test/java" -name "*.java" >> "$BUILD_DIR/test_sources.txt" 2>/dev/null || true
fi
if [ -s "$BUILD_DIR/test_sources.txt" ] && [ -n "$JUNIT_CP" ]; then
echo "Compiling tests..."
"$JAVAC_BIN" -cp "$FULL_CP" -d "$TEST_BUILD_DIR" @"$BUILD_DIR/test_sources.txt"
rm -f "$BUILD_DIR/test_sources.txt"
echo "Executing tests with JUnit Platform..."
cat << 'EOF' > "$BUILD_DIR/TestRunner.java"
import org.junit.platform.launcher.*;
import org.junit.platform.launcher.core.*;
import org.junit.platform.launcher.listeners.*;
import org.junit.platform.engine.discovery.*;
import static org.junit.platform.engine.discovery.DiscoverySelectors.*;
import java.io.PrintWriter;
public class TestRunner {
public static void main(String[] args) {
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("haus.nightmare.lib3270j.graphics"),
selectPackage("haus.nightmare.lib3270j.datastream"),
selectPackage("haus.nightmare.lib3270j.screen"),
selectPackage("haus.nightmare.lib3270j.protocol")
)
.build();
Launcher launcher = LauncherFactory.create();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
TestExecutionSummary summary = listener.getSummary();
summary.printTo(new PrintWriter(System.out));
summary.printFailuresTo(new PrintWriter(System.err));
if (summary.getTotalFailureCount() > 0) {
System.exit(1);
}
}
}
EOF
"$JAVAC_BIN" -cp "$FULL_CP:$TEST_BUILD_DIR" -d "$BUILD_DIR" "$BUILD_DIR/TestRunner.java"
JAVA_BIN_EXEC="${JAVA_HOME:+$JAVA_HOME/bin/java}"
JAVA_BIN_EXEC="${JAVA_BIN_EXEC:-java}"
"$JAVA_BIN_EXEC" -cp "$FULL_CP:$TEST_BUILD_DIR:$BUILD_DIR" TestRunner
fi
fi
Vendored
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
exec "$(dirname "$0")/build_all.sh" exec "$(dirname "$0")/build_all.sh" "$@"
@@ -501,9 +501,9 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
consoleHandler.setFilter(appFilter); consoleHandler.setFilter(appFilter);
globalRoot.addHandler(consoleHandler); globalRoot.addHandler(consoleHandler);
Logger.getLogger("haus.nightmare").setLevel(logLevel); Logger.getLogger("haus.nightmare").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.j3270").setLevel(logLevel); Logger.getLogger("haus.nightmare.j3270").setLevel(Level.ALL);
Logger.getLogger("haus.nightmare.lib3270j").setLevel(logLevel); Logger.getLogger("haus.nightmare.lib3270j").setLevel(Level.ALL);
try { try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) { java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false) {
@@ -59,6 +59,7 @@ public class DataStreamProcessor {
this.outputBuffer = new byte[32768]; this.outputBuffer = new byte[32768];
this.outputPos = 0; this.outputPos = 0;
this.gocaDecoder.setProgramSymbolManager(programSymbolManager); this.gocaDecoder.setProgramSymbolManager(programSymbolManager);
this.graphicsPlane.setProgramSymbolManager(programSymbolManager);
} }
public QueryReplyBuilder getQueryReplyBuilder() { public QueryReplyBuilder getQueryReplyBuilder() {
@@ -838,6 +839,13 @@ public class DataStreamProcessor {
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4); int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos)); int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < Math.min(32, fieldLen); i++) {
hexDump.append(String.format("%02x ", data[pos + i] & 0xFF));
}
log.info(String.format("SF 0x0F sub=0x%02x len=%d flags=0x%02x orderOffset=+%d orderLen=%d bytes=[%s]",
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
if (flags == 0x80) { // SPAN_FIRST if (flags == 0x80) { // SPAN_FIRST
@@ -856,6 +864,7 @@ public class DataStreamProcessor {
} }
byte[] fullStream = gocaAccumulator.toByteArray(); byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset(); gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { if (currentGocaSubtype == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length); gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} else { } else {
@@ -864,6 +873,7 @@ public class DataStreamProcessor {
notifyScreenUpdated(); notifyScreenUpdated();
} else { // SPAN_ONLY (0xC0) } else { // SPAN_ONLY (0xC0)
gocaAccumulator.reset(); gocaAccumulator.reset();
log.info(String.format("GOCA stream SPAN_ONLY: %d bytes", orderLen));
if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) { if (sfSubId == haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB) {
gocaDecoder.processProcedureOrders(data, orderOffset, orderLen); gocaDecoder.processProcedureOrders(data, orderOffset, orderLen);
} else { } else {
@@ -20,7 +20,7 @@ public class GocaDecoder {
private int curX = 0; private int curX = 0;
private int curY = 0; private int curY = 0;
private int curColor = GocaConstants.GOCA_COLORS[0]; private int curColor = GocaConstants.GOCA_COLORS[0];
private int bgMix = 2; // BMX_OVERPAINT default private int bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black
private int lineType = GocaConstants.LT_SOLID; private int lineType = GocaConstants.LT_SOLID;
private int lineWidth = GocaConstants.LW_NORMAL; private int lineWidth = GocaConstants.LW_NORMAL;
@@ -28,6 +28,7 @@ public class GocaDecoder {
private int markerSize = 5; private int markerSize = 5;
private int markerColor = GocaConstants.GOCA_COLORS[0]; private int markerColor = GocaConstants.GOCA_COLORS[0];
private int pattern = GocaConstants.PT_SOLID; private int pattern = GocaConstants.PT_SOLID;
private int patternSet = 0;
private int fillColor = GocaConstants.GOCA_COLORS[0]; private int fillColor = GocaConstants.GOCA_COLORS[0];
private int charDir = GocaConstants.CD_LR; private int charDir = GocaConstants.CD_LR;
private double charAngle = 0.0; private double charAngle = 0.0;
@@ -99,7 +100,7 @@ public class GocaDecoder {
private final java.util.List<SegmentBounds> activeSegmentsInOrder = new java.util.ArrayList<>(); private final java.util.List<SegmentBounds> activeSegmentsInOrder = new java.util.ArrayList<>();
private int currentSegId = 0; private int currentSegId = 0;
// Graphic Cursor (Light-Pen) state // Graphics Cursor state
private boolean graphicsCursorActive = false; private boolean graphicsCursorActive = false;
private int graphicCursorX = 0; private int graphicCursorX = 0;
private int graphicCursorY = 0; private int graphicCursorY = 0;
@@ -110,6 +111,9 @@ public class GocaDecoder {
public void setProgramSymbolManager(ProgramSymbolManager psm) { public void setProgramSymbolManager(ProgramSymbolManager psm) {
this.programSymbolManager = psm; this.programSymbolManager = psm;
if (this.plane != null) {
this.plane.setProgramSymbolManager(psm);
}
} }
public GraphicsPlane getGraphicsPlane() { public GraphicsPlane getGraphicsPlane() {
@@ -203,7 +207,7 @@ public class GocaDecoder {
public synchronized void resetAttributes() { public synchronized void resetAttributes() {
curColor = getColor(0); curColor = getColor(0);
bgMix = 2; // BMX_OVERPAINT bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec)
bgColor = GocaConstants.GOCA_COLORS[8]; // Black bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
@@ -211,6 +215,7 @@ public class GocaDecoder {
markerSize = 5; markerSize = 5;
markerColor = curColor; markerColor = curColor;
pattern = GocaConstants.PT_SOLID; pattern = GocaConstants.PT_SOLID;
patternSet = 0;
fillColor = curColor; fillColor = curColor;
charDir = GocaConstants.CD_LR; charDir = GocaConstants.CD_LR;
charAngle = 0.0; charAngle = 0.0;
@@ -232,30 +237,26 @@ public class GocaDecoder {
* *
* IMPORTANT ARCHITECTURE & PARSING SAFETY NOTE: * IMPORTANT ARCHITECTURE & PARSING SAFETY NOTE:
* GOCA orders follow IBM GA23-0059 and Host On-Demand (HOD) architecture rules: * GOCA orders follow IBM GA23-0059 and Host On-Demand (HOD) architecture rules:
* 1. 1-byte standalone orders (NOP, ERASE, etc.) -> length 1. * 1. 1-byte standalone orders (NOP, ERASE, GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> length 1.
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG, GPOP) -> 1 or 2 bytes (with 0x00 trailing byte). * 2. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
* 3. Fixed 1-byte operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes. * 3. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39,
* 4. Flexible 1-byte attribute orders (GBAR 0x68, GSCS 0x38, GSCD 0x3A, GSCC 0x3B, GSMP 0x39,
* GSPT 0x28, GSMT 0x29, GSMS_SET 0x3C) can be transmitted either as: * GSPT 0x28, GSMT 0x29, GSMS_SET 0x3C) can be transmitted either as:
* - Short 2-byte form: [opcode] [value] (e.g. GBAR with flags 0x80 -> '68 80') * - Short 2-byte form: [opcode] [value] (e.g. GBAR with flags 0x80 -> '68 80')
* - Self-defining 3-byte form: [opcode] [length=0x01] [value] (e.g. '68 01 80') * - Self-defining 3-byte form: [opcode] [length=0x01] [value] (e.g. '68 01 80')
* CRITICAL: NEVER allow 1-byte attribute orders (like GBAR 0x68 or GSCC 0x3B) to fall through to the * CRITICAL: NEVER allow 1-byte attribute orders (like GBAR 0x68 or GSCC 0x3B) to fall through to the
* variable-length formula `(data[idx + 1] & 0xFF) + 2`. If GBAR flags (e.g. 0x80 for boundary) are read as * variable-length formula `(data[idx + 1] & 0xFF) + 2`. If GBAR flags (e.g. 0x80 for boundary) are read as
* a length field, the decoder will skip 130 bytes, corrupting and skipping all subsequent drawing orders! * a length field, the decoder will skip 130 bytes, corrupting and skipping all subsequent drawing orders!
* 5. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.) * 4. Self-defining orders with multi-byte payloads (GLINE 0xC1, GARC 0xC6, GCHST 0xC3, GRLINE 0xE1, etc.)
* have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2. * have a 1-byte length byte at data[idx + 1], making total length = payloadLen + 2.
*/ */
private int getOrderLength(byte[] data, int idx, int end) { private int getOrderLength(byte[] data, int idx, int end) {
int order = data[idx] & 0xFF; int order = data[idx] & 0xFF;
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00) { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 ||
order == GocaConstants.G_GEAR || order == GocaConstants.G_ENDSEGM ||
order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_GEIMG ||
order == GocaConstants.G_GPOP || order == GocaConstants.G_GERASE) {
return 1; return 1;
} }
if (order == GocaConstants.G_GEAR ||
order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_ENDPROLOGUE ||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP ||
order == GocaConstants.G_GERASE) {
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
}
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
@@ -399,8 +400,11 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
int segId = 0;
int flag0 = 0;
int flag1 = 0;
if (idx + 5 < end) { if (idx + 5 < end) {
int segId = ((inputData[idx + 2] & 0xFF) << 24) | segId = ((inputData[idx + 2] & 0xFF) << 24) |
((inputData[idx + 3] & 0xFF) << 16) | ((inputData[idx + 3] & 0xFF) << 16) |
((inputData[idx + 4] & 0xFF) << 8) | ((inputData[idx + 4] & 0xFF) << 8) |
(inputData[idx + 5] & 0xFF); (inputData[idx + 5] & 0xFF);
@@ -409,13 +413,17 @@ public class GocaDecoder {
activeSegmentsInOrder.remove(sb); activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb); activeSegmentsInOrder.add(sb);
} }
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) { if (idx + 6 < end) flag0 = inputData[idx + 6] & 0xFF;
if (idx + 7 < end) flag1 = inputData[idx + 7] & 0xFF;
logger.info(String.format("GOCA BEGSEGM: segId=%d flag0=0x%02x flag1=0x%02x callDepth=%d", segId, flag0, flag1, callDepth));
if (idx + 7 < end && (flag1 & 0x06) == 0) {
resetAttributes(); resetAttributes();
} }
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_ENDSEGM: { // End Segment (0x71) case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
logger.info(String.format("GOCA ENDSEGM: segId=%d", currentSegId));
if (currentSegId != 0) { if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId); SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) { if (sb != null) {
@@ -486,6 +494,7 @@ public class GocaDecoder {
} else { } else {
targetSegId = -1; targetSegId = -1;
} }
logger.info(String.format("GOCA GCALL: targetSegId=%d callDepth=%d", targetSegId, callDepth));
if (targetSegId != -1) { if (targetSegId != -1) {
byte[] targetSeg = segmentStore.get(targetSegId); byte[] targetSeg = segmentStore.get(targetSegId);
if (targetSeg != null) { if (targetSeg != null) {
@@ -498,6 +507,8 @@ public class GocaDecoder {
decodeStreamDirect(targetSeg, 0, targetSeg.length); decodeStreamDirect(targetSeg, 0, targetSeg.length);
callDepth--; callDepth--;
currentSegId = savedSegId; currentSegId = savedSegId;
} else {
logger.warning("GOCA GCALL: target segment not found in store: " + targetSegId);
} }
} }
} }
@@ -530,7 +541,9 @@ public class GocaDecoder {
case GocaConstants.G_GSCOL: { // Set Color (0x0A) case GocaConstants.G_GSCOL: { // Set Color (0x0A)
int colIdx = inputData[idx + 1] & 0xFF; int colIdx = inputData[idx + 1] & 0xFF;
curColor = getColor(colIdx); curColor = getColor(colIdx);
if (!inArea) {
fillColor = curColor; fillColor = curColor;
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -538,8 +551,10 @@ public class GocaDecoder {
if (payloadLen >= 2 && idx + 3 < end) { if (payloadLen >= 2 && idx + 3 < end) {
int colIdx = inputData[idx + 3] & 0xFF; int colIdx = inputData[idx + 3] & 0xFF;
curColor = getColor(colIdx); curColor = getColor(colIdx);
if (!inArea) {
fillColor = curColor; fillColor = curColor;
} }
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -570,12 +585,14 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSPS: { // Set Pattern Set (0x08) case GocaConstants.G_GSPS: { // Set Pattern Set (0x08)
pattern = inputData[idx + 1] & 0xFF; patternSet = inputData[idx + 1] & 0xFF;
logger.info("GOCA GSPS: patternSet=" + patternSet);
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28) case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
logger.info("GOCA GSPT: pattern=" + pattern);
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -605,12 +622,14 @@ public class GocaDecoder {
} }
case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D) case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D)
bgMix = inputData[idx + 1] & 0xFF; bgMix = inputData[idx + 1] & 0xFF;
logger.info("GOCA GSBMX: bgMix=" + bgMix);
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF); int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0 || (flags & 0x40) != 0 || (flags == 0); boolean drawBoundary = (flags & 0x80) != 0;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b", flags, drawBoundary));
beginArea(drawBoundary); beginArea(drawBoundary);
idx += orderLen; idx += orderLen;
break; break;
@@ -837,6 +856,8 @@ public class GocaDecoder {
this.areaPointsY.clear(); this.areaPointsY.clear();
this.areaPolygons.clear(); this.areaPolygons.clear();
this.currentPolyPts = 0; this.currentPolyPts = 0;
logger.info(String.format("GOCA beginArea: drawBoundary=%b fillColor=0x%08x patternSet=%d pattern=%d",
drawBoundary, fillColor, patternSet, pattern));
} }
private void endArea() { private void endArea() {
@@ -846,6 +867,7 @@ public class GocaDecoder {
currentPolyPts = 0; currentPolyPts = 0;
} }
if (areaPointsX.size() < 3 || areaPolygons.isEmpty()) { if (areaPointsX.size() < 3 || areaPolygons.isEmpty()) {
logger.info(String.format("GOCA endArea: dropped (pts=%d, polys=%d)", areaPointsX.size(), areaPolygons.size()));
inArea = false; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -867,7 +889,10 @@ public class GocaDecoder {
polyCounts[i] = areaPolygons.get(i); polyCounts[i] = areaPolygons.get(i);
} }
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, logger.info(String.format("GOCA endArea: fillArea pts=%d polys=%d fillColor=0x%08x patternSet=%d pattern=%d drawBoundary=%b boundaryColor=0x%08x bgMix=%d",
n, numPolys, fillColor, patternSet, pattern, areaDrawBoundary, curColor, bgMix));
plane.fillArea(px, py, n, polyCounts, numPolys, fillColor, patternSet,
pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor); pattern, areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor);
inArea = false; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
@@ -1108,6 +1133,22 @@ public class GocaDecoder {
trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2)); trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2));
trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2)); trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2));
if (inArea) {
int steps = Math.max(8, (int) Math.round(Math.abs(sweepAngleDeg) / 15.0));
double startRad = Math.toRadians(startAngleDeg);
double sweepRad = Math.toRadians(sweepAngleDeg);
for (int i = 0; i <= steps; i++) {
double a = startRad + (sweepRad * i) / steps;
int ax = centerX + (int) Math.round(Math.cos(a) * semiAxis1);
int ay = centerY + (int) Math.round(Math.sin(a) * semiAxis2);
if (i == 0 && currentPolyPts == 0) {
addAreaLineStart(ax, ay);
} else {
addAreaPoint(ax, ay);
}
}
}
plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg, plane.drawArc(cx, cy, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull); curColor, lineType, lineWidth, isFull);
@@ -1125,6 +1166,13 @@ public class GocaDecoder {
if (fromCurPos) { if (fromCurPos) {
ptsX.add(plane.mapXDouble(curX)); ptsX.add(plane.mapXDouble(curX));
ptsY.add(plane.mapYDouble(curY)); ptsY.add(plane.mapYDouble(curY));
if (inArea) {
if (currentPolyPts == 0) {
addAreaLineStart(curX, curY);
} else {
addAreaPoint(curX, curY);
}
}
} }
while (pos + 4 <= end) { while (pos + 4 <= end) {
@@ -1132,6 +1180,13 @@ public class GocaDecoder {
int y = readCoord(data, pos + 2); int y = readCoord(data, pos + 2);
ptsX.add(plane.mapXDouble(x)); ptsX.add(plane.mapXDouble(x));
ptsY.add(plane.mapYDouble(y)); ptsY.add(plane.mapYDouble(y));
if (inArea) {
if (ptsX.size() == 1 && currentPolyPts == 0) {
addAreaLineStart(x, y);
} else {
addAreaPoint(x, y);
}
}
curX = x; curX = x;
curY = y; curY = y;
pos += 4; pos += 4;
@@ -44,6 +44,15 @@ public class GraphicsPlane {
private int screenCols = 80; private int screenCols = 80;
private int screenRows = 24; private int screenRows = 24;
private ProgramSymbolManager programSymbolManager;
public void setProgramSymbolManager(ProgramSymbolManager psm) {
this.programSymbolManager = psm;
}
public ProgramSymbolManager getProgramSymbolManager() {
return programSymbolManager;
}
public GraphicsPlane(int width, int height) { public GraphicsPlane(int width, int height) {
this.canvasWidth = Math.max(1, width); this.canvasWidth = Math.max(1, width);
@@ -338,7 +347,7 @@ public class GraphicsPlane {
} }
private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) { private void drawStyledLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1; int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
int dx = Math.abs(x2 - x1); int dx = Math.abs(x2 - x1);
@@ -517,12 +526,23 @@ public class GraphicsPlane {
int fillColorArgb, int pattern, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) { int bgMix, int bgColorArgb) {
fillArea(px, py, numPoints, polyCounts, numPolys, fillColorArgb, 0, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, bgMix, bgColorArgb);
}
/**
* Fills an area with symbol-set-aware and pattern-symbol-aware rasterization.
*/
public synchronized void fillArea(int[] px, int[] py, int numPoints, int[] polyCounts, int numPolys,
int fillColorArgb, int patternSet, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth,
int bgMix, int bgColorArgb) {
if (px == null || py == null || numPoints < 3) return; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) { boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
// Find polygon vertical bounds across all points // Find polygon vertical bounds across all points
int minY = py[0]; int minY = py[0];
int maxY = py[0]; int maxY = py[0];
@@ -534,7 +554,18 @@ public class GraphicsPlane {
maxY = Math.min(canvasHeight - 1, maxY); maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>(); List<Integer> nodeX = new ArrayList<>();
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0]; byte[] patRows = null;
ProgramSymbolSet.SymbolSlot psSlot = null;
if (patternSet >= 0x40 && programSymbolManager != null) {
psSlot = programSymbolManager.getSymbol(patternSet, pattern);
}
if (psSlot == null) {
if (pattern >= 0 && pattern < PATTERN_DATA.length) {
patRows = PATTERN_DATA[pattern];
} else {
patRows = PATTERN_DATA[0];
}
}
for (int y = minY; y <= maxY; y++) { for (int y = minY; y <= maxY; y++) {
nodeX.clear(); nodeX.clear();
@@ -567,13 +598,26 @@ public class GraphicsPlane {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) { for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern == 0 || pattern >= 16) { 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) {
setPixel(x, y, fill);
} else if (bgMix == GocaConstants.MIX_OVER) {
setPixel(x, y, bg);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else { } else {
int b = patRows[y & 7] & 0xFF; int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) { if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background) } else if (bgMix == GocaConstants.MIX_OVER) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg); setPixel(x, y, bg);
} }
} }
@@ -610,7 +654,7 @@ public class GraphicsPlane {
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision. * Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle) with sub-pixel precision.
*/ */
public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) { public synchronized void drawMarker(double x, double y, int markerType, int size, int colorArgb) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
double s = Math.max(3.0, size > 0 ? (double) size : 5.0); double s = Math.max(3.0, size > 0 ? (double) size : 5.0);
switch (markerType) { switch (markerType) {
@@ -731,7 +775,7 @@ public class GraphicsPlane {
public synchronized void drawVectorText(double x, double y, String text, int colorArgb, public synchronized void drawVectorText(double x, double y, String text, int colorArgb,
double cellWidth, double cellHeight, int dir, double angle) { double cellWidth, double cellHeight, int dir, double angle) {
if (text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF; int color = (colorArgb != 0) ? colorArgb : GocaConstants.GOCA_COLORS[0];
double curX = x; double curX = x;
double curY = y; double curY = y;
@@ -822,7 +866,7 @@ public class GraphicsPlane {
*/ */
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF; int fgColor = (fgColorArgb != 0) ? fgColorArgb : GocaConstants.GOCA_COLORS[0];
int bytesPerRow = (width + 7) / 8; int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { for (int row = 0; row < height; row++) {
@@ -520,6 +520,8 @@ public class GocaDecoderTest {
// Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20) // Polygon 2 (e.g. Letter 'T' stem): (18,20) to (22,20) to (22,40) to (18,40) to (18,20)
// GEAR (0x60) // GEAR (0x60)
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSPT);
out.write(GocaConstants.PT_SOLID);
out.write(GocaConstants.G_GBAR); out.write(GocaConstants.G_GBAR);
out.write(0x80); out.write(0x80);
@@ -555,4 +557,240 @@ public class GocaDecoderTest {
} }
assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels"); assertTrue(nonZero > 50, "Expected both filled subpath polygons to render filled pixels");
} }
@Test
public void testPatternSetAndPatternSymbolSeparation() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Color to Blue (Color 1)
out.write(GocaConstants.G_GSCOL);
out.write(0x01); // Blue
// Set Pattern Set to 0 (default standard patterns)
out.write(GocaConstants.G_GSPS);
out.write(0x00);
// Set Pattern Symbol to 5 (D5 50% checkerboard)
out.write(GocaConstants.G_GSPT);
out.write(0x05);
// Begin Area with boundary (0x80)
out.write(GocaConstants.G_GBAR);
out.write(0x80);
// Draw rectangle (0,0) to (50,0) to (50,50) to (0,50) to (0,0)
out.write(GocaConstants.G_GLINE);
out.write(0x14); // 5 points * 4 bytes = 20 bytes
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
out.write(0x00); out.write(50); out.write(0x00); out.write(0);
out.write(0x00); out.write(50); out.write(0x00); out.write(50);
out.write(0x00); out.write(0); out.write(0x00); out.write(50);
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
// End Area
out.write(GocaConstants.G_GEAR);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
// Verify that D5 checkerboard pattern is NOT solid fill (it should have transparent gaps)
int[] buffer = plane.getRgbBuffer();
int bluePixels = 0;
int zeroPixels = 0;
int blueArgb = GocaConstants.GOCA_COLORS[1];
for (int p : buffer) {
if (p == blueArgb) bluePixels++;
else if (p == 0) zeroPixels++;
}
assertTrue(bluePixels > 0, "Expected blue pixels in patterned area");
assertTrue(zeroPixels > 0, "Expected transparent zero pixels in patterned area due to D5 checkerboard gaps");
}
@Test
public void testEmptyPatternAreaDrawsOnlyBoundary() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Set Color to Red (Color 2)
out.write(GocaConstants.G_GSCOL);
out.write(0x02);
// Set Pattern Symbol to PT_EMPTY (15)
out.write(GocaConstants.G_GSPT);
out.write(GocaConstants.PT_EMPTY);
// Begin Area with boundary (0x80)
out.write(GocaConstants.G_GBAR);
out.write(0x80);
// Draw rectangle
out.write(GocaConstants.G_GLINE);
out.write(0x14);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(40); out.write(0x00); out.write(10);
out.write(0x00); out.write(40); out.write(0x00); out.write(40);
out.write(0x00); out.write(10); out.write(0x00); out.write(40);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
// End Area
out.write(GocaConstants.G_GEAR);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
// Verify that center of the box is transparent (not filled)
int centerX = plane.mapX(25);
int centerY = plane.mapY(25);
int centerPixel = plane.getRgbBuffer()[centerY * plane.getCanvasWidth() + centerX];
assertEquals(0, centerPixel, "Expected interior of PT_EMPTY area to remain transparent");
}
@Test
public void testGearFollowedByNopOrder() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// Area with PT_EMPTY
out.write(GocaConstants.G_GSPT);
out.write(GocaConstants.PT_EMPTY);
out.write(GocaConstants.G_GBAR);
out.write(0x80);
out.write(GocaConstants.G_GLINE);
out.write(0x10);
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
out.write(0x00); out.write(10); out.write(0x00); out.write(0);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(0); out.write(0x00); out.write(0);
out.write(GocaConstants.G_GEAR); // 0x60
out.write(GocaConstants.G_NOP1); // 0x00 NOP - must not be consumed as part of GEAR!
// Followed by a line
out.write(GocaConstants.G_GSCOL);
out.write(0x03); // Pink
out.write(GocaConstants.G_GLINE);
out.write(0x08);
out.write(0x00); out.write(20); out.write(0x00); out.write(20);
out.write(0x00); out.write(40); out.write(0x00); out.write(40);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
int pinkArgb = GocaConstants.GOCA_COLORS[3];
boolean foundPink = false;
for (int p : plane.getRgbBuffer()) {
if (p == pinkArgb) {
foundPink = true;
break;
}
}
assertTrue(foundPink, "Expected pink line after GEAR + NOP to be drawn");
}
@Test
public void testAdmopsSlidePreviewSequence() {
GraphicsPlane plane = new GraphicsPlane(200, 200);
GocaDecoder decoder = new GocaDecoder(plane);
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 1. Draw Blue stippled slide background (Pattern 6, Blue color 1)
out.write(GocaConstants.G_GSCOL); out.write(0x01); // Blue
out.write(GocaConstants.G_GSPT); out.write(0x06); // Pattern 6
out.write(GocaConstants.G_GBAR); out.write(0x80);
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(90);
out.write(0x00); out.write(10); out.write(0x00); out.write(90);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(GocaConstants.G_GEAR);
// 2. Draw Red banner at top (Solid Pattern 16, Red color 2)
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red
out.write(GocaConstants.G_GSPT); out.write(0x10); // Solid
out.write(GocaConstants.G_GBAR); out.write(0x80);
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(30);
out.write(0x00); out.write(10); out.write(0x00); out.write(30);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(GocaConstants.G_GEAR);
// 3. Draw Yellow bullet text (Color 6, default modal solid pattern)
out.write(GocaConstants.G_GSCOL); out.write(0x06); // Yellow
out.write(GocaConstants.G_GBAR); out.write(0x80);
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
out.write(0x00); out.write(80); out.write(0x00); out.write(40);
out.write(0x00); out.write(80); out.write(0x00); out.write(50);
out.write(0x00); out.write(20); out.write(0x00); out.write(50);
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
out.write(GocaConstants.G_GEAR);
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSCOL White inside GBAR)
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
out.write(GocaConstants.G_GBAR); out.write(0x80);
out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color
out.write(GocaConstants.G_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(10);
out.write(0x00); out.write(90); out.write(0x00); out.write(90);
out.write(0x00); out.write(10); out.write(0x00); out.write(90);
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(GocaConstants.G_GEAR);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent());
// Verify that the red banner (e.g. at (50, 20)) was NOT overwritten by black/white!
int bannerX = plane.mapX(50);
int bannerY = plane.mapY(20);
int bannerPixel = plane.getRgbBuffer()[bannerY * plane.getCanvasWidth() + bannerX];
int redArgb = GocaConstants.GOCA_COLORS[2];
assertEquals(redArgb, bannerPixel, "Red header banner must be solid red and preserved");
// Verify that the yellow text (e.g. at (50, 45)) was NOT overwritten!
int textX = plane.mapX(50);
int textY = plane.mapY(45);
int textPixel = plane.getRgbBuffer()[textY * plane.getCanvasWidth() + textX];
int yellowArgb = GocaConstants.GOCA_COLORS[6];
assertEquals(yellowArgb, textPixel, "Yellow bullet text must be solid yellow and preserved");
// Verify that the blue stipple (in body region (20..80, 60..80)) contains blue pixels and was NOT overwritten by white!
int blueArgb = GocaConstants.GOCA_COLORS[1];
boolean foundBlue = false;
int yMin = Math.min(plane.mapY(60), plane.mapY(80));
int yMax = Math.max(plane.mapY(60), plane.mapY(80));
int xMin = Math.min(plane.mapX(20), plane.mapX(80));
int xMax = Math.max(plane.mapX(20), plane.mapX(80));
for (int y = yMin; y <= yMax; y++) {
for (int x = xMin; x <= xMax; x++) {
int pix = plane.getRgbBuffer()[y * plane.getCanvasWidth() + x];
if (pix == blueArgb) {
foundBlue = true;
break;
}
}
if (foundBlue) break;
}
assertTrue(foundBlue, "Slide body must contain stippled blue pixels");
// Verify that the boundary (e.g. at (10, 50)) is drawn in White!
int whiteArgb = GocaConstants.GOCA_COLORS[7];
int borderX = plane.mapX(10);
int borderY = plane.mapY(50);
int borderPixel = plane.getRgbBuffer()[borderY * plane.getCanvasWidth() + borderX];
assertEquals(whiteArgb, borderPixel, "Slide border outline must be drawn in White");
}
} }