cirsim
Version:
Cirsim Circuit Simulator
652 lines (554 loc) • 17 kB
JavaScript
import {Selectable} from './Selectable';
import {In} from './In';
import {Out} from './Out';
import {OutInv} from './OutInv';
import {Connection} from './Connection';
import {ComponentPropertiesDlg} from './Dlg/ComponentPropertiesDlg';
import DOMPurify from 'dompurify';
import {Rect} from "./Utility/Rect";
/**
* Base object for a component in a circuit
* @constructor
*/
export const Component = function () {
Selectable.call(this);
let circuit = null;
Object.defineProperty(this, 'circuit', {
get: function() {
return circuit;
},
set: function(value) {
circuit = value;
}
});
this.height = 10;
this.width = 10;
this.prev = null;
this.id = ''; // Will be set to a unique id for this component
this.circuit = null;
this.naming = null; // Naming, as in U1 or I1
this.ins = [];
this.outs = [];
};
Component.prototype = Object.create(Selectable.prototype);
Component.prototype.constructor = Component;
/**
* Prefix for component naming
*/
Component.prototype.prefix = "U";
Component.prototype.nameRequired = false;
Component.prototype.delay = 11; ///< Propagation delay in nanoseconds
/**
* Assign this component a unique ID. This is done when a
* component is created by the view.
*/
Component.prototype.brand = function() {
// Every component get a unique ID when it is created
this.id = 'c' + (++Component.maxId);
}
/// Maximum ID integer value for any component
Component.maxId = 1000;
Component.prototype.copyFrom = function (component) {
this.height = component.height;
this.width = component.width;
this.prev = component.prev;
this.naming = component.naming;
this.id = component.id;
component.prev = this;
Selectable.prototype.copyFrom.call(this, component);
//
// Copy input and output states
//
for (let i = 0; i < this.ins.length; i++) {
this.ins[i].copyFrom(component.ins[i]);
}
for (let i = 0; i < this.outs.length; i++) {
this.outs[i].copyFrom(component.outs[i]);
}
};
Component.prototype.drop = function () {
if (this.x < this.width / 2) {
this.x = this.width / 2;
}
if (this.y < this.height / 2) {
this.y = this.height / 2;
}
};
Component.prototype.grab = function() {
Selectable.prototype.grab.call(this);
this.circuit.moveToFront(this);
}
Component.prototype.mouseUp = function () {
};
/**
* Called when a component is added to a circuit
* @param circuit
*/
Component.prototype.added = function (circuit) {
this.circuit = circuit;
if (this.naming === null && this.nameRequired) {
// Create a new name
for (var i = 1; ; i++) {
let naming;
if (this.prefix.charAt(0) === "*") {
if (i <= 26) {
naming = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.charAt(i - 1);
} else {
naming = this.prefix.charAt(1) + (i - 26);
}
} else {
naming = this.prefix + i;
}
const existing = this.circuit.getComponentByNaming(naming);
if (existing === null) {
this.naming = naming;
break;
}
}
}
};
/**
* Add an input to this component
* @param x Relative X location
* @param y Relative Y location
* @param len Length of the line to draw for the input
* @return {In}
*/
Component.prototype.addIn = function (x, y, len, name) {
const inObj = new In(this, x, y, len, name);
inObj.index = this.ins.length;
this.ins.push(inObj);
return inObj;
};
/**
* Add an output to this component
* @param x Relative X location
* @param y Relative Y location
* @param len Length of the line to draw for the output
* @return Created output object
*/
Component.prototype.addOut = function (x, y, len, name, inv) {
const outObj = new Out(this, x, y, len, name, inv);
outObj.index = this.outs.length;
this.outs.push(outObj);
return outObj;
};
/**
* Add an inverse output to this component
* @param x Relative X location
* @param y Relative Y location
* @param len Length of the line to draw for the output
* @return Created output object
*/
Component.prototype.addOutInv = function (x, y, len, name, inv) {
var outObj = new OutInv(this, x, y, len, name, inv);
outObj.index = this.outs.length;
this.outs.push(outObj);
return outObj;
};
/**
* Try to touch this component or some part of
* the component.
* @param x Mouse X
* @param y Mouse Y
*/
Component.prototype.touch = function (x, y) {
// First, try to touch the inputs and outputs
var touched = this.touchOut(x, y);
if (touched !== null) {
// We have touched an Out connector. Add a connection
// for this output, but with no current "to"
return new Connection(touched, null);
}
touched = this.touchIn(x, y);
if (touched !== null) {
// We have touched an In connector. Add a connection
// for this input, but with no current "from"
return new Connection(null, touched);
}
// Have we touched the component itself?
if (x >= this.x - this.width / 2 &&
x <= this.x + this.width / 2 &&
y >= this.y - this.height / 2 &&
y <= this.y + this.height / 2) {
return this;
}
// Test if we have touched an output connection or bend
for (var i = 0; i < this.outs.length; i++) {
var conn = this.outs[i].touchConnections(x, y);
if (conn !== null) {
return conn;
}
}
return null;
};
/**
* Try to touch an Out object for this componenet
* @param x
* @param y
* @return Out object touched or null if none
*/
Component.prototype.touchOut = function (x, y) {
for (var i = 0; i < this.outs.length; i++) {
if (this.outs[i].touch(x, y)) {
return this.outs[i]
}
}
return null;
};
/**
* Try to touch an In object for this componenet
* @param x
* @param y
* @return In object touched or null if none
*/
Component.prototype.touchIn = function (x, y) {
for (var i = 0; i < this.ins.length; i++) {
if (this.ins[i].touch(x, y)) {
return this.ins[i]
}
}
return null;
};
/**
* Collect all of this component and any bends that
* are contained in the rectangle.
* @param rect Rectangle to test
* @param collect Collection (array) to add items to.
*/
Component.prototype.inRect = function (rect, collect) {
if (rect.contains(this.x, this.y)) {
collect.push(this);
}
this.outs.forEach(function (out) {
out.selectRect(rect, collect);
});
};
Component.prototype.delete = function () {
// Delete all connection
for (var i = 0; i < this.ins.length; i++) {
this.ins[i].clear();
}
for (var i = 0; i < this.outs.length; i++) {
this.outs[i].clear();
}
this.circuit.delete(this);
};
/**
* Compute a bounding box that completely contains the component
* @returns {Rect}
*/
Component.prototype.bounds = function() {
const bounds = new Rect(this.x - this.width/2,
this.y - this.height/2,
this.x + this.width/2,
this.y + this.height/2);
for(let ins of this.ins) {
bounds.expand(ins.bounds());
}
for(let out of this.outs) {
bounds.expand(out.bounds());
}
return bounds;
}
/**
* Draw component object.
*
* Default version for simple box objects
* @param context Display context
* @param view View object
*/
Component.prototype.draw = function(context, view) {
this.selectStyle(context, view);
this.drawBox(context);
this.drawName(context, 0, 3);
this.drawIO(context, view);
}
/**
* Draw the input/output pins for this component.
*
* This also draws the connections.
*/
Component.prototype.drawIO = function (context, view) {
for (var i = 0; i < this.ins.length; i++) {
this.selectStyle(context, view);
this.ins[i].draw(context, view);
}
for (var i = 0; i < this.outs.length; i++) {
this.selectStyle(context, view);
this.outs[i].draw(context, view);
}
};
/**
* Save the component basic properties to an object
*
* The character ' is replaced with `. This is so the
* output JSON won't have any ' characters that would
* cause problems in PHP and Javascript
*
* @returns {{id: *, x: *, y: *, name: string, type: *}}
*/
Component.prototype.save = function () {
var type = this.constructor.type;
var naming = this.naming;
if (naming !== null) {
naming = naming.replace(/'/g, '`');
}
return {
"id": this.id,
"x": this.x,
"y": this.y,
"name": naming,
"type": type
};
};
Component.prototype.load = function (obj) {
this.id = this.sanitize(obj["id"]);
// Determine the maximum loaded ID value as we load
// in new components.
const idValue = +this.id.substr(1);
if(idValue > Component.maxId) {
Component.maxId = idValue;
}
this.x = +obj["x"];
this.y = +obj["y"];
this.moveX = this.x;
this.moveY = this.y;
let naming = obj["name"];
if (naming !== null) {
this.naming = this.sanitize(naming).replace(/`/g, "'");
} else {
this.naming = null;
}
};
Component.prototype.saveConnections = function () {
var connections = [];
for (var i = 0; i < this.outs.length; i++) {
var out = this.outs[i];
for (var j = 0; j < out.to.length; j++) {
var conn = out.to[j];
var connObj = conn.save();
if (connObj !== null) {
connections.push(connObj);
}
}
}
return connections;
};
Component.prototype.properties = function (main) {
var dlg = new ComponentPropertiesDlg(this, main);
dlg.open();
};
/**
* Advance the animation for this component by delta seconds
* @param delta Time to advance in seconds
* @returns {boolean} true if animation needs to be redrawn
*/
Component.prototype.advance = function (delta) {
return false;
};
/**
* This function is called when an input is changed on this
* component. It indicates that we need to queue a simulation
* event for this component.
*/
Component.prototype.pending = function () {
var delay = this.delay * 0.1;
var state = [];
for (var i = 0; i < this.ins.length; i++) {
state.push(this.ins[i].value);
}
if (this.circuit.circuits !== null) {
this.circuit.circuits.simulation.queue(this, delay, state);
}
};
Component.prototype.getSimulation = function() {
if(this.circuit !== null) {
return this.circuit.circuits.simulation;
}
return null;
}
/**
* Determine the propagation delay for this device
*/
Component.prototype.getDelay = function () {
return this.delay;
};
Component.prototype.compute = function (state) {
};
Component.prototype.newTab = function () {
};
/**
* Draw the name of a component
* @param context Context to draw on
* @param x X location
* @param y Y locatino
* @param font Optional font to use
*/
Component.prototype.drawName = function (context, x, y, font) {
// Name
if (this.naming !== null) {
context.beginPath();
context.font = font !== undefined ? font : "14px Times";
context.textAlign = "center";
context.fillText(this.naming, this.x + x, this.y + y);
context.stroke();
}
};
/**
* Draw text on a component
* @param context Context to draw on
* @param text Text to draw
* @param x X location
* @param y Y locatino
* @param font Optional font to use
*/
Component.prototype.drawText = function (context, text, x, y, font) {
context.beginPath();
context.font = font !== undefined ? font : "14px Times";
context.textAlign = "center";
context.fillText(text, this.x + x, this.y + y);
context.stroke();
}
/**
* Many components are just a box. This is a function to draw that box
* @param context Context to draw on
*/
Component.prototype.drawBox = function (context, fillStyle) {
if(fillStyle !== 'none') {
let save = context.fillStyle;
context.fillStyle = fillStyle !== undefined ? fillStyle : '#ffffff';
context.fillRect(this.x - this.width / 2 - 0.5,
this.y - this.height / 2 - 0.5,
this.width, this.height);
context.fillStyle = save;
}
context.beginPath();
context.rect(
this.x - this.width / 2 - 0.5,
this.y - this.height / 2 - 0.5,
this.width, this.height);
context.stroke();
}
/**
* Many components are a trapezoid. This is a function to draw that trapezoid
* @param context Conext to draw on
* @param indentL Top/bottom indent size for left side (default = 0)
* @param indentR Top/bottom indent size for right size (default = 20)
*/
Component.prototype.drawTrap = function (context, indentL, indentR) {
if (indentL === undefined) {
indentL = 0;
}
if (indentR === undefined) {
indentR = 20;
}
var leftX = this.x - this.width / 2 - 0.5;
var rightX = this.x + this.width / 2 + 0.5;
var topY = this.y - this.height / 2 - 0.5;
var botY = this.y + this.height / 2 + 0.5;
context.fillStyle = '#ffffff';
// Left side
context.beginPath();
context.moveTo(leftX, topY + indentL);
context.lineTo(leftX, botY - indentL);
// Bottom
context.lineTo(rightX, botY - indentR);
// Right side
context.lineTo(rightX, topY + indentR);
// Top
context.lineTo(leftX, topY + indentL);
context.fill();
// Left side
context.beginPath();
context.moveTo(leftX, topY + indentL);
context.lineTo(leftX, botY - indentL);
// Bottom
context.lineTo(rightX, botY - indentR);
// Right side
context.lineTo(rightX, topY + indentR);
// Top
context.lineTo(leftX, topY + indentL);
context.stroke();
}
/**
* Ability to send a command to a component.
*
* Commands are sent by tests.
*
* Default commands are...
* type:InPinBus - Validates that a component is the correct type.
*
* @param null if not handled, or command result otherwise.
*/
Component.prototype.command = function (value) {
if ((typeof value === 'string' || value instanceof String) &&
value.substr(0, 5) === "type:") {
const expected = value.substr(5);
if (expected !== this.constructor.type) {
let expectedType = this.circuit.circuits.model.main.components.get(expected);
if (expectedType !== null) {
expectedType = expectedType.label;
} else {
expectedType = expected;
}
return {
ok: false,
msg: "Component " + this.naming + " should be type <strong>" +
expectedType + "</strong> but is <strong>" +
this.constructor.label + "</strong>"
}
}
} else {
return null;
}
return {ok: true};
}
/**
* Override in the settable types, such as InPin and InPinBus
* @param value Value to set
*/
Component.prototype.setAsString = function (value) {
}
/**
* Override in the string testable types, such as InPin and InPinBus
* @param value Value to set
* @param input In object
*/
Component.prototype.testAsString = function (value, input) {
console.log(value);
}
/**
* Draw a jagged (stair-step) line from x1,y1 to x2,y2
* @param context Context to draw on
* @param x1 Starting x
* @param y1 Starting y
* @param x2 Ending x
* @param y2 Ending y
* @param t Percentage of say from x1 to x2 the vertical line is
*/
Component.prototype.jaggedLine = function (context, x1, y1, x2, y2, t) {
var xh = Math.round(x1 + (x2 - x1) * t) + 0.5;
y1 += 0.5;
y2 += 0.5;
context.moveTo(x1, y1);
context.lineTo(xh, y1);
context.lineTo(xh, y2)
context.lineTo(x2, y2);
context.stroke();
}
/**
* Sanitize text from user input and files to prevent XSS attacks.
* @param text Text to sanitize
* @returns Sanitized version
*/
Component.prototype.sanitize = function(text) {
return DOMPurify.sanitize(text);
}
/**
* Update component after a circuit change.
* This is used by CircuitRef components to ensure
* references are always correct.
*/
Component.prototype.update = function() {
}