diff --git a/js/schematic.js b/js/schematic.js index 22463d4867..3d9b64135c 100644 --- a/js/schematic.js +++ b/js/schematic.js @@ -13,7 +13,8 @@ // other attributes you can add to the input tag: // width -- width in pixels of diagram // height -- height in pixels of diagram -// parts -- comma-separated list of parts for parts bin (see parts_map) +// parts -- comma-separated list of parts for parts bin (see parts_map), +// parts="" disables editing of diagram // JSON schematic representation: // sch := [part, part, ...] @@ -70,7 +71,7 @@ schematic = (function() { annotation_style = 'rgb(255,64,64)'; // color for diagram annotations property_size = 5; // point size for Component property text - annotation_size = 8; // point size for diagram annotations + annotation_size = 7; // point size for diagram annotations // list of all the defined parts parts_map = { @@ -84,6 +85,7 @@ schematic = (function() { 'd': [Diode, 'Diode'], 'n': [NFet, 'NFet'], 'p': [PFet, 'PFet'], + 's': [Probe, 'Scope Probe'], }; // fix cursor bug in Chrome (default behavior: change to text cursor @@ -105,70 +107,105 @@ schematic = (function() { this.grid = 8; this.scale = 2; - this.origin_x = 0; - this.origin_y = 0; + this.origin_x = input.getAttribute("origin_x"); + if (this.origin_x == undefined) this.origin_x = 0; + this.origin_y = input.getAttribute("origin_y"); + if (this.origin_y == undefined) this.origin_y = 0; this.clipboard = undefined; - // start with a background element with normal positioning - this.background = document.createElement('canvas'); - this.background.style.backgroundColor = background_style; - this.background.style.borderStyle = 'solid'; - this.background.style.borderWidth = '2px'; + // use user-supplied list of parts if supplied + // else just populate parts bin with all the parts + this.edits_allowed = true; + var parts = input.getAttribute('parts'); + if (parts == undefined) { + parts = new Array(); + for (var p in parts_map) parts.push(p); + } else if (parts == '') { + this.edits_allowed = false; + parts = []; + } else parts = parts.split(','); - this.status_div = document.createElement('div'); - //this.status_div.style.borderStyle = 'solid'; - //this.status_div.style.borderWidth = '1px'; - this.status_div.style.position = 'absolute'; - this.status_div.style.padding = '2px'; - //this.status_div.style.backgroundColor = element_style; - this.status = document.createTextNode(''); - this.status_div.appendChild(this.status); + // use user-supplied list of analyses, otherwise provide them all + // analyses="" means no analyses + var analyses = input.getAttribute('analyses'); + if (analyses == undefined) analyses = ['dc','ac','tran']; + else if (analyses == '') analyses = []; + else analyses = analyses.split(','); + + if (parts.length == 0 && analyses.length == 0) this.diagram_only = true; + else this.diagram_only = false; + + if (!this.diagram_only) { + // start with a background element with normal positioning + this.background = document.createElement('canvas'); + this.background.style.backgroundColor = background_style; + this.background.style.borderStyle = 'solid'; + this.background.style.borderWidth = '2px'; + + this.status_div = document.createElement('div'); + this.status_div.style.position = 'absolute'; + this.status_div.style.padding = '2px'; + this.status = document.createTextNode(''); + this.status_div.appendChild(this.status); + } this.connection_points = new Array(); // location string => list of cp's this.components = []; // this is where schematic is rendered this.canvas = document.createElement('canvas'); - this.canvas.tabIndex = 1; // so we get keystrokes - this.canvas.style.borderStyle = 'solid'; - this.canvas.style.borderWidth = '1px'; - this.canvas.style.borderColor = grid_style; - this.canvas.style.position = 'absolute'; - this.canvas.style.outline = 'none'; + if (!this.diagram_only) { + this.canvas.tabIndex = 1; // so we get keystrokes + this.canvas.style.borderStyle = 'solid'; + this.canvas.style.borderWidth = '1px'; + this.canvas.style.borderColor = grid_style; + this.canvas.style.position = 'absolute'; + this.canvas.style.outline = 'none'; + } this.canvas.schematic = this; - this.canvas.addEventListener('mousemove',schematic_mouse_move,false); - this.canvas.addEventListener('mouseover',schematic_mouse_enter,false); - this.canvas.addEventListener('mouseout',schematic_mouse_leave,false); - this.canvas.addEventListener('mousedown',schematic_mouse_down,false); - this.canvas.addEventListener('mouseup',schematic_mouse_up,false); - this.canvas.addEventListener('dblclick',schematic_double_click,false); - this.canvas.addEventListener('keydown',schematic_key_down,false); - this.canvas.addEventListener('keyup',schematic_key_up,false); + if (this.edits_allowed) { + this.canvas.addEventListener('mousemove',schematic_mouse_move,false); + this.canvas.addEventListener('mouseover',schematic_mouse_enter,false); + this.canvas.addEventListener('mouseout',schematic_mouse_leave,false); + this.canvas.addEventListener('mousedown',schematic_mouse_down,false); + this.canvas.addEventListener('mouseup',schematic_mouse_up,false); + this.canvas.addEventListener('dblclick',schematic_double_click,false); + this.canvas.addEventListener('keydown',schematic_key_down,false); + this.canvas.addEventListener('keyup',schematic_key_up,false); + } // toolbar this.tools = new Array(); this.toolbar = []; - this.tools['cut'] = this.add_tool(cut_icon,'Cut: move selected components from diagram to the clipboard',this.cut); - this.tools['copy'] = this.add_tool(copy_icon,'Copy: copy selected components into the clipboard',this.copy); - this.tools['paste'] = this.add_tool(paste_icon,'Paste: copy clipboard into the diagram',this.paste); + if (this.edits_allowed) { + this.tools['cut'] = this.add_tool(cut_icon,'Cut: move selected components from diagram to the clipboard',this.cut); + this.tools['copy'] = this.add_tool(copy_icon,'Copy: copy selected components into the clipboard',this.copy); + this.tools['paste'] = this.add_tool(paste_icon,'Paste: copy clipboard into the diagram',this.paste); + this.toolbar.push(null); // spacer + } // simulation interface if cktsim.js is loaded if (typeof cktsim != 'undefined') { - this.toolbar.push(null); // spacer - this.tools['dc'] = this.add_tool('DC','DC Analysis',this.dc_analysis); - //this.tools['ac'] = this.add_tool('AC','AC Small-Signal Analysis',this.ac_analysis); - //this.tools['tran'] = this.add_tool('TRAN','Transient Analysis',this.transient_analysis); + if (analyses.indexOf('dc') != -1) { + this.tools['dc'] = this.add_tool('DC','DC Analysis',this.dc_analysis); + this.enable_tool('dc',true); + } - this.enable_tool('dc',true); - //this.enable_tool('ac',true); - //this.enable_tool('tran',true); + if (analyses.indexOf('ac') != -1) { + this.tools['ac'] = this.add_tool('AC','AC Small-Signal Analysis',this.ac_analysis); + this.enable_tool('ac',true); + this.ac_npts = '5'; // default values for AC Analysis + this.ac_fstart = '10'; + this.ac_fstop = '10MEG'; + } + + if (analyses.indexOf('tran') != -1) { + //this.tools['tran'] = this.add_tool('TRAN','Transient Analysis',this.tran_analysis); + //this.enable_tool('tran',true); + } } - - // make the canvas "clickable" by registering a dummy click handler - // this should make things work on the iPad - this.canvas.addEventListener('click',function(){},false); - + this.dragging = false; this.drawCursor = false; this.cursor_x = 0; @@ -188,15 +225,6 @@ schematic = (function() { // repaint simply draws this buffer and then adds selected elements on top this.bg_image = document.createElement('canvas'); - // use user-supplied list of parts if supplied - // else just populate parts bin with all the parts - var parts = input.getAttribute('parts'); - if (parts) parts = parts.split(','); - else { - parts = new Array(); - for (var p in parts_map) parts.push(p); - } - // now add the parts to the parts bin var parts_left = this.width + 3 + background_margin; var parts_top = background_margin; @@ -209,15 +237,17 @@ schematic = (function() { } // add all elements to the DOM - this.div.appendChild(this.background); - for (var i = 0; i < this.toolbar.length; i++) { - var tool = this.toolbar[i]; - if (tool != null) this.div.appendChild(tool); + if (!this.diagram_only) { + this.div.appendChild(this.background); + for (var i = 0; i < this.toolbar.length; i++) { + var tool = this.toolbar[i]; + if (tool != null) this.div.appendChild(tool); + } + this.div.appendChild(this.status_div); + for (var i = 0; i < this.parts_bin.length; i++) + this.div.appendChild(this.parts_bin[i].canvas); } this.div.appendChild(this.canvas); - this.div.appendChild(this.status_div); - for (var i = 0; i < this.parts_bin.length; i++) - this.div.appendChild(this.parts_bin[i].canvas); input.parentNode.insertBefore(this.div,input.nextSibling); // make sure other code can find us! @@ -243,21 +273,28 @@ schematic = (function() { // limit the shrinkage factor w = Math.max(w,120); h = Math.max(h,120); - this.width = w; this.height = h; this.bg_image.width = w; this.bg_image.height = h; + if (this.diagram_only) { + this.canvas.width = w; + this.canvas.height = h; + this.redraw_background(); // redraw diagram + return; + } + this.min_x = 0; this.min_y = 0; this.max_x = w/this.scale; this.max_y = h/this.scale; - var left = 2*background_margin; // space to the left + var left,top; // start with tool bar - var top = background_margin; + left = 2*background_margin; // space to the left + top = background_margin; var max_height = 0; if (this.toolbar.length > 0) { tool_left = left; @@ -296,7 +333,7 @@ schematic = (function() { for (var i = 0; i < this.parts_bin.length; i++) { var part = this.parts_bin[i]; part.set_location(parts_left,parts_top); - + total_w = part.right(); parts_top = part.bottom() + 2; if (parts_top + part_h > parts_h_limit) { @@ -604,11 +641,7 @@ schematic = (function() { // //////////////////////////////////////////////////////////////////////////////// - Schematic.prototype.dc_analysis = function() { - // remove any previous annotations - this.operating_point = undefined; - this.redraw(); - + Schematic.prototype.extract_circuit = function() { // give all the circuit nodes a name, extract netlist this.label_connection_points(); var netlist = this.json(); @@ -616,6 +649,16 @@ schematic = (function() { // create a circuit from the netlist var ckt = new cktsim.Circuit(); ckt.load_netlist(netlist); + + return ckt; + } + + Schematic.prototype.dc_analysis = function() { + // remove any previous annotations + this.unselect_all(-1); + this.redraw_background(); + + var ckt = this.extract_circuit(); // run the analysis this.operating_point = ckt.dc(); @@ -624,7 +667,88 @@ schematic = (function() { this.redraw(); } + // return a list of [color,node_label] for each probe in the diagram + Schematic.prototype.find_probes = function() { + var result = []; + for (var i = this.components.length - 1; i >= 0; --i) { + var c = this.components[i]; + if (c.type == 's') result.push(c.probe_info()); + } + return result; + } + Schematic.prototype.ac_analysis = function() { + this.unselect_all(-1); + this.redraw_background(); + + var npts_lbl = 'Number of points/decade'; + var fstart_lbl = 'Starting frequency (Hz)'; + var fstop_lbl = 'Ending frequency (Hz)'; + + if (this.find_probes().length == 0) { + this.message("AC Analysis: there are no scope probes in the diagram!"); + return; + } + + var fields = new Array(); + fields[npts_lbl] = build_input('text',10,this.ac_npts); + fields[fstart_lbl] = build_input('text',10,this.ac_fstart); + fields[fstop_lbl] = build_input('text',10,this.ac_fstop); + + var content = build_table(fields); + content.fields = fields; + content.sch = this; + + this.dialog('AC Analysis',content,function(content) { + var sch = content.sch; + var ckt = sch.extract_circuit(); + + // retrieve parameters, remember for next time + sch.ac_npts = content.fields[npts_lbl].value; + sch.ac_fstart = content.fields[fstart_lbl].value; + sch.ac_fstop = content.fields[fstop_lbl].value; + + // run the analysis + var results = ckt.ac(ckt.parse_number(sch.ac_npts), + ckt.parse_number(sch.ac_fstart), + ckt.parse_number(sch.ac_fstop)); + + if (typeof results == 'string') + sch.message(results); + else { + var x_values = results['frequencies']; + + // x axis will be a log scale + for (var i = x_values.length - 1; i >= 0; --i) + x_values[i] = Math.log(x_values[i])/Math.LN10; + + // set up plot values for each node with a probe + var y_values = []; // list of [color, result_array] + var probes = sch.find_probes(); + + for (var i = probes.length - 1; i >= 0; --i) { + var color = probes[i][0]; + var label = probes[i][1]; + var v = results[label]; + + // convert values into dB relative to max value + var v_max = -Infinity; + for (var j = v.length - 1; j >= 0; --j) { + v[j] = Math.abs(v[j]); + if (v[j] > v_max) v_max = v[j]; + } + for (var j = v.length - 1; j >= 0; --j) + // convert each value to dB relative to max + v[j] = 20.0 * Math.log(v[j]/v_max)/Math.LN10; + + y_values.push([color,v]); + } + + // graph the result and display in a window + var graph = sch.graph(x_values,y_values,'log(Frequency)','dB'); + sch.window('Results of AC Analysis',graph); + } + }) } Schematic.prototype.transient_analysis = function() { @@ -643,24 +767,24 @@ schematic = (function() { var w = this.bg_image.width; var h = this.bg_imageheight; - // paint background color - c.fillStyle = element_style; - c.fillRect(0,0,this.width,this.height); + c.lineCap = 'round'; - // border - //c.strokeStyle = "rgb(0,0,0)"; - //c.strokeRect(0,0,this.width,this.height); + if (!this.diagram_only) { + // paint background color + c.fillStyle = element_style; + c.fillRect(0,0,this.width,this.height); - // grid - c.strokeStyle = grid_style; - var first_x = this.min_x; - var last_x = this.max_x; - var first_y = this.min_y; - var last_y = this.max_y; - for (var i = first_x; i < last_x; i += this.grid) - this.draw_line(c,i,first_y,i,last_y,0.1); - for (var i = first_y; i < last_y; i += this.grid) - this.draw_line(c,first_x,i,last_x,i,0.1); + // grid + c.strokeStyle = grid_style; + var first_x = this.min_x; + var last_x = this.max_x; + var first_y = this.min_y; + var last_y = this.max_y; + for (var i = first_x; i < last_x; i += this.grid) + this.draw_line(c,i,first_y,i,last_y,0.1); + for (var i = first_y; i < last_y; i += this.grid) + this.draw_line(c,first_x,i,last_x,i,0.1); + } // unselected components for (var i = this.components.length - 1; i >= 0; --i) { @@ -720,14 +844,18 @@ schematic = (function() { // display operating point results if (this.operating_point) { - // make a copy of the operating_point info so we can mess with it - var temp = new Array(); - for (var i in this.operating_point) temp[i] = this.operating_point[i]; + if (typeof this.operating_point == 'string') + this.message(this.operating_point); + else { + // make a copy of the operating_point info so we can mess with it + var temp = new Array(); + for (var i in this.operating_point) temp[i] = this.operating_point[i]; - // run through connection points displaying (once) the voltage - // for each electrical node - for (var location in this.connection_points) - (this.connection_points[location])[0].display_voltage(c,temp); + // run through connection points displaying (once) the voltage + // for each electrical node + for (var location in this.connection_points) + (this.connection_points[location])[0].display_voltage(c,temp); + } } // finally overlay cursor @@ -745,6 +873,14 @@ schematic = (function() { this.draw_line(c,x,y-this.grid,x,y+this.grid,1); } + Schematic.prototype.moveTo = function(c,x,y) { + c.moveTo((x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale); + } + + Schematic.prototype.lineTo = function(c,x,y) { + c.lineTo((x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale); + } + Schematic.prototype.draw_line = function(c,x1,y1,x2,y2,width) { c.lineWidth = width*this.scale; c.beginPath(); @@ -1087,6 +1223,7 @@ schematic = (function() { // div to hold the content var body = document.createElement('div'); + content.style.marginBotton = '5px'; body.appendChild(content); body.style.padding = '5px'; dialog.appendChild(body); @@ -1146,6 +1283,50 @@ schematic = (function() { // //////////////////////////////////////////////////////////////////////////////// + // build a 2-column HTML table from an associative array (keys as text in + // column 1, values in column 2). + function build_table(a) { + var tbl = document.createElement('table'); + + // build a row for each element in associative array + for (var i in a) { + var label = document.createTextNode(i + ': '); + var col1 = document.createElement('td'); + col1.appendChild(label); + var col2 = document.createElement('td'); + col2.appendChild(a[i]); + var row = document.createElement('tr'); + row.appendChild(col1); + row.appendChild(col2); + row.style.verticalAlign = 'center'; + tbl.appendChild(row); + } + + return tbl; + } + + // build an input field + function build_input(type,size,value) { + var input = document.createElement('input'); + input.type = type; + input.size = size; + if (value == undefined) input.value = ''; + else input.value = value.toString(); + return input; + } + + // build a select widget using the strings found in the options array + function build_select(options,selected) { + var select = document.createElement('select'); + for (var i = 0; i < options.length; i++) { + var option = document.createElement('option'); + option.text = options[i]; + select.add(option); + if (options[i] == selected) select.selectedIndex = i; + } + return select; + } + Schematic.prototype.window = function(title,content) { // create the div for the top level of the window var win = document.createElement('div'); @@ -1299,13 +1480,16 @@ schematic = (function() { Schematic.prototype.enable_tool = function(tname,which) { var tool = this.tools[tname]; - tool.style.opacity = which ? 1.0 : 0.2; - tool.enabled = which; - // if disabling tool, remove border and tip - if (!which) { - tool.style.borderColor = background_style; - tool.sch.message(''); + if (tool != undefined) { + tool.style.opacity = which ? 1.0 : 0.2; + tool.enabled = which; + + // if disabling tool, remove border and tip + if (!which) { + tool.style.borderColor = background_style; + tool.sch.message(''); + } } } @@ -1349,6 +1533,239 @@ schematic = (function() { close_icon = 'data:image/gif;base64,R0lGODlhEAAQAMQAAGtra/f3/62tre/v9+bm787O1pycnHNzc6WlpcXFxd7e3tbW1nt7e7W1te/v74SEhMXFzmNjY+bm5v///87OzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAAQABAAAAVt4DRMZGmSwRQQBUS9MAwRIyQ5Uq7neEFSDtxOF4T8cobIQaE4RAQ5yjHHiCCSD510QtFGvoCFdppDfBu7bYzy+D7WP5ggAgA8Y3FKwi5IAhIweW1vbBGEWy5rilsFi2tGAwSJixAFBCkpJ5ojIQA7'; + /////////////////////////////////////////////////////////////////////////////// + // + // Graphing + // + /////////////////////////////////////////////////////////////////////////////// + + // add dashed lines! + // from http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/ + CanvasRenderingContext2D.prototype.dashedLineTo = function(fromX, fromY, toX, toY, pattern) { + // Our growth rate for our line can be one of the following: + // (+,+), (+,-), (-,+), (-,-) + // Because of this, our algorithm needs to understand if the x-coord and + // y-coord should be getting smaller or larger and properly cap the values + // based on (x,y). + var lt = function (a, b) { return a <= b; }; + var gt = function (a, b) { return a >= b; }; + var capmin = function (a, b) { return Math.min(a, b); }; + var capmax = function (a, b) { return Math.max(a, b); }; + + var checkX = { thereYet: gt, cap: capmin }; + var checkY = { thereYet: gt, cap: capmin }; + + if (fromY - toY > 0) { + checkY.thereYet = lt; + checkY.cap = capmax; + } + if (fromX - toX > 0) { + checkX.thereYet = lt; + checkX.cap = capmax; + } + + this.moveTo(fromX, fromY); + var offsetX = fromX; + var offsetY = fromY; + var idx = 0, dash = true; + while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) { + var ang = Math.atan2(toY - fromY, toX - fromX); + var len = pattern[idx]; + + offsetX = checkX.cap(toX, offsetX + (Math.cos(ang) * len)); + offsetY = checkY.cap(toY, offsetY + (Math.sin(ang) * len)); + + if (dash) this.lineTo(offsetX, offsetY); + else this.moveTo(offsetX, offsetY); + + idx = (idx + 1) % pattern.length; + dash = !dash; + } + }; + + // given a range of values, return a new range [vmin',vmax'] where the limits + // have been chosen "nicely". Taken from matplotlib.ticker.LinearLocator + function view_limits(vmin,vmax) { + var log_range = Math.log(vmax - vmin)/Math.LN10; + var exponent = Math.floor(log_range); + //if (log_range - exponent < 0.5) exponent -= 1; + var scale = Math.pow(10,-exponent); + vmin = Math.floor(scale*vmin)/scale; + vmax = Math.ceil(scale*vmax)/scale; + return [vmin,vmax,1.0/scale]; + } + + // x_values is an array of x coordinates for each of the plots + // y_values is an array of [color, value_array], one entry for each plot + Schematic.prototype.graph = function(x_values,y_values,x_legend,y_legend) { + var pwidth = 400; // dimensions of actual plot + var pheight = 300; // dimensions of actual plot + var left_margin = 55; + var top_margin = 25; + var right_margin = 25; + var bottom_margin = 45; + var tick_length = 5; + var pattern = [1,2]; + + var w = pwidth + left_margin + right_margin; + var h = pheight + top_margin + bottom_margin; + + var canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + + // the graph itself will be drawn here and this image will be copied + // onto canvas, where it can be overlayed with mouse cursors, etc. + var bg_image = document.createElement('canvas'); + bg_image.width = w; + bg_image.height = h; + canvas.bg_image = bg_image; // so we can find it during event handling + + // start by painting an opaque background + var c = bg_image.getContext('2d'); + c.fillStyle = background_style; + c.fillRect(0,0,w,h); + c.fillStyle = element_style; + c.fillRect(left_margin,top_margin,pwidth,pheight); + + // figure out scaling for plots + var x_min = array_min(x_values); + var x_max = array_max(x_values); + var x_limits = view_limits(x_min,x_max); + x_min = x_limits[0]; + x_max = x_limits[1]; + var x_scale = pwidth/(x_max - x_min); + + function plot_x(x) { + return (x - x_min)*x_scale + left_margin; + } + + // draw x grid + c.strokeStyle = grid_style; + c.lineWidth = 1; + c.fillStyle = normal_style; + c.font = '10pt sans-serif'; + c.textAlign = 'center'; + c.textBaseline = 'top'; + var end = top_margin + pheight; + for (var x = x_min; x <= x_max; x += x_limits[2]) { + var temp = plot_x(x) + 0.5; // keep lines crisp! + + // grid line + c.beginPath(); + if (x == x_min) { + c.moveTo(temp,top_margin); + c.lineTo(temp,end); + } else + c.dashedLineTo(temp,top_margin,temp,end,pattern); + c.stroke(); + + // tick mark + c.beginPath(); + c.moveTo(temp,end); + c.lineTo(temp,end + tick_length); + c.stroke(); + c.fillText(x.toString(),temp,end + tick_length); + } + + var y_min = Infinity; + var y_max = -Infinity; + var plot; + for (plot = y_values.length - 1; plot >= 0; --plot) { + var values = y_values[plot][1]; + var temp = array_min(values); + if (temp < y_min) y_min = temp; + temp = array_max(values); + if (temp > y_max) y_max = temp; + } + var y_limits = view_limits(y_min,y_max); + y_min = y_limits[0]; + y_max = y_limits[1]; + var y_scale = pheight/(y_max - y_min); + + function plot_y(y) { + return (y_max - y)*y_scale + top_margin; + } + + // draw y grid + c.textAlign = 'right'; + c.textBaseline = 'middle'; + for (var y = y_min; y <= y_max; y += y_limits[2]) { + var temp = plot_y(y) + 0.5; // keep lines crisp! + + // grid line + c.beginPath(); + if (y == y_min) { + c.moveTo(left_margin,temp); + c.lineTo(left_margin + pwidth,temp); + } else + c.dashedLineTo(left_margin,temp,left_margin + pwidth,temp,pattern); + c.stroke(); + + // tick mark + c.beginPath(); + c.moveTo(left_margin - tick_length,temp); + c.lineTo(left_margin,temp); + c.stroke(); + c.fillText(y.toString(),left_margin - tick_length -2,temp); + } + + // now draw each plot + var x,y; + c.lineWidth = 3; + for (plot = y_values.length - 1; plot >= 0; --plot) { + c.strokeStyle = probe_colors_rgb[y_values[plot][0]]; + var values = y_values[plot][1]; + + c.beginPath(); + x = plot_x(x_values[0]); + y = plot_y(values[0]); + c.moveTo(x,y); + for (var i = 1; i < x_values.length; i++) { + x = plot_x(x_values[i]); + y = plot_y(values[i]); + c.lineTo(x,y); + } + c.stroke(); + } + + // draw legends + c.font = '12pt sans-serif'; + c.textAlign = 'center'; + c.textBaseline = 'bottom'; + c.fillText(x_legend,left_margin + pwidth/2,h - 5); + + c.textBaseline = 'top'; + c.save(); + c.translate(5 ,top_margin + pheight/2); + c.rotate(-Math.PI/2); + c.fillText(y_legend,0,0); + c.restore(); + + // return our masterpiece + redraw_plot(canvas); + return canvas; + } + + function array_max(a) { + max = -Infinity; + for (var i = a.length - 1; i >= 0; --i) + if (a[i] > max) max = a[i]; + return max; + } + + function array_min(a) { + min = Infinity; + for (var i = a.length - 1; i >= 0; --i) + if (a[i] < min) min = a[i]; + return min; + } + + function redraw_plot(canvas) { + var c = canvas.getContext('2d'); + c.drawImage(canvas.bg_image,0,0); + } + /////////////////////////////////////////////////////////////////////////////// // // Parts bin @@ -1430,6 +1847,14 @@ schematic = (function() { // no connection points in the parts bin } + Part.prototype.moveTo = function(c,x,y) { + c.moveTo((x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale); + } + + Part.prototype.lineTo = function(c,x,y) { + c.lineTo((x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale); + } + Part.prototype.draw_line = function(c,x1,y1,x2,y2,width) { c.lineWidth = width*this.scale; c.beginPath(); @@ -1552,8 +1977,9 @@ schematic = (function() { // //////////////////////////////////////////////////////////////////////////////// - function Component(x,y,rotation) { + function Component(type,x,y,rotation) { this.sch = undefined; + this.type = type; this.x = x; this.y = y; this.rotation = rotation; @@ -1666,6 +2092,18 @@ schematic = (function() { else return y; } + Component.prototype.moveTo = function(c,x,y) { + var nx = this.transform_x(x,y) + this.x; + var ny = this.transform_y(x,y) + this.y; + this.sch.moveTo(c,nx,ny); + } + + Component.prototype.lineTo = function(c,x,y) { + var nx = this.transform_x(x,y) + this.x; + var ny = this.transform_y(x,y) + this.y; + this.sch.lineTo(c,nx,ny); + } + Component.prototype.draw_line = function(c,x1,y1,x2,y2) { c.strokeStyle = this.selected ? selected_style : normal_style; var nx1 = this.transform_x(x1,y1) + this.x; @@ -1775,39 +2213,19 @@ schematic = (function() { Component.prototype.edit_properties = function(x,y) { if (inside(this.bbox,x,y)) { - var content = document.createElement('table'); - content.style.marginBotton = '5px'; - content.fields = []; + // make an widget for each property + var fields = new Array(); + for (var i in this.properties) + fields[i] = build_input('text',10,this.properties[i]); - // add an field for each property - for (var i in this.properties) { - var label = document.createTextNode(i + ': '); - var field = document.createElement('input'); - field.type = 'text'; - var pvalue = this.properties[i]; - field.value = pvalue ? pvalue : ''; - field.size = 10; - content.fields.push([i,field]); - - var col1 = document.createElement('td'); - col1.appendChild(label); - var col2 = document.createElement('td'); - col2.appendChild(field); - var row = document.createElement('tr'); - row.appendChild(col1); - row.appendChild(col2); - row.style.verticalAlign = 'center'; - - content.appendChild(row); - } - - var component = this; // capture in closure below + var content = build_table(fields); + content.fields = fields; + content.component = this; this.sch.dialog('Edit Properties',content,function(content) { - var fields = content.fields; - for (var i = fields.length - 1; i >= 0; i--) - component.properties[fields[i][0]] = fields[i][1].value; - component.sch.redraw_background(); + for (var i in content.fields) + content.component.properties[i] = content.fields[i].value; + content.component.sch.redraw_background(); }); return true; } else return false; @@ -1912,8 +2330,10 @@ schematic = (function() { var label = v.toFixed(2) + 'V'; // first draw some solid blocks in the background - this.parent.draw_text(c,'\u2588\u2588\u2588\u2588',this.offset_x,this.offset_y, - 4,annotation_size,background_style); + c.globalAlpha = 0.85; + this.parent.draw_text(c,'\u2588\u2588\u2588',this.offset_x,this.offset_y, + 4,annotation_size,element_style); + c.globalAlpha = 1.0; // display the node voltage at this connection point this.parent.draw_text(c,label,this.offset_x,this.offset_y, @@ -1934,7 +2354,7 @@ schematic = (function() { function Wire(x1,y1,x2,y2) { // arbitrarily call x1,y1 the origin - Component.call(this,x1,y1,0); + Component.call(this,'w',x1,y1,0); this.dx = x2 - x1; this.dy = y2 - y1; this.add_connection(0,0); @@ -2045,11 +2465,10 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Ground(x,y,rotation) { - Component.call(this,x,y,rotation); + Component.call(this,'g',x,y,rotation); this.add_connection(0,0); this.bounding_box = [-6,0,6,8]; this.update_coords(); - this.type = 'g'; } Ground.prototype = new Component(); Ground.prototype.constructor = Ground; @@ -2073,6 +2492,83 @@ schematic = (function() { this.connections[0].propagate_label('0'); // canonical label for GND node } + //////////////////////////////////////////////////////////////////////////////// + // + // Scope Probe + // + //////////////////////////////////////////////////////////////////////////////// + + probe_colors = ['red','green','blue','cyan','magenta','black']; + probe_colors_rgb = { + 'red': 'rgb(255,64,64)', + 'green': 'rgb(64,255,64)', + 'blue': 'rgb(64,64,255)', + 'cyan': 'rgb(64,255,255)', + 'magenta' : 'rgb(255,64,255)', + 'black': 'rgb(0,0,0)', + }; + + function Probe(x,y,rotation,color) { + Component.call(this,'s',x,y,rotation); + this.add_connection(0,0); + this.properties['color'] = color ? color : 'cyan'; + this.bounding_box = [0,0,27,-21]; + this.update_coords(); + } + Probe.prototype = new Component(); + Probe.prototype.constructor = Probe; + + Probe.prototype.toString = function() { + return ''; + } + + Probe.prototype.draw = function(c) { + // draw outline + this.draw_line(c,0,0,4,-4); + this.draw_line(c,2,-6,6,-2); + this.draw_line(c,2,-6,17,-21); + this.draw_line(c,6,-2,21,-17); + this.draw_line(c,17,-21,21,-17); + this.draw_arc(c,19,-11,8,3*Math.PI/2,0); + + // fill body with plot color + c.fillStyle = probe_colors_rgb[this.properties['color']]; + c.beginPath(); + this.moveTo(c,2,-6) + this.lineTo(c,6,-2); + this.lineTo(c,21,-17); + this.lineTo(c,17,-21); + this.lineTo(c,2,-6); + c.fill(); + } + + Probe.prototype.clone = function(x,y) { + return new Probe(x,y,this.rotation,this.properties['color']); + } + + Probe.prototype.edit_properties = function(x,y) { + if (inside(this.bbox,x,y)) { + var fields = new Array(); + fields['Plot color'] = build_select(probe_colors,this.properties['color']); + + var content = build_table(fields); + content.fields = fields; + content.component = this; + + this.sch.dialog('Edit Properties',content,function(content) { + var color_choice = content.fields['Plot color']; + content.component.properties['color'] = probe_colors[color_choice.selectedIndex]; + content.component.sch.redraw_background(); + }); + return true; + } else return false; + } + + // return [color, node_label] for this probe + Probe.prototype.probe_info = function() { + return [this.properties['color'],this.connections[0].label]; + } + //////////////////////////////////////////////////////////////////////////////// // // Resistor @@ -2080,14 +2576,13 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Resistor(x,y,rotation,name,r) { - Component.call(this,x,y,rotation); + Component.call(this,'r',x,y,rotation); this.properties['name'] = name; this.properties['r'] = r ? r : '1'; this.add_connection(0,0); this.add_connection(0,48); this.bounding_box = [-4,0,4,48]; this.update_coords(); - this.type = 'r'; } Resistor.prototype = new Component(); Resistor.prototype.constructor = Resistor; @@ -2123,14 +2618,13 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Capacitor(x,y,rotation,name,c) { - Component.call(this,x,y,rotation); + Component.call(this,'c',x,y,rotation); this.properties['name'] = name; this.properties['c'] = c ? c : '1p'; this.add_connection(0,0); this.add_connection(0,48); this.bounding_box = [-8,0,8,48]; this.update_coords(); - this.type = 'c'; } Capacitor.prototype = new Component(); Capacitor.prototype.constructor = Capacitor; @@ -2161,14 +2655,13 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Inductor(x,y,rotation,name,l) { - Component.call(this,x,y,rotation); + Component.call(this,'l',x,y,rotation); this.properties['name'] = name; this.properties['l'] = l ? l : '1n'; this.add_connection(0,0); this.add_connection(0,48); this.bounding_box = [-4,0,5,48]; this.update_coords(); - this.type = 'l'; } Inductor.prototype = new Component(); Inductor.prototype.constructor = Inductor; @@ -2201,13 +2694,12 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Diode(x,y,rotation,name) { - Component.call(this,x,y,rotation); + Component.call(this,'d',x,y,rotation); this.properties['name'] = name; this.add_connection(0,0); // anode this.add_connection(0,48); // cathode this.bounding_box = [-8,0,8,48]; this.update_coords(); - this.type = 'd'; } Diode.prototype = new Component(); Diode.prototype.constructor = Diode; @@ -2239,7 +2731,7 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function NFet(x,y,rotation,name,sw,sl) { - Component.call(this,x,y,rotation); + Component.call(this,'n',x,y,rotation); this.properties['name'] = name; this.properties['scaled width'] = sw ? sw : '2'; this.properties['scaled length'] = sl ? sl : '1'; @@ -2248,7 +2740,6 @@ schematic = (function() { this.add_connection(-24,24); // gate this.bounding_box = [-24,0,8,48]; this.update_coords(); - this.type = 'n'; } NFet.prototype = new Component(); NFet.prototype.constructor = NFet; @@ -2287,7 +2778,7 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function PFet(x,y,rotation,name,sw,sl) { - Component.call(this,x,y,rotation); + Component.call(this,'p',x,y,rotation); this.properties['name'] = name; this.properties['scaled width'] = sw ? sw : '2'; this.properties['scaled length'] = sl ? sl : '1'; @@ -2296,7 +2787,6 @@ schematic = (function() { this.add_connection(-24,24); // gate this.bounding_box = [-24,0,8,48]; this.update_coords(); - this.type = 'p'; } PFet.prototype = new Component(); PFet.prototype.constructor = PFet; @@ -2337,14 +2827,13 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function OpAmp(x,y,rotation,name,sw,sl) { - Component.call(this,x,y,rotation); + Component.call(this,'o',x,y,rotation); this.properties['name'] = name; this.add_connection(0,0); // + this.add_connection(0,16); // - this.add_connection(48,8); // output this.bounding_box = [0,-8,48,24]; this.update_coords(); - this.type = 'o'; } OpAmp.prototype = new Component(); OpAmp.prototype.constructor = OpAmp; @@ -2382,8 +2871,7 @@ schematic = (function() { //////////////////////////////////////////////////////////////////////////////// function Source(x,y,rotation,name,type,value) { - Component.call(this,x,y,rotation); - this.type = type; + Component.call(this,type,x,y,rotation); this.properties['name'] = name; this.properties['value'] = value ? value : '1'; this.add_connection(0,0); @@ -2404,10 +2892,12 @@ schematic = (function() { this.draw_line(c,0,36,0,48); if (this.type == 'v') { // voltage source + this.draw_text(c,'+',0,12,1,property_size); + this.draw_text(c,'\u2013',0,36,7,property_size); // minus sign // draw + and - - this.draw_line(c,8,5,8,11); - this.draw_line(c,5,8,11,8); - this.draw_line(c,5,40,11,40); + //this.draw_line(c,8,5,8,11); + //this.draw_line(c,5,8,11,8); + //this.draw_line(c,5,40,11,40); // draw V this.draw_line(c,-3,20,0,28); this.draw_line(c,3,20,0,28);