This commit is contained in:
@@ -125,8 +125,8 @@ public class QueryReplyBuilder {
|
||||
// Vector Graphics QRs if enabled
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); // 0xA8
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics()); // 0xB0
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage()); // 0xB1
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows)); // 0xB0
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows)); // 0xB1
|
||||
appendQueryReply(out, QR_AUX_DEV, buildAuxDev()); // 0xB2
|
||||
appendOemFmt(out); // 0xB3
|
||||
appendQueryReply(out, QR_GCOLOR, buildGColor()); // 0xB4
|
||||
@@ -204,14 +204,14 @@ public class QueryReplyBuilder {
|
||||
break;
|
||||
case QR_GRAPHICS:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics());
|
||||
appendQueryReply(out, QR_GRAPHICS, buildGraphics(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
break;
|
||||
case QR_GIMAGE:
|
||||
if (graphicsMode.isVectorGraphicsEnabled()) {
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage());
|
||||
appendQueryReply(out, QR_GIMAGE, buildGImage(maxCols, maxRows));
|
||||
} else {
|
||||
appendQueryReply(out, QR_NULL, new byte[0]);
|
||||
}
|
||||
@@ -422,13 +422,25 @@ public class QueryReplyBuilder {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] buildGraphics() {
|
||||
return new byte[]{ (byte) 0x80, 0x02, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00 };
|
||||
private byte[] buildGraphics(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
(byte) 0x80, 0x02,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] buildGImage() {
|
||||
private byte[] buildGImage(int maxCols, int maxRows) {
|
||||
int width = maxCols * 9;
|
||||
int height = maxRows * 12;
|
||||
return new byte[]{
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xFC, 0x00,
|
||||
0x00, 0x01,
|
||||
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF),
|
||||
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF),
|
||||
0x00,
|
||||
0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
|
||||
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ public final class GocaConstants {
|
||||
public static final int G_GSMCEL = 0x37; // Set Marker Cell
|
||||
public static final int G_GSCS = 0x38; // Set Character Set
|
||||
public static final int G_GSMP = 0x39; // Set Marker Precision
|
||||
public static final int G_GSETAG = 0x39; // Set Pick Identifier / Tag
|
||||
public static final int G_GSCD = 0x3A; // Set Character Direction
|
||||
public static final int G_GSCC = 0x3B; // Set Character Precision
|
||||
public static final int G_GSMS_SET = 0x3C; // Set Marker Set
|
||||
|
||||
@@ -63,6 +63,39 @@ public class GocaDecoder {
|
||||
private final java.util.Set<Integer> chainedTargets = new java.util.HashSet<>();
|
||||
private int callDepth = 0;
|
||||
|
||||
/**
|
||||
* Bounding box and pick correlation tracking for GOCA segments.
|
||||
*/
|
||||
public static class SegmentBounds {
|
||||
public final int segId;
|
||||
public int minX = Integer.MAX_VALUE;
|
||||
public int maxX = Integer.MIN_VALUE;
|
||||
public int minY = Integer.MAX_VALUE;
|
||||
public int maxY = Integer.MIN_VALUE;
|
||||
public int tag = 0;
|
||||
|
||||
public SegmentBounds(int segId) {
|
||||
this.segId = segId;
|
||||
}
|
||||
|
||||
public void include(int x, int y) {
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
|
||||
public boolean contains(int x, int y, int margin) {
|
||||
if (minX == Integer.MAX_VALUE) return false;
|
||||
return x >= minX - margin && x <= maxX + margin &&
|
||||
y >= minY - margin && y <= maxY + margin;
|
||||
}
|
||||
}
|
||||
|
||||
private final java.util.Map<Integer, SegmentBounds> segmentBoundsMap = new java.util.HashMap<>();
|
||||
private final java.util.List<SegmentBounds> activeSegmentsInOrder = new java.util.ArrayList<>();
|
||||
private int currentSegId = 0;
|
||||
|
||||
// Graphic Cursor (Light-Pen) state
|
||||
private boolean graphicsCursorActive = false;
|
||||
private int graphicCursorX = 0;
|
||||
@@ -88,6 +121,10 @@ public class GocaDecoder {
|
||||
return curY;
|
||||
}
|
||||
|
||||
public int getCharSet() {
|
||||
return charSet;
|
||||
}
|
||||
|
||||
public synchronized boolean isGraphicsCursorActive() {
|
||||
return graphicsCursorActive;
|
||||
}
|
||||
@@ -116,6 +153,35 @@ public class GocaDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized int findPickedSegment(int gx, int gy) {
|
||||
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
|
||||
SegmentBounds sb = activeSegmentsInOrder.get(i);
|
||||
if (sb.contains(gx, gy, 12)) {
|
||||
System.err.println(String.format(
|
||||
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||
));
|
||||
return sb.segId;
|
||||
}
|
||||
}
|
||||
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public synchronized int getSegmentTag(int segId) {
|
||||
SegmentBounds sb = segmentBoundsMap.get(segId);
|
||||
return sb != null ? sb.tag : 0;
|
||||
}
|
||||
|
||||
private void trackPoint(int x, int y) {
|
||||
if (currentSegId != 0) {
|
||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||
if (sb != null) {
|
||||
sb.include(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void resetDefaults() {
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
@@ -126,6 +192,9 @@ public class GocaDecoder {
|
||||
segmentChainMap.clear();
|
||||
segmentOrderList.clear();
|
||||
chainedTargets.clear();
|
||||
segmentBoundsMap.clear();
|
||||
activeSegmentsInOrder.clear();
|
||||
currentSegId = 0;
|
||||
resetAttributes();
|
||||
}
|
||||
|
||||
@@ -154,6 +223,19 @@ public class GocaDecoder {
|
||||
|
||||
private byte[] partialOrderBuffer = new byte[0];
|
||||
|
||||
/**
|
||||
* Determines the total byte length of a GOCA drawing order starting at data[idx].
|
||||
*
|
||||
* IMPORTANT ARCHITECTURE NOTE:
|
||||
* GOCA orders follow IBM GA23-0059 architecture rules:
|
||||
* 1. 1-byte standalone orders (NOP, etc.) -> length 1.
|
||||
* 2. Delimiter orders (GEAR, ENDSEGM, ENDPROLOGUE, GEIMG) -> 1 or 2 bytes (with 0x00 trailing byte).
|
||||
* 3. Fixed 1-byte immediate operand orders (0x00..0x1F range: GSCOL, GSLT, GSLW, GSMS, GSMC, GSPS, GSBMX) -> 2 bytes.
|
||||
* 4. Orders with opcode >= 0x20 (including GCALL 0x2A, GSCS 0x38, GSCD 0x3A, GSPT 0x28, GSMT 0x29, GBAR 0x68,
|
||||
* GLINE 0xC1, GARC 0xC6, GCHST 0xC3, etc.) are self-defining with a 1-byte length field data[idx+1],
|
||||
* making total length = payloadLen + 2.
|
||||
* Never hardcode orders >= 0x20 to 2 bytes, as that desynchronizes the GOCA order stream.
|
||||
*/
|
||||
private int getOrderLength(byte[] data, int idx, int end) {
|
||||
int order = data[idx] & 0xFF;
|
||||
if (order == GocaConstants.G_NOP1 || order == 0xFF) {
|
||||
@@ -164,23 +246,26 @@ public class GocaDecoder {
|
||||
order == GocaConstants.G_GEIMG || order == GocaConstants.G_GPOP) {
|
||||
return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1;
|
||||
}
|
||||
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
||||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
||||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
|
||||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
|
||||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
|
||||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSMCEL ||
|
||||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMP ||
|
||||
order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCC ||
|
||||
order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
|
||||
return 2;
|
||||
}
|
||||
if (order == GocaConstants.G_GCALL) { // Call Segment (0x2A <32-bit segment ID>)
|
||||
return 5;
|
||||
}
|
||||
if (idx + 1 >= end) {
|
||||
return -1;
|
||||
}
|
||||
// Fixed 2-byte orders: 1-byte opcode + 1-byte operand
|
||||
if (order == 0x04 || order == GocaConstants.G_GSMC || order == GocaConstants.G_GSPS ||
|
||||
order == GocaConstants.G_GSCOL || order == GocaConstants.G_GSMX ||
|
||||
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSLT ||
|
||||
order == GocaConstants.G_GSLW || order == GocaConstants.G_GSMS) {
|
||||
return 2;
|
||||
}
|
||||
// Flexible 1-byte attribute orders (support both short 2-byte or long 3-byte if len byte == 1)
|
||||
if (order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMT ||
|
||||
order == GocaConstants.G_GSCS || order == GocaConstants.G_GSCD ||
|
||||
order == GocaConstants.G_GBAR) {
|
||||
return (data[idx + 1] == 0x01 && idx + 2 < end) ? 3 : 2;
|
||||
}
|
||||
if (order == GocaConstants.G_GCALL) {
|
||||
return (data[idx + 1] == 0x04 && idx + 5 < end) ? 6 : 5;
|
||||
}
|
||||
// Self-defining orders with 1-byte length field: order (1) + length byte (1) + payload (len)
|
||||
return (data[idx + 1] & 0xFF) + 2;
|
||||
}
|
||||
|
||||
@@ -303,6 +388,16 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70)
|
||||
if (idx + 5 < end) {
|
||||
int segId = ((inputData[idx + 2] & 0xFF) << 24) |
|
||||
((inputData[idx + 3] & 0xFF) << 16) |
|
||||
((inputData[idx + 4] & 0xFF) << 8) |
|
||||
(inputData[idx + 5] & 0xFF);
|
||||
currentSegId = segId;
|
||||
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
|
||||
activeSegmentsInOrder.remove(sb);
|
||||
activeSegmentsInOrder.add(sb);
|
||||
}
|
||||
if (idx + 7 < end && (inputData[idx + 7] & 0x06) == 0) {
|
||||
resetAttributes();
|
||||
}
|
||||
@@ -310,6 +405,27 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_ENDSEGM: { // End Segment (0x71)
|
||||
if (currentSegId != 0) {
|
||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||
if (sb != null) {
|
||||
System.err.println(String.format(
|
||||
"GocaDecoder: ENDSEGM segId=%d bounds=[%d..%d, %d..%d] tag=%d",
|
||||
sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
|
||||
));
|
||||
}
|
||||
}
|
||||
currentSegId = 0;
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSETAG: { // Set Pick Identifier / Tag (0x39)
|
||||
if (currentSegId != 0 && payloadLen >= 2 && idx + 3 < end) {
|
||||
int tag = ((inputData[idx + 2] & 0xFF) << 8) | (inputData[idx + 3] & 0xFF);
|
||||
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
|
||||
if (sb != null) {
|
||||
sb.tag = tag;
|
||||
}
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -344,16 +460,34 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCALL: { // Call Segment (0x2A)
|
||||
if (callDepth < 16 && idx + 4 < end) {
|
||||
int targetSegId = ((inputData[idx + 1] & 0xFF) << 24) |
|
||||
if (callDepth < 16) {
|
||||
int targetSegId;
|
||||
if (orderLen == 6 && idx + 5 < end) {
|
||||
targetSegId = ((inputData[idx + 2] & 0xFF) << 24) |
|
||||
((inputData[idx + 3] & 0xFF) << 16) |
|
||||
((inputData[idx + 4] & 0xFF) << 8) |
|
||||
(inputData[idx + 5] & 0xFF);
|
||||
} else if (idx + 4 < end) {
|
||||
targetSegId = ((inputData[idx + 1] & 0xFF) << 24) |
|
||||
((inputData[idx + 2] & 0xFF) << 16) |
|
||||
((inputData[idx + 3] & 0xFF) << 8) |
|
||||
(inputData[idx + 4] & 0xFF);
|
||||
byte[] targetSeg = segmentStore.get(targetSegId);
|
||||
if (targetSeg != null) {
|
||||
callDepth++;
|
||||
decodeStream(targetSeg, 0, targetSeg.length);
|
||||
callDepth--;
|
||||
} else {
|
||||
targetSegId = -1;
|
||||
}
|
||||
if (targetSegId != -1) {
|
||||
byte[] targetSeg = segmentStore.get(targetSegId);
|
||||
if (targetSeg != null) {
|
||||
int savedSegId = currentSegId;
|
||||
currentSegId = targetSegId;
|
||||
SegmentBounds targetSb = segmentBoundsMap.computeIfAbsent(targetSegId, SegmentBounds::new);
|
||||
activeSegmentsInOrder.remove(targetSb);
|
||||
activeSegmentsInOrder.add(targetSb);
|
||||
callDepth++;
|
||||
decodeStreamDirect(targetSeg, 0, targetSeg.length);
|
||||
callDepth--;
|
||||
currentSegId = savedSegId;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx += orderLen;
|
||||
@@ -420,7 +554,7 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSMT: { // Set Marker Type (0x29)
|
||||
markerType = inputData[idx + 1] & 0xFF;
|
||||
markerType = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
@@ -430,24 +564,23 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSPT: { // Set Pattern Symbol (0x28)
|
||||
pattern = inputData[idx + 1] & 0xFF;
|
||||
pattern = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCD: { // Set Character Direction (0x3A)
|
||||
charDir = inputData[idx + 1] & 0xFF;
|
||||
charDir = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GSCS: { // Set Character Set (0x38)
|
||||
charSet = inputData[idx + 1] & 0xFF;
|
||||
charSet = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case 0x04:
|
||||
case GocaConstants.G_GSMX:
|
||||
case GocaConstants.G_GSFLW:
|
||||
case GocaConstants.G_GSMP:
|
||||
case GocaConstants.G_GSCC:
|
||||
case GocaConstants.G_GSMS_SET:
|
||||
case GocaConstants.G_GPOP: {
|
||||
@@ -460,7 +593,7 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GBAR: { // Begin Area (0x68)
|
||||
int flags = inputData[idx + 1] & 0xFF;
|
||||
int flags = (orderLen == 3) ? (inputData[idx + 2] & 0xFF) : (inputData[idx + 1] & 0xFF);
|
||||
boolean drawBoundary = (flags & 0x80) != 0 || (flags == 0);
|
||||
boolean fill = (flags == 0) || (flags & 0x40) != 0 ||
|
||||
(pattern >= 1 && pattern <= 14);
|
||||
@@ -504,21 +637,21 @@ public class GocaDecoder {
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCFARC: { // Full Arc Current Position (0x87)
|
||||
if (payloadLen >= 2) {
|
||||
if (payloadLen >= 0) {
|
||||
processArc(inputData, idx + 2, payloadLen, true, true);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GARC: { // Partial Arc Absolute (0xC6)
|
||||
if (payloadLen >= 8) {
|
||||
if (payloadLen >= 4) {
|
||||
processArc(inputData, idx + 2, payloadLen, false, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
break;
|
||||
}
|
||||
case GocaConstants.G_GCARC: { // Partial Arc Current Position (0x86)
|
||||
if (payloadLen >= 4) {
|
||||
if (payloadLen >= 0) {
|
||||
processArc(inputData, idx + 2, payloadLen, true, false);
|
||||
}
|
||||
idx += orderLen;
|
||||
@@ -761,6 +894,8 @@ public class GocaDecoder {
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
trackPoint(startX, startY);
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(startX, startY);
|
||||
}
|
||||
@@ -770,6 +905,8 @@ public class GocaDecoder {
|
||||
int nextY = readCoord(data, pos + 2);
|
||||
pos += 4;
|
||||
|
||||
trackPoint(nextX, nextY);
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(nextX, nextY);
|
||||
} else {
|
||||
@@ -799,6 +936,8 @@ public class GocaDecoder {
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
trackPoint(startX, startY);
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(startX, startY);
|
||||
}
|
||||
@@ -811,6 +950,8 @@ public class GocaDecoder {
|
||||
int nextX = startX + dx;
|
||||
int nextY = startY + dy;
|
||||
|
||||
trackPoint(nextX, nextY);
|
||||
|
||||
if (inArea) {
|
||||
addAreaPoint(nextX, nextY);
|
||||
} else {
|
||||
@@ -859,36 +1000,63 @@ public class GocaDecoder {
|
||||
// Semi-axis lengths in GOCA coordinate space
|
||||
double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
|
||||
double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
|
||||
|
||||
// If arc parameters are at default (1,0,0,1) and curPos != center, use distance to center as radius
|
||||
double dist = Math.hypot(curX - centerX, curY - centerY);
|
||||
if (semiAxis1 <= 1.01 && dist > 1.0) {
|
||||
semiAxis1 = dist * multiplier;
|
||||
semiAxis2 = dist * multiplier;
|
||||
}
|
||||
|
||||
if (semiAxis1 < 1.0) semiAxis1 = 1.0;
|
||||
if (semiAxis2 < 1.0) semiAxis2 = 1.0;
|
||||
|
||||
// Map to pixel space
|
||||
int rx = Math.abs(plane.mapX((int) Math.round(semiAxis1)) - plane.mapX(0));
|
||||
int ry = Math.abs(plane.mapY(0) - plane.mapY((int) Math.round(semiAxis2)));
|
||||
if (rx <= 0) rx = 1;
|
||||
if (ry <= 0) ry = 1;
|
||||
if (rx <= 0) rx = Math.max(1, (int) Math.round(semiAxis1));
|
||||
if (ry <= 0) ry = Math.max(1, (int) Math.round(semiAxis2));
|
||||
|
||||
// For partial arcs, read sweep start and sweep angle
|
||||
// For partial arcs, determine start angle and sweep angle
|
||||
double startAngleDeg = 0.0;
|
||||
double sweepAngleDeg = 360.0;
|
||||
if (!isFull && pos + 4 <= off + len) {
|
||||
// Sweep start: integer.fraction of full revolution
|
||||
int startInt = data[pos];
|
||||
int startFrac = data[pos + 1] & 0xFF;
|
||||
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
||||
pos += 2;
|
||||
// Sweep angle: integer.fraction of full revolution
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
} else if (!isFull) {
|
||||
// No sweep data — default to full circle
|
||||
sweepAngleDeg = 360.0;
|
||||
if (!isFull) {
|
||||
if (pos + 4 <= off + len) {
|
||||
// Sweep start: integer.fraction of full revolution
|
||||
int startInt = data[pos];
|
||||
int startFrac = data[pos + 1] & 0xFF;
|
||||
startAngleDeg = (startInt + startFrac / 256.0) * 360.0;
|
||||
pos += 2;
|
||||
// Sweep angle: integer.fraction of full revolution
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
} else if (pos + 2 <= off + len) {
|
||||
// Single sweep parameter
|
||||
int sweepInt = data[pos];
|
||||
int sweepFrac = data[pos + 1] & 0xFF;
|
||||
sweepAngleDeg = (sweepInt + sweepFrac / 256.0) * 360.0;
|
||||
if (curX != centerX || curY != centerY) {
|
||||
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
|
||||
startAngleDeg = Math.toDegrees(startRad);
|
||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||
}
|
||||
} else {
|
||||
// No sweep data — if we have a current point, start there and sweep full circle
|
||||
if (curX != centerX || curY != centerY) {
|
||||
double startRad = Math.atan2(plane.mapY(centerY) - plane.mapY(curY), plane.mapX(curX) - plane.mapX(centerX));
|
||||
startAngleDeg = Math.toDegrees(startRad);
|
||||
if (startAngleDeg < 0) startAngleDeg += 360.0;
|
||||
}
|
||||
sweepAngleDeg = 360.0;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("processArc: center=(" + centerX + "," + centerY + ") mult=" + multiplier
|
||||
+ " P=" + arcParamP + " Q=" + arcParamQ + " R=" + arcParamR + " S=" + arcParamS
|
||||
+ " rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
|
||||
trackPoint(centerX - (int) Math.round(semiAxis1), centerY - (int) Math.round(semiAxis2));
|
||||
trackPoint(centerX + (int) Math.round(semiAxis1), centerY + (int) Math.round(semiAxis2));
|
||||
|
||||
System.out.println("processArc: center=(" + centerX + "," + centerY + ") cur=(" + curX + "," + curY
|
||||
+ ") rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
|
||||
+ " isFull=" + isFull);
|
||||
|
||||
plane.drawArc(plane.mapX(centerX), plane.mapY(centerY), rx, ry, startAngleDeg, sweepAngleDeg,
|
||||
@@ -937,12 +1105,14 @@ public class GocaDecoder {
|
||||
int end = off + len;
|
||||
|
||||
if (fromCurPos) {
|
||||
trackPoint(curX, curY);
|
||||
plane.drawMarker(plane.mapX(curX), plane.mapY(curY), markerType, markerSize, markerColor);
|
||||
}
|
||||
|
||||
while (pos + 4 <= end) {
|
||||
int x = readCoord(data, pos);
|
||||
int y = readCoord(data, pos + 2);
|
||||
trackPoint(x, y);
|
||||
plane.drawMarker(plane.mapX(x), plane.mapY(y), markerType, markerSize, markerColor);
|
||||
curX = x;
|
||||
curY = y;
|
||||
@@ -970,6 +1140,12 @@ public class GocaDecoder {
|
||||
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10;
|
||||
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14;
|
||||
|
||||
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
|
||||
int totalH = (charHeight > 0 ? charHeight : 14);
|
||||
trackPoint(startX, startY);
|
||||
trackPoint(startX + totalW, startY + totalH);
|
||||
trackPoint(startX + totalW, startY - totalH);
|
||||
|
||||
if (charSet != 0 && programSymbolManager != null) {
|
||||
for (int i = 0; i < textLen; i++) {
|
||||
int code = data[pos + i] & 0xFF;
|
||||
|
||||
@@ -30,6 +30,34 @@ public class GraphicInputBuilder {
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl) {
|
||||
return buildGraphicInput(gocaX, gocaY, aidCode, isMouseAction, isShift, isCtrl, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
|
||||
*
|
||||
* IMPORTANT ARCHITECTURE NOTE:
|
||||
* Per IBM GA23-0059 / GDDM specifications:
|
||||
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
|
||||
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
|
||||
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
|
||||
* the exact segment ID in bytes 28-31. If hardcoded or mismatched, GDDM rejects the click
|
||||
* with a WCC 0xF7 alarm beep.
|
||||
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
|
||||
*
|
||||
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
|
||||
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
|
||||
* @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3)
|
||||
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
|
||||
* @param isShift true if shift key was down
|
||||
* @param isCtrl true if ctrl key was down
|
||||
* @param pickedSegId Picked GOCA segment ID (0 if none)
|
||||
* @param pickTag Pick correlation tag
|
||||
* @return 56-byte payload
|
||||
*/
|
||||
public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode,
|
||||
boolean isMouseAction, boolean isShift, boolean isCtrl,
|
||||
int pickedSegId, int pickTag) {
|
||||
byte[] sf = new byte[MASK.length];
|
||||
System.arraycopy(MASK, 0, sf, 0, MASK.length);
|
||||
|
||||
@@ -41,11 +69,22 @@ public class GraphicInputBuilder {
|
||||
sf[26] = (byte) ((gocaY >> 8) & 0xFF);
|
||||
sf[27] = (byte) (gocaY & 0xFF);
|
||||
|
||||
if (pickedSegId != 0) {
|
||||
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
|
||||
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
|
||||
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
|
||||
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
|
||||
sf[31] = (byte) (pickedSegId & 0xFF);
|
||||
}
|
||||
|
||||
if (isMouseAction) {
|
||||
sf[31] = 0x04;
|
||||
sf[33] = 0x04;
|
||||
if (pickTag != 0) {
|
||||
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
|
||||
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
|
||||
sf[33] = (byte) (pickTag & 0xFF);
|
||||
}
|
||||
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
|
||||
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick
|
||||
sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
|
||||
} else {
|
||||
// Keyboard AID (Enter, PF keys)
|
||||
sf[31] = 0x07;
|
||||
|
||||
@@ -123,6 +123,7 @@ public class GraphicsPlane {
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
|
||||
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
|
||||
*/
|
||||
public int mapX(int gocaX) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
@@ -133,6 +134,8 @@ public class GraphicsPlane {
|
||||
|
||||
/**
|
||||
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
|
||||
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
|
||||
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
|
||||
*/
|
||||
public int mapY(int gocaY) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
@@ -143,6 +146,7 @@ public class GraphicsPlane {
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax).
|
||||
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapX(int px) {
|
||||
int nominalWidth = screenCols * 9;
|
||||
@@ -153,6 +157,7 @@ public class GraphicsPlane {
|
||||
|
||||
/**
|
||||
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up).
|
||||
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
|
||||
*/
|
||||
public int unmapY(int py) {
|
||||
int nominalHeight = screenRows * 12;
|
||||
|
||||
@@ -239,17 +239,6 @@ public class InputProcessor {
|
||||
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
// If GDDM attached graphic cursor (interactive graphics / light-pen mode):
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive() && aidCode != AID_CLEAR) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, false, false, false);
|
||||
out.write(AID_SF); // 0x88
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
}
|
||||
|
||||
out.write(aidCode);
|
||||
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
@@ -336,16 +325,25 @@ public class InputProcessor {
|
||||
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
|
||||
int gx = gocaDecoder.getGraphicCursorX();
|
||||
int gy = gocaDecoder.getGraphicCursorY();
|
||||
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, true, isShift, isCtrl);
|
||||
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
|
||||
int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
|
||||
byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
|
||||
gx, gy, aidCode, true, isShift, isCtrl, pickedSeg, pickTag
|
||||
);
|
||||
System.err.println(String.format(
|
||||
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
|
||||
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl
|
||||
));
|
||||
out.write(AID_SF);
|
||||
try {
|
||||
out.write(sf);
|
||||
} catch (java.io.IOException ignored) {}
|
||||
} else {
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
}
|
||||
out.write(aidCode);
|
||||
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
|
||||
out.write(caddr[0] & 0xFF);
|
||||
out.write(caddr[1] & 0xFF);
|
||||
|
||||
sendAidResponse(out.toByteArray());
|
||||
}
|
||||
|
||||
@@ -234,6 +234,12 @@ public class GocaDecoderTest {
|
||||
assertEquals(0, plane.mapY(257));
|
||||
// Bottom edge (-258) should map to 515
|
||||
assertEquals(515, plane.mapY(-258));
|
||||
|
||||
// Bidirectional roundtrip mapping must be exact
|
||||
assertEquals(0, plane.unmapX(plane.mapX(0)));
|
||||
assertEquals(0, plane.unmapY(plane.mapY(0)));
|
||||
assertEquals(100, plane.unmapY(plane.mapY(100)));
|
||||
assertEquals(-200, plane.unmapY(plane.mapY(-200)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -288,4 +294,45 @@ public class GocaDecoderTest {
|
||||
|
||||
assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGcallAndGscsWithLengthBytes() {
|
||||
GraphicsPlane plane = new GraphicsPlane(400, 300);
|
||||
GocaDecoder decoder = new GocaDecoder(plane);
|
||||
|
||||
// Construct Segment 2: Begin Segment (id=2), Set Color, draw line, End Segment
|
||||
ByteArrayOutputStream s2 = new ByteArrayOutputStream();
|
||||
s2.write(GocaConstants.G_BEGSEGM);
|
||||
s2.write(0x0C);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x02); // id=2
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(GocaConstants.G_GSCS); s2.write(0x01); s2.write(0x40); // charSet = 0x40
|
||||
s2.write(GocaConstants.G_GSCOL); s2.write(0x02); // Color = Red
|
||||
s2.write(GocaConstants.G_GLINE); s2.write(0x08);
|
||||
s2.write(0x00); s2.write(0x00); s2.write(0x00); s2.write(0x00);
|
||||
s2.write(0x00); s2.write(0x32); s2.write(0x00); s2.write(0x32);
|
||||
s2.write(GocaConstants.G_ENDSEGM); s2.write(0x00);
|
||||
|
||||
byte[] s2Bytes = s2.toByteArray();
|
||||
decoder.decodeStream(s2Bytes, 0, s2Bytes.length);
|
||||
|
||||
// Segment 1 calls Segment 2 via GCALL (0x2A len=4 segId=2)
|
||||
ByteArrayOutputStream s1 = new ByteArrayOutputStream();
|
||||
s1.write(GocaConstants.G_BEGSEGM);
|
||||
s1.write(0x0C);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x01); // id=1
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x00);
|
||||
// GCALL (0x2A len=4 segId=2)
|
||||
s1.write(GocaConstants.G_GCALL); s1.write(0x04);
|
||||
s1.write(0x00); s1.write(0x00); s1.write(0x00); s1.write(0x02);
|
||||
s1.write(GocaConstants.G_ENDSEGM); s1.write(0x00);
|
||||
|
||||
byte[] s1Bytes = s1.toByteArray();
|
||||
decoder.decodeStream(s1Bytes, 0, s1Bytes.length);
|
||||
|
||||
assertEquals(0x40, decoder.getCharSet());
|
||||
assertTrue(plane.hasContent(), "Expected plane to have content after GCALL segment execution");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user