A simple circuit using an Op-Amp and variable resistance. Its a simple amplifier stage.

Using a simple line drawing program written in python, draw out the bones of the circuit to scale as it would appear on the circuit board and save the resulting image as a bitmap.

Create an outline of the image and use this to route out a line of copper, separating the track from the rest of the board and each other. The surrounding copper is left in situ for shielding.

Outlining the image mathematically proved to be a real headache so I cheated slightly. By using a bitmap scaled to provide 1 pixel per stepper step and drawing from one concurrent pixel to the next, a high resolution outline can be made.

Using a technique from the image recognition in AIMOS, the picture is scanned from top left to bottom right 4 pixels at a time. Any group of pixels with an artifact in them is an edge...

Once the routine has completed I'm left with a tuple containing only coordinates of edge pixels. These are then pulled in one by one by checking to see if each has neighbours, and adding them too. By the time all the pixels have been pulled in, the list contains the outline as an ordered sequence of coordinates.

(I'm not in the habit of commenting my code, even though I was taught to eventually at college. Bad Toad perhaps, but good code should be clear and concise so it explains itself. Not that theres anything wrong with code golf of course. But for those who cant think in code, the download is fully commented...)

def render(crd):
  global board,sw,sh,scl
  minx=1900; miny=1000; maxx=0; maxy=0
  if crd!=[]:
    for i,(x1,y1),(x2,y2) in crd:
      if x1>maxx: maxx=x1
      if x1<minx: minx=x1
      if y1>maxy: maxy=y1
      if y1<miny: miny=y1
      if i==1:
        if x2>maxx: maxx=x2
        if x2<minx: minx=x2
        if y2>maxy: maxy=y2
        if y2<miny: miny=y2
    minx=minx-20; maxx=maxx+20; miny=miny-20; maxy=maxy+20
    sw=int(maxx-minx)
    sh=int(maxy-miny)
    board=pygame.Surface((sw,sh),depth=24)
    whi=pygame.Surface.map_rgb(board,(255,255,255))
    bla=pygame.Surface.map_rgb(board,(0,0,0))
    red=pygame.Surface.map_rgb(board,(255,0,0))
    yel=pygame.Surface.map_rgb(board,(255,255,0))
    pygame.draw.rect(board,(0,0,0),(0,0,sw,sh),0)
    for i,(x1,y1),(x2,y2) in crd:
      if i==1: rline((x1-minx,y1-miny),(x2-minx,y2-miny))
      if i==2: rpad((x1-minx,y1-miny),[0.8,0])
      if i==3: rpad((x1-minx,y1-miny),[1.0,1])
    neblst=[]
    pix=pygame.PixelArray(board)
    for y in range(sh-1):
      for x in range(sw-1):
        if (pix[x,y]==whi and pix[x+1,y]==bla): neblst.append((x+1,y))
        if (pix[x,y]==bla and pix[x+1,y]==whi): neblst.append((x,y))
        if (pix[x,y]==whi and pix[x,y+1]==bla): neblst.append((x,y+1))
        if (pix[x,y]==bla and pix[x,y+1]==whi): neblst.append((x,y))
    pygame.draw.rect(board,(0,0,0),(0,0,sw,sh),0)
  for (x,y) in neblst:
    pix[x,y]=red
  pix=None
  pygame.Surface.blit(screen,board,(0,0))
  pygame.display.flip()
  sleep(3)
  return [neblst,sw,sh]
Finally the redundant coordinates are removed - I only need the beginning and end of each line to send to the mill as GCode.

Make a destination list to put the start end ends into and get the first source coordinate. It will always be the top leftmost pixel in the list after any others are transferred into the destination.

Scan for a line of pixels, take the first and last in any line found and put them into the destination list.

Keep doing this until there are no more pixels...

Eventually the source list will be empty, and the destination full of pairs of pixels.

def trace(crd):
  
  def nebscan(nebs,dctn):
    fnd=[]
    if nebs!=[]:
      srch=False
      x,y=nebs[0]; sx,sy=nebs[0]
      dx,dy=dctn
      while not srch:
        if (x+dx,y+dy) in nebs:
          fnd.append((x+dx,y+dy))
          x=x+dx; y=y+dy
        else:
          srch=True
      if x!=sx or y!=sy:
        fnd.insert(0,(sx,sy))
      return fnd
    
  coords=[]
  orphans=[]
  done=False
  
  while not done:
    lst=nebscan(crd,(1,0))
    if lst==[]: lst=nebscan(crd,(-1,0))
    if lst==[]: lst=nebscan(crd,(0,1))
    if lst==[]: lst=nebscan(crd,(0,-1))                   
    if lst==[]: lst=nebscan(crd,(1,1))
    if lst==[]: lst=nebscan(crd,(-1,-1))
    if lst==[]: lst=nebscan(crd,(1,-1))
    if lst==[]: lst=nebscan(crd,(-1,1))
    if lst>[]:
      coords.append([lst[0],lst[len(lst)-1]])
      for xy in lst: crd.pop(crd.index(xy))
    else: orphans.append(crd.pop(0))
    if len(crd)==0: done=True
  return coords
# orphaned pixels should be integrated,
# just ignoring them for now

Once the destination list has been obtained its relatively simple to sort into order. Check the pairs of coordinates and see if any lay next to each other. If they do, reorder the pair if necessary and place next to the first pair.

If a pair cannot be matched, they arent part of a contiguous line. This tells the mill to lift the head before moving to this new coordinate and starting another contiguous line. Because these lines begin and end in the same place, following the edge of a track, the head only has to move to the next nearest start position: Always beginning top left of the section, the path of the head is fairly well optimised over the drawing as well.

def gcodeify(neblst):
  gsc=60.0*scl
  gcode=';CirKit Output\n'
  gcode=gcode+'G28 ;Home\n'
  x=neblst[0][0]; y=neblst[0][1]
  gcode=gcode+'G1 Z3 ;Raise Tool\n'
  gcode=gcode+'G1 X'+str((x/gsc)*2.54)+' Y'+str((y/gsc)*2.54)+' ;Position Tool\n'
  gcode=gcode+'G1 Z1.2 ;Lower tool\n;Begin Section\n'
  for c in neblst:
    lx=x;ly=y
    x=c[0]; y=c[1]
    if abs(lx-x)>1 or abs(ly-y)>1:
      gcode=gcode+';End of Section\n'
      gcode=gcode+'G1 Z3 ;Raise Tool\n'
      gcode=gcode+'G1 X'+str((x/gsc)*2.54)+' Y'+str((y/gsc)*2.54)+' ;Reposition\n'
      gcode=gcode+'G1 Z1.2 ;Lower Tool\n;Begin Section\n'
    if lx!=x and ly!=y: gcode=gcode+'G1'+' X'+str((x/gsc)*2.54)+' Y'+str((y/gsc)*2.54)+'\n'
    if lx==x and ly!=y: gcode=gcode+'G1'+' Y'+str((y/gsc)*2.54)+'\n'
    if lx!=x and ly==y: gcode=gcode+'G1'+' X'+str((x/gsc)*2.54)+'\n'
  gcode=gcode+';End of Section\n'
  gcode=gcode+'G1 Z3 ;Raise Tool\nG1 X0 Y0 Z10 ;Rest\n;Finish\n'
  return gcode