This commit is contained in:
@@ -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++) {
|
||||
|
||||
@@ -96,4 +96,143 @@ public class FillAreaTest {
|
||||
assertTrue(redCount > 0, "Expected pattern foreground red pixels");
|
||||
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());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
boolean allCrispOpaque = true;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int y = 0; y < 100; y++) {
|
||||
@@ -371,15 +371,15 @@ public class GocaDecoderTest {
|
||||
int alpha = (pixel >>> 24) & 0xFF;
|
||||
int r = (pixel >>> 16) & 0xFF;
|
||||
assertEquals(255, r, "Red channel must be preserved");
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
if (alpha != 255) {
|
||||
allCrispOpaque = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -392,21 +392,21 @@ public class GocaDecoderTest {
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
boolean allCrispOpaque = true;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
if (alpha != 255) {
|
||||
allCrispOpaque = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -419,21 +419,21 @@ public class GocaDecoderTest {
|
||||
assertTrue(plane.hasContent());
|
||||
|
||||
int[] buffer = plane.getRgbBuffer();
|
||||
boolean hasIntermediateAlpha = false;
|
||||
boolean allCrispOpaque = true;
|
||||
int nonZeroPixels = 0;
|
||||
|
||||
for (int p : buffer) {
|
||||
if (p != 0) {
|
||||
nonZeroPixels++;
|
||||
int alpha = (p >>> 24) & 0xFF;
|
||||
if (alpha > 0 && alpha < 255) {
|
||||
hasIntermediateAlpha = true;
|
||||
if (alpha != 255) {
|
||||
allCrispOpaque = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -748,10 +748,11 @@ public class GocaDecoderTest {
|
||||
out.write(0x00); out.write(20); out.write(0x00); out.write(40);
|
||||
out.write(GocaConstants.G_GEAR);
|
||||
|
||||
// 4. Draw White slide boundary frame (Color set to 8/Black before GBAR, 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_GSPT); out.write(0x0F); // Empty pattern (transparent interior)
|
||||
out.write(GocaConstants.G_GBAR); out.write(0x80);
|
||||
out.write(GocaConstants.G_GSPT); out.write(0x10); // Solid pattern (16)
|
||||
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_GSLT); out.write(GocaConstants.LT_DOT); // Dotted line
|
||||
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");
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -159,13 +159,13 @@ public class Phase1ColorCalibrationTest {
|
||||
public void testItem1_4_FillAreaBackgroundMix() {
|
||||
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);
|
||||
fillAreaTrans.addEdge(0, 0, 10, 0);
|
||||
fillAreaTrans.addEdge(10, 0, 10, 10);
|
||||
fillAreaTrans.addEdge(10, 10, 0, 10);
|
||||
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);
|
||||
|
||||
// Verify plane pixels remain unpainted (transparent / 0)
|
||||
|
||||
Reference in New Issue
Block a user