diff --git a/courseware/capa_module.py b/courseware/capa_module.py index 36d396d245..617e33d31e 100644 --- a/courseware/capa_module.py +++ b/courseware/capa_module.py @@ -30,7 +30,19 @@ class LoncapaModule(XModule): def max_score(self): return len(self.lcp.questions) - def get_html(self, encapsulate=True): + def get_html(self): + return render_to_string('problem_ajax.html', + {'id':self.filename, + 'ajax_url':self.ajax_url, + }) + + def get_init_js(self): + return render_to_string('problem.js', + {'id':self.filename, + 'ajax_url':self.ajax_url, + }) + + def get_problem_html(self, encapsulate=True): html = self.lcp.get_html() content={'name':self.name, 'html':html} @@ -47,11 +59,9 @@ class LoncapaModule(XModule): }) if encapsulate: html = '
'.format(id=self.item_id)+html+"
" + return html - def get_init_js(self): - return "" - def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None): XModule.__init__(self, xml, item_id, ajax_url, track_url, state) dom=parseString(xml) @@ -63,17 +73,25 @@ class LoncapaModule(XModule): def handle_ajax(self, dispatch, get): if dispatch=='problem_check': - html = self.check_problem(get) + response = self.check_problem(get) elif dispatch=='problem_reset': - html = self.reset_problem(get) + response = self.reset_problem(get) + elif dispatch=='problem_get': + response = self.get_problem(get) else: return "Error" - return html + return response - # Temporary -- move to capa_problem + # Figure out if we should move these to capa_problem? + def get_problem(self, get): + ''' Same as get_problem_html -- if we want to reconfirm we have the right + thing e.g. after several AJAX calls. ''' + return self.get_problem_html(encapsulate=False) def check_problem(self, get): + ''' Checks whether answers to a problem are correct, and returns + a map of correct/incorrect answers ''' self.lcp.done=True answer=dict() # input_resistor_1 ==> resistor_1 @@ -85,6 +103,8 @@ class LoncapaModule(XModule): return js def reset_problem(self, get): + ''' Changes problem state to unfinished -- removes student answers, + and causes problem to rerender itself. ''' self.lcp.done=False self.lcp.answers=dict() self.lcp.context=dict() @@ -92,9 +112,6 @@ class LoncapaModule(XModule): self.lcp.answers=dict() # Student answers self.lcp.correct_map=dict() self.lcp.seed=None - # Minor cleanup would be nice - # We recreate the capa_problem on a reset filename=settings.DATA_DIR+self.filename+".xml" self.lcp=LoncapaProblem(filename, self.item_id, self.lcp.get_state()) -# self.lcp.__init__(filename, self.item_id, self.lcp.get_state()) - return json.dumps(self.get_html(encapsulate=False)) + return json.dumps(self.get_problem_html(encapsulate=False)) diff --git a/courseware/static/schematic.js b/courseware/static/schematic.js new file mode 100644 index 0000000000..486166b0fc --- /dev/null +++ b/courseware/static/schematic.js @@ -0,0 +1,1580 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Simple schematic capture +// +//////////////////////////////////////////////////////////////////////////////// + +// Chris Terman, Nov. 2011 + +// 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) + +// TO DO: + +// - read initial diagram from value of hidden input field +// - write diagram state into value of hidden input field +// - wire labels? +// - devices: diode, nfet, pfet, opamp, scope probe +// - icons for test equipment? (scope, sig gen, counter, ...) + +// - rotate multiple objects around their center of mass +// - rubber band wires when moving components +// - add help messages/tooltips +// - tool bar (zoom in/zoom out, rotate, mode: wire/select, save/restore, simulate, help) +// - scroll canvas +// - freeze_diagram, freeze_properties attributes (freeze certain components/properties?) +// - add thumb to resize work area + +// - label nodes, extract component netlist +// - simulation: operating points, trans, ac analysis, sweeps? +// - how to integrate plot display? + +// add ourselves to the tasks that get performed when window is loaded +window.onload = add_schematic_handler(window.onload); + +function add_schematic_handler(other_onload) { + return function() { + // execute othe onload functions first + if (other_onload) other_onload(); + + // set up each schematic on the page + var schematics = document.getElementsByClassName('schematic'); + for (var i = schematics.length - 1; i >= 0; i--) + new Schematic(schematics[i]); + } +} + +background_style = 'rgb(200,255,200)'; +element_style = 'rgb(255,255,255)'; + +// list of all the defined parts +parts_map = { + 'g': Ground, + 'v': VSource, + 'i': ISource, + 'r': Resistor, + 'c': Capacitor, + 'l': Inductor, +}; + +/////////////////////////////////////////////////////////////////////////////// +// +// Schematic = diagram + parts bin + status area +// +//////////////////////////////////////////////////////////////////////////////// + +// setup a schematic by populating the
with the appropriate children +function Schematic(input) { + var div = document.createElement('div'); + // set up div so we can position elements inside of it + div.style.position = 'relative'; + + // grab attributes from the
that created us + this.div = div; + this.grid = 8; + + this.scale = 2; + this.origin_x = 0; + this.origin_y = 0; + + // start with a background element with normal positioning + this.background = document.createElement('canvas'); + this.background.style.backgroundColor = background_style; + + this.status_div = document.createElement('div'); + this.status_div.style.borderStyle = 'solid'; + this.status_div.style.borderWidth = '1px'; + this.status_div.style.position = 'absolute'; + this.status_div.style.padding = '2px'; + this.status_div.style.backgroundColor = element_style; + this.status = document.createTextNode('Wow, schematic entry all in javascript!'); + this.status_div.appendChild(this.status); + + this.connection_points = new Array(); // location string => list of cp's + this.components = []; + + // this is where schematic is rendered + this.canvas = document.createElement('canvas'); + this.canvas.tabIndex = 1; // so we get keystrokes + this.canvas.style.borderStyle = 'solid'; + this.canvas.style.borderWidth = '1px'; + this.canvas.style.position = 'absolute'; + + this.canvas.schematic = this; + this.canvas.addEventListener('mousemove',this.mouse_move,false); + this.canvas.addEventListener('mouseover',this.mouse_enter,false); + this.canvas.addEventListener('mouseout',this.mouse_leave,false); + this.canvas.addEventListener('mousedown',this.mouse_down,false); + this.canvas.addEventListener('mouseup',this.mouse_up,false); + this.canvas.addEventListener('dblclick',this.double_click,false); + this.canvas.addEventListener('keydown',this.key_down,false); + this.canvas.addEventListener('keypress',this.key_press,false); + + // make the canvas "clickable" by registering a dummy click handler + // this should make things work on the iPad + this.canvas.addEventListener('click',function(){},false); + + this.dragging = false; + this.drawCursor = false; + this.cursor_x = 0; + this.cursor_y = 0; + this.draw_cursor = null; + this.select_rect = null; + this.wire = null; + + // repaint simply draws this buffer and then adds selected elements on top + this.bg_image = document.createElement('canvas'); + + // use user-supplied list of parts if supplied + // else just populate parts bin with all the parts + var parts = input.getAttribute('parts'); + if (parts) parts = parts.split(','); + else { + parts = new Array(); + for (var p in parts_map) parts.push(p); + } + + // now add the parts to the parts bin + var parts_left = this.width + 3 + background_margin; + var parts_top = background_margin; + this.parts_bin = []; + for (var i = 0; i < parts.length; i++) { + var part = new Part(this); + part.set_component(new parts_map[parts[i]](part,0,0,0)); + this.parts_bin.push(part); + } + + // add all elements to the DOM + div.appendChild(this.background); + div.appendChild(this.canvas); + div.appendChild(this.status_div); + for (var i = 0; i < this.parts_bin.length; i++) + div.appendChild(this.parts_bin[i].canvas); + input.parentNode.insertBefore(div,input.nextSibling); + + // make sure other code can find us! + input.schematic = this; + this.input = input; + + // set locations of all the elements in the editor + var w = parseInt(input.getAttribute('width')); + var h = parseInt(input.getAttribute('height')); + this.set_locations(w,h); +} + +background_margin = 5; +part_w = 56; // size of a parts bin compartment +part_h = 56; +status_height = 18; +thumb_style = 'rgb(128,128,128)'; + +// w,h are the dimensions of the canvas, everyone else is positioned accordingly +Schematic.prototype.set_locations = function(w,h) { + // limit the shrinkage factor + w = Math.max(w,120); + h = Math.max(h,120); + + this.width = w; + this.height = h; + this.bg_image.width = w; + this.bg_image.height = h; + + this.min_x = 0; + this.min_y = 0; + this.max_x = w/this.scale; + this.max_y = h/this.scale; + + // configure canvas + this.canvas.style.left = background_margin + 'px'; + this.canvas.style.top = background_margin + 'px'; + this.canvas.width = w; + this.canvas.height = h; + this.redraw_background(); // redraw diagram + + // configure status bar + this.status_div.style.left = background_margin + 'px'; + this.status_div.style.top = this.canvas.offsetTop + this.canvas.offsetHeight + 3 + 'px'; + this.status_div.style.width = (w - 4) + 'px'; // subtract interior padding + this.status_div.style.height = status_height + 'px'; + + // configure parts bin + var total_w = this.canvas.offsetLeft + this.canvas.offsetWidth; + var parts_left = total_w + 3; + var parts_top = background_margin; + var parts_h_limit = this.canvas.offsetTop + this.canvas.offsetHeight; + for (var i = 0; i < this.parts_bin.length; i++) { + var part = this.parts_bin[i]; + part.set_location(parts_left,parts_top); + + total_w = part.right(); + parts_top = part.bottom()-1; + if (parts_top + part_h > parts_h_limit) { + parts_left = total_w - 1; + parts_top = background_margin; + } + } + + // configure background + var total_h = this.status_div.offsetTop + this.status_div.offsetHeight + background_margin; + total_w += background_margin; + this.background.height = total_h; + this.background.width = total_w; + + /* enable when there's support for resizing schematic + // redraw thumb + var c = this.background.getContext('2d'); + c.clearRect(0,0,w,h); + c.strokeStyle = thumb_style; + c.lineWidth = 1; + c.beginPath(); + w = total_w - 1; + h = total_h - 1; + c.moveTo(w,h-4); c.lineTo(w-4,h); + c.moveTo(w,h-8); c.lineTo(w-8,h); + c.moveTo(w,h-12); c.lineTo(w-12,h); + c.stroke(); + */ +} + +// update the value field of our corresponding input field with JSON +// representation of schematic +Schematic.prototype.update_value = function() { + // to do: fill in this.input.value +} + +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); +} + +// add connection point to list of connection points at that location +Schematic.prototype.add_connection_point = function(cp) { + 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(this,x1,y1,x2,y2); + this.add_component(new_wire); + new_wire.move_end(); + return new_wire; +} + +// see if connection points of component c split any wires +Schematic.prototype.check_wires = function(c) { + for (var i = this.components.length - 1; i >=0; --i) { + var cc = this.components[i]; + if (cc != c) { // don't check a component against itself + // only wires will do return non-null from a bisect call + var cp = cc.bisect(c); + if (cp) { + // cc is a wire bisected by connection point cp + + // remove biscted wire + cc.delete(); + + // add two new wires with cp in the middle + this.add_wire(cc.x,cc.y,cp.x,cp.y); + this.add_wire(cc.x+cc.dx,cc.y+cc.dy,cp.x,cp.y); + this.redraw_background(); + break; + } + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// +// 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'); + var w = this.bg_image.width; + var h = this.bg_imageheight; + + // paint background color + c.fillStyle = element_style; + c.fillRect(0,0,this.width,this.height); + + // border + //c.strokeStyle = "rgb(0,0,0)"; + //c.strokeRect(0,0,this.width,this.height); + + // grid + c.strokeStyle = "rgb(128,128,128)"; + var first_x = this.min_x; + var last_x = this.max_x; + var first_y = this.min_y; + var last_y = this.max_y; + for (var i = first_x; i < last_x; i += this.grid) + this.draw_line(c,i,first_y,i,last_y,0.1); + for (var i = first_y; i < last_y; i += this.grid) + this.draw_line(c,first_x,i,last_x,i,0.1); + + // unselected components + for (var i = this.components.length - 1; i >= 0; --i) { + var component = this.components[i]; + if (!component.selected) component.draw(c); + } + + 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 + for (var i = this.components.length - 1; i >= 0; --i) { + var component = this.components[i]; + if (component.selected) component.draw(c); + } + + // 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(); + } + + // finally overlay cursor + if (this.drawCursor && this.draw_cursor) { + //var x = this.cursor_x; + //var y = this.cursor_y; + //this.draw_text(c,'('+x+','+y+')',x+this.grid,y-this.grid,10); + this.draw_cursor(c,this.cursor_x,this.cursor_y); + } +} + +// 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.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 canvasY = 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; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Event handling +// +//////////////////////////////////////////////////////////////////////////////// + +// process special keys here since they don't get delivered correctly on keypress +Schematic.prototype.key_down = function(event) { + if (!event) event = window.event; + var sch = (window.event) ? event.srcElement.schematic : event.target.schematic; + var code = event.keyCode; + + 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.delete(1); + } + sch.redraw(); + event.preventDefault(); + return false; + } + return true; +} + +// process normal characters +Schematic.prototype.key_press = function(event) { + if (!event) event = window.event; + var sch = (window.event) ? event.srcElement.schematic : event.target.schematic; + var code = window.event ? event.keyCode : event.charCode; + var char = String.fromCharCode(code); + + if (char == 'r' || char == 'R') { + // rotate + for (var i = sch.components.length - 1; i >= 0; --i) { + var component = sch.components[i]; + if (component.selected) component.rotate(1); + } + sch.redraw(); + event.preventDefault(); + return false; + } + return true; +} + +Schematic.prototype.mouse_enter = function(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 = null; + part.select(false); + + // make a clone of the component in the parts bin + part = part.component.clone(sch,sch.cursor_x,sch.cursor_y); + + // 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 + sch.add_component(part); + part.set_select(true); + + // and start dragging it + sch.drag_begin(); + } + + sch.drawCursor = true; + sch.redraw(); + sch.canvas.focus(); // capture key strokes +} + +Schematic.prototype.mouse_leave = function(event) { + if (!event) event = window.event; + var sch = (window.event) ? event.srcElement.schematic : event.target.schematic; + sch.drawCursor = false; + sch.redraw(); +} + +Schematic.prototype.unselect_all = function(which) { + for (var i = this.components.length - 1; i >= 0; --i) + if (i != which) this.components[i].set_select(false); +} + +Schematic.prototype.mouse_down = function(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; + + // 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(); +} + +Schematic.prototype.mouse_up = function(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 = null; + + if (r[0]!=r[2] || r[1]!=r[3]) { + // insert wire component + sch.add_wire(r[0],r[1],r[2],r[3]); + 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 = null; + sch.redraw_background(); + } +} + +Schematic.prototype.double_click = function(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; +} + +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; +} + +Schematic.prototype.mouse_move = function(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(); +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Status message and dialogs +// +//////////////////////////////////////////////////////////////////////////////// + +Schematic.prototype.message = function(message) { + this.status.nodeValue = message; + this.recompute_height(); +} + +Schematic.prototype.append_message = function(message) { + this.status.nodeValue += ' / '+message; + this.recompute_height(); +} + +// 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; + + // 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)); + dialog.appendChild(head); + + // div to hold the content + var body = document.createElement('div'); + 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.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.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.appendChild(ok_button); + buttons.appendChild(cancel_button); + buttons.style.padding = '5px'; + buttons.style.margin = '10px'; + dialog.appendChild(buttons); + + // add to DOM + dialog.style.background = 'white'; + dialog.style.zindex = '1000'; + dialog.style.position = 'absolute'; + dialog.style.left = this.canvas.mouse_x+'px'; + dialog.style.top = this.canvas.mouse_y+'px'; + dialog.style.border = '2px solid'; + dialog.callback = callback; + this.div.appendChild(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; + + // remove the dialog from the top-level div of the schematic + dialog.parentNode.removeChild(dialog); +} + +// 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; + + // remove the dialog from the top-level div of the schematic + dialog.parentNode.removeChild(dialog); + + // invoke the callback with the dialog contents as the argument + if (dialog.callback) dialog.callback(dialog.content); +} + +/////////////////////////////////////////////////////////////////////////////// +// +// Parts bin +// +//////////////////////////////////////////////////////////////////////////////// + +// one instance will be created for each part in the parts bin +function Part(sch) { + this.sch = sch; + this.component = null; + this.selected = false; + + // set up canvas + this.canvas = document.createElement('canvas'); + this.canvas.style.borderStyle = 'solid'; + this.canvas.style.borderWidth = '1px'; + this.canvas.style.position = 'absolute'; + this.canvas.height = part_w; + this.canvas.width = part_h; + this.canvas.part = this; + + this.canvas.addEventListener('mousedown',this.mouse_down,false); + this.canvas.addEventListener('mouseup',this.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) { + this.component = component; + + // 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 = 1; //Math.min(part_w/(1.2*dx),part_h/(1.2*dy)); + this.origin_x = this.scale*(b[0] + dx/2.0) - part_w/2.0; + this.origin_y = this.scale*(b[1] + dy/2.0) - part_h/2.0; + + this.redraw(); +} + +Part.prototype.redraw = function(part) { + var c = this.canvas.getContext('2d'); + + // paint background color + c.fillStyle = this.selected ? selected_style : element_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.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 +} + +Part.prototype.mouse_down = function(event) { + if (!event) event = window.event; + var part = (window.event) ? event.srcElement.part : event.target.part; + + part.select(true); + part.sch.new_part = part; +} + +Part.prototype.mouse_up = function(event) { + if (!event) event = window.event; + var part = (window.event) ? event.srcElement.part : event.target.part; + + part.select(false); + part.sch.new_part = null; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// 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 +// +//////////////////////////////////////////////////////////////////////////////// + +property_size = 5; // point size for Component property text +normal_style = 'rgb(0,0,0)'; // color for unselected components +selected_style = 'rgb(64,255,64)'; // highlight color for selected components + +function Component(sch,x,y,rotation) { + this.sch = sch; + 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.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.delete = 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); + + // 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.draw_line = function(c,x1,y1,x2,y2) { + c.strokeStyle = this.selected ? selected_style : normal_style; + var nx1 = this.transform_x(x1,y1) + this.x; + 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 : normal_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 : normal_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) { +} + +// 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) { + var a = aOrient[this.rotation*9 + alignment]; + c.textAlign = textAlign[a]; + c.textBaseline = textBaseline[a]; + c.fillStyle = this.selected ? selected_style : normal_style; + 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 (inside(this.bbox,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; +} + +Component.prototype.edit_properties = function(x,y) { + if (inside(this.bbox,x,y)) { + var content = document.createElement('table'); + content.style.marginBotton = '5px'; + content.fields = []; + + // add an field for each property + for (var i in this.properties) { + var label = document.createTextNode(i + ': '); + var field = document.createElement('input'); + field.type = 'text'; + field.value = this.properties[i]; + field.size = 10; + content.fields.push([i,field]); + + var col1 = document.createElement('td'); + col1.appendChild(label); + var col2 = document.createElement('td'); + col2.appendChild(field); + var row = document.createElement('tr'); + row.appendChild(col1); + row.appendChild(col2); + row.style.verticalAlign = 'center'; + + content.appendChild(row); + } + + var component = this; // capture in closure below + this.sch.dialog('Edit Properties',content,function(content) { + var fields = content.fields; + for (var i = fields.length - 1; i >= 0; i--) + component.properties[fields[i][0]] = fields[i][1].value; + component.sch.redraw(); // component is selected, so this will redraw it + }); + return true; + } else return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// 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(); +} + +ConnectionPoint.prototype.toString = function() { + return ''; +} + +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 + 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); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Wire +// +//////////////////////////////////////////////////////////////////////////////// + +near_distance = 2; // how close to wire counts as "near by" + +function Wire(sch,x1,y1,x2,y2) { + // arbitrarily call x1,y1 the origin + Component.call(this,sch,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 ''; +} + +Wire.prototype.draw = function(c) { + this.draw_line(c,0,0,this.dx,this.dy); +} + +Wire.prototype.clone = function(sch,x,y) { + return new Wire(sch,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; +} + +Wire.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; +} + +// 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 of component c bisects the +// wire represented by this compononent, return that +// connection point. Otherwise return null. +Wire.prototype.bisect = function(c) { + for (var i = c.connections.length - 1; i >= 0; --i) { + var cp = c.connections[i]; + 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 cp; + } + } + return null; +} + +Wire.prototype.move_end = function() { + this.sch.check_wires(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Ground +// +//////////////////////////////////////////////////////////////////////////////// + +function Ground(sch,x,y,rotation) { + Component.call(this,sch,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) { + this.draw_line(c,0,0,0,8); + this.draw_line(c,-6,8,6,8); +} + +Ground.prototype.clone = function(sch,x,y) { + return new Ground(sch,x,y,this.rotation); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Resistor +// +//////////////////////////////////////////////////////////////////////////////// + +function Resistor(sch,x,y,rotation,name,r) { + Component.call(this,sch,x,y,rotation); + this.properties['name'] = name; + this.properties['r'] = r ? r : '1'; + this.add_connection(0,0); + this.add_connection(0,48); + this.bounding_box = [-4,0,4,48]; + this.update_coords(); +} +Resistor.prototype = new Component(); +Resistor.prototype.constructor = Resistor; + +Resistor.prototype.toString = function() { + return ''; +} + +Resistor.prototype.draw = function(c) { + this.draw_line(c,0,0,0,12); + this.draw_line(c,0,12,4,14); + this.draw_line(c,4,14,-4,18); + this.draw_line(c,-4,18,4,22); + this.draw_line(c,4,22,-4,26); + this.draw_line(c,-4,26,4,30); + this.draw_line(c,4,30,-4,34); + this.draw_line(c,-4,34,0,36); + this.draw_line(c,0,36,0,48); + if (this.properties['r']) + this.draw_text(c,this.properties['r']+'\u03A9',5,24,3,property_size); + if (this.properties['name']) + this.draw_text(c,this.properties['name'],-5,24,5,property_size); +} + +Resistor.prototype.clone = function(sch,x,y) { + return new Resistor(sch,x,y,this.rotation,'',this.properties['r']); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Capacitor +// +//////////////////////////////////////////////////////////////////////////////// + +function Capacitor(sch,x,y,rotation,name,c) { + Component.call(this,sch,x,y,rotation); + this.properties['name'] = name; + this.properties['c'] = c ? c : '1p'; + this.add_connection(0,0); + this.add_connection(0,48); + this.bounding_box = [-8,0,8,48]; + this.update_coords(); +} +Capacitor.prototype = new Component(); +Capacitor.prototype.constructor = Capacitor; + +Capacitor.prototype.toString = function() { + return ''; +} + +Capacitor.prototype.draw = function(c) { + this.draw_line(c,0,0,0,22); + this.draw_line(c,-8,22,8,22); + this.draw_line(c,-8,26,8,26); + this.draw_line(c,0,26,0,48); + if (this.properties['c']) + this.draw_text(c,this.properties['c']+'F',9,24,3,property_size); + if (this.properties['name']) + this.draw_text(c,this.properties['name'],-9,24,5,property_size); +} + +Capacitor.prototype.clone = function(sch,x,y) { + return new Capacitor(sch,x,y,this.rotation,'',this.properties['c']); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Inductor +// +//////////////////////////////////////////////////////////////////////////////// + +function Inductor(sch,x,y,rotation,name,l) { + Component.call(this,sch,x,y,rotation); + this.properties['name'] = name; + this.properties['l'] = l ? l : '1n'; + this.add_connection(0,0); + this.add_connection(0,48); + this.bounding_box = [-4,0,5,48]; + this.update_coords(); +} +Inductor.prototype = new Component(); +Inductor.prototype.constructor = Inductor; + +Inductor.prototype.toString = function() { + return ''; +} + +Inductor.prototype.draw = function(c) { + this.draw_line(c,0,0,0,14); + this.draw_arc(c,0,18,4,6*Math.PI/4,3*Math.PI/4); + this.draw_arc(c,0,24,4,5*Math.PI/4,3*Math.PI/4); + this.draw_arc(c,0,30,4,5*Math.PI/4,2*Math.PI/4); + this.draw_line(c,0,34,0,48); + + if (this.properties['l']) + this.draw_text(c,this.properties['l']+'H',6,24,3,property_size); + if (this.properties['name']) + this.draw_text(c,this.properties['name'],-3,24,5,property_size); +} + +Inductor.prototype.clone = function(sch,x,y) { + return new Inductor(sch,x,y,this.rotation,'',this.properties['l']); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Source +// +//////////////////////////////////////////////////////////////////////////////// + +function Source(sch,x,y,rotation,name,type,value) { + Component.call(this,sch,x,y,rotation); + this.type = type; + this.properties['name'] = name; + this.properties['value'] = value ? value : '1'; + this.add_connection(0,0); + this.add_connection(0,48); + this.bounding_box = [-12,0,12,48]; + this.update_coords(); +} +Source.prototype = new Component(); +Source.prototype.constructor = Source; + +Source.prototype.toString = function() { + return '<'+this.type+'source '+this.properties['params']+' ('+this.x+','+this.y+')>'; +} + +Source.prototype.draw = function(c) { + this.draw_line(c,0,0,0,12); + this.draw_circle(c,0,24,12,false); + this.draw_line(c,0,36,0,48); + + if (this.type == 'v') { // voltage source + // draw + and - + this.draw_line(c,8,5,8,11); + this.draw_line(c,5,8,11,8); + this.draw_line(c,5,40,11,40); + // draw V + this.draw_line(c,-3,20,0,28); + this.draw_line(c,3,20,0,28); + } else if (this.type == 'i') { // current source + // draw arrow: pos to neg + this.draw_line(c,0,16,0,32); + this.draw_line(c,-3,24,0,32); + this.draw_line(c,3,24,0,32); + } + + if (this.properties['name']) + this.draw_text(c,this.properties['name'],-13,24,5,property_size); + if (this.properties['value']) + this.draw_text(c,this.properties['value']+(this.type=='v'?'V':'A'),13,24,3,property_size); +} + +Source.prototype.clone = function(sch,x,y) { + return new Source(sch,x,y,this.rotation,'',this.type,this.properties['value']); +} + +function VSource(sch,x,y,rotation,name,value) { + Source.call(this,sch,x,y,rotation,name,'v',value); + +} +VSource.prototype = new Component(); +VSource.prototype.constructor = VSource; +VSource.prototype.toString = Source.prototype.toString; +VSource.prototype.draw = Source.prototype.draw; +VSource.prototype.clone = Source.prototype.clone; + +function ISource(sch,x,y,rotation,name,value) { + Source.call(this,sch,x,y,rotation,name,'i',value); + +} +ISource.prototype = new Component(); +ISource.prototype.constructor = ISource; +ISource.prototype.toString = Source.prototype.toString; +ISource.prototype.draw = Source.prototype.draw; +ISource.prototype.clone = Source.prototype.clone; diff --git a/courseware/views.py b/courseware/views.py index 3434a21037..0712407d4b 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -188,7 +188,7 @@ def render_x_module(request, xml_module): return content def modx_dispatch(request, module=None, dispatch=None, id=None): - ''' Generic module for extensions. This handles AJAX. ''' + ''' Generic module for extensions. ''' s = StudentModule.objects.filter(module_type=module, student=request.user, module_id=id) if len(s) == 0: raise Http404 @@ -229,7 +229,8 @@ def index(request, course="6.002 Spring 2012", chapter="Using the System", secti if not request.user.is_authenticated(): return redirect('/') - # Fix URLs + # Fixes URLs -- we don't get funny encoding characters from spaces + # so they remain readable course=course.replace("_"," ") chapter=chapter.replace("_"," ") section=section.replace("_"," ")