%pylab inline pylab.rcParams["figure.figsize"] = (5,5) MIL = 25.4/1000 FEEDS = np.array([750,750,30]) # feeds, per-axis FEEDS_RAPID = np.array([1500,1500,1500]) class Path: "A complete three-dimensional path of points" def __init__(self, pts): self.pts = pts def path(self): return np.copy(self.pts) def toplot(self): pts = self.path() return pts[:,0:2].T # oi def plot(self, plotfn): plotfn(self.toplot()) def gcode_name(self): return "Path, %d pts" % len(self.path()) def extent(self): pts = self.path() minima = [999,999,999] maxima = [-999,-999,-99] for p in pts: for i in range(3): if p[i] < minima[i]: minima[i] = p[i] if p[i] > maxima[i]: maxima[i] = p[i] return [ minima, maxima ] def gcode(self, pt0): spindle_speed = 15000 pts = self.path() epsilon = 0.01 dt = 0.0 if len(pts) < 1: return "" retval = "" speed0 = 0 for pt in pts: travel = pt-pt0 feeds_here = FEEDS if pt[2] >= 0.5 and pt0[2] >= 0.5: # not in work, hopefully... feeds_here = FEEDS_RAPID dp = travel if np.abs(dp[0]) < epsilon and np.abs(dp[1]) < epsilon and dp[2] > 0: # just a retraction feeds_here = FEEDS_RAPID wdp = feeds_here * dp speed = np.sqrt(np.dot(wdp,wdp)/np.dot(dp,dp)) line_dt = np.sqrt(np.dot(dp,dp)) / speed if not np.isnan(line_dt): dt += line_dt dspeed = speed - speed0 cur_line = "G1 "; emitted = False; for (axis,delta,value) in zip("XYZF", (travel[0],travel[1],travel[2],dspeed), (pt[0],pt[1],pt[2],speed)): if axis == "Z" or np.abs(delta) > epsilon: cur_line += "%s%f " % (axis, value) emitted = True if emitted: retval += cur_line + " (dt %0.3f)\n" % (line_dt,) pt0 = pt speed0 = speed header = "" header += "(%s)\n" % self.gcode_name() header += "(Extent: %s)" % str(self.extent()) header += "(Estimated time: %f)\n\n" % dt header += "S%d M3\n" % spindle_speed footer = "G0 Z15\nM30\n" return header + retval + footer class PathFlat(Path): "A flat path of a given depth" def __init__(self, xy_pts, depth): self.pts = [ (p[0],p[1],z) for p,z in zip(xy_pts, depth) ] self.depth = depth def gcode_name(self): return "PathFlat: depth=%d, %d points" % (self.depth, len(self.pts)) class PathArc(Path): """Create a partial (flat) circular path, approximated by N segments""" def __init__(self, center, radius, depth, theta0_deg, theta1_deg, N=None): self.center = center self.radius = radius self.depth = depth self.theta0 = theta0_deg / 180.0 * np.pi self.theta1 = theta1_deg / 180.0 * np.pi self.N = N n = N if None == n: n = np.abs(radius * (self.theta1-self.theta0)/(2*np.pi) * 10) self.pts = self._pts(n) def _pts(self, n): thetas = np.linspace(self.theta0, self.theta1, n+1) xs = self.center[0] + self.radius*np.cos(thetas) ys = self.center[1] + self.radius*np.sin(thetas) zs = [ -1*self.depth for _ in ys ] return zip(xs, ys, zs) class PathCircle(PathArc): """Create a complete (flat) circular path, approximated as an N-gon""" def __init__(self, center, radius, depth, N=None): PathArc.__init__(self, center, radius, depth, 0, 360, N) class PathJoin(Path): """Create a sequence of paths""" def __init__(self, children=None): if None == children: self.children = [] else: self.children = children def add(self, p): self.children.append(p) def path(self): retval = [] for p in self.children: retval.extend(p.path()) return np.array(retval) class PathJoinClosed(PathJoin): def path(self): self.real_children = self.children pt0 = self.children[0].path()[0] pt1 = self.children[-1].path()[-1] self.add(Path([pt0,pt1])) retval = PathJoin.path(self) self.children = self.real_children return retval class PathJoinSafe(PathJoin): def __init__(self, children=None, z_safe=1.0): self.z_safe = z_safe self.children = [] if children: for c in children: self.add(c) def add(self, p): pts = p.path() if len(pts) < 1: return safe_in = pts[0].copy() safe_in[2] = self.z_safe self.children.append(Path([safe_in])) safe_in = pts[0].copy() safe_in[2] = safe_in[2]+0.2 self.children.append(Path([safe_in])) safe_out = pts[-1].copy() safe_out[2] = self.z_safe self.children.append(p) self.children.append(Path([safe_out])) class PathTranslate(Path): def __init__(self, child, xlate): self.xlate = xlate self.child = child def path(self): pts = self.child.path() return pts + self.xlate class PathRotateAboutZOrigin(Path): def __init__(self, child, phi_deg): self.child = child self.phi = -1 * phi_deg * np.pi / 180.0 def path(self): c = np.cos(self.phi) s = np.sin(self.phi) R = np.array([[c, -s, 0], [s, c, 0], [0,0,1]]) return np.dot(self.child.path(),R) class PathRotateAboutXYPoint(Path): def __init__(self, child, center, phi_deg): center3 = np.array([center[0], center[1], 0]) self.pts = PathTranslate(PathRotateAboutZOrigin(PathTranslate(child, center3*-1), phi_deg), center3).path() class PathSpiral(Path): def __init__(self, center, radius, z_top, z_bottom, z_step, direction="CCW", N=None, retract=False): self.center = center self.radius = radius self.z_top = z_top self.z_bottom = z_bottom self.z_step = z_step self.direction = direction self.N = N self.z_safe = 1.0 n = N if None == self.N: n = int(np.abs(radius * 10)) if n < 4: n = 4 if self.direction == "CCW": self.theta0 = 0 self.theta1 = 360 elif self.direction == "CW": self.theta0 = 360 self.theta1 = 0 else: raise Exception("Unknown direction '%s'" % self.direction) # numpy.arange is explicitly documented as possibly overflowing, so sanity check. zs = [ z for z in np.arange(z_top, z_bottom, z_step*-1.0) if z >= z_bottom ] # guarantee we get "really close" to the bottom if (len(zs) == 0) or (np.abs(zs[-1] - z_bottom) > z_step/10.0): zs.append(z_bottom) n_steps = len(zs) * n thetas = [ 2.0*np.pi*i/n for i in range(n_steps) ] if self.direction == "CW": thetas = [ t * -1 for t in thetas ] x_coords = [ self.center[0] + self.radius*np.cos(t) for t in thetas ] y_coords = [ self.center[1] + self.radius*np.sin(t) for t in thetas ] z_coords = [ self.z_top - (self.z_top-self.z_bottom+0.0)*i/n_steps for i in range(n_steps) ] path_coords = zip(x_coords, y_coords, z_coords) if retract: p_f = path_coords[-1] path_coords.append( [p_f[0],p_f[1],self.z_safe] ) self.pts = path_coords # self.children = [ PathArc(self.center, self.radius, z, self.theta0, self.theta1, self.N) for z in zs ] class PathFlipY(Path): def __init__(self, child): self.pts = [ (pt[0], -1*pt[1], pt[2]) for pt in child.path() ] def xyplot(*args, **kwargs): data = args[0] x = data[0] y = data[1] if "linestyle" not in kwargs: kwargs["linestyle"] = "dashed" #if "marker" not in kwargs: # kwargs["marker"] = "o" plot(x,y, **kwargs) class HeaderPins(PathJoin): def __init__(self, depth, flip=True): self.children = [] pin_spacing = 100*MIL def pa(n, t0, t1): p = PathArc((0, n*pin_spacing), pin_spacing/2, depth, t0, t1) self.add(p) return p # Ground is a "C" shape from pin 6 to pin 2 gnd_out_semi = pa(0, -90,90) a0_q_bl = pa(1, 270, 180) #a0 quarter, bottom left last_pt = a0_q_bl.path()[-1] v_pt = last_pt + np.array([0, pin_spacing,0]) self.add(Path(np.array([last_pt, v_pt]))) a0_h_t = pa(2,180,0) last_pt = a0_h_t.path()[-1] v_pt = last_pt - np.array([0, pin_spacing,0]) self.add(Path(np.array([last_pt, v_pt]))) # finish up a0_h_b = pa(1,0,-180) last_pt = a0_h_b.path()[-1] v_pt = last_pt + np.array([0, pin_spacing,0]) self.add(Path(np.array([last_pt, v_pt]))) a1_q_tl = pa(2,180,90) d0_full = pa(3,-90,360+90) gnd_semi = pa(4,-90,90) pwr_semi = pa(5,270,90) if flip: p = PathTranslate(PathFlipY(PathJoin(self.children)), (0,5*pin_spacing,0)) self.children = [p] h = HeaderPins(6*MIL) xlim(-3,15) ylim(-3,15) h.plot(xyplot) class HeaderPinsHole(Path): def __init__(self, depth, z_step=10*MIL): self.children = [] pin_spacing = 100*MIL cut_rad = (50-32)*MIL/2 self.hole = PathSpiral((0,0), cut_rad, 0, -depth, z_step, retract=True) def path(self): return self.hole.path() class HeaderHoleArray(PathJoinSafe): def __init__(self, depth, n=6): pin_spacing = 100*MIL self.z_safe = 1.0 self.children = [] for i in range(n): self.add(PathTranslate(HeaderPinsHole(depth), [0, i*pin_spacing,0])) h = HeaderPins(6*MIL) h_holes = HeaderHoleArray(1.5) xlim(-3,15) ylim(-3,15) h.plot(xyplot) h_holes.plot(xyplot) print h_holes.children[1].gcode((0,0,0)) cut_path = PathJoinClosed() drill_path = PathJoinSafe() pcb_thickness = 1.55 for i in range(9): base_xlate = np.array([30,-3*100*MIL,0]) h = HeaderPins(6*MIL) drills = HeaderHoleArray(pcb_thickness) base_header = PathTranslate(h, base_xlate) rot_header = PathRotateAboutZOrigin(base_header, -40*i) cut_path.add(rot_header) base_drill = PathTranslate(drills, base_xlate) rot_drill = PathRotateAboutZOrigin(base_drill, -40*i) drill_path.add(rot_drill) for i in range(3): xlate = np.array([24,0,0]) mount_drill = PathSpiral((0,0), 2, 0, -pcb_thickness, 0.5) drill_path.add( PathRotateAboutZOrigin( PathTranslate(mount_drill, xlate), -20+120*i ) ) pwr_drill = PathSpiral((0,0), 0.5, 0, pcb_thickness, 0.5) pwr_holes = PathJoin([PathTranslate(pwr_drill, np.array([-23,0,0])), PathTranslate(pwr_drill, np.array([-28,0,0]))]) drill_path.add(PathRotateAboutZOrigin(pwr_holes, -2.5)) inner_cutout = PathSpiral((0,0), 20, 0, -pcb_thickness, 0.5) outer_cutout = PathSpiral((0,0), 40, 0, -pcb_thickness, 0.5) figure(figsize=(15,15)) xlim(-40, 40) ylim(-40, 40) job = PathJoinSafe([cut_path, drill_path, inner_cutout, outer_cutout]) #job = PathJoinSafe([inner_cutout, outer_cutout]) job.plot(xyplot) figure(figsize=(20,20)) cut_path.plot(xyplot) h.path().shape gcode = job.gcode((10,10,10)) f = file("/tmp/foo.nc", "w") f.write(gcode) f.close() print inner_cutout.gcode((0,0,0))