def generate_half_moon(x, y, direction = +1):
	direction = int(direction)
	factor = 10.0**int(decimal_places)
	debug(str(factor))
	if (stencil_half_moon_location == "NorthSouth"):
		eii = +1.0
		eij = +0.0
	else:
		eii = +0.0
		eij = +1.0
	x = x - width_of_half_moon / 2.0
	y = y + height_of_stubs * direction
	gerber_instructions.append("G01X" + str(int(factor*(eii*x+eij*y))) + "Y" + str(int(factor*(eii*y+eij*x))) + "D02")
	y = y - height_of_stubs * direction
	gerber_instructions.append("G01X" + str(int(factor*(eii*x+eij*y))) + "Y" + str(int(factor*(eii*y+eij*x))) + "D01")
	x = x + width_of_half_moon
	gerber_instructions.append("G01X" + str(int(factor*(eii*x+eij*y))) + "Y" + str(int(factor*(eii*y+eij*x))) + "D01")
	y = y + height_of_stubs * direction
	gerber_instructions.append("G01X" + str(int(factor*(eii*x+eij*y))) + "Y" + str(int(factor*(eii*y+eij*x))) + "D01")
	if (direction < 0):
		if (stencil_half_moon_location == "NorthSouth"):
			string = "G02"
		else:
			string = "G03"
	else:
		if (stencil_half_moon_location == "NorthSouth"):
			string = "G03"
		else:
			string = "G02"
	x_new = x - width_of_half_moon
	gerber_instructions.append(string + "X" + str(int(factor*(eii*x_new+eij*y))) + "Y" + str(int(factor*(eii*y+eij*x_new))) + "I" + str(int(-factor*eii*radius_of_half_moon_arc_segment)) + "J" + str(int(-factor*eij*radius_of_half_moon_arc_segment)) + "D01")
def generate_stencil_layer():
	#stencil_half_moon_location = "NorthSouth" or "EastWest"
	if (stencil_half_moon_location == "NorthSouth"):
		pw = panel_width
		ph = panel_height
	else:
		pw = panel_height
		ph = panel_width
	global gerber_instructions
	gerber_instructions = []
	ratio = 1.0 / (10.0**int(decimal_places))
	gerber_instructions.append("setratio" + str(ratio))
	distance_between_board_edge_and_rows_of_half_moons = 13.0 # mm fixme
	n = int(math.ceil(pw / pitch_of_half_moons))
	n = n + number_of_extra_half_moons_per_side
	n = 2*int(math.ceil(n/2.0)) # ensure it's even to center the stencil on the stencil holder
	stencil_border_extra_width_on_each_side = pitch_of_half_moons - width_of_half_moon - 0.5 # mm
	stencil_border_extra_height_on_each_side = 3.75 # mm
	global stencil_width
	global stencil_height
	stencil_width = (n - 1) * pitch_of_half_moons + width_of_half_moon + 2.0 * stencil_border_extra_width_on_each_side + laser_stroke_width
	stencil_height = ph + 2.0 * distance_between_board_edge_and_rows_of_half_moons + 2.0 * stencil_border_extra_height_on_each_side + laser_stroke_width
	debug("stencil \"width\" = " + str(stencil_width))
	debug("stencil \"height\" = " + str(stencil_height))
	if (stencil_width > 310.0) or (stencil_height > 370.0):
		warning("stencil will not fit on stencil holder")
	global stencil_border_x
	global stencil_border_y
	stencil_border_x = (pw - stencil_width) / 2.0
	stencil_border_y = (ph - stencil_height) / 2.0
	#gerber_instructions.append("setlayerNorth")
	x = pw / 2.0 - (n-1) * pitch_of_half_moons / 2.0
	y = -distance_between_board_edge_and_rows_of_half_moons 
	for i in range(0, n):
		generate_half_moon(x, y, +1)
		x = x + pitch_of_half_moons
	#gerber_instructions.append("setlayerSouth")
	x = pw / 2.0 - (n-1) * pitch_of_half_moons / 2.0
	y = ph + distance_between_board_edge_and_rows_of_half_moons 
	for i in range(0, n):
		generate_half_moon(x, y, -1)
		x = x + pitch_of_half_moons
	return gerber_instructions
def generate_drill_layers():
	debug("creating svg from drill file info...")
	layer_name_string = "drill holes"
	layer_name_string = layer_name_string + "(" + str(horizontal_instance) + "," + str(vertical_instance) + ")"
	global drill_layer
	drill_layer = add_layer(board_layer, layer_name_string)
	drill_extents = add_layer(drill_layer, "drill extents")
	drill_centers = add_layer(drill_layer, "drill centers")
	for diameter in tool:
		drill_centers_layer = add_layer(drill_centers, str(diameter))
		drill_extents_layer = add_layer(drill_extents, str(diameter))
		drill_centers_group = add_group(drill_centers_layer, color="#ff0000")
		drill_extents_group = add_group(drill_extents_layer)
		#debug(diameter + ":")
		for location in tool[diameter]:
			radius = float(diameter) / 2.0
			(x, y) = location
			#debug("(" + str(x) + "," + str(y) + ") ",)
			drill_centers_group.add(svg.line( (x-stroke_length/2.0, y), (x+stroke_length/2.0, y) ))
			drill_extents_group.add(svg.circle( (x, y), radius, fill="none" ))
	#drill_layer.translate(-x_offset+horizontal_instance*board_width+horizontal_instance*x_gap_between_instances_of_boards, +y_offset+panel_height-vertical_instance*board_height-vertical_instance*y_gap_between_instances_of_boards)
	drill_layer.translate(-x_offset,+y_offset)
	drill_layer.translate(+horizontal_instance*board_width,+panel_height-vertical_instance*board_height)
	drill_layer.translate(+horizontal_instance*x_gap_between_instances_of_boards,-vertical_instance*y_gap_between_instances_of_boards)
def draw_gerber_layer(parent_object, gerber_instructions, layer_name, color = "#000000", stroke_width = laser_stroke_width):
	layer = add_layer(parent_object, layer_name)
	overall_layer = layer
	group = add_group(layer, color=color, stroke_width=stroke_width)
	#group.stroke(color=color, width=float(stroke_width), miterlimit=4, opacity=1)
	#group.dasharray("none")
	#group.fill("none")
	debug2("instructions:")
	x = 0
	y = 0
	x_old = 0
	y_old = 0
	x_string = "0"
	y_string = "0"
	i_string = "0"
	d_string = "02" # pen up
	i = 0
	j = 0
	aperture = "00"
	mode = "linear"
	ratio = 5.0
	#global verbosity
	#verbosity = 4
	for instruction in gerber_instructions:
		debug("instruction: " + instruction)
		match = re.search("^setratio([.0-9]*[e]*[-0-9]*)$", instruction)
		if match:
			ratio = float(match.group(1))
			debug("found ratio: " + str(ratio))
		match = re.search("^setlayer(.*)$", instruction)
		if match:
			layer_name = match.group(1)
			layer = add_layer(overall_layer, layer_name)
			group = add_group(layer, color=color, stroke_width=stroke_width)
		match = re.search("^G54D([1-9][0-9])$", instruction) # "G54D22" means follow the goto X,Y instructions after this line, and flash the D22 aperture every time we get a D03 ("flash aperture") command
		if match:
			debug("aperture selection: " + match.group(1))
			aperture = int(match.group(1))
			if not aperture in apertures.keys():
				error("can't find aperture #" + str(aperture), 4)
			(CROP, w, h) = apertures[aperture]
			w = 25.4 * w # fixme/todo:  magic number here
			h = 25.4 * h # fixme/todo:  magic number here
			debug("aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h))
			d_string = "04" # skip doing anything this pass through the following code
		match = re.search("^G(0[0-3])(.*)$", instruction)
		if match:
			instruction = match.group(2)
			#debug2("remaining instruction: " + instruction)
			if (match.group(1) == "01"):
				mode = "linear"
				debug("mode = linear")
			elif (match.group(1) == "02"):
				mode = "cc_arc"
				debug("mode = cc arc")
			elif (match.group(1) == "03"):
				mode = "cw_arc"
				debug("mode = cw arc")
			else:
				error("?", 3)
		match = 1
		while match:
			#match = re.search("^([XYIJ])([-0-9]{1," + str(number_of_digits) + "})(.*)$", instruction)
			match = re.search("^([XYIJ])([-0-9]{1,10})(.*)$", instruction)
			if match:
				instruction = match.group(3)
				#debug2("remaining instruction: " + instruction)
				if (match.group(1) == "X"):
					x_string = match.group(2)
					x = +int(x_string) * ratio
				elif (match.group(1) == "Y"):
					y_string = match.group(2)
					y = +int(y_string) * ratio
				elif (match.group(1) == "I"):
					i_string = match.group(2)
					i = +int(i_string) * ratio
				elif (match.group(1) == "J"):
					j_string = match.group(2)
					j = +int(j_string) * ratio
		match = re.search("^D(0[1-3])$", instruction)
		if match:
			d_string = match.group(1)
		if (d_string == "01"): # pen down
			debug(" " + x_string + " " + y_string + " " + d_string)
			if (mode == "linear"):
				group.add(svg.line( (x_old,y_old), (x,y) ))
				#debug("(" + str(x_old) + "," + str(y_old) + ") -> (" + str(x) + "," + str(y) + ") with pen down")
			else:
				# PADS: "G03X58250I-3315J-5D01*" or "G03X201969Y117031I0J-1969D01*"
				# altium: "G03*", then "X-1778Y-10160I2540J0D01*"
				radius = math.sqrt(i**2+j**2)
				delta_x = x - x_old
				delta_y = y - y_old
				x_center = x_old + i
				y_center = y_old + j
				debug("old(" + str(x_old) + "," + str(y_old) + ") -> xy(" + str(x) + "," + str(y) + ") -> ij(" + str(i) + "," + str(j) + ") -> center(" + str(x_center) + "," + str(y_center) + ")")
				#debug("(x,y,i,j) = (" + str(x) + "," + str(y) + "," + str(i) + "," + str(j) + ")")
				if (mode == "cc_arc"):
					#debug("cw arc here")
					# from http://stackoverflow.com/questions/25019441/arc-pie-cut-in-svgwrite
					# dwg.path(d="M {0},{1} l {2},{3} a {4},{4} 0 0,0 {5},{6} z".format(start_x, start_y, m0, n0, radius, m1, n1),
					# M=moveto, L=lineto, a=arc?, z=close path
					# a: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
					string = "M {0},{1} a {2},{2} 0 0,0 {3},{4}".format(x_old, y_old, radius, delta_x, delta_y)
				elif (mode == "cw_arc"):
					string = "M {0},{1} a {2},{2} 0 0,1 {3},{4}".format(x_old, y_old, radius, delta_x, delta_y)
				else:
					error("?", 4)
				#group.add(svg.circle( (x_center,y_center), radius, fill="none" ))
				#group.add(svg.line( (x_old, y_old), (x, y) ))
				debug(string)
				group.add(svg.path(d=string, fill="none"))
		elif (d_string == "02"):
			pass
			# maybe change to a new group here?
		elif (d_string == "03"):
			if (aperture == 0):
				warning("unhandled D03 flash operation")
			else:
				t = laser_stroke_width / 2.0
				#x = x / 1.5
				#y = y / 1.5
				debug("flashing aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h) + " at (" + str(x) + "," + str(y) + ")")
				if (CROP == "R"):
					group.add(svg.rect( (x-w/2.0+t,y-h/2.0+t), (w-2*t,h-2*t) ))
				# should add a special case here for O (oval) and make a rectangle and two half-circles
		elif (d_string == "04"):
			pass # silently fall thorough, otherwise G54D commands trigger output the first pass through
		else:
			error("unknown d string", 5)
		x_old = x
		y_old = y
	return overall_layer
def parse_gerber(filename):
	info("parsing gerber file \"" + filename + "\"...")
	#global units
	#global zero_suppression_mode
	#global coordinate_mode
	#global layer_polarity
	#global x_format
	#global y_format
	#global number_of_digits
	#global ratio
	global decimal_places # fixme - a bit funny if this changes between gerbers we're reading
	zero_suppression_mode = "leading"
	coordinate_mode = "absolute"
	layer_polarity = "dark"
	x_format = "55"
	y_format = "55"
	#units = "mm"
	lines = []
	for line in open(filename):
		line = line.rstrip("\n\r")
		lines.append(line)
	gerber_instructions = []
	global apertures
	apertures = {}
	aperture = "00"
	matched_units = 0
	matched_format = 0
	set_ratio = 0
	#gerber_instructions.append("hi there")
	for line in lines:
		matches = 0
		match = re.search("^%MO([IM][NM])\*%$", line)
		if match:
			matches = matches + 1
			if (match.group(1) == "IN"):
				#debug("set units to inches: " + line)
				ratio = 1000.0 / 25.4 / 1.55 # fixme/todo: 1.55 is a magic number here
			else:
				#debug("set units to mm: " + line)
				ratio = 1.0
			matched_units = 1
		match = re.search("^%FS([LTD])([AI])X([0-9][0-9])Y([0-9][0-9])\*%$", line)
		if match:
			matches = matches + 1
			debug("set format: " + line)
			if (match.group(1) == "L"):
				zero_suppression_mode = "leading"
			else:
				zero_suppression_mode = "trailing"
			if (match.group(2) == "A"):
				coordinate_mode = "absolute"
			else:
				coordinate_mode = "incremental"
			x_format = match.group(3)
			y_format = match.group(4)
			decimal_places = int(x_format[1])
			number_of_digits = int(x_format[0]) + int(x_format[1])
			number_of_digits = number_of_digits + 1 # some gerber files output 7 digits for "24" format...
			#decimal_places = decimal_places + 1
			debug("number of digits to use for coordinates = " + str(number_of_digits))
			matched_format = 1
		match = re.search("^%ADD([0-9]+)C,([.0-9]+)\*%$", line) # %ADD010C,0.0254*%
		if match:
			matches = matches + 1
			#aperture_length = len(match.group(1))
			ap = int(match.group(1))
			apertures[ap] = ("C", float(match.group(2)), float(match.group(2)))
			debug("aperture definition: " + line)
		match = re.search("^%ADD([0-9]+)R,([.0-9]+)X([.0-9]+)\*%$", line) # %ADD028R,2.6X1.6*% or %ADD34R,0.0138X0.0472*%
		if match:
			matches = matches + 1
			ap = int(match.group(1))
			apertures[ap] = ("R", float(match.group(2)), float(match.group(3)))
			debug("aperture definition: " + line)
		match = re.search("^%ADD([0-9]+)O,([.0-9]+)X([.0-9]+)\*%$", line) # %ADD11O,0.0138X0.0669*%
		if match:
			matches = matches + 1
			ap = int(match.group(1))
			apertures[ap] = ("O", float(match.group(2)), float(match.group(3)))
			debug("aperture definition: " + line)
		match = re.search("^%LN([a-zA-Z0-9]+)\*%$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append("setlayer" + match.group(1))
		match = re.search("^(G0[1-3])\*$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append(match.group(1))
		match = re.search("^(G0[1-3][XY].*)\*$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append(match.group(1))
		match = re.search("^([XY].*)\*$", line)
		if match:
			matches = matches + 1
			#debug(line)
			gerber_instructions.append(match.group(1))
		match = re.search("^(G54D)([0-9]+)\*$", line)
		if match:
			matches = matches + 1
			debug("aperture selection: " + match.group(2))
			gerber_instructions.append(match.group(1) + match.group(2))
			#debug(match.group(1))
		match = re.search("^%LP([DC])\*%$", line)
		if match:
			# this unfortunately only grabs the last instance...
			matches = matches + 1
			if (match.group(1) == "D"):
				layer_polarity = "dark"
			else:
				layer_polarity = "clear"
		match = re.search("^%IPNEG\*%$", line) # %IPNEG% = reverse whole layer
		if match:
			matches = matches + 1
			# do something here for this...
		match = re.search("^%IPPOS\*%$", line) # %IPPOS% = whole layer normal
		if match:
			matches = matches + 1
		match = re.search("^G04.*$", line)
		if match:
			matches = matches + 1
		match = re.search("^%$", line)
		if match:
			matches = matches + 1
		match = re.search("^\*$", line)
		if match:
			matches = matches + 1
		match = re.search("^G74\*$", line)
		if match:
			warning("cannot handle single quadrant G74 mode")
			matches = matches + 1
			#exit()
		match = re.search("^G75\*$", line) # multi-quadrant mode
		if match:
			matches = matches + 1
		match = re.search("^$", line)
		if match:
			matches = matches + 1
		match = re.search("^M02\*$", line) # M02* = end of job
		if match:
			matches = matches + 1
		match = re.search("^%AM(.*)\*$", line) # AM = aperture macro definition
		if match:
			matches = matches + 1
			warning("ignoring aperture macro \"" + match.group(1) + "\"")
		match = re.search("^([$0-9]*),", line) # aperture macro definition continuation
		if match:
			matches = matches + 1
		match = re.search("^%AD(.*)\*$", line) # AD = aperture macro instantiation
		if match:
			matches = matches + 1
			error("aperture instantiation \"" + match.group(1) + "\" ignored - this part needs to be coded")
		match = re.search("^%IN(.*)\*%$", line) # IN = image name
		if match:
			matches = matches + 1
			debug("ignoring image name \"" + match.group(1) + "\"")
		if (matches == 0):
			warning("did not parse \"" + line + "\"")
		if (set_ratio == 0) and (matched_units == 1) and (matched_format == 1):
			ratio = ratio / (10.0**int(decimal_places))
			debug("ratio = " + str(ratio))
			gerber_instructions.append("setratio" + str(ratio))
			set_ratio = 1
	if (matched_format == 0):
		error("did not find format line", 2)
	for aperture in apertures:
		(CROP, w, h) = apertures[aperture]
		debug("aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h))
	#debug("units: " + units)
	debug("x_format: " + x_format)
	debug("y_format: " + y_format)
	debug("layer_polarity: " + layer_polarity)
	debug("zero_suppression_mode: " + zero_suppression_mode)
	debug("coordinate_mode: " + coordinate_mode)
	return gerber_instructions