import os import sys import Image import ImageDraw import ImageFont import xml.dom.minidom import re import math import decimal #--------------------------------------------------------------------------------------- HOLE_ABSENT = 0 HOLE_EMPTY = 1 HOLE_SOLDERED = 2 RGB_DEFAULT = (0xff, 0xff, 0xff) RGB_OUTLINE = (0x00, 0x10, 0xff) RGB_TRACE = ( 243, 215, 114) RGB_TEXT = (0x00, 0x00, 0x00) RGB_PUNCHOUT = (0x9f, 0x9f, 0x9f) RGB_WIRE = (0xff, 0x00, 0x00) RGB_Hole = ( None, # place-holder for HOLE_ABSENT #---------------------------------------------------------------------- # fill outline # ------------------ -------------------- ( (0xc0, 0xc0, 0xc0), (0xa0, 0xa0, 0xa0) ), # empty hole ( ( 72, 90, 94), ( 142, 2, 2) ) # soldered hole ) PUNCHOUT_RADIUS = 9 HOLE_RADIUS = 4 TRACE_DIST = 6 PIXELS_PER_UNIT = 17 #--------------------------------------------------------------------------------------- ResistorColorTable = [ "black", "brown", "red", "orange", "yellow", "green", "blue", "violet", "grey", "white" ] def ResistorColorCode (digit): digit = int(digit) if digit < 0 or digit > 9: raise CircuitException ("Invalid decimal digit '{0}' - cannot convert to resistor color code.".format(digit)) return ResistorColorTable[digit] #--------------------------------------------------------------------------------------- def ParseRGB (s): if s == "": return None elif re.match (r"^\s*\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)\s*$",s): return eval(s) else: raise CircuitException ("Invalid RGB expression '{0}'".format(s)) #--------------------------------------------------------------------------------------- class CircuitException(Exception): def __init__ (self, message): self.textMessage = message def __str__ (self): return repr (self.textMessage) #--------------------------------------------------------------------------------------- class PixelTransform: def __init__ (self, px0, py0, pdx, pdy): self.px0 = px0 self.py0 = py0 self.pdx = pdx self.pdy = pdy def pixelx (self, x): return self.px0 + (x * self.pdx) def pixely (self, y): return self.py0 + (y * self.pdy) def pixel (self, x, y): return (self.pixelx(x), self.pixely(y)) def pixelpair (self, xy): x, y = xy return self.pixel (x, y) #--------------------------------------------------------------------------------------- def FatLine (picture, path, fillColor): path2 = [(x+1,y) for x,y in path] path3 = [(x,y+1) for x,y in path] picture.line (path, fill=fillColor) picture.line (path2, fill=fillColor) picture.line (path3, fill=fillColor) #--------------------------------------------------------------------------------------- class TraceArray: def __init__ (self, element): self.type = "trace array" self.arrayWidth = int (element.attributes["arrayWidth" ].value) self.arrayHeight = int (element.attributes["arrayHeight" ].value) self.traceWidth = int (element.attributes["traceWidth" ].value) self.traceHeight = int (element.attributes["traceHeight" ].value) self.xInterval = int (element.attributes["xInterval" ].value) self.yInterval = int (element.attributes["yInterval" ].value) self.xOrigin = int (element.attributes["xOrigin" ].value) self.yOrigin = int (element.attributes["yOrigin" ].value) class GapArray: def __init__ (self, element): self.type = "gap array" self.arrayWidth = int (element.attributes["arrayWidth" ].value) self.arrayHeight = int (element.attributes["arrayHeight" ].value) self.gapWidth = int (element.attributes["gapWidth" ].value) self.gapHeight = int (element.attributes["gapHeight" ].value) self.xInterval = int (element.attributes["xInterval" ].value) self.yInterval = int (element.attributes["yInterval" ].value) self.xOrigin = int (element.attributes["xOrigin" ].value) self.yOrigin = int (element.attributes["yOrigin" ].value) #--------------------------------------------------------------------------------------- class Grid: def __init__ (self, element, board): self.type = "grid" self.board = board self.traceArrays = [] self.gapArrays = [] self.name = element.attributes["name"].value self.width = int (element.attributes["width"].value) self.height = int (element.attributes["height"].value) self.left = int (element.attributes["left"].value) self.top = int (element.attributes["top"].value) if self.width <= 0 or self.height <= 0: raise CircuitException ("Grid must have positive integer width and height.") for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "TraceArray": ta = TraceArray (child) self.traceArrays.append (ta) elif child.localName == "GapArray": ga = GapArray (child) self.gapArrays.append (ga) # Remember which holes exist and which don't (because of being in a GapArray)... self.holeState = [] for x in range (self.width): self.holeState.append ([]) for y in range (self.height): self.holeState[x].append (HOLE_EMPTY) for ga in self.gapArrays: for ax in range(ga.arrayWidth): x1 = ga.xOrigin + (ga.xInterval * ax) for x in range (x1, x1 + ga.gapWidth): for ay in range(ga.arrayHeight): y1 = ga.yOrigin + (ga.yInterval * ay) for y in range (y1, y1 + ga.gapHeight): self.holeState[x][y] = HOLE_ABSENT def DrawFoilSide (self, picture, transform): # The order of drawing things is important: # Whatever gets drawn for a particular pixel last is what remains. self.DrawTraceArrays (picture, transform) self.DrawHoles (picture, transform) def DrawFaceSide (self, picture, transform): self.DrawHoles (picture, transform) def DrawTraceArrays (self, picture, transform): for ta in self.traceArrays: for ax in range(ta.arrayWidth): x1 = self.left + ta.xOrigin + (ta.xInterval * ax) x2 = x1 + ta.traceWidth - 1 px1 = transform.pixelx(x1) - TRACE_DIST px2 = transform.pixely(x2) + TRACE_DIST for ay in range(ta.arrayHeight): y1 = self.top + ta.yOrigin + (ta.yInterval * ay) y2 = y1 + ta.traceHeight - 1 py1 = transform.pixely(y1) - TRACE_DIST py2 = transform.pixely(y2) + TRACE_DIST picture.rectangle ([(px1,py1), (px2,py2)], outline=RGB_TRACE, fill=RGB_TRACE) def DrawHoles (self, picture, transform): # Draw holes, but omit any holes that are in a contained gap array... for x in range (self.width): for y in range (self.height): self.DrawHole (picture, x, y, transform) def DrawHole (self, picture, x, y, transform): state = self.holeState[x][y] if state != HOLE_ABSENT: fillColor, outlineColor = RGB_Hole[state] px, py = transform.pixel (x + self.left, y + self.top) picture.ellipse ([(px-HOLE_RADIUS, py-HOLE_RADIUS), (px+HOLE_RADIUS, py+HOLE_RADIUS)], fill=fillColor, outline=outlineColor) def AddSolderJoint (self, x, y): if self.holeState[x][y] == HOLE_EMPTY: self.holeState[x][y] = HOLE_SOLDERED else: description = ("absent", None, "already soldered") [self.holeState[x][y]] raise CircuitException ("Cannot solder hole at x={0}, y={1} in grid '{2}', because it is {3}.".format (x, y, self.name, description)) #--------------------------------------------------------------------------------------- class CircuitBoard: def __init__ (self, element): self.width = int (element.attributes["width"].value) self.height = int (element.attributes["height"].value) self.grids = {} self.wires = [] self.components = {} for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "Grid": self.AddGrid (Grid (child, self)) def AddGrid (self, g): self.CheckBounds (g) if g.name in self.grids: raise CircuitException ("Attempt to define more than one grid with the name '{0}'".format(g.name)) self.grids[g.name] = g def CreateImage (self): # We want to draw both the top (blank) side and the bottom (foil) side. # Orient the image based on the width and height of the foil side if self.width > self.height: # board is wider than it is tall, so stack the images vertically... pixelsWide = 2 + (PIXELS_PER_UNIT * self.width) pixelsHigh = 2 + (PIXELS_PER_UNIT * ((2 * self.height) + 1)) foilTransform = PixelTransform (1, 1, PIXELS_PER_UNIT, PIXELS_PER_UNIT) faceTransform = PixelTransform (1 + (PIXELS_PER_UNIT * self.width), 1 + PIXELS_PER_UNIT * (self.height + 1), -PIXELS_PER_UNIT, PIXELS_PER_UNIT) else: # board is taller than it is wide, so stack the images side by side... pixelsWide = 2 + (PIXELS_PER_UNIT * ((2 * self.width) + 1)) pixelsHigh = 2 + (PIXELS_PER_UNIT * self.height) foilTransform = PixelTransform (1, 1, PIXELS_PER_UNIT, PIXELS_PER_UNIT) faceTransform = PixelTransform (pixelsWide-1, 1, -PIXELS_PER_UNIT, PIXELS_PER_UNIT) image = Image.new ("RGB", (pixelsWide,pixelsHigh), RGB_DEFAULT) picture = ImageDraw.Draw (image) self.DrawFoilSide (picture, foilTransform) self.DrawFaceSide (picture, faceTransform) return image def CheckBounds (self, x): badSides = [] if not (0 < x.left < self.width): badSides.append ("left"); if not (0 < x.top < self.height): badSides.append ("top"); right = x.left + x.width - 1 if not (0 < right < self.width): badSides.append ("right"); bottom = x.top + x.height - 1 if not (0 < bottom < self.height): badSides.append ("bottom"); if len(badSides) > 0: if len(badSides) > 2: badSides[-1] = "and " + badSides[-1] sidesText = ", ".join (badSides).capitalize() plural = "s" elif len(badSides) == 2: sidesText = " and ".join(badSides).capitalize() plural = "s" else: # len(badSides) == 1, by process of elimination sidesText = badSides[0].capitalize() plural = "" pattern = "{0} side{1} of {2} must be inside the enclosing board." raise CircuitException (pattern.format(sidesText, plural, x.type)) def DrawFoilSide (self, picture, transform): self.DrawBorder (picture, transform) for g in self.grids.values(): g.DrawFoilSide (picture, transform) def DrawFaceSide (self, picture, transform): self.DrawBorder (picture, transform) for g in self.grids.values(): g.DrawFaceSide (picture, transform) for c in self.components.values(): c.DrawPins (picture, transform, self) c.DrawPackage (picture, transform) for w in self.wires: # Make a path consisting of the first solder connection, all the waypoints, then the last solder connection... path = w.BoardPath (self.grids, transform) FatLine (picture, path, RGB_WIRE) def DrawBorder (self, picture, transform): px1, py1 = transform.pixel (0, 0) px2, py2 = transform.pixel (self.width, self.height) picture.rectangle ([(px1,py1), (px2,py2)], fill=None, outline=RGB_OUTLINE) def AddSolderJoint (self, joint): self.grids[joint.gridName].AddSolderJoint (joint.x, joint.y) def InstallWire (self, wire): for s in wire.solderConnections: if s.gridName not in self.grids: raise CircuitException ("Undefined grid name '{0}' referenced by wire solder connection.".format (s.gridName)) s.grid = self.grids [s.gridName] s.grid.AddSolderJoint (s.x, s.y) for p in wire.waypoints: if p.gridName not in self.grids: raise CircuitException ("Undefined grid name '{0}' referenced by wire waypoint.".format (s.gridName)) p.grid = self.grids[p.gridName] self.wires.append (wire) def AddComponent (self, component): # Hook up all of the pins in the correct grid. # Note that pins may be in different grids, as in a resistor spanning two grids. for s in component.solderConnections.values(): if not s.gridName in self.grids: raise CircuitException ("Undefined grid name '{0}' referenced by solder pin '{1}' in component '{2}'.".format (s.gridName, s.pinIndex, component.id)) s.grid = self.grids [s.gridName] s.grid.AddSolderJoint (s.x, s.y) # Point each component to its grid so it knows what coordinates to use to draw its package... if component.gridName not in self.grids: raise CircuitException ("Undefined grid name '{0}' referenced by component '{1}'.".format (component.gridName, component.id)) component.grid = self.grids [component.gridName] # Remember the component using its unique identifier... if component.id in self.components: raise CircuitException ("Attempt to define component with ID '{0}' more than once.".format (component.id)) self.components [component.id] = component #--------------------------------------------------------------------------------------- class SolderJoint: def __init__ (self, element): self.gridName = element.attributes["grid"].value self.x = int (element.attributes["x"].value) self.y = int (element.attributes["y"].value) #--------------------------------------------------------------------------------------- class HalfDisc: def __init__ (self, element): self.radius = float (element.attributes["radius"].value) self.angle = float (element.attributes["angle"].value) self.dx = float (element.attributes["dx"].value) self.dy = float (element.attributes["dy"].value) self.fill = ParseRGB (element.attributes["fill"].value) self.outline = ParseRGB (element.attributes["outline"].value) def Draw (self, picture, transform, component): cx = component.cx() cy = component.cy() rotation = component.angle scale = component.scale bbx1, bby1 = transform.pixel (cx + scale*(self.dx - self.radius), cy + scale*(self.dy - self.radius)) bbx2, bby2 = transform.pixel (cx + scale*(self.dx + self.radius), cy + scale*(self.dy + self.radius)) bbx1 = int(round(bbx1)) bbx2 = int(round(bbx2)) bby1 = int(round(bby1)) bby2 = int(round(bby2)) if bbx1 > bbx2: bbx1, bbx2 = bbx2, bbx1 if bby1 > bby2: bby1, bby2 = bby2, bby1 angle2 = int(round(self.angle + rotation)) angle1 = angle2 + 180 picture.chord ((bbx1,bby1,bbx2+1,bby2+1), angle1, angle2, fill=self.fill, outline=self.outline) #--------------------------------------------------------------------------------------- class Ellipse: def __init__ (self, element): boxText = element.attributes["box"].value floatRegex = r"[+\-]?\d+(\.\d+)?" boxRegex = r"^\s*\(\s*{0}\s*,\s*{0}\s*,\s*{0}\s*,\s*{0}\s*\)\s*$".format (floatRegex) if not re.match (boxRegex, boxText): raise CircuitException ("Invalid box attribute syntax in '{0}'".format (boxText)) x1, y1, x2, y2 = eval (boxText) self.box = [(x1,y1), (x2,y2)] self.fill = ParseRGB (element.attributes["fill"].value) self.outline = ParseRGB (element.attributes["outline"].value) def Draw (self, picture, transform, component): cx = component.cx() cy = component.cy() rotation = component.angle scale = component.scale rotatedAndScaledBox = [transform.pixelpair(ShiftCoordinates(RotateCoordinates(scale*x, scale*y, rotation), (cx, cy))) for x, y in self.box] fixedBox = FixEllipseBox (rotatedAndScaledBox) picture.ellipse (fixedBox, fill=self.fill, outline=self.outline) #--------------------------------------------------------------------------------------- class Polygon: def __init__ (self, element): self.vertexList = [] self.fillIndex, self.fill = self.ParseColor (element.attributes["fill"].value) self.outlineIndex, self.outline = self.ParseColor (element.attributes["outline"].value) for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "vertex": x = float (child.attributes["x"].value) y = float (child.attributes["y"].value) self.vertexList.append ((x,y)) else: raise CircuitException ("Unknown element '{0}' in polygon.".format (child.localName)) def ParseColor (self, text): if text.startswith("@"): return int(text[1:]), None else: return None, ParseRGB(text) def Draw (self, picture, transform, component): cx = component.cx() cy = component.cy() rotation = component.angle scale = component.scale colorCodeArray = component.colorCodeArray rotatedList = [RotateCoordinates(scale*x, scale*y, rotation) for x, y in self.vertexList] path = [transform.pixel(cx+rx, cy+ry) for rx, ry in rotatedList] fill = self.LookupColor (colorCodeArray, self.fillIndex, self.fill ) outline = self.LookupColor (colorCodeArray, self.outlineIndex, self.outline) picture.polygon (path, fill=fill, outline=outline) def LookupColor (self, colorCodeArray, index, color): if index is None: return color else: return ResistorColorCode (colorCodeArray[index]) #--------------------------------------------------------------------------------------- class PackagePin: def __init__ (self, element): self.index = int (element.attributes["index"].value) self.x = float (element.attributes["x"].value) self.y = float (element.attributes["y"].value) #--------------------------------------------------------------------------------------- def RotateCoordinates (x, y, angle): radians = math.radians (angle) c = math.cos (radians) s = math.sin (radians) z = complex(x,y) * complex(c,s) return z.real, z.imag def ShiftCoordinates (a, b): x1, y1 = a x2, y2 = b return x1+x2, y1+y2 def FixEllipseBox (box): # Fix quirks with ImageDraw ellipse... # First of all, the x- and y-coordinates must be sorted, or nothing is drawn... [(x1, y1), (x2, y2)] = box if x1 > x2: x1, x2 = x2, x1 if y1 > y2: y1, y2 = y2, y1 # Secondly, the first pair is inclusive, but the second one is exclusive. # Add 1 to x2 and y2 to draw the proper ellipse as we intended. return [(x1, y1), (x2+1, y2+1)] #--------------------------------------------------------------------------------------- class PackageType: def __init__ (self, element): self.shapes = [] self.pins = {} self.name = element.attributes["name"].value self.numPins = int (element.attributes["pins"].value) if self.numPins < 1: raise CircuitException ("Invalid number of pins '{0}' in package '{1}'".format(self.numPins, self.name)) self.valueType = element.attributes["valueType"].value if "colorCodeExponent" in element.attributes.keys(): self.colorCodeExponent = int (element.attributes["colorCodeExponent"].value) else: self.colorCodeExponent = None for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "halfdisc": self.shapes.append (HalfDisc (child)) elif child.localName == "polygon": self.shapes.append (Polygon (child)) elif child.localName == "ellipse": self.shapes.append (Ellipse (child)) elif child.localName == "pin": pin = PackagePin (child) if not (0 <= pin.index < self.numPins): raise CircuitException ("Pin in package '{0}' has invalid index {1}.".format (self.name, pin.index)) if pin.index in self.pins: raise CircuitException ("More than one pin in package '{0}' has index {1}.".format (self.name, pin.index)) self.pins[pin.index] = pin else: raise CircuitException ("Unknown child node '{0}' in PackageType element.".format (child.localName)) # Make sure all the required pins are there for i in range (self.numPins): if i not in self.pins: raise CircuitException ("Missing pin index {0} in package '{1}'.".format (i, self.name)) def Draw (self, picture, transform, component): if self.valueType == "numeric": mantissa, exponent, unit = self.ParseNumeric (component.value) if (self.colorCodeExponent is not None) and (component.colorCodeArray is None): if not (0 <= exponent <= 9): raise CircuitException ("Exponent is out of range in value '{0}'".format (component.value)) mstr = str(mantissa) if len(mstr) < 2: raise CircuitException ("Internal error: mantissa '{0}' is too short: extracted from value '{1}'".format (mstr, component.value)) estr = str(exponent) # ... color codes for resistor are : mstr[0], mstr[1], estr[0]. component.colorCodeArray = [ int(mstr[0]), int(mstr[1]), int(estr[0]) ] for s in self.shapes: s.Draw (picture, transform, component) def PinCoordinates (self, index, cx, cy, rotation, scale): pin = self.pins[index] x, y = RotateCoordinates (pin.x, pin.y, rotation) return cx + scale*x, cy + scale*y def ParseNumeric (self, value): m = re.match (r"^(\d+(\.\d+)?)([MKmunp]?)([FHRV]?)", value) if m: mantissa = decimal.Decimal (m.group(1)) if mantissa <= 0: raise CircuitException ("Nonpositive mantissa not allowed in '{0}'".format (value)) exponent = {'':0, 'M':6, 'K':3, 'm':-3, 'u':-6, 'n':-9, 'p':-12} [m.group(3)] # Resistors are based on unit ohms, but inductors are based on microhenries. # So for a resistor, self.colorCodeExponent == 0, # but for an inductor, self.colorCodeExponent == -6. if self.colorCodeExponent is not None: exponent -= self.colorCodeExponent unit = m.group(4) if unit == "": unit = "R" # Normalize so that color codes make sense. # The mantissa must be a value between 10 and 99... while mantissa >= 100: mantissa /= 10 exponent += 1 while mantissa < 10: mantissa *= 10 exponent -= 1 return mantissa, exponent, unit else: raise CircuitException ("Invalid numeric component value '{0}'".format (value)) #--------------------------------------------------------------------------------------- class SolderConnection: def __init__ (self, element): if "pin" in element.attributes.keys(): self.pinIndex = int (element.attributes["pin"].value) else: self.pinIndex = -1 # Used for wires, which don't have packages or pins self.gridName = element.attributes["grid"].value self.x = int (element.attributes["x"].value) self.y = int (element.attributes["y"].value) def BoardCoordinates (self, grids): grid = grids[self.gridName] return (self.x + grid.left, self.y + grid.top) #--------------------------------------------------------------------------------------- class Waypoint: def __init__ (self, element): self.gridName = element.attributes["grid"].value self.x = float (element.attributes["x"].value) self.y = float (element.attributes["y"].value) self.grid = None def BoardCoordinates (self, grids): grid = grids[self.gridName] return (self.x + grid.left, self.y + grid.top) #--------------------------------------------------------------------------------------- class Component: def __init__ (self, element): self.package = None self.grid = None self.colorCodeArray = None self.packageName = element.attributes["package"].value self.id = element.attributes["id"].value self.value = element.attributes["value"].value self.gridName = element.attributes["grid"].value self.x = float (element.attributes["x"].value) self.y = float (element.attributes["y"].value) self.angle = int (element.attributes["angle"].value) self.solderConnections = {} # subscripted by pin index: 0..(numpins-1) if "scale" in element.attributes.keys(): self.scale = float (element.attributes["scale"].value) else: self.scale = 1.0 for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "solder": self.AddSolderConnection (SolderConnection (child)) else: raise CircuitException ("Unknown child node '{0}' in component '{1}'.".format (child.localName, self.id)) def AddSolderConnection (self, sc): if sc.pinIndex in self.solderConnections: raise CircuitException ("Attempt to solder pin {0} of component '{1}' more than once.".format (sc.pinIndex, self.id)) self.solderConnections[sc.pinIndex] = sc def DrawPackage (self, picture, transform): self.package.Draw (picture, transform, self) def cx (self): return self.x + self.grid.left def cy (self): return self.y + self.grid.top def DrawPins (self, picture, transform, board): # draw a line from each pin to each solder connection for i, s in self.solderConnections.items(): # draw a line from where the pin starts on the package, to the hole being soldered... x1, y1 = s.BoardCoordinates (board.grids) cx = self.grid.left + self.x cy = self.grid.top + self.y x2, y2 = self.package.PinCoordinates (i, cx, cy, self.angle, self.scale) FatLine (picture, [transform.pixel(x1,y1), transform.pixel(x2,y2)], RGB_WIRE) def BindPackage (self, package): # Make sure our solder connections are consistent with this package. # First make sure that every pin required by the package has a matching solder connection. for i in range(package.numPins): if i not in self.solderConnections: raise CircuitException ("Missing solder connection for pin {0} in component '{1}'".format(i, self.id)) # Now make sure there are no extraneous solder connections... for i in self.solderConnections.keys(): if not (0 <= i < package.numPins): raise CircuitException ("Extraneous pin {0} in component '{1}'".format (i, self.id)) self.package = package #--------------------------------------------------------------------------------------- class Wire: def __init__ (self, element): self.solderConnections = [] self.waypoints = [] for child in element.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "solder": # must refer to integer coordinates corresponding to an empty hole... if len(self.solderConnections) == 2: raise CircuitException ("A wire may not have more than 2 solder connections.") self.solderConnections.append (SolderConnection (child)) elif child.localName == "waypoint": if len(self.solderConnections) == 2: raise CircuitException ("No wire waypoint may appear after both solder connections.") if len(self.solderConnections) == 0: raise CircuitException ("No wire waypoint may appear before the first solder connection.") self.waypoints.append (Waypoint (child)) else: raise CircuitException ("Unknown element '{0}' in Wire".format (child.localName)) if len(self.solderConnections) != 2: raise CircuitException ("A wire must have exactly 2 solder connections.") def BoardPath (self, grids, transform): # Make a path consisting of the first solder connection, all the waypoints, then the last solder connection... path = self.solderConnections[:1] + self.waypoints + self.solderConnections[-1:] # Convert grid-relative coordinates to board coordinates, then absolute pixel coordinates... return [transform.pixelpair(vertex.BoardCoordinates(grids)) for vertex in path] #--------------------------------------------------------------------------------------- class CircuitProject: def __init__ (self): self.board = None self.packageTypes = {} def LoadFile (self, filename): print " Loading", filename self.LoadDocument (xml.dom.minidom.parse(filename)) def LoadDocument (self, xmldoc): rootCount = 0 for root in xmldoc.childNodes: rootCount = rootCount + 1 for child in root.childNodes: if child.nodeType == child.ELEMENT_NODE: if child.localName == "CircuitBoard": # There must be exactly one board per project. if self.board is None: self.board = CircuitBoard (child) else: raise CircuitException ("There may be no more than one board per project.") elif child.localName == "SolderJoint": # Just a hack to test finding an empty hole and soldering it. self.board.AddSolderJoint (SolderJoint (child)) elif child.localName == "PackageType": self.AddPackageType (PackageType (child)) elif child.localName == "Component": self.AddComponent (Component (child)) elif child.localName == "Wire": self.AddWire (Wire (child)) else: raise CircuitException ("Unknown element '{0}'".format (child.localName)) if rootCount != 1: raise CircuitException ("Found {0} CircuitProject elements, but there must be exactly 1.".format(rootCount)) def CreateImage (self): picture = self.board.CreateImage() return picture def AddPackageType (self, pt): if pt.name in self.packageTypes: raise CircuitException ("More than one PackageType is named '{0}'".format (pt.name)) else: self.packageTypes[pt.name] = pt def AddComponent (self, component): if component.packageName not in self.packageTypes: raise CircuitException ("Undefined package type '{0}' referenced by component '{1}'.".format (component.packageName, component.id)) component.BindPackage (self.packageTypes [component.packageName]) # Hand off the component to the circuit board... self.board.AddComponent (component) def AddWire (self, wire): self.board.InstallWire (wire) #--------------------------------------------------------------------------------------- def Main(): returnCode = 1 try: if len(sys.argv) < 3: print "Usage: perf.py outfile.png infile1.perf [infile2.perf...]" else: project = CircuitProject() # Automatically load library.perf from the same directory as this program file... programDir = os.path.dirname (os.path.realpath (sys.argv[0])) libraryFileName = os.path.join (programDir, "library.perf") project.LoadFile (libraryFileName) for filename in sys.argv[2:]: project.LoadFile (filename) picture = project.CreateImage() picture.save (sys.argv[1]) returnCode = 0 except CircuitException as e: print "Error: ", e.textMessage returnCode = 2 return returnCode #--------------------------------------------------------------------------------------- sys.exit (Main()) #--------------------------------------------------------------------------------------- # # $Log: perf.py,v $ # Revision 1.29 2009/08/11 18:48:16 Don.Cross # Added axial capacitor package type. # Added support for inductors with color codes. # Inductor color codes are based on microhenries, not unit henries, # so I had to add the concept of a colorCodeExponent. # # Revision 1.28 2009/08/06 20:22:27 Don.Cross # Now scale applies to all shapes. # # Revision 1.27 2009/08/06 17:54:43 Don.Cross # Instead of adding more and more parameters to each and every Draw function, # I realized it makes more sense to just pass in each component instance with its own properties set. # Added support for ceramic capacitors. # Added the concept of scaling the size of ceramic capacitors. May want to support this in all package types. # # Revision 1.26 2009/08/05 16:01:30 Don.Cross # Resistor color codes are working! # To accomplish this, added the ability to defer a fill/outline color until a particular instance requests a package be drawn. # # Revision 1.25 2009/08/04 21:09:51 Don.Cross # Major refactoring: # I got rid of the concept of "resolving"; there will be no such thing as a forward reference. # Instead, everything must be defined before it can be referred to. # From now on, the CircuitBoard object owns all components and wires, not the CircuitProject and CircuitGrids. # The CircuitProject owns the CircuitBoard and all PackageTypes. # Now we draw all components and wires after the grids have been drawn, fixing a bug noted in the previous checkin comments. # It also will make future code simpler with regard to dynamically moving components around so as to automatically plan a layout. # # Revision 1.24 2009/08/04 20:32:23 Don.Cross # Draw fatter lines to see the pins and wires better. # # Revision 1.23 2009/08/02 14:57:30 Don.Cross # Added support for resistors. # Fixed bug in drawing pins on components that span more than one grid. # Found another bug that still needs to be fixed: grids are drawn one at a time, and components are thought of # as belonging to a grid, even when they span grids. This results in solder connections sometimes being drawn # after the pin connected to it, not before. Perhaps all the components should belong to the board, and be drawn # after all the grids. # # Revision 1.22 2009/08/02 13:58:39 Don.Cross # It looks like I have wires working the way I want. # I am starting to think that, instead of "resolving" a board, I need to just dynamically use grid dictionaries, etc, at the moment needed. # # Revision 1.21 2009/08/02 02:59:26 Don.Cross # Made a function Main, so that there are no unintentional global variables created by the top-level logic. # # Revision 1.20 2009/08/02 02:44:06 Don.Cross # I realize I need component coordinates to be floating point, # so that they can appear to be offset from the holes in any way desired. # # Revision 1.19 2009/08/02 01:08:43 Don.Cross # Component wires almost working... but still having problems when component is rotated. # # Revision 1.18 2009/08/01 21:55:10 Don.Cross # Starting to get somewhere! Drawing a primitive transistor package. # # Revision 1.17 2009/08/01 02:04:01 Don.Cross # Starting to implement the concept of a PackageType, which defines how to draw a particular component. # I'm basically making this up as I go along, so no doubt it will need considerable refactoring later! # # Revision 1.16 2009/07/31 22:13:21 Don.Cross # Adding automatic include of the file library.perf from the same path as perf.py. # This will contain all the standard definitions for component types. # I am starting to work on defining transistors and other devices that come in the TO-92 package. # This is going to take some thought, because I'm not sure how to have the data explain how to draw the package, # where the pins come out of the body, and how long the pins are. # # Revision 1.15 2009/07/31 21:35:22 Don.Cross # Each grid must now have a unique name. # Solder joints can be specified by the triplet (gridname, x, y). # # Revision 1.14 2009/07/31 14:06:46 Don.Cross # Use lookup table instead of if/else logic for hole colors. # # Revision 1.13 2009/07/31 01:02:45 Don.Cross # Starting to experiment with the concepts involved in depicting components, starting with soldered holes. # I changed the 2D array named holeExists to holeState, which is now ternary: # a hole may be absent, empty, or soldered. # # Revision 1.12 2009/07/30 23:40:21 Don.Cross # Starting to implement DrawFaceSide. # # Revision 1.11 2009/07/30 23:34:02 Don.Cross # Catch our own exceptions and display them nicely for a more pleasant user interface. # Report all out-of-bounds sides of an object at once, not just the first one found. # Fixed problem with displaying reflected sides of the border: made entire display 2 pixels larger for "slop" space. # # Revision 1.10 2009/07/30 12:38:34 Don.Cross # Starting to add the concept of drawing both sides of the perf board: the "foil" side and the "face" side. # Adjusted some of the default pixel distances, though I think I will soon require these be defined either in the # input data files or on the command line. # Added class PixelTransform, which abstracts converting foil-side hole coordinates into pixel coordinates, # including the mirror imaging of the face side. # # Revision 1.9 2009/07/30 01:49:38 Don.Cross # I realize it makes more sense to cache the holeExists array inside the Grid object, # since I will need it for doing circuit design also. # # Revision 1.8 2009/07/30 01:35:16 Don.Cross # Adding another input file, board2.perf, to represent the other kind of perf board I have used. # I realized I needed another concept: a GapArray, which cancels out holes that otherwise exist in the Grid. # # Revision 1.7 2009/07/29 22:01:43 Don.Cross # Added the concept of a TraceArray, which allows a compact representation of a regular array of trace blocks. # The output graphics filename must now be the first argument. # If there are not at least 2 arguments supplied, print usage text. # # Revision 1.6 2009/07/29 01:16:54 Don.Cross # The holes look better with a radius of 4 pixels instead of 3 pixels. # # Revision 1.5 2009/07/28 22:10:22 Don.Cross # I forgot about the difference between an Image object and an ImageDraw object. # Starting to see holes now! # # Revision 1.4 2009/07/28 21:50:48 Don.Cross # Starting to parse Grid elements. More sanity checking of user input. # # Revision 1.3 2009/07/28 21:33:10 Don.Cross # Starting to add graphics generation. # # Revision 1.2 2009/07/28 21:10:38 Don.Cross # Load one or more perf files from the command line. # # Revision 1.1 2009/07/28 21:04:48 Don.Cross # I am starting to experiment with a more generic program for displaying perf boards and circuit layouts on boards. # #