Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ console.log(svgString);
| `+` | Adder / Add |
| `-` | Subtractor / Sub |
| `*` | Multiplier / Mul |
| `?` | MUX (multiplexer) |
| `$dff` | D flip-flop |

### JavaScript Expression Rendering

Expand Down
89 changes: 88 additions & 1 deletion lib/draw_body.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,94 @@ const gater2 = {
}
};

function drawBody (type, ymin, ymax) {
function drawMux (ymin, ymax, dataInputYs) {
if (ymin === ymax) { ymin = -4; ymax = 4; }

const g = ['g'];

// Trapezoid with fixed inset=6 for consistent angle, pad=11 for label clearance
g.push(['path', {
class: 'gate',
d: 'M -16,' + (ymin - 11) +
' L 0,' + (ymin - 5) +
' L 0,' + (ymax + 5) +
' L -16,' + (ymax + 11) +
' Z'
}]);

// Data input index labels inside the body
if (dataInputYs) {
for (let i = 0; i < dataInputYs.length; i++) {
g.push(['text', {x: -15, y: dataInputYs[i] + 4, class: 'wirename'}]
.concat(tspan.parse(String(i))));
}
}

return g;
}

function drawFlipFlop (ymin, ymax, portInfo) {
if (ymin === ymax) { ymin = -4; ymax = 4; }

const g = ['g'];

// Rectangle body — 32px wide for dual-side labels (x: -32 to 0)
g.push(['path', {
class: 'gate',
d: 'm -32,' + (ymin - 8) + ' 32,0 0,' + (ymax - ymin + 16) + ' -32,0 z'
}]);

// Port labels and markers — portInfo is {activePorts, portRelYs, hasQn, qPortRelY, qnPortRelY}
if (portInfo) {
const activePorts = portInfo.activePorts || {};
const portRelYs = portInfo.portRelYs || {};

// Left-side input labels
if (activePorts.d && portRelYs.d != null) {
g.push(['text', {x: -30, y: portRelYs.d + 4, class: 'wirename'}, 'D']);
}
if (activePorts.s && portRelYs.s != null) {
g.push(['text', {x: -30, y: portRelYs.s + 4, class: 'wirename'}, 'S']);
}
if (activePorts.r && portRelYs.r != null) {
g.push(['text', {x: -30, y: portRelYs.r + 4, class: 'wirename'}, 'R']);
}

// Clock input — triangle marker instead of text label
if (activePorts.clk && portRelYs.clk != null) {
const cy = portRelYs.clk;
g.push(['path', {
class: 'wire',
d: 'M -32,' + (cy - 4) + ' -26,' + cy + ' -32,' + (cy + 4)
}]);
}

// Right-side output labels
if (portInfo.qPortRelY != null) {
g.push(['text', {x: -2, y: portInfo.qPortRelY + 4, class: 'pinname'}, 'Q']);
}

// Qn — label + inversion bubble on right edge
if (portInfo.hasQn && portInfo.qnPortRelY != null) {
g.push(['text', {x: -2, y: portInfo.qnPortRelY + 4, class: 'pinname'}]
.concat(tspan.parse('Q\u0305')));
// Inversion bubble at right edge
g.push(['path', {
class: 'gate',
d: 'M 4,' + portInfo.qnPortRelY + ' C 4,' + (portInfo.qnPortRelY + 1.1) + ' 3.1,' + (portInfo.qnPortRelY + 2) + ' 2,' + (portInfo.qnPortRelY + 2) +
' 0.9,' + (portInfo.qnPortRelY + 2) + ' 0,' + (portInfo.qnPortRelY + 1.1) + ' 0,' + portInfo.qnPortRelY +
' 0,' + (portInfo.qnPortRelY - 1.1) + ' 0.9,' + (portInfo.qnPortRelY - 2) + ' 2,' + (portInfo.qnPortRelY - 2) +
' 3.1,' + (portInfo.qnPortRelY - 2) + ' 4,' + (portInfo.qnPortRelY - 1.1) + ' 4,' + portInfo.qnPortRelY + ' z'
}]);
}
}

return g;
}

function drawBody (type, ymin, ymax, dataInputYs) {
if (type === '$dff') { return drawFlipFlop(ymin, ymax, dataInputYs); }
if (type === '?') { return drawMux(ymin, ymax, dataInputYs); }
if (gater1.is(type)) { return gater1.render(type); }
if (gater2.is(type)) { return gater2.render(type, ymin, ymax); }
return ['text', {x:-14, y:4, class: 'wirename'}].concat(tspan.parse(type));
Expand Down
16 changes: 16 additions & 0 deletions lib/draw_boxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ function drawBoxes (tree, xmax) {
spec.push([32 * (xmax - branch.x), 8 * branch.y]);
}
}
// Pass MUX port positions (in SVG coords) through the spec
if (tree[0].ports) {
spec.ports = tree[0].ports.map(py => 8 * py);
}
// Pass flip-flop metadata through the spec
if (tree[0].name === '$dff') {
spec.portMap = tree[0].portMap;
spec.portPositions = {};
for (const pn of Object.keys(tree[0].portPositions)) {
spec.portPositions[pn] = 8 * tree[0].portPositions[pn];
}
spec.hasQn = tree[0].hasQn;
spec.qnName = tree[0].qnName;
spec.qPortY = tree[0].qPortY != null ? 8 * tree[0].qPortY : null;
spec.qnPortY = tree[0].qnPortY != null ? 8 * tree[0].qnPortY : null;
}
ret.push(drawGate(spec));
for (let i = 1; i < tree.length; i++) {
const branch = tree[i];
Expand Down
230 changes: 230 additions & 0 deletions lib/draw_gate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,239 @@
const tspan = require('tspan');
const drawBody = require('./draw_body.js');

function drawMuxGate (spec) {
const ilen = spec.length;
const ret = ['g'];

const gateX = spec[1][0];
const gateY = spec[1][1];
const ports = spec.ports; // SVG-coord port Y positions from layout

// Collect actual data input positions
const dataYs = [];
const dataXs = [];
for (let i = 3; i < ilen; i++) {
dataYs.push(spec[i][1]);
dataXs.push(spec[i][0]);
}

const numData = dataYs.length;
const portYmin = ports[0];
const portYmax = ports[numData - 1];
const bodyLeftX = gateX - 16;

// Identify wires that need vertical jogs (inputY != portY)
const jogIndices = [];
for (let i = 0; i < numData; i++) {
if (dataYs[i] !== ports[i]) {
jogIndices.push(i);
}
}
const numJogs = jogIndices.length;

// Assign staggered jog columns to avoid wire overlaps.
// Topmost port gets rightmost column (closest to body),
// bottommost gets leftmost — prevents horizontal-to-body segments
// from crossing other wires' verticals.
const inputMaxX = Math.max.apply(null, dataXs);
const colSpacing = numJogs > 0 ? (bodyLeftX - inputMaxX) / (numJogs + 1) : 0;
const jogColumnMap = {};
for (let k = 0; k < numJogs; k++) {
jogColumnMap[jogIndices[k]] = Math.round(bodyLeftX - (k + 1) * colSpacing);
}

// Wire each data input to its corresponding MUX port
for (let i = 0; i < numData; i++) {
const inputX = dataXs[i];
const inputY = dataYs[i];
const portY = ports[i];

if (inputY === portY) {
// Straight horizontal wire from input to body
ret.push(['g',
['path', {
d: 'M' + inputX + ',' + inputY + ' L' + bodyLeftX + ',' + portY,
class: 'wire'
}]
]);
} else {
// L-shaped route via assigned jog column
const jogX = jogColumnMap[i];
ret.push(['g',
['path', {
d: 'M' + inputX + ',' + inputY +
' L' + jogX + ',' + inputY +
' L' + jogX + ',' + portY +
' L' + bodyLeftX + ',' + portY,
class: 'wire'
}]
]);
}
}

// Selector wire: horizontal to below gate center, then vertical up to body bottom
const selX = spec[2][0];
const selY = spec[2][1];
const bodyBottomAbs = gateY + (portYmax - gateY) + 8;
const selWireX = gateX - 8;

ret.push(['g',
['path', {
d: 'M' + selX + ',' + selY + ' L' + selWireX + ',' + selY + ' L' + selWireX + ',' + bodyBottomAbs,
class: 'wire'
}]
]);

// Gate body (trapezoid) sized by port positions, with port-relative index labels
const portRelYs = ports.map(py => py - gateY);

ret.push(['g',
{transform: 'translate(' + gateX + ',' + gateY + ')'},
['title'].concat(tspan.parse(spec[0])),
drawBody(spec[0], portYmin - gateY, portYmax - gateY, portRelYs)
]);

return ret;
}

function drawFlipFlopGate (spec) {
const ret = ['g'];

const gateX = spec[1][0];
const gateY = spec[1][1];
const portMap = spec.portMap; // {d: treeIdx, clk: treeIdx, ...}
const portPositions = spec.portPositions; // {d: svgY, clk: svgY, ...}
const bodyLeftX = gateX - 32;

// Collect input positions and their target port Y positions
const inputEntries = [];
const portNames = Object.keys(portMap);
for (let k = 0; k < portNames.length; k++) {
const pn = portNames[k];
const treeIdx = portMap[pn];
const inputX = spec[treeIdx + 1][0]; // spec[2], spec[3], ... (offset by +1 for gate pos)
const inputY = spec[treeIdx + 1][1];
const portY = portPositions[pn];
inputEntries.push({port: pn, inputX: inputX, inputY: inputY, portY: portY});
}

// Identify wires that need vertical jogs (inputY != portY)
const jogEntries = [];
for (let k = 0; k < inputEntries.length; k++) {
if (inputEntries[k].inputY !== inputEntries[k].portY) {
jogEntries.push(k);
}
}
const numJogs = jogEntries.length;

// Assign staggered jog columns
const inputMaxX = inputEntries.length > 0 ? Math.max.apply(null, inputEntries.map(function (e) { return e.inputX; })) : bodyLeftX;
const colSpacing = numJogs > 0 ? (bodyLeftX - inputMaxX) / (numJogs + 1) : 0;
const jogColumnMap = {};
for (let k = 0; k < numJogs; k++) {
jogColumnMap[jogEntries[k]] = Math.round(bodyLeftX - (k + 1) * colSpacing);
}

// Wire each input to its port position on the body
for (let k = 0; k < inputEntries.length; k++) {
const e = inputEntries[k];
if (e.inputY === e.portY) {
// Straight horizontal wire
ret.push(['g',
['path', {
d: 'M' + e.inputX + ',' + e.inputY + ' L' + bodyLeftX + ',' + e.portY,
class: 'wire'
}]
]);
} else {
// L-shaped route via jog column
const jogX = jogColumnMap[k];
ret.push(['g',
['path', {
d: 'M' + e.inputX + ',' + e.inputY +
' L' + jogX + ',' + e.inputY +
' L' + jogX + ',' + e.portY +
' L' + bodyLeftX + ',' + e.portY,
class: 'wire'
}]
]);
}
}

// Q output wire — short stub from right edge of body going right
// (parent gate handles wiring from gate position to output label)
// The Q port is inside the body; the parent wire connects at gateX,gateY

// Qn output — wire + inversion bubble + label on right side
if (spec.hasQn && spec.qnPortY != null) {
const qnY = spec.qnPortY;
// Wire from after the inversion bubble (4px) to label area
ret.push(['g',
['path', {
d: 'M' + (gateX + 4) + ',' + qnY + ' L' + (gateX + 16) + ',' + qnY,
class: 'wire'
}]
]);
// Qn signal name label — placed past the wire endpoint (gateX + 16)
if (spec.qnName) {
ret.push(['g',
['text', {x: gateX + 20, y: qnY + 4, class: 'wirename'}]
.concat(tspan.parse(spec.qnName))
]);
}
}

// Compute port Y range for body sizing
const allPortYs = [];
for (let k = 0; k < portNames.length; k++) {
allPortYs.push(portPositions[portNames[k]]);
}
// Include Q and Qn output positions in body range
if (spec.qPortY != null) { allPortYs.push(spec.qPortY); }
if (spec.qnPortY != null) { allPortYs.push(spec.qnPortY); }

const portYmin = Math.min.apply(null, allPortYs);
const portYmax = Math.max.apply(null, allPortYs);

// Build portInfo for drawBody
const portRelYs = {};
for (let k = 0; k < portNames.length; k++) {
portRelYs[portNames[k]] = portPositions[portNames[k]] - gateY;
}
const activePorts = {};
for (let k = 0; k < portNames.length; k++) {
activePorts[portNames[k]] = true;
}

const portInfo = {
activePorts: activePorts,
portRelYs: portRelYs,
hasQn: spec.hasQn,
qPortRelY: spec.qPortY != null ? spec.qPortY - gateY : null,
qnPortRelY: spec.qnPortY != null ? spec.qnPortY - gateY : null
};

// Gate body
ret.push(['g',
{transform: 'translate(' + gateX + ',' + gateY + ')'},
['title'].concat(tspan.parse(spec[0])),
drawBody(spec[0], portYmin - gateY, portYmax - gateY, portInfo)
]);

return ret;
}

// ['type', [x,y], [x,y] ... ]
function drawGate (spec) { // ['type', [x,y], [x,y] ... ]

if (spec[0] === '$dff') {
return drawFlipFlopGate(spec);
}

if (spec[0] === '?') {
return drawMuxGate(spec);
}

const ilen = spec.length;
const ys = [];

Expand Down
Loading