diff --git a/lms/static/js/schematic.js b/lms/static/js/schematic.js
new file mode 100644
index 0000000000..a92bca65cd
--- /dev/null
+++ b/lms/static/js/schematic.js
@@ -0,0 +1,4325 @@
+/////////////////////////////////////////////////////////////////////////////
+//
+// Simple schematic capture
+//
+////////////////////////////////////////////////////////////////////////////////
+
+// Copyright (C) 2011 Massachusetts Institute of Technology
+
+// add schematics to a document with
+//
+//
+//
+// 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="" disables editing of diagram
+
+// JSON schematic representation:
+// sch := [part, part, ...]
+// part := [type, coords, properties, connections]
+// type := string (see parts_map)
+// coords := [number, ...] // (x,y,rot) or (x1,y1,x2,y2)
+// properties := {name: value, ...}
+// connections := [node, ...] // one per connection point in canoncial order
+// node := string
+// need a netlist? just use the part's type, properites and connections
+
+// TO DO:
+// - wire labels?
+// - zoom/scroll canvas
+// - rotate multiple objects around their center of mass
+// - rubber band wires when moving components
+
+// set up each schematic entry widget
+function update_schematics() {
+ // set up each schematic on the page
+ var schematics = document.getElementsByClassName('schematic');
+ for (var i = 0; i < schematics.length; ++i)
+ if (schematics[i].getAttribute("loaded") != "true") {
+ try {
+ new schematic.Schematic(schematics[i]);
+ } catch (err) {
+ var msgdiv = document.createElement('div');
+ msgdiv.style.border = 'thick solid #FF0000';
+ msgdiv.style.margins = '20px';
+ msgdiv.style.padding = '20px';
+ var msg = document.createTextNode('Sorry, there a browser error in starting the schematic tool. The tool is known to be compatible with the latest versions of Firefox and Chrome, which we recommend you use.');
+ msgdiv.appendChild(msg);
+ schematics[i].parentNode.insertBefore(msgdiv,schematics[i]);
+ }
+ schematics[i].setAttribute("loaded","true");
+ }
+}
+
+// add ourselves to the tasks that get performed when window is loaded
+function add_schematic_handler(other_onload) {
+ return function() {
+ // execute othe onload functions first
+ if (other_onload) other_onload();
+
+ update_schematics();
+ }
+}
+window.onload = add_schematic_handler(window.onload);
+
+// ask each schematic input widget to update its value field for submission
+function prepare_schematics() {
+ var schematics = document.getElementsByClassName('schematic');
+ for (var i = schematics.length - 1; i >= 0; i--)
+ schematics[i].schematic.update_value();
+}
+
+schematic = (function() {
+ background_style = 'rgb(220,220,220)';
+ element_style = 'rgb(255,255,255)';
+ thumb_style = 'rgb(128,128,128)';
+ normal_style = 'rgb(0,0,0)'; // default drawing color
+ component_style = 'rgb(64,64,255)'; // color for unselected components
+ selected_style = 'rgb(64,255,64)'; // highlight color for selected components
+ grid_style = "rgb(128,128,128)";
+ annotation_style = 'rgb(255,64,64)'; // color for diagram annotations
+
+ property_size = 5; // point size for Component property text
+ annotation_size = 6; // point size for diagram annotations
+
+ // list of all the defined parts
+ parts_map = {
+ 'g': [Ground, 'Ground connection'],
+ 'L': [Label, 'Node label'],
+ 'v': [VSource, 'Voltage source'],
+ 'i': [ISource, 'Current source'],
+ 'r': [Resistor, 'Resistor'],
+ 'c': [Capacitor, 'Capacitor'],
+ 'l': [Inductor, 'Inductor'],
+ 'o': [OpAmp, 'Op Amp'],
+ 'd': [Diode, 'Diode'],
+ 'n': [NFet, 'NFet'],
+ 'p': [PFet, 'PFet'],
+ 's': [Probe, 'Voltage Probe'],
+ 'a': [Ammeter, 'Current Probe'],
+ };
+
+ // global clipboard
+ if (typeof sch_clipboard == 'undefined')
+ sch_clipboard = [];
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Schematic = diagram + parts bin + status area
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // setup a schematic by populating the
with the appropriate children
+ function Schematic(input) {
+ // set up diagram viewing parameters
+ this.show_grid = true;
+ this.grid = 8;
+ this.scale = 2;
+ 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.cursor_x = 0;
+ this.cursor_y = 0;
+
+ this.window_list = []; // list of pop-up windows in increasing z order
+
+ // 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 == 'None') {
+ parts = new Array();
+ for (var p in parts_map) parts.push(p);
+ } else if (parts == '') {
+ this.edits_allowed = false;
+ parts = [];
+ } else parts = parts.split(',');
+
+ // now add the parts to the parts bin
+ this.parts_bin = [];
+ for (var i = 0; i < parts.length; i++) {
+ var part = new Part(this);
+ var pm = parts_map[parts[i]];
+ part.set_component(new pm[0](0,0,0),pm[1]);
+ this.parts_bin.push(part);
+ }
+
+ // use user-supplied list of analyses, otherwise provide them all
+ // analyses="" means no analyses
+ var analyses = input.getAttribute('analyses');
+ if (analyses == undefined || analyses == 'None')
+ 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;
+
+ // see what we need to submit. Expecting attribute of the form
+ // submit_analyses="{'tran':[[node_name,t1,t2,t3],...],
+ // 'ac':[[node_name,f1,f2,...],...]}"
+ var submit = input.getAttribute('submit_analyses');
+ if (submit && submit.indexOf('{') != -1)
+ this.submit_analyses = JSON.parse(submit);
+ else
+ this.submit_analyses = undefined;
+
+ // toolbar
+ this.tools = new Array();
+ this.toolbar = [];
+
+ if (!this.diagram_only) {
+ this.tools['help'] = this.add_tool(help_icon,'Help: display help page',this.help);
+ this.enable_tool('help',true);
+ this.toolbar.push(null); // spacer
+ }
+
+ if (this.edits_allowed) {
+ this.tools['grid'] = this.add_tool(grid_icon,'Grid: toggle grid display',this.toggle_grid);
+ this.enable_tool('grid',true);
+ 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') {
+ if (analyses.indexOf('dc') != -1) {
+ this.tools['dc'] = this.add_tool('DC','DC Analysis',this.dc_analysis);
+ this.enable_tool('dc',true);
+ this.dc_max_iters = '1000'; // default values dc solution
+ }
+
+ if (analyses.indexOf('ac') != -1) {
+ this.tools['ac'] = this.add_tool('AC','AC Small-Signal Analysis',this.setup_ac_analysis);
+ this.enable_tool('ac',true);
+ this.ac_npts = '50'; // default values for AC Analysis
+ this.ac_fstart = '10';
+ this.ac_fstop = '1G';
+ this.ac_source_name = undefined;
+ }
+
+ if (analyses.indexOf('tran') != -1) {
+ this.tools['tran'] = this.add_tool('TRAN','Transient Analysis',this.transient_analysis);
+ this.enable_tool('tran',true);
+ this.tran_npts = '100'; // default values for transient analysis
+ this.tran_tstop = '1';
+ }
+ }
+
+ // set up diagram canvas
+ this.canvas = document.createElement('canvas');
+ this.width = input.getAttribute('width');
+ this.width = parseInt(this.width == undefined ? '400' : this.width);
+ this.canvas.width = this.width;
+ this.height = input.getAttribute('height');
+ this.height = parseInt(this.height == undefined ? '300' : this.height);
+ this.canvas.height = this.height;
+
+ this.sctl_r = 16; // scrolling control parameters
+ this.sctl_x = this.sctl_r + 8; // upper left
+ this.sctl_y = this.sctl_r + 8;
+ this.zctl_left = this.sctl_x - 8;
+ this.zctl_top = this.sctl_y + this.sctl_r + 8;
+
+ // repaint simply draws this buffer and then adds selected elements on top
+ this.bg_image = document.createElement('canvas');
+ this.bg_image.width = this.width;
+ this.bg_image.height = this.height;
+
+ 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.outline = 'none';
+ }
+
+ this.canvas.schematic = this;
+ 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('mousewheel',schematic_mouse_wheel,false);
+ this.canvas.addEventListener('DOMMouseScroll',schematic_mouse_wheel,false); // for FF
+ this.canvas.addEventListener('dblclick',schematic_double_click,false);
+ this.canvas.addEventListener('keydown',schematic_key_down,false);
+ this.canvas.addEventListener('keyup',schematic_key_up,false);
+ }
+
+ // set up message area
+ if (!this.diagram_only) {
+ this.status_div = document.createElement('div');
+ this.status = document.createTextNode('');
+ this.status_div.appendChild(this.status);
+ this.status_div.style.height = status_height + 'px';
+ } else this.status_div = undefined;
+
+ this.connection_points = new Array(); // location string => list of cp's
+ this.components = [];
+
+ this.dragging = false;
+ this.select_rect = undefined;
+ this.wire = undefined;
+
+ this.operating_point = undefined; // result from DC analysis
+ this.dc_results = undefined; // saved analysis results for submission
+ this.ac_results = undefined; // saved analysis results for submission
+ this.transient_results = undefined; // saved analysis results for submission
+
+ // state of modifier keys
+ this.ctrlKey = false;
+ this.shiftKey = false;
+ this.altKey = false;
+ this.cmdKey = false;
+
+ // make sure other code can find us!
+ input.schematic = this;
+ this.input = input;
+
+ // set up DOM -- use nested tables to do the layout
+ var table,tr,td;
+ table = document.createElement('table');
+ table.rules = 'none';
+ if (!this.diagram_only) {
+ table.frame = 'box';
+ table.style.borderStyle = 'solid';
+ table.style.borderWidth = '2px';
+ table.style.borderColor = normal_style;
+ table.style.backgroundColor = background_style;
+ }
+
+ // add tools to DOM
+ if (this.toolbar.length > 0) {
+ tr = document.createElement('tr');
+ table.appendChild(tr);
+ td = document.createElement('td');
+ td.style.verticalAlign = 'top';
+ td.colSpan = 2;
+ tr.appendChild(td);
+ for (var i = 0; i < this.toolbar.length; ++i) {
+ var tool = this.toolbar[i];
+ if (tool != null) td.appendChild(tool);
+ }
+ }
+
+ // add canvas and parts bin to DOM
+ tr = document.createElement('tr');
+ table.appendChild(tr);
+
+ td = document.createElement('td');
+ tr.appendChild(td);
+ var wrapper = document.createElement('div'); // for inserting pop-up windows
+ td.appendChild(wrapper);
+ wrapper.style.position = 'relative'; // so we can position subwindows
+ wrapper.appendChild(this.canvas);
+
+ td = document.createElement('td');
+ td.style.verticalAlign = 'top';
+ tr.appendChild(td);
+ var parts_table = document.createElement('table');
+ td.appendChild(parts_table);
+ parts_table.rules = 'none';
+ parts_table.frame = 'void';
+ parts_table.cellPadding = '0';
+ parts_table.cellSpacing = '0';
+
+ // fill in parts_table
+ var parts_per_column = Math.floor(this.height / (part_h + 5)); // mysterious extra padding
+ for (var i = 0; i < parts_per_column; ++i) {
+ tr = document.createElement('tr');
+ parts_table.appendChild(tr);
+ for (var j = i; j < this.parts_bin.length; j += parts_per_column) {
+ td = document.createElement('td');
+ tr.appendChild(td);
+ td.appendChild(this.parts_bin[j].canvas);
+ }
+ }
+
+ if (this.status_div != undefined) {
+ tr = document.createElement('tr');
+ table.appendChild(tr);
+ td = document.createElement('td');
+ tr.appendChild(td);
+ td.colSpan = 2;
+ td.appendChild(this.status_div);
+ }
+
+ // add to dom
+ // avoid Chrome bug that changes to text cursor whenever
+ // drag starts. Just do this in schematic tool...
+ var toplevel = document.createElement('div');
+ toplevel.onselectstart = function(){ return false; };
+ toplevel.appendChild(table);
+ this.input.parentNode.insertBefore(toplevel,this.input.nextSibling);
+
+ // process initial contents of diagram
+ this.load_schematic(this.input.getAttribute('value'),
+ this.input.getAttribute('initial_value'));
+
+ // start by centering diagram on the screen
+ this.zoomall();
+ }
+
+ part_w = 42; // size of a parts bin compartment
+ part_h = 42;
+ status_height = 18;
+
+ Schematic.prototype.add_component = function(new_c) {
+ this.components.push(new_c);
+
+ // create undoable edit record here
+ }
+
+ Schematic.prototype.remove_component = function(c) {
+ var index = this.components.indexOf(c);
+ if (index != -1) this.components.splice(index,1);
+ }
+
+ Schematic.prototype.find_connections = function(cp) {
+ return this.connection_points[cp.location];
+ }
+
+ // add connection point to list of connection points at that location
+ Schematic.prototype.add_connection_point = function(cp) {
+ var cplist = this.connection_points[cp.location];
+ if (cplist) cplist.push(cp);
+ else {
+ cplist = [cp];
+ this.connection_points[cp.location] = cplist;
+ }
+
+ // return list of conincident connection points
+ return cplist;
+ }
+
+ // remove connection point from the list points at the old location
+ Schematic.prototype.remove_connection_point = function(cp,old_location) {
+ // remove cp from list at old location
+ var cplist = this.connection_points[old_location];
+ if (cplist) {
+ var index = cplist.indexOf(cp);
+ if (index != -1) {
+ cplist.splice(index,1);
+ // if no more connections at this location, remove
+ // entry from array to keep our search time short
+ if (cplist.length == 0)
+ delete this.connection_points[old_location];
+ }
+ }
+ }
+
+ // connection point has changed location: remove, then add
+ Schematic.prototype.update_connection_point = function(cp,old_location) {
+ this.remove_connection_point(cp,old_location);
+ return this.add_connection_point(cp);
+ }
+
+ // add a wire to the schematic
+ Schematic.prototype.add_wire = function(x1,y1,x2,y2) {
+ var new_wire = new Wire(x1,y1,x2,y2);
+ new_wire.add(this);
+ new_wire.move_end();
+ return new_wire;
+ }
+
+ Schematic.prototype.split_wire = function(w,cp) {
+ // remove bisected wire
+ w.remove();
+
+ // add two new wires with connection point cp in the middle
+ this.add_wire(w.x,w.y,cp.x,cp.y);
+ this.add_wire(w.x+w.dx,w.y+w.dy,cp.x,cp.y);
+ }
+
+ // see if connection points of component c split any wires
+ Schematic.prototype.check_wires = function(c) {
+ for (var i = 0; i < this.components.length; i++) {
+ var cc = this.components[i];
+ if (cc != c) { // don't check a component against itself
+ // only wires will return non-null from a bisect call
+ var cp = cc.bisect(c);
+ if (cp) {
+ // cc is a wire bisected by connection point cp
+ this.split_wire(cc,cp);
+ this.redraw_background();
+ }
+ }
+ }
+ }
+
+ // see if there are any existing connection points that bisect wire w
+ Schematic.prototype.check_connection_points = function(w) {
+ for (var locn in this.connection_points) {
+ var cplist = this.connection_points[locn];
+ if (cplist && w.bisect_cp(cplist[0])) {
+ this.split_wire(w,cplist[0]);
+ this.redraw_background();
+
+ // stop here, new wires introduced by split will do their own checks
+ return;
+ }
+ }
+ }
+
+ // merge collinear wires sharing an end point
+ Schematic.prototype.clean_up_wires = function() {
+ for (var locn in this.connection_points) {
+ var cplist = this.connection_points[locn];
+ if (cplist && cplist.length == 2) {
+ // found a connection with just two connections, see if they're wires
+ var c1 = cplist[0].parent;
+ var c2 = cplist[1].parent;
+ if (c1.type == 'w' && c2.type == 'w') {
+ var e1 = c1.other_end(cplist[0]);
+ var e2 = c2.other_end(cplist[1]);
+ var e3 = cplist[0]; // point shared by the two wires
+ if (collinear(e1,e2,e3)) {
+ c1.remove();
+ c2.remove();
+ this.add_wire(e1.x,e1.y,e2.x,e2.y);
+ }
+ }
+ }
+ }
+ }
+
+ Schematic.prototype.unselect_all = function(which) {
+ this.operating_point = undefined; // remove annotations
+
+ for (var i = this.components.length - 1; i >= 0; --i)
+ if (i != which) this.components[i].set_select(false);
+ }
+
+ Schematic.prototype.drag_begin = function() {
+ // let components know they're about to move
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var component = this.components[i];
+ if (component.selected) component.move_begin();
+ }
+
+ // remember where drag started
+ this.drag_x = this.cursor_x;
+ this.drag_y = this.cursor_y;
+ this.dragging = true;
+ }
+
+ Schematic.prototype.drag_end = function() {
+ // let components know they're done moving
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var component = this.components[i];
+ if (component.selected) component.move_end();
+ }
+ this.dragging = false;
+
+ this.clean_up_wires();
+ this.redraw_background();
+ }
+
+ Schematic.prototype.help = function() {
+ window.open('/static/handouts/schematic_tutorial.pdf');
+ }
+
+ // zoom diagram around given coords
+ Schematic.prototype.rescale = function(nscale,cx,cy) {
+ if (cx == undefined) {
+ // use current center point if no point has been specified
+ cx = this.origin_x + this.width/(2*this.scale);
+ cy = this.origin_y + this.height/(2*this.scale);
+ }
+
+ this.origin_x += cx*(this.scale - nscale);
+ this.origin_y += cy*(this.scale - nscale);
+ this.scale = nscale;
+
+
+ //this.origin_x = cx - this.width/(2*this.scale);
+ //this.origin_y = cy - this.height/(2*this.scale);
+
+ this.redraw_background();
+ }
+
+ Schematic.prototype.toggle_grid = function() {
+ this.show_grid = !this.show_grid;
+ this.redraw_background();
+ }
+
+ zoom_factor = 1.25; // scaling is some power of zoom_factor
+ zoom_min = 0.5;
+ zoom_max = 4.0;
+ origin_min = -200; // in grids
+ origin_max = 200;
+
+ Schematic.prototype.zoomin = function() {
+ var nscale = this.scale * zoom_factor;
+
+ if (nscale < zoom_max) {
+ // keep center of view unchanged
+ this.origin_x += (this.width/2)*(1.0/this.scale - 1.0/nscale);
+ this.origin_y += (this.height/2)*(1.0/this.scale - 1.0/nscale);
+ this.scale = nscale;
+ this.redraw_background();
+ }
+ }
+
+ Schematic.prototype.zoomout = function() {
+ var nscale = this.scale / zoom_factor;
+
+ if (nscale > zoom_min) {
+ // keep center of view unchanged
+ this.origin_x += (this.width/2)*(1.0/this.scale - 1.0/nscale);
+ this.origin_y += (this.height/2)*(1.0/this.scale - 1.0/nscale);
+ this.scale = nscale;
+ this.redraw_background();
+ }
+ }
+
+ Schematic.prototype.zoomall = function() {
+ // w,h for schematic including a 25% margin on all sides
+ var sch_w = 1.5*(this.bbox[2] - this.bbox[0]);
+ var sch_h = 1.5*(this.bbox[3] - this.bbox[1]);
+
+ if (sch_w == 0 && sch_h == 0) {
+ this.origin_x = 0;
+ this.origin_y = 0;
+ this.scale = 2;
+ } else {
+ // compute scales that would make schematic fit, choose smallest
+ var scale_x = this.width/sch_w;
+ var scale_y = this.height/sch_h;
+ this.scale = Math.pow(zoom_factor,Math.ceil(Math.log(Math.min(scale_x,scale_y))/Math.log(zoom_factor)));
+ if (this.scale < zoom_min) this.scale = zoom_min;
+ else if (this.scale > zoom_max) this.scale = zoom_max;
+
+ // center the schematic
+ this.origin_x = (this.bbox[2] + this.bbox[0])/2 - this.width/(2*this.scale);
+ this.origin_y = (this.bbox[3] + this.bbox[1])/2 - this.height/(2*this.scale);
+ }
+
+ this.redraw_background();
+ }
+
+ Schematic.prototype.cut = function() {
+ // clear previous contents
+ sch_clipboard = [];
+
+ // look for selected components, move them to clipboard.
+ for (var i = this.components.length - 1; i >=0; --i) {
+ var c = this.components[i];
+ if (c.selected) {
+ c.remove();
+ sch_clipboard.push(c);
+ }
+ }
+
+ // update diagram view
+ this.redraw();
+ }
+
+ Schematic.prototype.copy = function() {
+ // clear previous contents
+ sch_clipboard = [];
+
+ // look for selected components, copy them to clipboard.
+ for (var i = this.components.length - 1; i >=0; --i) {
+ var c = this.components[i];
+ if (c.selected)
+ sch_clipboard.push(c.clone(c.x,c.y));
+ }
+ }
+
+ Schematic.prototype.paste = function() {
+ // compute left,top of bounding box for origins of
+ // components in the clipboard
+ var left = undefined;
+ var top = undefined;
+ for (var i = sch_clipboard.length - 1; i >= 0; --i) {
+ var c = sch_clipboard[i];
+ left = left ? Math.min(left,c.x) : c.x;
+ top = top ? Math.min(top,c.y) : c.y;
+ }
+
+ this.message('cursor '+this.cursor_x+','+this.cursor_y);
+
+ // clear current selections
+ this.unselect_all(-1);
+ this.redraw_background(); // so we see any components that got unselected
+
+ // make clones of components on the clipboard, positioning
+ // them relative to the cursor
+ for (var i = sch_clipboard.length - 1; i >= 0; --i) {
+ var c = sch_clipboard[i];
+ var new_c = c.clone(this.cursor_x + (c.x - left),this.cursor_y + (c.y - top));
+ new_c.set_select(true);
+ new_c.add(this);
+ }
+
+ // see what we've wrought
+ this.redraw();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Netlist and Simulation interface
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // load diagram from JSON representation
+ Schematic.prototype.load_schematic = function(value,initial_value) {
+ // use default value if no schematic info in value
+ if (value == undefined || value.indexOf('[') == -1)
+ value = initial_value;
+
+ if (value && value.indexOf('[') != -1) {
+ // convert string value into data structure
+ var json = JSON.parse(value);
+
+ // top level is a list of components
+ for (var i = json.length - 1; i >= 0; --i) {
+ var c = json[i];
+ if (c[0] == 'view') {
+ // special hack: view component lets us recreate view
+ // ignore saved view parameters as they sometimes screw students
+ //this.origin_x = c[1];
+ //this.origin_y = c[2];
+ //this.scale = c[3];
+ //this.ac_npts = c[4];
+ this.ac_fstart = c[5];
+ this.ac_fstop = c[6];
+ this.ac_source_name = c[7];
+ this.tran_npts = c[8];
+ this.tran_tstop = c[9];
+ this.dc_max_iters = c[10];
+ } else if (c[0] == 'w') {
+ // wire
+ this.add_wire(c[1][0],c[1][1],c[1][2],c[1][3]);
+ } else if (c[0] == 'dc') {
+ this.dc_results = c[1];
+ } else if (c[0] == 'transient') {
+ this.transient_results = c[1];
+ } else if (c[0] == 'ac') {
+ this.ac_results = c[1];
+ } else {
+ // ordinary component
+ // c := [type, coords, properties, connections]
+ var type = c[0];
+ var coords = c[1];
+ var properties = c[2];
+
+ // make the part
+ var part = new parts_map[type][0](coords[0],coords[1],coords[2]);
+
+ // give it its properties
+ for (var name in properties)
+ part.properties[name] = properties[name];
+
+ // add component to the diagram
+ part.add(this);
+ }
+ }
+ }
+
+ // see what we've got!
+ this.redraw_background();
+ }
+
+ // label all the nodes in the circuit
+ Schematic.prototype.label_connection_points = function() {
+ // start by clearing all the connection point labels
+ for (var i = this.components.length - 1; i >=0; --i)
+ this.components[i].clear_labels();
+
+ // components are in charge of labeling their unlabeled connections.
+ // labels given to connection points will propagate to coincident connection
+ // points and across Wires.
+
+ // let special components like GND label their connection(s)
+ for (var i = this.components.length - 1; i >=0; --i)
+ this.components[i].add_default_labels();
+
+ // now have components generate labels for unlabeled connections
+ this.next_label = 0;
+ for (var i = this.components.length - 1; i >=0; --i)
+ this.components[i].label_connections();
+ }
+
+ // generate a new label
+ Schematic.prototype.get_next_label = function() {
+ // generate next label in sequence
+ this.next_label += 1;
+ return this.next_label.toString();
+ }
+
+ // propagate label to coincident connection points
+ Schematic.prototype.propagate_label = function(label,location) {
+ var cplist = this.connection_points[location];
+ for (var i = cplist.length - 1; i >= 0; --i)
+ cplist[i].propagate_label(label);
+ }
+
+ // update the value field of our corresponding input field with JSON
+ // representation of schematic
+ Schematic.prototype.update_value = function() {
+ // label connection points
+ this.label_connection_points();
+
+ // build JSON data structure, convert to string value for
+ // input field
+ this.input.value = JSON.stringify(this.json_with_analyses());
+ }
+
+ // produce a JSON representation of the diagram
+ Schematic.prototype.json = function() {
+ var json = [];
+
+ // output all the components/wires in the diagram
+ var n = this.components.length;
+ for (var i = 0; i < n; i++)
+ json.push(this.components[i].json(i));
+
+ // capture the current view parameters
+ json.push(['view',this.origin_x,this.origin_y,this.scale,
+ this.ac_npts,this.ac_fstart,this.ac_fstop,
+ this.ac_source_name,this.tran_npts,this.tran_tstop,
+ this.dc_max_iters]);
+
+ return json;
+ }
+
+ // produce a JSON representation of the diagram
+ Schematic.prototype.json_with_analyses = function() {
+ var json = this.json();
+
+ if (this.dc_results != undefined) json.push(['dc',this.dc_results]);
+ if (this.ac_results != undefined) json.push(['ac',this.ac_results]);
+ if (this.transient_results != undefined) json.push(['transient',this.transient_results]);
+
+ return json;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Simulation interface
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ Schematic.prototype.extract_circuit = function() {
+ // give all the circuit nodes a name, extract netlist
+ this.label_connection_points();
+ var netlist = this.json();
+
+ // since we've done the heavy lifting, update input field value
+ // so user can grab diagram if they want
+ this.input.value = JSON.stringify(netlist);
+
+ // create a circuit from the netlist
+ var ckt = new cktsim.Circuit();
+ if (ckt.load_netlist(netlist))
+ return ckt;
+ else
+ return null;
+ }
+
+ Schematic.prototype.dc_analysis = function() {
+ // remove any previous annotations
+ this.unselect_all(-1);
+ this.redraw_background();
+
+ var ckt = this.extract_circuit();
+ if (ckt === null) return;
+
+ // run the analysis
+ this.operating_point = ckt.dc();
+
+ if (this.operating_point != undefined) {
+ // save a copy of the results for submission
+ this.dc_results = {};
+ for (var i in this.operating_point) this.dc_results[i] = this.operating_point[i];
+
+ // display results on diagram
+ this.redraw();
+ }
+ }
+
+ // return a list of [color,node_label,offset,type] for each probe in the diagram
+ // type == 'voltage' or 'current'
+ Schematic.prototype.find_probes = function() {
+ var result = [];
+ var result = [];
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var c = this.components[i];
+ var info = c.probe_info();
+ if (info != undefined) result.push(c.probe_info());
+ }
+ return result;
+ }
+
+ // use a dialog to get AC analysis parameters
+ Schematic.prototype.setup_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)';
+ var source_name_lbl = 'Name of V or I source for ac'
+
+ if (this.find_probes().length == 0) {
+ alert("AC Analysis: there are no voltage 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);
+ fields[source_name_lbl] = build_input('text',10,this.ac_source_name);
+
+ var content = build_table(fields);
+ content.fields = fields;
+ content.sch = this;
+
+ this.dialog('AC Analysis',content,function(content) {
+ var sch = content.sch;
+
+ // 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;
+ sch.ac_source_name = content.fields[source_name_lbl].value;
+
+ sch.ac_analysis(cktsim.parse_number(sch.ac_npts),
+ cktsim.parse_number(sch.ac_fstart),
+ cktsim.parse_number(sch.ac_fstop),
+ sch.ac_source_name);
+ });
+ }
+
+ // perform ac analysis
+ Schematic.prototype.ac_analysis = function(npts,fstart,fstop,ac_source_name) {
+ // run the analysis
+ var ckt = this.extract_circuit();
+ if (ckt === null) return;
+ var results = ckt.ac(npts,fstart,fstop,ac_source_name);
+
+ if (typeof results == 'string')
+ this.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;
+
+
+ if (this.submit_analyses != undefined) {
+ var submit = this.submit_analyses['ac'];
+ if (submit != undefined) {
+ // save a copy of the results for submission
+ this.ac_results = {};
+
+ // save requested values for each requested node
+ for (var j = 0; j < submit.length; j++) {
+ var flist = submit[j]; // [node_name,f1,f2,...]
+ var node = flist[0];
+ var values = results[node];
+ var fvlist = [];
+ // for each requested freq, interpolate response value
+ for (var k = 1; k < flist.length; k++) {
+ var f = flist[k];
+ var v = interpolate(f,x_values,values);
+ // convert to dB
+ fvlist.push([f,v == undefined ? 'undefined' : 20.0 * Math.log(v)/Math.LN10]);
+ }
+ // save results as list of [f,response] paris
+ this.ac_results[node] = fvlist;
+ }
+ }
+ }
+
+ // set up plot values for each node with a probe
+ var y_values = []; // list of [color, result_array]
+ var z_values = []; // list of [color, result_array]
+ var probes = this.find_probes();
+
+ var probe_maxv = [];
+ var probe_color = [];
+
+ // Check for probe with near zero transfer function and warn
+ for (var i = probes.length - 1; i >= 0; --i) {
+ if (probes[i][3] != 'voltage') continue;
+ probe_color[i] = probes[i][0];
+ var label = probes[i][1];
+ var v = results[label];
+ probe_maxv[i] = array_max(v); // magnitudes always > 0
+ }
+ var all_max = array_max(probe_maxv);
+
+ if (all_max < 1.0e-16) {
+ alert('Zero ac response, -infinity on DB scale.');
+ } else {
+ for (var i = probes.length - 1; i >= 0; --i) {
+ if (probes[i][3] != 'voltage') continue;
+ if ((probe_maxv[i] / all_max) < 1.0e-10) {
+ alert('Near zero ac response, remove ' + probe_color[i] + ' probe');
+ return;
+ }
+ }
+ }
+
+ for (var i = probes.length - 1; i >= 0; --i) {
+ if (probes[i][3] != 'voltage') continue;
+ var color = probes[i][0];
+ var label = probes[i][1];
+ var offset = cktsim.parse_number(probes[i][2]);
+
+ var v = results[label];
+ // convert values into dB relative to source amplitude
+ var v_max = 1;
+ 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,offset,v]);
+
+ var v = results[label+'_phase'];
+ z_values.push([color,0,v]);
+ }
+
+ // graph the result and display in a window
+ var graph2 = this.graph(x_values,'log(Frequency in Hz)',z_values,'degrees');
+ this.window('AC Analysis - Phase',graph2);
+ var graph1 = this.graph(x_values,'log(Frequency in Hz)',y_values,'dB');
+ this.window('AC Analysis - Magnitude',graph1,50);
+ }
+ }
+
+ Schematic.prototype.transient_analysis = function() {
+ this.unselect_all(-1);
+ this.redraw_background();
+
+ var npts_lbl = 'Minimum number of timepoints';
+ var tstop_lbl = 'Stop Time (seconds)';
+
+ var probes = this.find_probes();
+ if (probes.length == 0) {
+ alert("Transient Analysis: there are no probes in the diagram!");
+ return;
+ }
+
+ var fields = new Array();
+ //fields[npts_lbl] = build_input('text',10,this.tran_npts);
+ fields[tstop_lbl] = build_input('text',10,this.tran_tstop);
+
+ var content = build_table(fields);
+ content.fields = fields;
+ content.sch = this;
+
+ this.dialog('Transient Analysis',content,function(content) {
+ var sch = content.sch;
+ var ckt = sch.extract_circuit();
+ if (ckt === null) return;
+
+ // retrieve parameters, remember for next time
+ //sch.tran_npts = content.fields[npts_lbl].value;
+ sch.tran_tstop = content.fields[tstop_lbl].value;
+
+ // gather a list of nodes that are being probed. These
+ // will be added to the list of nodes checked during the
+ // LTE calculations in transient analysis
+ var probe_list = sch.find_probes();
+ var probe_names = new Array(probe_list.length);
+ for (var i = probe_list.length - 1; i >= 0; --i)
+ probe_names[i] = probe_list[i][1];
+
+ // run the analysis
+ var results = ckt.tran(ckt.parse_number(sch.tran_npts), 0,
+ ckt.parse_number(sch.tran_tstop), probe_names, false);
+
+ if (typeof results == 'string')
+ sch.message(results);
+ else {
+ if (sch.submit_analyses != undefined) {
+ var submit = sch.submit_analyses['tran'];
+ if (submit != undefined) {
+ // save a copy of the results for submission
+ sch.transient_results = {};
+ var times = results['_time_'];
+
+ // save requested values for each requested node
+ for (var j = 0; j < submit.length; j++) {
+ var tlist = submit[j]; // [node_name,t1,t2,...]
+ var node = tlist[0];
+ var values = results[node];
+ var tvlist = [];
+ // for each requested time, interpolate waveform value
+ for (var k = 1; k < tlist.length; k++) {
+ var t = tlist[k];
+ var v = interpolate(t,times,values);
+ tvlist.push([t,v == undefined ? 'undefined' : v]);
+ }
+ // save results as list of [t,value] pairs
+ sch.transient_results[node] = tvlist;
+ }
+ }
+ }
+
+ var x_values = results['_time_'];
+ var x_legend = 'Time';
+
+ // set up plot values for each node with a probe
+ var v_values = []; // voltage values: list of [color, result_array]
+ var i_values = []; // current 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 offset = cktsim.parse_number(probes[i][2]);
+ var v = results[label];
+ if (v == undefined) {
+ alert('The ' + color + ' probe is connected to node ' + '"' + label + '"' + ' which is not an actual circuit node');
+ } else if (probes[i][3] == 'voltage') {
+ if (color == 'x-axis') {
+ x_values = v;
+ x_legend = 'Voltage';
+ } else v_values.push([color,offset,v]);
+ } else {
+ if (color == 'x-axis') {
+ x_values = v;
+ x_legend = 'Current';
+ } else i_values.push([color,offset,v]);
+ }
+ }
+
+ // graph the result and display in a window
+ var graph = sch.graph(x_values,x_legend,v_values,'Voltage',i_values,'Current');
+ sch.window('Results of Transient Analysis',graph);
+ }
+ })
+ }
+
+ // t is the time at which we want a value
+ // times is a list of timepoints from the simulation
+ function interpolate(t,times,values) {
+ if (values == undefined) return undefined;
+
+ for (var i = 0; i < times.length; i++)
+ if (t < times[i]) {
+ // t falls between times[i-1] and times[i]
+ var t1 = (i == 0) ? times[0] : times[i-1];
+ var t2 = times[i];
+
+ if (t2 == undefined) return undefined;
+
+ var v1 = (i == 0) ? values[0] : values[i-1];
+ var v2 = values[i];
+ var v = v1;
+ if (t != t1) v += (t - t1)*(v2 - v1)/(t2 - t1);
+ return v;
+ }
+ }
+
+ // external interface for setting the property value of a named component
+ Schematic.prototype.set_property = function(component_name,property,value) {
+ this.unselect_all(-1);
+
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var component = this.components[i];
+ if (component.properties['name'] == component_name) {
+ component.properties[property] = value.toString();
+ break;
+ }
+ }
+
+ // update diagram
+ this.redraw_background();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Drawing support -- deals with scaling and scrolling of diagrama
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // here to redraw background image containing static portions of the schematic.
+ // Also redraws dynamic portion.
+ Schematic.prototype.redraw_background = function() {
+ var c = this.bg_image.getContext('2d');
+
+ c.lineCap = 'round';
+
+ // paint background color
+ c.fillStyle = element_style;
+ c.fillRect(0,0,this.width,this.height);
+
+ if (!this.diagram_only && this.show_grid) {
+ // grid
+ c.strokeStyle = grid_style;
+ var first_x = this.origin_x;
+ var last_x = first_x + this.width/this.scale;
+ var first_y = this.origin_y;
+ var last_y = first_y + this.height/this.scale;
+
+ for (var i = this.grid*Math.ceil(first_x/this.grid); i < last_x; i += this.grid)
+ this.draw_line(c,i,first_y,i,last_y,0.1);
+
+ for (var i = this.grid*Math.ceil(first_y/this.grid); i < last_y; i += this.grid)
+ this.draw_line(c,first_x,i,last_x,i,0.1);
+ }
+
+ // unselected components
+ var min_x = Infinity; // compute bounding box for diagram
+ var max_x = -Infinity;
+ var min_y = Infinity;
+ var max_y = -Infinity;
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var component = this.components[i];
+ if (!component.selected) {
+ component.draw(c);
+ min_x = Math.min(component.bbox[0],min_x);
+ max_x = Math.max(component.bbox[2],max_x);
+ min_y = Math.min(component.bbox[1],min_y);
+ max_y = Math.max(component.bbox[3],max_y);
+ }
+ }
+ this.unsel_bbox = [min_x,min_y,max_x,max_y];
+
+ this.redraw(); // background changed, redraw on screen
+ }
+
+ // redraw what user sees = static image + dynamic parts
+ Schematic.prototype.redraw = function() {
+ var c = this.canvas.getContext('2d');
+
+ // put static image in the background
+ c.drawImage(this.bg_image, 0, 0);
+
+ // selected components
+ var min_x = this.unsel_bbox[0]; // compute bounding box for diagram
+ var max_x = this.unsel_bbox[2];
+ var min_y = this.unsel_bbox[1];
+ var max_y = this.unsel_bbox[3];
+ var selections = false;
+ for (var i = this.components.length - 1; i >= 0; --i) {
+ var component = this.components[i];
+ if (component.selected) {
+ component.draw(c);
+ selections = true;
+ min_x = Math.min(component.bbox[0],min_x);
+ max_x = Math.max(component.bbox[2],max_x);
+ min_y = Math.min(component.bbox[1],min_y);
+ max_y = Math.max(component.bbox[3],max_y);
+ }
+ }
+ if (min_x == Infinity) this.bbox = [0,0,0,0];
+ else this.bbox = [min_x,min_y,max_x,max_y];
+ this.enable_tool('cut',selections);
+ this.enable_tool('copy',selections);
+ this.enable_tool('paste',sch_clipboard.length > 0);
+
+ // connection points: draw one at each location
+ for (var location in this.connection_points) {
+ var cplist = this.connection_points[location];
+ cplist[0].draw(c,cplist.length);
+ }
+
+ // draw new wire
+ if (this.wire) {
+ var r = this.wire;
+ c.strokeStyle = selected_style;
+ this.draw_line(c,r[0],r[1],r[2],r[3],1);
+ }
+
+ // draw selection rectangle
+ if (this.select_rect) {
+ var r = this.select_rect;
+ c.lineWidth = 1;
+ c.strokeStyle = selected_style;
+ c.beginPath();
+ c.moveTo(r[0],r[1]);
+ c.lineTo(r[0],r[3]);
+ c.lineTo(r[2],r[3]);
+ c.lineTo(r[2],r[1]);
+ c.lineTo(r[0],r[1]);
+ c.stroke();
+ }
+
+ // display operating point results
+ if (this.operating_point) {
+ 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);
+
+ // let components display branch current info if available
+ for (var i = this.components.length - 1; i >= 0; --i)
+ this.components[i].display_current(c,temp)
+ }
+ }
+
+ // add scrolling/zooming control
+ if (!this.diagram_only) {
+ var r = this.sctl_r;
+ var x = this.sctl_x;
+ var y = this.sctl_y;
+
+ // circle with border
+ c.fillStyle = element_style;
+ c.beginPath();
+ c.arc(x,y,r,0,2*Math.PI);
+ c.fill();
+
+ c.strokeStyle = grid_style;
+ c.lineWidth = 0.5;
+ c.beginPath();
+ c.arc(x,y,r,0,2*Math.PI);
+ c.stroke();
+
+ // direction markers for scroll
+ c.lineWidth = 3;
+ c.beginPath();
+
+ c.moveTo(x + 4,y - r + 8); // north
+ c.lineTo(x,y - r + 4);
+ c.lineTo(x - 4,y - r + 8);
+
+ c.moveTo(x + r - 8,y + 4); // east
+ c.lineTo(x + r - 4,y);
+ c.lineTo(x + r - 8,y - 4);
+
+ c.moveTo(x + 4,y + r - 8); // south
+ c.lineTo(x,y + r - 4);
+ c.lineTo(x - 4,y + r - 8);
+
+ c.moveTo(x - r + 8,y + 4); // west
+ c.lineTo(x - r + 4,y);
+ c.lineTo(x - r + 8,y - 4);
+
+ c.stroke();
+
+ // zoom control
+ x = this.zctl_left;
+ y = this.zctl_top;
+ c.lineWidth = 0.5;
+ c.fillStyle = element_style; // background
+ c.fillRect(x,y,16,48);
+ c.strokeStyle = grid_style; // border
+ c.strokeRect(x,y,16,48);
+ c.lineWidth = 1.0;
+ c.beginPath();
+ // zoom in label
+ c.moveTo(x+4,y+8); c.lineTo(x+12,y+8); c.moveTo(x+8,y+4); c.lineTo(x+8,y+12);
+ // zoom out label
+ c.moveTo(x+4,y+24); c.lineTo(x+12,y+24);
+ // surround label
+ c.strokeRect(x+4,y+36,8,8);
+ c.stroke();
+ }
+ }
+
+ // draws a cross cursor
+ Schematic.prototype.cross_cursor = function(c,x,y) {
+ this.draw_line(c,x-this.grid,y,x+this.grid,y,1);
+ 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();
+ c.moveTo((x1 - this.origin_x) * this.scale,(y1 - this.origin_y) * this.scale);
+ c.lineTo((x2 - this.origin_x) * this.scale,(y2 - this.origin_y) * this.scale);
+ c.stroke();
+ }
+
+ Schematic.prototype.draw_arc = function(c,x,y,radius,start_radians,end_radians,anticlockwise,width,filled) {
+ c.lineWidth = width*this.scale;
+ c.beginPath();
+ c.arc((x - this.origin_x)*this.scale,(y - this.origin_y)*this.scale,radius*this.scale,
+ start_radians,end_radians,anticlockwise);
+ if (filled) c.fill();
+ else c.stroke();
+ }
+
+ Schematic.prototype.draw_text = function(c,text,x,y,size) {
+ c.font = size*this.scale+'pt sans-serif'
+ c.fillText(text,(x - this.origin_x) * this.scale,(y - this.origin_y) * this.scale);
+ }
+
+ // add method to canvas to compute relative coords for event
+ HTMLCanvasElement.prototype.relMouseCoords = function(event){
+ // run up the DOM tree to figure out coords for top,left of canvas
+ var totalOffsetX = 0;
+ var totalOffsetY = 0;
+ var currentElement = this;
+ do {
+ totalOffsetX += currentElement.offsetLeft;
+ totalOffsetY += currentElement.offsetTop;
+ }
+ while (currentElement = currentElement.offsetParent);
+
+ // now compute relative position of click within the canvas
+ this.mouse_x = event.pageX - totalOffsetX;
+ this.mouse_y = event.pageY - totalOffsetY;
+
+ this.page_x = event.pageX;
+ this.page_y = event.pageY;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Event handling
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // process keystrokes, consuming those that are meaningful to us
+ function schematic_key_down(event) {
+ if (!event) event = window.event;
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+ var code = event.keyCode;
+
+ // keep track of modifier key state
+ if (code == 16) sch.shiftKey = true;
+ else if (code == 17) sch.ctrlKey = true;
+ else if (code == 18) sch.altKey = true;
+ else if (code == 91) sch.cmdKey = true;
+
+ // backspace or delete: delete selected components
+ else if (code == 8 || code == 46) {
+ // delete selected components
+ for (var i = sch.components.length - 1; i >= 0; --i) {
+ var component = sch.components[i];
+ if (component.selected) component.remove();
+ }
+ sch.clean_up_wires();
+ sch.redraw_background();
+ event.preventDefault();
+ return false;
+ }
+
+ // cmd/ctrl x: cut
+ else if ((sch.ctrlKey || sch.cmdKey) && code == 88) {
+ sch.cut();
+ event.preventDefault();
+ return false;
+ }
+
+ // cmd/ctrl c: copy
+ else if ((sch.ctrlKey || sch.cmdKey) && code == 67) {
+ sch.copy();
+ event.preventDefault();
+ return false;
+ }
+
+ // cmd/ctrl v: paste
+ else if ((sch.ctrlKey || sch.cmdKey) && code == 86) {
+ sch.paste();
+ event.preventDefault();
+ return false;
+ }
+
+ // 'r': rotate component
+ else if (!sch.ctrlKey && !sch.altKey && !sch.cmdKey && code == 82) {
+ // rotate
+ for (var i = sch.components.length - 1; i >= 0; --i) {
+ var component = sch.components[i];
+ if (component.selected) {
+ component.rotate(1);
+ sch.check_wires(component);
+ }
+ }
+ sch.clean_up_wires();
+ sch.redraw_background();
+ event.preventDefault();
+ return false;
+ }
+
+ else return true;
+
+ // consume keystroke
+ sch.redraw();
+ event.preventDefault();
+ return false;
+ }
+
+ function schematic_key_up(event) {
+ if (!event) event = window.event;
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+ var code = event.keyCode;
+
+ if (code == 16) sch.shiftKey = false;
+ else if (code == 17) sch.ctrlKey = false;
+ else if (code == 18) sch.altKey = false;
+ else if (code == 91) sch.cmdKey = false;
+ }
+
+ function schematic_mouse_enter(event) {
+ if (!event) event = window.event;
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ // see if user has selected a new part
+ if (sch.new_part) {
+ // grab incoming part, turn off selection of parts bin
+ var part = sch.new_part;
+ sch.new_part = undefined;
+ part.select(false);
+
+ // unselect everything else in the schematic, add part and select it
+ sch.unselect_all(-1);
+ sch.redraw_background(); // so we see any components that got unselected
+
+ // make a clone of the component in the parts bin
+ part = part.component.clone(sch.cursor_x,sch.cursor_y);
+ part.add(sch); // add it to schematic
+ part.set_select(true);
+
+ // and start dragging it
+ sch.drag_begin();
+ }
+
+ sch.drawCursor = true;
+ sch.redraw();
+ sch.canvas.focus(); // capture key strokes
+ return false;
+ }
+
+ function schematic_mouse_leave(event) {
+ if (!event) event = window.event;
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+ sch.drawCursor = false;
+ sch.redraw();
+ return false;
+ }
+
+ function schematic_mouse_down(event) {
+ if (!event) event = window.event;
+ else event.preventDefault();
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ // determine where event happened in schematic coordinates
+ sch.canvas.relMouseCoords(event);
+
+ var mx = sch.canvas.mouse_x;
+ var my = sch.canvas.mouse_y;
+ var sx = mx - sch.sctl_x;
+ var sy = my - sch.sctl_y;
+ var zx = mx - sch.zctl_left;
+ var zy = my - sch.zctl_top;
+ if (sx*sx + sy*sy <= sch.sctl_r*sch.sctl_r) { // click in scrolling control
+ // click on scrolling control, check which quadrant
+ if (Math.abs(sy) > Math.abs(sx)) { // N or S
+ var delta = this.height / 8;
+ if (sy > 0) delta = -delta;
+ var temp = sch.origin_y - delta;
+ if (temp > origin_min*sch.grid && temp < origin_max*sch.grid) sch.origin_y = temp;
+ } else { // E or W
+ var delta = this.width / 8;
+ if (sx < 0) delta = -delta;
+ var temp = sch.origin_x + delta;
+ if (temp > origin_min*sch.grid && temp < origin_max*sch.grid) sch.origin_x = temp;
+ }
+ } else if (zx >= 0 && zx < 16 && zy >= 0 && zy < 48) { // click in zoom control
+ if (zy < 16) sch.zoomin();
+ else if (zy < 32) sch.zoomout();
+ else sch.zoomall();
+ } else {
+ var x = mx/sch.scale + sch.origin_x;
+ var y = my/sch.scale + sch.origin_y;
+ sch.cursor_x = Math.round(x/sch.grid) * sch.grid;
+ sch.cursor_y = Math.round(y/sch.grid) * sch.grid;
+
+ // is mouse over a connection point? If so, start dragging a wire
+ var cplist = sch.connection_points[sch.cursor_x + ',' + sch.cursor_y];
+ if (cplist && !event.shiftKey) {
+ sch.unselect_all(-1);
+ sch.wire = [sch.cursor_x,sch.cursor_y,sch.cursor_x,sch.cursor_y];
+ } else {
+ // give all components a shot at processing the selection event
+ var which = -1;
+ for (var i = sch.components.length - 1; i >= 0; --i)
+ if (sch.components[i].select(x,y,event.shiftKey)) {
+ if (sch.components[i].selected) {
+ sch.drag_begin();
+ which = i; // keep track of component we found
+ }
+ break;
+ }
+ // did we just click on a previously selected component?
+ var reselect = which!=-1 && sch.components[which].was_previously_selected;
+
+ if (!event.shiftKey) {
+ // if shift key isn't pressed and we didn't click on component
+ // that was already selected, unselect everyone except component
+ // we just clicked on
+ if (!reselect) sch.unselect_all(which);
+
+ // if there's nothing to drag, set up a selection rectangle
+ if (!sch.dragging) sch.select_rect = [sch.canvas.mouse_x,sch.canvas.mouse_y,
+ sch.canvas.mouse_x,sch.canvas.mouse_y];
+ }
+ }
+ }
+
+ sch.redraw_background();
+ return false;
+ }
+
+ function schematic_mouse_move(event) {
+ if (!event) event = window.event;
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ sch.canvas.relMouseCoords(event);
+ var x = sch.canvas.mouse_x/sch.scale + sch.origin_x;
+ var y = sch.canvas.mouse_y/sch.scale + sch.origin_y;
+ sch.cursor_x = Math.round(x/sch.grid) * sch.grid;
+ sch.cursor_y = Math.round(y/sch.grid) * sch.grid;
+
+ if (sch.wire) {
+ // update new wire end point
+ sch.wire[2] = sch.cursor_x;
+ sch.wire[3] = sch.cursor_y;
+ } else if (sch.dragging) {
+ // see how far we moved
+ var dx = sch.cursor_x - sch.drag_x;
+ var dy = sch.cursor_y - sch.drag_y;
+ if (dx != 0 || dy != 0) {
+ // update position for next time
+ sch.drag_x = sch.cursor_x;
+ sch.drag_y = sch.cursor_y;
+
+ // give all components a shot at processing the event
+ for (var i = sch.components.length - 1; i >= 0; --i) {
+ var component = sch.components[i];
+ if (component.selected) component.move(dx,dy);
+ }
+ }
+ } else if (sch.select_rect) {
+ // update moving corner of selection rectangle
+ sch.select_rect[2] = sch.canvas.mouse_x;
+ sch.select_rect[3] = sch.canvas.mouse_y;
+ //sch.message(sch.select_rect.toString());
+ }
+
+ // just redraw dynamic components
+ sch.redraw();
+ //sch.message(sch.canvas.page_x + ',' + sch.canvas.page_y + ';' + sch.canvas.mouse_x + ',' + sch.canvas.mouse_y + ';' + sch.cursor_x + ',' + sch.cursor_y);
+
+ return false;
+ }
+
+ function schematic_mouse_up(event) {
+ if (!event) event = window.event;
+ else event.preventDefault();
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ // drawing a new wire
+ if (sch.wire) {
+ var r = sch.wire;
+ sch.wire = undefined;
+
+ if (r[0]!=r[2] || r[1]!=r[3]) {
+ // insert wire component
+ sch.add_wire(r[0],r[1],r[2],r[3]);
+ sch.clean_up_wires();
+ sch.redraw_background();
+ } else sch.redraw();
+ }
+
+ // dragging
+ if (sch.dragging) sch.drag_end();
+
+ // selection rectangle
+ if (sch.select_rect) {
+ var r = sch.select_rect;
+
+ // if select_rect is a point, we've already dealt with selection
+ // in mouse_down handler
+ if (r[0]!=r[2] || r[1]!=r[3]) {
+ // convert to schematic coordinates
+ var s = [r[0]/sch.scale + sch.origin_x, r[1]/sch.scale + sch.origin_y,
+ r[2]/sch.scale + sch.origin_x, r[3]/sch.scale + sch.origin_y];
+ canonicalize(s);
+
+ if (!event.shiftKey) sch.unselect_all();
+
+ // select components that intersect selection rectangle
+ for (var i = sch.components.length - 1; i >= 0; --i)
+ sch.components[i].select_rect(s,event.shiftKey);
+ }
+
+ sch.select_rect = undefined;
+ sch.redraw_background();
+ }
+ return false;
+ }
+
+ function schematic_mouse_wheel(event) {
+ if (!event) event = window.event;
+ else event.preventDefault();
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ var delta = 0;
+ if (event.wheelDelta) delta = event.wheelDelta;
+ else if (event.detail) delta = -event.detail;
+
+ if (delta) {
+ var nscale = (delta > 0) ? sch.scale*zoom_factor : sch.scale/zoom_factor;
+
+ if (nscale > zoom_min && nscale < zoom_max) {
+ // zoom around current mouse position
+ sch.canvas.relMouseCoords(event);
+ var s = 1.0/sch.scale - 1.0/nscale;
+ sch.origin_x += sch.canvas.mouse_x*s;
+ sch.origin_y += sch.canvas.mouse_y*s;
+ sch.scale = nscale;
+ sch.redraw_background();
+ }
+ }
+ }
+
+ function schematic_double_click(event) {
+ if (!event) event = window.event;
+ else event.preventDefault();
+ var sch = (window.event) ? event.srcElement.schematic : event.target.schematic;
+
+ // determine where event happened in schematic coordinates
+ sch.canvas.relMouseCoords(event);
+ var x = sch.canvas.mouse_x/sch.scale + sch.origin_x;
+ var y = sch.canvas.mouse_y/sch.scale + sch.origin_y;
+ sch.cursor_x = Math.round(x/sch.grid) * sch.grid;
+ sch.cursor_y = Math.round(y/sch.grid) * sch.grid;
+
+ // see if we double-clicked a component. If so, edit it's properties
+ for (var i = sch.components.length - 1; i >= 0; --i)
+ if (sch.components[i].edit_properties(x,y))
+ break;
+
+ return false;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Status message and dialogs
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ Schematic.prototype.message = function(message) {
+ this.status.nodeValue = message;
+ }
+
+ Schematic.prototype.append_message = function(message) {
+ this.status.nodeValue += ' / '+message;
+ }
+
+ // set up a dialog with specified title, content and two buttons at
+ // the bottom: OK and Cancel. If Cancel is clicked, dialog goes away
+ // and we're done. If OK is clicked, dialog goes away and the
+ // callback function is called with the content as an argument (so
+ // that the values of any fields can be captured).
+ Schematic.prototype.dialog = function(title,content,callback) {
+ // create the div for the top level of the dialog, add to DOM
+ var dialog = document.createElement('div');
+ dialog.sch = this;
+ dialog.content = content;
+ dialog.callback = callback;
+
+ // look for property input fields in the content and give
+ // them a keypress listener that interprets ENTER as
+ // clicking OK.
+ var plist = content.getElementsByClassName('property');
+ for (var i = plist.length - 1; i >= 0; --i) {
+ var field = plist[i];
+ field.dialog = dialog; // help event handler find us...
+ field.addEventListener('keypress',dialog_check_for_ENTER,false);
+ }
+
+ // div to hold the content
+ var body = document.createElement('div');
+ content.style.marginBotton = '5px';
+ body.appendChild(content);
+ body.style.padding = '5px';
+ dialog.appendChild(body);
+
+ // OK button
+ var ok_button = document.createElement('span');
+ ok_button.appendChild(document.createTextNode('OK'));
+ ok_button.dialog = dialog; // for the handler to use
+ ok_button.addEventListener('click',dialog_okay,false);
+ ok_button.style.display = 'inline';
+ ok_button.style.border = '1px solid';
+ ok_button.style.padding = '5px';
+ ok_button.style.margin = '10px';
+
+ // cancel button
+ var cancel_button = document.createElement('span');
+ cancel_button.appendChild(document.createTextNode('Cancel'));
+ cancel_button.dialog = dialog; // for the handler to use
+ cancel_button.addEventListener('click',dialog_cancel,false);
+ cancel_button.style.display = 'inline';
+ cancel_button.style.border = '1px solid';
+ cancel_button.style.padding = '5px';
+ cancel_button.style.margin = '10px';
+
+ // div to hold the two buttons
+ var buttons = document.createElement('div');
+ buttons.style.textAlign = 'center';
+ buttons.appendChild(ok_button);
+ buttons.appendChild(cancel_button);
+ buttons.style.padding = '5px';
+ buttons.style.margin = '10px';
+ dialog.appendChild(buttons);
+
+ // put into an overlay window
+ this.window(title,dialog);
+ }
+
+ // callback when user click "Cancel" in a dialog
+ function dialog_cancel(event) {
+ if (!event) event = window.event;
+ var dialog = (window.event) ? event.srcElement.dialog : event.target.dialog;
+
+ window_close(dialog.win);
+ }
+
+ // callback when user click "OK" in a dialog
+ function dialog_okay(event) {
+ if (!event) event = window.event;
+ var dialog = (window.event) ? event.srcElement.dialog : event.target.dialog;
+
+ window_close(dialog.win);
+
+ // invoke the callback with the dialog contents as the argument
+ if (dialog.callback) dialog.callback(dialog.content);
+ }
+
+ // callback for keypress in input fields: if user typed ENTER, act
+ // like they clicked OK button.
+ function dialog_check_for_ENTER(event) {
+ var key = (window.event) ? window.event.keyCode : event.keyCode;
+ if (key == 13) dialog_okay(event);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Draggable, resizeable, closeable window
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // 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;
+ input.className = 'property'; // make this easier to find later
+ 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,offset) {
+ // create the div for the top level of the window
+ var win = document.createElement('div');
+ win.sch = this;
+ win.content = content;
+ win.drag_x = undefined;
+ win.draw_y = undefined;
+
+ // div to hold the title
+ var head = document.createElement('div');
+ head.style.backgroundColor = 'black';
+ head.style.color = 'white';
+ head.style.textAlign = 'center';
+ head.style.padding = '5px';
+ head.appendChild(document.createTextNode(title));
+ head.win = win;
+ win.head = head;
+
+ var close_button = new Image();
+ close_button.src = close_icon;
+ close_button.style.cssFloat = 'right';
+ close_button.addEventListener('click',window_close_button,false);
+ close_button.win = win;
+ head.appendChild(close_button);
+
+ win.appendChild(head);
+
+ // capture mouse events in title bar
+ head.addEventListener('mousedown',window_mouse_down,false);
+
+ // div to hold the content
+ //var body = document.createElement('div');
+ //body.appendChild(content);
+ win.appendChild(content);
+ content.win = win; // so content can contact us
+
+ // compute location relative to canvas
+ if (offset == undefined) offset = 0;
+ win.left = this.canvas.mouse_x + offset;
+ win.top = this.canvas.mouse_y + offset;
+
+ // add to DOM
+ win.style.background = 'white';
+ //win.style.zIndex = '1000';
+ win.style.position = 'absolute';
+ win.style.left = win.left + 'px';
+ win.style.top = win.top + 'px';
+ win.style.border = '2px solid';
+
+ this.canvas.parentNode.insertBefore(win,this.canvas);
+ bring_to_front(win,true);
+ }
+
+ // adjust zIndex of pop-up window so that it is in front
+ function bring_to_front(win,insert) {
+ var wlist = win.sch.window_list;
+ var i = wlist.indexOf(win);
+
+ // remove from current position (if any) in window list
+ if (i != -1) wlist.splice(i,1);
+
+ // if requested, add to end of window list
+ if (insert) wlist.push(win);
+
+ // adjust all zIndex values
+ for (i = 0; i < wlist.length; i += 1)
+ wlist[i].style.zIndex = 1000 + i;
+ }
+
+ // close the window
+ function window_close(win) {
+ // remove the window from the top-level div of the schematic
+ win.parentNode.removeChild(win);
+
+ // remove from list of pop-up windows
+ bring_to_front(win,false);
+ }
+
+ function window_close_button(event) {
+ if (!event) event = window.event;
+ var src = (window.event) ? event.srcElement : event.target;
+ window_close(src.win);
+ }
+
+ // capture mouse events in title bar of window
+ function window_mouse_down(event) {
+ if (!event) event = window.event;
+ var src = (window.event) ? event.srcElement : event.target;
+ var win = src.win;
+
+ bring_to_front(win,true);
+
+ // add handlers to document so we capture them no matter what
+ document.addEventListener('mousemove',window_mouse_move,false);
+ document.addEventListener('mouseup',window_mouse_up,false);
+ document.tracking_window = win;
+
+ // remember where mouse is so we can compute dx,dy during drag
+ win.drag_x = event.pageX;
+ win.drag_y = event.pageY;
+
+ return false;
+ }
+
+ function window_mouse_up(event) {
+ var win = document.tracking_window;
+
+ // show's over folks...
+ document.removeEventListener('mousemove',window_mouse_move,false);
+ document.removeEventListener('mouseup',window_mouse_up,false);
+ document.tracking_window = undefined;
+ win.drag_x = undefined;
+ win.drag_y = undefined;
+ return true; // consume event
+ }
+
+ function window_mouse_move(event) {
+ var win = document.tracking_window;
+
+ if (win.drag_x) {
+ var dx = event.pageX - win.drag_x;
+ var dy = event.pageY - win.drag_y;
+
+ // move the window
+ win.left += dx;
+ win.top += dy;
+ win.style.left = win.left + 'px';
+ win.style.top = win.top + 'px';
+
+ // update reference point
+ win.drag_x += dx;
+ win.drag_y += dy;
+
+ return true; // consume event
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Toolbar
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ Schematic.prototype.add_tool = function(icon,tip,callback) {
+ var tool;
+ if (icon.search('data:image') != -1) {
+ tool = document.createElement('img');
+ tool.src = icon;
+ } else {
+ tool = document.createElement('span');
+ tool.style.font = 'small-caps small sans-serif';
+ var label = document.createTextNode(icon);
+ tool.appendChild(label);
+ }
+
+ // decorate tool
+ tool.style.borderWidth = '1px';
+ tool.style.borderStyle = 'solid';
+ tool.style.borderColor = background_style;
+ tool.style.padding = '2px';
+ tool.style.verticalAlign = 'middle';
+ tool.style.cursor = 'default';
+
+ // set up event processing
+ tool.addEventListener('mouseover',tool_enter,false);
+ tool.addEventListener('mouseout',tool_leave,false);
+ tool.addEventListener('click',tool_click,false);
+
+ // add to toolbar
+ tool.sch = this;
+ tool.tip = tip;
+ tool.callback = callback;
+ this.toolbar.push(tool);
+
+ tool.enabled = false;
+ tool.style.opacity = 0.2;
+
+ return tool;
+ }
+
+ Schematic.prototype.enable_tool = function(tname,which) {
+ var tool = this.tools[tname];
+
+ 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('');
+ }
+ }
+ }
+
+ // highlight tool button by turning on border, changing background
+ function tool_enter(event) {
+ if (!event) event = window.event;
+ var tool = (window.event) ? event.srcElement : event.target;
+
+ if (tool.enabled) {
+ tool.style.borderColor = normal_style;
+ tool.sch.message(tool.tip);
+ tool.opacity = 1.0;
+ }
+ }
+
+ // unhighlight tool button by turning off border, reverting to normal background
+ function tool_leave(event) {
+ if (!event) event = window.event;
+ var tool = (window.event) ? event.srcElement : event.target;
+
+ if (tool.enabled) {
+ tool.style.borderColor = background_style;
+ tool.sch.message('');
+ }
+ }
+
+ // handle click on a tool
+ function tool_click(event) {
+ if (!event) event = window.event;
+ var tool = (window.event) ? event.srcElement : event.target;
+
+ if (tool.enabled) {
+ tool.sch.canvas.relMouseCoords(event); // so we can position pop-up window correctly
+ tool.callback.call(tool.sch);
+ }
+ }
+
+ help_icon = 'data:image/gif;base64,R0lGODlhEAAQAJEAAAAAAP///wAAAAAAACH5BAkAAAIAIf8LSUNDUkdCRzEwMTL/AAAHqGFwcGwCIAAAbW50clJHQiBYWVogB9kAAgAZAAsAGgALYWNzcEFQUEwAAAAAYXBwbAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALZGVzYwAAAQgAAABvZHNjbQAAAXgAAAVsY3BydAAABuQAAAA4d3RwdAAABxwAAAAUclhZWgAABzAAAAAUZ1hZWgAAB0QAAAAUYlhZWgAAB1gAAAAUclRSQwAAB2wAAAAOY2hhZAAAB3wAAAAsYlRSQwAAB2wAAAAOZ1RS/0MAAAdsAAAADmRlc2MAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAABRHZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAB4AAAAMc2tTSwAAACgAAAF4aHJIUgAAACgAAAGgY2FFUwAAACQAAAHIcHRCUgAAACYAAAHsdWtVQQAAACoAAAISZnJGVQAAACgAAAI8emhUVwAAABYAAAJkaXRJVAAAACgAAAJ6bmJOTwAAACYAAAKia29LUgAAABYAAP8CyGNzQ1oAAAAiAAAC3mhlSUwAAAAeAAADAGRlREUAAAAsAAADHmh1SFUAAAAoAAADSnN2U0UAAAAmAAAConpoQ04AAAAWAAADcmphSlAAAAAaAAADiHJvUk8AAAAkAAADomVsR1IAAAAiAAADxnB0UE8AAAAmAAAD6G5sTkwAAAAoAAAEDmVzRVMAAAAmAAAD6HRoVEgAAAAkAAAENnRyVFIAAAAiAAAEWmZpRkkAAAAoAAAEfHBsUEwAAAAsAAAEpHJ1UlUAAAAiAAAE0GFyRUcAAAAmAAAE8mVuVVMAAAAmAAAFGGRhREsAAAAuAAAFPgBWAWEAZQBvAGIAZQD/YwBuAP0AIABSAEcAQgAgAHAAcgBvAGYAaQBsAEcAZQBuAGUAcgBpAQ0AawBpACAAUgBHAEIAIABwAHIAbwBmAGkAbABQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBlAG4A6AByAGkAYwBQAGUAcgBmAGkAbAAgAFIARwBCACAARwBlAG4A6QByAGkAYwBvBBcEMAQzBDAEOwRMBD0EOAQ5ACAEPwRABD4ERAQwBDkEOwAgAFIARwBCAFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1AGUAIABSAFYAQpAadSgAIABSAEcAQgAggnJfaWPPj/AAUAByAG8AZgBp/wBsAG8AIABSAEcAQgAgAGcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBzAGsAIABSAEcAQgAtAHAAcgBvAGYAaQBsx3y8GAAgAFIARwBCACDVBLhc0wzHfABPAGIAZQBjAG4A/QAgAFIARwBCACAAcAByAG8AZgBpAGwF5AXoBdUF5AXZBdwAIABSAEcAQgAgBdsF3AXcBdkAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbADBAGwAdABhAGwA4QBuAG8AcwAgAFIARwBCACAAcAByAG8AZgBpAGxmbpAaACAAUgBHAEIAIGPPj//wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrAFAAcgBvAGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjA5MDtQO9A7kDugPMACADwAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBlAG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8AZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBHAGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGX/AGkAbgBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBpAGwAaQBVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBmAGkAbAAgAFIARwBCBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgZFBkQGQQAgBioGOQYxBkoGQQAgAFIARwBCACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBpAGMAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGUARwBlAG4AZQByAGUAbAAgAFIARwBCAC0AYgBlAHMAawByAGkAdgBlAGwAcwBldGV4dAAAAABDb3B5cmlnaHQgMjAwrzcgQXBwbGUgSW5jLiwgYWxsIHJpZ2h0cyByZXNlcnZlZC4AWFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0AAD3uAAAD0FhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAACgaAAAVnwAAuDZjdXJ2AAAAAAAAAAEBzQAAc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeSAAD9kf//+6L///2jAAAD3AAAwGwALAAAAAAQABAAAAIglI+pwK3XInhSLoZc0oa/7lHRB4bXRJZoaqau+o6ujBQAOw==';
+
+ cut_icon = 'data:image/gif;base64,R0lGODlhEAAQALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//////yH5BAEAAAcALAAAAAAQABAAAAQu8MhJqz1g5qs7lxv2gRkQfuWomarXEgDRHjJhf3YtyRav0xcfcFgR0nhB5OwTAQA7';
+
+ copy_icon = 'data:image/gif;base64,R0lGODlhEAAQALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//////yH5BAEAAAcALAAAAAAQABAAAAQ+8MhJ6wE4Wwqef9gmdV8HiKZJrCz3ecS7TikWfzExvk+M9a0a4MbTkXCgTMeoHPJgG5+yF31SLazsTMTtViIAOw==';
+
+ paste_icon = 'data:image/gif;base64,R0lGODlhEAAQALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//////yH5BAEAAAcALAAAAAAQABAAAARL8MhJqwUYWJnxWp3GDcgAgCdQIqLKXmVLhhnyHiqpr7rME8AgocVDEB5IJHD0SyofBFzxGIQGAbvB0ZkcTq1CKK6z5YorwnR0w44AADs=';
+
+ close_icon = 'data:image/gif;base64,R0lGODlhEAAQAMQAAGtra/f3/62tre/v9+bm787O1pycnHNzc6WlpcXFxd7e3tbW1nt7e7W1te/v74SEhMXFzmNjY+bm5v///87OzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAAAQABAAAAVt4DRMZGmSwRQQBUS9MAwRIyQ5Uq7neEFSDtxOF4T8cobIQaE4RAQ5yjHHiCCSD510QtFGvoCFdppDfBu7bYzy+D7WP5ggAgA8Y3FKwi5IAhIweW1vbBGEWy5rilsFi2tGAwSJixAFBCkpJ5ojIQA7';
+
+ grid_icon = 'data:image/gif;base64,R0lGODlhEAAQAMQAAAAAAP///zAwYT09bpGRqZ6et5iYsKWlvbi40MzM5cXF3czM5OHh5tTU2fDw84uMom49DbWKcfLy8g0NDcDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABQALAAAAAAQABAAAAUtICWOZGmeKDCqIlu68AvMdO2ueHvGuslTN6Bt6MsBd8Zg77hsDW3FpRJFrYpCADs=';
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // 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) {
+ // deal with degenerate case...
+ if (vmin == vmax) {
+ if (vmin == 0) { vmin = -0.5; vmax = 0.5; }
+ else {
+ vmin = vmin > 0 ? 0.9*vmin : 1.1*vmin;
+ vmax = vmax > 0 ? 1.1*vmax : 0.9*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];
+ }
+
+ function engineering_notation(n,nplaces,trim) {
+ if (n == 0) return '0';
+ if (n == undefined) return 'undefined';
+ if (trim == undefined) trim = true;
+
+ var sign = n < 0 ? -1 : 1;
+ var log10 = Math.log(sign*n)/Math.LN10;
+ var exp = Math.floor(log10/3); // powers of 1000
+ var mantissa = sign*Math.pow(10,log10 - 3*exp);
+
+ // keep specified number of places following decimal point
+ var mstring = (mantissa + sign*0.5*Math.pow(10,-nplaces)).toString();
+ var mlen = mstring.length;
+ var endindex = mstring.indexOf('.');
+ if (endindex != -1) {
+ if (nplaces > 0) {
+ endindex += nplaces + 1;
+ if (endindex > mlen) endindex = mlen;
+ if (trim) {
+ while (mstring.charAt(endindex-1) == '0') endindex -= 1;
+ if (mstring.charAt(endindex-1) == '.') endindex -= 1;
+ }
+ }
+ if (endindex < mlen)
+ mstring = mstring.substring(0,endindex);
+ }
+
+ switch(exp) {
+ case -5: return mstring+"f";
+ case -4: return mstring+"p";
+ case -3: return mstring+"n";
+ case -2: return mstring+"u";
+ case -1: return mstring+"m";
+ case 0: return mstring;
+ case 1: return mstring+"K";
+ case 2: return mstring+"M";
+ case 3: return mstring+"G";
+ }
+
+ // don't have a good suffix, so just print the number
+ return n.toString();
+ }
+
+ var grid_pattern = [1,2];
+ var cursor_pattern = [5,5];
+
+ // 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 on left vertical axis
+ // z_values is an array of [color, value_array], one entry for each plot on right vertical axis
+ Schematic.prototype.graph = function(x_values,x_legend,y_values,y_legend,z_values,z_legend) {
+ var pwidth = 400; // dimensions of actual plot
+ var pheight = 300; // dimensions of actual plot
+ var left_margin = (y_values != undefined && y_values.length > 0) ? 55 : 25;
+ var top_margin = 25;
+ var right_margin = (z_values != undefined && z_values.length > 0) ? 55 : 25;
+ var bottom_margin = 45;
+ var tick_length = 5;
+
+ 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,grid_pattern);
+ c.stroke();
+
+ // tick mark
+ c.beginPath();
+ c.moveTo(temp,end);
+ c.lineTo(temp,end + tick_length);
+ c.stroke();
+ c.fillText(engineering_notation(x,2),temp,end + tick_length);
+ }
+
+ if (y_values != undefined && y_values.length > 0) {
+ var y_min = Infinity;
+ var y_max = -Infinity;
+ var plot;
+ for (plot = y_values.length - 1; plot >= 0; --plot) {
+ var values = y_values[plot][2];
+ if (values == undefined) continue; // no data points
+ var offset = y_values[plot][1];
+ var temp = array_min(values) + offset;
+ if (temp < y_min) y_min = temp;
+ temp = array_max(values) + offset;
+ 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]) {
+ if (Math.abs(y/y_max) < 0.001) y = 0.0; // Just 3 digits
+ 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,grid_pattern);
+ c.stroke();
+
+ // tick mark
+ c.beginPath();
+ c.moveTo(left_margin - tick_length,temp);
+ c.lineTo(left_margin,temp);
+ c.stroke();
+ c.fillText(engineering_notation(y,2),left_margin - tick_length -2,temp);
+ }
+
+ // now draw each plot
+ var x,y;
+ var nx,ny;
+ c.lineWidth = 3;
+ c.lineCap = 'round';
+ for (plot = y_values.length - 1; plot >= 0; --plot) {
+ var color = probe_colors_rgb[y_values[plot][0]];
+ if (color == undefined) continue; // no plot color (== x-axis)
+ c.strokeStyle = color;
+ var values = y_values[plot][2];
+ if (values == undefined) continue; // no data points
+ var offset = y_values[plot][1];
+
+ x = plot_x(x_values[0]);
+ y = plot_y(values[0] + offset);
+ c.beginPath();
+ c.moveTo(x,y);
+ for (var i = 1; i < x_values.length; i++) {
+ nx = plot_x(x_values[i]);
+ ny = plot_y(values[i] + offset);
+ c.lineTo(nx,ny);
+ x = nx;
+ y = ny;
+ if (i % 100 == 99) {
+ // too many lineTo's cause canvas to break
+ c.stroke();
+ c.beginPath();
+ c.moveTo(x,y);
+ }
+ }
+ c.stroke();
+ }
+ }
+
+ if (z_values != undefined && z_values.length > 0) {
+ var z_min = Infinity;
+ var z_max = -Infinity;
+ for (plot = z_values.length - 1; plot >= 0; --plot) {
+ var values = z_values[plot][2];
+ if (values == undefined) continue; // no data points
+ var offset = z_values[plot][1];
+ var temp = array_min(values) + offset;
+ if (temp < z_min) z_min = temp;
+ temp = array_max(values) + offset;
+ if (temp > z_max) z_max = temp;
+ }
+ var z_limits = view_limits(z_min,z_max);
+ z_min = z_limits[0];
+ z_max = z_limits[1];
+ var z_scale = pheight/(z_max - z_min);
+
+ function plot_z(z) {
+ return (z_max - z)*z_scale + top_margin;
+ }
+
+ // draw z ticks
+ c.textAlign = 'left';
+ c.textBaseline = 'middle';
+ c.lineWidth = 1;
+ c.strokeStyle = normal_style;
+ var tick_length_half = Math.floor(tick_length/2);
+ var tick_delta = tick_length - tick_length_half;
+ for (var z = z_min; z <= z_max; z += z_limits[2]) {
+ if (Math.abs(z/z_max) < 0.001) z = 0.0; // Just 3 digits
+ var temp = plot_z(z) + 0.5; // keep lines crisp!
+
+ // tick mark
+ c.beginPath();
+ c.moveTo(left_margin + pwidth - tick_length_half,temp);
+ c.lineTo(left_margin + pwidth + tick_delta,temp);
+ c.stroke();
+ c.fillText(engineering_notation(z,2),left_margin + pwidth + tick_length + 2,temp);
+ }
+
+ var z;
+ var nz;
+ c.lineWidth = 3;
+ for (plot = z_values.length - 1; plot >= 0; --plot) {
+ var color = probe_colors_rgb[z_values[plot][0]];
+ if (color == undefined) continue; // no plot color (== x-axis)
+ c.strokeStyle = color;
+ var values = z_values[plot][2];
+ if (values == undefined) continue; // no data points
+ var offset = z_values[plot][1];
+
+ x = plot_x(x_values[0]);
+ z = plot_z(values[0] + offset);
+ c.beginPath();
+ c.moveTo(x,z);
+ for (var i = 1; i < x_values.length; i++) {
+ nx = plot_x(x_values[i]);
+ nz = plot_z(values[i] + offset);
+ c.lineTo(nx,nz);
+ x = nx;
+ z = nz;
+ if (i % 100 == 99) {
+ // too many lineTo's cause canvas to break
+ c.stroke();
+ c.beginPath();
+ c.moveTo(x,z);
+ }
+ }
+ 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);
+
+ if (y_values != undefined && y_values.length > 0) {
+ 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();
+ }
+
+ if (z_values != undefined && z_values.length > 0) {
+ c.textBaseline = 'bottom';
+ c.save();
+ c.translate(w-5 ,top_margin + pheight/2);
+ c.rotate(-Math.PI/2);
+ c.fillText(z_legend,0,0);
+ c.restore();
+ }
+
+ // save info need for interactions with the graph
+ canvas.x_values = x_values;
+ canvas.y_values = y_values;
+ canvas.z_values = z_values;
+ canvas.x_legend = x_legend;
+ canvas.y_legend = y_legend;
+ canvas.z_legend = y_legend;
+ canvas.x_min = x_min;
+ canvas.x_scale = x_scale;
+ canvas.y_min = y_min;
+ canvas.y_scale = y_scale;
+ canvas.z_min = z_min;
+ canvas.z_scale = z_scale;
+ canvas.left_margin = left_margin;
+ canvas.top_margin = top_margin;
+ canvas.pwidth = pwidth;
+ canvas.pheight = pheight;
+ canvas.tick_length = tick_length;
+
+ canvas.cursor1_x = undefined;
+ canvas.cursor2_x = undefined;
+ canvas.sch = this;
+
+ // do something useful when user mouses over graph
+ canvas.addEventListener('mousemove',graph_mouse_move,false);
+
+ // 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 plot_cursor(c,graph,cursor_x,left_margin) {
+ // draw dashed vertical marker that follows mouse
+ var x = graph.left_margin + cursor_x;
+ var end_y = graph.top_margin + graph.pheight + graph.tick_length;
+ c.strokeStyle = grid_style;
+ c.lineWidth = 1;
+ c.beginPath();
+ c.dashedLineTo(x,graph.top_margin,x,end_y,cursor_pattern);
+ c.stroke();
+
+ // add x label at bottom of marker
+ var graph_x = cursor_x/graph.x_scale + graph.x_min;
+ c.font = '10pt sans-serif';
+ c.textAlign = 'center';
+ c.textBaseline = 'top';
+ c.fillStyle = background_style;
+ c.fillText('\u2588\u2588\u2588\u2588\u2588',x,end_y);
+ c.fillStyle = normal_style;
+ c.fillText(engineering_notation(graph_x,3,false),x,end_y);
+
+ // compute which points marker is between
+ var x_values = graph.x_values;
+ var len = x_values.length;
+ var index = 0;
+ while (index < len && graph_x >= x_values[index]) index += 1;
+ var x1 = (index == 0) ? x_values[0] : x_values[index-1];
+ var x2 = x_values[index];
+
+ if (x2 != undefined) {
+ // for each plot, interpolate and output value at intersection with marker
+ c.textAlign = 'left';
+ var tx = graph.left_margin + left_margin;
+ var ty = graph.top_margin;
+ if (graph.y_values != undefined) {
+ for (var plot = 0; plot < graph.y_values.length; plot++) {
+ var values = graph.y_values[plot][2];
+ var color = probe_colors_rgb[graph.y_values[plot][0]];
+ if (values == undefined || color == undefined) continue; // no data points or x-axis
+
+ // interpolate signal value at graph_x using values[index-1] and values[index]
+ var y1 = (index == 0) ? values[0] : values[index-1];
+ var y2 = values[index];
+ var y = y1;
+ if (graph_x != x1) y += (graph_x - x1)*(y2 - y1)/(x2 - x1);
+
+ // annotate plot with value of signal at marker
+ c.fillStyle = element_style;
+ c.fillText('\u2588\u2588\u2588\u2588\u2588',tx-3,ty);
+ c.fillStyle = color;
+ c.fillText(engineering_notation(y,3,false),tx,ty);
+ ty += 14;
+ }
+ }
+
+ c.textAlign = 'right';
+ if (graph.z_values != undefined) {
+ var tx = graph.left_margin + graph.pwidth - left_margin;
+ var ty = graph.top_margin;
+ for (var plot = 0; plot < graph.z_values.length; plot++) {
+ var values = graph.z_values[plot][2];
+ var color = probe_colors_rgb[graph.z_values[plot][0]];
+ if (values == undefined || color == undefined) continue; // no data points or x-axis
+
+ // interpolate signal value at graph_x using values[index-1] and values[index]
+ var z1 = (index == 0) ? values[0]: values[index-1];
+ var z2 = values[index];
+ var z = z1;
+ if (graph_x != x1) z += (graph_x - x1)*(z2 - z1)/(x2 - x1);
+
+ // annotate plot with value of signal at marker
+ c.fillStyle = element_style;
+ c.fillText('\u2588\u2588\u2588\u2588\u2588',tx+3,ty);
+ c.fillStyle = color;
+ c.fillText(engineering_notation(z,3,false),tx,ty);
+ ty += 14;
+ }
+ }
+ }
+ }
+
+ function redraw_plot(graph) {
+ var c = graph.getContext('2d');
+ c.drawImage(graph.bg_image,0,0);
+
+ if (graph.cursor1_x != undefined) plot_cursor(c,graph,graph.cursor1_x,4);
+ if (graph.cursor2_x != undefined) plot_cursor(c,graph,graph.cursor2_x,30);
+
+ /*
+ if (graph.cursor1_x != undefined) {
+ // draw dashed vertical marker that follows mouse
+ var x = graph.left_margin + graph.cursor1_x;
+ var end_y = graph.top_margin + graph.pheight + graph.tick_length;
+ c.strokeStyle = grid_style;
+ c.lineWidth = 1;
+ c.beginPath();
+ c.dashedLineTo(x,graph.top_margin,x,end_y,cursor_pattern);
+ c.stroke();
+
+ // add x label at bottom of marker
+ var graph_x = graph.cursor1_x/graph.x_scale + graph.x_min;
+ c.font = '10pt sans-serif';
+ c.textAlign = 'center';
+ c.textBaseline = 'top';
+ c.fillStyle = background_style;
+ c.fillText('\u2588\u2588\u2588\u2588\u2588',x,end_y);
+ c.fillStyle = normal_style;
+ c.fillText(engineering_notation(graph_x,3,false),x,end_y);
+
+ // compute which points marker is between
+ var x_values = graph.x_values;
+ var len = x_values.length;
+ var index = 0;
+ while (index < len && graph_x >= x_values[index]) index += 1;
+ var x1 = (index == 0) ? x_values[0] : x_values[index-1];
+ var x2 = x_values[index];
+
+ if (x2 != undefined) {
+ // for each plot, interpolate and output value at intersection with marker
+ c.textAlign = 'left';
+ var tx = graph.left_margin + 4;
+ var ty = graph.top_margin;
+ for (var plot = 0; plot < graph.y_values.length; plot++) {
+ var values = graph.y_values[plot][1];
+
+ // interpolate signal value at graph_x using values[index-1] and values[index]
+ var y1 = (index == 0) ? values[0] : values[index-1];
+ var y2 = values[index];
+ var y = y1;
+ if (graph_x != x1) y += (graph_x - x1)*(y2 - y1)/(x2 - x1);
+
+ // annotate plot with value of signal at marker
+ c.fillStyle = element_style;
+ c.fillText('\u2588\u2588\u2588\u2588\u2588',tx-3,ty);
+ c.fillStyle = probe_colors_rgb[graph.y_values[plot][0]];
+ c.fillText(engineering_notation(y,3,false),tx,ty);
+ ty += 14;
+ }
+ }
+ }
+ */
+ }
+
+ function graph_mouse_move(event) {
+ if (!event) event = window.event;
+ var g = (window.event) ? event.srcElement : event.target;
+
+ g.relMouseCoords(event);
+ // not sure yet where the 3,-3 offset correction comes from (borders? padding?)
+ var gx = g.mouse_x - g.left_margin - 3;
+ var gy = g.pheight - (g.mouse_y - g.top_margin) + 3;
+ if (gx >= 0 && gx <= g.pwidth && gy >=0 && gy <= g.pheight) {
+ //g.sch.message('button: '+event.button+', which: '+event.which);
+ g.cursor1_x = gx;
+ } else {
+ g.cursor1_x = undefined;
+ g.cursor2_x = undefined;
+ }
+
+ redraw_plot(g);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Parts bin
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // one instance will be created for each part in the parts bin
+ function Part(sch) {
+ this.sch = sch;
+ this.component = undefined;
+ this.selected = false;
+
+ // set up canvas
+ this.canvas = document.createElement('canvas');
+ this.canvas.style.borderStyle = 'solid';
+ this.canvas.style.borderWidth = '1px';
+ this.canvas.style.borderColor = background_style;
+ //this.canvas.style.position = 'absolute';
+ this.canvas.style.cursor = 'default';
+ this.canvas.height = part_w;
+ this.canvas.width = part_h;
+ this.canvas.part = this;
+
+ this.canvas.addEventListener('mouseover',part_enter,false);
+ this.canvas.addEventListener('mouseout',part_leave,false);
+ this.canvas.addEventListener('mousedown',part_mouse_down,false);
+ this.canvas.addEventListener('mouseup',part_mouse_up,false);
+
+ // make the part "clickable" by registering a dummy click handler
+ // this should make things work on the iPad
+ this.canvas.addEventListener('click',function(){},false);
+ }
+
+ Part.prototype.set_location = function(left,top) {
+ this.canvas.style.left = left + 'px';
+ this.canvas.style.top = top + 'px';
+ }
+
+ Part.prototype.right = function() {
+ return this.canvas.offsetLeft + this.canvas.offsetWidth;
+ }
+
+ Part.prototype.bottom = function() {
+ return this.canvas.offsetTop + this.canvas.offsetHeight;
+ }
+
+ Part.prototype.set_component = function(component,tip) {
+ component.sch = this;
+ this.component = component;
+ this.tip = tip;
+
+ // figure out scaling and centering of parts icon
+ var b = component.bounding_box;
+ var dx = b[2] - b[0];
+ var dy = b[3] - b[1];
+ this.scale = 0.8; //Math.min(part_w/(1.2*dx),part_h/(1.2*dy));
+ this.origin_x = b[0] + dx/2.0 - part_w/(2.0*this.scale);
+ this.origin_y = b[1] + dy/2.0 - part_h/(2.0*this.scale);
+
+ this.redraw();
+ }
+
+ Part.prototype.redraw = function(part) {
+ var c = this.canvas.getContext('2d');
+
+ // paint background color
+ c.fillStyle = this.selected ? selected_style : background_style;
+ c.fillRect(0,0,part_w,part_h);
+
+ if (this.component) this.component.draw(c);
+ }
+
+ Part.prototype.select = function(which) {
+ this.selected = which;
+ this.redraw();
+ }
+
+ Part.prototype.update_connection_point = function(cp,old_location) {
+ // 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();
+ c.moveTo((x1 - this.origin_x) * this.scale,(y1 - this.origin_y) * this.scale);
+ c.lineTo((x2 - this.origin_x) * this.scale,(y2 - this.origin_y) * this.scale);
+ c.stroke();
+ }
+
+ Part.prototype.draw_arc = function(c,x,y,radius,start_radians,end_radians,anticlockwise,width,filled) {
+ c.lineWidth = width*this.scale;
+ c.beginPath();
+ c.arc((x - this.origin_x)*this.scale,(y - this.origin_y)*this.scale,radius*this.scale,
+ start_radians,end_radians,anticlockwise);
+ if (filled) c.fill();
+ else c.stroke();
+ }
+
+ Part.prototype.draw_text = function(c,text,x,y,size) {
+ // no text displayed for the parts icon
+ }
+
+ function part_enter(event) {
+ if (!event) event = window.event;
+ var canvas = (window.event) ? event.srcElement : event.target;
+ var part = canvas.part;
+
+ // avoid Chrome bug that changes to text cursor whenever
+ // drag starts. We'll restore the default handler at
+ // the appropriate point so behavior in other parts of
+ // the document are unaffected.
+ //part.sch.saved_onselectstart = document.onselectstart;
+ //document.onselectstart = function () { return false; };
+
+ canvas.style.borderColor = normal_style;
+ part.sch.message(part.tip+': drag onto diagram to insert');
+ return false;
+ }
+
+ function part_leave(event) {
+ if (!event) event = window.event;
+ var canvas = (window.event) ? event.srcElement : event.target;
+ var part = canvas.part;
+
+ if (typeof part.sch.new_part == 'undefined') {
+ // leaving with no part selected? revert handler
+ //document.onselectstart = part.sch.saved_onselectstart;
+ }
+
+ canvas.style.borderColor = background_style;
+ part.sch.message('');
+ return false;
+ }
+
+ function part_mouse_down(event) {
+ if (!event) event = window.event;
+ var part = (window.event) ? event.srcElement.part : event.target.part;
+
+ part.select(true);
+ part.sch.new_part = part;
+ return false;
+ }
+
+ function part_mouse_up(event) {
+ if (!event) event = window.event;
+ var part = (window.event) ? event.srcElement.part : event.target.part;
+
+ part.select(false);
+ part.sch.new_part = undefined;
+ return false;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Rectangle helper functions
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ // rect is an array of the form [left,top,right,bottom]
+
+ // ensure left < right, top < bottom
+ function canonicalize(r) {
+ var temp;
+
+ // canonicalize bounding box
+ if (r[0] > r[2]) {
+ temp = r[0];
+ r[0] = r[2];
+ r[2] = temp;
+ }
+ if (r[1] > r[3]) {
+ temp = r[1];
+ r[1] = r[3];
+ r[3] = temp;
+ }
+ }
+
+ function between(x,x1,x2) {
+ return x1 <= x && x <= x2;
+ }
+
+ function inside(rect,x,y) {
+ return between(x,rect[0],rect[2]) && between(y,rect[1],rect[3]);
+ }
+
+ // only works for manhattan rectangles
+ function intersect(r1,r2) {
+ // look for non-intersection, negate result
+ var result = !(r2[0] > r1[2] ||
+ r2[2] < r1[0] ||
+ r2[1] > r1[3] ||
+ r2[3] < r1[1]);
+
+ // if I try to return the above expression, javascript returns undefined!!!
+ return result;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Component base class
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ function Component(type,x,y,rotation) {
+ this.sch = undefined;
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.rotation = rotation;
+ this.selected = false;
+ this.properties = new Array();
+ this.bounding_box = [0,0,0,0]; // in device coords [left,top,right,bottom]
+ this.bbox = this.bounding_box; // in absolute coords
+ this.connections = [];
+ }
+
+ Component.prototype.json = function(index) {
+ this.properties['_json_'] = index; // remember where we are in the JSON list
+
+ var props = {};
+ for (var p in this.properties) props[p] = this.properties[p];
+
+ var conns = [];
+ for (var i = 0; i < this.connections.length; i++)
+ conns.push(this.connections[i].json());
+
+ var json = [this.type,[this.x, this.y, this.rotation],props,conns];
+ return json;
+ }
+
+ Component.prototype.add_connection = function(offset_x,offset_y) {
+ this.connections.push(new ConnectionPoint(this,offset_x,offset_y));
+ }
+
+ Component.prototype.update_coords = function() {
+ var x = this.x;
+ var y = this.y;
+
+ // update bbox
+ var b = this.bounding_box;
+ this.bbox[0] = this.transform_x(b[0],b[1]) + x;
+ this.bbox[1] = this.transform_y(b[0],b[1]) + y;
+ this.bbox[2] = this.transform_x(b[2],b[3]) + x;
+ this.bbox[3] = this.transform_y(b[2],b[3]) + y;
+ canonicalize(this.bbox);
+
+ // update connections
+ for (var i = this.connections.length - 1; i >= 0; --i)
+ this.connections[i].update_location();
+ }
+
+ Component.prototype.rotate = function(amount) {
+ var old_rotation = this.rotation;
+ this.rotation = (this.rotation + amount) % 8;
+ this.update_coords();
+
+ // create an undoable edit record here
+ // using old_rotation
+ }
+
+ Component.prototype.move_begin = function() {
+ // remember where we started this move
+ this.move_x = this.x;
+ this.move_y = this.y;
+ }
+
+ Component.prototype.move = function(dx,dy) {
+ // update coordinates
+ this.x += dx;
+ this.y += dy;
+ this.update_coords();
+ }
+
+ Component.prototype.move_end = function() {
+ var dx = this.x - this.move_x;
+ var dy = this.y - this.move_y;
+
+ if (dx != 0 || dy != 0) {
+ // create an undoable edit record here
+
+ this.sch.check_wires(this);
+ }
+ }
+
+ Component.prototype.add = function(sch) {
+ this.sch = sch; // we now belong to a schematic!
+ sch.add_component(this);
+ this.update_coords();
+ }
+
+ Component.prototype.remove = function() {
+ // remove connection points from schematic
+ for (var i = this.connections.length - 1; i >= 0; --i) {
+ var cp = this.connections[i];
+ this.sch.remove_connection_point(cp,cp.location);
+ }
+
+ // remove component from schematic
+ this.sch.remove_component(this);
+ this.sch = undefined;
+
+ // create an undoable edit record here
+ }
+
+ Component.prototype.transform_x = function(x,y) {
+ var rot = this.rotation;
+ if (rot == 0 || rot == 6) return x;
+ else if (rot == 1 || rot == 5) return -y;
+ else if (rot == 2 || rot == 4) return -x;
+ else return y;
+ }
+
+ Component.prototype.transform_y = function(x,y) {
+ var rot = this.rotation;
+ if (rot == 1 || rot == 7) return x;
+ else if (rot == 2 || rot == 6) return -y;
+ else if (rot == 3 || rot == 5) return -x;
+ 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 :
+ this.type == 'w' ? normal_style : component_style;
+ var nx1 = this.transform_x(x1,y1) + this.x;
+ var ny1 = this.transform_y(x1,y1) + this.y;
+ var nx2 = this.transform_x(x2,y2) + this.x;
+ var ny2 = this.transform_y(x2,y2) + this.y;
+ this.sch.draw_line(c,nx1,ny1,nx2,ny2,1);
+ }
+
+ Component.prototype.draw_circle = function(c,x,y,radius,filled) {
+ if (filled) c.fillStyle = this.selected ? selected_style : normal_style;
+ else c.strokeStyle = this.selected ? selected_style :
+ this.type == 'w' ? normal_style : component_style;
+ var nx = this.transform_x(x,y) + this.x;
+ var ny = this.transform_y(x,y) + this.y;
+
+ this.sch.draw_arc(c,nx,ny,radius,0,2*Math.PI,false,1,filled);
+ }
+
+ rot_angle = [
+ 0.0, // NORTH (identity)
+ Math.PI/2, // EAST (rot270)
+ Math.PI, // SOUTH (rot180)
+ 3*Math.PI/2, // WEST (rot90)
+ 0.0, // RNORTH (negy)
+ Math.PI/2, // REAST (int-neg)
+ Math.PI, // RSOUTH (negx)
+ 3*Math.PI/2, // RWEST (int-pos)
+ ];
+
+ Component.prototype.draw_arc = function(c,x,y,radius,start_radians,end_radians) {
+ c.strokeStyle = this.selected ? selected_style :
+ this.type == 'w' ? normal_style : component_style;
+ var nx = this.transform_x(x,y) + this.x;
+ var ny = this.transform_y(x,y) + this.y;
+ this.sch.draw_arc(c,nx,ny,radius,
+ start_radians+rot_angle[this.rotation],end_radians+rot_angle[this.rotation],
+ false,1,false);
+ }
+
+ Component.prototype.draw = function(c) {
+ /*
+ for (var i = this.connections.length - 1; i >= 0; --i) {
+ var cp = this.connections[i];
+ cp.draw_x(c);
+ }
+ */
+ }
+
+ // result of rotating an alignment [rot*9 + align]
+ aOrient = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, // NORTH (identity)
+ 2, 5, 8, 1, 4, 7, 0, 3, 6, // EAST (rot270)
+ 8, 7, 6, 5, 4, 3, 2, 1, 0, // SOUTH (rot180)
+ 6, 3, 0, 7, 4, 1, 8, 5, 3, // WEST (rot90)
+ 2, 1, 0, 5, 4, 3, 8, 7, 6, // RNORTH (negy)
+ 8, 5, 2, 7, 4, 1, 6, 3, 0, // REAST (int-neg)
+ 6, 7, 8, 3, 4, 5, 0, 1, 2, // RSOUTH (negx)
+ 0, 3, 6, 1, 4, 7, 2, 5, 8 // RWEST (int-pos)
+ ];
+
+ textAlign = [
+ 'left', 'center', 'right',
+ 'left', 'center', 'right',
+ 'left', 'center', 'right'
+ ];
+
+ textBaseline = [
+ 'top', 'top', 'top',
+ 'middle', 'middle', 'middle',
+ 'bottom', 'bottom', 'bottom'
+ ];
+
+ Component.prototype.draw_text = function(c,text,x,y,alignment,size,fill) {
+ var a = aOrient[this.rotation*9 + alignment];
+ c.textAlign = textAlign[a];
+ c.textBaseline = textBaseline[a];
+ if (fill == undefined)
+ c.fillStyle = this.selected ? selected_style : normal_style;
+ else
+ c.fillStyle = fill;
+ this.sch.draw_text(c,text,
+ this.transform_x(x,y) + this.x,
+ this.transform_y(x,y) + this.y,
+ size);
+ }
+
+ Component.prototype.set_select = function(which) {
+ if (which != this.selected) {
+ this.selected = which;
+ // create an undoable edit record here
+ }
+ }
+
+ Component.prototype.select = function(x,y,shiftKey) {
+ this.was_previously_selected = this.selected;
+ if (this.near(x,y)) {
+ this.set_select(shiftKey ? !this.selected : true);
+ return true;
+ } else return false;
+ }
+
+ Component.prototype.select_rect = function(s) {
+ this.was_previously_selected = this.selected;
+ if (intersect(this.bbox,s))
+ this.set_select(true);
+ }
+
+ // if connection point of component c bisects the
+ // wire represented by this compononent, return that
+ // connection point. Otherwise return null.
+ Component.prototype.bisect = function(c) {
+ return null;
+ }
+
+ // does mouse click fall on this component?
+ Component.prototype.near = function(x,y) {
+ return inside(this.bbox,x,y);
+ }
+
+ Component.prototype.edit_properties = function(x,y) {
+ if (this.near(x,y)) {
+ // make an widget for each property
+ var fields = new Array();
+ for (var i in this.properties)
+ // underscore at beginning of property name => system property
+ if (i.charAt(0) != '_')
+ fields[i] = build_input('text',10,this.properties[i]);
+
+ var content = build_table(fields);
+ content.fields = fields;
+ content.component = this;
+
+ this.sch.dialog('Edit Properties',content,function(content) {
+ for (var i in content.fields)
+ content.component.properties[i] = content.fields[i].value;
+ content.component.sch.redraw_background();
+ });
+ return true;
+ } else return false;
+ }
+
+ // clear the labels on all connections
+ Component.prototype.clear_labels = function() {
+ for (var i = this.connections.length - 1; i >=0; --i) {
+ this.connections[i].clear_label();
+ }
+ }
+
+ // default action: don't propagate label
+ Component.prototype.propagate_label = function(label) {
+ }
+
+ // give components a chance to generate default labels for their connection(s)
+ // default action: do nothing
+ Component.prototype.add_default_labels = function() {
+ }
+
+ // component should generate labels for all unlabeled connections
+ Component.prototype.label_connections = function() {
+ for (var i = this.connections.length - 1; i >=0; --i) {
+ var cp = this.connections[i];
+ if (!cp.label)
+ cp.propagate_label(this.sch.get_next_label());
+ }
+ }
+
+ // default behavior: no probe info
+ Component.prototype.probe_info = function() { return undefined; }
+
+ // default behavior: nothing to display for DC analysis
+ Component.prototype.display_current = function(c,vmap) {
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Connection point
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ connection_point_radius = 2;
+
+ function ConnectionPoint(parent,x,y) {
+ this.parent = parent;
+ this.offset_x = x;
+ this.offset_y = y;
+ this.location = '';
+ this.update_location();
+ this.label = undefined;
+ }
+
+ ConnectionPoint.prototype.toString = function() {
+ return '';
+ }
+
+ ConnectionPoint.prototype.json = function() {
+ return this.label;
+ }
+
+ ConnectionPoint.prototype.clear_label = function() {
+ this.label = undefined;
+ }
+
+ ConnectionPoint.prototype.propagate_label = function(label) {
+ // should we check if existing label is the same? it should be...
+
+ if (this.label === undefined) {
+ // label this connection point
+ this.label = label;
+
+ // propagate label to coincident connection points
+ this.parent.sch.propagate_label(label,this.location);
+
+ // possibly label other cp's for this device?
+ this.parent.propagate_label(label);
+ } else if (this.label != '0' && label != '0' && this.label != label)
+ alert("Node has two conflicting labels: "+this.label+", "+label);
+ }
+
+ ConnectionPoint.prototype.update_location = function() {
+ // update location string which we use as a key to find coincident connection points
+ var old_location = this.location;
+ var parent = this.parent;
+ var nx = parent.transform_x(this.offset_x,this.offset_y) + parent.x;
+ var ny = parent.transform_y(this.offset_x,this.offset_y) + parent.y;
+ this.x = nx;
+ this.y = ny;
+ this.location = nx + ',' + ny;
+
+ // add ourselves to the connection list for the new location
+ if (parent.sch)
+ parent.sch.update_connection_point(this,old_location);
+ }
+
+ ConnectionPoint.prototype.coincident = function(x,y) {
+ return this.x==x && this.y==y;
+ }
+
+ ConnectionPoint.prototype.draw = function(c,n) {
+ if (n != 2)
+ this.parent.draw_circle(c,this.offset_x,this.offset_y,connection_point_radius,n > 2);
+ }
+
+ ConnectionPoint.prototype.draw_x = function(c) {
+ this.parent.draw_line(c,this.offset_x-2,this.offset_y-2,this.offset_x+2,this.offset_y+2,grid_style);
+ this.parent.draw_line(c,this.offset_x+2,this.offset_y-2,this.offset_x-2,this.offset_y+2,grid_style);
+ }
+
+ ConnectionPoint.prototype.display_voltage = function(c,vmap) {
+ var v = vmap[this.label];
+ if (v != undefined) {
+ var label = v.toFixed(2) + 'V';
+
+ // first draw some solid blocks in the background
+ 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,
+ 4,annotation_size,annotation_style);
+
+ // only display each node voltage once
+ delete vmap[this.label];
+ }
+ }
+
+ // see if three connection points are collinear
+ function collinear(p1,p2,p3) {
+ // from http://mathworld.wolfram.com/Collinear.html
+ var area = p1.x*(p2.y - p3.y) + p2.x*(p3.y - p1.y) + p3.x*(p1.y - p2.y);
+ return area == 0;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Wire
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ near_distance = 2; // how close to wire counts as "near by"
+
+ function Wire(x1,y1,x2,y2) {
+ // arbitrarily call x1,y1 the origin
+ Component.call(this,'w',x1,y1,0);
+ this.dx = x2 - x1;
+ this.dy = y2 - y1;
+ this.add_connection(0,0);
+ this.add_connection(this.dx,this.dy);
+
+ // compute bounding box (expanded slightly)
+ var r = [0,0,this.dx,this.dy];
+ canonicalize(r);
+ r[0] -= near_distance;
+ r[1] -= near_distance;
+ r[2] += near_distance;
+ r[3] += near_distance;
+ this.bounding_box = r;
+ this.update_coords(); // update bbox
+
+ // used in selection calculations
+ this.len = Math.sqrt(this.dx*this.dx + this.dy*this.dy);
+ }
+ Wire.prototype = new Component();
+ Wire.prototype.constructor = Wire;
+
+ Wire.prototype.toString = function() {
+ return '';
+ }
+
+ // return connection point at other end of wire from specified cp
+ Wire.prototype.other_end = function(cp) {
+ if (cp == this.connections[0]) return this.connections[1];
+ else if (cp == this.connections[1]) return this.connections[0];
+ else return undefined;
+ }
+
+ Wire.prototype.json = function(index) {
+ var json = ['w',[this.x, this.y, this.x+this.dx, this.y+this.dy]];
+ return json;
+ }
+
+ Wire.prototype.draw = function(c) {
+ this.draw_line(c,0,0,this.dx,this.dy);
+ }
+
+ Wire.prototype.clone = function(x,y) {
+ return new Wire(x,y,x+this.dx,y+this.dy);
+ }
+
+ Wire.prototype.near = function(x,y) {
+ // crude check: (x,y) within expanded bounding box of wire
+ if (inside(this.bbox,x,y)) {
+ // compute distance between x,y and nearst point on line
+ // http://www.allegro.cc/forums/thread/589720
+ var D = Math.abs((x - this.x)*this.dy - (y - this.y)*this.dx)/this.len;
+ if (D <= near_distance) return true;
+ }
+ return false;
+ }
+
+ // selection rectangle selects wire only if it includes
+ // one of the end points
+ Wire.prototype.select_rect = function(s) {
+ this.was_previously_selected = this.selected;
+ if (inside(s,this.x,this.y) || inside(s,this.x+this.dx,this.y+this.dy))
+ this.set_select(true);
+ }
+
+ // if connection point cp bisects the
+ // wire represented by this compononent, return true
+ Wire.prototype.bisect_cp = function(cp) {
+ var x = cp.x;
+ var y = cp.y;
+
+ // crude check: (x,y) within expanded bounding box of wire
+ if (inside(this.bbox,x,y)) {
+ // compute distance between x,y and nearst point on line
+ // http://www.allegro.cc/forums/thread/589720
+ var D = Math.abs((x - this.x)*this.dy - (y - this.y)*this.dx)/this.len;
+ // final check: ensure point isn't an end point of the wire
+ if (D < 1 && !this.connections[0].coincident(x,y) && !this.connections[1].coincident(x,y))
+ return true;
+ }
+ return false;
+ }
+
+ // if some connection point of component c bisects the
+ // wire represented by this compononent, return that
+ // connection point. Otherwise return null.
+ Wire.prototype.bisect = function(c) {
+ if (c == undefined) return;
+ for (var i = c.connections.length - 1; i >= 0; --i) {
+ var cp = c.connections[i];
+ if (this.bisect_cp(cp)) return cp;
+ }
+ return null;
+ }
+
+ Wire.prototype.move_end = function() {
+ // look for wires bisected by this wire
+ this.sch.check_wires(this);
+
+ // look for connection points that might bisect us
+ this.sch.check_connection_points(this);
+ }
+
+ // wires "conduct" their label to the other end
+ Wire.prototype.propagate_label = function(label) {
+ // don't worry about relabeling a cp, it won't recurse!
+ this.connections[0].propagate_label(label);
+ this.connections[1].propagate_label(label);
+ }
+
+ // Wires have no properties to edit
+ Wire.prototype.edit_properties = function(x,y) {
+ return false;
+ }
+
+ // some actual component will start the labeling of electrical nodes,
+ // so do nothing here
+ Wire.prototype.label_connections = function() {
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Ground
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ function Ground(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();
+ }
+ Ground.prototype = new Component();
+ Ground.prototype.constructor = Ground;
+
+ Ground.prototype.toString = function() {
+ return '';
+ }
+
+ Ground.prototype.draw = function(c) {
+ Component.prototype.draw.call(this,c); // give superclass a shot
+ this.draw_line(c,0,0,0,8);
+ this.draw_line(c,-6,8,6,8);
+ }
+
+ Ground.prototype.clone = function(x,y) {
+ return new Ground(x,y,this.rotation);
+ }
+
+ // Grounds no properties to edit
+ Ground.prototype.edit_properties = function(x,y) {
+ return false;
+ }
+
+ // give components a chance to generate a label for their connection(s)
+ // default action: do nothing
+ Ground.prototype.add_default_labels = function() {
+ this.connections[0].propagate_label('0'); // canonical label for GND node
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Label
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ function Label(x,y,rotation,label) {
+ Component.call(this,'L',x,y,rotation);
+ this.properties['label'] = label ? label : '???';
+ this.add_connection(0,0);
+ this.bounding_box = [-2,0,2,8];
+ this.update_coords();
+ }
+ Label.prototype = new Component();
+ Label.prototype.constructor = Label;
+
+ Label.prototype.toString = function() {
+ return '