diff --git a/src/cells/arith.mjs b/src/cells/arith.mjs index 33bc254..5de1f1c 100644 --- a/src/cells/arith.mjs +++ b/src/cells/arith.mjs @@ -6,30 +6,56 @@ import bigInt from 'big-integer'; import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; -// Unary arithmetic operations -export const Arith11 = Gate.define('Arith11', { +// base class for arithmetic operations displayed with a circle +export const Arith = Gate.define('Arith', { size: { width: 40, height: 40 }, attrs: { - 'circle.body': { r: 20, cx: 20, cy: 20 }, + 'circle.body': { refR: 0.5, refCx: 0.5, refCy: 0.5 }, 'text.oper': { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' + refX: .5, refY: .5, + xAlignment: 'middle', yAlignment: 'middle', + fontSize: '12pt' + } + }, + ports: { + groups: { + 'in': { position: { name: 'left', args: { dx: 10 } }, attrs: { 'line.wire': { x2: -30 }, 'circle.port': { refX: -30 } }, z: -1 }, + 'out': { position: { name: 'right', args: { dx: -10 } }, attrs: { 'line.wire': { x2: 30 }, 'circle.port': { refX: 30 } }, z: -1 } } } }, { - constructor: function(args) { - if (!args.bits) args.bits = { in: 1, out: 1 }; - if (!args.signed) args.signed = false; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits.out }), - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.bits.in }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + markup: Gate.prototype.markup.concat([{ + tagName: 'circle', + className: 'body' + }, { + tagName: 'text', + className: 'oper' + } + ]), + gateParams: Gate.prototype.gateParams.concat(['bits', 'signed']), + unsupportedPropChanges: Gate.prototype.unsupportedPropChanges.concat(['signed']) +}); + +// Unary arithmetic operations +export const Arith11 = Arith.define('Arith11', { + /* default properties */ + bits: { in: 1, out: 1 }, + signed: false +}, { + initialize: function() { + Arith.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: bits.in }, + { id: 'out', group: 'out', dir: 'out', bits: bits.out } + ]); + + this.on('change:bits', (_,bits) => { + this.setPortBits('in', bits.in); + this.setPortBits('out', bits.out); + }); }, operation: function(data) { const bits = this.get('bits'); @@ -38,35 +64,31 @@ export const Arith11 = Gate.define('Arith11', { return { out: help.bigint2sig(this.arithop(help.sig2bigint(data.in, this.get('signed'))), bits.out) }; - }, - gateParams: Gate.prototype.gateParams.concat(['bits', 'signed']) + } }); // Binary arithmetic operations -export const Arith21 = Gate.define('Arith21', { - size: { width: 40, height: 40 }, - attrs: { - 'circle.body': { r: 20, cx: 20, cy: 20 }, - 'text.oper': { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' - } - } +export const Arith21 = Arith.define('Arith21', { + /* default properties */ + bits: { in1: 1, in2: 1, out: 1 }, + signed: { in1: false, in2: false } }, { - constructor: function(args) { - if (!args.bits) args.bits = { in1: 1, in2: 1, out: 1 }; - if (!args.signed) args.signed = { in1: false, in2: false }; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits.out }), - this.addWire(args, 'left', 0.3, { id: 'in1', dir: 'in', bits: args.bits.in1 }), - this.addWire(args, 'left', 0.7, { id: 'in2', dir: 'in', bits: args.bits.in2 }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function() { + Arith.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in1', group: 'in', dir: 'in', bits: bits.in1 }, + { id: 'in2', group: 'in', dir: 'in', bits: bits.in2 }, + { id: 'out', group: 'out', dir: 'out', bits: bits.out } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in1', bits.in1); + this.setPortBits('in2', bits.in2); + this.setPortBits('out', bits.out); + }); }, operation: function(data) { const bits = this.get('bits'); @@ -78,36 +100,32 @@ export const Arith21 = Gate.define('Arith21', { help.sig2bigint(data.in1, sgn.in1 && sgn.in2), help.sig2bigint(data.in2, sgn.in1 && sgn.in2)), bits.out) }; - }, - gateParams: Gate.prototype.gateParams.concat(['bits', 'signed']) + } }); // Bit shift operations -export const Shift = Gate.define('Shift', { - size: { width: 40, height: 40 }, - attrs: { - 'circle.body': { r: 20, cx: 20, cy: 20 }, - 'text.oper': { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' - } - } +export const Shift = Arith.define('Shift', { + /* default properties */ + bits: { in1: 1, in2: 1, out: 1 }, + signed: { in1: false, in2: false, out: false }, + fillx: false }, { - constructor: function(args) { - if (!args.bits) args.bits = { in1: 1, in2: 1, out: 1 }; - if (!args.signed) args.signed = { in1: false, in2: false, out: false }; - if (!args.fillx) args.fillx = false; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits.out }), - this.addWire(args, 'left', 0.3, { id: 'in1', dir: 'in', bits: args.bits.in1 }), - this.addWire(args, 'left', 0.7, { id: 'in2', dir: 'in', bits: args.bits.in2 }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function() { + Arith.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in1', group: 'in', dir: 'in', bits: bits.in1 }, + { id: 'in2', group: 'in', dir: 'in', bits: bits.in2 }, + { id: 'out', group: 'out', dir: 'out', bits: bits.out } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in1', bits.in1); + this.setPortBits('in2', bits.in2); + this.setPortBits('out', bits.out); + }); }, operation: function(data) { const bits = this.get('bits'); @@ -125,34 +143,31 @@ export const Shift = Gate.define('Shift', { : my_in.slice(am).concat(Vector3vl.make(am, fillx ? 0 : sgn.out ? my_in.get(my_in.bits-1) : -1)); return { out: out.slice(0, bits.out) }; }, - gateParams: Gate.prototype.gateParams.concat(['bits', 'signed', 'fillx']) + gateParams: Arith.prototype.gateParams.concat(['fillx']), + unsupportedPropChanges: Arith.prototype.unsupportedPropChanges.concat(['fillx']) }); // Comparison operations -export const Compare = Gate.define('Compare', { - size: { width: 40, height: 40 }, - attrs: { - 'circle.body': { r: 20, cx: 20, cy: 20 }, - 'text.oper': { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' - } - } +export const Compare = Arith.define('Compare', { + /* default properties */ + bits: { in1: 1, in2: 1 }, + signed: { in1: false, in2: false } }, { - constructor: function(args) { - if (!args.bits) args.bits = { in1: 1, in2: 1 }; - if (!args.signed) args.signed = { in1: false, in2: false }; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: 1 }), - this.addWire(args, 'left', 0.3, { id: 'in1', dir: 'in', bits: args.bits.in1 }), - this.addWire(args, 'left', 0.7, { id: 'in2', dir: 'in', bits: args.bits.in2 }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function() { + Arith.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in1', group: 'in', dir: 'in', bits: bits.in1 }, + { id: 'in2', group: 'in', dir: 'in', bits: bits.in2 }, + { id: 'out', group: 'out', dir: 'out', bits: 1 } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in1', bits.in1); + this.setPortBits('in2', bits.in2); + }); }, operation: function(data) { const bits = this.get('bits'); @@ -164,35 +179,11 @@ export const Compare = Gate.define('Compare', { help.sig2bigint(data.in1, sgn.in1 && sgn.in2), help.sig2bigint(data.in2, sgn.in1 && sgn.in2))) }; - }, - gateParams: Gate.prototype.gateParams.concat(['bits', 'signed']) + } }); -export const EqCompare = Gate.define('EqCompare', { - size: { width: 40, height: 40 }, - attrs: { - 'circle.body': { r: 20, cx: 20, cy: 20 }, - 'text.oper': { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' - } - } -}, { - constructor: function(args) { - if (!args.bits) args.bits = { in1: 1, in2: 1 }; - if (!args.signed) args.signed = { in1: false, in2: false }; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: 1 }), - this.addWire(args, 'left', 0.3, { id: 'in1', dir: 'in', bits: args.bits.in1 }), - this.addWire(args, 'left', 0.7, { id: 'in2', dir: 'in', bits: args.bits.in2 }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); - }, +// Equality operations +export const EqCompare = Compare.define('EqCompare', {}, { operation: function(data) { const bits = this.get('bits'); const sgn = this.get('signed'); @@ -203,8 +194,7 @@ export const EqCompare = Gate.define('EqCompare', { return { out: this.bincomp(in1, in2) }; - }, - gateParams: Gate.prototype.gateParams.concat(['bits', 'signed']) + } }); // Negation diff --git a/src/cells/base.mjs b/src/cells/base.mjs index 175332e..cc9a9fe 100644 --- a/src/cells/base.mjs +++ b/src/cells/base.mjs @@ -5,91 +5,183 @@ import bigInt from 'big-integer'; import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; +export const portGroupAttrs = { + 'line.wire': { + stroke: '#4B4F6A', + x1: 0, y1: 0, + x2: undefined, y2: 0 + }, + 'circle.port': { + magnet: undefined, + r: 7, + stroke: 'black', + fill: 'white', + strokeWidth: 2, + strokeOpacity: 0.5, + jointSelector: '.port' + }, + 'text.bits': { + ref: 'circle.port', + fill: 'black', + fontSize: '7pt' + }, + 'text.iolabel': { + yAlignment: 'middle', + fill: 'black', + fontSize: '8pt' + }, + 'path.decor': { + stroke: 'black', + fill: 'transparent', + d: undefined + } +}; + // Common base class for gate models export const Gate = joint.shapes.basic.Generic.define('Gate', { + /* default properties */ + propagation: 1, + label: '', + size: { width: 80, height: 30 }, inputSignals: {}, outputSignals: {}, - propagation: 1, attrs: { '.': { magnet: false }, - 'rect.body': { width: 80, height: 30 }, - 'circle.port': { r: 7, stroke: 'black', fill: 'transparent', 'stroke-width': 2 }, - 'text.label': { - text: '', 'ref-x': 0.5, 'ref-dy': 2, 'text-anchor': 'middle', + '.body': { stroke: 'black', strokeWidth: 2 }, + 'text': { + fontSize: '8pt', fill: 'black' }, - 'text.bits': { - fill: 'black' + 'text.label': { + refX: .5, refDy: 3, + xAlignment: 'middle' + } + }, + ports: { + groups: { + 'in': { + position: 'left', + attrs: _.merge({}, portGroupAttrs, { 'line.wire': { x2: -20 }, 'circle.port': { magnet: 'passive', refX: -20 }, 'text.bits': { refDx: 3, refY: -4, textAnchor: 'start' }, 'text.iolabel': { refX: 5, textAnchor: 'start' } }) + }, + 'out': { + position: 'right', + attrs: _.merge({}, portGroupAttrs, { 'line.wire': { x2: 20 }, 'circle.port': { magnet: true, refX: 20 }, 'text.bits': { refX: -3, refY: -4, textAnchor: 'end' }, 'text.iolabel': { refX: -5, textAnchor: 'end' } }) + } } } }, { operation: function() { return {}; }, - constructor: function(args) { - if ('label' in args) _.set(args, ['attrs', 'text.label', 'text'], args.label); - joint.shapes.basic.Generic.prototype.constructor.apply(this, arguments); - }, initialize: function() { joint.shapes.basic.Generic.prototype.initialize.apply(this, arguments); - this.listenTo(this, 'change:size', (model, size) => this.attr('rect.body', size)); + + this.bindAttrToProp('text.label/text', 'label'); + if (this.unsupportedPropChanges.length > 0) { + this.on(this.unsupportedPropChanges.map(prop => 'change:'+prop).join(' '), function(model, _, opt) { + if (opt.init) return; + + if (opt.propertyPath) + console.warn('Beta property change support: "' + opt.propertyPath + '" changes on ' + model.prop('type') + ' are currently not reflected.'); + else + console.warn('Beta property change support: changes on ' + model.prop('type') + ' are currently not reflected. Also consider using Cell.prop() instead of Model.set().'); + }); + } }, - addWire: function(args, side, loc, port) { - const vert = side == 'top'; - const wire_args = { - d: 'M 0 0 L ' + (vert ? '0 40' : side == 'left' ? '40 0' : '-40 0') - }; - const circle_args = { - magnet: port.dir == 'out' ? true : 'passive', - port: port - }; - const ref_args = {}; - ref_args[vert ? 'ref-x' : 'ref-y'] = loc; - if (side == 'left') { - ref_args['ref-x'] = -20; - } else if (side == 'right') { - ref_args['ref-dx'] = 20; - } else if (side == 'top') { - ref_args['ref-y'] = -10; // currently mux only - } else console.assert(false); - _.assign(wire_args, ref_args); - _.assign(circle_args, ref_args); - _.set(args, ['attrs', 'path.wire.port_' + port.id], wire_args); - _.set(args, ['attrs', 'circle.port_' + port.id], circle_args); - let markup = ''; - - markup += ''; - const bits_args = { - text: port.bits > 1 ? port.bits : "", - ref: 'circle.port_' + port.id - }; - if (vert) { - // TODO - } else { - bits_args['ref-y'] = -3; - bits_args['text-anchor'] = 'middle'; + bindAttrToProp: function(attr, prop) { + this.attr(attr, this.prop(prop)); + this.on('change:' + prop, (_, val) => this.attr(attr, val)); + }, + setPortBits: function(port, bits) { + this.portProp(port, 'bits', bits); + this.portProp(port, 'attrs/text.bits/text', bits > 1 ? bits : ''); + this.resetPortSignals(port, bits); + //todo: handle connected wires + console.warn('Beta property change support: Connected wires are currently not rechecked for connection'); + }, + resetPortSignals: function(port, bits) { + const signame = this.portProp(port, 'dir') === 'in' ? 'inputSignals' : 'outputSignals'; + this.prop([signame, this.portProp(port, 'id')], Vector3vl.xes(bits)); + }, + removePortSignals: function(port) { + const signame = port.dir === 'in' ? 'inputSignals' : 'outputSignals'; + this.removeProp([signame, port.id]); + }, + addPort: function(port, opt = {}) { + joint.shapes.basic.Generic.prototype.addPort.apply(this, arguments); + this.resetPortSignals(port.id, port.bits); + if (opt.labelled) { + this.portProp(port, 'attrs/text.iolabel/text', 'portlabel' in port ? port.portlabel : port.id); + if (port.polarity === false) + this.portProp(port, 'attrs/text.iolabel/text-decoration', 'overline'); + if (port.decor) { + console.assert(port.group == 'in'); + this.portProp(port, 'attrs/text.iolabel/refX', 10); + } } - if (side == 'left') { - bits_args['ref-dx'] = 6; - } else if (side == 'right') { - bits_args['ref-x'] = -6; - } else if (side == 'top') { - bits_args['ref-y'] = 6; - } else console.assert(false); - _.set(args, ['attrs', 'text.bits.port_' + port.id], bits_args); - - const signame = port.dir == 'in' ? 'inputSignals' : 'outputSignals'; - if (_.get(args, [signame, port.id]) === undefined) { - _.set(args, [signame, port.id], Vector3vl.xes(port.bits)); + if (port.decor) { + this.portProp(port, 'attrs/path.decor/d', port.decor); + } + }, + addPorts: function(ports, opt) { + ports.forEach((port) => this.addPort(port, opt), this); + }, + removePort: function(port, opt) { + joint.shapes.basic.Generic.prototype.removePort.apply(this, arguments); + this.removePortSignals(port.id !== undefined ? port.id : port); + }, + removePorts: function(ports, opt) { + ports.forEach((port) => this.removePort(port, opt), this); + }, + getStackedPosition: function(opt) { + return function(portsArgs, elBBox) { + // ports stacked from top to bottom or left to right + const side = opt.side || 'left'; + const step = opt.step || 16; + const offset = opt.offset || 12; + const x = side == 'left' ? elBBox.topLeft().x : side == 'right' ? elBBox.topRight().x : undefined; + const y = side == 'top' ? elBBox.topLeft().y : side == 'bottom' ? elBBox.bottomRight().y : undefined; + if (x !== undefined) { + return _.map(portsArgs, function(portArgs, index) { + index += portArgs.idxOffset || 0; + return joint.g.Point({ x: x, y: index*step + offset }); + }); + } else { + return _.map(portsArgs, function(portArgs, index) { + index += portArgs.idxOffset || 0; + return joint.g.Point({ x: index*step + offset, y: y }); + }); + } } - return '' + markup + ''; }, + portMarkup: [{ + tagName: 'line', + className: 'wire' + }, { + tagName: 'circle', + className: 'port' + }, { + tagName: 'text', + className: 'bits' + }, { + tagName: 'text', + className: 'iolabel' + }, { + tagName: 'path', + className: 'decor' + }], + //portLabelMarkup: null, //todo: see https://github.com/clientIO/joint/issues/1278 + markup: [{ + tagName: 'text', + className: 'label' + }], getGateParams: function(layout) { return _.cloneDeep(_.pick(this.attributes, this.gateParams.concat(layout ? this.gateLayoutParams : []))); }, gateParams: ['label', 'type', 'propagation'], - gateLayoutParams: ['position'] + gateLayoutParams: ['position'], + unsupportedPropChanges: [] }); export const GateView = joint.dia.ElementView.extend({ @@ -103,20 +195,19 @@ export const GateView = joint.dia.ElementView.extend({ confirmUpdate(flags) { if (this.hasFlag(flags, 'flag:inputSignals')) { this.updatePortSignals('in', this.model.get('inputSignals')); - }; + } if (this.hasFlag(flags, 'flag:outputSignals')) { this.updatePortSignals('out', this.model.get('outputSignals')); - }; + } joint.dia.ElementView.prototype.confirmUpdate.apply(this, arguments); }, updatePortSignals(dir, signal) { - for (const port of Object.values(this.model.ports)) { + for (const port of this.model.getPorts()) { if (port.dir !== dir) continue; - let classes = ['port', port.dir, 'port_' + port.id]; - if (signal[port.id].isHigh) classes.push('live'); - else if (signal[port.id].isLow) classes.push('low'); - else if (signal[port.id].isDefined) classes.push('defined'); - this.$('circle.port_' + port.id).attr('class', classes.join(' ')); + const portel = this.el.querySelector('[port='+port.id+']'); + portel.classList.toggle('live', signal[port.id].isHigh); + portel.classList.toggle('low', signal[port.id].isLow); + portel.classList.toggle('defined', signal[port.id].isDefined); } }, render() { @@ -321,53 +412,67 @@ export const WireView = joint.dia.LinkView.extend({ export const Box = Gate.define('Box', { attrs: { - 'text.iolabel': { fill: 'black', 'dominant-baseline': 'ideographic' }, - 'path.decor': { stroke: 'black', fill: 'transparent' } + 'rect.body': { refWidth: 1, refHeight: 1 }, + '.tooltip': { refX: 0, refY: -30, height: 30 } } }, { - addLabelledWire: function(args, lblmarkup, side, loc, port) { - console.assert(side == 'left' || side == 'right'); - const ret = this.addWire(args, side, loc, port); - lblmarkup.push(''); - const textattrs = { - 'ref-y': loc, 'text-anchor': side == 'left' ? 'start' : 'end', - text: 'label' in port ? port.label : port.id - }; - const dist = port.clock ? 10 : 5; - if (side == 'left') textattrs['ref-x'] = dist; - else if (side == 'right') textattrs['ref-dx'] = -dist; - if (port.polarity === false) textattrs['text-decoration'] = 'overline'; - if (port.clock) { - console.assert(side == 'left'); - let vpath = [ - [0, -6], - [6, 0], - [0, 6] - ]; - const path = 'M' + vpath.map(l => l.join(' ')).join(' L'); - lblmarkup.push(''); - _.set(args, ['attrs', 'path.decor.port_' + port.id], { - 'ref-x': 0, 'ref-y': loc - }); + initialize: function(args) { + Gate.prototype.initialize.apply(this, arguments); + this.on('change:size', (_, size) => { + if (size.width > this.tooltipMinWidth) { + this.attr('.tooltip', { refWidth: 1, width: null }); + } else { + this.attr('.tooltip', { refWidth: null, width: this.tooltipMinWidth }); + } + }); + this.trigger('change:size', this, this.prop('size')); + }, + markup: Gate.prototype.markup.concat([{ + tagName: 'rect', + className: 'body' } - _.set(args, ['attrs', 'text.iolabel.port_' + port.id], textattrs); - return ret; - } + ]), + markupZoom: [{ + tagName: 'foreignObject', + className: 'tooltip', + children: [{ + tagName: 'body', + namespaceURI: 'http://www.w3.org/1999/xhtml', + children: [{ + tagName: 'a', + className: 'zoom', + textContent: '🔍', + style: { cursor: 'pointer' } + }] + }] + }], + tooltipMinWidth: 20, + decorClock: 'M' + [ + [0, -6], + [6, 0], + [0, 6] + ].map(l => l.join(' ')).join(' L') }); +// base class for gates displayed as a box export const BoxView = GateView.extend({ + autoResizeBox: false, render: function() { GateView.prototype.render.apply(this, arguments); - if (this.model.get('box_resized')) return; - this.model.set('box_resized', true); - const labels = Array.from(this.el.querySelectorAll('text.iolabel')); - const leftlabels = labels.filter(x => x.classList.contains('iolabel_left')); - const rightlabels = labels.filter(x => x.classList.contains('iolabel_right')); + if (this.autoResizeBox) { + if (this.model.get('box_resized')) return; + this.model.set('box_resized', true); + this.model.prop('size/width', this.calculateBoxWidth()); + } + }, + calculateBoxWidth: function() { + const leftlabels = Array.from(this.el.querySelectorAll('[port-group=in] > text.iolabel')); + const rightlabels = Array.from(this.el.querySelectorAll('[port-group=out] > text.iolabel')); const leftwidth = Math.max(...leftlabels.map(x => x.getBBox().width)); const rightwidth = Math.max(...rightlabels.map(x => x.getBBox().width)); const fixup = x => x == -Infinity ? -5 : x; const width = fixup(leftwidth) + fixup(rightwidth) + 25; - this.model.set('size', _.set(_.clone(this.model.get('size')), 'width', width)); + return width; } }); diff --git a/src/cells/bus.mjs b/src/cells/bus.mjs index d302673..5692592 100644 --- a/src/cells/bus.mjs +++ b/src/cells/bus.mjs @@ -1,40 +1,59 @@ "use strict"; import * as joint from 'jointjs'; -import { Gate, GateView, Box, BoxView } from './base'; +import { Box, BoxView } from './base'; import bigInt from 'big-integer'; import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; // Bit extending -export const BitExtend = Gate.define('BitExtend', { +export const BitExtend = Box.define('BitExtend', { + /* default properties */ + extend: { input: 1, output: 1 }, propagation: 0, + attrs: { "text.value": { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'y-alignment': 'middle', - 'text-anchor': 'middle', - 'font-size': '14px' + refX: .5, refY: .5, + xAlignment: 'middle', yAlignment: 'middle' } } }, { - constructor: function(args) { - console.assert(args.extend.input <= args.extend.output); - this.markup = [ - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.extend.input}), - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.extend.output}), - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function() { + Box.prototype.initialize.apply(this, arguments); + + const extend = this.prop('extend'); + + console.assert(extend.input <= extend.output); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: extend.input }, + { id: 'out', group: 'out', dir: 'out', bits: extend.output } + ]); + + this.on('change:extend', (_, extend) => { + this.setPortBits('in', extend.input); + this.setPortBits('out', extend.output); + }); }, operation: function(data) { const ex = this.get('extend'); return { out: data.in.concat(Vector3vl.make(ex.output - ex.input, this.extbit(data.in))) }; }, - gateParams: Gate.prototype.gateParams.concat(['extend']) + markup: Box.prototype.markup.concat([{ + tagName: 'text', + className: 'value' + } + ]), + gateParams: Box.prototype.gateParams.concat(['extend']) +}); +export const BitExtendView = BoxView.extend({ + autoResizeBox: true, + calculateBoxWidth: function() { + const text = this.el.querySelector('text.value'); + return text.getBBox().width + 10; + } }); -export const BitExtendView = GateView; export const ZeroExtend = BitExtend.define('ZeroExtend', { attrs: { @@ -60,59 +79,64 @@ export const SignExtendView = BitExtendView; // Bus slicing export const BusSlice = Box.define('BusSlice', { + /* default properties */ + slice: { first: 0, count: 1, total: 2 }, propagation: 0, - size: { width: 40, height: 24 }, + + size: { width: 40, height: 24 } }, { - constructor: function(args) { - const lblmarkup = []; - const markup = []; - args.bits = 0; - const val = args.slice.count == 1 ? args.slice.first : - args.slice.first + "-" + (args.slice.first + args.slice.count - 1); - this.markup = [ - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.slice.total}), - this.addLabelledWire(args, lblmarkup, 'right', 0.5, { id: 'out', dir: 'out', bits: args.slice.count, label: val}), - '', - lblmarkup.join(''), - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function() { + Box.prototype.initialize.apply(this, arguments); + + const slice = this.prop('slice'); + + const val = slice.count == 1 ? slice.first : + slice.first + "-" + (slice.first + slice.count - 1); + + this.addPort({ id: 'in', group: 'in', dir: 'in', bits: slice.total }); + this.addPort({ id: 'out', group: 'out', dir: 'out', bits: slice.count, portlabel: val }, { labelled: true }); }, operation: function(data) { const s = this.get('slice'); return { out: data.in.slice(s.first, s.first + s.count) }; }, - gateParams: Gate.prototype.gateParams.concat(['slice']) + gateParams: Box.prototype.gateParams.concat(['slice']) +}); +export const BusSliceView = BoxView.extend({ + autoResizeBox: true }); -export const BusSliceView = BoxView; // Bus grouping export const BusRegroup = Box.define('BusRegroup', { - propagation: 0, + /* default properties */ + groups: [1], + propagation: 0 }, { - constructor: function(args) { - const markup = []; - const lblmarkup = []; - args.bits = 0; - const size = { width: 40, height: args.groups.length*16+8 }; - args.size = size; - for (const [num, gbits] of args.groups.entries()) { - const y = num*16+12; - const lbl = args.bits + (gbits > 1 ? '-' + (args.bits + gbits - 1) : ''); - args.bits += gbits; - markup.push(this.addLabelledWire(args, lblmarkup, this.group_dir == 'out' ? 'right' : 'left', y, - { id: this.group_dir + num, dir: this.group_dir, bits: gbits, label: lbl })); + initialize: function() { + Box.prototype.initialize.apply(this, arguments); + + var bits = 0; + const groups = this.prop('groups'); + + const size = { width: 40, height: groups.length*16+8 }; + this.prop('size', size); + + for (const [num, gbits] of groups.entries()) { + const lbl = bits + (gbits > 1 ? '-' + (bits + gbits - 1) : ''); + bits += gbits; + this.addPort({ id: this.group_dir + num, group: this.group_dir, dir: this.group_dir, bits: gbits, portlabel: lbl }, { labelled: true }); } + this.prop('bits', bits); + const contra = this.group_dir == 'out' ? 'in' : 'out'; - markup.push(this.addWire(args, this.group_dir == 'out' ? 'left' : 'right', 0.5, - { id: contra, dir: contra, bits: args.bits })); - markup.push(''); - markup.push(lblmarkup.join('')); - this.markup = markup.join(''); - Gate.prototype.constructor.apply(this, arguments); + this.addPort({ id: contra, group: contra, dir: contra, bits: bits }); }, - gateParams: Gate.prototype.gateParams.concat(['groups']) + gateParams: Box.prototype.gateParams.concat(['groups']), + unsupportedPropChanges: Box.prototype.unsupportedPropChanges.concat(['groups']) +}); +export const BusRegroupView = BoxView.extend({ + autoResizeBox: true }); -export const BusRegroupView = BoxView; export const BusGroup = BusRegroup.define('BusGroup', { }, { diff --git a/src/cells/dff.mjs b/src/cells/dff.mjs index ad5cc0e..16d6c91 100644 --- a/src/cells/dff.mjs +++ b/src/cells/dff.mjs @@ -8,32 +8,54 @@ import { Vector3vl } from '3vl'; // D flip-flops export const Dff = Box.define('Dff', { + /* default properties */ + bits: 1, + polarity: { clock: true }, + initial: 'x', + + ports: { + groups: { + 'in': { + position: Box.prototype.getStackedPosition({ side: 'left' }) + }, + 'out': { + position: Box.prototype.getStackedPosition({ side: 'right' }) + } + } + } }, { - constructor: function(args) { - _.defaults(args, { bits: 1, polarity: {}, initial: 'x' }); - if (!args.outputSignals) - args.outputSignals = { - out: Vector3vl.fromBin(args.initial, args.bits) - }; - if ('arst' in args.polarity && !args.arst_value) - args.arst_value = Array(args.bits).fill('0').join(''); - const markup = []; - const lblmarkup = []; - markup.push(this.addLabelledWire(args, lblmarkup, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits, label: 'Q' })); - let num = 0; - markup.push(this.addLabelledWire(args, lblmarkup, 'left', (num++*16)+12, { id: 'in', dir: 'in', bits: args.bits, label: 'D' })); - if ('clock' in args.polarity) - markup.push(this.addLabelledWire(args, lblmarkup, 'left', (num++*16)+12, { id: 'clk', dir: 'in', bits: 1, polarity: args.polarity.clock, clock: true })); - if ('arst' in args.polarity) - markup.push(this.addLabelledWire(args, lblmarkup, 'left', (num++*16)+12, { id: 'arst', dir: 'in', bits: 1, polarity: args.polarity.arst })); - if ('enable' in args.polarity) - markup.push(this.addLabelledWire(args, lblmarkup, 'left', (num++*16)+12, { id: 'en', dir: 'in', bits: 1, polarity: args.polarity.enable })); - markup.push(''); - markup.push(lblmarkup.join('')); - this.markup = markup.join(''); - const size = { width: 80, height: num*16+8 }; - args.size = size; - Box.prototype.constructor.apply(this, arguments); + initialize: function() { + Box.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + const initial = this.prop('initial'); + const polarity = this.prop('polarity'); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: bits, portlabel: 'D' }, + { id: 'out', group: 'out', dir: 'out', bits: bits, portlabel: 'Q' } + ], { labelled: true }); + this.prop('outputSignals/out', Vector3vl.fromBin(initial, bits)); + + if ('arst' in polarity && this.prop('arst_value')) + this.prop('arst_value', Array(bits).fill('0').join('')); + + let num = 1; + if ('clock' in polarity) { + num++; + this.addPort({ id: 'clk', group: 'in', dir: 'in', bits: 1, polarity: polarity.clock, decor: Box.prototype.decorClock }, { labelled: true }); + } + if ('arst' in polarity) { + num++; + this.addPort({ id: 'arst', group: 'in', dir: 'in', bits: 1, polarity: polarity.arst }, { labelled: true }); + } + if ('enable' in polarity) { + num++; + this.addPort({ id: 'en', group: 'in', dir: 'in', bits: 1, polarity: polarity.enable }, { labelled: true }); + } + + this.prop('size', { width: 80, height: num*16+8 }); + this.last_clk = 0; }, operation: function(data) { @@ -55,7 +77,10 @@ export const Dff = Box.define('Dff', { return this.get('outputSignals'); } else return { out: data.in }; }, - gateParams: Box.prototype.gateParams.concat(['polarity', 'bits']) + gateParams: Box.prototype.gateParams.concat(['polarity', 'bits', 'initial']), + unsupportedPropChanges: Box.prototype.unsupportedPropChanges.concat(['polarity', 'bits', 'initial']) +}); +export const DffView = BoxView.extend({ + autoResizeBox: true }); -export const DffView = BoxView; diff --git a/src/cells/fsm.mjs b/src/cells/fsm.mjs index 4fa06bd..f6c88e0 100644 --- a/src/cells/fsm.mjs +++ b/src/cells/fsm.mjs @@ -11,18 +11,76 @@ import dagre from 'dagre'; import graphlib from 'graphlib'; export const FSM = Box.define('FSM', { + /* default properties */ + bits: { in: 1, out: 1}, + polarity: { clock: true }, + init_state: 0, + states: 1, + trans_table: [], + size: { width: 80, height: 3*16+8 }, - attrs: { - '.tooltip': { - 'ref-x': 0, 'ref-y': -30, - width: 80, height: 30 - }, + ports: { + groups: { + 'in': { + position: Box.prototype.getStackedPosition({ side: 'left' }) + }, + 'out': { + position: Box.prototype.getStackedPosition({ side: 'right' }) + } + } } }, { initialize: function() { - this.listenTo(this, 'change:size', (model, size) => { - this.attr('.tooltip/width', size.width) - }); + Box.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + const polarity = this.prop('polarity'); + + const init_state = this.prop('init_state'); + var current_state = this.prop('current_state'); + if (current_state === undefined) { + current_state = init_state; + this.prop('current_state', current_state); + } + const states = this.prop('states'); + const trans_table = this.prop('trans_table'); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: bits.in }, + { id: 'clk', group: 'in', dir: 'in', bits: 1, polarity: polarity.clock, decor: Box.prototype.decorClock }, + { id: 'arst', group: 'in', dir: 'in', bits: 1, polarity: polarity.arst }, + { id: 'out', group: 'out', dir: 'out', bits: bits.out }, + ], { labelled: true }); + + this.fsmgraph = new joint.dia.Graph; + const statenodes = []; + for (let n = 0; n < states; n++) { + const node = new joint.shapes.standard.Circle({stateNo: n, id: 'state' + n, isInit: n == init_state}); + node.attr('label/text', String(n)); + node.resize(100,50); + node.addTo(this.fsmgraph); + statenodes.push(node); + } + for (const tr of trans_table) { + const trans = new joint.shapes.standard.Link({ + ctrlIn: Vector3vl.fromBin(tr.ctrl_in, bits.in), + ctrlOut: Vector3vl.fromBin(tr.ctrl_out, bits.out) + }); + trans.appendLabel({ + attrs: { + text: { + text: trans.get('ctrlIn').toBin() + '/' + trans.get('ctrlOut').toBin() + } + } + }); + trans.source({ id: 'state' + tr.state_in }); + trans.target({ id: 'state' + tr.state_out }); + trans.addTo(this.fsmgraph); + } + + this.last_clk = 0; + //args.next_trans = undefined; //todo: needed? + this.listenTo(this, 'change:current_state', (model, state) => { const pstate = model.previous('current_state'); this.fsmgraph.getCell('state' + pstate).removeAttr('body/class'); @@ -46,52 +104,6 @@ export const FSM = Box.define('FSM', { }); } }); - Box.prototype.initialize.apply(this, arguments); - }, - constructor: function(args) { - if (!args.init_state) args.init_state = 0; - if (!('current_state' in args)) args.current_state = args.init_state; - args.next_trans = undefined; - const markup = []; - const lblmarkup = []; - markup.push(this.addLabelledWire(args, lblmarkup, 'left', 16+12, { id: 'clk', dir: 'in', bits: 1, polarity: args.polarity.clock, clock: true })); - markup.push(this.addLabelledWire(args, lblmarkup, 'left', 2*16+12, { id: 'arst', dir: 'in', bits: 1, polarity: args.polarity.arst })); - markup.push(this.addLabelledWire(args, lblmarkup, 'left', 12, { id: 'in', dir: 'in', bits: args.bits.in })); - markup.push(this.addLabelledWire(args, lblmarkup, 'right', 12, { id: 'out', dir: 'out', bits: args.bits.out })); - markup.push(''); - markup.push(lblmarkup.join('')); - markup.push(['', - '', - '🔍', - ''].join('')); - this.markup = markup.join(''); - this.fsmgraph = new joint.dia.Graph; - const statenodes = []; - for (let n = 0; n < args.states; n++) { - const node = new joint.shapes.standard.Circle({stateNo: n, id: 'state' + n, isInit: n == args.init_state}); - node.attr('label/text', String(n)); - node.resize(100,50); - node.addTo(this.fsmgraph); - statenodes.push(node); - } - for (const tr of args.trans_table) { - const trans = new joint.shapes.standard.Link({ - ctrlIn: Vector3vl.fromBin(tr.ctrl_in, args.bits.in), - ctrlOut: Vector3vl.fromBin(tr.ctrl_out, args.bits.out) - }); - trans.appendLabel({ - attrs: { - text: { - text: trans.get('ctrlIn').toBin() + '/' + trans.get('ctrlOut').toBin() - } - } - }); - trans.source({ id: 'state' + tr.state_in }); - trans.target({ id: 'state' + tr.state_out }); - trans.addTo(this.fsmgraph); - } - Box.prototype.constructor.apply(this, arguments); - this.last_clk = 0; }, operation: function(data) { const bits = this.get('bits'); @@ -127,10 +139,13 @@ export const FSM = Box.define('FSM', { return { out: trans.get('ctrlOut') }; } }, - gateParams: Box.prototype.gateParams.concat(['bits', 'polarity', 'wirename', 'states', 'init_state', 'trans_table']) + markup: Box.prototype.markup.concat(Box.prototype.markupZoom), + gateParams: Box.prototype.gateParams.concat(['bits', 'polarity', 'states', 'init_state', 'trans_table']), + unsupportedPropChanges: Box.prototype.unsupportedPropChanges.concat(['bits', 'polarity', 'states', 'init_state', 'trans_table']) }); export const FSMView = BoxView.extend({ + autoResizeBox: true, events: { "click foreignObject.tooltip": "stopprop", "mousedown foreignObject.tooltip": "stopprop", diff --git a/src/cells/gates.mjs b/src/cells/gates.mjs index a727971..b8339aa 100644 --- a/src/cells/gates.mjs +++ b/src/cells/gates.mjs @@ -6,64 +6,84 @@ import bigInt from 'big-integer'; import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; -// Single-input gate model -export const Gate11 = Gate.define('Gate11', { +// base class for gates displayed using an external svg image +export const GateSVG = Gate.define('GateSVG', { + /* default properties */ + bits: 1, + size: { width: 60, height: 40 }, attrs: { - '.body': { width: 60, height: 40 } - } -}, { - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits }), - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.bits }), - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + 'image.body': { refWidth: 1, refHeight: 1 } }, + ports: { + groups: { + 'in': { position: { name: 'left', args: { dx: 20 } }, attrs: { 'line.wire': { x2: -35 }, 'circle.port': { refX: -35 } }, z: -1 }, + 'out': { position: { name: 'right', args: { dx: -20 } }, attrs: { 'line.wire': { x2: 35 }, 'circle.port': { refX: 35 } }, z: -1 } + } + } +}, { + markup: Gate.prototype.markup.concat([{ + tagName: 'image', + className: 'body' + } + ]), gateParams: Gate.prototype.gateParams.concat(['bits']) }); +// Single-input gate model +export const Gate11 = GateSVG.define('Gate11', {}, { + initialize: function() { + GateSVG.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: bits }, + { id: 'out', group: 'out', dir: 'out', bits: bits } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in', bits); + this.setPortBits('out', bits); + }); + } +}); + // Two-input gate model -export const Gate21 = Gate.define('Gate21', { - size: { width: 60, height: 40 }, - attrs: { - '.body': { width: 60, height: 40 } +export const Gate21 = GateSVG.define('Gate21', {}, { + initialize: function() { + GateSVG.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + this.addPorts([ + { id: 'in1', group: 'in', dir: 'in', bits: bits }, + { id: 'in2', group: 'in', dir: 'in', bits: bits }, + { id: 'out', group: 'out', dir: 'out', bits: bits } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in1', bits); + this.setPortBits('in2', bits); + this.setPortBits('out', bits); + }); } -}, { - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits }), - this.addWire(args, 'left', 0.3, { id: 'in1', dir: 'in', bits: args.bits }), - this.addWire(args, 'left', 0.7, { id: 'in2', dir: 'in', bits: args.bits }), - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); - }, - gateParams: Gate.prototype.gateParams.concat(['bits']) }); // Reducing gate model -export const GateReduce = Gate.define('GateReduce', { - size: { width: 60, height: 40 }, - attrs: { - '.body': { width: 60, height: 40 } +export const GateReduce = GateSVG.define('GateReduce', {}, { + initialize: function() { + GateSVG.prototype.initialize.apply(this, arguments); + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'in', group: 'in', dir: 'in', bits: bits }, + { id: 'out', group: 'out', dir: 'out', bits: 1 } + ]); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in', bits); + }); } -}, { - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: 1 }), - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.bits }), - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); - }, }); // Repeater (buffer) gate model diff --git a/src/cells/io.mjs b/src/cells/io.mjs index 91cd18d..5eb6c28 100644 --- a/src/cells/io.mjs +++ b/src/cells/io.mjs @@ -1,7 +1,7 @@ "use strict"; import * as joint from 'jointjs'; -import { Gate, GateView } from './base'; +import { Box, BoxView } from './base'; import _ from 'lodash'; import $ from 'jquery'; import bigInt from 'big-integer'; @@ -9,35 +9,46 @@ import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; // Things with numbers -export const NumBase = Gate.define('NumBase', { - numbase: 'hex', - attrs: { - '.tooltip': { - 'ref-x': 0, 'ref-y': -30, - width: 80, height: 30 - }, - } +export const NumBase = Box.define('NumBase', { + /* default properties */ + numbase: 'hex' }, { - initialize: function(args) { - this.listenTo(this, 'change:size', (x, size) => { - this.attr('.tooltip/width', Math.max(size.width, 50)) - }); - Gate.prototype.initialize.apply(this, arguments); - }, - numbaseMarkup: [ - // requiredExtensions="http://www.w3.org/1999/xhtml" not supported by Chrome - '', - '', - '', - ''].join(''), - gateParams: Gate.prototype.gateParams.concat(['numbase']) + tooltipMinWidth: 55, + markup: Box.prototype.markup.concat([{ + tagName: 'foreignObject', + className: 'tooltip', + children: [{ + tagName: 'body', + //todo: investigate about namespaceURI on other browsers, works on Firefox only if attribute set, but not added to SVG code, what happens with e.g. Chrome here? + namespaceURI: 'http://www.w3.org/1999/xhtml', // requiredExtensions="http://www.w3.org/1999/xhtml" not supported by Chrome + children: [{ + tagName: 'select', + className: 'numbase', + children: [{ + tagName: 'option', + attributes: { value: 'hex' }, + textContent: 'hex' + }, { + tagName: 'option', + attributes: { value: 'dec' }, + textContent: 'dec' + }, { + tagName: 'option', + attributes: { value: 'oct' }, + textContent: 'oct' + }, { + tagName: 'option', + attributes: { value: 'bin' }, + textContent: 'bin' + }] + }] + }] + } + ]), + gateParams: Box.prototype.gateParams.concat(['numbase']) }); -export const NumBaseView = GateView.extend({ +export const NumBaseView = BoxView.extend({ + autoResizeBox: true, events: { "click select.numbase": "stopprop", "mousedown select.numbase": "stopprop", @@ -46,94 +57,99 @@ export const NumBaseView = GateView.extend({ changeNumbase: function(evt) { this.model.set('numbase', evt.target.value || 'bin'); }, - render: function() { - GateView.prototype.render.apply(this, arguments); - if (this.model.get('box_resized')) return; - this.model.set('box_resized', true); + calculateBoxWidth: function() { const testtext = document.createElementNS('http://www.w3.org/2000/svg', 'text'); $(testtext).text(Array(this.model.get('bits')).fill('0').join('')) .attr('class', 'numvalue') .appendTo(this.$el); const width = testtext.getBBox().width + 20; testtext.remove(); - this.model.set('size', _.set(_.clone(this.model.get('size')), 'width', width)); + return width; } }); // Numeric display -- displays a number export const NumDisplay = NumBase.define('NumDisplay', { + /* default properties */ bits: 1, propagation: 0, + attrs: { - '.body': { fill: 'white', stroke: 'black', 'stroke-width': 2 }, 'text.value': { - text: '', - 'ref-x': .5, 'ref-y': .5, - 'dominant-baseline': 'ideographic', + refX: .5, refY: .5, + yAlignment: 'middle' }, } }, { - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: args.bits }), - '', - this.numbaseMarkup, - '', - '', - ].join(''); - NumBase.prototype.constructor.apply(this, arguments); - }, initialize: function(args) { NumBase.prototype.initialize.apply(this, arguments); - const settext = () => { - this.attr('text.value/text', help.sig2base(this.get('inputSignals').in, this.get('numbase'))); - } + + const bits = this.prop('bits'); + + this.addPort({ id: 'in', group: 'in', dir: 'in', bits: bits }); + + this.on('change:bits', (_, bits) => { + this.setPortBits('in', bits); + }); + + const settext = () => this.attr('text.value/text', help.sig2base(this.get('inputSignals').in, this.get('numbase'))); settext(); - this.listenTo(this, 'change:inputSignals', settext); - this.listenTo(this, 'change:numbase', settext); + + this.on('change:inputSignals change:numbase', settext); }, + markup: NumBase.prototype.markup.concat([{ + tagName: 'text', + className: 'value numvalue' + } + ]), gateParams: NumBase.prototype.gateParams.concat(['bits']) }); export const NumDisplayView = NumBaseView; // Numeric entry -- parses a number from a text box export const NumEntry = NumBase.define('NumEntry', { + /* default properties */ bits: 1, propagation: 0, buttonState: Vector3vl.xes(1), + attrs: { - '.body': { fill: 'white', stroke: 'black', 'stroke-width': 2 }, 'foreignObject.valinput': { - 'ref-x': 5, 'ref-y': 0, - width: 60, height: 30 + refX: .5, refY: .5, + refWidth: -10, refHeight: -10, + xAlignment: 'middle', yAlignment: 'middle', } } }, { initialize: function(args) { - this.listenTo(this, 'change:size', (x, size) => { - this.attr('foreignObject.valinput/width', size.width - 10) - }); NumBase.prototype.initialize.apply(this, arguments); - }, - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.bits }), - '', - this.numbaseMarkup, - '', - '', - '', - '', - '', - ].join(''); - args.buttonState = args.outputSignals.out; - NumBase.prototype.constructor.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPort({ id: 'out', group: 'out', dir: 'out', bits: bits }); + + this.on('change:bits', (_, bits) => { + this.setPortBits('out', bits); + }); + + this.prop('buttonState', this.prop('outputSignals/out')); }, operation: function() { return { out: this.get('buttonState') }; }, + markup: NumBase.prototype.markup.concat([{ + tagName: 'foreignObject', + className: 'valinput', + children: [{ + tagName: 'body', + namespaceURI: 'http://www.w3.org/1999/xhtml', + children: [{ + tagName: 'input', + attributes: { type: 'text' } + }] + }] + } + ]), gateParams: NumBase.prototype.gateParams.concat(['bits']) }); export const NumEntryView = NumBaseView.extend({ @@ -170,29 +186,29 @@ export const NumEntryView = NumBaseView.extend({ }); // Lamp model -- displays a single-bit input -export const Lamp = Gate.define('Lamp', { +export const Lamp = Box.define('Lamp', { size: { width: 30, height: 30 }, attrs: { - 'rect.body': { fill: 'white', stroke: 'black', 'stroke-width': 2, width: 30, height: 30 }, '.led': { - 'ref-x': .5, 'ref-y': .5, - r: 10 + refX: .5, refY: .5, + refR: .35, + stroke: 'black' } } }, { - constructor: function(args) { - this.markup = [ - this.addWire(args, 'left', 0.5, { id: 'in', dir: 'in', bits: 1 }), - '', - '', - '', - ].join('') - Gate.prototype.constructor.apply(this, arguments); - } + initialize: function(args) { + Box.prototype.initialize.apply(this, arguments); + this.addPort({ id: 'in', group: 'in', dir: 'in', bits: 1 }); + }, + markup: Box.prototype.markup.concat([{ + tagName: 'circle', + className: 'led' + } + ]) }); -export const LampView = GateView.extend({ +export const LampView = BoxView.extend({ confirmUpdate(flags) { - GateView.prototype.confirmUpdate.apply(this, arguments); + BoxView.prototype.confirmUpdate.apply(this, arguments); if (this.hasFlag(flags, 'flag:inputSignals')) { this.updateLamp(this.model.get('inputSignals')); }; @@ -202,48 +218,50 @@ export const LampView = GateView.extend({ this.$(".led").toggleClass('low', signal.in.isLow); }, render() { - GateView.prototype.render.apply(this, arguments); + BoxView.prototype.render.apply(this, arguments); this.updateLamp(this.model.get('inputSignals')); } }); // Button model -- single-bit clickable input -export const Button = Gate.define('Button', { - size: { width: 30, height: 30 }, +export const Button = Box.define('Button', { + /* default properties */ buttonState: false, propagation: 0, + + size: { width: 30, height: 30 }, attrs: { - 'rect.body': { fill: 'white', stroke: 'black', 'stroke-width': 2, width: 30, height: 30 }, '.btnface': { - stroke: 'black', 'stroke-width': 2, - 'ref-height': .6, 'ref-width': .6, 'ref-x': .2, 'ref-y': .2, + stroke: 'black', strokeWidth: 2, + refX: .2, refY: .2, + refHeight: .6, refWidth: .6, cursor: 'pointer' } } }, { - constructor: function(args) { - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: 1 }), - '', - '', - '', - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function(args) { + Box.prototype.initialize.apply(this, arguments); + this.addPort({ id: 'out', group: 'out', dir: 'out', bits: 1 }); }, operation: function() { return { out: this.get('buttonState') ? Vector3vl.ones(1) : Vector3vl.zeros(1) }; - } + }, + markup: Box.prototype.markup.concat([{ + tagName: 'rect', + className: 'btnface' + } + ]) }); -export const ButtonView = GateView.extend({ - presentationAttributes: GateView.addPresentationAttributes({ +export const ButtonView = BoxView.extend({ + presentationAttributes: BoxView.addPresentationAttributes({ buttonState: 'flag:buttonState', }), initialize: function() { - GateView.prototype.initialize.apply(this, arguments); + BoxView.prototype.initialize.apply(this, arguments); this.$(".btnface").toggleClass('live', this.model.get('buttonState')); }, confirmUpdate(flags) { - GateView.prototype.confirmUpdate.apply(this, arguments); + BoxView.prototype.confirmUpdate.apply(this, arguments); if (this.hasFlag(flags, 'flag:buttonState')) { this.$(".btnface").toggleClass('live', this.model.get('buttonState')); } @@ -258,40 +276,46 @@ export const ButtonView = GateView.extend({ }); // Input/output model -export const IO = Gate.define('IO', { +export const IO = Box.define('IO', { + /* default properties */ bits: 1, + net: '', propagation: 0, + attrs: { - '.body': { fill: 'white', stroke: 'black', 'stroke-width': 2 }, - text: { - fill: 'black', - 'ref-x': .5, 'ref-y': .5, 'dominant-baseline': 'ideographic', - 'text-anchor': 'middle', - 'font-weight': 'bold', - 'font-size': '14px' + 'text.ioname': { + refX: .5, refY: .5, + xAlignment: 'middle', yAlignment: 'middle', + fontWeight: 'bold', + fontSize: '10pt' } } }, { - constructor: function(args) { - if (!args.bits) args.bits = 1; - this.markup = [ - this.addWire(args, this.io_dir == 'out' ? 'right' : 'left', 0.5, { id: this.io_dir, dir: this.io_dir, bits: args.bits }), - '', - '', - ].join(''); - if ('bits' in args) _.set(args, ['attrs', 'circle', 'port', 'bits'], args.bits); - _.set(args, ['attrs', 'text', 'text'], args.net); - Gate.prototype.constructor.apply(this, arguments); + initialize: function(args) { + Box.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPort({ id: this.io_dir, group: this.io_dir, dir: this.io_dir, bits: bits }); + + this.on('change:bits', (_, bits) => { + this.setPortBits(this.io_dir, bits); + }); + this.bindAttrToProp('text.ioname/text', 'net'); }, - gateParams: Gate.prototype.gateParams.concat(['bits','net']) + markup: Box.prototype.markup.concat([{ + tagName: 'text', + className: 'ioname' + } + ]), + gateParams: Box.prototype.gateParams.concat(['bits','net']) }); -export const IOView = GateView.extend({ - render: function() { - GateView.prototype.render.apply(this, arguments); - if (this.model.get('box_resized')) return; - this.model.set('box_resized', true); - const width = this.el.querySelector('text.ioname').getBBox().width + 10; - this.model.set('size', _.set(_.clone(this.model.get('size')), 'width', width)); +export const IOView = BoxView.extend({ + autoResizeBox: true, + calculateBoxWidth: function() { + const text = this.el.querySelector('text.ioname'); + if (text.getAttribute('display') !== 'none') return text.getBBox().width + 10; + return 20; } }); @@ -311,77 +335,89 @@ export const OutputView = IOView; // Constant export const Constant = NumBase.define('Constant', { + /* default properties */ + constant: '0', propagation: 0, + attrs: { - '.body': { fill: 'white', stroke: 'black', 'stroke-width': 2 }, - 'text.value': { - text: '', - 'ref-x': .5, 'ref-y': .5, - 'dominant-baseline': 'ideographic', + 'text.value': { + refX: .5, refY: .5, + yAlignment: 'middle' } } }, { - constructor: function(args) { - args.constantCache = Vector3vl.fromBin(args.constant, args.constant.length); - args.bits = args.constant.length; - args.outputSignals = { out: args.constantCache }; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: args.constant.length }), - '', - this.numbaseMarkup, - '', - '', - ].join(''); - NumBase.prototype.constructor.apply(this, arguments); - }, initialize: function(args) { NumBase.prototype.initialize.apply(this, arguments); - const settext = () => { - this.attr('text.value/text', help.sig2base(this.get('constantCache'), this.get('numbase'))); - } + + const constant = this.prop('constant'); + this.prop('bits', constant.length); + this.prop('constantCache', Vector3vl.fromBin(constant, constant.length)); + + this.addPort({ id: 'out', group: 'out', dir: 'out', bits: constant.length }); + + const settext = () => this.attr('text.value/text', help.sig2base(this.get('constantCache'), this.get('numbase'))); settext(); - this.listenTo(this, 'change:numbase', settext); + + this.on('change:constant', (_, constant) => { + this.setPortBits('out', constant.length); + this.prop('bits', constant.length); + this.prop('constantCache', Vector3vl.fromBin(constant, constant.length)); + settext(); + }); + this.on('change:numbase', settext); }, operation: function() { return { out: this.get('constantCache') }; }, + markup: NumBase.prototype.markup.concat([{ + tagName: 'text', + className: 'value numvalue' + } + ]), gateParams: NumBase.prototype.gateParams.concat(['constant']) }); export const ConstantView = NumBaseView; // Clock -export const Clock = Gate.define('Clock', { - size: { width: 30, height: 30 }, - attrs: { - 'rect.body': { fill: 'white', stroke: 'black', 'stroke-width': 2, width: 30, height: 30 }, - 'path.decor': { stroke: 'black' }, - '.tooltip': { - 'ref-x': 0, 'ref-y': -30, - width: 80, height: 30 - }, - } +export const Clock = Box.define('Clock', { + /* default properties */ + propagation: 100, + + size: { width: 30, height: 30 } }, { - constructor: function(args) { - args.outputSignals = { out: Vector3vl.zeros(1) }; - this.markup = [ - this.addWire(args, 'right', 0.5, { id: 'out', dir: 'out', bits: 1 }), - '', - '', - '', - '', - '', - '', - '' - ].join(''); - Gate.prototype.constructor.apply(this, arguments); + initialize: function(args) { + Box.prototype.initialize.apply(this, arguments); + + this.addPort({ id: 'out', group: 'out', dir: 'out', bits: 1 }); + + this.prop('outputSignals/out', Vector3vl.zeros(1)); }, operation: function() { + // trigger next clock edge this.trigger("change:inputSignals", this, {}); return { out: this.get('outputSignals').out.not() }; - } + }, + tooltipMinWidth: 55, + markup: Box.prototype.markup.concat([{ + tagName: 'path', + className: 'decor', + attributes: { d: 'M7.5 7.5 L7.5 22.5 L15 22.5 L15 7.5 L22.5 7.5 L22.5 22.5', stroke: 'black' } + }, { + tagName: 'foreignObject', + className: 'tooltip', + children: [{ + tagName: 'body', + namespaceURI: 'http://www.w3.org/1999/xhtml', + children: [{ + tagName: 'input', + attributes: { type: 'number', min: 1, step: 1 } + }] + }] + } + ]) }); -export const ClockView = GateView.extend({ - presentationAttributes: GateView.addPresentationAttributes({ +export const ClockView = BoxView.extend({ + presentationAttributes: BoxView.addPresentationAttributes({ propagation: 'flag:propagation' }), events: { @@ -391,11 +427,11 @@ export const ClockView = GateView.extend({ "input input": "changePropagation" }, render(args) { - GateView.prototype.render.apply(this, arguments); + BoxView.prototype.render.apply(this, arguments); this.updatePropagation(); }, confirmUpdate(flags) { - GateView.prototype.confirmUpdate.apply(this, arguments); + BoxView.prototype.confirmUpdate.apply(this, arguments); if (this.hasFlag(flags, 'flag:propagation')) this.updatePropagation(); }, changePropagation(evt) { diff --git a/src/cells/memory.mjs b/src/cells/memory.mjs index 2d94fe6..080ddfc 100644 --- a/src/cells/memory.mjs +++ b/src/cells/memory.mjs @@ -10,84 +10,110 @@ import { Vector3vl, Mem3vl } from '3vl'; // Memory cell export const Memory = Box.define('Memory', { + /* default properties */ + bits: 1, + abits: 1, + rdports: [{clock_polarity: true}], + wrports: [{}], + words: undefined, + offset: 0, + attrs: { - 'line.portsplit': { - stroke: 'black', x1: 0, x2: 40 - }, - '.tooltip': { - 'ref-x': 0, 'ref-y': -30, - width: 80, height: 30 - }, + 'path.portsplit': { + stroke: 'black', d: undefined + } + }, + ports: { + groups: { + 'in': { + position: Box.prototype.getStackedPosition({ side: 'left' }) + }, + 'out': { + position: Box.prototype.getStackedPosition({ side: 'right' }) + } + } } }, { initialize: function() { - this.listenTo(this, 'change:size', (model, size) => { - this.attr('line.portsplit/x2', size.width); - this.attr('.tooltip/width', size.width) - }); Box.prototype.initialize.apply(this, arguments); - }, - constructor: function(args) { - if (!args.bits) args.bits = 1; - if (!args.abits) args.abits = 1; - if (!args.rdports) args.rdports = []; - if (!args.wrports) args.wrports = []; - if (!args.words) args.words = 1 << args.abits; - if (!args.offset) args.offset = 0; - if (args.memdata) - this.memdata = Mem3vl.fromJSON(args.bits, args.memdata); + + const bits = this.prop('bits'); + const abits = this.prop('abits'); + const rdports = this.prop('rdports'); + const wrports = this.prop('wrports'); + var words = this.prop('words'); + const memdata = this.prop('memdata'); + + if (!words) { + words = 1 << abits; + this.prop('words', words, { init: true }); + } + if (memdata) + this.memdata = Mem3vl.fromJSON(bits, memdata); else - this.memdata = new Mem3vl(args.bits, args.words); - delete args.memdata; // performance hack - console.assert(this.memdata.words == args.words); + this.memdata = new Mem3vl(bits, words); + console.assert(this.memdata.words == words); + this.removeProp('memdata'); // performance hack + this.last_clk = {}; - const markup = []; - const lblmarkup = []; let num = 0; + let idxOffset = 0; const portsplits = []; - function num_y(num) { return num * 16 + 12; } - for (const [pnum, port] of args.rdports.entries()) { + for (const [pnum, port] of rdports.entries()) { const portname = "rd" + pnum; - markup.push(this.addLabelledWire(args, lblmarkup, 'right', num_y(num), { id: portname + 'data', dir: 'out', bits: args.bits, label: 'data' })); - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'addr', dir: 'in', bits: args.abits, label: 'addr' })); - if ('enable_polarity' in port) - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'en', dir: 'in', bits: 1, label: 'en', polarity: port.enable_polarity })); + this.addPorts([ + { id: portname + 'addr', group: 'in', dir: 'in', bits: bits, portlabel: 'addr' }, + { id: portname + 'data', group: 'out', dir: 'out', bits: bits, portlabel: 'data', args: { idxOffset: idxOffset } } + ], { labelled: true }); + num += 1; + if ('enable_polarity' in port) { + num++; + idxOffset++; + this.addPort({ id: portname + 'en', group: 'in', dir: 'in', bits: 1, portlabel: 'en', polarity: port.enable_polarity }, { labelled: true }); + } if ('clock_polarity' in port) { - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'clk', dir: 'in', bits: 1, label: 'clk', polarity: port.clock_polarity, clock: true })); + num++; + idxOffset++; + this.addPort({ id: portname + 'clk', group: 'in', dir: 'in', bits: 1, portlabel: 'clk', polarity: port.clock_polarity, decor: Box.prototype.decorClock }, { labelled: true }); this.last_clk[portname + 'clk'] = 0; } else { port.transparent = true; } portsplits.push(num); } - for (const [pnum, port] of args.wrports.entries()) { + for (const [pnum, port] of wrports.entries()) { const portname = "wr" + pnum; - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'data', dir: 'in', bits: args.bits, label: 'data' })); - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'addr', dir: 'in', bits: args.abits, label: 'addr' })); - if ('enable_polarity' in port) - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'en', dir: 'in', bits: args.bits, label: 'en', polarity: port.enable_polarity })); + num += 2; + this.addPorts([ + { id: portname + 'data', group: 'in', dir: 'in', bits: bits, portlabel: 'data' }, + { id: portname + 'addr', group: 'in', dir: 'in', bits: bits, portlabel: 'addr' } + ], { labelled: true }); + if ('enable_polarity' in port) { + num++; + this.addPort({ id: portname + 'en', group: 'in', dir: 'in', bits: 1, portlabel: 'en', polarity: port.enable_polarity }, { labelled: true }); + } if ('clock_polarity' in port) { - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num_y(num++), { id: portname + 'clk', dir: 'in', bits: 1, label: 'clk', polarity: port.clock_polarity, clock: true })); + num++; + this.addPort({ id: portname + 'clk', group: 'in', dir: 'in', bits: 1, portlabel: 'clk', polarity: port.clock_polarity, decor: Box.prototype.decorClock }, { labelled: true }); this.last_clk[portname + 'clk'] = 0; } portsplits.push(num); } - const size = { width: 80, height: num*16+8 }; - args.size = size; portsplits.pop(); - markup.push(''); - for (const num of portsplits) { - const yline = num_y(num) - 8; - markup.push(''); - } - markup.push(''); - markup.push(lblmarkup.join('')); - markup.push(['', - '', - '🔍', - ''].join('')); - this.markup = markup.join(''); - Box.prototype.constructor.apply(this, arguments); + + this.on('change:size', (_, size) => { + // only adapting to changed width + const path = []; + for (const num of portsplits) { + path.push([ + [0, 16*num + 4], + [size.width, 16*num + 4] + ].map(p => p.join(' ')).join(' L ')); + } + this.attr('path.portsplit/d', 'M ' + path.join(' M ')); + }); + const height = num*16+8; + this.prop('size/height', height); }, operation: function(data) { const out = {}; @@ -165,15 +191,21 @@ export const Memory = Box.define('Memory', { } if (changed) this.set('outputSignals', sigs); }, + markup: Box.prototype.markup.concat([{ + tagName: 'path', + className: 'portsplit' + }], Box.prototype.markupZoom), getGateParams: function() { // hack to get memdata back const params = Box.prototype.getGateParams.apply(this, arguments); params.memdata = this.memdata.toJSON(); return params; }, - gateParams: Box.prototype.gateParams.concat(['bits', 'abits', 'rdports', 'wrports', 'words', 'offset']) + gateParams: Box.prototype.gateParams.concat(['bits', 'abits', 'rdports', 'wrports', 'words', 'offset']), + unsupportedPropChanges: Box.prototype.unsupportedPropChanges.concat(['bits', 'abits', 'rdports', 'wrports', 'words', 'offset']) }); export const MemoryView = BoxView.extend({ + autoResizeBox: true, events: { "click foreignObject.tooltip": "stopprop", "mousedown foreignObject.tooltip": "stopprop", @@ -224,7 +256,7 @@ export const MemoryView = BoxView.extend({ col.text('0'.repeat(ahex - addrs.length) + addrs) col = col.next(); for (let c = 0; c < columns; c++, col = col.next()) { - if (address + r * columns + c > words) break; + if (address + r * columns + c >= words) break; col.find('input').val(help.sig2base(memdata.get(address + r * columns + c), numbase)) .removeClass('invalid'); } diff --git a/src/cells/mux.mjs b/src/cells/mux.mjs index ac826f5..0be008a 100644 --- a/src/cells/mux.mjs +++ b/src/cells/mux.mjs @@ -1,56 +1,68 @@ "use strict"; import * as joint from 'jointjs'; -import { Gate, GateView } from './base'; +import { Gate, GateView, portGroupAttrs } from './base'; import bigInt from 'big-integer'; import * as help from '../help.mjs'; import { Vector3vl } from '3vl'; // Multiplexers export const GenMux = Gate.define('GenMux', { - attrs: { - "rect.wtf": { - y: -4, width: 40, height: 1, visibility: 'hidden' - }, - "text.arrow": { - text: '✔', 'y-alignment': 'middle', fill: 'black', - visibility: 'hidden' + /* default properties */ + bits: { in: 1, sel: 1 }, + + size: { width: 40, height: undefined }, + ports: { + groups: { + 'in2': { + position: { name: 'top', args: { y: 10 } }, + attrs: _.merge({}, portGroupAttrs, { 'line.wire': { x2: 0, y2: -30 }, 'circle.port': { magnet: 'passive', refY: -30 }, 'text.bits': { refDx: -4, refDy: 3, textAnchor: 'start' } }), + z: -1 + } } } }, { - constructor: function(args) { - if (!args.bits) args.bits = { in: 1, sel: 1 }; - const n_ins = this.muxNumInputs(args.bits.sel); - const size = { width: 40, height: n_ins*16+8 }; - _.set(args, ['attrs', '.body', 'points'], - [[0,0],[40,10],[40,size.height-10],[0,size.height]] + initialize: function() { + Gate.prototype.initialize.apply(this, arguments); + + const bits = this.prop('bits'); + + this.addPorts([ + { id: 'sel', group: 'in2', dir: 'in', bits: bits.sel }, + { id: 'out', group: 'out', dir: 'out', bits: bits.in } + ]); + + this.on('change:size', (_, size) => { + this.attr(['polygon.body', 'points'], + [[0,0],[size.width,10],[size.width,size.height-10],[0,size.height]] .map(x => x.join(',')).join(' ')); - args.size = size; - const markup = []; + }); + const n_ins = this.muxNumInputs(bits.sel); + this.prop('size/height', n_ins*16+8); + + const vpath = [ + [2, 0], + [5, 5], + [11, -5] + ]; + const path = 'M' + vpath.map(l => l.join(' ')).join(' L'); + for (const num of Array(n_ins).keys()) { - const y = num*16+12; - markup.push(this.addWire(args, 'left', y, { id: 'in' + num, dir: 'in', bits: args.bits.in })); + this.addPort({ id: 'in' + num, group: 'in', dir: 'in', bits: bits.in, decor: path }); } - markup.push(this.addWire(args, 'top', 0.5, { id: 'sel', dir: 'in', bits: args.bits.sel })); - markup.push(this.addWire(args, 'right', (size.height)/2, { id: 'out', dir: 'out', bits: args.bits.in })); - markup.push(''); - for (const num of Array(n_ins).keys()) { - const y = num*16+12; - markup.push(''); - args.attrs['text.arrow_in' + num] = { - 'ref-x': 2, - 'ref-y': y, - }; - } - this.markup = markup.join(''); - Gate.prototype.constructor.apply(this, arguments); }, operation: function(data) { const i = this.muxInput(data.sel); if (i === undefined) return { out: Vector3vl.xes(this.get('bits').in) }; return { out: data['in' + i] }; }, - gateParams: Gate.prototype.gateParams.concat(['bits']) + markup: Gate.prototype.markup.concat([{ + tagName: 'polygon', + className: 'body' + } + ]), + gateParams: Gate.prototype.gateParams.concat(['bits']), + unsupportedPropChanges: Gate.prototype.unsupportedPropChanges.concat(['bits']) }); export const GenMuxView = GateView.extend({ initialize() { @@ -70,7 +82,7 @@ export const GenMuxView = GateView.extend({ updateMux(data) { const i = this.model.muxInput(data.sel); for (const num of Array(this.n_ins).keys()) { - this.$('text.arrow_in' + num).css('visibility', i == num ? 'visible' : 'hidden'); + this.$('[port=in' + num + '] path.decor').css('visibility', i == num ? 'visible' : 'hidden'); } } }); diff --git a/src/cells/subcircuit.mjs b/src/cells/subcircuit.mjs index 2218e87..a44e8cf 100644 --- a/src/cells/subcircuit.mjs +++ b/src/cells/subcircuit.mjs @@ -1,35 +1,30 @@ "use strict"; import * as joint from 'jointjs'; -import { Gate, Box, BoxView } from './base'; +import { Box, BoxView } from './base'; import { IO, Input, Output } from './io'; import bigInt from 'big-integer'; import * as help from '../help.mjs'; // Subcircuit model -- embeds a circuit graph in an element export const Subcircuit = Box.define('Subcircuit', { + /* default properties */ propagation: 0, + attrs: { - 'path.wire' : { 'ref-y': .5, stroke: 'black' }, 'text.type': { - text: '', 'ref-x': 0.5, 'ref-y': -10, - 'dominant-baseline': 'ideographic', - 'text-anchor': 'middle', - fill: 'black' - }, - '.tooltip': { - 'ref-x': 0, 'ref-y': -30, - width: 80, height: 30 - }, + refX: .5, refY: -10, + xAlignment: 'middle', yAlignment: 'middle' + } } }, { initialize: function() { - this.listenTo(this, 'change:size', (model, size) => this.attr('.tooltip/width', size.width)); Box.prototype.initialize.apply(this, arguments); - }, - constructor: function(args) { - console.assert(args.graph instanceof joint.dia.Graph); - const graph = args.graph; + + this.bindAttrToProp('text.type/text', 'celltype'); + + const graph = this.prop('graph'); + console.assert(graph instanceof joint.dia.Graph); graph.set('subcircuit', this); const IOs = graph.getCells() .filter((cell) => cell instanceof IO); @@ -44,39 +39,32 @@ export const Subcircuit = Box.define('Subcircuit', { outputs.sort(sortfun); const vcount = Math.max(inputs.length, outputs.length); const size = { width: 80, height: vcount*16+8 }; - const markup = []; - const lblmarkup = []; const iomap = {}; - _.set(args, ['attrs', 'text.type', 'text'], args.celltype); - args.inputSignals = args.inputSignals || {}; - args.outputSignals = args.outputSignals || {}; for (const [num, io] of inputs.entries()) { - markup.push(this.addLabelledWire(args, lblmarkup, 'left', num*16+12, { id: io.get('net'), dir: 'in', bits: io.get('bits') })); - args.inputSignals[io.get('net')] = io.get('outputSignals').out; + this.addPort({ id: io.get('net'), group: 'in', dir: 'in', bits: io.get('bits') }, { labelled: true }); + this.prop(['inputSignals', io.get('net')], io.get('outputSignals').out); } for (const [num, io] of outputs.entries()) { - markup.push(this.addLabelledWire(args, lblmarkup, 'right', num*16+12, { id: io.get('net'), dir: 'out', bits: io.get('bits') })); - args.outputSignals[io.get('net')] = io.get('inputSignals').in; + this.addPort({ id: io.get('net'), group: 'out', dir: 'out', bits: io.get('bits') }, { labelled: true }); + this.prop(['outputSignals', io.get('net')], io.get('inputSignals').in); } - markup.push(''); - markup.push(lblmarkup.join('')); for (const io of IOs) { iomap[io.get('net')] = io.get('id'); } - markup.push(''); - markup.push(''); - markup.push('🔍') - markup.push(''); - this.markup = markup.join(''); - args.size = size; - args.attrs['rect.body'] = size; - args.circuitIOmap = iomap; - Gate.prototype.constructor.apply(this, arguments); + this.prop('size', size); + this.prop('circuitIOmap', iomap); }, - gateParams: Box.prototype.gateParams.concat(['celltype']) + markup: Box.prototype.markup.concat([{ + tagName: 'text', + className: 'type' + } + ], Box.prototype.markupZoom), + gateParams: Box.prototype.gateParams.concat(['celltype']), + unsupportedPropChanges: Box.prototype.unsupportedPropChanges.concat(['celltype']) }); export const SubcircuitView = BoxView.extend({ + autoResizeBox: true, events: { "click foreignObject.tooltip": "stopprop", "mousedown foreignObject.tooltip": "stopprop", diff --git a/src/circuit.mjs b/src/circuit.mjs index 6c90bb8..38018c8 100644 --- a/src/circuit.mjs +++ b/src/circuit.mjs @@ -122,7 +122,8 @@ export class HeadlessCircuit { } } function clearInput(end, gate) { - setInput(Vector3vl.xes(gate.ports[end.port].bits), end, gate); + var bits = gate.getPort(end.port).bits; + setInput(Vector3vl.xes(bits), end, gate); } this.listenTo(graph, 'change:target', function(wire, end) { const gate = graph.getCell(end.id); @@ -149,7 +150,7 @@ export class HeadlessCircuit { const sgate = graph.getCell(strt.id); if (sgate && 'port' in strt) { cell.set('signal', sgate.get('outputSignals')[strt.port]); - cell.set('bits', sgate.ports[strt.port].bits); + cell.set('bits', sgate.getPort(strt.port).bits); } }); let laid_out = false; @@ -167,8 +168,8 @@ export class HeadlessCircuit { } for (const conn of data.connectors) { graph.addCell(new this._cells.Wire({ - source: {id: conn.from.id, port: conn.from.port}, - target: {id: conn.to.id, port: conn.to.port}, + source: {id: conn.from.id, port: conn.from.port, magnet: '.port'}, + target: {id: conn.to.id, port: conn.to.port, magnet: '.port'}, netname: conn.name, vertices: conn.vertices || [] })); diff --git a/src/index.mjs b/src/index.mjs index 59899a2..faeb7cb 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -90,24 +90,27 @@ export class Circuit extends HeadlessCircuit { el: elem, model: graph, width: 100, height: 100, gridSize: 5, + magnetThreshold: 'onleave', snapLinks: true, linkPinning: false, + markAvailable: true, defaultLink: new this._cells.Wire, cellViewNamespace: this._cells, validateConnection: function(vs, ms, vt, mt, e, vl) { if (e === 'target') { if (!mt) return false; - const pt = vt.model.ports[mt.getAttribute('port')]; + const pt = vt.model.getPort(vt.findAttribute('port', mt)); if (typeof pt !== 'object' || pt.dir !== 'in' || pt.bits !== vl.model.get('bits')) return false; const link = this.model.getConnectedLinks(vt.model).find((l) => l.id !== vl.model.id && l.get('target').id === vt.model.id && - l.get('target').port === mt.getAttribute('port') + l.get('target').port === vt.findAttribute('port', mt) ); return !link; } else if (e === 'source') { - const ps = vs.model.ports[ms.getAttribute('port')]; + if (!ms) return false; + const ps = vs.model.getPort(vs.findAttribute('port', ms)); if (typeof ps !== 'object' || ps.dir !== 'out' || ps.bits !== vl.model.get('bits')) return false; return true; diff --git a/src/style.css b/src/style.css index 7c580c1..a141872 100644 --- a/src/style.css +++ b/src/style.css @@ -100,45 +100,26 @@ cursor: crosshair; } -.joint-element .body { - fill: white; - stroke: black; - transition: all 0.2s; -} - -.djs .joint-element circle { - fill: #fff; - stroke: #7f8c8d; - stroke-opacity: 0.5; - stroke-width: 2px; -} - -.djs .joint-element circle.live { +.djs .joint-port-body.defined.live circle.port { stroke: #03c03c; } -.djs .joint-element circle.low { +.djs .joint-port-body.defined.low circle.port { stroke: #fc7c68; } -.djs .joint-element circle.defined { +.djs .joint-port-body.defined circle.port { stroke: #779ecb; } .djs .joint-element circle.led.live { fill: #03c03c; + stroke-width: 0; } .djs .joint-element circle.led.low { fill: #fc7c68; -} - -.joint-element text { - font-size: 8pt; -} - -.joint-element text.bits { - font-size: 7pt; + stroke-width: 0; } .joint-link.live > .connection { @@ -631,10 +612,6 @@ g:hover > foreignObject.tooltip { padding: 5px; } -.joint-element foreignObject.tooltip a.zoom { - cursor: pointer; -} - div.wire_hover { position: fixed; pointer-events: none;