From 4f344b027a52877616fa7d4f116c5591d42f6fd5 Mon Sep 17 00:00:00 2001 From: Spencer Williams Date: Thu, 28 May 2026 00:47:05 -0400 Subject: [PATCH 1/3] Adding support for muxes --- README.md | 1 + lib/draw_body.js | 29 +- lib/draw_boxes.js | 4 + lib/draw_gate.js | 99 +++ lib/render.js | 106 ++- ref/functions.md | 1 + report.html | 735 +++++++++++++++++++- report_mux.html | 1633 +++++++++++++++++++++++++++++++++++++++++++++ test/mux.js | 41 ++ 9 files changed, 2612 insertions(+), 37 deletions(-) create mode 100644 report_mux.html create mode 100644 test/mux.js diff --git a/README.md b/README.md index 4d7e081..1d51c7c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ console.log(svgString); | `+` | Adder / Add | | `-` | Subtractor / Sub | | `*` | Multiplier / Mul | +| `?` | MUX (multiplexer) | ### JavaScript Expression Rendering diff --git a/lib/draw_body.js b/lib/draw_body.js index 5093ec8..53016de 100644 --- a/lib/draw_body.js +++ b/lib/draw_body.js @@ -66,7 +66,34 @@ 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 drawBody (type, 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)); diff --git a/lib/draw_boxes.js b/lib/draw_boxes.js index 614c0a1..a143796 100644 --- a/lib/draw_boxes.js +++ b/lib/draw_boxes.js @@ -19,6 +19,10 @@ 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); + } ret.push(drawGate(spec)); for (let i = 1; i < tree.length; i++) { const branch = tree[i]; diff --git a/lib/draw_gate.js b/lib/draw_gate.js index 68e7a9c..3a30382 100644 --- a/lib/draw_gate.js +++ b/lib/draw_gate.js @@ -3,9 +3,108 @@ 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; +} + // ['type', [x,y], [x,y] ... ] function drawGate (spec) { // ['type', [x,y], [x,y] ... ] + if (spec[0] === '?') { + return drawMuxGate(spec); + } + const ilen = spec.length; const ys = []; diff --git a/lib/render.js b/lib/render.js index f4f2815..967923b 100644 --- a/lib/render.js +++ b/lib/render.js @@ -1,37 +1,101 @@ 'use strict'; -function render(tree, state) { - // var y, i, ilen; +function processBranch (tree, i, state) { + const branch = tree[i]; + if (Array.isArray(branch)) { + state = render(branch, { + x: (state.x + 1), + y: state.y, + xmax: state.xmax + }); + } else { + tree[i] = { + name: branch, + x: (state.x + 1), + y: state.y + }; + state.y += 2; + } + return state; +} - state.xmax = Math.max(state.xmax, state.x); +function renderMux (tree, ilen, state) { + const numDataInputs = ilen - 2; + const portSpacing = 2; // fixed grid units between MUX ports - const y = state.y; - const ilen = tree.length; + // Render data inputs with natural spacing (each gets only the space it needs) + for (let i = 2; i < ilen; i++) { + state = processBranch(tree, i, state); + } - for (let i = 1; i < ilen; i++) { + // Compute fixed port positions independent of actual input positions + // Ensure port range is at least as tall as data input range + const dataInputYs = []; + for (let i = 2; i < ilen; i++) { const branch = tree[i]; - if (Array.isArray(branch)) { - state = render(branch, { - x: (state.x + 1), - y: state.y, - xmax: state.xmax - }); - } else { - tree[i] = { - name: branch, - x: (state.x + 1), - y: state.y - }; - state.y += 2; - } + dataInputYs.push(Array.isArray(branch) ? branch[0].y : branch.y); + } + + // Align last port with last data input so tightly-packed simple inputs + // match their ports exactly; complex inputs (with room) absorb the jog + const lastInputY = dataInputYs[dataInputYs.length - 1]; + const portStart = lastInputY - (numDataInputs - 1) * portSpacing; + const ports = []; + for (let i = 0; i < numDataInputs; i++) { + ports.push(portStart + i * portSpacing); } + // Center gate on port range + const portCenter = (ports[0] + ports[numDataInputs - 1]) / 2; tree[0] = { name: tree[0], x: state.x, - y: Math.round((y + (state.y - 2)) / 2) + y: Math.round(portCenter), + ports: ports }; + // Ensure state.y accounts for port range extending below data inputs + const portBottom = ports[ports.length - 1] + 2; + if (portBottom > state.y) { + state.y = portBottom; + } + + // Selector (position 1) placed below data inputs and ports + state = processBranch(tree, 1, state); + + return state; +} + +function render(tree, state) { + state.xmax = Math.max(state.xmax, state.x); + + const y = state.y; + const ilen = tree.length; + const isMux = (tree[0] === '?'); + + if (isMux) { + state = renderMux(tree, ilen, state); + } else { + for (let i = 1; i < ilen; i++) { + state = processBranch(tree, i, state); + } + + if (ilen === 2 && Array.isArray(tree[1])) { + // Single compound input: align to its output position + tree[0] = { + name: tree[0], + x: state.x, + y: tree[1][0].y + }; + } else { + tree[0] = { + name: tree[0], + x: state.x, + y: Math.round((y + (state.y - 2)) / 2) + }; + } + } + state.x--; return state; } diff --git a/ref/functions.md b/ref/functions.md index 4f2d0c0..71e11bf 100644 --- a/ref/functions.md +++ b/ref/functions.md @@ -13,6 +13,7 @@ | xnor | `~^` | | add | `+` | | mul | `*` | +| mux | `?` | ## structure diff --git a/report.html b/report.html index f2d7a33..8e5800f 100644 --- a/report.html +++ b/report.html @@ -1,15 +1,720 @@ -
[["x",["&","a","b"]]]
-xx&aabb -
[["x",["&","a","b","c","d","e"]]]
-xx&aabbccddee -
[["x",["|","a","b"]]]
-xx|aabb -
[["x",["^","a","b"]]]
-xx^aabb -
[["x",["~&","a","b"]]]
-xx~&aabb -
[["x",["~|","a","b"]]]
-xx~|aabb -
[["x",["~|","a0","a1","a2",["~&","a3","a4","a5"]]]]
-xx~|a0a0a1a1a2a2~&a3a3a4a4a5a5 - \ No newline at end of file +
[
+    [
+        "x",
+        [
+            "&",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + & + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "&",
+            "a",
+            "b",
+            "c",
+            "d",
+            "e"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + + + + & + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + e + + + + e + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "|",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + | + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "^",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + ^ + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "~&",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + ~& + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "~|",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + ~| + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "~|",
+            "a0",
+            "a1",
+            "a2",
+            [
+                "~&",
+                "a3",
+                "a4",
+                "a5"
+            ]
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ~| + + + + + + + + a0 + + + + a0 + + + + + + + a1 + + + + a1 + + + + + + + a2 + + + + a2 + + + + + + + + + + + + + + + + + + + + ~& + + + + + + + + a3 + + + + a3 + + + + + + + a4 + + + + a4 + + + + + + + a5 + + + + a5 + + + + + + + + + + +
[
+    [
+        "foo",
+        [
+            42,
+            5
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + foo + + + foo + + + + + + + + + + + + + 42 + 42 + + + + + 5 + + 5 + + + + + + + + +
\ No newline at end of file diff --git a/report_mux.html b/report_mux.html new file mode 100644 index 0000000..0a3ec7e --- /dev/null +++ b/report_mux.html @@ -0,0 +1,1633 @@ +
[
+    [
+        "x",
+        [
+            "?",
+            "sel",
+            "a",
+            "b"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + + + + + + sel + + + + sel + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "sel",
+            "a",
+            "b",
+            "c",
+            "d"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + sel + + + + sel + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "sel",
+            "a",
+            "b",
+            "c",
+            "d",
+            "e",
+            "f",
+            "g",
+            "h"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + + + + + + sel + + + + sel + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + e + + + + e + + + + + + + f + + + + f + + + + + + + g + + + + g + + + + + + + h + + + + h + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "s",
+            [
+                "&",
+                "a",
+                "b"
+            ],
+            "c",
+            "d",
+            "e"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s + + + + s + + + + + + + + + + + + + + + + + & + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + e + + + + e + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "s",
+            [
+                "&",
+                "a",
+                "b",
+                "c",
+                "d",
+                "e"
+            ],
+            "f",
+            "g",
+            "h"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s + + + + s + + + + + + + + + + + + + + + + + + + + + + + + + + & + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + e + + + + e + + + + + + + + f + + + + f + + + + + + + g + + + + g + + + + + + + h + + + + h + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "s",
+            [
+                "&",
+                "a0",
+                "a1",
+                "a2",
+                "a3",
+                "a4"
+            ],
+            [
+                "|",
+                "b0",
+                "b1",
+                "b2",
+                "b3"
+            ],
+            [
+                "&",
+                "c0",
+                "c1",
+                "c2"
+            ],
+            "d"
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s + + + + s + + + + + + + + + + + + + + + + + + + + + + + + + + & + + + + + + + + a0 + + + + a0 + + + + + + + a1 + + + + a1 + + + + + + + a2 + + + + a2 + + + + + + + a3 + + + + a3 + + + + + + + a4 + + + + a4 + + + + + + + + + + + + + + + + + + + + + + + + | + + + + + + + + b0 + + + + b0 + + + + + + + b1 + + + + b1 + + + + + + + b2 + + + + b2 + + + + + + + b3 + + + + b3 + + + + + + + + + + + + + + + + + + + + + & + + + + + + + + c0 + + + + c0 + + + + + + + c1 + + + + c1 + + + + + + + c2 + + + + c2 + + + + + + + + d + + + + d + + + + + + + + + +
[
+    [
+        "x",
+        [
+            "?",
+            "s1",
+            [
+                "?",
+                "s0",
+                "a",
+                "b",
+                "c",
+                "d"
+            ],
+            [
+                "?",
+                "s0",
+                "e",
+                "f",
+                "g",
+                "h"
+            ],
+            [
+                "?",
+                "s0",
+                "i",
+                "j",
+                "k",
+                "l"
+            ],
+            [
+                "?",
+                "s0",
+                "m",
+                "n",
+                "o",
+                "p"
+            ]
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + x + + + x + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s1 + + + + s1 + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + e + + + + e + + + + + + + f + + + + f + + + + + + + g + + + + g + + + + + + + h + + + + h + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + i + + + + i + + + + + + + j + + + + j + + + + + + + k + + + + k + + + + + + + l + + + + l + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + m + + + + m + + + + + + + n + + + + n + + + + + + + o + + + + o + + + + + + + p + + + + p + + + + + + + + + + +
\ No newline at end of file diff --git a/test/mux.js b/test/mux.js new file mode 100644 index 0000000..009e1d8 --- /dev/null +++ b/test/mux.js @@ -0,0 +1,41 @@ +'use strict'; + +const fs = require('fs'); +const onml = require('onml'); + +const lib = require('../lib/'); + +var dat = { + 'mux2' : [['x', ['?', 'sel', 'a', 'b']]], + 'mux4' : [['x', ['?', 'sel', 'a', 'b', 'c', 'd']]], + 'mux8' : [['x', ['?', 'sel', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']]], + 'mux_nested' : [['x', ['?', 's', ['&', 'a', 'b'], 'c', 'd', 'e']]], + 'mux_asymmetric' : [['x', ['?', 's', ['&', 'a', 'b', 'c', 'd', 'e'], 'f', 'g', 'h']]], + 'mux_multi_wide' : [['x', ['?', 's', ['&', 'a0', 'a1', 'a2', 'a3', 'a4'], ['|', 'b0', 'b1', 'b2', 'b3'], ['&', 'c0', 'c1', 'c2'], 'd']]], + 'mux_tree' : [['x', ['?', 's1', + ['?', 's0', 'a', 'b', 'c', 'd'], + ['?', 's0', 'e', 'f', 'g', 'h'], + ['?', 's0', 'i', 'j', 'k', 'l'], + ['?', 's0', 'm', 'n', 'o', 'p'] + ]]] +}; + +describe('mux', function () { + let res = ''; + Object.keys(dat).map(function (key, index) { + it(key, function (done) { + const src = dat[key]; + res += '
' + JSON.stringify(src, null, 4) + '
\n'; + var svg = lib.renderAssign(index, {assign: src }); + res += onml.stringify(svg, 2) + '\n'; + res += '
'; + done(); + }); + }); + after(function () { + res += ''; + fs.writeFileSync('report_mux.html', res, {encoding: 'utf8'}); + }); +}); + +/* eslint-env mocha */ From 05049ed3d07b9391b82d3f788b704f3df3ae171d Mon Sep 17 00:00:00 2001 From: Spencer Williams Date: Sat, 30 May 2026 17:42:13 -0400 Subject: [PATCH 2/3] Adding support for flip-flops --- README.md | 1 + lib/draw_body.js | 60 ++ lib/draw_boxes.js | 12 + lib/draw_gate.js | 131 +++++ lib/render.js | 85 ++- ref/functions.md | 22 + report_flop.html | 1376 +++++++++++++++++++++++++++++++++++++++++++++ test/flop.js | 47 ++ 8 files changed, 1733 insertions(+), 1 deletion(-) create mode 100644 report_flop.html create mode 100644 test/flop.js diff --git a/README.md b/README.md index 1d51c7c..1de9333 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ console.log(svgString); | `-` | Subtractor / Sub | | `*` | Multiplier / Mul | | `?` | MUX (multiplexer) | +| `$dff` | D flip-flop | ### JavaScript Expression Rendering diff --git a/lib/draw_body.js b/lib/draw_body.js index 53016de..5a2a41f 100644 --- a/lib/draw_body.js +++ b/lib/draw_body.js @@ -92,7 +92,67 @@ function drawMux (ymin, ymax, dataInputYs) { 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: -6, 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); } diff --git a/lib/draw_boxes.js b/lib/draw_boxes.js index a143796..d2ec720 100644 --- a/lib/draw_boxes.js +++ b/lib/draw_boxes.js @@ -23,6 +23,18 @@ function drawBoxes (tree, xmax) { 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]; diff --git a/lib/draw_gate.js b/lib/draw_gate.js index 3a30382..f09f09b 100644 --- a/lib/draw_gate.js +++ b/lib/draw_gate.js @@ -98,9 +98,140 @@ function drawMuxGate (spec) { 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); } diff --git a/lib/render.js b/lib/render.js index 967923b..2b69c87 100644 --- a/lib/render.js +++ b/lib/render.js @@ -66,14 +66,97 @@ function renderMux (tree, ilen, state) { return state; } +function renderFlipFlop (tree, state) { + const config = tree[1]; // {d, clk, s, r, qn} + + // Rebuild tree array: expand config object into positioned branches + tree.length = 1; + const portMap = {}; // port name -> tree index + let idx = 1; + + // Order: S (top), D, clk, R (bottom) — visual top-to-bottom + const inputOrder = []; + if (config.s != null) { inputOrder.push({port: 's', signal: config.s}); } + if (config.d != null) { inputOrder.push({port: 'd', signal: config.d}); } + if (config.clk != null) { inputOrder.push({port: 'clk', signal: config.clk}); } + if (config.r != null) { inputOrder.push({port: 'r', signal: config.r}); } + + // Pin gate x-position: compound inputs decrement state.x when they return + // from render(), which would drift subsequent inputs. Use a fixed gateX + // so all inputs are consistently placed at gateX + 2 (extra unit for the + // wider flip-flop body which is 32px = 1 full grid unit wide). + const gateX = state.x; + + for (let k = 0; k < inputOrder.length; k++) { + const signal = inputOrder[k].signal; + tree[idx] = signal; + portMap[inputOrder[k].port] = idx; + + if (Array.isArray(signal)) { + state = render(signal, {x: gateX + 2, y: state.y, xmax: state.xmax}); + } else { + tree[idx] = {name: signal, x: gateX + 2, y: state.y}; + state.xmax = Math.max(state.xmax, gateX + 2); + state.y += 2; + } + idx++; + } + + // Restore state.x to gateX so render() decrements correctly for the parent + state.x = gateX; + + // Calculate port positions from actual rendered positions + const portPositions = {}; + for (const portName of Object.keys(portMap)) { + const treeIdx = portMap[portName]; + const branch = tree[treeIdx]; + portPositions[portName] = Array.isArray(branch) ? branch[0].y : branch.y; + } + + // Compute port Y range for body sizing + const allYs = Object.values(portPositions); + if (allYs.length === 0) { + throw new Error('$dff requires at least one input port (d, clk, s, or r)'); + } + const portYmin = Math.min.apply(null, allYs); + const portYmax = Math.max.apply(null, allYs); + + // Q and Qn output ports at fixed offsets relative to body + // Q aligns with D (or top input), Qn aligns with clk (or bottom input) + const qPortY = portPositions.d != null ? portPositions.d : portYmin; + let qnPortY = null; + if (config.qn != null) { + qnPortY = portPositions.clk != null ? portPositions.clk : portYmax; + } + + // Position gate at Q output port so the parent output wire aligns with Q + tree[0] = { + name: '$dff', + x: gateX, + y: qPortY, + portMap: portMap, + portPositions: portPositions, + hasQn: config.qn != null, + qnName: config.qn || null, + qPortY: qPortY, + qnPortY: qnPortY + }; + + // Note: do NOT state.x-- here; render() does it after we return + return state; +} + function render(tree, state) { state.xmax = Math.max(state.xmax, state.x); const y = state.y; const ilen = tree.length; const isMux = (tree[0] === '?'); + const isFlop = (tree[0] === '$dff' && ilen === 2 && tree[1] !== null && typeof tree[1] === 'object' && !Array.isArray(tree[1])); - if (isMux) { + if (isFlop) { + state = renderFlipFlop(tree, state); + } else if (isMux) { state = renderMux(tree, ilen, state); } else { for (let i = 1; i < ilen; i++) { diff --git a/ref/functions.md b/ref/functions.md index 71e11bf..a0a693a 100644 --- a/ref/functions.md +++ b/ref/functions.md @@ -14,9 +14,31 @@ | add | `+` | | mul | `*` | | mux | `?` | +| dff | `$dff` | ## structure ```js [and, a, b, [or, c, [not, d]]] ``` + +## flip-flop + +Flip-flops use a config object instead of positional inputs. Ports set to `null` or omitted are not drawn. + +```js +['q', ['$dff', { d: 'din', clk: 'clock', r: 'reset' }]] +['q', ['$dff', { d: ['&', 'a', 'b'], clk: 'clk', s: 'set', r: 'rst', qn: 'q_bar' }]] +``` + +Supported ports: + +| port | description | +|-------|------------------------------------------| +| `d` | data input | +| `clk` | clock input (drawn with triangle) | +| `s` | set input | +| `r` | reset input | +| `qn` | inverted output (drawn with bubble) | + +Any port value can be a sub-expression (e.g. `['&', 'a', 'b']`). diff --git a/report_flop.html b/report_flop.html new file mode 100644 index 0000000..22dde93 --- /dev/null +++ b/report_flop.html @@ -0,0 +1,1376 @@ +

dff_minimal

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": "din",
+                "clk": "clock"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + $dff + + + + D + + Q + + + + + + + din + + + + din + + + + + + + clock + + + + clock + + + + + + + + + +

dff_with_reset

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": "din",
+                "clk": "clock",
+                "r": "reset"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + $dff + + + + D + R + + Q + + + + + + + din + + + + din + + + + + + + clock + + + + clock + + + + + + + reset + + + + reset + + + + + + + + + +

dff_with_set_reset

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": "din",
+                "clk": "clock",
+                "s": "set",
+                "r": "reset"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + + + + $dff + + + + D + S + R + + Q + + + + + + + set + + + + set + + + + + + + din + + + + din + + + + + + + clock + + + + clock + + + + + + + reset + + + + reset + + + + + + + + + +

dff_full

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": "din",
+                "clk": "clock",
+                "s": "set",
+                "r": "reset",
+                "qn": "q_bar"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + + + + + + + q_bar + + + + + $dff + + + + D + S + R + + Q + + + + + + + + + + + set + + + + set + + + + + + + din + + + + din + + + + + + + clock + + + + clock + + + + + + + reset + + + + reset + + + + + + + + + +

dff_with_qn

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": "din",
+                "clk": "clock",
+                "qn": "q_n"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + q_n + + + + + $dff + + + + D + + Q + + + + + + + + + + + din + + + + din + + + + + + + clock + + + + clock + + + + + + + + + +

dff_expr_input

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": [
+                    "&",
+                    "a",
+                    "b"
+                ],
+                "clk": "clock"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + $dff + + + + D + + Q + + + + + + + + + + + + + + + + + & + + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + + clock + + + + clock + + + + + + + + + +

dff_expr_full

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": [
+                    "|",
+                    "x",
+                    "y"
+                ],
+                "clk": "clk",
+                "s": "set",
+                "r": [
+                    "&",
+                    "rst0",
+                    "rst1"
+                ],
+                "qn": "q_bar"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + + + + + + + q_bar + + + + + $dff + + + + D + S + R + + Q + + + + + + + + + + + set + + + + set + + + + + + + + + + + + + + + + + | + + + + + + + + x + + + + x + + + + + + + y + + + + y + + + + + + + + clk + + + + clk + + + + + + + + + + + + + + + + + & + + + + + + + + rst0 + + + + rst0 + + + + + + + rst1 + + + + rst1 + + + + + + + + + + +

dff_mux_tree

+
[
+    [
+        "q",
+        [
+            "$dff",
+            {
+                "d": [
+                    "?",
+                    "s1",
+                    [
+                        "?",
+                        "s0",
+                        "a",
+                        "b",
+                        "c",
+                        "d"
+                    ],
+                    [
+                        "?",
+                        "s0",
+                        "e",
+                        "f",
+                        "g",
+                        "h"
+                    ],
+                    [
+                        "?",
+                        "s0",
+                        "i",
+                        "j",
+                        "k",
+                        "l"
+                    ],
+                    [
+                        "?",
+                        "s0",
+                        "m",
+                        "n",
+                        "o",
+                        "p"
+                    ]
+                ],
+                "clk": "clk",
+                "r": "reset"
+            }
+        ]
+    ]
+]
+ + + + + + + + + + + + + + + q + + + q + + + + + + + + + + + + + + + + + $dff + + + + D + R + + Q + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s1 + + + + s1 + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + a + + + + a + + + + + + + b + + + + b + + + + + + + c + + + + c + + + + + + + d + + + + d + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + e + + + + e + + + + + + + f + + + + f + + + + + + + g + + + + g + + + + + + + h + + + + h + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + i + + + + i + + + + + + + j + + + + j + + + + + + + k + + + + k + + + + + + + l + + + + l + + + + + + + + + + + + + + + + + + + + + + + + ? + + + + + 0 + + + 1 + + + 2 + + + 3 + + + + + + + + s0 + + + + s0 + + + + + + + m + + + + m + + + + + + + n + + + + n + + + + + + + o + + + + o + + + + + + + p + + + + p + + + + + + + + + clk + + + + clk + + + + + + + reset + + + + reset + + + + + + + + + +
\ No newline at end of file diff --git a/test/flop.js b/test/flop.js new file mode 100644 index 0000000..61d2076 --- /dev/null +++ b/test/flop.js @@ -0,0 +1,47 @@ +'use strict'; + +const fs = require('fs'); +const onml = require('onml'); + +const lib = require('../lib/'); + +var dat = { + 'dff_minimal': [['q', ['$dff', {d: 'din', clk: 'clock'}]]], + 'dff_with_reset': [['q', ['$dff', {d: 'din', clk: 'clock', r: 'reset'}]]], + 'dff_with_set_reset': [['q', ['$dff', {d: 'din', clk: 'clock', s: 'set', r: 'reset'}]]], + 'dff_full': [['q', ['$dff', {d: 'din', clk: 'clock', s: 'set', r: 'reset', qn: 'q_bar'}]]], + 'dff_with_qn': [['q', ['$dff', {d: 'din', clk: 'clock', qn: 'q_n'}]]], + 'dff_expr_input': [['q', ['$dff', {d: ['&', 'a', 'b'], clk: 'clock'}]]], + 'dff_expr_full': [['q', ['$dff', {d: ['|', 'x', 'y'], clk: 'clk', s: 'set', r: ['&', 'rst0', 'rst1'], qn: 'q_bar'}]]], + 'dff_mux_tree': [['q', ['$dff', { + d: ['?', 's1', + ['?', 's0', 'a', 'b', 'c', 'd'], + ['?', 's0', 'e', 'f', 'g', 'h'], + ['?', 's0', 'i', 'j', 'k', 'l'], + ['?', 's0', 'm', 'n', 'o', 'p'] + ], + clk: 'clk', + r: 'reset' + }]]] +}; + +describe('flop', function () { + let res = ''; + Object.keys(dat).map(function (key, index) { + it(key, function (done) { + const src = dat[key]; + res += '

' + key + '

\n'; + res += '
' + JSON.stringify(src, null, 4) + '
\n'; + var svg = lib.renderAssign(index, {assign: src }); + res += onml.stringify(svg, 2) + '\n'; + res += '
'; + done(); + }); + }); + after(function () { + res += ''; + fs.writeFileSync('report_flop.html', res, {encoding: 'utf8'}); + }); +}); + +/* eslint-env mocha */ From 8b018da9aac72518750031d4cfd811e9c485ea1d Mon Sep 17 00:00:00 2001 From: Spencer Williams Date: Tue, 2 Jun 2026 01:35:59 -0400 Subject: [PATCH 3/3] Fix issue with Q-bar placement --- lib/draw_body.js | 2 +- report_flop.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/draw_body.js b/lib/draw_body.js index 5a2a41f..f4826f1 100644 --- a/lib/draw_body.js +++ b/lib/draw_body.js @@ -135,7 +135,7 @@ function drawFlipFlop (ymin, ymax, portInfo) { // Qn — label + inversion bubble on right edge if (portInfo.hasQn && portInfo.qnPortRelY != null) { - g.push(['text', {x: -6, y: portInfo.qnPortRelY + 4, class: 'pinname'}] + g.push(['text', {x: -2, y: portInfo.qnPortRelY + 4, class: 'pinname'}] .concat(tspan.parse('Q\u0305'))); // Inversion bubble at right edge g.push(['path', { diff --git a/report_flop.html b/report_flop.html index 22dde93..54e5ab0 100644 --- a/report_flop.html +++ b/report_flop.html @@ -362,7 +362,7 @@ R Q - + @@ -479,7 +479,7 @@ D Q - + @@ -708,7 +708,7 @@ R Q - +