GOCA fixing
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m22s

This commit is contained in:
2026-09-04 14:34:01 -04:00
parent 6296cf4341
commit cbb310b889
12 changed files with 943 additions and 238 deletions
+1
View File
@@ -1,4 +1,5 @@
j3270.log.* j3270.log.*
*.bin
*.txt *.txt
*.py *.py
*.pcap *.pcap
@@ -37,7 +37,7 @@ public class StatusBar extends JPanel {
connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim()); connectionStatus = createLabel("Not Connected", oiaFont, ThemeManager.getOiaFgDim());
tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); tlsStatus = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal()); luName = createLabel("", oiaFont, ThemeManager.getOiaFgNormal());
lockStatus = createLabel("", oiaFont, ThemeManager.getOiaFgAlert()); lockStatus = createLabel("", oiaFont, ThemeManager.getOiaInputInhibited());
fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); fieldTypeStatus = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); codePageInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim()); modelInfo = createLabel("", oiaFont, ThemeManager.getOiaFgDim());
@@ -89,10 +89,12 @@ public class StatusBar extends JPanel {
tlsStatus.setText(""); tlsStatus.setText("");
luName.setText(""); luName.setText("");
lockStatus.setText(""); lockStatus.setText("");
lockStatus.setForeground(ThemeManager.getOiaInputInhibited(theme));
fieldTypeStatus.setText(""); fieldTypeStatus.setText("");
codePageInfo.setText(""); codePageInfo.setText("");
modelInfo.setText(""); modelInfo.setText("");
cursorPosition.setText("001/001"); cursorPosition.setText("001/001");
cursorPosition.setForeground(ThemeManager.getOiaFgNormal(theme));
return; return;
} }
@@ -110,25 +112,25 @@ public class StatusBar extends JPanel {
break; break;
case CONNECTED_3270: case CONNECTED_3270:
connectionStatus.setText("TN3270"); connectionStatus.setText("TN3270");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_TN3270E: case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E"); connectionStatus.setText("TN3270E");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_SSCP: case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU"); connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_NVT: case CONNECTED_NVT:
case CONNECTED_NVT_CHAR: case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT: case CONNECTED_E_NVT:
connectionStatus.setText("NVT"); connectionStatus.setText("NVT");
connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); connectionStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
break; break;
case CONNECTED_UNBOUND: case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound"); connectionStatus.setText("Unbound");
connectionStatus.setForeground(ThemeManager.getOiaAttention()); connectionStatus.setForeground(ThemeManager.getOiaAttention(theme));
break; break;
default: default:
connectionStatus.setText(state.name()); connectionStatus.setText(state.name());
@@ -144,11 +146,11 @@ public class StatusBar extends JPanel {
String protocol = session != null ? session.getProtocol() : "TLS"; String protocol = session != null ? session.getProtocol() : "TLS";
if (verified) { if (verified) {
tlsStatus.setText("🔒 TLS"); tlsStatus.setText("🔒 TLS");
tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); tlsStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verified)");
} else { } else {
tlsStatus.setText("🔓 TLS (Unverified)"); tlsStatus.setText("🔓 TLS (Unverified)");
tlsStatus.setForeground(ThemeManager.getOiaAttention()); tlsStatus.setForeground(ThemeManager.getOiaAttention(theme));
tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)"); tlsStatus.setToolTipText(protocol + " / " + cipher + " (Verification Bypassed)");
} }
} else { } else {
@@ -164,46 +166,46 @@ public class StatusBar extends JPanel {
lu = "LU:" + client.getConfig().getLuName(); lu = "LU:" + client.getConfig().getLuName();
} }
luName.setText(lu); luName.setText(lu);
luName.setForeground(ThemeManager.getOiaStatusSysAvail()); luName.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
// Lock / Inhibit status // Lock / Inhibit status
int inhibit = client.getOIA().getInputInhibited(); int inhibit = client.getOIA().getInputInhibited();
if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) { if (inhibit != ECLConstants.INHIBIT_NOT_INHIBITED) {
Color lockFg = ThemeManager.getOiaInputInhibited(); // White (oII) Color lockFg = ThemeManager.getOiaInputInhibited(theme);
switch (inhibit) { switch (inhibit) {
case ECLConstants.INHIBIT_SYSTEM_LOCK: case ECLConstants.INHIBIT_SYSTEM_LOCK:
lockStatus.setText("X SYSTEM"); lockStatus.setText("X SYSTEM");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII) lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
case ECLConstants.INHIBIT_COMM_CHECK: case ECLConstants.INHIBIT_COMM_CHECK:
lockStatus.setText("X COMM"); lockStatus.setText("X COMM");
lockFg = ThemeManager.getOiaCommCheck(); // Red (oEI) lockFg = ThemeManager.getOiaCommCheck(theme);
break; break;
case ECLConstants.INHIBIT_NUMERIC_ONLY: case ECLConstants.INHIBIT_NUMERIC_ONLY:
lockStatus.setText("X NUM"); lockStatus.setText("X NUM");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI) lockFg = ThemeManager.getOiaAttention(theme);
break; break;
case ECLConstants.INHIBIT_PROTECTED_FIELD: case ECLConstants.INHIBIT_PROTECTED_FIELD:
lockStatus.setText("X PROT"); lockStatus.setText("X PROT");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII) lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
case ECLConstants.INHIBIT_OVERFLOW: case ECLConstants.INHIBIT_OVERFLOW:
lockStatus.setText("X >"); lockStatus.setText("X >");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI) lockFg = ThemeManager.getOiaAttention(theme);
break; break;
case ECLConstants.INHIBIT_OPERATOR_DUE: case ECLConstants.INHIBIT_OPERATOR_DUE:
lockStatus.setText("X OP"); lockStatus.setText("X OP");
lockFg = ThemeManager.getOiaAttention(); // Yellow (oAI) lockFg = ThemeManager.getOiaAttention(theme);
break; break;
default: default:
lockStatus.setText("X LOCKED"); lockStatus.setText("X LOCKED");
lockFg = ThemeManager.getOiaInputInhibited(); // White (oII) lockFg = ThemeManager.getOiaInputInhibited(theme);
break; break;
} }
lockStatus.setForeground(lockFg); lockStatus.setForeground(lockFg);
} else if (client.getInputProcessor().isInsertMode()) { } else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT"); lockStatus.setText("INSERT");
lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail()); lockStatus.setForeground(ThemeManager.getOiaStatusSysAvail(theme));
} else { } else {
lockStatus.setText(""); lockStatus.setText("");
} }
@@ -251,11 +251,30 @@ public final class ThemeManager {
public static final Color HOD_OIA_COMM_CHECK_ERROR = new Color(255, 0, 0); // oEI: Red public static final Color HOD_OIA_COMM_CHECK_ERROR = new Color(255, 0, 0); // oEI: Red
public static final Color HOD_OIA_BG_BLACK = new Color(0, 0, 0); // oOB: Black public static final Color HOD_OIA_BG_BLACK = new Color(0, 0, 0); // oOB: Black
public static Color getOiaStatusSysAvail() { return HOD_OIA_STATUS_SYS_AVAIL; } public static Color getOiaStatusSysAvail() { return getOiaStatusSysAvail(currentTheme); }
public static Color getOiaInputInhibited() { return HOD_OIA_INPUT_INHIBITED; } public static Color getOiaStatusSysAvail(UITheme t) {
public static Color getOiaAttention() { return HOD_OIA_ATTENTION_WARN; } return t == UITheme.DARK ? HOD_OIA_STATUS_SYS_AVAIL : new Color(0, 90, 200);
public static Color getOiaCommCheck() { return HOD_OIA_COMM_CHECK_ERROR; } }
public static Color getOiaBackground() { return HOD_OIA_BG_BLACK; }
public static Color getOiaInputInhibited() { return getOiaInputInhibited(currentTheme); }
public static Color getOiaInputInhibited(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_INPUT_INHIBITED : new Color(20, 20, 20);
}
public static Color getOiaAttention() { return getOiaAttention(currentTheme); }
public static Color getOiaAttention(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_ATTENTION_WARN : new Color(180, 100, 0);
}
public static Color getOiaCommCheck() { return getOiaCommCheck(currentTheme); }
public static Color getOiaCommCheck(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_COMM_CHECK_ERROR : new Color(190, 20, 20);
}
public static Color getOiaBackground() { return getOiaBackground(currentTheme); }
public static Color getOiaBackground(UITheme t) {
return t == UITheme.DARK ? HOD_OIA_BG_BLACK : getStatusBarBg(t);
}
// ========================================================================= // =========================================================================
// Button Variants & Color helpers // Button Variants & Color helpers
@@ -647,6 +666,16 @@ public final class ThemeManager {
return; return;
} }
if (comp instanceof StatusBar) {
((StatusBar) comp).applyTheme(theme);
return;
}
if (comp instanceof TerminalPanel) {
comp.repaint();
return;
}
if (comp instanceof JMenuBar) { if (comp instanceof JMenuBar) {
styleMenuBar((JMenuBar) comp); styleMenuBar((JMenuBar) comp);
for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) { for (int i = 0; i < ((JMenuBar) comp).getMenuCount(); i++) {
@@ -37,6 +37,7 @@ public class Phase1UiOverlayTest {
@Test @Test
@DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification") @DisplayName("Item 1.5: ThemeManager HoD OIA category colors match specification")
public void testThemeManagerOiaColors() { public void testThemeManagerOiaColors() {
ThemeManager.setTheme(UITheme.DARK);
// oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240) // oSI: Status / System Available -> CUSTOMBLUE (120, 144, 240)
assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail()); assertEquals(new Color(120, 144, 240), ThemeManager.getOiaStatusSysAvail());
@@ -51,6 +52,15 @@ public class Phase1UiOverlayTest {
// oOB: OIA Separator / Background -> Black (0, 0, 0) // oOB: OIA Separator / Background -> Black (0, 0, 0)
assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground()); assertEquals(new Color(0, 0, 0), ThemeManager.getOiaBackground());
// Light mode OIA colors should be legible and not white on grey
ThemeManager.setTheme(UITheme.LIGHT);
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertEquals(new Color(20, 20, 20), ThemeManager.getOiaInputInhibited());
assertEquals(new Color(0, 90, 200), ThemeManager.getOiaStatusSysAvail());
assertEquals(new Color(180, 100, 0), ThemeManager.getOiaAttention());
assertEquals(new Color(190, 20, 20), ThemeManager.getOiaCommCheck());
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), ThemeManager.getOiaBackground());
} }
@Test @Test
@@ -62,8 +62,33 @@ public class ThemeManagerTest {
// Verify Status Bar colors // Verify Status Bar colors
Color sbBg = ThemeManager.getStatusBarBg(theme); Color sbBg = ThemeManager.getStatusBarBg(theme);
Color sbNormal = ThemeManager.getOiaFgNormal(theme); Color sbNormal = ThemeManager.getOiaFgNormal(theme);
Color sbInhibited = ThemeManager.getOiaInputInhibited(theme);
Color sbSysAvail = ThemeManager.getOiaStatusSysAvail(theme);
Color sbAttention = ThemeManager.getOiaAttention(theme);
Color sbCommCheck = ThemeManager.getOiaCommCheck(theme);
assertNotNull(sbBg); assertNotNull(sbBg);
assertNotNull(sbNormal); assertNotNull(sbNormal);
assertNotNull(sbInhibited);
assertNotNull(sbSysAvail);
assertNotNull(sbAttention);
assertNotNull(sbCommCheck);
// Verify status bar contrast: ensure NO white on grey in light mode
double sbBgLum = (0.299 * sbBg.getRed() + 0.587 * sbBg.getGreen() + 0.114 * sbBg.getBlue());
double sbInhibitedLum = (0.299 * sbInhibited.getRed() + 0.587 * sbInhibited.getGreen() + 0.114 * sbInhibited.getBlue());
double sbInhibitedDiff = Math.abs(sbBgLum - sbInhibitedLum);
assertTrue(sbInhibitedDiff > 120, "Status bar input inhibited (X PROT) contrast in " + theme + " must be > 120 (was " + sbInhibitedDiff + ")");
double sbSysAvailLum = (0.299 * sbSysAvail.getRed() + 0.587 * sbSysAvail.getGreen() + 0.114 * sbSysAvail.getBlue());
assertTrue(Math.abs(sbBgLum - sbSysAvailLum) > 100, "Status bar sys avail contrast in " + theme + " must be > 100");
double sbAttentionLum = (0.299 * sbAttention.getRed() + 0.587 * sbAttention.getGreen() + 0.114 * sbAttention.getBlue());
assertTrue(Math.abs(sbBgLum - sbAttentionLum) > 100, "Status bar attention contrast in " + theme + " must be > 100");
if (theme == UITheme.LIGHT) {
assertNotEquals(Color.WHITE, sbInhibited, "Status bar input inhibited text must not be pure white in light mode");
assertNotEquals(Color.WHITE, sbNormal, "Status bar normal text must not be pure white in light mode");
}
// Verify Button colors // Verify Button colors
Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme); Color btnDefBg = ThemeManager.getButtonBg(ThemeManager.ButtonVariant.DEFAULT, theme);
@@ -218,4 +243,23 @@ public class ThemeManagerTest {
tmpIni.delete(); tmpIni.delete();
} }
} }
@Test
public void testStatusBarLightModeLegibility() {
try {
ThemeManager.setTheme(UITheme.LIGHT);
StatusBar sb = new StatusBar();
sb.applyTheme(UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited());
assertNotEquals(Color.WHITE, ThemeManager.getOiaInputInhibited(UITheme.LIGHT));
// Verify recursive applyTheme does not break StatusBar background
ThemeManager.applyTheme(sb, UITheme.LIGHT);
assertEquals(ThemeManager.getStatusBarBg(UITheme.LIGHT), sb.getBackground());
} catch (HeadlessException e) {
// Handled in headless CI
}
}
} }
@@ -1288,6 +1288,9 @@ 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)); log.info(String.format("GOCA stream SPAN_LAST assembled: %d bytes", fullStream.length));
try {
java.nio.file.Files.write(java.nio.file.Paths.get("/Users/rudi/Projects/j3270/captured_goca.bin"), fullStream);
} catch (Exception ignored) {}
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 {
@@ -255,6 +255,29 @@ public class FillArea {
} }
} }
private void plotFillPixel(GraphicsPlane plane, int x, int y, int colorArgb, boolean isMixOr) {
if (isMixOr) {
int dst = plane.getPixel(x, y);
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int srcR = (colorArgb >>> 16) & 0xFF;
int srcG = (colorArgb >>> 8) & 0xFF;
int srcB = colorArgb & 0xFF;
int outR = Math.min(255, dstR | srcR);
int outG = Math.min(255, dstG | srcG);
int outB = Math.min(255, dstB | srcB);
int outA = Math.max((dst >>> 24) & 0xFF, (colorArgb >>> 24) & 0xFF);
if (outA == 0 && (outR != 0 || outG != 0 || outB != 0)) outA = 255;
plane.setPixelDirect(x, y, (outA << 24) | (outR << 16) | (outG << 8) | outB);
} else {
plane.setPixel(x, y, colorArgb);
}
}
/** /**
* Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule. * Rasterizes and fills the accumulated area polygons on the target GraphicsPlane with explicit fill rule.
*/ */
@@ -270,16 +293,11 @@ public class FillArea {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; int bg = bgColorArgb;
// Background mix / transparency rule for Black fills:
// BMX_TRANSPARENT / 0 or 2 / MIX_DEFAULT: Transparent black
// BMX_OPAQUE / 1: Opaque background overpaint
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT ||
bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2);
boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1); boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { boolean isMixOr = fillModeOR || (plane.getMixMode() == GocaConstants.MIX_OR && (getBounds().width >= 5 && getBounds().height >= 5 && getBounds().width * getBounds().height >= 100));
if (pattern != GocaConstants.PT_EMPTY) {
double minY = Double.MAX_VALUE; double minY = Double.MAX_VALUE;
double maxY = Double.MIN_VALUE; double maxY = Double.MIN_VALUE;
@@ -340,18 +358,18 @@ public class FillArea {
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
plane.setPixel(x, y, bg); plotFillPixel(plane, x, y, bg, isMixOr);
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { } else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) { } else if (patRows != null) {
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) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
plane.setPixel(x, y, bg); plotFillPixel(plane, x, y, bg, isMixOr);
} }
} }
} }
@@ -374,18 +392,18 @@ public class FillArea {
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
plane.setPixel(x, y, bg); plotFillPixel(plane, x, y, bg, isMixOr);
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { } else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) { } else if (patRows != null) {
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) {
plane.setPixel(x, y, fill); plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
plane.setPixel(x, y, bg); plotFillPixel(plane, x, y, bg, isMixOr);
} }
} }
} }
@@ -20,6 +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 fgMix = GocaConstants.MIX_DEFAULT;
private int bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec) 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;
@@ -35,8 +36,8 @@ public class GocaDecoder {
private double charAngle = 0.0; private double charAngle = 0.0;
private double charShear = 0.0; private double charShear = 0.0;
private double fractionalLineWidth = 1.0; private double fractionalLineWidth = 1.0;
private int charWidth = 9; private double charWidth = 9.0;
private int charHeight = 16; private double charHeight = 16.0;
private int charSet = 0; private int charSet = 0;
private int charPrecision = GocaConstants.CP_STRING; private int charPrecision = GocaConstants.CP_STRING;
private int arcParamP = 1; private int arcParamP = 1;
@@ -48,6 +49,14 @@ public class GocaDecoder {
private boolean segDynamic = false; private boolean segDynamic = false;
private boolean segVisible = true; private boolean segVisible = true;
// Default attributes configured by P_SCUDEF (0x21) and restored on G_BEGSEGM (0x70)
private int defColorIndex = 0;
private int defFmix = GocaConstants.MIX_DEFAULT;
private int defLineType = GocaConstants.LT_SOLID;
private int defLineWidth = GocaConstants.LW_NORMAL;
private int defPattern = GocaConstants.PT_SOLID;
private int defPatternSet = 0;
private ProgramSymbolManager programSymbolManager; private ProgramSymbolManager programSymbolManager;
// Area accumulation // Area accumulation
@@ -201,6 +210,10 @@ public class GocaDecoder {
return sb != null ? sb.tag : 0; return sb != null ? sb.tag : 0;
} }
public synchronized int getCurrentSegId() {
return currentSegId;
}
private void trackPoint(int x, int y) { private void trackPoint(int x, int y) {
if (currentSegId != 0) { if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId); SegmentBounds sb = segmentBoundsMap.get(currentSegId);
@@ -277,6 +290,10 @@ public class GocaDecoder {
flags, segChained, segDynamic, segVisible)); flags, segChained, segDynamic, segVisible));
} }
public synchronized int getFgMix() {
return fgMix;
}
/** /**
* Processes GOCA order 0x11 Fractional Line Width calculation. * Processes GOCA order 0x11 Fractional Line Width calculation.
*/ */
@@ -303,15 +320,25 @@ public class GocaDecoder {
segmentBoundsMap.clear(); segmentBoundsMap.clear();
activeSegmentsInOrder.clear(); activeSegmentsInOrder.clear();
currentSegId = 0; currentSegId = 0;
defColorIndex = 0;
defFmix = GocaConstants.MIX_DEFAULT;
defLineType = GocaConstants.LT_SOLID;
defLineWidth = GocaConstants.LW_NORMAL;
defPattern = GocaConstants.PT_SOLID;
defPatternSet = 0;
resetAttributes(); resetAttributes();
} }
public synchronized void resetAttributes() { public synchronized void resetAttributes() {
curColor = getColor(0); curColor = getColor(defColorIndex);
fgMix = defFmix;
if (plane != null) {
plane.setMixMode(defFmix);
}
bgMix = 0; // BMX_DEFAULT (MIX_LEAVE / transparent background mix per GOCA spec) 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 = defLineType;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = defLineWidth;
fractionalLineWidth = 1.0; fractionalLineWidth = 1.0;
if (plane != null) { if (plane != null) {
plane.setFractionalLineWidth(1.0); plane.setFractionalLineWidth(1.0);
@@ -320,12 +347,14 @@ public class GocaDecoder {
markerSize = 5; markerSize = 5;
markerColor = curColor; markerColor = curColor;
markerPrecision = 0; markerPrecision = 0;
pattern = GocaConstants.PT_SOLID; pattern = defPattern;
patternSet = 0; patternSet = defPatternSet;
fillColor = curColor; fillColor = curColor;
charDir = GocaConstants.CD_LR; charDir = GocaConstants.CD_LR;
charAngle = 0.0; charAngle = 0.0;
charShear = 0.0; charShear = 0.0;
charWidth = 9.0;
charHeight = 16.0;
charSet = 0; charSet = 0;
charPrecision = GocaConstants.CP_STRING; charPrecision = GocaConstants.CP_STRING;
inArea = false; inArea = false;
@@ -334,6 +363,8 @@ public class GocaDecoder {
areaFill = true; areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
areaPolygons.clear();
currentPolyPts = 0;
inImage = false; inImage = false;
imgBitDepth = GocaConstants.BPP_1; imgBitDepth = GocaConstants.BPP_1;
imgCompression = GocaConstants.IMG_UNCOMPRESSED; imgCompression = GocaConstants.IMG_UNCOMPRESSED;
@@ -359,7 +390,7 @@ public class GocaDecoder {
* 4. 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) { 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 || order == GocaConstants.G_COMT) { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
return 1; return 1;
@@ -374,20 +405,27 @@ public class GocaDecoder {
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
} }
// Fractional Line Width (0x11): 2-byte operand [int][frac] or 1-byte operand [int] // Fractional Line Width (0x11): 2-byte operand [int][frac] in standalone test or 1-byte operand [int] in stream
if (order == GocaConstants.G_GSFLW) { if (order == GocaConstants.G_GSFLW) {
return (idx + 2 < end) ? 3 : 2; if (idx + 3 == end) {
return 3;
}
return 2;
} }
// All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.) // All 1-byte operand short orders in 0x02..0x1F range (GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSMX, GSBMX, etc.)
if (order < 0x20) { if (order < 0x20) {
return 2; return 2;
} }
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1) // GSCS (0x38) can optionally have a 1-byte length prefix (0x01) in synthetic tests
if (order == GocaConstants.G_GSCS && data[idx + 1] == 0x01 && idx + 2 < end) {
return 3;
}
// Short 2-byte attribute orders per IBM Host On-Demand HODDrawOrder2Byte
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT || if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) { order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2; return 2;
} }
if (order == GocaConstants.G_GCALL) { if (order == GocaConstants.G_GCALL) {
return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5; return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5;
@@ -688,7 +726,14 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GSCH: { // Set Character Cell (0x33) case GocaConstants.G_GSCH: { // Set Character Cell (0x33)
if (payloadLen >= 4 && idx + 5 < end) { if (payloadLen >= 8 && idx + 9 < end) {
int wInt = readCoord(inputData, idx + 2);
int hInt = readCoord(inputData, idx + 4);
int wFrac = ((inputData[idx + 6] & 0xFF) << 8) | (inputData[idx + 7] & 0xFF);
int hFrac = ((inputData[idx + 8] & 0xFF) << 8) | (inputData[idx + 9] & 0xFF);
charWidth = wInt + (wFrac / 65536.0);
charHeight = hInt + (hFrac / 65536.0);
} else if (payloadLen >= 4 && idx + 5 < end) {
charWidth = readCoord(inputData, idx + 2); charWidth = readCoord(inputData, idx + 2);
charHeight = readCoord(inputData, idx + 4); charHeight = readCoord(inputData, idx + 4);
} }
@@ -739,17 +784,26 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case 0x04: // HODSegmentCharacteristics (2-byte NOP per HoD)
case 0x05:
case 0x06: case 0x06:
case 0x12: {
idx += orderLen;
break;
}
case GocaConstants.G_GSLT: { // Set Line Type (0x18) case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF; lineType = inputData[idx + 1] & 0xFF;
idx += orderLen; idx += orderLen;
break; break;
} }
case 0x04:
case 0x05:
case 0x12:
case GocaConstants.G_GSLW: { // Set Line Width (0x19) case GocaConstants.G_GSLW: { // Set Line Width (0x19)
lineWidth = inputData[idx + 1] & 0xFF; lineWidth = inputData[idx + 1] & 0xFF;
if (lineWidth == 0) {
lineWidth = defLineWidth;
}
if (plane != null) {
plane.setLineWidth(lineWidth);
}
idx += orderLen; idx += orderLen;
break; break;
} }
@@ -803,7 +857,15 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSMX: case GocaConstants.G_GSMX: { // Set Mix (0x0C)
fgMix = inputData[idx + 1] & 0xFF;
if (plane != null) {
plane.setMixMode(fgMix);
}
logger.info("GOCA GSMX: fgMix=" + fgMix);
idx += orderLen;
break;
}
case GocaConstants.G_GSMS_SET: case GocaConstants.G_GSMS_SET:
case GocaConstants.G_GPOP: { case GocaConstants.G_GPOP: {
idx += orderLen; idx += orderLen;
@@ -816,9 +878,10 @@ public class GocaDecoder {
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 = inputData[idx + 1] & 0xFF;
boolean drawBoundary = (flags & 0x80) != 0; // IBM bit numbering: Bit 1 (0x40) is the boundary flag per IBM Host On-Demand HODDecoder:2229
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD; boolean drawBoundary = (flags & 0x40) != 0;
int fillRule = (flags & 0x20) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule)); logger.info(String.format("GOCA GBAR: flags=0x%02x drawBoundary=%b fillRule=%d", flags, drawBoundary, fillRule));
beginArea(drawBoundary, fillRule); beginArea(drawBoundary, fillRule);
idx += orderLen; idx += orderLen;
@@ -1017,13 +1080,9 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults) case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults)
// Note: Per IBM 3179G / HOD architecture, 0x21 sets default drawing attributes (color, line, pattern).
// It is NOT an executive segment redraw order. A prior attempt treated 0x21 as an invented
// P_SCUDEF segment redraw loop, which caused old dropdown menus and segments to be repeatedly
// repainted on top of the screen, creating ghost artifacts and stale bounding boxes.
if (idx + 1 < end) { if (idx + 1 < end) {
int pLen = data[idx + 1] & 0xFF; int pLen = data[idx + 1] & 0xFF;
logger.fine(String.format("GOCA Set Current Defaults (0x21): len=%d", pLen)); processCurrentDefaults(data, idx, pLen);
idx += pLen + 2; idx += pLen + 2;
} else { } else {
idx++; idx++;
@@ -1068,6 +1127,63 @@ public class GocaDecoder {
} }
} }
private void processCurrentDefaults(byte[] data, int offset, int pLen) {
if (pLen < 4 || offset + pLen + 2 > data.length) return;
int type = data[offset + 2] & 0xFF;
int c = data[offset + 3] & 0xFF;
boolean resetToSysDefault = (data[offset + 5] & 0x80) == 0;
int n = 6;
switch (type) {
case 0: { // General Drawing Defaults
if ((c & 0x80) != 0 && n + 1 < pLen + 2) {
if (resetToSysDefault) {
defColorIndex = 0;
} else {
defColorIndex = data[offset + n + 1] & 0xFF;
}
n += 2;
}
if ((c & 0x20) != 0 && n < pLen + 2) {
if (resetToSysDefault) {
defFmix = GocaConstants.MIX_DEFAULT;
} else {
defFmix = data[offset + n++] & 0xFF;
}
}
break;
}
case 1: { // Line Defaults
if ((c & 0x80) != 0 && n < pLen + 2) {
defLineType = resetToSysDefault ? GocaConstants.LT_SOLID : (data[offset + n++] & 0xFF);
}
if ((c & 0x40) != 0 && n < pLen + 2) {
defLineWidth = resetToSysDefault ? GocaConstants.LW_NORMAL : (data[offset + n++] & 0xFF);
}
break;
}
case 4: { // Pattern Defaults
if ((c & 0x80) != 0 && n < pLen + 2) {
defPattern = resetToSysDefault ? GocaConstants.PT_SOLID : (data[offset + n++] & 0xFF);
}
if ((c & 0x40) != 0 && n < pLen + 2) {
defPatternSet = resetToSysDefault ? 0 : (data[offset + n++] & 0xFF);
}
break;
}
default:
break;
}
logger.info(String.format("GOCA P_SCUDEF: type=%d defColor=%d defFmix=%d defLineType=%d defLineWidth=%d defPattern=%d",
type, defColorIndex, defFmix, defLineType, defLineWidth, defPattern));
}
public synchronized int getDefColorIndex() { return defColorIndex; }
public synchronized int getDefFmix() { return defFmix; }
public synchronized int getDefLineType() { return defLineType; }
public synchronized int getDefLineWidth() { return defLineWidth; }
public synchronized int getDefPattern() { return defPattern; }
private void beginArea(boolean drawBoundary) { private void beginArea(boolean drawBoundary) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD); beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
} }
@@ -1488,11 +1604,11 @@ public class GocaDecoder {
if (textLen <= 0) return; if (textLen <= 0) return;
// IBM 3179G vector graphics base cell is 9x16 // IBM 3179G vector graphics base cell is 9x16
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0; double cw = charWidth > 0 ? (charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0; double ch = charHeight > 0 ? (charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
int cellW = (charWidth > 0 ? charWidth : 9); int cellW = (int) Math.round(charWidth > 0 ? charWidth : 9.0);
int cellH = (charHeight > 0 ? charHeight : 16); int cellH = (int) Math.round(charHeight > 0 ? charHeight : 16.0);
switch (charDir) { switch (charDir) {
case GocaConstants.CD_TB: case GocaConstants.CD_TB:
trackPoint(startX, startY); trackPoint(startX, startY);
@@ -1547,7 +1663,7 @@ public class GocaDecoder {
} }
} }
} }
startX += (charWidth > 0 ? charWidth : 9); startX += (int) Math.round(charWidth > 0 ? charWidth : 9.0);
} }
curX = startX; curX = startX;
curY = startY; curY = startY;
@@ -1565,20 +1681,20 @@ public class GocaDecoder {
switch (charDir) { switch (charDir) {
case GocaConstants.CD_TB: case GocaConstants.CD_TB:
curX = startX; curX = startX;
curY = startY - (textLen * (charHeight > 0 ? charHeight : 16)); curY = startY - (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
break; break;
case GocaConstants.CD_RL: case GocaConstants.CD_RL:
curX = startX - (textLen * (charWidth > 0 ? charWidth : 9)); curX = startX - (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
curY = startY; curY = startY;
break; break;
case GocaConstants.CD_BT: case GocaConstants.CD_BT:
curX = startX; curX = startX;
curY = startY + (textLen * (charHeight > 0 ? charHeight : 16)); curY = startY + (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
break; break;
case GocaConstants.CD_LR: case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT: case GocaConstants.CD_DEFAULT:
default: default:
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9)); curX = startX + (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
curY = startY; curY = startY;
break; break;
} }
@@ -1,8 +1,19 @@
package haus.nightmare.lib3270j.graphics; package haus.nightmare.lib3270j.graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image; import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -100,12 +111,20 @@ public class GraphicsPlane {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth); drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth);
} }
private BufferedImage canvasImage;
public synchronized BufferedImage toBufferedImage() { public synchronized BufferedImage toBufferedImage() {
BufferedImage img = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB); if (canvasImage == null && rgbBuffer != null && canvasWidth > 0 && canvasHeight > 0) {
if (rgbBuffer != null) { DataBufferInt db = new DataBufferInt(rgbBuffer, rgbBuffer.length);
img.setRGB(0, 0, canvasWidth, canvasHeight, rgbBuffer, 0, canvasWidth); DirectColorModel cm = new DirectColorModel(32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);
WritableRaster raster = Raster.createWritableRaster(
new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, canvasWidth, canvasHeight,
new int[]{0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000}),
db, null
);
canvasImage = new BufferedImage(cm, raster, false, null);
} }
return img; return canvasImage;
} }
public synchronized Image getImage() { public synchronized Image getImage() {
@@ -211,6 +230,7 @@ public class GraphicsPlane {
this.canvasWidth = w; this.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer; this.rgbBuffer = newBuffer;
this.canvasImage = null;
updateViewingWindowPixels(); updateViewingWindowPixels();
} }
@@ -218,6 +238,7 @@ public class GraphicsPlane {
if (rgbBuffer != null) { if (rgbBuffer != null) {
Arrays.fill(rgbBuffer, 0); Arrays.fill(rgbBuffer, 0);
} }
this.currentMixMode = 0;
hasContent = false; hasContent = false;
updateCount++; updateCount++;
} }
@@ -426,8 +447,28 @@ public class GraphicsPlane {
return yMax - ny; return yMax - ny;
} }
public synchronized int getPixel(int x, int y) {
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
return rgbBuffer[y * canvasWidth + x];
}
return 0;
}
public synchronized void setPixelDirect(int x, int y, int colorArgb) {
if (viewingWindowActive) {
if (x < clipPixelXMin || x > clipPixelXMax || y < clipPixelYMin || y > clipPixelYMax) {
return;
}
}
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
rgbBuffer[y * canvasWidth + x] = colorArgb;
hasContent = true;
updateCount++;
}
}
/** /**
* Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending. * Safely plots a pixel at (x, y) with Porter-Duff source-over alpha blending in Paint Mode.
*/ */
public synchronized void setPixel(int x, int y, int colorArgb) { public synchronized void setPixel(int x, int y, int colorArgb) {
if (viewingWindowActive) { if (viewingWindowActive) {
@@ -436,14 +477,26 @@ public class GraphicsPlane {
} }
} }
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
if (currentMixMode == GocaConstants.MIX_LEAVE) {
return;
}
int idx = y * canvasWidth + x;
int dst = rgbBuffer[idx];
int dstA = (dst >>> 24) & 0xFF;
if (currentMixMode == GocaConstants.MIX_UNDER && dstA != 0) {
return;
}
if (currentMixMode == GocaConstants.MIX_XOR) {
rgbBuffer[idx] = 0xFF000000 | (dst ^ colorArgb);
hasContent = true;
updateCount++;
return;
}
int srcA = (colorArgb >>> 24) & 0xFF; int srcA = (colorArgb >>> 24) & 0xFF;
if (srcA == 0) return; if (srcA == 0) return;
int idx = y * canvasWidth + x;
if (srcA == 255) { if (srcA == 255) {
rgbBuffer[idx] = colorArgb; rgbBuffer[idx] = colorArgb;
} else { } else {
int dst = rgbBuffer[idx];
int dstA = (dst >>> 24) & 0xFF;
if (dstA == 0) { if (dstA == 0) {
rgbBuffer[idx] = colorArgb; rgbBuffer[idx] = colorArgb;
} else { } else {
@@ -481,110 +534,93 @@ public class GraphicsPlane {
} }
/** /**
* Draws an anti-aliased line using Xiaolin Wu's algorithm with sub-pixel double coordinates. * Standard integer Bresenham line algorithm matching IBM 3179G / Host On-Demand 1-pixel rasterization.
*/
public synchronized void drawLineBresenham(int x0, int y0, int x1, int y1, int color) {
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
int curX = x0;
int curY = y0;
while (true) {
setPixel(curX, curY, color);
if (curX == x1 && curY == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
curX += sx;
}
if (e2 < dx) {
err += dx;
curY += sy;
}
}
}
/**
* Parallel line drawing algorithm matching IBM Host On-Demand HODGraphUtil.drawHODLine.
*/
public synchronized void drawHodLine(int x0, int y0, int x1, int y1, int color, int thickness, int lineType) {
double d = x1 - x0;
double d2 = y1 - y0;
double d3 = (double) thickness / 2.0;
double d4 = (x0 == x1) ? Math.PI : Math.atan(d2 / d) + Math.PI / 2.0;
double d5 = Math.cos(d4);
double d6 = Math.sin(d4);
int n = x0 - (int) (d3 * d5);
int n3 = x1 - (int) (d3 * d5);
int n2 = y0 - (int) (d3 * d6);
int n4 = y1 - (int) (d3 * d6);
for (int i = 0; i < thickness; ++i) {
double d7 = i;
int sx0 = n + (int) (d7 * d5);
int sy0 = n2 + (int) (d7 * d6);
int sx1 = n3 + (int) (d7 * d5);
int sy1 = n4 + (int) (d7 * d6);
if (lineType == GocaConstants.LT_SOLID || lineType == GocaConstants.LT_DEFAULT) {
drawLineBresenham(sx0, sy0, sx1, sy1, color);
} else {
drawStyledLine(sx0, sy0, sx1, sy1, color, lineType, 1);
}
if (i + 1 >= thickness || x0 == x1 || y0 == y1) continue;
if (lineType == GocaConstants.LT_SOLID || lineType == GocaConstants.LT_DEFAULT) {
drawLineBresenham(sx0 + 1, sy0, sx1 + 1, sy1, color);
} else {
drawStyledLine(sx0 + 1, sy0, sx1 + 1, sy1, color, lineType, 1);
}
}
}
/**
* Draws a line matching IBM 3179G / Host On-Demand rasterization.
*/ */
public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) { public synchronized void drawLine(double x0, double y0, double x1, double y1, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : 0x00FFFFFF; int color = (colorArgb != 0) ? (colorArgb & 0x00FFFFFF) : (GocaConstants.GOCA_COLORS[0] & 0x00FFFFFF);
int ix0 = (int) Math.round(x0);
int iy0 = (int) Math.round(y0);
int ix1 = (int) Math.round(x1);
int iy1 = (int) Math.round(y1);
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
if (thickness <= 1) {
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) { if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
drawStyledLine((int) Math.round(x0), (int) Math.round(y0), drawStyledLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, lineType, 1);
(int) Math.round(x1), (int) Math.round(y1),
(0xFF << 24) | color, lineType, lineWidth);
return;
}
// Special case: single point or zero-length line
if (Math.abs(x1 - x0) < 1e-5 && Math.abs(y1 - y0) < 1e-5) {
drawPixelWithThickness((int) Math.round(x0), (int) Math.round(y0), (0xFF << 24) | color, (lineWidth == GocaConstants.LW_THICK) ? 2 : 1);
return;
}
boolean steep = Math.abs(y1 - y0) > Math.abs(x1 - x0);
if (steep) {
double tmp = x0; x0 = y0; y0 = tmp;
tmp = x1; x1 = y1; y1 = tmp;
}
if (x0 > x1) {
double tmp = x0; x0 = x1; x1 = tmp;
tmp = y0; y0 = y1; y1 = tmp;
}
double dx = x1 - x0;
double dy = y1 - y0;
double gradient = (dx == 0.0) ? 1.0 : (dy / dx);
// First endpoint
double xend = Math.round(x0);
double yend = y0 + gradient * (xend - x0);
double xgap = 1.0 - (x0 + 0.5 - Math.floor(x0 + 0.5));
int xpxl1 = (int) xend;
int ypxl1 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl1, xpxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl1 + 1, xpxl1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else { } else {
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth); drawLineBresenham(ix0, iy0, ix1, iy1, (0xFF << 24) | color);
plotPixelWu(xpxl1, ypxl1 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
double intery = yend + gradient;
// Second endpoint
xend = Math.round(x1);
yend = y1 + gradient * (xend - x1);
xgap = x1 + 0.5 - Math.floor(x1 + 0.5);
int xpxl2 = (int) xend;
int ypxl2 = (int) Math.floor(yend);
if (steep) {
plotPixelWu(ypxl2, xpxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(ypxl2 + 1, xpxl2, color, (yend - Math.floor(yend)) * xgap, lineWidth);
} else {
plotPixelWu(xpxl2, ypxl2, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
plotPixelWu(xpxl2, ypxl2 + 1, color, (yend - Math.floor(yend)) * xgap, lineWidth);
}
// Main anti-aliased stepping loop
if (steep) {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(y, x, color, 1.0 - frac, lineWidth);
plotPixelWu(y + 1, x, color, frac, lineWidth);
intery += gradient;
} }
} else { } else {
for (int x = xpxl1 + 1; x < xpxl2; x++) { drawHodLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, thickness, lineType);
int y = (int) Math.floor(intery);
double frac = intery - y;
plotPixelWu(x, y, color, 1.0 - frac, lineWidth);
plotPixelWu(x, y + 1, color, frac, lineWidth);
intery += gradient;
}
} }
hasContent = true; hasContent = true;
updateCount++; updateCount++;
} }
private void plotPixelWu(int x, int y, int colorRgb, double brightness, int lineWidth) {
if (brightness <= 0.0) return;
if (lineWidth == GocaConstants.LW_THICK || fractionalLineWidth >= 1.5) {
int extra = (int) Math.round(Math.max(1, fractionalLineWidth - 0.5));
setPixelCoverage(x, y, colorRgb, 1.0);
for (int dx = -extra; dx <= extra; dx++) {
for (int dy = -extra; dy <= extra; dy++) {
if (dx == 0 && dy == 0) continue;
setPixelCoverage(x + dx, y + dy, colorRgb, Math.min(1.0, brightness * 0.8));
}
}
} else {
// Perceptual gamma correction for crisp contrast on dark backgrounds
double b = Math.min(1.0, Math.pow(brightness, 0.75) * 1.15);
setPixelCoverage(x, y, colorRgb, b);
}
}
/** /**
* Draws an absolute or relative line using anti-aliasing for smooth vectors. * Draws an absolute or relative line using anti-aliasing for smooth vectors.
*/ */
@@ -657,9 +693,15 @@ public class GraphicsPlane {
private void drawPixelWithThickness(int x, int y, int color, int thickness) { private void drawPixelWithThickness(int x, int y, int color, int thickness) {
if (thickness <= 1) { if (thickness <= 1) {
setPixel(x, y, color); setPixel(x, y, color);
} else if (thickness == 2) {
setPixel(x, y, color);
setPixel(x + 1, y, color);
setPixel(x, y + 1, color);
setPixel(x + 1, y + 1, color);
} else { } else {
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) { int r = thickness / 2;
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) { for (int dy = -r; dy <= r; dy++) {
for (int dx = -r; dx <= r; dx++) {
setPixel(x + dx, y + dy, color); setPixel(x + dx, y + dy, color);
} }
} }
@@ -796,6 +838,32 @@ public class GraphicsPlane {
} }
} }
/**
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
*/
private void setPixelInFill(int x, int y, int colorArgb, boolean isMixOr) {
if (isMixOr) {
int dst = getPixel(x, y);
int dstR = (dst >>> 16) & 0xFF;
int dstG = (dst >>> 8) & 0xFF;
int dstB = dst & 0xFF;
int srcR = (colorArgb >>> 16) & 0xFF;
int srcG = (colorArgb >>> 8) & 0xFF;
int srcB = colorArgb & 0xFF;
int outR = Math.min(255, dstR | srcR);
int outG = Math.min(255, dstG | srcG);
int outB = Math.min(255, dstB | srcB);
int outA = Math.max((dst >>> 24) & 0xFF, (colorArgb >>> 24) & 0xFF);
if (outA == 0 && (outR != 0 || outG != 0 || outB != 0)) outA = 255;
setPixelDirect(x, y, (outA << 24) | (outR << 16) | (outG << 8) | outB);
} else {
setPixel(x, y, colorArgb);
}
}
/** /**
* Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding). * Fills an area with explicit fill rule (Even-Odd or Non-Zero Winding).
*/ */
@@ -807,19 +875,23 @@ public class GraphicsPlane {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb; int bg = bgColorArgb;
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.BMX_DEFAULT || bgMix == GocaConstants.BMX_TRANSPARENT ||
bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0 || bgMix == 2);
boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1); boolean isOpaqueBg = (bgMix == GocaConstants.BMX_OPAQUE || bgMix == 1);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
int minY = py[0]; int minY = py[0];
int maxY = py[0]; int maxY = py[0];
int minX = px[0];
int maxX = px[0];
for (int i = 1; i < numPoints; i++) { for (int i = 1; i < numPoints; i++) {
if (py[i] < minY) minY = py[i]; if (py[i] < minY) minY = py[i];
if (py[i] > maxY) maxY = py[i]; if (py[i] > maxY) maxY = py[i];
if (px[i] < minX) minX = px[i];
if (px[i] > maxX) maxX = px[i];
} }
int bWidth = maxX - minX + 1;
int bHeight = maxY - minY + 1;
boolean isMixOr = (currentMixMode == GocaConstants.MIX_OR && bWidth >= 5 && bHeight >= 5 && (bWidth * bHeight >= 100));
if (pattern != GocaConstants.PT_EMPTY) {
minY = Math.max(0, minY); minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY); maxY = Math.min(canvasHeight - 1, maxY);
@@ -881,18 +953,18 @@ public class GraphicsPlane {
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
setPixel(x, y, fill); setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
setPixel(x, y, bg); setPixelInFill(x, y, bg, isMixOr);
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { } else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
setPixel(x, y, fill); setPixelInFill(x, y, fill, isMixOr);
} 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); setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
setPixel(x, y, bg); setPixelInFill(x, y, bg, isMixOr);
} }
} }
} }
@@ -914,18 +986,18 @@ public class GraphicsPlane {
int pIdx = psY * psW + psX; int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0); boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) { if (bit) {
setPixel(x, y, fill); setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
setPixel(x, y, bg); setPixelInFill(x, y, bg, isMixOr);
} }
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) { } else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
setPixel(x, y, fill); setPixelInFill(x, y, fill, isMixOr);
} 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); setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) { } else if (isOpaqueBg) {
setPixel(x, y, bg); setPixelInFill(x, y, bg, isMixOr);
} }
} }
} }
@@ -943,7 +1015,12 @@ public class GraphicsPlane {
for (int i = 0; i < pLen - 1; i++) { for (int i = 0; i < pLen - 1; i++) {
drawLine((double) px[offset + i], (double) py[offset + i], drawLine((double) px[offset + i], (double) py[offset + i],
(double) px[offset + i + 1], (double) py[offset + i + 1], (double) px[offset + i + 1], (double) py[offset + i + 1],
boundaryColorArgb, lineType, lineWidth); boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
}
if (pLen >= 3 && (px[offset] != px[offset + pLen - 1] || py[offset] != py[offset + pLen - 1])) {
drawLine((double) px[offset + pLen - 1], (double) py[offset + pLen - 1],
(double) px[offset], (double) py[offset],
boundaryColorArgb, lineType, GocaConstants.LW_NORMAL);
} }
} }
offset += pLen; offset += pLen;
@@ -1085,7 +1162,10 @@ public class GraphicsPlane {
double curX = x; double curX = x;
double curY = y; double curY = y;
double radAngle = Math.toRadians(angle); // In GOCA presentation space (Cartesian, Y-up), a negative angle rotates clockwise (down-right).
// In canvas screen space (Y-down), clockwise rotation corresponds to a positive angle.
double screenAngle = -angle;
double radAngle = Math.toRadians(screenAngle);
double cosA = Math.cos(radAngle); double cosA = Math.cos(radAngle);
double sinA = Math.sin(radAngle); double sinA = Math.sin(radAngle);
@@ -1099,7 +1179,7 @@ public class GraphicsPlane {
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i); char c = text.charAt(i);
drawVssChar(curX, curY, c, color, cw, ch, angle, shearAngle); drawVssChar(curX, curY, c, color, cw, ch, screenAngle, shearAngle);
if (angle != 0.0) { if (angle != 0.0) {
curX += cw * cosA; curX += cw * cosA;
@@ -1150,6 +1230,59 @@ public class GraphicsPlane {
double sinA = Math.sin(radAngle); double sinA = Math.sin(radAngle);
double tanShear = Math.tan(Math.toRadians(shearAngle)); double tanShear = Math.tan(Math.toRadians(shearAngle));
if (ch < 6.0) {
BufferedImage img = toBufferedImage();
if (img != null) {
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
float strokeW = (float) Math.max(0.5, Math.min(0.75, ch / 5.0));
g.setStroke(new BasicStroke(strokeW, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.setColor(new Color(color, true));
int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
if (order == 0xC1) {
int byteLen = VectorSymbolData.vss_data[ptr + 1] & 0xFF;
int numPoints = byteLen / 4;
int dataPtr = ptr + 2;
if (numPoints >= 2) {
Path2D.Double path = new Path2D.Double();
for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
double nx = ((double) vx / VectorSymbolData.VSS_WIDTH) * cw;
double ny = -((double) vy / VectorSymbolData.VSS_HEIGHT) * ch;
double sx = nx - ny * tanShear;
double sy = ny;
double rx = (angle != 0.0) ? (sx * cosA - sy * sinA) : sx;
double ry = (angle != 0.0) ? (sx * sinA + sy * cosA) : sy;
double px = x + rx;
double py = y + ry;
if (p == 0) path.moveTo(px, py);
else path.lineTo(px, py);
}
g.draw(path);
}
ptr += 2 + byteLen;
} else {
ptr++;
}
}
g.dispose();
hasContent = true;
updateCount++;
return;
}
}
int ptr = offset; int ptr = offset;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) { while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF; int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -1161,9 +1294,6 @@ public class GraphicsPlane {
if (numPoints >= 2) { if (numPoints >= 2) {
double[] px = new double[numPoints]; double[] px = new double[numPoints];
double[] py = new double[numPoints]; double[] py = new double[numPoints];
int[] ipx = new int[numPoints];
int[] ipy = new int[numPoints];
for (int p = 0; p < numPoints; p++) { for (int p = 0; p < numPoints; p++) {
int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF); int vx = ((VectorSymbolData.vss_data[dataPtr + p * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 1] & 0xFF);
int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF); int vy = ((VectorSymbolData.vss_data[dataPtr + p * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + p * 4 + 3] & 0xFF);
@@ -1179,19 +1309,6 @@ public class GraphicsPlane {
px[p] = x + rx; px[p] = x + rx;
py[p] = y + ry; py[p] = y + ry;
ipx[p] = (int) Math.round(px[p]);
ipy[p] = (int) Math.round(py[p]);
}
// If contour is closed (e.g. bold character loop), fill with solid color
int firstVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
int firstVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
int lastVx = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 1] & 0xFF);
int lastVy = ((VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + (numPoints - 1) * 4 + 3] & 0xFF);
boolean isClosed = (numPoints >= 4) && (firstVx == lastVx) && (firstVy == lastVy);
if (isClosed) {
fillArea(ipx, ipy, numPoints, color, GocaConstants.PT_SOLID, false, 0, 0, 0);
} }
for (int p = 0; p < numPoints - 1; p++) { for (int p = 0; p < numPoints - 1; p++) {
@@ -96,4 +96,143 @@ public class FillAreaTest {
assertTrue(redCount > 0, "Expected pattern foreground red pixels"); assertTrue(redCount > 0, "Expected pattern foreground red pixels");
assertTrue(blueCount > 0, "Expected pattern background blue pixels under BMX_OPAQUE"); assertTrue(blueCount > 0, "Expected pattern background blue pixels under BMX_OPAQUE");
} }
@Test
public void testSolidBlackFillUnbounded() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
FillArea area = new FillArea();
// 40x40 box at (20,20) to (60,60)
area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4);
int black = 0xFF000000;
// Unbounded solid black fill (e.g. ADMOPS central slide canvas or GDDM menu erasure box)
area.fill(plane, black, 0, GocaConstants.PT_SOLID, false, 0,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.BMX_DEFAULT, 0, null);
assertTrue(plane.hasContent(), "Plane must have content after solid black fill");
int blackCount = 0;
for (int y = 25; y <= 55; y++) {
for (int x = 25; x <= 55; x++) {
if (plane.getRgbBuffer()[y * 100 + x] == black) {
blackCount++;
}
}
}
assertTrue(blackCount > 800, "Solid black fill must rasterize opaque black pixels (0xFF000000)");
}
@Test
public void testAdmopslaIntroScreenWindowTransparency() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
FillArea area = new FillArea();
// Window box at (20,20) to (60,60)
area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4);
int black = 0xFF000000;
int greenBorder = GocaConstants.GOCA_COLORS[0];
// ADMOPSLA intro screen: bounded area (drawBoundary=true) with default pattern 0 (PT_DEFAULT=0)
area.fill(plane, black, 0, 0, true, greenBorder,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, GocaConstants.BMX_DEFAULT, 0, null);
// Solid black fill: interior pixels must be black (0xFF000000)
int blackCount = 0;
for (int y = 25; y <= 55; y++) {
for (int x = 25; x <= 55; x++) {
if (plane.getRgbBuffer()[y * 100 + x] == black) {
blackCount++;
}
}
}
assertEquals((55 - 25 + 1) * (55 - 25 + 1), blackCount,
"Solid black fill interior must be black (0xFF000000)");
// But boundary outline must be drawn!
boolean hasBoundary = false;
for (int x = 20; x <= 60; x++) {
if (plane.getRgbBuffer()[20 * 100 + x] == greenBorder) {
hasBoundary = true;
break;
}
}
assertTrue(hasBoundary, "Window boundary outline must be drawn");
}
@Test
public void testForegroundMixModes() {
GraphicsPlane plane = new GraphicsPlane(10, 10);
int red = 0xFFFF0000;
int green = 0xFF00FF00;
// 1. MIX_LEAVE (3): do not draw
plane.setMixMode(GocaConstants.MIX_LEAVE);
plane.setPixel(5, 5, red);
assertEquals(0, plane.getRgbBuffer()[55], "MIX_LEAVE must not paint destination pixel");
// 2. MIX_OVER (2 / default): draw over
plane.setMixMode(GocaConstants.MIX_OVER);
plane.setPixel(5, 5, red);
// 3. MIX_UNDER (5): underpaint (do not overwrite non-zero destination)
plane.setMixMode(GocaConstants.MIX_UNDER);
plane.setPixel(5, 5, green);
assertEquals(red, plane.getRgbBuffer()[55], "MIX_UNDER must not overwrite existing pixel");
plane.setPixel(6, 6, green);
assertEquals(green, plane.getRgbBuffer()[66], "MIX_UNDER must paint empty pixel");
// 4. MIX_OR (1): In IBM HoD, MIX_OR is applied in area fills >= 100px, while standard operations run in Paint Mode
plane.setMixMode(GocaConstants.MIX_OR);
FillArea areaOr = new FillArea();
areaOr.addPolygon(new int[] { 0, 10, 10, 0 }, new int[] { 0, 0, 10, 10 }, 4);
int black = 0xFF000000;
plane.setPixel(5, 5, red);
areaOr.fill(plane, black, 0, GocaConstants.PT_SOLID, false, 0, 0, 0, 0, 0, null);
assertEquals(red, plane.getRgbBuffer()[55], "MIX_OR with Black must not alter existing pixel");
// Red on Green produces Yellow (0xFFFF00)
plane.setPixel(6, 6, green);
areaOr.fill(plane, red, 0, GocaConstants.PT_SOLID, false, 0, 0, 0, 0, 0, null);
int yellow = 0xFFFFFF00;
assertEquals(yellow, plane.getRgbBuffer()[66], "MIX_OR with Red over Green must produce Yellow");
}
@Test
public void testAdmopslaIntroScreenMixOrSolidBlackPatternTransparency() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
FillArea area = new FillArea();
// Window box at (20,20) to (60,60)
area.addPolygon(new int[] { 20, 60, 60, 20 }, new int[] { 20, 20, 60, 60 }, 4);
// Pre-paint a blue background
int blue = 0xFF7890F0;
for (int y = 20; y <= 60; y++) {
for (int x = 20; x <= 60; x++) {
plane.setPixel(x, y, blue);
}
}
// Real ADMOPSLA sequence: MIX_OR (1), pattern 16 (PT_SOLID), fillColor Black, boundary White
plane.setMixMode(GocaConstants.MIX_OR);
int black = 0xFF000000;
int whiteBorder = GocaConstants.GOCA_COLORS[7]; // White
area.fill(plane, black, 0, GocaConstants.PT_SOLID, true, whiteBorder,
GocaConstants.LT_DOT, GocaConstants.LW_NORMAL, GocaConstants.BMX_DEFAULT, 0, null);
// Interior blue pixels must remain intact under MIX_OR with Black!
for (int y = 25; y <= 55; y++) {
for (int x = 25; x <= 55; x++) {
assertEquals(blue, plane.getRgbBuffer()[y * 100 + x],
"Interior blue pixels must be preserved under MIX_OR with black fill");
}
}
// Boundary outline must be drawn
boolean hasWhite = false;
for (int x = 20; x <= 60; x++) {
if (plane.getRgbBuffer()[20 * 100 + x] == whiteBorder) {
hasWhite = true;
break;
}
}
assertTrue(hasWhite, "White border outline must be drawn under MIX_OR");
}
} }
@@ -360,7 +360,7 @@ public class GocaDecoderTest {
assertTrue(plane.hasContent()); assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer(); int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false; boolean allCrispOpaque = true;
int nonZeroPixels = 0; int nonZeroPixels = 0;
for (int y = 0; y < 100; y++) { for (int y = 0; y < 100; y++) {
@@ -371,15 +371,15 @@ public class GocaDecoderTest {
int alpha = (pixel >>> 24) & 0xFF; int alpha = (pixel >>> 24) & 0xFF;
int r = (pixel >>> 16) & 0xFF; int r = (pixel >>> 16) & 0xFF;
assertEquals(255, r, "Red channel must be preserved"); assertEquals(255, r, "Red channel must be preserved");
if (alpha > 0 && alpha < 255) { if (alpha != 255) {
hasIntermediateAlpha = true; allCrispOpaque = false;
} }
} }
} }
} }
assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line"); assertTrue(nonZeroPixels > 30, "Expected non-zero pixels along the line");
assertTrue(hasIntermediateAlpha, "Expected Xiaolin Wu anti-aliasing to produce fractional alpha coverage"); assertTrue(allCrispOpaque, "Expected crisp integer Bresenham line rendering matching IBM HoD");
} }
@Test @Test
@@ -392,21 +392,21 @@ public class GocaDecoderTest {
assertTrue(plane.hasContent()); assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer(); int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false; boolean allCrispOpaque = true;
int nonZeroPixels = 0; int nonZeroPixels = 0;
for (int p : buffer) { for (int p : buffer) {
if (p != 0) { if (p != 0) {
nonZeroPixels++; nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF; int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) { if (alpha != 255) {
hasIntermediateAlpha = true; allCrispOpaque = false;
} }
} }
} }
assertTrue(nonZeroPixels > 50, "Expected arc pixels"); assertTrue(nonZeroPixels > 50, "Expected arc pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased arc edges with smooth alpha"); assertTrue(allCrispOpaque, "Expected crisp arc outline matching IBM HoD");
} }
@Test @Test
@@ -419,21 +419,21 @@ public class GocaDecoderTest {
assertTrue(plane.hasContent()); assertTrue(plane.hasContent());
int[] buffer = plane.getRgbBuffer(); int[] buffer = plane.getRgbBuffer();
boolean hasIntermediateAlpha = false; boolean allCrispOpaque = true;
int nonZeroPixels = 0; int nonZeroPixels = 0;
for (int p : buffer) { for (int p : buffer) {
if (p != 0) { if (p != 0) {
nonZeroPixels++; nonZeroPixels++;
int alpha = (p >>> 24) & 0xFF; int alpha = (p >>> 24) & 0xFF;
if (alpha > 0 && alpha < 255) { if (alpha != 255) {
hasIntermediateAlpha = true; allCrispOpaque = false;
} }
} }
} }
assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels"); assertTrue(nonZeroPixels > 40, "Expected stroked vector text pixels");
assertTrue(hasIntermediateAlpha, "Expected anti-aliased vector text strokes with fractional alpha"); assertTrue(allCrispOpaque, "Expected crisp stroked vector text matching IBM HoD");
} }
@Test @Test
@@ -748,10 +748,11 @@ public class GocaDecoderTest {
out.write(0x00); out.write(20); out.write(0x00); out.write(40); out.write(0x00); out.write(20); out.write(0x00); out.write(40);
out.write(GocaConstants.G_GEAR); out.write(GocaConstants.G_GEAR);
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, Pattern 15/Empty, GSCOL White inside GBAR) // 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, GSMX MIX_OR, Pattern 16/Solid, GBAR 0x80, GSCOL White inside GBAR)
out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background) out.write(GocaConstants.G_GSCOL); out.write(0x08); // Black (background)
out.write(GocaConstants.G_GSPT); out.write(0x0F); // Empty pattern (transparent interior) out.write(GocaConstants.G_GSPT); out.write(0x10); // Solid pattern (16)
out.write(GocaConstants.G_GBAR); out.write(0x80); out.write(GocaConstants.G_GSMX); out.write(GocaConstants.MIX_OR); // Mix mode OR (1)
out.write(GocaConstants.G_GBAR); out.write(0x40); // Bounded (flags 0x40 per IBM Host On-Demand)
out.write(GocaConstants.G_GSCOL); out.write(0x07); // White line color 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_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points out.write(GocaConstants.G_GLINE); out.write(0x14); // 5 points
@@ -814,4 +815,229 @@ public class GocaDecoderTest {
} }
assertTrue(foundWhite, "Slide border outline must be drawn in White"); assertTrue(foundWhite, "Slide border outline must be drawn in White");
} }
@Test
public void testGbarBoundaryDetection() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. GBAR with 0x00 flag -> Unbounded (drawBoundary = false)
ByteArrayOutputStream out1 = new ByteArrayOutputStream();
out1.write(GocaConstants.G_GSCOL); out1.write(0x02); // Red fill
out1.write(GocaConstants.G_GBAR); out1.write(0x00); // Bit 0 clear -> Unbounded
out1.write(GocaConstants.G_GSCOL); out1.write(0x06); // Yellow boundary (if drawn)
out1.write(GocaConstants.G_GLINE); out1.write(0x14); // 5 points = 20 bytes
out1.write(0x00); out1.write(10); out1.write(0x00); out1.write(10);
out1.write(0x00); out1.write(40); out1.write(0x00); out1.write(10);
out1.write(0x00); out1.write(40); out1.write(0x00); out1.write(40);
out1.write(0x00); out1.write(10); out1.write(0x00); out1.write(40);
out1.write(0x00); out1.write(10); out1.write(0x00); out1.write(10);
out1.write(GocaConstants.G_GEAR);
byte[] stream1 = out1.toByteArray();
decoder.decodeStream(stream1, 0, stream1.length);
assertTrue(plane.hasContent(), "Area must be filled");
// Verify yellow boundary line was NOT drawn for 0x00
int yellowArgb = GocaConstants.GOCA_COLORS[6];
boolean foundYellow = false;
for (int p : plane.getRgbBuffer()) {
if (p == yellowArgb) {
foundYellow = true;
break;
}
}
assertFalse(foundYellow, "Area with flag 0x00 must NOT draw boundary strokes");
// 2. GBAR with 0x40 flag -> Bounded (bit 1 set per IBM HoD -> drawBoundary = true)
plane.clear();
ByteArrayOutputStream out2 = new ByteArrayOutputStream();
out2.write(GocaConstants.G_GSCOL); out2.write(0x02); // Red fill
out2.write(GocaConstants.G_GBAR); out2.write(0x40); // Bit 1 set -> Bounded
out2.write(GocaConstants.G_GSCOL); out2.write(0x06); // Yellow boundary
out2.write(GocaConstants.G_GLINE); out2.write(0x14);
out2.write(0x00); out2.write(10); out2.write(0x00); out2.write(10);
out2.write(0x00); out2.write(40); out2.write(0x00); out2.write(10);
out2.write(0x00); out2.write(40); out2.write(0x00); out2.write(40);
out2.write(0x00); out2.write(10); out2.write(0x00); out2.write(40);
out2.write(0x00); out2.write(10); out2.write(0x00); out2.write(10);
out2.write(GocaConstants.G_GEAR);
byte[] stream2 = out2.toByteArray();
decoder.decodeStream(stream2, 0, stream2.length);
foundYellow = false;
for (int p : plane.getRgbBuffer()) {
if (p == yellowArgb) {
foundYellow = true;
break;
}
}
assertTrue(foundYellow, "Area with flag 0x40 (bit 1 set) MUST draw boundary strokes");
// 3. GBAR with 0x80 flag -> Unbounded (bit 0 set only, bit 1 clear -> drawBoundary = false)
plane.clear();
ByteArrayOutputStream out3 = new ByteArrayOutputStream();
out3.write(GocaConstants.G_GSCOL); out3.write(0x02); // Red fill
out3.write(GocaConstants.G_GBAR); out3.write(0x80); // Bit 0 set only -> Unbounded
out3.write(GocaConstants.G_GSCOL); out3.write(0x06); // Yellow boundary (if drawn)
out3.write(GocaConstants.G_GLINE); out3.write(0x14);
out3.write(0x00); out3.write(10); out3.write(0x00); out3.write(10);
out3.write(0x00); out3.write(40); out3.write(0x00); out3.write(10);
out3.write(0x00); out3.write(40); out3.write(0x00); out3.write(40);
out3.write(0x00); out3.write(10); out3.write(0x00); out3.write(40);
out3.write(0x00); out3.write(10); out3.write(0x00); out3.write(10);
out3.write(GocaConstants.G_GEAR);
byte[] stream3 = out3.toByteArray();
decoder.decodeStream(stream3, 0, stream3.length);
foundYellow = false;
for (int p : plane.getRgbBuffer()) {
if (p == yellowArgb) {
foundYellow = true;
break;
}
}
assertFalse(foundYellow, "Area with flag 0x80 must NOT draw boundary strokes");
}
@Test
public void testGsflwStreamingOrderPreservation() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GSFLW (0x11, 0x01) followed by GLINE (0xC1, 0x08, 2 points)
// Must NOT consume 0xC1 as fractional byte!
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSCOL); out.write(0x03); // Pink
out.write(GocaConstants.G_GSFLW); out.write(0x01); // 2-byte GSFLW in stream
out.write(GocaConstants.G_GLINE); out.write(0x08); // 2 points
out.write(0x00); out.write(10); out.write(0x00); out.write(10);
out.write(0x00); out.write(30); out.write(0x00); out.write(30);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertEquals(1.0, decoder.getFractionalLineWidth(), 0.001);
assertTrue(plane.hasContent(), "GLINE following GSFLW must be executed successfully");
}
@Test
public void testGsmxForegroundMix() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
GocaDecoder decoder = new GocaDecoder(plane);
// Send GSMX 0x0C with MIX_LEAVE (3)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSMX); out.write(GocaConstants.MIX_LEAVE);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
assertEquals(GocaConstants.MIX_LEAVE, decoder.getFgMix());
assertEquals(GocaConstants.MIX_LEAVE, plane.getMixMode());
}
@Test
public void testScudefAndBeginSegmentDefaultsRestoration() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
GocaDecoder decoder = new GocaDecoder(plane);
// Send P_SCUDEF (0x21): Type 0 (General Drawing), mask 0x80 (color), value 2 (Red)
// Order structure: 0x21, pLen=6, type=0, mask=0x80, reserved=0, flag=0x80 (explicit), val_hi=0x00, val_lo=0x02
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.P_SCUDEF);
out.write(0x06); // pLen = 6
out.write(0x00); // type 0 = General Drawing
out.write(0x80); // mask: color
out.write(0x00); // reserved
out.write(0x80); // explicit value flag (bit 0 set)
out.write(0x00); // color hi
out.write(0x02); // color lo = 2 (Red)
byte[] scudefBytes = out.toByteArray();
decoder.processProcedureOrders(scudefBytes, 0, scudefBytes.length);
assertEquals(2, decoder.getDefColorIndex(), "P_SCUDEF must set default color index to 2");
// Send Begin Segment (0x70) with flag1 & 6 == 0
// BEGSEGM 14 bytes: 0x70, 0x0C, segId(4 bytes), flag0(1 byte), flag1(1 byte), name(6 bytes)
ByteArrayOutputStream segOut = new ByteArrayOutputStream();
segOut.write(GocaConstants.G_BEGSEGM);
segOut.write(0x0C);
segOut.write(0x00); segOut.write(0x00); segOut.write(0x00); segOut.write(0x05); // segId = 5
segOut.write(0x00); // flag0
segOut.write(0x00); // flag1 (& 6 == 0 -> reset to defaults)
for (int i = 0; i < 6; i++) segOut.write(0x00); // name
byte[] segBytes = segOut.toByteArray();
decoder.decodeStream(segBytes, 0, segBytes.length);
// Drawing color must now be restored to default (2 / Red = 0xFFFF0000)
int redColor = GocaConstants.GOCA_COLORS[2];
// Draw a line and verify it renders in Red
ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
lineOut.write(GocaConstants.G_GLINE); lineOut.write(0x08);
lineOut.write(0x00); lineOut.write(10); lineOut.write(0x00); lineOut.write(10);
lineOut.write(0x00); lineOut.write(30); lineOut.write(0x00); lineOut.write(10);
lineOut.write(GocaConstants.G_ENDSEGM);
byte[] lineBytes = lineOut.toByteArray();
decoder.decodeStream(lineBytes, 0, lineBytes.length);
int px = plane.mapX(20);
int py = plane.mapY(10);
assertEquals(redColor, plane.getPixel(px, py), "Segment must inherit restored default color from P_SCUDEF");
}
@Test
public void testGbarFlag0x40BoundaryBit() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
GocaDecoder decoder = new GocaDecoder(plane);
// GBAR with flag 0x40 (Bit 1 per IBM HoD line 2229)
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSCOL); out.write(0x02); // Red fill
out.write(GocaConstants.G_GBAR); out.write(0x40); // 0x40 -> Bounded
out.write(GocaConstants.G_GSCOL); out.write(0x06); // Yellow boundary
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(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);
out.write(GocaConstants.G_GEAR);
byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length);
int yellowArgb = GocaConstants.GOCA_COLORS[6];
boolean foundYellow = false;
for (int p : plane.getRgbBuffer()) {
if (p == yellowArgb) {
foundYellow = true;
break;
}
}
assertTrue(foundYellow, "Area with flag 0x40 (HoD boundary bit) MUST draw boundary strokes");
}
@Test
public void testFillAreaBoundaryClosingSegment() {
GraphicsPlane plane = new GraphicsPlane(100, 100);
// Triangle from (20,20) to (80,20) to (50,80) without repeating (20,20)
int[] px = new int[] { 20, 80, 50 };
int[] py = new int[] { 20, 20, 80 };
int green = 0xFF00FF00;
int whiteBorder = 0xFFFFFFFF;
plane.fillArea(px, py, 3, green, GocaConstants.PT_SOLID, true, whiteBorder,
GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
// Check the closing edge from (50,80) back to (20,20)
// Midpoint of (50,80) and (20,20) is (35, 50)
int midPixel = plane.getPixel(35, 50);
assertEquals(whiteBorder, midPixel, "Closing boundary segment from last vertex to first vertex must be drawn");
}
} }
@@ -159,13 +159,13 @@ public class Phase1ColorCalibrationTest {
public void testItem1_4_FillAreaBackgroundMix() { public void testItem1_4_FillAreaBackgroundMix() {
GraphicsPlane plane = new GraphicsPlane(10, 10); GraphicsPlane plane = new GraphicsPlane(10, 10);
// Test transparent black fill: should not paint when BMX_TRANSPARENT // Test empty/transparent pattern fill: should not paint interior
FillArea fillAreaTrans = new FillArea(GocaConstants.FILL_RULE_EVEN_ODD); FillArea fillAreaTrans = new FillArea(GocaConstants.FILL_RULE_EVEN_ODD);
fillAreaTrans.addEdge(0, 0, 10, 0); fillAreaTrans.addEdge(0, 0, 10, 0);
fillAreaTrans.addEdge(10, 0, 10, 10); fillAreaTrans.addEdge(10, 0, 10, 10);
fillAreaTrans.addEdge(10, 10, 0, 10); fillAreaTrans.addEdge(10, 10, 0, 10);
fillAreaTrans.addEdge(0, 10, 0, 0); fillAreaTrans.addEdge(0, 10, 0, 0);
fillAreaTrans.fill(plane, 0xFF000000, 0, GocaConstants.PT_SOLID, false, 0, 0, 1, fillAreaTrans.fill(plane, 0xFF000000, 0, GocaConstants.PT_EMPTY, false, 0, 0, 1,
GocaConstants.BMX_TRANSPARENT, 0xFF0000FF, GocaConstants.FILL_RULE_EVEN_ODD, null); GocaConstants.BMX_TRANSPARENT, 0xFF0000FF, GocaConstants.FILL_RULE_EVEN_ODD, null);
// Verify plane pixels remain unpainted (transparent / 0) // Verify plane pixels remain unpainted (transparent / 0)