3 Commits

Author SHA1 Message Date
rudi bf5e4410e6 Additional logging to debug a3270
Release j3270 / Build & Publish Release (push) Successful in 41s
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m6s
2026-08-21 12:51:31 -04:00
rudi 7e658a6c20 Update readme
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-21 11:57:14 -04:00
rudi c32521fd4b Cleaner rendering
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m6s
2026-08-21 11:51:37 -04:00
16 changed files with 527 additions and 341 deletions
+1
View File
@@ -1,3 +1,4 @@
j3270.log.*
*.log *.log
.DS_Store .DS_Store
*.jar *.jar
+7 -3
View File
@@ -1,7 +1,7 @@
# j3270 # j3270
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions) [![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Latest Release](https://git.hugfreevikings.wtf/rudi/j3270/badges/release.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases) [![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net) [![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net)
[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE) [![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE)
@@ -71,7 +71,7 @@ The project includes self-contained, portable build scripts:
# Build executable JAR in 2 seconds: # Build executable JAR in 2 seconds:
sh ./build_all.sh sh ./build_all.sh
# Run all 31 automated unit tests in ~180ms: # Run all 48 automated unit tests in ~500ms:
sh ./test_all.sh sh ./test_all.sh
``` ```
@@ -94,6 +94,10 @@ Contributions and issue reports are welcome! Please feel free to open a bug repo
## 📜 Acknowledgements ## 📜 Acknowledgements
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications - [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
- [Antigravity](https://antigravity.google) - [Antigravity](https://antigravity.google/)
- Claude Opus 4.6 - Claude Opus 4.6
- Gemini 3.1 Pro - Gemini 3.1 Pro
- Gemini 3.7 Flash
- Gemma4 12B and 26B
- GPT-OSS 120B
- Qwen3 4B and 32B
Binary file not shown.
@@ -925,7 +925,14 @@ public class TerminalPanel extends JPanel {
int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0); int cs = (ea.cs != 0) ? (ea.cs & 0xFF) : (currentFieldEa != null ? (currentFieldEa.cs & 0xFF) : 0);
boolean drawnAsPs = false; boolean drawnAsPs = false;
if (cs >= 0x40 && client.getProgramSymbolManager() != null) { if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
drawnAsPs = client.getProgramSymbolManager().drawSymbol(g2, cs, ea.ec & 0xFF, x, y, cellWidth, cellHeight, fgColor, bgColor); org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) {
int[] rgb = slot.getRgbPixels(fgColor.getRGB(), bgColor.getRGB());
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(slot.getWidth(), slot.getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
img.setRGB(0, 0, slot.getWidth(), slot.getHeight(), rgb, 0, slot.getWidth());
g2.drawImage(img, x, y, cellWidth, cellHeight, null);
drawnAsPs = true;
}
} }
if (!drawnAsPs) { if (!drawnAsPs) {
@@ -958,7 +965,12 @@ public class TerminalPanel extends JPanel {
int gridW = cols * cellWidth; int gridW = cols * cellWidth;
int gridH = rows * cellHeight; int gridH = rows * cellHeight;
client.getGraphicsPlane().resize(gridW, gridH); client.getGraphicsPlane().resize(gridW, gridH);
g2.drawImage(client.getGraphicsPlane().getImage(), ox, oy, gridW, gridH, null); int[] rgb = client.getGraphicsPlane().getRgbBuffer();
if (rgb != null) {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW);
g2.drawImage(img, ox, oy, gridW, gridH, null);
}
} }
// Draw cursor // Draw cursor
+6
View File
@@ -3,6 +3,12 @@ plugins {
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
} }
jar { jar {
@@ -946,7 +946,7 @@ public class DataStreamProcessor {
if ((i + 1) % 32 == 0) if ((i + 1) % 32 == 0)
sb.append("\n "); sb.append("\n ");
} }
log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim()); log.warning(">>> SENDING Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) { if (outputSender != null) {
outputSender.send3270Data(qr); outputSender.send3270Data(qr);
} }
@@ -961,7 +961,7 @@ public class DataStreamProcessor {
if ((i + 1) % 32 == 0) if ((i + 1) % 32 == 0)
sb.append("\n "); sb.append("\n ");
} }
log.info("Requested query reply (" + qr.length + " bytes):\n " + sb.toString().trim()); log.warning(">>> SENDING Requested Query Reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) { if (outputSender != null) {
outputSender.send3270Data(qr); outputSender.send3270Data(qr);
} }
@@ -103,26 +103,34 @@ public class QueryReplyBuilder {
// Highlighting // Highlighting
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting()); appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
// Reply Modes // Reply Modes (0x88)
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes()); appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
// Distributed Data Management (DFT File Transfer) if (graphicsMode.isVectorGraphicsEnabled()) {
// Save/Restore (0x8C)
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore());
}
// Distributed Data Management (0x95)
appendQueryReply(out, QR_DDM, buildDdm(4096)); appendQueryReply(out, QR_DDM, buildDdm(4096));
// Implicit Partition if (graphicsMode.isVectorGraphicsEnabled()) {
// Transparency (0x99)
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency());
}
// Implicit Partition (0xA6)
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows)); appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
// Vector Graphics QRs if enabled // Vector Graphics QRs if enabled
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_SAVE_RESTORE, buildSaveRestore()); appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
appendQueryReply(out, QR_TRANSPARENCY, buildTransparency()); appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1
appendQueryReply(out, QR_GRAPHICS, buildGraphics()); appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
appendQueryReply(out, QR_GIMAGE, buildGImage()); appendOemFmt(out); // 0xB3
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
appendOemFmt(out); appendQueryReply(out, QR_GSYMBOLS, buildGSymbols()); // 0xB6
appendQueryReply(out, QR_GCOLOR, buildGColor());
appendQueryReply(out, QR_GSYMBOLS, buildGSymbols());
} }
log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")"); log.info("Built " + out.size() + " bytes of all query replies (graphicsMode=" + graphicsMode + ")");
@@ -304,48 +312,48 @@ public class QueryReplyBuilder {
} }
private byte[] buildCharsets() { private byte[] buildCharsets() {
if (graphicsMode.isVectorGraphicsEnabled() || graphicsMode == GraphicsMode.NONE) { if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Standard 3179G / Base character sets (matches HOD QR_CHARSETS_S_STRING, 27 bytes total) // Programmed Symbols mode (3279 PS with LoadPS 0x0A)
ByteArrayOutputStream out = new ByteArrayOutputStream(23); ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0x82); // flags: GE, CGCSGID present out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags out.write(0x00); // more flags
out.write(SW_3279_2); // SDW - default char width (9) out.write(SW_3279_2); // SDW (9)
out.write(14); // SDH - default char height (14) out.write(SH_3279_2); // SDH (12)
out.write(0x00); // LoadPS format (0x00) out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
out.write(0x00); out.write(0x00); // Load PS device type (high)
out.write(0x00); out.write(0x00); // Load PS device type (low)
out.write(0x00); out.write(0x00); // reserved
out.write(0x07); // DL = 7 out.write(0x07); // DL = 7 bytes per descriptor
// Set 0 (Base EBCDIC) // Descriptor 1 (SET 0): default character set (non-loadable, single plane, CP037)
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Set 1 (APL/Text) // Descriptor 2 (SET 1): APL/GE character set
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
return out.toByteArray(); return out.toByteArray();
} }
// Programmed Symbols mode (3279 PS) // Standard 3179G / Base character sets (matches sf.c / HOD)
ByteArrayOutputStream out = new ByteArrayOutputStream(65); ByteArrayOutputStream out = new ByteArrayOutputStream(23);
out.write(0x82); // flags: GE, CGCSGID present out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags out.write(0x00); // more flags
out.write(SW_3279_2); // SDW (9) out.write(SW_3279_2); // SDW - default char width (9)
out.write(14); // SDH (14) out.write(SH_3279_2); // SDH - default char height (12)
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3 out.write(0x00); // LoadPS format (0x00)
out.write(0x00); // Load PS device type (high) out.write(0x00);
out.write(0x00); // Load PS device type (low) out.write(0x00);
out.write(0x00); // reserved out.write(0x00);
out.write(0x07); // DL = 7 bytes per descriptor out.write(0x07); // DL = 7
// Descriptor 1 (SET 0): default character set // Set 0 (Base EBCDIC - Non-loadable, single plane, CP037)
out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x01); out.write(0xf4); out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
// Descriptor 2 (SET 1): APL/GE character set // Set 1 (APL/Text)
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36); out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
return out.toByteArray(); return out.toByteArray();
} }
@@ -473,10 +481,13 @@ public class QueryReplyBuilder {
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
out.write(0x00); out.write(0x00);
out.write(i); out.write(i);
java.awt.Color c = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i]; int argb = org.lib3270j.graphics.GocaConstants.GOCA_COLORS[i];
out.write(c.getRed()); int r = (argb >> 16) & 0xFF;
out.write(c.getGreen()); int g = (argb >> 8) & 0xFF;
out.write(c.getBlue()); int b = argb & 0xFF;
out.write(r);
out.write(g);
out.write(b);
out.write(0x00); // 6th byte in HOD color table out.write(0x00); // 6th byte in HOD color table
} }
return out.toByteArray(); return out.toByteArray();
@@ -150,24 +150,35 @@ public final class GocaConstants {
public static final int MIX_XOR = 4; public static final int MIX_XOR = 4;
public static final int MIX_UNDER = 5; public static final int MIX_UNDER = 5;
// Graphic Colors (IBM 3179G / HOD 16-color table) // Graphic Colors (IBM 3179G / HOD 16-color table in 32-bit ARGB)
public static final java.awt.Color[] GOCA_COLORS = new java.awt.Color[] { public static final int[] GOCA_COLORS = new int[] {
new java.awt.Color(0, 255, 0), // 0: Default (Green) 0xFF00FF00, // 0: Default (Green)
new java.awt.Color(120, 144, 240), // 1: Blue 0xFF7890F0, // 1: Blue (120, 144, 240)
new java.awt.Color(255, 0, 0), // 2: Red 0xFFFF0000, // 2: Red (255, 0, 0)
new java.awt.Color(255, 0, 255), // 3: Pink / Magenta 0xFFFF00FF, // 3: Pink / Magenta (255, 0, 255)
new java.awt.Color(0, 255, 0), // 4: Green 0xFF00FF00, // 4: Green (0, 255, 0)
new java.awt.Color(0, 255, 255), // 5: Turquoise / Cyan 0xFF00FFFF, // 5: Turquoise / Cyan (0, 255, 255)
new java.awt.Color(255, 255, 0), // 6: Yellow 0xFFFFFF00, // 6: Yellow (255, 255, 0)
new java.awt.Color(255, 255, 255), // 7: Neutral White 0xFFFFFFFF, // 7: Neutral White (255, 255, 255)
new java.awt.Color(0, 0, 0), // 8: Black 0xFF000000, // 8: Black (0, 0, 0)
new java.awt.Color(0, 0, 128), // 9: Deep Blue 0xFF000080, // 9: Deep Blue (0, 0, 128)
new java.awt.Color(128, 0, 0), // 10: Orange / Dark Red 0xFF800000, // 10: Orange / Dark Red (128, 0, 0)
new java.awt.Color(128, 0, 128), // 11: Purple 0xFF800080, // 11: Purple (128, 0, 128)
new java.awt.Color(0, 128, 0), // 12: Pale Green 0xFF008000, // 12: Pale Green (0, 128, 0)
new java.awt.Color(0, 128, 128), // 13: Pale Cyan 0xFF008080, // 13: Pale Cyan (0, 128, 128)
new java.awt.Color(215, 151, 0), // 14: Mustard 0xFFD79700, // 14: Mustard (215, 151, 0)
new java.awt.Color(192, 192, 192), // 15: Grey / Light White 0xFFC0C0C0, // 15: Grey / Light White (192, 192, 192)
new java.awt.Color(73, 36, 0) // 16: Brown 0xFF492400 // 16: Brown (73, 36, 0)
}; };
/**
* Retrieves the 32-bit ARGB color value for a given GOCA color index (0-16).
* Returns Green (0xFF00FF00) if the index is out of range.
*/
public static int getGocaColorArgb(int colorIndex) {
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
return GOCA_COLORS[colorIndex];
}
return GOCA_COLORS[0];
}
} }
@@ -1,10 +1,7 @@
package org.lib3270j.graphics; package org.lib3270j.graphics;
import java.awt.Color;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.lib3270j.charset.EbcdicTranslator; import org.lib3270j.charset.EbcdicTranslator;
@@ -22,14 +19,14 @@ public class GocaDecoder {
// Drawing state // Drawing state
private int curX = 0; private int curX = 0;
private int curY = 0; private int curY = 0;
private Color curColor = Color.GREEN; private int curColor = GocaConstants.GOCA_COLORS[0];
private int lineType = GocaConstants.LT_SOLID; private int lineType = GocaConstants.LT_SOLID;
private int lineWidth = GocaConstants.LW_NORMAL; private int lineWidth = GocaConstants.LW_NORMAL;
private int markerType = GocaConstants.MK_PLUS; private int markerType = GocaConstants.MK_PLUS;
private int markerSize = 5; private int markerSize = 5;
private Color markerColor = Color.GREEN; private int markerColor = GocaConstants.GOCA_COLORS[0];
private int pattern = GocaConstants.PT_SOLID; private int pattern = GocaConstants.PT_SOLID;
private Color fillColor = Color.GREEN; 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;
private int charWidth = 9; private int charWidth = 9;
@@ -781,8 +778,22 @@ public class GocaDecoder {
int code = data[pos + i] & 0xFF; int code = data[pos + i] & 0xFF;
int px = plane.mapX(startX); int px = plane.mapX(startX);
int py = plane.mapY(startY) - ch; int py = plane.mapY(startY) - ch;
boolean drawn = programSymbolManager.drawSymbol(plane.getGraphics(), charSet, code, px, py, cw, ch, curColor, null); ProgramSymbolSet.SymbolSlot slot = programSymbolManager.getSymbol(charSet, code);
if (!drawn) { if (slot != null) {
int[] rgb = slot.getRgbPixels(curColor, 0);
int symW = slot.getWidth();
int symH = slot.getHeight();
for (int dy = 0; dy < ch; dy++) {
int sy = (dy * symH) / ch;
for (int dx = 0; dx < cw; dx++) {
int sx = (dx * symW) / cw;
int pixelArgb = rgb[sy * symW + sx];
if ((pixelArgb >>> 24) != 0) {
plane.setPixel(px + dx, py + dy, pixelArgb);
}
}
}
} else {
char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]); char c = EbcdicTranslator.ebcdicToAscii(data[pos + i]);
plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle); plane.drawVectorText(px, py, String.valueOf(c), curColor, cw, ch, charDir, charAngle);
} }
@@ -810,10 +821,7 @@ public class GocaDecoder {
return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF)); return (short) (((data[off] & 0xFF) << 8) | (data[off + 1] & 0xFF));
} }
private Color getColor(int colorIndex) { private int getColor(int colorIndex) {
if (colorIndex >= 0 && colorIndex < GocaConstants.GOCA_COLORS.length) { return GocaConstants.getGocaColorArgb(colorIndex);
return GocaConstants.GOCA_COLORS[colorIndex];
}
return Color.GREEN;
} }
} }
@@ -1,20 +1,15 @@
package org.lib3270j.graphics; package org.lib3270j.graphics;
import java.awt.BasicStroke; import java.util.ArrayList;
import java.awt.Color; import java.util.Arrays;
import java.awt.Graphics2D; import java.util.Collections;
import java.awt.Polygon; import java.util.List;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* Offscreen rendering surface for GOCA vector graphics. * Offscreen rendering surface for GOCA vector graphics.
* Maintained as an ARGB BufferedImage that overlays the 3270 character cell matrix. * Maintained as an ARGB 32-bit integer pixel buffer that overlays the 3270 character cell matrix.
* Pure Java software rasterizer compatible with standard Java SE (Swing) and Android (Bitmap).
*/ */
public class GraphicsPlane { public class GraphicsPlane {
@@ -43,50 +38,45 @@ public class GraphicsPlane {
private int canvasWidth = 800; private int canvasWidth = 800;
private int canvasHeight = 600; private int canvasHeight = 600;
private BufferedImage image; private int[] rgbBuffer;
private Graphics2D g2d;
private boolean hasContent = false; private boolean hasContent = false;
private int screenCols = 80;
private int screenRows = 24;
public GraphicsPlane(int width, int height) { public GraphicsPlane(int width, int height) {
resize(width, height); this.canvasWidth = Math.max(1, width);
this.canvasHeight = Math.max(1, height);
this.rgbBuffer = new int[canvasWidth * canvasHeight];
} }
public synchronized void resize(int width, int height) { public synchronized void resize(int width, int height) {
int w = Math.max(1, width); int w = Math.max(1, width);
int h = Math.max(1, height); int h = Math.max(1, height);
if (w == canvasWidth && h == canvasHeight && image != null) { if (w == canvasWidth && h == canvasHeight && rgbBuffer != null) {
return; return;
} }
int[] newBuffer = new int[w * h];
if (rgbBuffer != null && hasContent) {
// Scale existing content to new dimensions using nearest-neighbor
for (int dy = 0; dy < h; dy++) {
int sy = (dy * canvasHeight) / h;
for (int dx = 0; dx < w; dx++) {
int sx = (dx * canvasWidth) / w;
newBuffer[dy * w + dx] = rgbBuffer[sy * canvasWidth + sx];
}
}
}
this.canvasWidth = w; this.canvasWidth = w;
this.canvasHeight = h; this.canvasHeight = h;
this.rgbBuffer = newBuffer;
BufferedImage newImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D newG2d = newImage.createGraphics();
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
newG2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
if (image != null && hasContent) {
newG2d.drawImage(image, 0, 0, w, h, null);
}
if (this.g2d != null) {
this.g2d.dispose();
}
this.image = newImage;
this.g2d = newG2d;
} }
public synchronized void clear() { public synchronized void clear() {
if (image != null && g2d != null) { if (rgbBuffer != null) {
BufferedImage newImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB); Arrays.fill(rgbBuffer, 0);
Graphics2D newG2d = newImage.createGraphics();
newG2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (this.g2d != null) {
this.g2d.dispose();
}
this.image = newImage;
this.g2d = newG2d;
} }
hasContent = false; hasContent = false;
} }
@@ -95,8 +85,8 @@ public class GraphicsPlane {
return hasContent; return hasContent;
} }
public synchronized BufferedImage getImage() { public synchronized int[] getRgbBuffer() {
return image; return rgbBuffer;
} }
public int getCanvasWidth() { public int getCanvasWidth() {
@@ -107,13 +97,6 @@ public class GraphicsPlane {
return canvasHeight; return canvasHeight;
} }
public synchronized Graphics2D getGraphics() {
return g2d;
}
private int screenCols = 80;
private int screenRows = 24;
public void setScreenDimensions(int cols, int rows) { public void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80; this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24; this.screenRows = rows > 0 ? rows : 24;
@@ -148,35 +131,114 @@ public class GraphicsPlane {
} }
/** /**
* Draws an absolute or relative line. * Safely plots a pixel at (x, y).
*/ */
public synchronized void drawLine(int x1, int y1, int x2, int y2, Color color, int lineType, int lineWidth) { public synchronized void setPixel(int x, int y, int colorArgb) {
if (g2d == null) return; if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
g2d.setColor(color != null ? color : Color.WHITE); rgbBuffer[y * canvasWidth + x] = colorArgb;
g2d.setStroke(createStroke(lineType, lineWidth)); hasContent = true;
g2d.drawLine(x1, y1, x2, y2); }
}
/**
* Draws an absolute or relative line using Bresenham's algorithm with line styles and widths.
*/
public synchronized void drawLine(int x1, int y1, int x2, int y2, int colorArgb, int lineType, int lineWidth) {
int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
int dx = Math.abs(x2 - x1);
int dy = Math.abs(y2 - y1);
int sx = x1 < x2 ? 1 : -1;
int sy = y1 < y2 ? 1 : -1;
int err = dx - dy;
int curX = x1;
int curY = y1;
int stepIndex = 0;
while (true) {
if (shouldPlotLinePixel(stepIndex, lineType)) {
drawPixelWithThickness(curX, curY, color, thickness);
}
stepIndex++;
if (curX == x2 && curY == y2) {
break;
}
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
curX += sx;
}
if (e2 < dx) {
err += dx;
curY += sy;
}
}
hasContent = true; hasContent = true;
} }
private boolean shouldPlotLinePixel(int step, int lineType) {
switch (lineType) {
case GocaConstants.LT_DOT:
return (step % 4) < 2;
case GocaConstants.LT_SHORTDASH:
return (step % 6) < 4;
case GocaConstants.LT_DASHDOT:
int m12 = step % 12;
return m12 < 6 || (m12 >= 8 && m12 < 10);
case GocaConstants.LT_DOUBLEDOT:
int m10 = step % 10;
return m10 < 2 || (m10 >= 4 && m10 < 6);
case GocaConstants.LT_LONGDASH:
return (step % 11) < 8;
case GocaConstants.LT_DASHDOUBLEDOT:
int m18 = step % 18;
return m18 < 8 || (m18 >= 10 && m18 < 12) || (m18 >= 14 && m18 < 16);
case GocaConstants.LT_SOLID:
case GocaConstants.LT_DEFAULT:
default:
return true;
}
}
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
if (thickness <= 1) {
setPixel(x, y, color);
} else {
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
setPixel(x + dx, y + dy, color);
}
}
}
}
/** /**
* Draws a full or partial arc / ellipse. * Draws a full or partial arc / ellipse.
*/ */
public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg, public synchronized void drawArc(int cx, int cy, int rx, int ry, double startAngleDeg, double sweepAngleDeg,
Color color, int lineType, int lineWidth, boolean isFull) { int colorArgb, int lineType, int lineWidth, boolean isFull) {
if (g2d == null) return; if (rx <= 0) rx = 1;
g2d.setColor(color != null ? color : Color.WHITE); if (ry <= 0) ry = 1;
g2d.setStroke(createStroke(lineType, lineWidth));
double x = cx - rx; int numSteps = Math.max(24, Math.max(rx, ry) * 4);
double y = cy - ry; double startRad = Math.toRadians(startAngleDeg);
double w = rx * 2.0; double sweepRad = isFull ? (2.0 * Math.PI) : Math.toRadians(sweepAngleDeg);
double h = ry * 2.0; double stepRad = sweepRad / numSteps;
if (isFull) { int prevX = (int) Math.round(cx + rx * Math.cos(startRad));
g2d.drawOval((int) Math.round(x), (int) Math.round(y), (int) Math.round(w), (int) Math.round(h)); int prevY = (int) Math.round(cy - ry * Math.sin(startRad));
} else {
Arc2D.Double arc = new Arc2D.Double(x, y, w, h, startAngleDeg, sweepAngleDeg, Arc2D.OPEN); for (int i = 1; i <= numSteps; i++) {
g2d.draw(arc); double angle = startRad + i * stepRad;
int nextX = (int) Math.round(cx + rx * Math.cos(angle));
int nextY = (int) Math.round(cy - ry * Math.sin(angle));
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
prevX = nextX;
prevY = nextY;
} }
hasContent = true; hasContent = true;
} }
@@ -184,53 +246,101 @@ public class GraphicsPlane {
/** /**
* Draws a Fillet (spline / curve approximation across control points). * Draws a Fillet (spline / curve approximation across control points).
*/ */
public synchronized void drawFillet(int[] px, int[] py, int numPoints, Color color, int lineType, int lineWidth) { public synchronized void drawFillet(int[] px, int[] py, int numPoints, int colorArgb, int lineType, int lineWidth) {
if (g2d == null || numPoints < 2) return; if (px == null || py == null || numPoints < 2) return;
g2d.setColor(color != null ? color : Color.WHITE);
g2d.setStroke(createStroke(lineType, lineWidth));
GeneralPath path = new GeneralPath();
path.moveTo(px[0], py[0]);
if (numPoints == 2) { if (numPoints == 2) {
path.lineTo(px[1], py[1]); drawLine(px[0], py[0], px[1], py[1], colorArgb, lineType, lineWidth);
} else { return;
for (int i = 1; i < numPoints - 1; i++) {
double midX = (px[i] + px[i + 1]) / 2.0;
double midY = (py[i] + py[i + 1]) / 2.0;
path.quadTo(px[i], py[i], midX, midY);
}
path.lineTo(px[numPoints - 1], py[numPoints - 1]);
} }
g2d.draw(path); int prevX = px[0];
int prevY = py[0];
for (int i = 0; i < numPoints - 1; i++) {
double p0x = (i == 0) ? px[0] : (px[i - 1] + px[i]) / 2.0;
double p0y = (i == 0) ? py[0] : (py[i - 1] + py[i]) / 2.0;
double p1x = px[i];
double p1y = py[i];
double p2x = (i == numPoints - 2) ? px[numPoints - 1] : (px[i] + px[i + 1]) / 2.0;
double p2y = (i == numPoints - 2) ? py[numPoints - 1] : (py[i] + py[i + 1]) / 2.0;
int steps = 20;
for (int s = 1; s <= steps; s++) {
double t = (double) s / steps;
double oneMinusT = 1.0 - t;
double bx = oneMinusT * oneMinusT * p0x + 2.0 * oneMinusT * t * p1x + t * t * p2x;
double by = oneMinusT * oneMinusT * p0y + 2.0 * oneMinusT * t * p1y + t * t * p2y;
int nextX = (int) Math.round(bx);
int nextY = (int) Math.round(by);
drawLine(prevX, prevY, nextX, nextY, colorArgb, lineType, lineWidth);
prevX = nextX;
prevY = nextY;
}
}
hasContent = true; hasContent = true;
} }
/** /**
* Fills a closed polygon area with a solid color or hatching pattern. * Fills a closed polygon area with a solid color or hatching pattern.
*/ */
public synchronized void fillArea(int[] px, int[] py, int numPoints, Color fillColor, int pattern, public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, Color boundaryColor, int lineType, int lineWidth) { boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) {
if (g2d == null || numPoints < 3) return; if (px == null || py == null || numPoints < 3) return;
Polygon poly = new Polygon(px, py, numPoints); int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { if (pattern != GocaConstants.PT_EMPTY) {
g2d.setColor(fillColor != null ? fillColor : Color.WHITE); // Find polygon vertical bounds
g2d.fill(poly); int minY = py[0];
} else if (pattern != GocaConstants.PT_EMPTY) { int maxY = py[0];
// Fill with pattern texture for (int i = 1; i < numPoints; i++) {
BufferedImage patImg = createPatternTexture(pattern, fillColor != null ? fillColor : Color.WHITE); if (py[i] < minY) minY = py[i];
java.awt.TexturePaint tp = new java.awt.TexturePaint(patImg, new java.awt.Rectangle(0, 0, 8, 8)); if (py[i] > maxY) maxY = py[i];
g2d.setPaint(tp); }
g2d.fill(poly); minY = Math.max(0, minY);
maxY = Math.min(canvasHeight - 1, maxY);
List<Integer> nodeX = new ArrayList<>();
byte[] patRows = (pattern >= 0 && pattern < PATTERN_DATA.length) ? PATTERN_DATA[pattern] : PATTERN_DATA[0];
for (int y = minY; y <= maxY; y++) {
nodeX.clear();
int j = numPoints - 1;
for (int i = 0; i < numPoints; i++) {
if ((py[i] < y && py[j] >= y) || (py[j] < y && py[i] >= y)) {
int x = px[i] + (int) Math.round((double) (y - py[i]) / (py[j] - py[i]) * (px[j] - px[i]));
nodeX.add(x);
}
j = i;
}
Collections.sort(nodeX);
for (int i = 0; i < nodeX.size(); i += 2) {
if (i + 1 >= nodeX.size()) break;
int leftX = Math.max(0, nodeX.get(i));
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) {
if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) {
setPixel(x, y, fill);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
}
}
}
}
}
} }
if (drawBoundary && boundaryColor != null) { if (drawBoundary && boundaryColorArgb != 0) {
g2d.setColor(boundaryColor); for (int i = 0; i < numPoints; i++) {
g2d.setStroke(createStroke(lineType, lineWidth)); int next = (i + 1) % numPoints;
g2d.draw(poly); drawLine(px[i], py[i], px[next], py[next], boundaryColorArgb, lineType, lineWidth);
}
} }
hasContent = true; hasContent = true;
} }
@@ -238,67 +348,92 @@ public class GraphicsPlane {
/** /**
* Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle). * Draws a GOCA marker symbol (+, x, diamond, square, star, dot, circle).
*/ */
public synchronized void drawMarker(int x, int y, int markerType, int size, Color color) { public synchronized void drawMarker(int x, int y, int markerType, int size, int colorArgb) {
if (g2d == null) return; int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
g2d.setColor(color != null ? color : Color.WHITE);
g2d.setStroke(new BasicStroke(1.5f));
int s = Math.max(3, size > 0 ? size : 5); int s = Math.max(3, size > 0 ? size : 5);
switch (markerType) { switch (markerType) {
case GocaConstants.MK_CROSS: // x case GocaConstants.MK_CROSS: // x
g2d.drawLine(x - s, y - s, x + s, y + s); drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x - s, y + s, x + s, y - s); drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_PLUS: // + case GocaConstants.MK_PLUS: // +
case GocaConstants.MK_DEFAULT: case GocaConstants.MK_DEFAULT:
g2d.drawLine(x - s, y, x + s, y); drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x, y - s, x, y + s); drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_DIAMOND: // <> case GocaConstants.MK_DIAMOND: // <>
g2d.drawPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); drawLine(x, y - s, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x, y + s, x - s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y, x, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_SQUARE: // [] case GocaConstants.MK_SQUARE: // []
g2d.drawRect(x - s, y - s, s * 2, s * 2); drawLine(x - s, y - s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x + s, y + s, x - s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
drawLine(x - s, y + s, x - s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_6STAR: // 6-point star case GocaConstants.MK_6STAR: // 6-point star
g2d.drawLine(x - s, y, x + s, y); drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x - s / 2, y - s, x + s / 2, y + s); drawLine(x - s / 2, y - s, x + s / 2, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x - s / 2, y + s, x + s / 2, y - s); drawLine(x - s / 2, y + s, x + s / 2, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_8STAR: // 8-point star case GocaConstants.MK_8STAR: // 8-point star
g2d.drawLine(x - s, y, x + s, y); drawLine(x - s, y, x + s, y, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x, y - s, x, y + s); drawLine(x, y - s, x, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x - s, y - s, x + s, y + s); drawLine(x - s, y - s, x + s, y + s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
g2d.drawLine(x - s, y + s, x + s, y - s); drawLine(x - s, y + s, x + s, y - s, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
break; break;
case GocaConstants.MK_SDIAMOND: // solid diamond case GocaConstants.MK_SDIAMOND: // solid diamond
g2d.fillPolygon(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4); fillArea(new int[]{x, x + s, x, x - s}, new int[]{y - s, y, y + s, y}, 4,
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break; break;
case GocaConstants.MK_SSQUARE: // solid square case GocaConstants.MK_SSQUARE: // solid square
g2d.fillRect(x - s, y - s, s * 2, s * 2); fillArea(new int[]{x - s, x + s, x + s, x - s}, new int[]{y - s, y - s, y + s, y + s}, 4,
color, GocaConstants.PT_SOLID, false, 0, 0, 0);
break; break;
case GocaConstants.MK_DOT: // dot case GocaConstants.MK_DOT: // dot
g2d.fillOval(x - 2, y - 2, 4, 4); for (int dy = -2; dy <= 2; dy++) {
for (int dx = -2; dx <= 2; dx++) {
if (dx * dx + dy * dy <= 4) {
setPixel(x + dx, y + dy, color);
}
}
}
break; break;
case GocaConstants.MK_CIRCLE: // circle case GocaConstants.MK_CIRCLE: // circle
g2d.drawOval(x - s, y - s, s * 2, s * 2);
break;
default: default:
g2d.drawOval(x - s, y - s, s * 2, s * 2); drawArc(x, y, s, s, 0.0, 360.0, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL, true);
break; break;
} }
hasContent = true; hasContent = true;
} }
private static final int[] VSS_OFFSETS = new int[256];
static {
Arrays.fill(VSS_OFFSETS, -1);
int sym = VectorSymbolData.VSS_SYMBOL_START; // 33
if (sym < 256) {
VSS_OFFSETS[sym] = 0;
}
for (int i = 0; i < VectorSymbolData.vss_data.length; i++) {
if (VectorSymbolData.vss_data[i] == VectorSymbolData.END_DEFAULT) { // 0xFF
sym++;
if (sym < 256 && i + 1 < VectorSymbolData.vss_data.length) {
VSS_OFFSETS[sym] = i + 1;
}
}
}
}
/** /**
* Draws stroked vector text using IBM Vector Symbol Set (VSS). * Draws stroked vector text using IBM Vector Symbol Set (VSS).
*/ */
public synchronized void drawVectorText(int x, int y, String text, Color color, public synchronized void drawVectorText(int x, int y, String text, int colorArgb,
int cellWidth, int cellHeight, int dir, double angle) { int cellWidth, int cellHeight, int dir, double angle) {
if (g2d == null || text == null || text.isEmpty()) return; if (text == null || text.isEmpty()) return;
g2d.setColor(color != null ? color : Color.WHITE); int color = (colorArgb != 0) ? colorArgb : 0xFFFFFFFF;
g2d.setStroke(new BasicStroke(1.2f));
int curX = x; int curX = x;
int curY = y; int curY = y;
@@ -307,7 +442,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, cw, ch); drawVssChar(curX, curY, c, color, cw, ch);
switch (dir) { switch (dir) {
case GocaConstants.CD_TB: curY += ch; break; case GocaConstants.CD_TB: curY += ch; break;
@@ -323,24 +458,54 @@ public class GraphicsPlane {
hasContent = true; hasContent = true;
} }
private void drawVssChar(int x, int y, char c, int cw, int ch) { private void drawVssChar(int x, int y, char c, int color, int cw, int ch) {
int idx = (int) c; int code = (int) c;
if (idx < VectorSymbolData.VSS_SYMBOL_START || idx > VectorSymbolData.VSS_SYMBOL_END) { if (code < VectorSymbolData.VSS_SYMBOL_START || code >= 256) {
// Draw simple bounding box or space
return; return;
} }
// Fallback vector stroke rendering int offset = VSS_OFFSETS[code];
g2d.drawString(String.valueOf(c), x, y + ch); if (offset < 0) {
return;
}
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) {
int prevVx = ((VectorSymbolData.vss_data[dataPtr] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 1] & 0xFF);
int prevVy = ((VectorSymbolData.vss_data[dataPtr + 2] & 0xFF) << 8) | (VectorSymbolData.vss_data[dataPtr + 3] & 0xFF);
int prevPx = x + (int) Math.round(((double) prevVx / VectorSymbolData.VSS_WIDTH) * cw);
int prevPy = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - prevVy) / VectorSymbolData.VSS_HEIGHT) * ch);
for (int p = 1; 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);
int px = x + (int) Math.round(((double) vx / VectorSymbolData.VSS_WIDTH) * cw);
int py = y + (int) Math.round(((double)(VectorSymbolData.VSS_HEIGHT - vy) / VectorSymbolData.VSS_HEIGHT) * ch);
drawLine(prevPx, prevPy, px, py, color, GocaConstants.LT_SOLID, GocaConstants.LW_NORMAL);
prevPx = px;
prevPy = py;
}
}
ptr += 2 + byteLen;
} else {
ptr++;
}
}
} }
/** /**
* Draws raw image pixel bitmap. * Draws raw image pixel bitmap.
*/ */
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, Color fgColor) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (g2d == null || imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF;
for (int row = 0; row < height; row++) { for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) { for (int col = 0; col < width; col++) {
@@ -349,52 +514,11 @@ public class GraphicsPlane {
if (byteIdx < imageData.length) { if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0;
if (bit) { if (bit) {
img.setRGB(col, row, fgRgb); setPixel(x + col, y + row, fgColor);
} }
} }
} }
} }
g2d.drawImage(img, x, y, null);
hasContent = true; hasContent = true;
} }
private Stroke createStroke(int lineType, int lineWidth) {
float width = (lineWidth == GocaConstants.LW_THICK) ? 2.5f : 1.2f;
switch (lineType) {
case GocaConstants.LT_DOT:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f}, 0.0f);
case GocaConstants.LT_SHORTDASH:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{4.0f, 2.0f}, 0.0f);
case GocaConstants.LT_DASHDOT:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{6.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
case GocaConstants.LT_DOUBLEDOT:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{2.0f, 2.0f, 2.0f, 4.0f}, 0.0f);
case GocaConstants.LT_LONGDASH:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 3.0f}, 0.0f);
case GocaConstants.LT_DASHDOUBLEDOT:
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{8.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f}, 0.0f);
case GocaConstants.LT_SOLID:
case GocaConstants.LT_DEFAULT:
default:
return new BasicStroke(width);
}
}
private BufferedImage createPatternTexture(int patternIndex, Color color) {
BufferedImage pat = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB);
byte[] patternRows = (patternIndex >= 0 && patternIndex < PATTERN_DATA.length) ? PATTERN_DATA[patternIndex] : PATTERN_DATA[0];
int rgb = color.getRGB();
for (int r = 0; r < 8; r++) {
int b = patternRows[r] & 0xFF;
for (int c = 0; c < 8; c++) {
if (((b >> (7 - c)) & 1) != 0) {
pat.setRGB(c, r, rgb);
}
}
}
return pat;
}
} }
@@ -1,7 +1,5 @@
package org.lib3270j.graphics; package org.lib3270j.graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.Arrays; import java.util.Arrays;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -65,21 +63,6 @@ public class ProgramSymbolManager {
return set.getSlot(index); return set.getSlot(index);
} }
/**
* Draws a programmed symbol character cell if defined.
* Returns true if symbol was drawn, false if not found.
*/
public synchronized boolean drawSymbol(Graphics2D g2d, int lcid, int codePoint,
int x, int y, int cellWidth, int cellHeight,
Color fgColor, Color bgColor) {
ProgramSymbolSet.SymbolSlot slot = getSymbol(lcid, codePoint);
if (slot != null) {
slot.draw(g2d, x, y, cellWidth, cellHeight, fgColor, bgColor);
return true;
}
return false;
}
/** /**
* Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload. * Processes a Load Programmed Symbols (LOADPS structured field 0x0F) payload.
*/ */
@@ -1,11 +1,5 @@
package org.lib3270j.graphics; package org.lib3270j.graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
/** /**
* Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE). * Represents a set of up to 191 custom bitmapped symbols (LCID 0x40 - 0xFE).
* Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite). * Supports both Single-Plane (monochrome) and Triple-Plane (7-color RGB composite).
@@ -67,7 +61,7 @@ public class ProgramSymbolSet {
private final int height; private final int height;
private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome) private final byte[] pixelData; // 1 byte per pixel: 0 = background, 1..7 = color index (or 1 for monochrome)
private final boolean isTriplePlane; private final boolean isTriplePlane;
private BufferedImage cachedImage; private int[] cachedRgbArray;
private int cachedFgRgb = -1; private int cachedFgRgb = -1;
private int cachedBgRgb = -1; private int cachedBgRgb = -1;
@@ -95,28 +89,25 @@ public class ProgramSymbolSet {
} }
/** /**
* Renders this symbol to a BufferedImage. * Computes and returns the 32-bit ARGB pixel array for this symbol.
* The returned array has length (width * height).
*/ */
public synchronized BufferedImage getImage(Color fgColor, Color bgColor) { public synchronized int[] getRgbPixels(int fgArgb, int bgArgb) {
int fgRgb = fgColor != null ? fgColor.getRGB() : 0xFFFFFFFF; if (cachedRgbArray != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
int bgRgb = bgColor != null ? bgColor.getRGB() : 0x00000000; return cachedRgbArray;
if (cachedImage != null && cachedFgRgb == fgRgb && cachedBgRgb == bgRgb) {
return cachedImage;
} }
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
int[] rgbArray = new int[width * height]; int[] rgbArray = new int[width * height];
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
int idx = y * width + x; int idx = y * width + x;
int val = (idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0; int val = (pixelData != null && idx < pixelData.length) ? (pixelData[idx] & 0xFF) : 0;
if (val == 0) { if (val == 0) {
rgbArray[idx] = bgRgb; rgbArray[idx] = bgArgb;
} else if (!isTriplePlane) { } else if (!isTriplePlane) {
rgbArray[idx] = fgRgb; rgbArray[idx] = fgArgb;
} else { } else {
// Triple-Plane RGB composite: // Triple-Plane RGB composite:
// val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue // val is bitmask: bit 0 (0x01) = Red, bit 1 (0x02) = Green, bit 2 (0x04) = Blue
@@ -128,21 +119,10 @@ public class ProgramSymbolSet {
} }
} }
img.setRGB(0, 0, width, height, rgbArray, 0, width); this.cachedRgbArray = rgbArray;
this.cachedImage = img; this.cachedFgRgb = fgArgb;
this.cachedFgRgb = fgRgb; this.cachedBgRgb = bgArgb;
this.cachedBgRgb = bgRgb; return rgbArray;
return img;
}
/**
* Draws the symbol directly onto a Graphics2D surface with aspect scaling.
*/
public void draw(Graphics2D g2d, int x, int y, int cellWidth, int cellHeight, Color fgColor, Color bgColor) {
BufferedImage img = getImage(fgColor, bgColor);
if (img != null) {
g2d.drawImage(img, x, y, cellWidth, cellHeight, null);
}
} }
} }
} }
@@ -401,7 +401,7 @@ public class TelnetFSM {
out.write(IAC); out.write(IAC);
out.write(SE); out.write(SE);
sendBytes(out.toByteArray()); sendBytes(out.toByteArray());
log.info("SENT SB TTYPE IS " + termType + " SE"); log.warning(">>> SENT SB TTYPE IS " + termType + " SE");
} }
} }
@@ -475,7 +475,7 @@ public class TelnetFSM {
out.write(IAC); out.write(IAC);
out.write(SE); out.write(SE);
sendBytes(out.toByteArray()); sendBytes(out.toByteArray());
log.info("SENT SB TN3270E DEVICE-TYPE REQUEST " + termType + log.warning(">>> SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE"); (config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
} }
@@ -54,6 +54,52 @@ public class QueryReplyBuilderTest {
assertTrue(hasB4); assertTrue(hasB4);
} }
@Test
public void testBuildAllQueryRepliesWithBoth() {
qrBuilder.setGraphicsMode(GraphicsMode.BOTH);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Vector Graphics (0xB0) must be present and Charsets must have LoadPS (0x0A)
boolean hasB0 = false;
boolean hasCharsetsWithLoadPs = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
hasCharsetsWithLoadPs = true;
}
}
}
assertTrue(hasB0);
assertTrue(hasCharsetsWithLoadPs);
}
@Test
public void testBuildAllQueryRepliesWithProgrammedSymbols() {
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
assertNotNull(replies);
// Charsets with LoadPS (0x0A) must be present, and QR_GRAPHICS must NOT be present
boolean hasB0 = false;
boolean hasCharsetsWithLoadPs = false;
for (int i = 0; i < replies.length - 3; i++) {
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
hasB0 = true;
}
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
if (i + 6 < replies.length && (replies[i + 6] & 0xFF) == 0x0A) {
hasCharsetsWithLoadPs = true;
}
}
}
assertFalse(hasB0);
assertTrue(hasCharsetsWithLoadPs);
}
@Test @Test
public void testBuildFilteredQueryRepliesWithSupportedCodes() { public void testBuildFilteredQueryRepliesWithSupportedCodes() {
byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA }; byte[] requested = new byte[] { (byte) QR_DDM, (byte) QR_USABLE_AREA };
@@ -1,9 +1,6 @@
package org.lib3270j.graphics; package org.lib3270j.graphics;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
public class ProgramSymbolManagerTest { public class ProgramSymbolManagerTest {
@@ -62,12 +59,13 @@ public class ProgramSymbolManagerTest {
assertNotNull(pixels); assertNotNull(pixels);
assertEquals(1, pixels[0]); // Row 0 Col 0 is 1 assertEquals(1, pixels[0]); // Row 0 Col 0 is 1
// Test rendering into BufferedImage // Test rendering into 32-bit ARGB pixels
BufferedImage canvas = new BufferedImage(80, 24, BufferedImage.TYPE_INT_ARGB); int fg = 0xFF00FF00; // Green
Graphics2D g2 = canvas.createGraphics(); int bg = 0xFF000000; // Black
boolean drawn = manager.drawSymbol(g2, 0x40, 0x41, 0, 0, 10, 20, Color.GREEN, Color.BLACK); int[] rgb = slot.getRgbPixels(fg, bg);
g2.dispose(); assertNotNull(rgb);
assertTrue(drawn); assertEquals(9 * 16, rgb.length);
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
} }
@Test @Test
@@ -104,16 +102,18 @@ public class ProgramSymbolManagerTest {
byte[] pixels = slot.getPixelData(); byte[] pixels = slot.getPixelData();
assertEquals(7, pixels[0] & 0xFF); assertEquals(7, pixels[0] & 0xFF);
// Render image // Render ARGB pixels
BufferedImage img = slot.getImage(Color.WHITE, Color.BLACK); int[] rgb = slot.getRgbPixels(0xFFFFFFFF, 0xFF000000);
assertNotNull(img); assertNotNull(rgb);
// Pixel (0,0) should be white composite (Red=255, Green=255, Blue=255) // Pixel (0,0) should be white composite (0xFFFFFFFF)
int rgb = img.getRGB(0, 0); int pixelArgb = rgb[0];
Color pixelColor = new Color(rgb, true); int r = (pixelArgb >> 16) & 0xFF;
assertEquals(255, pixelColor.getRed()); int g = (pixelArgb >> 8) & 0xFF;
assertEquals(255, pixelColor.getGreen()); int b = pixelArgb & 0xFF;
assertEquals(255, pixelColor.getBlue()); assertEquals(255, r);
assertEquals(255, g);
assertEquals(255, b);
} }
@Test @Test