Lightpen on
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s

This commit is contained in:
2026-08-24 16:55:38 +00:00
parent e28b7dc30a
commit d2bdb9bb0a
4 changed files with 70 additions and 32 deletions
@@ -20,6 +20,7 @@ public class StatusBar extends JPanel {
private final JLabel luName; private final JLabel luName;
private final JLabel lockStatus; private final JLabel lockStatus;
private final JLabel modelInfo; private final JLabel modelInfo;
private final JButton lpButton;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel; private TerminalPanel terminalPanel;
@@ -45,7 +46,7 @@ public class StatusBar extends JPanel {
modelInfo = createLabel("", oiaFont, OIA_DIM); modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG); cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
JButton lpButton = new JButton("LightPen: OFF"); lpButton = new JButton("LightPen: OFF");
lpButton.setFont(oiaFont); lpButton.setFont(oiaFont);
lpButton.setForeground(OIA_FG); lpButton.setForeground(OIA_FG);
lpButton.setBackground(OIA_BG); lpButton.setBackground(OIA_BG);
@@ -177,5 +178,11 @@ public class StatusBar extends JPanel {
int row = sb.getCursorRow() + 1; int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1; int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d", row, col)); cursorPosition.setText(String.format("%03d/%03d", row, col));
// Light Pen indicator — sync with actual state
if (terminalPanel != null) {
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
lpButton.setForeground(terminalPanel.isLightPenMode() ? new Color(255, 255, 80) : OIA_FG);
}
} }
} }
@@ -215,7 +215,12 @@ public class TerminalPanel extends JPanel {
sb.setCursorAddress(newAddr); sb.setCursorAddress(newAddr);
clearSelection(); clearSelection();
if (lightPenMode) { if (lightPenMode) {
if (client.getInputProcessor().lightPenSelect(newAddr)) { System.out.println("LP click: row=" + selectionStartRow + " col=" + selectionStartCol
+ " addr=" + newAddr + " mouseY=" + e.getY() + " oy=" + getRenderOffsetY()
+ " cellH=" + cellHeight);
boolean result = client.getInputProcessor().lightPenSelect(newAddr);
System.out.println("LP lightPenSelect result: " + result);
if (result) {
refreshScreen(); refreshScreen();
return; return;
} }
@@ -35,9 +35,9 @@ public class GocaDecoder {
private int charHeight = 16; private int charHeight = 16;
private int charSet = 0; private int charSet = 0;
private int arcParamP = 1; private int arcParamP = 1;
private int arcParamQ = 1; private int arcParamQ = 0;
private int arcParamR = 0; private int arcParamR = 0;
private int arcParamS = 0; private int arcParamS = 1;
private ProgramSymbolManager programSymbolManager; private ProgramSymbolManager programSymbolManager;
@@ -832,48 +832,70 @@ public class GocaDecoder {
int centerX = curX; int centerX = curX;
int centerY = curY; int centerY = curY;
// For absolute variants, read the center point from data
if (!fromCurPos && pos + 4 <= off + len) { if (!fromCurPos && pos + 4 <= off + len) {
centerX = readCoord(data, pos); centerX = readCoord(data, pos);
centerY = readCoord(data, pos + 2); centerY = readCoord(data, pos + 2);
pos += 4; pos += 4;
} }
double sweepFraction = 1.0; // Read the multiplier (integer.fraction format)
if (!isFull && pos + 2 <= off + len) { double multiplier = 1.0;
byte intPart = data[pos]; if (pos + 2 <= off + len) {
double fracPart = (data[pos + 1] & 0xFF) / 256.0; int intPart = data[pos]; // signed byte
sweepFraction = intPart + (intPart >= 0 ? fracPart : -fracPart); int fracPart = data[pos + 1] & 0xFF;
multiplier = intPart + fracPart / 256.0;
if (multiplier == 0.0) multiplier = 1.0;
pos += 2;
} }
int pxCenter = plane.mapX(centerX); // Compute semi-axes from arc parameters scaled by multiplier
int pyCenter = plane.mapY(centerY); // P,Q and R,S are conjugate semi-diameter vectors
int pxCur = plane.mapX(curX); double pScaled = arcParamP * multiplier;
int pyCur = plane.mapY(curY); double qScaled = arcParamQ * multiplier;
double rScaled = arcParamR * multiplier;
double sScaled = arcParamS * multiplier;
double radius = Math.sqrt(Math.pow(pxCur - pxCenter, 2) + Math.pow(pyCur - pyCenter, 2)); // Semi-axis lengths in GOCA coordinate space
int rx = (int) Math.round(radius); double semiAxis1 = Math.sqrt(pScaled * pScaled + qScaled * qScaled);
int ry = rx; double semiAxis2 = Math.sqrt(rScaled * rScaled + sScaled * sScaled);
if (rx <= 0) { if (semiAxis1 < 1.0) semiAxis1 = 1.0;
rx = 10; if (semiAxis2 < 1.0) semiAxis2 = 1.0;
ry = 10;
// 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;
// For partial arcs, read sweep start 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;
} }
double startAngleRad = Math.atan2(pyCenter - pyCur, pxCur - pxCenter); System.out.println("processArc: center=(" + centerX + "," + centerY + ") mult=" + multiplier
double startAngleDeg = Math.toDegrees(startAngleRad); + " P=" + arcParamP + " Q=" + arcParamQ + " R=" + arcParamR + " S=" + arcParamS
if (startAngleDeg < 0) startAngleDeg += 360.0; + " rx=" + rx + " ry=" + ry + " start=" + startAngleDeg + " sweep=" + sweepAngleDeg
+ " isFull=" + isFull);
double sweepAngleDeg = isFull ? 360.0 : (sweepFraction * 360.0); plane.drawArc(plane.mapX(centerX), plane.mapY(centerY), rx, ry, startAngleDeg, sweepAngleDeg,
plane.drawArc(pxCenter, pyCenter, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull); curColor, lineType, lineWidth, isFull);
// Arc ends at the end of the sweep curX = centerX;
double endAngleRad = Math.toRadians(startAngleDeg + sweepAngleDeg); curY = centerY;
int pxEnd = (int) Math.round(pxCenter + rx * Math.cos(endAngleRad));
int pyEnd = (int) Math.round(pyCenter - ry * Math.sin(endAngleRad));
curX = plane.unmapX(pxEnd);
curY = plane.unmapY(pyEnd);
} }
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) { private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
@@ -357,16 +357,20 @@ public class InputProcessor {
*/ */
public boolean lightPenSelect(int address) { public boolean lightPenSelect(int address) {
if (screen == null || !screen.isFormatted()) { if (screen == null || !screen.isFormatted()) {
System.out.println("LP: screen null or unformatted");
return false; return false;
} }
int size = screen.getRows() * screen.getCols(); int size = screen.getRows() * screen.getCols();
int faPos = screen.findFieldAttribute(address); int faPos = screen.findFieldAttribute(address);
if (faPos < 0) { if (faPos < 0) {
System.out.println("LP: no FA found for addr=" + address);
return false; return false;
} }
ExtendedAttribute faCell = screen.getCell(faPos); ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF; int fa = faCell.fa & 0xFF;
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
+ " selectable=" + org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
if (!org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) { if (!org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
return false; return false;
} }