Files
Learn_System/frontend/js/labs/logic.js
T
Maxim Dolgolyov 8b3159b529 feat(labs): wave 3 — 5 new sims + optics merger
Оптическая скамья (opticsbench) — merger thinlens + mirror + refraction
- 4 режима: «Свободная сборка» / «Линза» / «Зеркало» / «Преломление»
- Все 3 движка слиты в OpticsBenchSim (1583 строк)
- Backward compat: #thinlens / #mirrors / #refraction → #opticsbench
- Удалены: thinlens.js, mirror.js, refraction.js

Радиоактивный распад (radioactive) — новая сима
- Monte-Carlo распад: λ·dt вероятность на тик, частицы меняют цвет, эмитируются α/β/γ
- Real-time N(t) график с теоретической кривой N₀·exp(-λt)
- 7 изотопов: ¹⁴C, ¹³¹I, ¹³⁷Cs, ²²⁶Ra, ⁴⁰K, ²³⁸U-chain, ²³⁵U-chain
- Цепочки распадов (U-238: 14 шагов сокращены до 5 ключевых)
- Dating mode для C-14: t = ln(N₀/N)/λ
- HUD: периодов прошло, % распалось, активность в Бк

Тепловые двигатели (heatengine) — новая сима
- 4 цикла: Карно / Отто / Дизель / Брайтон
- PV-диаграмма с замкнутым циклом, заполненной площадью работы
- Аналитически точные изотермы (PV=nRT) и адиабаты (PV^γ=const)
- Анимированный поршень с резервуарами (красный T_h / синий T_c)
- Частицы газа, скорость ∝ √T
- Hover-tooltips с формулами для каждого сегмента

Логические схемы (logic) — новая сима для информатики
- Drag-drop конструктор: 12 типов компонентов (INPUT/CLOCK/OUTPUT/AND/OR/NOT/XOR/NAND/NOR/XNOR/BUF/wire)
- Топологическая сортировка для propagation, цветовая подсветка HIGH/LOW
- Авто-генерация булевого выражения (∧ ∨ ¬ ⊕)
- Авто-таблица истинности (до 2^6 = 64 строк)
- 6 пресетов: полусумматор, полный сумматор, RS-триггер, D-триггер, декодер 2-в-4, мультиплексор 2-в-1

Стехиометрия (stoichiometry) — новая сима
- 10 реакций: Zn+HCl, H₂+O₂, CH₄+O₂, N₂+H₂ (Габер), Al+CuSO₄, Mg+O₂, CaCO₃→, HCl+NaOH, KMnO₄→, C₂H₅OH+O₂
- Sliders с переключением m/n/V (для газов V=n·22.4 при н.у.)
- Анимация частиц при реакции, подсветка лимитирующего реагента
- Пошаговый расчёт m→n→n_product→m_product с KaTeX
- HUD: лимит, избытки, теоретический выход

Каталог: 33 → 35 сим (5 новых − 3 удалённых merger)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 13:25:16 +03:00

824 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ══════════════════════════════════════════════════════════
LogicSim — Логические схемы
Canvas-based digital logic circuit builder.
Exports: LogicSim class, logicTool(), logicPreset(), logicClearAll(), _openLogic()
══════════════════════════════════════════════════════════ */
/* ── Gate definitions ── */
const GATE_DEFS = {
INPUT: { ins: 0, outs: 1, label: 'IN', w: 56, h: 36 },
CLOCK: { ins: 0, outs: 1, label: 'CLK', w: 56, h: 36 },
OUTPUT: { ins: 1, outs: 0, label: 'OUT', w: 56, h: 36 },
AND: { ins: 2, outs: 1, label: 'AND', w: 64, h: 44 },
OR: { ins: 2, outs: 1, label: 'OR', w: 64, h: 44 },
NOT: { ins: 1, outs: 1, label: 'NOT', w: 56, h: 36 },
XOR: { ins: 2, outs: 1, label: 'XOR', w: 64, h: 44 },
NAND: { ins: 2, outs: 1, label: 'NAND', w: 64, h: 44 },
NOR: { ins: 2, outs: 1, label: 'NOR', w: 64, h: 44 },
XNOR: { ins: 2, outs: 1, label: 'XNOR', w: 64, h: 44 },
BUFFER: { ins: 1, outs: 1, label: 'BUF', w: 56, h: 36 },
};
const PORT_R = 5; // port dot radius
const GRID = 20; // snap grid size
/* ── evaluate a single gate ── */
function evalGate(type, inputs) {
const a = inputs[0] || 0;
const b = inputs[1] || 0;
switch (type) {
case 'AND': return a & b;
case 'OR': return a | b;
case 'NOT': return a ? 0 : 1;
case 'XOR': return a ^ b;
case 'NAND': return (a & b) ? 0 : 1;
case 'NOR': return (a | b) ? 0 : 1;
case 'XNOR': return (a ^ b) ? 0 : 1;
case 'BUFFER': return a;
case 'INPUT': return a; // value from state.value
case 'CLOCK': return a;
case 'OUTPUT': return a;
default: return 0;
}
}
/* ═══════════════════════════════════════════════════════════
LogicSim
═══════════════════════════════════════════════════════════ */
class LogicSim {
constructor(canvas, exprEl, tableEl) {
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
this._exprEl = exprEl; // element for boolean expression display
this._tableEl = tableEl; // element for truth table
this._gates = []; // { id, type, x, y, value, label, freq, _phase }
this._wires = []; // { from: {gateId, port:'out'|'in0'|'in1'}, to: {gateId, port} }
this._nextId = 1;
this._tool = 'select'; // 'select' | type key
this._drag = null;
this._wireStart = null; // { gateId, side:'out', px, py }
this._history = [];
this._histIdx = -1;
this._raf = null;
this._clockRaf = null;
this._bindEvents();
this._startClock();
}
/* ── port pixel positions ── */
_portPx(gate, port) {
const def = GATE_DEFS[gate.type];
const hw = def.w / 2, hh = def.h / 2;
const cx = gate.x, cy = gate.y;
if (port === 'out') return { x: cx + hw, y: cy };
if (port === 'in0') {
if (def.ins === 1) return { x: cx - hw, y: cy };
return { x: cx - hw, y: cy - hh / 2 };
}
if (port === 'in1') return { x: cx - hw, y: cy + hh / 2 };
return { x: cx, y: cy };
}
/* ── snap to grid ── */
_snap(v) { return Math.round(v / GRID) * GRID; }
/* ── find gate near point ── */
_hitGate(px, py) {
for (let i = this._gates.length - 1; i >= 0; i--) {
const g = this._gates[i];
const def = GATE_DEFS[g.type];
if (Math.abs(px - g.x) <= def.w / 2 + 2 && Math.abs(py - g.y) <= def.h / 2 + 2) return g;
}
return null;
}
/* ── find port near point; returns { gateId, port, px, py } or null ── */
_hitPort(px, py) {
for (const g of this._gates) {
const def = GATE_DEFS[g.type];
const ports = [];
if (def.outs > 0) ports.push('out');
if (def.ins >= 1) ports.push('in0');
if (def.ins >= 2) ports.push('in1');
for (const port of ports) {
const p = this._portPx(g, port);
if (Math.hypot(px - p.x, py - p.y) <= 10) {
return { gateId: g.id, port, px: p.x, py: p.y };
}
}
}
return null;
}
/* ── canvas coordinates from event ── */
_pos(e) {
const r = this._canvas.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
/* ══ Event binding ══ */
_bindEvents() {
const c = this._canvas;
c.addEventListener('mousedown', this._onDown.bind(this));
c.addEventListener('mousemove', this._onMove.bind(this));
c.addEventListener('mouseup', this._onUp.bind(this));
c.addEventListener('dblclick', this._onDbl.bind(this));
c.addEventListener('contextmenu', e => { e.preventDefault(); this._onRightClick(e); });
window.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 'z') { e.preventDefault(); this.undo(); }
if (e.ctrlKey && e.key === 'y') { e.preventDefault(); this.redo(); }
});
}
_onDown(e) {
if (e.button !== 0) return;
const { x, y } = this._pos(e);
if (this._tool !== 'select') {
// place a new gate
const type = this._tool;
const def = GATE_DEFS[type];
if (!def) return;
this._pushHistory();
const g = this._addGate(type, this._snap(x), this._snap(y));
this._propagate();
this._updatePanels();
this.draw();
return;
}
// select tool: check port first (wire drawing)
const hitP = this._hitPort(x, y);
if (hitP && (hitP.port === 'out')) {
this._wireStart = hitP;
this._mouseX = x; this._mouseY = y;
return;
}
// check gate drag
const g = this._hitGate(x, y);
if (g) {
this._drag = { gate: g, ox: x - g.x, oy: y - g.y };
}
}
_onMove(e) {
const { x, y } = this._pos(e);
this._mouseX = x; this._mouseY = y;
if (this._drag) {
this._drag.gate.x = this._snap(x - this._drag.ox);
this._drag.gate.y = this._snap(y - this._drag.oy);
this._propagate();
this._updatePanels();
this.draw();
return;
}
if (this._wireStart) {
this.draw();
return;
}
// hover cursor
const hp = this._hitPort(x, y);
this._canvas.style.cursor = hp ? 'crosshair' : (this._hitGate(x, y) ? 'grab' : 'default');
}
_onUp(e) {
if (this._drag) {
this._pushHistory();
this._drag = null;
return;
}
if (this._wireStart) {
const { x, y } = this._pos(e);
const hitP = this._hitPort(x, y);
if (hitP && (hitP.port === 'in0' || hitP.port === 'in1') && hitP.gateId !== this._wireStart.gateId) {
// check not already wired
const exists = this._wires.some(w => w.to.gateId === hitP.gateId && w.to.port === hitP.port);
if (!exists) {
this._pushHistory();
this._wires.push({ from: { gateId: this._wireStart.gateId, port: this._wireStart.port }, to: { gateId: hitP.gateId, port: hitP.port } });
this._propagate();
this._updatePanels();
}
}
this._wireStart = null;
this.draw();
}
}
_onDbl(e) {
const { x, y } = this._pos(e);
const g = this._hitGate(x, y);
if (g && (g.type === 'INPUT')) {
this._pushHistory();
g.value = g.value ? 0 : 1;
this._propagate();
this._updatePanels();
this.draw();
}
if (g && g.type === 'OUTPUT') {
// rename label cycle: OUT → OUT₁ → OUT₂ → OUT (no-op, just show)
}
}
_onRightClick(e) {
const { x, y } = this._pos(e);
// delete wire near click
for (let i = this._wires.length - 1; i >= 0; i--) {
const w = this._wires[i];
const g1 = this._gateById(w.from.gateId); if (!g1) continue;
const g2 = this._gateById(w.to.gateId); if (!g2) continue;
const p1 = this._portPx(g1, w.from.port);
const p2 = this._portPx(g2, w.to.port);
if (this._distToSeg(x, y, p1.x, p1.y, p2.x, p2.y) < 8) {
this._pushHistory();
this._wires.splice(i, 1);
this._propagate();
this._updatePanels();
this.draw();
return;
}
}
// delete gate
const g = this._hitGate(x, y);
if (g) {
this._pushHistory();
this._wires = this._wires.filter(w => w.from.gateId !== g.id && w.to.gateId !== g.id);
this._gates = this._gates.filter(gg => gg.id !== g.id);
this._propagate();
this._updatePanels();
this.draw();
}
}
_distToSeg(px, py, ax, ay, bx, by) {
const dx = bx - ax, dy = by - ay;
const len2 = dx * dx + dy * dy;
if (len2 === 0) return Math.hypot(px - ax, py - ay);
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2));
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
}
/* ══ Gate management ══ */
_addGate(type, x, y) {
const id = this._nextId++;
const def = GATE_DEFS[type];
const g = { id, type, x, y, value: 0, label: def.label };
if (type === 'INPUT') { g.label = String.fromCharCode(64 + this._gates.filter(gg => gg.type === 'INPUT').length + 1); }
if (type === 'OUTPUT') { g.label = 'OUT' + (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1 > 1 ? (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1) : ''); }
if (type === 'CLOCK') { g.freq = 1; g._phase = 0; }
this._gates.push(g);
return g;
}
_gateById(id) { return this._gates.find(g => g.id === id) || null; }
/* ══ Logic propagation (topological sort + eval) ══ */
_propagate() {
// Build in-degree map
const inDeg = {};
this._gates.forEach(g => { inDeg[g.id] = 0; });
const deps = {}; // id -> [id of gates this gate depends on]
this._gates.forEach(g => { deps[g.id] = []; });
this._wires.forEach(w => {
inDeg[w.to.gateId]++;
deps[w.to.gateId].push(w.from.gateId);
});
// Kahn's algorithm
const queue = this._gates.filter(g => inDeg[g.id] === 0).map(g => g.id);
const sorted = [];
const visited = new Set();
while (queue.length) {
const id = queue.shift();
if (visited.has(id)) continue;
visited.add(id);
sorted.push(id);
// find wires going FROM this gate
this._wires.forEach(w => {
if (w.from.gateId === id) {
inDeg[w.to.gateId]--;
if (inDeg[w.to.gateId] === 0) queue.push(w.to.gateId);
}
});
}
// any unvisited (loops): add them to sorted anyway
this._gates.forEach(g => { if (!visited.has(g.id)) sorted.push(g.id); });
for (const id of sorted) {
const g = this._gateById(id);
if (!g) continue;
if (g.type === 'INPUT' || g.type === 'CLOCK') continue; // value set externally
const ins = this._getInputValues(g);
g.value = evalGate(g.type, ins);
}
}
_getInputValues(gate) {
const def = GATE_DEFS[gate.type];
const vals = new Array(def.ins).fill(0);
this._wires.forEach(w => {
if (w.to.gateId !== gate.id) return;
const src = this._gateById(w.from.gateId);
if (!src) return;
const idx = w.to.port === 'in0' ? 0 : 1;
vals[idx] = src.value;
});
return vals;
}
/* ══ Clock ══ */
_startClock() {
let last = 0;
const tick = (now) => {
this._clockRaf = requestAnimationFrame(tick);
const dt = (now - last) / 1000;
last = now;
let changed = false;
this._gates.forEach(g => {
if (g.type !== 'CLOCK') return;
g._phase = (g._phase || 0) + dt * (g.freq || 1);
const newVal = g._phase % 1 < 0.5 ? 1 : 0;
if (newVal !== g.value) { g.value = newVal; changed = true; }
});
if (changed) {
this._propagate();
this._updatePanels();
this.draw();
}
};
this._clockRaf = requestAnimationFrame(tick);
}
/* ══ Undo / Redo ══ */
_pushHistory() {
const snap = JSON.stringify({ gates: this._gates, wires: this._wires, nextId: this._nextId });
this._history = this._history.slice(0, this._histIdx + 1);
this._history.push(snap);
if (this._history.length > 50) this._history.shift();
this._histIdx = this._history.length - 1;
}
undo() {
if (this._histIdx <= 0) return;
this._histIdx--;
this._restoreHistory(this._history[this._histIdx]);
}
redo() {
if (this._histIdx >= this._history.length - 1) return;
this._histIdx++;
this._restoreHistory(this._history[this._histIdx]);
}
_restoreHistory(snap) {
const s = JSON.parse(snap);
this._gates = s.gates;
this._wires = s.wires;
this._nextId = s.nextId;
this._propagate();
this._updatePanels();
this.draw();
}
/* ══ Fit canvas to element ══ */
fit() {
const el = this._canvas.parentElement || this._canvas;
const dpr = window.devicePixelRatio || 1;
const w = el.clientWidth || 800;
const h = el.clientHeight || 500;
this._canvas.width = w * dpr;
this._canvas.height = h * dpr;
this._canvas.style.width = w + 'px';
this._canvas.style.height = h + 'px';
this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this.draw();
}
/* ══ Drawing ══ */
draw() {
const ctx = this._ctx;
const W = this._canvas.width / (window.devicePixelRatio || 1);
const H = this._canvas.height / (window.devicePixelRatio || 1);
ctx.clearRect(0, 0, W, H);
// grid
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
for (let x = 0; x < W; x += GRID) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
for (let y = 0; y < H; y += GRID) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
// wires
this._wires.forEach(w => this._drawWire(ctx, w));
// ghost wire while dragging
if (this._wireStart) {
const p = this._wireStart;
ctx.beginPath();
ctx.moveTo(p.px, p.py);
ctx.lineTo(this._mouseX || p.px, this._mouseY || p.py);
ctx.strokeStyle = 'rgba(255,255,100,0.7)';
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
}
// gates
this._gates.forEach(g => this._drawGate(ctx, g));
}
_drawWire(ctx, w) {
const g1 = this._gateById(w.from.gateId);
const g2 = this._gateById(w.to.gateId);
if (!g1 || !g2) return;
const p1 = this._portPx(g1, w.from.port);
const p2 = this._portPx(g2, w.to.port);
const val = g1.value;
ctx.beginPath();
// L-route
const mx = (p1.x + p2.x) / 2;
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(mx, p1.y);
ctx.lineTo(mx, p2.y);
ctx.lineTo(p2.x, p2.y);
ctx.strokeStyle = val ? '#00ff88' : 'rgba(255,255,255,0.25)';
ctx.lineWidth = val ? 2.2 : 1.5;
ctx.stroke();
}
_drawGate(ctx, g) {
const def = GATE_DEFS[g.type];
const hw = def.w / 2, hh = def.h / 2;
const x = g.x, y = g.y;
// gate body
const isHigh = g.value === 1;
ctx.beginPath();
ctx.roundRect(x - hw, y - hh, def.w, def.h, 6);
let fill = 'rgba(30,30,60,0.9)';
if (g.type === 'INPUT') fill = isHigh ? 'rgba(0,220,100,0.35)' : 'rgba(60,60,100,0.8)';
if (g.type === 'CLOCK') fill = isHigh ? 'rgba(0,180,255,0.35)' : 'rgba(40,40,100,0.8)';
if (g.type === 'OUTPUT') fill = isHigh ? 'rgba(255,80,80,0.55)' : 'rgba(60,30,30,0.8)';
ctx.fillStyle = fill;
ctx.fill();
const borderCol = g.type === 'OUTPUT' ? (isHigh ? '#ff6060' : 'rgba(255,255,255,0.2)')
: g.type === 'INPUT' ? (isHigh ? '#00cc66' : 'rgba(255,255,255,0.2)')
: g.type === 'CLOCK' ? (isHigh ? '#00aaff' : 'rgba(255,255,255,0.2)')
: 'rgba(155,93,229,0.6)';
ctx.strokeStyle = borderCol;
ctx.lineWidth = 1.5;
ctx.stroke();
// label
ctx.fillStyle = isHigh ? '#fff' : 'rgba(255,255,255,0.75)';
ctx.font = `bold ${def.ins <= 1 ? 10 : 9}px Manrope,sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lbl = (g.type === 'INPUT' || g.type === 'OUTPUT') ? g.label : def.label;
ctx.fillText(lbl, x, y);
// value badge (INPUT / OUTPUT / CLOCK)
if (g.type === 'INPUT' || g.type === 'OUTPUT' || g.type === 'CLOCK') {
ctx.fillStyle = isHigh ? '#00ff88' : 'rgba(255,255,255,0.3)';
ctx.font = 'bold 9px Manrope,sans-serif';
ctx.fillText(isHigh ? '1' : '0', x + hw - 10, y - hh + 9);
}
// ports
this._drawPorts(ctx, g);
}
_drawPorts(ctx, g) {
const def = GATE_DEFS[g.type];
const ports = [];
if (def.outs > 0) ports.push('out');
if (def.ins >= 1) ports.push('in0');
if (def.ins >= 2) ports.push('in1');
for (const port of ports) {
const p = this._portPx(g, port);
const isOut = port === 'out';
const srcGate = isOut ? g : null;
const val = isOut ? g.value : this._getInputValues(g)[port === 'in0' ? 0 : 1];
ctx.beginPath();
ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
ctx.fillStyle = val ? '#00ff88' : 'rgba(255,255,255,0.3)';
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
/* ══ Boolean expression panel ══ */
_updatePanels() {
this._updateExprPanel();
this._updateTruthTable();
}
_buildExpr(gateId, depth) {
if (depth > 20) return '…';
const g = this._gateById(gateId);
if (!g) return '?';
if (g.type === 'INPUT') return g.label;
if (g.type === 'CLOCK') return g.label || 'CLK';
const srcOf = (port) => {
const w = this._wires.find(ww => ww.to.gateId === gateId && ww.to.port === port);
return w ? this._buildExpr(w.from.gateId, depth + 1) : '0';
};
const a = g.type !== 'NOT' && g.type !== 'BUFFER' ? srcOf('in0') : srcOf('in0');
const b = srcOf('in1');
switch (g.type) {
case 'AND': return `(${a}${b})`;
case 'OR': return `(${a} ${b})`;
case 'NOT': return ${a}`;
case 'XOR': return `(${a}${b})`;
case 'NAND': return `¬(${a}${b})`;
case 'NOR': return `¬(${a} ${b})`;
case 'XNOR': return `¬(${a}${b})`;
case 'BUFFER': return a;
default: return '?';
}
}
_updateExprPanel() {
if (!this._exprEl) return;
const outputs = this._gates.filter(g => g.type === 'OUTPUT');
if (outputs.length === 0) {
this._exprEl.textContent = 'Добавьте OUTPUT для вывода выражения';
return;
}
const lines = outputs.map(out => {
const w = this._wires.find(ww => ww.to.gateId === out.id);
if (!w) return `${out.label} = ?`;
const expr = this._buildExpr(w.from.gateId, 0);
return `${out.label} = ${expr}`;
});
this._exprEl.textContent = lines.join(' | ');
}
_updateTruthTable() {
if (!this._tableEl) return;
const inputs = this._gates.filter(g => g.type === 'INPUT');
const outputs = this._gates.filter(g => g.type === 'OUTPUT');
if (inputs.length === 0 || outputs.length === 0) {
this._tableEl.innerHTML = '<span style="color:rgba(255,255,255,0.35)">Добавьте INPUT и OUTPUT</span>';
return;
}
const n = inputs.length;
if (n > 6) {
this._tableEl.innerHTML = '<span style="color:rgba(255,255,255,0.35)">Слишком много входов (макс 6)</span>';
return;
}
const rows = 1 << n;
// save current values
const savedVals = inputs.map(g => g.value);
let html = '<table class="logic-tt"><thead><tr>';
inputs.forEach(g => { html += `<th>${g.label}</th>`; });
outputs.forEach(g => { html += `<th>${g.label}</th>`; });
html += '</tr></thead><tbody>';
// determine current row
const curRow = savedVals.reduce((acc, v, i) => acc | (v << (n - 1 - i)), 0);
for (let r = 0; r < rows; r++) {
inputs.forEach((g, i) => { g.value = (r >> (n - 1 - i)) & 1; });
this._propagate();
const isCur = r === curRow;
html += `<tr${isCur ? ' class="logic-tt-cur"' : ''}>`;
inputs.forEach((g, i) => { html += `<td>${(r >> (n - 1 - i)) & 1}</td>`; });
outputs.forEach(g => { html += `<td style="color:${g.value ? '#00ff88' : 'rgba(255,255,255,0.4)'}">${g.value}</td>`; });
html += '</tr>';
}
html += '</tbody></table>';
this._tableEl.innerHTML = html;
// restore
inputs.forEach((g, i) => { g.value = savedVals[i]; });
this._propagate();
}
/* ══ Presets ══ */
preset(name) {
this._pushHistory();
this._gates = [];
this._wires = [];
this._nextId = 1;
const add = (type, x, y) => this._addGate(type, x, y);
const wire = (a, ap, b, bp) => this._wires.push({ from: { gateId: a.id, port: ap }, to: { gateId: b.id, port: bp } });
switch (name) {
case 'half-adder': {
const A = add('INPUT', 80, 120); A.label = 'A';
const B = add('INPUT', 80, 200); B.label = 'B';
const xor = add('XOR', 200, 120);
const and = add('AND', 200, 200);
const S = add('OUTPUT', 320, 120); S.label = 'S';
const C = add('OUTPUT', 320, 200); C.label = 'C';
wire(A, 'out', xor, 'in0'); wire(B, 'out', xor, 'in1');
wire(A, 'out', and, 'in0'); wire(B, 'out', and, 'in1');
wire(xor, 'out', S, 'in0');
wire(and, 'out', C, 'in0');
break;
}
case 'full-adder': {
const A = add('INPUT', 60, 100); A.label = 'A';
const B = add('INPUT', 60, 180); B.label = 'B';
const Cin = add('INPUT', 60, 260); Cin.label = 'Cin';
const xr1 = add('XOR', 180, 140);
const xr2 = add('XOR', 300, 140);
const an1 = add('AND', 180, 220);
const an2 = add('AND', 300, 220);
const or1 = add('OR', 400, 220);
const S = add('OUTPUT', 420, 140); S.label = 'S';
const Cout = add('OUTPUT', 520, 220); Cout.label = 'Cout';
wire(A, 'out', xr1, 'in0'); wire(B, 'out', xr1, 'in1');
wire(xr1, 'out', xr2, 'in0'); wire(Cin, 'out', xr2, 'in1');
wire(A, 'out', an1, 'in0'); wire(B, 'out', an1, 'in1');
wire(xr1, 'out', an2, 'in0'); wire(Cin, 'out', an2, 'in1');
wire(an1, 'out', or1, 'in0'); wire(an2, 'out', or1, 'in1');
wire(xr2, 'out', S, 'in0');
wire(or1, 'out', Cout, 'in0');
break;
}
case 'rs-latch': {
// Cross-coupled NOR gates: Q=NOR(R,Qbar), Qbar=NOR(S,Q)
// We simplify: R→NOR1, S→NOR2 cross-coupled; initial state stabilised
const R = add('INPUT', 80, 120); R.label = 'R';
const S = add('INPUT', 80, 220); S.label = 'S';
const nr1 = add('NOR', 220, 120);
const nr2 = add('NOR', 220, 220);
const Q = add('OUTPUT', 340, 120); Q.label = 'Q';
const Qb = add('OUTPUT', 340, 220); Qb.label = 'Q̅';
wire(R, 'out', nr1, 'in0');
wire(S, 'out', nr2, 'in1');
// Cross connections — we add them and propagate will stabilise
wire(nr2, 'out', nr1, 'in1');
wire(nr1, 'out', nr2, 'in0');
wire(nr1, 'out', Q, 'in0');
wire(nr2, 'out', Qb, 'in0');
// run propagation twice to stabilise
this._propagate(); this._propagate();
break;
}
case 'd-latch': {
// D latch: Q = D when CLK=1, holds otherwise
// SR from D: S=D∧CLK, R=¬D∧CLK
const D = add('INPUT', 60, 140); D.label = 'D';
const CLK = add('CLOCK', 60, 220); CLK.label = 'CLK'; CLK.freq = 1;
const notD = add('NOT', 160, 180);
const an1 = add('AND', 260, 120);
const an2 = add('AND', 260, 220);
const nr1 = add('NOR', 360, 120);
const nr2 = add('NOR', 360, 220);
const Q = add('OUTPUT', 480, 120); Q.label = 'Q';
const Qb = add('OUTPUT', 480, 220); Qb.label = 'Q̅';
wire(D, 'out', notD, 'in0');
wire(D, 'out', an1, 'in0'); wire(CLK, 'out', an1, 'in1');
wire(notD, 'out', an2, 'in0'); wire(CLK, 'out', an2, 'in1');
wire(an1, 'out', nr1, 'in0'); wire(nr2, 'out', nr1, 'in1');
wire(an2, 'out', nr2, 'in1'); wire(nr1, 'out', nr2, 'in0');
wire(nr1, 'out', Q, 'in0');
wire(nr2, 'out', Qb, 'in0');
break;
}
case 'decoder-2to4': {
const A = add('INPUT', 60, 100); A.label = 'A';
const B = add('INPUT', 60, 200); B.label = 'B';
const nA = add('NOT', 160, 100);
const nB = add('NOT', 160, 200);
const g0 = add('AND', 280, 80);
const g1 = add('AND', 280, 160);
const g2 = add('AND', 280, 240);
const g3 = add('AND', 280, 320);
const o0 = add('OUTPUT', 400, 80); o0.label = 'Y0';
const o1 = add('OUTPUT', 400, 160); o1.label = 'Y1';
const o2 = add('OUTPUT', 400, 240); o2.label = 'Y2';
const o3 = add('OUTPUT', 400, 320); o3.label = 'Y3';
wire(A, 'out', nA, 'in0');
wire(B, 'out', nB, 'in0');
wire(nA, 'out', g0, 'in0'); wire(nB, 'out', g0, 'in1');
wire(A, 'out', g1, 'in0'); wire(nB, 'out', g1, 'in1');
wire(nA, 'out', g2, 'in0'); wire(B, 'out', g2, 'in1');
wire(A, 'out', g3, 'in0'); wire(B, 'out', g3, 'in1');
wire(g0, 'out', o0, 'in0');
wire(g1, 'out', o1, 'in0');
wire(g2, 'out', o2, 'in0');
wire(g3, 'out', o3, 'in0');
break;
}
case 'mux-2to1': {
// Y = (A ∧ ¬S) (B ∧ S)
const A = add('INPUT', 60, 100); A.label = 'A';
const B = add('INPUT', 60, 200); B.label = 'B';
const Sel= add('INPUT', 60, 300); Sel.label = 'S';
const nS = add('NOT', 160, 300);
const an1= add('AND', 280, 120);
const an2= add('AND', 280, 240);
const or1= add('OR', 380, 180);
const Y = add('OUTPUT', 480, 180); Y.label = 'Y';
wire(Sel, 'out', nS, 'in0');
wire(A, 'out', an1, 'in0'); wire(nS, 'out', an1, 'in1');
wire(B, 'out', an2, 'in0'); wire(Sel, 'out', an2, 'in1');
wire(an1, 'out', or1, 'in0'); wire(an2, 'out', or1, 'in1');
wire(or1, 'out', Y, 'in0');
break;
}
default: {
const A = add('INPUT', 100, 160); A.label = 'A';
const B = add('INPUT', 100, 240); B.label = 'B';
const g = add('AND', 240, 200);
const O = add('OUTPUT', 360, 200);
wire(A, 'out', g, 'in0'); wire(B, 'out', g, 'in1');
wire(g, 'out', O, 'in0');
}
}
this._propagate();
this._updatePanels();
this.draw();
}
/* ── Clear ── */
clear() {
this._pushHistory();
this._gates = [];
this._wires = [];
this._propagate();
this._updatePanels();
this.draw();
}
/* ── Destroy ── */
destroy() {
if (this._clockRaf) cancelAnimationFrame(this._clockRaf);
if (this._raf) cancelAnimationFrame(this._raf);
}
/* ── Set tool ── */
setTool(t) { this._tool = t; }
}
/* ═══════════════════════════════════════════════════════════
Global helpers called from HTML
═══════════════════════════════════════════════════════════ */
var logicSim = null;
var _logicTableOpen = true;
function logicTool(t, el) {
if (logicSim) logicSim.setTool(t);
document.querySelectorAll('.lgc-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === t));
}
function logicPreset(name) {
if (logicSim) logicSim.preset(name);
}
function logicClearAll() {
if (logicSim) logicSim.clear();
}
function logicToggleTable() {
_logicTableOpen = !_logicTableOpen;
const panel = document.getElementById('logic-tt-panel');
if (panel) panel.style.display = _logicTableOpen ? '' : 'none';
const btn = document.getElementById('btn-logic-tt');
if (btn) btn.classList.toggle('active', _logicTableOpen);
}
function _openLogic() {
document.getElementById('sim-topbar-title').textContent = 'Логические схемы';
_simShow('sim-logic');
requestAnimationFrame(() => requestAnimationFrame(() => {
const canvas = document.getElementById('logic-canvas');
const exprEl = document.getElementById('logic-expr');
const tableEl = document.getElementById('logic-tt-body');
if (!logicSim) {
logicSim = new LogicSim(canvas, exprEl, tableEl);
} else {
// re-attach panels in case DOM was re-created
logicSim._exprEl = exprEl;
logicSim._tableEl = tableEl;
}
logicSim.fit();
if (logicSim._gates.length === 0) logicSim.preset('half-adder');
logicSim._updatePanels();
logicSim.draw();
// select tool active by default
logicTool('select', null);
}));
}