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
@@ -1288,6 +1288,9 @@ public class DataStreamProcessor {
byte[] fullStream = gocaAccumulator.toByteArray();
gocaAccumulator.reset();
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) {
gocaDecoder.processProcedureOrders(fullStream, 0, fullStream.length);
} 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.
*/
@@ -270,16 +293,11 @@ public class FillArea {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
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);
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 maxY = Double.MIN_VALUE;
@@ -340,18 +358,18 @@ public class FillArea {
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plane.setPixel(x, y, bg);
plotFillPixel(plane, x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
plotFillPixel(plane, x, y, fill, isMixOr);
} 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;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
plane.setPixel(x, y, fill);
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (isOpaqueBg) {
plane.setPixel(x, y, bg);
plotFillPixel(plane, x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
plane.setPixel(x, y, fill);
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
plotFillPixel(plane, x, y, fill, isMixOr);
} else if (patRows != null) {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
plane.setPixel(x, y, fill);
plotFillPixel(plane, x, y, fill, isMixOr);
} 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 curY = 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 bgColor = GocaConstants.GOCA_COLORS[8]; // Black
private int lineType = GocaConstants.LT_SOLID;
@@ -35,8 +36,8 @@ public class GocaDecoder {
private double charAngle = 0.0;
private double charShear = 0.0;
private double fractionalLineWidth = 1.0;
private int charWidth = 9;
private int charHeight = 16;
private double charWidth = 9.0;
private double charHeight = 16.0;
private int charSet = 0;
private int charPrecision = GocaConstants.CP_STRING;
private int arcParamP = 1;
@@ -48,6 +49,14 @@ public class GocaDecoder {
private boolean segDynamic = false;
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;
// Area accumulation
@@ -201,6 +210,10 @@ public class GocaDecoder {
return sb != null ? sb.tag : 0;
}
public synchronized int getCurrentSegId() {
return currentSegId;
}
private void trackPoint(int x, int y) {
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
@@ -277,6 +290,10 @@ public class GocaDecoder {
flags, segChained, segDynamic, segVisible));
}
public synchronized int getFgMix() {
return fgMix;
}
/**
* Processes GOCA order 0x11 Fractional Line Width calculation.
*/
@@ -303,15 +320,25 @@ public class GocaDecoder {
segmentBoundsMap.clear();
activeSegmentsInOrder.clear();
currentSegId = 0;
defColorIndex = 0;
defFmix = GocaConstants.MIX_DEFAULT;
defLineType = GocaConstants.LT_SOLID;
defLineWidth = GocaConstants.LW_NORMAL;
defPattern = GocaConstants.PT_SOLID;
defPatternSet = 0;
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)
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL;
lineType = defLineType;
lineWidth = defLineWidth;
fractionalLineWidth = 1.0;
if (plane != null) {
plane.setFractionalLineWidth(1.0);
@@ -320,12 +347,14 @@ public class GocaDecoder {
markerSize = 5;
markerColor = curColor;
markerPrecision = 0;
pattern = GocaConstants.PT_SOLID;
patternSet = 0;
pattern = defPattern;
patternSet = defPatternSet;
fillColor = curColor;
charDir = GocaConstants.CD_LR;
charAngle = 0.0;
charShear = 0.0;
charWidth = 9.0;
charHeight = 16.0;
charSet = 0;
charPrecision = GocaConstants.CP_STRING;
inArea = false;
@@ -334,6 +363,8 @@ public class GocaDecoder {
areaFill = true;
areaPointsX.clear();
areaPointsY.clear();
areaPolygons.clear();
currentPolyPts = 0;
inImage = false;
imgBitDepth = GocaConstants.BPP_1;
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.)
* 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;
if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) {
return 1;
@@ -374,20 +405,27 @@ public class GocaDecoder {
if (idx + 1 >= end) {
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) {
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.)
if (order < 0x20) {
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 ||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMP ||
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) {
return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5;
@@ -688,7 +726,14 @@ public class GocaDecoder {
break;
}
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);
charHeight = readCoord(inputData, idx + 4);
}
@@ -739,17 +784,26 @@ public class GocaDecoder {
idx += orderLen;
break;
}
case 0x04: // HODSegmentCharacteristics (2-byte NOP per HoD)
case 0x05:
case 0x06:
case 0x12: {
idx += orderLen;
break;
}
case GocaConstants.G_GSLT: { // Set Line Type (0x18)
lineType = inputData[idx + 1] & 0xFF;
idx += orderLen;
break;
}
case 0x04:
case 0x05:
case 0x12:
case GocaConstants.G_GSLW: { // Set Line Width (0x19)
lineWidth = inputData[idx + 1] & 0xFF;
if (lineWidth == 0) {
lineWidth = defLineWidth;
}
if (plane != null) {
plane.setLineWidth(lineWidth);
}
idx += orderLen;
break;
}
@@ -803,7 +857,15 @@ public class GocaDecoder {
idx += orderLen;
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_GPOP: {
idx += orderLen;
@@ -816,9 +878,10 @@ public class GocaDecoder {
break;
}
case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
boolean drawBoundary = (flags & 0x80) != 0;
int fillRule = (flags & 0x40) != 0 ? GocaConstants.FILL_RULE_WINDING : GocaConstants.FILL_RULE_EVEN_ODD;
int flags = inputData[idx + 1] & 0xFF;
// IBM bit numbering: Bit 1 (0x40) is the boundary flag per IBM Host On-Demand HODDecoder:2229
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));
beginArea(drawBoundary, fillRule);
idx += orderLen;
@@ -1017,13 +1080,9 @@ public class GocaDecoder {
break;
}
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) {
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;
} else {
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) {
beginArea(drawBoundary, GocaConstants.FILL_RULE_EVEN_ODD);
}
@@ -1488,11 +1604,11 @@ public class GocaDecoder {
if (textLen <= 0) return;
// IBM 3179G vector graphics base cell is 9x16
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
double cw = charWidth > 0 ? (charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? (charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
int cellW = (charWidth > 0 ? charWidth : 9);
int cellH = (charHeight > 0 ? charHeight : 16);
int cellW = (int) Math.round(charWidth > 0 ? charWidth : 9.0);
int cellH = (int) Math.round(charHeight > 0 ? charHeight : 16.0);
switch (charDir) {
case GocaConstants.CD_TB:
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;
curY = startY;
@@ -1565,20 +1681,20 @@ public class GocaDecoder {
switch (charDir) {
case GocaConstants.CD_TB:
curX = startX;
curY = startY - (textLen * (charHeight > 0 ? charHeight : 16));
curY = startY - (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
break;
case GocaConstants.CD_RL:
curX = startX - (textLen * (charWidth > 0 ? charWidth : 9));
curX = startX - (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
curY = startY;
break;
case GocaConstants.CD_BT:
curX = startX;
curY = startY + (textLen * (charHeight > 0 ? charHeight : 16));
curY = startY + (int) Math.round(textLen * (charHeight > 0 ? charHeight : 16.0));
break;
case GocaConstants.CD_LR:
case GocaConstants.CD_DEFAULT:
default:
curX = startX + (textLen * (charWidth > 0 ? charWidth : 9));
curX = startX + (int) Math.round(textLen * (charWidth > 0 ? charWidth : 9.0));
curY = startY;
break;
}
@@ -1,8 +1,19 @@
package haus.nightmare.lib3270j.graphics;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
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.Arrays;
import java.util.Collections;
@@ -100,12 +111,20 @@ public class GraphicsPlane {
drawLine((double) x1, (double) y1, (double) x2, (double) y2, currentColorArgb, currentLineType, currentLineWidth);
}
private BufferedImage canvasImage;
public synchronized BufferedImage toBufferedImage() {
BufferedImage img = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
if (rgbBuffer != null) {
img.setRGB(0, 0, canvasWidth, canvasHeight, rgbBuffer, 0, canvasWidth);
if (canvasImage == null && rgbBuffer != null && canvasWidth > 0 && canvasHeight > 0) {
DataBufferInt db = new DataBufferInt(rgbBuffer, rgbBuffer.length);
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() {
@@ -211,6 +230,7 @@ public class GraphicsPlane {
this.canvasWidth = w;
this.canvasHeight = h;
this.rgbBuffer = newBuffer;
this.canvasImage = null;
updateViewingWindowPixels();
}
@@ -218,6 +238,7 @@ public class GraphicsPlane {
if (rgbBuffer != null) {
Arrays.fill(rgbBuffer, 0);
}
this.currentMixMode = 0;
hasContent = false;
updateCount++;
}
@@ -426,8 +447,28 @@ public class GraphicsPlane {
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) {
if (viewingWindowActive) {
@@ -436,14 +477,26 @@ public class GraphicsPlane {
}
}
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;
if (srcA == 0) return;
int idx = y * canvasWidth + x;
if (srcA == 255) {
rgbBuffer[idx] = colorArgb;
} else {
int dst = rgbBuffer[idx];
int dstA = (dst >>> 24) & 0xFF;
if (dstA == 0) {
rgbBuffer[idx] = colorArgb;
} 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) {
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);
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
drawStyledLine((int) Math.round(x0), (int) Math.round(y0),
(int) Math.round(x1), (int) Math.round(y1),
(0xFF << 24) | color, lineType, lineWidth);
return;
}
int thickness = (lineWidth == GocaConstants.LW_THICK) ? 2 : 1;
// 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 {
plotPixelWu(xpxl1, ypxl1, color, (1.0 - (yend - Math.floor(yend))) * xgap, lineWidth);
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;
if (thickness <= 1) {
if (lineType != GocaConstants.LT_SOLID && lineType != GocaConstants.LT_DEFAULT) {
drawStyledLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, lineType, 1);
} else {
drawLineBresenham(ix0, iy0, ix1, iy1, (0xFF << 24) | color);
}
} else {
for (int x = xpxl1 + 1; x < xpxl2; x++) {
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;
}
drawHodLine(ix0, iy0, ix1, iy1, (0xFF << 24) | color, thickness, lineType);
}
hasContent = true;
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.
*/
@@ -657,9 +693,15 @@ public class GraphicsPlane {
private void drawPixelWithThickness(int x, int y, int color, int thickness) {
if (thickness <= 1) {
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 {
for (int dy = -(thickness - 1); dy <= (thickness - 1); dy++) {
for (int dx = -(thickness - 1); dx <= (thickness - 1); dx++) {
int r = thickness / 2;
for (int dy = -r; dy <= r; dy++) {
for (int dx = -r; dx <= r; dx++) {
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).
*/
@@ -807,19 +875,23 @@ public class GraphicsPlane {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
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);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
int minY = py[0];
int maxY = py[0];
for (int i = 1; i < numPoints; i++) {
if (py[i] < minY) minY = py[i];
if (py[i] > maxY) maxY = py[i];
}
int minY = py[0];
int maxY = py[0];
int minX = px[0];
int maxX = px[0];
for (int i = 1; i < numPoints; i++) {
if (py[i] < minY) minY = 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);
maxY = Math.min(canvasHeight - 1, maxY);
@@ -881,18 +953,18 @@ public class GraphicsPlane {
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) {
setPixel(x, y, bg);
setPixelInFill(x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
setPixelInFill(x, y, fill, isMixOr);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) {
setPixel(x, y, bg);
setPixelInFill(x, y, bg, isMixOr);
}
}
}
@@ -914,18 +986,18 @@ public class GraphicsPlane {
int pIdx = psY * psW + psX;
boolean bit = (psPix != null && pIdx < psPix.length && psPix[pIdx] != 0);
if (bit) {
setPixel(x, y, fill);
setPixelInFill(x, y, fill, isMixOr);
} else if (isOpaqueBg) {
setPixel(x, y, bg);
setPixelInFill(x, y, bg, isMixOr);
}
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16) {
setPixel(x, y, fill);
} else if (pattern == GocaConstants.PT_SOLID || pattern == 16 || pattern == 0) {
setPixelInFill(x, y, fill, isMixOr);
} else {
int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill);
setPixelInFill(x, y, fill, isMixOr);
} 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++) {
drawLine((double) px[offset + i], (double) py[offset + i],
(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;
@@ -1085,7 +1162,10 @@ public class GraphicsPlane {
double curX = x;
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 sinA = Math.sin(radAngle);
@@ -1099,7 +1179,7 @@ public class GraphicsPlane {
for (int i = 0; i < text.length(); 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) {
curX += cw * cosA;
@@ -1150,6 +1230,59 @@ public class GraphicsPlane {
double sinA = Math.sin(radAngle);
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;
while (ptr < VectorSymbolData.vss_data.length && VectorSymbolData.vss_data[ptr] != VectorSymbolData.END_DEFAULT) {
int order = VectorSymbolData.vss_data[ptr] & 0xFF;
@@ -1161,9 +1294,6 @@ public class GraphicsPlane {
if (numPoints >= 2) {
double[] px = 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++) {
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);
@@ -1179,19 +1309,6 @@ public class GraphicsPlane {
px[p] = x + rx;
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++) {