Skip to content

Commit d5cc528

Browse files
esaruohoclaude
andcommitted
Add optocoupler CTR, SRAM reload on reset, voltage-limited current source, macOS paste fix
- Configurable optocoupler CTR (#150): Add CTR % parameter to edit dialog, scale CCCS transfer expression by CTR multiplier. Default 100%. - SRAM restore on reset (#184): Add "Restore Contents on Reset" checkbox. Stores initial map snapshot, restores it on simulation reset. - Voltage-limited current source (#149): Add compliance voltage mode. When enabled, current source becomes open circuit beyond max voltage. - macOS Electron paste fix (#138): Replace Menu.setApplicationMenu(false) with proper Edit menu so Cmd+C/V/X/A work in text fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b769d0 commit d5cc528

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

app/main.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,38 @@ const url = require('url')
1212
// be closed automatically when the JavaScript object is garbage collected.
1313
var windows = [];
1414

15-
Menu.setApplicationMenu(false);
15+
// Build a minimal application menu so that standard keyboard shortcuts
16+
// (Cmd+C, Cmd+V, Cmd+X, Cmd+A) work in text fields on macOS.
17+
// Setting the menu to false disables these shortcuts entirely.
18+
var template = [
19+
{
20+
label: 'Edit',
21+
submenu: [
22+
{ role: 'undo' },
23+
{ role: 'redo' },
24+
{ type: 'separator' },
25+
{ role: 'cut' },
26+
{ role: 'copy' },
27+
{ role: 'paste' },
28+
{ role: 'selectAll' }
29+
]
30+
}
31+
];
32+
if (process.platform === 'darwin') {
33+
template.unshift({
34+
label: app.getName(),
35+
submenu: [
36+
{ role: 'about' },
37+
{ type: 'separator' },
38+
{ role: 'hide' },
39+
{ role: 'hideOthers' },
40+
{ role: 'unhide' },
41+
{ type: 'separator' },
42+
{ role: 'quit' }
43+
]
44+
});
45+
}
46+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
1647

1748
// save arguments
1849
global.sharedObject = {prop1: process.argv};

src/com/lushprojects/circuitjs1/client/CurrentElm.java

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
import com.google.gwt.xml.client.Document;
2424

2525
class CurrentElm extends CircuitElm {
26+
static final int FLAG_VOLTAGE_LIMIT = 1;
2627
double currentValue;
28+
double maxVoltage = 1e8;
29+
double lastVoltDiff;
2730
boolean broken;
2831
public CurrentElm(int xx, int yy) {
2932
super(xx, yy);
@@ -34,22 +37,32 @@ public CurrentElm(int xa, int ya, int xb, int yb, int f,
3437
super(xa, ya, xb, yb, f);
3538
try {
3639
currentValue = new Double(st.nextToken()).doubleValue();
37-
} catch (Exception e) {
40+
maxVoltage = new Double(st.nextToken()).doubleValue();
41+
} catch (Exception e) {}
42+
if (currentValue == 0)
3843
currentValue = .01;
39-
}
44+
}
45+
boolean isVoltageLimited() { return (flags & FLAG_VOLTAGE_LIMIT) != 0; }
46+
boolean nonLinear() { return isVoltageLimited(); }
47+
void reset() {
48+
super.reset();
49+
lastVoltDiff = 0;
4050
}
4151
String dump() {
42-
return super.dump() + " " + currentValue;
52+
return super.dump() + " " + currentValue + " " + maxVoltage;
4353
}
4454

4555
void dumpXml(Document doc, Element elem) {
4656
super.dumpXml(doc, elem);
4757
XMLSerializer.dumpAttr(elem, "cu", currentValue);
58+
if (isVoltageLimited())
59+
XMLSerializer.dumpAttr(elem, "mv", maxVoltage);
4860
}
4961

5062
void undumpXml(XMLDeserializer xml) {
5163
super.undumpXml(xml);
5264
currentValue = xml.parseDoubleAttr("cu", currentValue);
65+
maxVoltage = xml.parseDoubleAttr("mv", maxVoltage);
5366
}
5467
int getDumpType() { return 'i'; }
5568

@@ -95,25 +108,67 @@ void stamp() {
95108
// no current path; stamping a current source would cause a matrix error.
96109
sim.stampResistor(nodes[0], nodes[1], 1e8);
97110
current = 0;
111+
} else if (isVoltageLimited()) {
112+
// voltage-limited mode: stamp as nonlinear, doStep() will handle it
113+
sim.stampNonLinear(nodes[0]);
114+
sim.stampNonLinear(nodes[1]);
98115
} else {
99116
// ok to stamp a current source
100117
sim.stampCurrentSource(nodes[0], nodes[1], currentValue);
101118
current = currentValue;
102119
}
103120
}
121+
122+
void doStep() {
123+
if (!isVoltageLimited() || broken)
124+
return;
125+
double vd = volts[1] - volts[0];
126+
if (Math.abs(lastVoltDiff - vd) > .01)
127+
sim.converged = false;
128+
lastVoltDiff = vd;
129+
130+
double absVd = Math.abs(vd);
131+
if (absVd <= maxVoltage) {
132+
// within compliance: act as ideal current source
133+
sim.stampResistor(nodes[0], nodes[1], 1e8);
134+
sim.stampCurrentSource(nodes[0], nodes[1], currentValue);
135+
current = currentValue;
136+
} else {
137+
// beyond compliance: act as high-impedance (open circuit)
138+
sim.stampResistor(nodes[0], nodes[1], 1e8);
139+
current = vd / 1e8;
140+
}
141+
}
104142

105143
public EditInfo getEditInfo(int n) {
106144
if (n == 0)
107145
return new EditInfo("Current (A)", currentValue, 0, .1);
146+
if (n == 1) {
147+
EditInfo ei = new EditInfo("", 0, -1, -1);
148+
ei.checkbox = new Checkbox("Voltage Limited",
149+
isVoltageLimited());
150+
return ei;
151+
}
152+
if (n == 2 && isVoltageLimited())
153+
return new EditInfo("Max Voltage (V)", maxVoltage, 0, 0);
108154
return null;
109155
}
110156
public void setEditValue(int n, EditInfo ei) {
111-
currentValue = ei.value;
157+
if (n == 0)
158+
currentValue = ei.value;
159+
if (n == 1) {
160+
flags = ei.changeFlag(flags, FLAG_VOLTAGE_LIMIT);
161+
ei.newDialog = true;
162+
}
163+
if (n == 2 && ei.value > 0)
164+
maxVoltage = ei.value;
112165
}
113166
void getInfo(String arr[]) {
114167
arr[0] = "current source";
115168
int i = getBasicInfo(arr);
116169
arr[i++] = "P = " + getUnitText(getPower(), "W");
170+
if (isVoltageLimited())
171+
arr[i++] = "Vmax = " + getVoltageText(maxVoltage);
117172
}
118173
double getVoltageDiff() {
119174
return volts[1] - volts[0];

src/com/lushprojects/circuitjs1/client/OptocouplerElm.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public class OptocouplerElm extends CompositeElm {
44
int csize, cspc, cspc2;
55
int rectPointsX[], rectPointsY[];
66
double curCounts[];
7+
double ctr = 1.0; // current transfer ratio (1.0 = 100%)
78

89
private static String modelString = "DiodeElm 6 1\rCCCSElm 1 2 3 4\rNTransistorElm 3 4 5";
910
private static int[] modelExternalNodes = { 6, 2, 4, 5 };
@@ -21,11 +22,16 @@ public OptocouplerElm(int xa, int ya, int xb, int yb, int f, StringTokenizer st)
2122
// pass st=null since we don't need to undump any of the sub-elements
2223
super(xa, ya, xb, yb, f, null, modelString, modelExternalNodes);
2324
noDiagonal = true;
25+
try {
26+
ctr = new Double(st.nextToken()).doubleValue();
27+
} catch (Exception e) {
28+
ctr = 1.0;
29+
}
2430
initOptocoupler();
2531
}
2632

2733
public String dump() {
28-
return dumpWithMask(0);
34+
return dumpWithMask(0) + " " + ctr;
2935
}
3036

3137
private void initOptocoupler() {
@@ -36,7 +42,8 @@ private void initOptocoupler() {
3642
CCCSElm cccs = (CCCSElm) compElmList.get(1);
3743

3844
// from http://www.cel.com/pdf/appnotes/an3017.pdf
39-
cccs.setExpr("max(0,min(.0001, select(i-.003, (-80000000000*(i)^5+800000000*(i)^4-3000000*(i)^3+5177.2*(i)^2+.2453*(i)-.00005)*1.04/700, (9000000*(i)^5-998113*(i)^4+42174*(i)^3-861.32*(i)^2+9.0836*(i)-.0078)*.945/700)))");
45+
// the base expression models a ~100% CTR device; we scale by ctr
46+
cccs.setExpr(ctr + "*max(0,min(.0001, select(i-.003, (-80000000000*(i)^5+800000000*(i)^4-3000000*(i)^3+5177.2*(i)^2+.2453*(i)-.00005)*1.04/700, (9000000*(i)^5-998113*(i)^4+42174*(i)^3-861.32*(i)^2+9.0836*(i)-.0078)*.945/700)))");
4047

4148
transistor = (TransistorElm) compElmList.get(2);
4249
transistor.setBeta(700);
@@ -177,11 +184,21 @@ public int getDumpType() {
177184

178185
void getInfo(String arr[]) {
179186
arr[0] = "optocoupler";
180-
arr[1] = "Iin = " + getCurrentText(getCurrentIntoNode(0));
181-
arr[2] = "Iout = " + getCurrentText(getCurrentIntoNode(2));
187+
arr[1] = "CTR = " + (int)(ctr*100) + "%";
188+
arr[2] = "Iin = " + getCurrentText(getCurrentIntoNode(0));
189+
arr[3] = "Iout = " + getCurrentText(getCurrentIntoNode(2));
182190
}
183191

184192
public EditInfo getEditInfo(int n) {
185-
return null;
193+
if (n == 0)
194+
return new EditInfo("CTR (%)", ctr * 100, 0, 0).setDimensionless();
195+
return null;
196+
}
197+
198+
public void setEditValue(int n, EditInfo ei) {
199+
if (n == 0 && ei.value > 0) {
200+
ctr = ei.value / 100;
201+
initOptocoupler();
202+
}
186203
}
187204
}

src/com/lushprojects/circuitjs1/client/SRAMElm.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class SRAMElm extends ChipElm {
3131
int addressNodes, dataNodes, internalNodes;
3232
int addressBits, dataBits;
3333
HashMap<Integer, Integer> map;
34+
HashMap<Integer, Integer> initialMap; // saved initial contents for restore on reset
35+
static final int FLAG_RELOAD_ON_RESET = 2;
3436
static String contentsOverride = null;
3537
TextArea editTextArea;
3638
boolean hexTogglePending;
@@ -66,6 +68,8 @@ public SRAMElm(int xa, int ya, int xb, int yb, int f,
6668
}
6769
}
6870
} catch (Exception e) {}
71+
if ((flags & FLAG_RELOAD_ON_RESET) != 0)
72+
initialMap = new HashMap<Integer, Integer>(map);
6973
}
7074

7175
void dumpXml(Document doc, Element elem) {
@@ -89,6 +93,12 @@ void undumpXml(XMLDeserializer xml) {
8993
setupPins();
9094
}
9195

96+
void reset() {
97+
super.reset();
98+
if ((flags & FLAG_RELOAD_ON_RESET) != 0 && initialMap != null)
99+
map = new HashMap<Integer, Integer>(initialMap);
100+
}
101+
92102
boolean nonLinear() { return true; }
93103
String getChipName() { return "Static RAM"; }
94104
void setupPins() {
@@ -144,6 +154,13 @@ public EditInfo getChipEditInfo(int n) {
144154
ei.newDialog = true;
145155
return ei;
146156
}
157+
int reloadIdx = SRAMLoadFile.isSupported() ? 5 : 4;
158+
if (n == reloadIdx) {
159+
EditInfo ei = new EditInfo("", 0, -1, -1);
160+
ei.checkbox = new Checkbox("Restore Contents on Reset",
161+
(flags & FLAG_RELOAD_ON_RESET) != 0);
162+
return ei;
163+
}
147164
return super.getChipEditInfo(n);
148165
}
149166

@@ -218,6 +235,8 @@ public void setChipEditValue(int n, EditInfo ei) {
218235
// skip re-parse during apply() if hex toggle already handled it
219236
if (!hexTogglePending)
220237
parseContentsString(ei.textArea.getText());
238+
if ((flags & FLAG_RELOAD_ON_RESET) != 0)
239+
initialMap = new HashMap<Integer, Integer>(map);
221240
}
222241
if (n == 3) {
223242
if (hexTogglePending) {
@@ -239,6 +258,14 @@ public void setChipEditValue(int n, EditInfo ei) {
239258
ei.newDialog = true;
240259
}
241260
}
261+
int reloadIdx = SRAMLoadFile.isSupported() ? 5 : 4;
262+
if (n == reloadIdx) {
263+
flags = ei.changeFlag(flags, FLAG_RELOAD_ON_RESET);
264+
if ((flags & FLAG_RELOAD_ON_RESET) != 0)
265+
initialMap = new HashMap<Integer, Integer>(map);
266+
else
267+
initialMap = null;
268+
}
242269
}
243270
int getVoltageSourceCount() { return dataBits; }
244271
int getInternalNodeCount() { return dataBits; }

0 commit comments

Comments
 (0)