original TravisProg added

Change-Id: I2a3be656b5f7237491958d79a5ab3e4c0cb1d9d9
Signed-off-by: Max Fisch <maxf6.283@gmail.com>
diff --git a/manufacturing/TravisProg1/lib_files/data/codecs/data_view.rb b/manufacturing/TravisProg1/lib_files/data/codecs/data_view.rb
new file mode 100755
index 0000000..8b7d846
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/data/codecs/data_view.rb
@@ -0,0 +1,135 @@
+class DataView
+	attr_accessor :i, :endi
+	def initialize(val)
+		@val = val
+		@i = 0
+		@endi = @val.length
+	end
+	def unpack_single(i, l, code)
+		raise "overran buffer" if (i + l > @val.bytesize)
+		v = @val.byteslice(i, l).unpack(code)
+		@i = i + l
+		return v[0]
+	end
+	def unpack_multiple(i, l, n, code)
+		raise "overran buffer" if (i + l * n > @val.bytesize)
+		v = @val.byteslice(i, l * n).unpack("#{code}#{n}")
+		@i = i + l * n
+		return v
+	end
+	def f64(i = @i)
+		unpack_single(i, 8, "D")
+	end
+	def nf64(n, i = @i)
+		unpack_multiple(i, 8, n, "D")
+	end
+	def i8(i = @i)
+		unpack_single(i, 1, "c")
+	end
+	def i16(i = @i)
+		unpack_single(i, 2, "s")
+	end
+	def i32(i = @i)
+		unpack_single(i, 4, "i")
+	end
+	def u8(i = @i)
+		unpack_single(i, 1, "C")
+	end
+	def u16(i = @i)
+		unpack_single(i, 2, "S")
+	end
+	def u32(i = @i)
+		unpack_single(i, 4, "I")
+	end
+	def varint(i = @i)
+		@i = i
+		v = 0
+		p = 0
+		while true
+			k = @val.getbyte(@i)
+			v += (k & 0x7f) << p
+			p += 7
+			@i += 1
+			if (k & 0x80 == 0)
+				return v
+			end
+		end
+	end
+	def get_utf8_string(len, i = @i)
+		val = @val.byteslice(i, len)
+		@i = i + len
+		return val.force_encoding("UTF-8")
+	end
+	def null_utf16(i = @i)
+		i_n = i
+		i_n += 2 while @val.byteslice(i_n, 2) != "\0\0"
+		val = @val.byteslice(i, i_n - i).force_encoding("UTF-16LE")
+		@i = i_n + 2
+		val
+	end
+	def null_ascii8(i = @i)
+		i_n = i
+		i_n += 1 while @val.byteslice(i_n, 1) != "\0"
+		val = @val.byteslice(i, i_n - i)
+		@i = i_n + 1
+		val
+	end
+end
+BaseNullString = "\0".force_encoding("ASCII-8BIT")
+class OutputView
+	attr_accessor :i, :val
+	def initialize(len)
+		@val = BaseNullString * len
+		@i = 0
+	end
+	def pack_single(v, i, l, code)
+		raise "overran buffer" if (i + l > @val.bytesize)
+		add_bytes([v].pack(code).bytes, i)
+		return self
+	end
+	def f64(v, i = @i)
+		pack_single(v, i, 8, "D")
+	end
+	def i8(v, i = @i)
+		pack_single(v, i, 1, "c")
+	end
+	def i16(v, i = @i)
+		pack_single(v, i, 2, "s")
+	end
+	def i32(v, i = @i)
+		pack_single(v, i, 4, "i")
+	end
+	def u8(v, i = @i)
+		pack_single(v, i, 1, "C")
+	end
+	def u16(v, i = @i)
+		pack_single(v, i, 2, "S")
+	end
+	def u32(v, i = @i)
+		pack_single(v, i, 4, "I")
+	end
+	def add_bytes(v, i = @i)
+		v.each do |j|
+			@val.setbyte(i, j)
+			i += 1
+		end
+		@i = i
+	end
+	def varint(v, i = @i)
+		@i = i
+		v &= 0xFFFFFFFF
+		while (v >> 7) != 0
+			@val.setbyte(@i, (v & 0x7f) | 0x80)
+			@i += 1
+			v >>= 7
+		end
+		@val.setbyte(@i, (v & 0x7f))
+		@i += 1
+	end
+	def self.varint_size(v)
+		i = 1
+		v &= 0xFFFFFFFF
+		i += 1 while (v >>= 7) != 0
+		return i
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/data/codecs/serialize_buffer.rb b/manufacturing/TravisProg1/lib_files/data/codecs/serialize_buffer.rb
new file mode 100755
index 0000000..5aea1c2
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/data/codecs/serialize_buffer.rb
@@ -0,0 +1,186 @@
+require "data/codecs/data_view.rb"
+class SerializeBuffer
+	class Writer
+		def initialize()
+			@build_stack = [[]]
+		end
+		def write_varint(varint)
+			@build_stack[-1].push(SerializeBuffer::WriteVarint.new(varint))
+		end
+		def code_string(str)
+			@build_stack[-1].push(SerializeBuffer::WriteString.new(str))
+		end
+		def write_f64(val)
+			@build_stack[-1].push(SerializeBuffer::WriteF64.new(val))
+		end
+		def push()
+			@build_stack.push([])
+		end
+		def pop()
+			t = @build_stack.pop()
+			@build_stack[-1].push(SerializeBuffer::WriteList.new(t))
+		end
+		def data() @build_stack[0] end
+		def depth() @build_stack.length end
+	end
+	class WriteVarint
+		attr_accessor :varint
+		def initialize(varint)
+			@varint = varint
+		end
+		def inspect() @varint.inspect end
+		def pretty_print(i)
+			puts "#{"  " * i}#{@varint.inspect}"
+		end
+		def get_concat_len()
+			return OutputView.varint_size(@varint)
+		end
+		def concat_output(output)
+			#puts "varint: #{output.i}"
+			output.varint(@varint)
+		end
+	end
+	class WriteF64
+		attr_accessor :val
+		def initialize(val)
+			@val = val
+		end
+		def inspect() @val.inspect end
+		def pretty_print(i)
+			puts "#{"  " * i}#{@val.inspect}"
+		end
+		def get_concat_len()
+			return 8
+		end
+		def concat_output(output)
+			#puts "varint: #{output.i}"
+			output.f64(@val)
+		end
+	end
+	class WriteString
+		attr_accessor :str
+		def initialize(str)
+			@str = str
+		end
+		def inspect() @str.inspect end
+		def pretty_print(i)
+			puts "#{"  " * i}#{@str.inspect}"
+		end
+		def get_concat_len()
+			len = @str.bytes.length
+			return len + OutputView.varint_size(len)
+		end
+		def concat_output(output)
+			#puts "varstring: #{output.i} #{@str.inspect}"
+			output.varint(@str.bytes.length)
+			#puts "varstrdata: #{output.i}"
+			output.add_bytes(@str.bytes)
+		end
+	end
+	class WriteList
+		attr_accessor :items
+		def initialize(items)
+			@items = items
+		end
+		def inspect()
+			"#{@items.inspect}"
+		end
+		def pretty_print(i)
+			@items.each do |item|
+				if (item.class == self.class)
+					#puts "#{"  " * i}["
+					@items.each do |v|
+						v.pretty_print(i + 1)
+					end
+					#puts "#{"  " * i}]"
+					return
+				end
+			end
+			#puts "#{"  " * i}#{@items.inspect}"
+		end
+		def get_concat_len()
+			@len = SerializeBuffer.get_concat_length(@items)
+			return @len + OutputView.varint_size(@len)
+		end
+		def concat_output(output)
+			#puts "write_list: #{output.i} #{@len}"
+			output.varint(@len)
+			#				puts "start_list_list: #{output.i}"
+			SerializeBuffer.concat_output(output, @items)
+		end
+		def get_ipos(ids, idoff)
+			@len || get_concat_len()
+			return OutputView.varint_size(@len) +
+				SerializeBuffer.get_ipos(ids, @items, idoff)
+		end
+	end
+	def self.concat_output(output, arr)
+		arr.each do |obj|
+			obj.concat_output(output)
+		end
+	end
+	def self.get_concat_length(arr)
+		len = 0
+		arr.each do |obj|
+			len += obj.get_concat_len()
+		end
+		return len
+	end
+	def self.get_ipos(ids, arr, idoff = 0)
+		ipos = 0
+		arr.each.with_index do |obj, i|
+			if (i == ids[idoff])
+				idoff += 1
+				if (idoff < ids.length)
+					return ipos + obj.get_ipos(ids, idoff) 
+				else
+					return ipos 
+				end
+			else
+				ipos += obj.get_concat_len()
+			end
+		end
+		return ipos
+	end
+	def self.pretty_print(arr)
+		arr.each do |v|
+			v.pretty_print(0)
+		end
+	end
+	# example:
+	# buf = SerializeBuffer.code { |writer| ... }
+	# File.write(fname, buf)
+	def self.code(&blk)
+		ser = SerializeBuffer::Writer.new()
+		blk.call(ser)
+		data = ser.data()
+		len = SerializeBuffer.get_concat_length(data)
+		buffer = OutputView.new(len)
+		SerializeBuffer.concat_output(buffer, data)
+		buffer.val
+	end
+end
+class StreamReader
+	def initialize(data)
+		@data = DataView.new(data)
+		@endis = []
+	end
+	def ipos()
+		@data.i
+	end
+	def push()
+		len = @data.varint()
+		@endis.push(@data.endi)
+		newendi = @data.i + len
+		raise "problem!" if (newendi > @data.endi)
+		@data.endi = @data.i + len
+	end
+	def parse_f64() @data.f64() end
+	def parse_varint() @data.varint() end
+	def parse_string() @data.get_utf8_string(@data.varint()) end
+	def not_done() @data.i < @data.endi end
+	def pop()
+		raise "problem!" if @data.endi != @data.i
+		@data.endi = @endis.pop()
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_offset.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_offset.rb
new file mode 100755
index 0000000..7e3d2fa
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_offset.rb
@@ -0,0 +1,29 @@
+require "geometry/dxf_to_gcode/2dpoint.rb"
+require_relative "offset_profiles.rb"
+
+require "geometry/dxf_to_gcode/dxf.rb"
+require_relative "dxf_to_offset_profiles.rb"
+
+require "geometry/dxf_to_gcode/timed_view.rb"
+require_relative "overlay.rb"
+require_relative "pairwise_inter.rb"
+
+def offset_dxf_integer(dxf, scale, resolution, offset)
+	lns = dxf[0].to_offsetable(scale)
+
+	all_lns = []
+	lns.each do |ln|
+		rlns = ln.offset(((offset) * scale).to_i)
+		segments = []
+		rlns.each do |rln|
+			rln.append_to(segments, resolution)
+		end
+		shape = Shape.new(segments)
+		all_lns << shape
+	end
+
+#	swl_p, nticks = SweepLineUnion.make_outside_profile(all_lns) 
+	swl_p = AllPairsInter.inter(all_lns) 
+
+	return swl_p#, all_lns
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_to_offset_profiles.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_to_offset_profiles.rb
new file mode 100755
index 0000000..54b045b
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/dxf_to_offset_profiles.rb
@@ -0,0 +1,47 @@
+
+class Line2d
+	def to_offsetable(st, scale)
+		st = CompGeo::Point2di.new((st.x * scale).to_i, (st.y * scale).to_i)
+		ed = CompGeo::Point2di.new((@ed.x * scale).to_i, (@ed.y * scale).to_i)
+		return OffsetableLine.new(st, ed)
+	end
+	def to_offsetable_rev(ed, scale)
+		st = CompGeo::Point2di.new((@st.x * scale).to_i, (@st.y * scale).to_i)
+		ed = CompGeo::Point2di.new((ed.x * scale).to_i, (ed.y * scale).to_i)
+		return OffsetableLine.new(st, ed)
+	end
+end
+
+class QuadrentArc
+	def to_offsetable(st, scale)
+		st_ed_offsettable(st, ed, scale)
+	end
+	def to_offsetable_rev(ed, scale)
+		st_ed_offsettable(st, ed, scale)
+	end
+	def st_ed_offsettable(st, ed, scale)
+		st = CompGeo::Point2di.new((st.x * scale).to_i, (st.y * scale).to_i)
+		ed = CompGeo::Point2di.new((ed.x * scale).to_i, (ed.y * scale).to_i)
+		c = CompGeo::Point2di.new((@c.x * scale).to_i, (@c.y * scale).to_i)
+		OffsettableArc.new(st, ed, c, (r * scale).to_i)
+	end
+end
+
+class SegmentRef
+	class ReversedSegRef
+		def to_offsetable(st, scale)
+			@seg.to_offsetable_rev(st, scale)
+		end
+	end
+	def to_offsetable(st, scale)
+		@seg.to_offsetable(st, scale)
+	end
+end
+class TwinableProfile
+	def to_offsetable(scale)
+		@items.length.times.collect do |i|
+			item = @items[i] 
+			item.to_offsetable(item.st, scale)
+		end
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/offset_profiles.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/offset_profiles.rb
new file mode 100755
index 0000000..d0ca72d
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/offset_profiles.rb
@@ -0,0 +1,217 @@
+class XMonotonicSubdivisionArc
+	attr_accessor :st, :ed
+	def initialize(st, ed, d)
+		@st = st
+		@ed = ed
+		@d = d
+
+		@d_denom = 1
+
+		#d = d.abs
+		#a = (ed - st).rot90()
+		#mag = a.mag_sqr() 
+
+		#df = d.to_f
+		#@r_real = ((df) ** 2 + mag / 4) / (2 * df)
+
+#		@guard
+
+	end
+	def draw_subseq(cr, scale, st, ed, d, d_denom, d_limit, l)
+		sign = d <=> 0
+		d = d.abs
+		a = (ed - st).rot90()
+		mag = a.mag_sqr() 
+
+		magv = mag * d_denom * d_denom
+		df = d.to_f / d_denom
+		restimate = ((df) ** 2 + mag / 4) / (2 * df)
+		if (d < d_denom * d_limit)# * 4)
+			(@k &1 == 0 ? Color::Red : Color::Green).set(cr)
+			@k += 1
+			cr.move_to(st.x * scale, -st.y * scale)
+			cr.line_to(ed.x * scale, -ed.y * scale)
+			cr.stroke()
+		else
+			vec = (a * (sign * d * 2)) / integer_sqrt(magv)
+			mp = (st + ed + vec) / 2 + a.round_up_fudge
+
+			puts "restimate: #{l} -> #{restimate}"
+
+#			l1 = (st - mp).mag_sqr()
+#			l2 = (ed - mp).mag_sqr()
+
+			#psqr = ((restimate - df / 2)) ** 2 + mag / (4**2)
+			psqr = ((((df) ** 2 + mag / 4) / (2 * df) - df / 2)) ** 2 + mag / (4**2)
+			dnxt = restimate - Math.sqrt(psqr) #integer_sqrt(psqr)
+			d_denom = d_denom * 16 #* 16
+
+			#puts "d: #{df / dnxt}"
+			#puts "d: #{df} -> #{dnxt}"
+			dnxt = sign * (dnxt * d_denom).floor.to_i
+
+			draw_subseq(cr, scale, st, mp, dnxt, d_denom, d_limit, l + 1)
+			draw_subseq(cr, scale, mp, ed, dnxt, d_denom, d_limit, l + 1)
+
+
+			#arbn = ((4 * d * d_denom)) 
+			#arbd = (mag * d_denom * d_denom - d * d * 4)
+
+			#arbd2 = (restimate * d_denom - d) * 4 * 2 * d * d_denom
+			#puts "restimate: #{restimate}  arb: #{arbn} / #{arbd} arbd: #{arbd / arbd2}"
+=begin
+			s = 2 * Math.sqrt(mag) / ((restimate - df) * 4)
+
+
+			m = (a * s).to_i_vec + a.round_up_fudge * 2
+			#puts m.inspect
+			k = (m * sign + st + ed) / 2
+			
+			Color::Pink.set(cr)
+			cr.move_to(st.x * scale, -st.y * scale)
+			cr.line_to(k.x * scale, -k.y * scale)
+			cr.line_to(ed.x * scale, -ed.y * scale)
+			cr.stroke()
+=end
+		end
+	end
+	def draw_to(cr, scale)
+#		derr = (@st - @ed)
+#		p = derr.mag_sqr()
+
+#		num = 
+
+#		c = ((derr.rot90() * (2 * num)) / denom + (@st + @ed)) / 2
+		
+		@k = 0
+		st = @st
+#		cr.move_to(@st.x * scale, -@st.y * scale)
+		d_denom_limit = [1, (1 / scale)].max
+		#d_denom_limit = 1
+		draw_subseq(cr, scale, @st, @ed, @d * 1, @d_denom, d_denom_limit, 0)
+		cr.stroke()
+=begin
+		c = to_offsetable().c
+		Color::Pink.set(cr)
+		cr.line_to(@ed.x * scale, -@ed.y * scale)
+		cr.line_to(c .x * scale, -c.y * scale)
+		cr.line_to(@st.x * scale, -@st.y * scale)
+		cr.stroke()
+=end
+		#cr.arc(@c.x * scale, -@c.y * scale, @r * scale, @sa, @ea)
+	end
+	def append_subseq_to(segs, st, ed, d, d_denom, d_limit, l)
+		sign = d <=> 0
+		d = d.abs
+		a = (ed - st).rot90()
+		mag = a.mag_sqr() 
+
+		magv = mag * d_denom * d_denom
+		df = d.to_f / d_denom
+		restimate = ((df) ** 2 + mag / 4) / (2 * df)
+		if (d < d_denom * d_limit)# * 4)
+			segs << Shape::Segment.new(st, ed)
+		else
+			vec = (a * (sign * d * 2)) / integer_sqrt(magv)
+			mp = (st + ed + vec) / 2 + a.round_up_fudge
+
+			psqr = ((((df) ** 2 + mag / 4) / (2 * df) - df / 2)) ** 2 + mag / (4**2)
+			dnxt = restimate - Math.sqrt(psqr) #integer_sqrt(psqr)
+			d_denom = d_denom * 16 #* 16
+
+			dnxt = sign * (dnxt * d_denom).floor.to_i
+
+			append_subseq_to(segs, st, mp, dnxt, d_denom, d_limit, l + 1)
+			append_subseq_to(segs, mp, ed, dnxt, d_denom, d_limit, l + 1)
+		end
+	end
+	def to_offsetable()
+		sign = -(@d <=> 0)
+		d = @d.abs
+		a = (@st - @ed).rot90()
+		mag = a.mag_sqr() 
+
+		magv = mag * @d_denom * @d_denom
+		df = d.to_f / @d_denom
+		restimate = ((df) ** 2 + mag / 4) / (2 * df)
+		vec = (a * (sign * (d - (restimate * @d_denom).to_i) * 2)) / integer_sqrt(magv)
+
+		c = (@st + @ed + vec) / 2
+		OffsettableArc.new(@st, @ed, c, restimate.to_i)
+	end
+	def append_to(segs, scale)
+		append_subseq_to(segs, @st, @ed, @d * 1, @d_denom, scale, 0)
+	end
+	def flip()
+		@st, @ed = @ed, @st
+		@d = -@d
+	end
+end
+
+class OffsettableArc
+	attr_accessor :c
+	def initialize(st, ed, c, r)
+		@st, @ed, @c, @r = st, ed, c, r
+	end
+	def offset_pt(pt, dist)
+		v = (pt - @c)
+		magsqr = v.mag_sqr()
+		l = integer_sqrt(magsqr * 2**20)
+		v = (v * dist * 2**10) / l
+		return pt + v, pt - v
+	end
+	def offset(dist)
+		sign = (@ed - @c).cross(@st - @c) <=> 0
+		#lns = []
+		sto, sti = offset_pt(@st, dist)
+		edo, edi = offset_pt(@ed, dist)
+		#lns << OffsettableArc.new(sto, edo, @c, @r + dist)
+		#lns << OffsettableArc.new(sti, edi, @c, @r - dist)
+		#lns << OffsettableArc.new(sti, sto, @st, dist)
+		#lns << OffsettableArc.new(edo, edi, @ed, dist)
+		rlns = []
+		#lns.each { |ln| ln.to_xmonotone(rlns, sign) }
+		OffsettableArc.new(edo, sto, @c, @r + dist).to_xmonotone(rlns, -sign)
+		OffsettableArc.new(sto, sti, @st, dist).to_xmonotone(rlns, -sign)
+		OffsettableArc.new(sti, edi, @c, @r - dist).to_xmonotone(rlns, sign)
+		OffsettableArc.new(edi, edo, @ed, dist).to_xmonotone(rlns, -sign)
+		if (sign < 0)
+			rlns.each do |rln|
+				rln.flip()
+			end
+			rlns.reverse!
+		end
+		return rlns
+	end
+	def to_xmonotone(lns, sign)
+		magsqr = ((@st + @ed) - @c * 2).mag_sqr()
+		lns << XMonotonicSubdivisionArc.new(@st, @ed, sign * (@r - integer_sqrt(magsqr) / 2))
+	end
+end
+class TransferLine
+	def initialize(st, ed) @st, @ed = st, ed end
+	def append_to(segs, scale)
+		segs << Shape::Segment.new(@st, @ed)
+	end
+end
+class OffsetableLine
+	def initialize(st, ed)
+		@st, @ed = st, ed
+	end
+	def offset_pt(pt, dist)
+		v = (@ed - @st).rot90()
+		magsqr = v.mag_sqr()
+		l = integer_sqrt(magsqr * 2**20)
+		v = (v * dist * 2**10) / l
+		return pt + v, pt - v
+	end
+	def offset(dist)
+		sto, sti = offset_pt(@st, dist)
+		edo, edi = offset_pt(@ed, dist)
+		rlns = []
+		rlns << TransferLine.new(edo, sto)
+		OffsettableArc.new(sto, sti, @st, dist).to_xmonotone(rlns, -1)
+		rlns << TransferLine.new(sti, edi)
+		OffsettableArc.new(edi, edo, @ed, dist).to_xmonotone(rlns, -1)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/overlay.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/overlay.rb
new file mode 100755
index 0000000..f844803
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/overlay.rb
@@ -0,0 +1,498 @@
+require_relative "predicate.rb"
+
+class Shape
+	class Segment
+		init_fields :st, :ed
+	end
+	attr_accessor :segs
+	def initialize(segs)
+		@segs = segs
+	end
+	def draw_blank_to(cr, scale)
+#		cr.move_to(@segs[0].st.x * scale, -@segs[0].st.y * scale)
+		@segs.each do |seg|
+			cr.move_to(seg.st.x * scale, -seg.st.y * scale)
+			cr.line_to(seg.ed.x * scale, -seg.ed.y * scale)
+		end
+		cr.stroke()
+	end
+	def draw_to(cr, scale)
+#		cr.move_to(@segs[0].st.x * scale, -@segs[0].st.y * scale)
+		i = 0
+		@segs.each do |seg|
+			k = (1.0 * i) / (@segs.length - 1)
+			cr.set_source_rgb(1.0, k, 1.0 - k)
+			cr.move_to(seg.st.x * scale, -seg.st.y * scale)
+			cr.line_to(seg.ed.x * scale, -seg.ed.y * scale)
+			cr.stroke()
+			mp = (seg.st + seg.ed) * (2**-1 * scale)
+			v = (seg.ed - seg.st)
+			l = v.mag_sqr().to_i
+			if (l > 1000)
+			puts "#{v.mag_sqr()} -> #{v.mag_sqr().to_i}" if v.mag_sqr().to_i == 0
+			v = v * (integer_sqrt(v.mag_sqr().to_i * 2**10)**-1 * 2**5)
+			#puts "#{mp.x.inspect} #{mp.y.inspect}"
+			#puts "#{v.x.inspect} #{v.y.inspect}"
+			s1 = mp - v * 10 - v.rot90() * 5
+			s2 = mp - v * 10 + v.rot90() * 5
+			cr.move_to(mp.x, -mp.y)
+			cr.line_to(s1.x, -s1.y)
+			cr.line_to(s2.x, -s2.y)
+			cr.line_to(mp.x, -mp.y)
+			cr.stroke()
+			end
+			i += 1
+		end
+	end
+	def add_pts(pts)
+		@segs.each do |seg|
+			pts << seg.st
+		end
+	end
+	def add_evts(evts)
+		status_lines = @segs.collect do |seg|
+			side = x_lex_order(seg.st, seg.ed)
+			(side < 0) ? StatusLine.new(seg.st, seg.ed, 1) : StatusLine.new(seg.ed, seg.st, -1)
+		end
+
+		@segs.length.times do |i|
+			pt = @segs[i].st
+			to_add = []
+			to_del = []
+			((status_lines[i].side > 0) ? to_add : to_del) << status_lines[i]
+			((status_lines[i - 1].side < 0) ? to_add : to_del) << status_lines[i - 1]
+=begin
+			to_add.each do |spt|
+				puts "to_add: #{(spt.st - pt).inspect}"
+			end
+			to_del.each do |spt|
+				puts "to_del: #{(spt.ed - pt).inspect}"
+			end
+=end
+			evts.add_event(AddDelEvent.new(pt, to_add, to_del))
+		end
+	end
+end
+
+class EventQueue
+	attr_accessor :event_limit
+	def initialize()
+		@events = []
+	end
+	def flip()
+		@events.sort!() { |a, b| b <=> a }
+		@lnevents = @events
+		@events = []
+	end
+	def add_event(event)
+		if @event_limit
+			return if @event_limit && (event <=> @event_limit) <= 0
+		end
+		@events << event
+	end
+	def pop_min_helper()
+		@lnevents.sort!() { |a, b| b <=> a }
+		m1 = @lnevents[-1]
+		m2 = @events[-1]
+		return @lnevents.pop() if !m2
+		return @events.pop() if !m1
+		d = m1 <=> m2
+		#raise "events match!" if (d == 0)
+		return (d < 0 ? @lnevents : @events).pop()
+	end
+	def pop_min()
+		min = pop_min_helper()
+		#puts "min: #{min}"
+		while ((min2 = next_event()) && (min2 <=> min) == 0)
+			min.merge_in(min2)
+			pop_min()
+		end
+		return min
+	end
+	def next_event()
+		@lnevents.sort!() { |a, b| b <=> a }
+		m1 = @lnevents[-1]
+		m2 = @events[-1]
+		return m1 if !m2
+		return m2 if !m1
+		d = m1 <=> m2
+		raise "events match!" if (d == 0)
+		return d < 0 ? m1 : m2
+	end
+	def empty()
+		@events.empty?() && @lnevents.empty?() 
+	end
+end
+
+class StatusLine
+	attr_accessor :st, :ed, :side, :last_event
+	def initialize(st, ed, side) # side is int from -1 to 1
+		@st, @ed = st, ed
+		@side = side
+		@last_event = nil
+	end
+	def get_n_d_at(x)
+		n = st.y * (ed.x - x) + ed.y * (x - st.x)
+		return n, (ed.x - st.x)
+	end
+	def compare_at_x(x, o)
+		n,d = get_n_d_at(x)
+		no, d_o = o.get_n_d_at(x)
+		n * d_o <=> d * no
+	end
+	def compare_to_xy(x, pt)
+		n,d = get_n_d_at(x)
+		n <=> d * pt.y
+	end
+	def wrap_up(ed_pt)
+		@last_event.connect_to(ed_pt, @side)
+	end
+end
+
+class Status
+	def initialize()
+		@active = []
+	end
+	def add_lines(evts, to_adds)
+		st = to_adds[0].st
+		x = st.x
+		i = 0
+		i += 1 while (i < @active.length && @active[i].compare_to_xy(x, st) < 0)
+		imin = i
+		to_adds.each do |to_add|
+			@active.insert(i, to_add)
+		end
+		imax = to_adds.length + imin - 1
+		check_pair(evts, imin - 1) if (imin > 0)
+		check_pair(evts, imax) if (imax < @active.length - 1)
+	end
+	def check_pair(evts, ival)
+		if (@active[ival].ed != @active[ival + 1].ed && @active[ival].st != @active[ival + 1].st)
+			if (inter = intersect(@active[ival], @active[ival + 1]))
+				evts.add_event(InterEvent.make(inter, @active[ival], @active[ival + 1]))
+			end
+		end
+	end
+	def del_line(evts, to_del)
+		i = 0
+		i += 1 while (i < @active.length && @active[i] != to_del)
+#		raise "error" if i == @active.length
+		@active.delete_at(i)
+		return if (i <= 0 || i >= @active.length)
+		check_pair(evts, i - 1)
+	end
+	def inter_lines(evts, line1, line2)
+		i = 0
+		i += 1 while (i < @active.length && @active[i] != line1)
+		return false if i == @active.length
+		return false if @active[i] != line1 || @active[i + 1] != line2
+		@active[i], @active[i + 1] = @active[i + 1], @active[i]
+		check_pair(evts, i - 1) if (i > 0)
+		check_pair(evts, i + 1) if (i + 2 < @active.length)
+		return true
+	end
+	def get_regions(x, yst, yed)
+		#@active.sort! { |a, b|
+		#	a.compare_at_x(x, b)
+		#}
+		y_cur = yst
+		regions = []
+		cnt = 0
+		@active.each do |active|
+			n,d = active.get_n_d_at(x)
+			if (d != 0)
+				y_val = n * d**-1
+			else
+				y_val = active.st.y
+			end
+#			puts "yval: #{y_val}"
+			regions << [y_cur, y_val, cnt]
+			cnt += active.side
+			y_cur = y_val
+		end
+		regions << [y_cur, yed, cnt]
+	end
+end
+
+
+
+class PointInfo
+	class TwinnedLine
+		attr_accessor :twin, :st, :ed
+		def initialize(st, ed, side) @st, @ed, @side = st, ed, side end
+		def self.make(st, ed, side)
+			ln = self.new(st, ed, side) 
+			ltwin = self.new(ed, st, -side)
+			ln.twin = ltwin
+			ltwin.twin = ln
+			return ln
+		end
+		def to_seg()
+			return Shape::Segment.new(@st.pt, @ed.pt) 
+		end
+		def rot()
+			@st.rot(self)
+		end
+		def next()
+			@twin.rot()
+		end
+	end
+	def pt() @pt end
+	def initialize(pt)
+		raise "problem" if pt.class == Integer
+		@pt = pt
+		@cons = []
+		@ordered = nil
+	end
+	def do_dedup()
+		@cons.uniq!()
+	end
+	def compare_rot_order(a, b)
+		p1 = x_lex_order(a.ed.pt, @pt) <=> x_lex_order(b.ed.pt, @pt)
+		return p1 if p1 != 0
+		return (a.ed.pt - @pt).cross(b.ed.pt - @pt) <=> 0
+	end
+	def rot_order()
+		@ordered ||= ( 
+		@cons.sort! { |a, b|
+			compare_rot_order(a, b)
+		};
+		)
+	end
+	def rot(item)
+		rot_order()
+		@cons[(@cons.index(item) - 1) % @cons.length]
+	end
+	def connect_to(ept, side)
+		ln = TwinnedLine.make(self, ept, side)
+		insert_ln(ln)
+		ept.insert_ln(ln.twin)
+	end
+	def insert_ln(ln)
+		@cons << ln
+	end
+	# assuming pt is the lex min, get an outside line.
+	def lex_min_line()
+		rot_order()
+		@cons[0]
+	end
+end
+
+class AddDelEvent
+	def initialize(pt, to_add, to_del)
+		@pt = pt
+		@to_add = to_add
+		@to_del = to_del
+		@to_add.sort! { |a, b|
+			(a.ed - @pt).cross(b.ed - @pt) <=> 0
+		}
+		@point_info = PointInfo.new(@pt)
+	end
+	def <=>(o) x_lex_order(pt, o.pt) end
+	def pt() @pt end
+	def inspect()
+		n = "<#{pt.x}, #{pt.y}"
+		n += ", a:#{@to_add.length}" if @to_add.length > 0
+		n += ", d:#{@to_del.length}" if @to_del.length > 0
+		return n + ">"
+	end
+	def do_event(status, evts)
+		@to_del.each do |to_del|
+			status.del_line(evts, to_del)
+		end
+		status.add_lines(evts, @to_add) if @to_add.length > 0
+		@to_del.each do |to_del|
+			to_del.wrap_up(@point_info)
+		end
+		@to_add.each do |to_add|
+			to_add.last_event = @point_info
+		end
+	end
+	def lex_min_line() @point_info.lex_min_line() end
+	def to_del() @to_del.dup() end
+	def to_add() @to_add.dup() end
+	def merge_in(other)
+		@to_del += other.to_del()
+		@to_add += other.to_add()
+		@to_add.uniq()
+		@to_del.uniq()
+		@to_add.sort! { |a, b|
+			(a.ed - @pt).cross(b.ed - @pt) <=> 0
+		}
+	end
+end
+
+class InterEvent
+	def self.make(pt, item1, item2)
+		its = [item1, item2]
+		AddDelEvent.new(pt, its, its.dup())
+	end
+	def initialize(pt, item1, item2)
+		@pt = pt
+		@item1, @item2 = item1, item2
+		@point_info = PointInfo.new(@pt)
+	end
+	def inspect()
+		n = "<#{pt.x}, #{pt.y}"
+		return n + ">"
+	end
+	def <=>(o) x_lex_order(pt, o.pt) end
+	def pt() @pt end
+	def do_event(status, evts)
+		if status.inter_lines(evts, @item1, @item2)
+			@item1.wrap_up(@point_info)
+			@item1.last_event = @point_info 
+			@item2.wrap_up(@point_info)
+			@item2.last_event = @point_info 
+		end
+	end
+end
+
+class AddEvent
+	def initialize(to_add)
+		@to_add = to_add
+	end
+	def <=>(o) x_lex_order(pt, o.pt) end
+	def pt() @to_add.st() end
+	def inspect() "a<#{pt.x}, #{pt.y}>" end
+	def do_event(status)
+		status.add_line(@to_add)
+	end
+end
+
+class DelEvent
+	def initialize(to_del)
+		@to_del = to_del
+	end
+	def <=>(o) x_lex_order(pt, o.pt) end
+	def pt() @to_del.ed() end
+	def inspect() "d<#{pt.x}, #{pt.y}>" end
+	def do_event(status)
+		status.del_line(@to_del)
+	end
+end
+
+class LabeledLineList
+	def initialize()
+		@labs = []
+	end
+	attr_accessor :labs
+	def draw_to(cr, scale)
+		@labs.each do |ln|
+			st, ed = ln.st.pt, ln.ed.pt
+			cr.move_to(scale * st.x, -scale * st.y)
+			cr.line_to(scale * ed.x, -scale * ed.y)
+			cr.stroke()
+		end
+	end
+	def add_line(ln)
+		@labs << ln
+	end
+	def to_shape()
+		Shape.new(@labs.collect { |lab| lab.to_seg() })
+	end
+end
+
+class ActiveSweepLine
+	def initialize(evts)
+		@evts = evts
+		@status = Status.new()
+		@past_evts = []
+	end
+	def empty()
+		@evts.empty()
+	end
+	def tick()
+		min = @evts.pop_min()
+		@past_evts << min
+		@evts.event_limit = min
+		puts "processing event: #{min.inspect}"
+		@first_evt ||= min
+		@last_x = min.pt.x
+		min.do_event(@status, @evts)
+	end
+	def last_x()
+		return @last_x if @evts.empty()
+		return @evts.next_event().pt.x
+	end
+	def get_regions(x, st, ed)
+		@status.get_regions(x, st, ed)
+	end
+	def extract_polys()
+		labl = LabeledLineList.new()
+		lno = ln = @first_evt.lex_min_line().twin
+		i = 0
+		while (labl.add_line(ln) ; ln = ln.next(); ln != lno) 
+			i += 1
+			break if i == 20
+		end
+
+#		@first_evt.extract_polys()
+		return [labl.to_shape(), @past_evts]
+	end
+end
+
+class SweepLineUnion
+	def self.reset_sweep(shapes)
+		evts = EventQueue.new()
+		shapes.each do |shape|
+			shape.add_evts(evts)
+		end
+		evts.flip()
+		return ActiveSweepLine.new(evts)
+	end
+	def self.make_outside_profile(shapes)
+		sweep_line = reset_sweep(shapes)
+		nticks = 0
+		while !sweep_line.empty()
+			sweep_line.tick()
+			nticks += 1
+		end
+		return sweep_line.extract_polys(), nticks
+	end
+	def initialize(shapes)
+		@shapes = shapes
+		@poly, @nticks = self.class.make_outside_profile(shapes)
+		@sweep_line = self.class.reset_sweep(@shapes)
+		@ticki = 0
+	end
+	def n_views()
+		@nticks
+	end
+	CntColors = [Color::White, Color::Red, Color::Blue, Color.new(0.5, 0.5, 0.0), Color::Pink, Color.new(0.0,1.0,0.0)]
+	def draw_to(cr, scale, view_n)
+		if (@ticki > view_n)
+			@sweep_line = self.class.reset_sweep(@shapes)
+			@ticki = 0
+		end
+		while (@ticki < view_n)
+			@ticki += 1
+			@sweep_line.tick()
+		end
+
+		@shapes.each { |shape| shape.draw_to(cr, scale) }
+		cr.set_source_rgb(0.0, 0.5, 1.0)
+		last_x = @sweep_line.last_x
+		yed = -cr.get_clip_rectangle[1].y
+		yst = yed - cr.get_clip_rectangle[1].height
+#		puts cr.get_clip_rectangle[1].y
+#		puts cr.get_clip_rectangle[1].height
+
+		yst = ((yst + 10) / scale).to_i
+		yed = ((yed - 10) / scale).to_i
+
+		segs = @sweep_line.get_regions(last_x, yst, yed)
+		#puts segs.inspect
+		segs.each do |y1, y2, cnt|
+			CntColors[cnt].set(cr)
+#			cr.set_source_rgb(rand(), rand(), rand())
+			cr.move_to(last_x * scale, -y1 * scale)
+			cr.line_to(last_x * scale, -y2 * scale)
+			cr.stroke()
+		end
+
+		#Color::.set(cr)
+		cr.set_source_rgb(0.0, 1.0, 0.5)
+		@poly.draw_to(cr, scale)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/pairwise_inter.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/pairwise_inter.rb
new file mode 100755
index 0000000..3c447bd
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/pairwise_inter.rb
@@ -0,0 +1,101 @@
+require_relative "predicate.rb"
+
+def lex_order_pt(a, b)
+	return a, b if x_lex_order(a, b) < 0
+	return b, a
+end
+
+def intersect_parallel_filter(a, b)
+	ast, aed = lex_order_pt(a.st, a.ed)
+	bst, bed = lex_order_pt(b.st, b.ed)
+	return nil if x_lex_order(aed, bst) < 0
+	return nil if x_lex_order(bed, ast) < 0
+	return intersect(a, b)
+end
+
+def find_intersect_overlap_dup(ln1, ln2, inters, i, j)
+	ast, aed = lex_order_pt(ln1.st, ln1.ed)
+	bst, bed = lex_order_pt(ln2.st, ln2.ed)
+	return if x_lex_order(aed, bst) < 0
+	return if x_lex_order(bed, ast) < 0
+	return if !test_inter_or_parrallel(ln1, ln2)
+	return if !test_inter_or_parrallel(ln2, ln1)
+
+	a1, b1, c1 = get_sys_eqn(ln1)
+	a2, b2, c2 = get_sys_eqn(ln2)
+	d = a1 * b2 - b1 * a2
+	if d == 0
+		if (x_lex_order(ast, bst) > 0) #ast, aed
+			inters << [bed, i, j]
+			if (x_lex_order(ast, bed) != 0)
+				inters << [ast, i, j]
+			end
+		else # aed, bst
+			inters << [bst, i, j]
+			if (x_lex_order(aed, bst) != 0)
+				inters << [aed, i, j]
+			end
+		end
+		return
+	end
+	xn = c1 * b2 - c2 * b1
+	yn = c2 * a1 - c1 * a2
+	
+	inters << [Point2di.new(Rational(xn, d), Rational(yn, d)), i, j]
+end
+
+class AllPairsInter
+	def self.inter(shapes)
+		all_lns = []
+		shapes.each do |shape|
+			all_lns += shape.segs
+		end
+		inters = []
+		0.upto(all_lns.length - 1) do |i|
+			(i + 1).upto(all_lns.length - 1) do |j|
+				find_intersect_overlap_dup(all_lns[i], all_lns[j], inters, i, j)
+			end
+		end
+		inters.sort! { |a, b| x_lex_order(a[0], b[0]) }
+		prevs = [] 
+		prev_pt = nil
+		inters_lst = []
+		inters.each do |a, b, c|
+			if (!prev_pt)
+				prevs = []
+				prev_pt = a
+			elsif (x_lex_order(prev_pt, a) != 0)
+				inters_lst << [prev_pt, prevs.uniq()]
+				prevs = []
+				prev_pt = a
+			end
+			prevs << b << c
+		end
+		inters_lst << [prev_pt, prevs.uniq()] if prev_pt
+
+		line_last_inter = []
+		pt_infos = inters_lst.collect do |pt, is|
+			pt_i = PointInfo.new(pt)
+			#puts is.inspect
+			is.each do |i|
+				if line_last_inter[i]
+					line_last_inter[i].connect_to(pt_i, -1)
+				end
+				line_last_inter[i] = pt_i
+			end
+			pt_i
+		end
+		pt_infos.each do |pt_info|
+			pt_info.do_dedup()
+		end
+		
+		labl = LabeledLineList.new()
+		lno = ln = pt_infos[0].lex_min_line().twin
+		i = 0
+		while (labl.add_line(ln) ; ln = ln.next(); ln != lno) 
+			i += 1
+		end
+
+		return labl.to_shape() 
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/bool_ops/predicate.rb b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/predicate.rb
new file mode 100755
index 0000000..93c20e1
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/bool_ops/predicate.rb
@@ -0,0 +1,51 @@
+def x_lex_order(pta, ptb)
+	return pta.x <=> ptb.x if pta.x != ptb.x
+	return pta.y <=> ptb.y
+end
+
+# test that line1 intersects line2
+def test_inter(ln1, ln2)
+	st = ln1.st
+	tst = ln1.ed - st
+	as = tst.cross(ln2.st - st) <=> 0
+	bs = tst.cross(ln2.ed - st) <=> 0
+	
+	raise "colinear" if as == 0 && bs == 0
+	return true if as == 0 || bs == 0
+	return true if as != bs
+	return false
+end
+
+# test that line1 intersects line2
+def test_inter_or_parrallel(ln1, ln2)
+	st = ln1.st
+	tst = ln1.ed - st
+	as = tst.cross(ln2.st - st) <=> 0
+	bs = tst.cross(ln2.ed - st) <=> 0
+	
+	return true if as == 0 && bs == 0
+	return true if as == 0 || bs == 0
+	return true if as != bs
+	return false
+end
+
+def get_sys_eqn(ln)
+	a = ln.ed.y - ln.st.y
+	b = ln.st.x - ln.ed.x
+	c = ln.st.x * a + ln.st.y * b
+	return a, b, c
+end
+
+def intersect(ln1, ln2)
+	# puts "intersecting: #{ln1} #{ln2}"
+	return nil if !test_inter(ln1, ln2)
+	return nil if !test_inter(ln2, ln1)
+
+	a1, b1, c1 = get_sys_eqn(ln1)
+	a2, b2, c2 = get_sys_eqn(ln2)
+	d = a1 * b2 - b1 * a2
+	raise "problem!" if d == 0
+	xn = c1 * b2 - c2 * b1
+	yn = c2 * a1 - c1 * a2
+	return Point2di.new(Rational(xn, d), Rational(yn, d))
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/2dpoint.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/2dpoint.rb
new file mode 100755
index 0000000..8996429
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/2dpoint.rb
@@ -0,0 +1,53 @@
+#system("bazel build -c opt //geometry/voronoi:test.so")
+
+require "tokens/tokenizer_lib_base.rb"
+#require "geometry/voronoi/test.so"
+class CompGeo
+end
+
+class CompGeo::Point2di
+	init_fields :x, :y
+	def self.make(a, b)
+		CompGeo::Point2di.new(a, b)
+	end
+	def +(o) CompGeo::Point2di.new(@x + o.x, @y + o.y) end
+	def -(o) CompGeo::Point2di.new(@x - o.x, @y - o.y) end
+	def /(o) CompGeo::Point2di.new(@x / o, @y / o) end
+	def *(o) CompGeo::Point2di.new(@x * o, @y * o) end
+	def mag_sqr() return @x * @x + @y * @y end
+	def xf() @x.to_f end
+	def yf() @y.to_f end
+	def to_i_vec() CompGeo::Point2di.new(@x.to_i, @y.to_i) end
+	def round_up_fudge() CompGeo::Point2di.new(@x <=> 0, @y <=> 0) end
+	def dot(o) @x * o.x + @y * o.y end
+#	alias :old_cross :cross
+	def cross(o)
+		v = @x * o.y - @y * o.x
+		#v2 = old_cross(o)
+		#raise "problem! #{v} #{v2}" if (v != v2)
+		return v
+	end
+	def ==(o) self.class == o.class && @x == o.x && @y == o.y end
+	
+	def dist(o)
+		return ArcPointTime.new((self - o).mag_sqr(), 0)
+	end
+	def rot90()
+		return CompGeo::Point2di.new(-@y, @x) 
+	end
+end
+Point2di = CompGeo::Point2di
+
+def integer_sqrt(n)
+	n = n * 4
+	i = n >> (n.bit_length / 2)
+	oi = n 
+	k = 0
+	while (oi / 2 != i / 2 && k < 100)
+		oi = i
+		i = (n / i + i) / 2
+		k += 1
+	end
+	raise "integer sqrt did not converge #{n}" if k == 100
+	return i / 2
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf.rb
new file mode 100755
index 0000000..96cbbe7
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf.rb
@@ -0,0 +1,16 @@
+require_relative "dxf_parse.rb"
+require_relative "entities.rb"
+require_relative "profile_utils.rb"
+require_relative "entity_draw.rb"
+require_relative "make_loops.rb"
+require_relative "2dpoint.rb"
+
+
+def full_load_dxf(fname)
+	items = get_entities(fname)
+	items2d = items.collect { |item| item.to_2d() }
+
+	items2d_orient = Oriented.import_2d(items2d)
+	items2d_orient = SweepLine.stitch_curves(items2d_orient)
+	return items2d_orient
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf_parse.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf_parse.rb
new file mode 100755
index 0000000..42b9460
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/dxf_parse.rb
@@ -0,0 +1,248 @@
+class DXFParser
+	class Section
+		attr_accessor :sec_name
+		def initialize(sec_name)
+			@sec_name = sec_name
+			@items = []
+		end
+
+		def print_sec()
+			puts "section #{@sec_name}:"
+			@items.each do |item|
+				puts "  #{item.inspect}"
+			end
+		end
+
+		def add_item(item)
+			@items << item
+		end
+
+		def self.make_section(sec_name)
+			return if sec_name != "ENTITIES"
+			return Section.new(sec_name)
+		end
+	end
+	class GroupPair
+		attr_accessor :group_code, :value
+		def initialize(file)
+			@group_code = file.gets().match(/\d+/)[0].to_i
+			@value = file.gets().chomp()
+			if (@group_code == 0)
+				@value.upcase!
+			end
+#		rescue
+#			raise Exception.new("dxf file does not follow expected format.")
+		end
+		def inspect()
+			"#{@group_code}: #{@value.inspect}"
+		end
+	end
+	def initialize(fname)
+		@file = fname.class == File ? fname : File.open(fname, "r")
+		@seen_eof = false
+	end
+	def get_next()
+		return GroupPair.new(@file)
+	end
+	def expect_next(code)
+		obj = get_next()
+		raise "expected group_code: #{code} not #{obj.inspect}" if obj.group_code != code
+		return obj
+	end
+	def get_section(section_parser = Section)
+		while true
+			return if @seen_eof
+			obj = expect_next(0)
+			if obj.value == "EOF"
+				@seen_eof = true
+				return
+			end
+			raise "expected SECTION\n" if (obj.value != "SECTION")
+			secname = expect_next(2).value
+			secval = section_parser.make_section(secname)
+			while true
+				obj = get_next()
+				if (obj.group_code == 0 && obj.value == "ENDSEC")
+					if secval
+						secval.finished_parsing() if secval.respond_to?(:finished_parsing)
+						return secval
+					end
+					break
+				end
+				secval.add_item(obj) if secval
+			end
+		end
+	end
+	class SectionRep
+		def self.set_section_type(section_type) @section_type = section_type end
+		def self.section_type() @section_type end
+		def self.make_section(section)
+			return self.new()
+		end
+	end
+	class Entities < SectionRep
+		set_section_type "ENTITIES"
+		def initialize()
+		end
+		def add_item(item)
+			if (item.group_code == 0)
+			end
+		end
+	end
+	class EntityParse
+		class Instance
+			def initialize(entity_parse)
+				@entity_parse = entity_parse
+				@items = []
+			end
+			attr_accessor :items
+			def add_item(item)
+				if (item.group_code == 0)
+					@items.push(@last_item) if @last_item
+					@last_item = nil
+					@item_parse = @entity_parse.item_parser(item.value)
+					@last_item = @item_parse.start_item() if @item_parse
+				else
+					@item_parse.handle(item, @last_item) if(@item_parse)
+				end
+			end
+			def print_sec()
+				puts "section ENTITIES:"
+				@items.each do |item|
+					puts item.inspect
+				end
+			end
+		end
+		class ItemParser
+			def initialize(item_type) @item_type = item_type
+				@handlers = {}
+			end
+			def add(group_code, &parse_block)
+				@handlers[group_code] = parse_block
+			end
+			def handle(item, obj)
+				handler = @handlers[item.group_code]
+				if (handler)
+					handler.call(obj, item.value)
+				elsif obj.respond_to?(:extras)
+					obj.extras(item)
+				end
+			end
+			def start_item()
+				return @item_type.new()
+			end
+		end
+		def section_type() return "ENTITIES" end
+		def initialize()
+			@item_parsers = {}
+		end
+		def add_item_type(key, parser)
+			@item_parsers[key] = ItemParser.new(parser)
+		end
+		def item_parser(key)
+			return @item_parsers[key]
+		end
+		def make_section(section)
+			return Instance.new(self)
+		end
+	end
+	class DelagateParser
+		def initialize(parser_list)
+			@per_section = {}
+			parser_list.each do |parser|
+				@per_section[parser.section_type()] = parser
+			end
+		end
+		def make_section(section)
+			return if !(parse = @per_section[section])
+			return parse.make_section(section)
+		end
+	end
+	JustEntities = DelagateParser.new([Entities])
+end
+
+class Point3d
+	attr_accessor :x, :y, :z
+	def initialize(x, y, z) @x, @y, @z = x, y, z end
+	def inspect() "<#{@x}, #{@y}, #{@z}>" end
+	def project() Point2d.new(@x, @y) end
+end
+class Line
+	attr_accessor :st, :ed
+	def initialize()
+		@st = Point3d.new(0.0, 0.0, 0.0)
+		@ed = Point3d.new(0.0, 0.0, 0.0)
+	end
+	def draw_to(cr, scale)
+		cr.move_to(@st.x * scale, - @st.y * scale)
+		cr.line_to(@ed.x * scale, - @ed.y * scale)
+		cr.stroke()
+	end
+	def corners() return [@st.project, @ed.project] end
+	def to_2d() return Line2d.new(@st.project, @ed.project) end
+end
+class Arc
+	attr_accessor :c, :r, :st, :ed
+	def initialize()
+		@c = Point3d.new(0.0, 0.0, 0.0)
+	end
+	def draw_to(cr, scale)
+		cr.arc(@c.x * scale, - @c.y * scale, @r * scale, -@ed * Math::PI / 180.0, -@st * Math::PI / 180.0)
+		cr.stroke()
+	end
+	def corners()
+		c = @c.project()
+		st_theta = @st * Math::PI / 180.0
+		ed_theta = @ed * Math::PI / 180.0
+		[
+		 c + Point2d.new(@r * Math.cos(st_theta), @r * Math.sin(st_theta)),
+		 c + Point2d.new(@r * Math.cos(ed_theta), @r * Math.sin(ed_theta)),
+		]
+	end
+	def to_2d() return Arc2d.new(@c.project, @r, @st / 180.0 * Math::PI, @ed / 180.0 * Math::PI) end
+end
+class Circle
+	attr_accessor :c, :r
+	def initialize()
+		@c = Point3d.new(0.0, 0.0, 0.0)
+	end
+	def draw_to(cr, scale)
+		cr.arc(@c.x * scale, - @c.y * scale, @r * scale, 0, Math::PI * 2)
+		cr.stroke()
+	end
+	def corners() return [] end
+	def to_2d() return CircleProfile2d.new(@c.project, @r) end
+end
+def get_entities(filename)
+	entity_parse = DXFParser::EntityParse.new()
+	line_parse = entity_parse.add_item_type("LINE", Line)
+	line_parse.add(10) { |l, v| l.st.x = v.to_f}
+	line_parse.add(20) { |l, v| l.st.y = v.to_f}
+	line_parse.add(30) { |l, v| l.st.z = v.to_f}
+	line_parse.add(11) { |l, v| l.ed.x = v.to_f}
+	line_parse.add(21) { |l, v| l.ed.y = v.to_f}
+	line_parse.add(31) { |l, v| l.ed.z = v.to_f}
+
+	line_parse = entity_parse.add_item_type("ARC", Arc)
+	line_parse.add(10) { |l, v| l.c.x = v.to_f}
+	line_parse.add(20) { |l, v| l.c.y = v.to_f}
+	line_parse.add(30) { |l, v| l.c.z = v.to_f}
+	line_parse.add(40) { |l, v| l.r = v.to_f}
+	line_parse.add(50) { |l, v| l.st = v.to_f}
+	line_parse.add(51) { |l, v| l.ed = v.to_f}
+
+	line_parse = entity_parse.add_item_type("CIRCLE", Circle)
+	line_parse.add(10) { |l, v| l.c.x = v.to_f}
+	line_parse.add(20) { |l, v| l.c.y = v.to_f}
+	line_parse.add(30) { |l, v| l.c.z = v.to_f}
+	line_parse.add(40) { |l, v| l.r = v.to_f}
+
+	parser = DXFParser::DelagateParser.new([entity_parse])
+	dxf_file = DXFParser.new(filename)
+	#dxf_file = DXFParser.new("flat_dxf.dxf") 
+
+	sec = dxf_file.get_section(parser)
+
+	raise "error! two entity sections" if dxf_file.get_section(parser) != nil
+	return sec.items
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entities.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entities.rb
new file mode 100755
index 0000000..e130d0f
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entities.rb
@@ -0,0 +1,292 @@
+require 'set'
+require_relative "../point2d.rb"
+
+class Line2d
+	init_fields :st, :ed
+end
+
+class Arc2d
+	init_fields :c, :r, :sta, :eda
+	def pt_for(a) @c + Point2d.new(@r * Math.cos(a), @r * Math.sin(a)) end
+	def st() pt_for(@sta) end
+	def ed() pt_for(@eda) end
+end
+
+class CircleProfile2d
+	init_fields :c, :r
+	def prepare_sweepline()
+	end
+end
+
+
+class LoopProfile2d
+	def initialize(items)
+		@items = items
+	end
+end
+
+class PtRef
+	init_fields :pt, :obj, :key
+	attr_accessor :refpair, :pair
+	def inspect() "#{@pt.inspect}" end
+	def comp_v(o) @pt.comp_v(o.pt) end
+	def comp_h(o) @pt.comp_h(o.pt) end
+	def self.add_pair(item, k, pts)
+		pts << a = PtRef.new(item.st, k, pts)
+		pts << b = PtRef.new(item.ed, item, k)
+		a.refpair = b
+		b.refpair = a
+	end
+	def self.add_twin_pair(item, k, pts)
+		pts << a = PtRef.new(item.st, item, k)
+		pts << b = PtRef.new(item.twin.st, item.twin, k)
+		a.refpair = b
+		b.refpair = a
+	end
+end
+
+def point_ref_cluster_x(pts, broken, tolerance) 
+	return if (pts.length == 0)
+	pts.sort!() { |a, b| a.comp_h(b) } 
+	
+	broken_pts = []
+	old_pt_pt_x = pts[0].pt.x
+
+	pts.each do |pt|
+		if (pt.pt.x - old_pt_pt_x > tolerance)
+			broken << broken_pts
+			broken_pts = []
+		end
+		broken_pts << pt
+		old_pt_pt_x = pt.pt.x
+	end
+	broken << broken_pts
+end
+
+def point_ref_cluster_y(pts, broken, tolerance) 
+	return if (pts.length == 0)
+	pts.sort!() { |a, b| a.comp_v(b) } 
+	
+	broken_pts = []
+	old_pt_pt_y = pts[0].pt.y
+
+	pts.each do |pt|
+		if (pt.pt.y - old_pt_pt_y > tolerance)
+			broken << broken_pts
+			broken_pts = []
+		end
+		broken_pts << pt
+		old_pt_pt_y = pt.pt.y
+	end
+	broken << broken_pts
+end
+
+
+def clock_wise_cluster(a, b)
+	#puts " --- #{a.inspect} --- #{b.inspect} --- "
+	if a.obj.st.comp_v(a.obj.ed) != a.obj.st.comp_v(b.obj.ed)
+		flip = a.obj.ed.comp_v(b.obj.ed)
+	else
+#		puts orient2d(a.obj.st, a.obj.ed, b.obj.ed)
+	end
+	return flip > 0 ? b : a
+end
+
+def point_ref_cluster(pts, tolerance)
+	pts = [pts]
+	broken = []
+	pts.each do |segment|
+		point_ref_cluster_x(segment, broken, tolerance)
+	end
+	pts = broken
+	broken = []
+	pts.each do |segment|
+		point_ref_cluster_y(segment, broken, tolerance)
+	end
+	return broken
+end
+
+def point_ref_cluster_and_validate(pts, tolerance)
+	clusters = point_ref_cluster(pts, tolerance)
+
+	clusters.each do |cluster|
+		if (cluster.length > 2)
+			raise "profile features too fine for tolerance = #{tolerance}" 
+		end
+		if (cluster.length < 2)
+			#raise "sketch has open contours..."
+			cluster[0].pair = cluster[0]
+		else
+		cluster[0].pair = cluster[1]
+		cluster[1].pair = cluster[0]
+		end
+		#cluster[0].pair = cluster[1]
+		#cluster[1].pair = cluster[0]
+	end
+	clusters.sort! { |a, b|
+		a[0].comp_h(b[0])
+	}
+
+	return clusters
+end
+
+def join_entities_into_loop_graph(item_list, tolerance)
+	profiles = []
+	pts = []
+
+	k = 0
+	item_list.each do |item|
+		if (item.respond_to?(:st))
+			PtRef.add_pair(item, k, pts)
+			k += 1
+		else
+			profiles << item
+		end
+	end
+
+	clusters = point_ref_cluster_and_validate(pts, tolerance)
+
+	visited = []
+
+	pts.each do |ptref|
+		if (!visited[ptref.key])
+			sub_profile = []
+			while !visited[ptref.key]
+				visited[ptref.key] = true
+				sub_profile << ptref
+				ptref = ptref.pair.refpair
+			end
+			profiles << LoopProfile2d.new(sub_profile)
+		end
+	end
+
+	return profiles
+end
+
+require 'algorithms'
+require "containers/priority_queue.rb"
+
+class ForkEvent
+	attr_accessor :profile_id
+	init_fields :pt, :obj1, :obj2
+	def draw_to(cr, scale)
+		cr.set_source_rgb(0.0, 1.0, 0.0)
+		cr.arc(@pt.x * scale, - @pt.y * scale, 0.05 * scale, 0, 6.25)
+		cr.fill()
+	end
+end
+
+class MergeEvent
+	attr_accessor :profile_id
+	init_fields :pt, :obj1, :obj2
+	def draw_to(cr, scale)
+		cr.set_source_rgb(1.0, 0.0, 0.0)
+		cr.arc(@pt.x * scale, - @pt.y * scale, 0.05 * scale, 0, 6.25)
+		cr.fill()
+	end
+end
+
+class ChainEvent
+	attr_accessor :profile_id
+	init_fields :pt, :obj_in, :obj_out
+	def draw_to(cr, scale)
+		cr.set_source_rgb(1.0, 0.0, 0.0)
+		cr.arc(@pt.x * scale, - @pt.y * scale, 0.05 * scale, 0, 6.25)
+		cr.fill()
+	end
+end
+
+class EventObj
+	def fork_event(other) ForkEvent.new(st(), self, other)  end
+	def merge_event(other) MergeEvent.new(ed(), self, other) end
+	def st_chain_event(other) ChainEvent.new(st(), self, other) end
+	def ed_chain_event(other) ChainEvent.new(ed(), other, self) end
+end
+
+class UpperArc < EventObj
+	init_fields :sta, :eda, :c, :r
+	def pt_for(a) @c + Point2d.new(@r * Math.cos(a), @r * Math.sin(a)) end
+	def st() pt_for(@sta) end
+	def ed() pt_for(@eda) end
+end
+
+class LowerArc < EventObj
+	init_fields :sta, :eda, :c, :r
+	def pt_for(a) @c + Point2d.new(@r * Math.cos(a), @r * Math.sin(a)) end
+	def st() pt_for(@sta) end
+	def ed() pt_for(@eda) end
+end
+
+class LinePt < EventObj
+	init_fields :st, :ed
+end
+
+module Math; PI2 = Math::PI * 2; end
+
+class Arc2d
+	def get_add_events_to(sweep, i)
+		if (@sta < Math::PI && @eda > Math::PI)
+			larc = UpperArc.new(Math::PI, @sta, @c, @r)
+			uarc = LowerArc.new(-Math::PI, @eda - Math::PI2, @c, @r)
+			puts "arc2d: #{@sta / Math::PI} #{@eda / Math::PI}"
+
+			sweep.add_event(larc.fork_event(uarc), i)
+		end
+		if (@eda < @sta && @sta != 0 && @eda != 0)
+			larc = UpperArc.new(@sta, 0, @c, @r)
+			uarc = LowerArc.new(@eda - Math::PI2, 0, @c, @r)
+			puts "arc2d: #{@sta / Math::PI} #{@eda / Math::PI}"
+
+			sweep.add_event(larc.merge_event(uarc), i)
+		end
+	end
+end
+
+class Line2d
+	def get_add_events_to(sweep, i)
+		if (@st.comp_h(@ed) < 0)
+			sweep.add_event(ChainEvent.new(@st, self, nil), i)
+		else
+			sweep.add_event(ChainEvent.new(@ed, self, nil), i)
+		end
+	end
+end
+
+class LoopProfile2d
+	def add_events_to(sweep, i)
+		@items.each do |item|
+			item.obj.get_add_events_to(sweep, i)
+		end
+	end
+end
+
+class CircleProfile2d
+	def add_events_to(sweep, i)
+		obju = UpperArc.new(Math::PI, 0, @c, @r)
+		objl = LowerArc.new(-Math::PI, 0, @c, @r)
+		sweep.add_event(obju.fork_event(objl), i)
+		sweep.add_event(obju.merge_event(objl), i)
+	end
+end
+
+class SweepLineConnectivity
+	class ArcSweepLinePoint
+
+	end
+	attr_accessor :events
+	def initialize()
+		@profile_ids = []
+		@events = []
+	end
+	def add_event(evt, i)
+		evt.profile_id = i
+		@events << evt
+	end
+	def get_lex_min_profile()
+		@events.sort! { |a, b| a.pt.comp_h(b.pt) }
+		@profile_ids[@events[0].profile_id]
+	end
+	def set_profile_id(i, profile)
+		@profile_ids[i] = profile
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entity_draw.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entity_draw.rb
new file mode 100755
index 0000000..4187615
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/entity_draw.rb
@@ -0,0 +1,102 @@
+require_relative "entities.rb"
+class TwinableProfile
+	def draw_to(cr, scale)
+		@items.each do |item|
+			cr.set_source_rgb(rand(), rand(), rand())
+			item.seg.draw_to(cr, scale)
+		end
+	end
+	def draw_path_to(cr, scale)
+		draw_to(cr, scale)
+		return;
+	#	@items[0].draw_start_path(cr, scale)
+		@items.each do |item|
+			item.draw_path_to(cr, scale)
+		end
+		cr.stroke()
+	end
+	def draw_direction_to(cr, scale)
+		@items.each do |item|
+			item.draw_direction_to(cr, scale)
+		end
+	end
+end
+
+class SegmentRef
+	class ReversedSegRef
+		def draw_to(cr, scale)
+			@seg.draw_to(cr, scale)
+			draw_direction_to(cr, scale)
+		end
+		def draw_start_path(cr, scale)
+			@seg.draw_rev_start_path(cr, scale)
+		end
+		
+		def draw_path_to(cr, scale)
+			@seg.draw_rev_path_to(cr, scale)
+		end
+		def draw_direction_to(cr, scale)
+			mp, tang = @seg.midpoint_and_tangent()
+	#		cr.set_source_rgb(0, 1, 1)
+			tang = tang.scale(-0.05)
+			arrt = 0.5
+			cr.move_to((mp.x + tang.x) * scale, -(mp.y + tang.y) * scale)
+			cr.line_to((mp.x - arrt * tang.y) * scale, -(mp.y + arrt * tang.x) * scale)
+			cr.line_to((mp.x + arrt * tang.y) * scale, -(mp.y - arrt * tang.x) * scale)
+			cr.line_to((mp.x + tang.x) * scale, -(mp.y + tang.y) * scale)
+			cr.fill()
+		end
+	end
+	def draw_path_to(cr, scale)
+		@seg.draw_path_to(cr, scale)
+	end
+	def draw_start_path(cr, scale)
+		@seg.draw_start_path(cr, scale)
+	end
+	def draw_to(cr, scale)
+		@seg.draw_to(cr, scale)
+		draw_direction_to(cr, scale)
+	end
+	def draw_direction_to(cr, scale)
+		mp, tang = @seg.midpoint_and_tangent()
+#		cr.set_source_rgb(0, 1, 1)
+		tang = tang.scale(0.05)
+		arrt = 0.5
+		cr.move_to((mp.x + tang.x) * scale, -(mp.y + tang.y) * scale)
+		cr.line_to((mp.x - arrt * tang.y) * scale, -(mp.y + arrt * tang.x) * scale)
+		cr.line_to((mp.x + arrt * tang.y) * scale, -(mp.y - arrt * tang.x) * scale)
+		cr.line_to((mp.x + tang.x) * scale, -(mp.y + tang.y) * scale)
+		cr.fill()
+	end
+end
+
+class QuadrentArc
+	def draw_to(cr, scale)
+#		puts "arclen: #{@eda - @sta} "
+		cr.arc(@c.x * scale, - @c.y * scale, @r * scale, -@eda, -@sta)
+		cr.stroke()
+	end
+	def draw_path_to(cr, scale)
+		cr.arc_negative(@c.x * scale, - @c.y * scale, @r * scale, -@sta, -@eda)
+		#cr.stroke()
+	end
+	def draw_rev_path_to(cr, scale)
+		cr.arc(@c.x * scale, - @c.y * scale, @r * scale, -@eda, -@sta)
+	end
+end
+
+class Line2d
+	def draw_to(cr, scale)
+		cr.move_to(@st.x * scale, - @st.y * scale)
+		cr.line_to(@ed.x * scale, - @ed.y * scale)
+		cr.stroke()
+	end
+	def draw_path_to(cr, scale)
+		#cr.move_to(@st.x * scale, - @st.y * scale)
+		cr.line_to(@ed.x * scale, - @ed.y * scale)
+	end
+	def draw_rev_path_to(cr, scale)
+		#cr.move_to(@ed.x * scale, - @ed.y * scale)
+		cr.line_to(@st.x * scale, - @st.y * scale)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/make_loops.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/make_loops.rb
new file mode 100755
index 0000000..defccc8
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/make_loops.rb
@@ -0,0 +1,168 @@
+module SweepLine
+	def self.order_st_ed(items)
+		items.collect { |item|
+			if (item.st().comp_h(item.ed()) > 0)
+				item.twin()
+			else
+				item
+			end
+		}
+	end
+	def self.stitch_curves(items, tolerance = 0.00001)
+		items_out = []
+		pts = []
+		items.each.with_index { |item, k| PtRef.add_twin_pair(item, k, pts) }
+
+		clusters = point_ref_cluster_and_validate(pts, tolerance)
+
+		visited = []
+		clusters.each do |a, b|
+			ptref = a
+			if (!visited[ptref.key])
+				ptref = clock_wise_cluster(a, b)
+				sub_profile_a = []
+				sub_profile_b = []
+				while !visited[ptref.key]
+					visited[ptref.key] = true
+					sub_profile_a << ptref.obj
+					sub_profile_b << ptref.obj.twin
+					ptref = ptref.pair.refpair
+				end
+				sub_profile_a.reverse!
+				items_out << TwinableProfile.make_pair(sub_profile_a, sub_profile_b)
+				#profiles << LoopProfile2d.new(sub_profile)
+			end
+		end
+		return items_out
+	end
+end
+
+class TwinableProfile
+	def self.make_pair(a, b)
+		a, b = self.new(a), self.new(b)
+		a.twin = b
+		b.twin = a
+		return a
+	end
+	attr_accessor :twin, :items
+	def initialize(items)
+		@items = items
+	end
+end
+
+module ProfileOffset
+	def self.add_if_is_tangent(a, b)
+		tana = a.tangent_at(a.eda)
+		tanb = b.tangent_at(b.sta)
+		return nil if (tana.area(tanb) > -1e-6 && tana.dot(tanb) > 0.0)
+		puts "#{tana.inspect} #{tanb.inspect} = #{tana.area(tanb)}"
+
+		sta = Math.atan2(-tana.x, tana.y)
+		eda = Math.atan2(-tanb.x, tanb.y)
+		join_arcs = []
+		sta += Q4 if (sta < 0)
+		eda += Q4 if (eda < 0)
+		Arc2d.new(a.ed, 0, sta, eda).to_oriented(join_arcs)
+		return join_arcs.collect { |a| SegmentRef.make_pair(a) }
+	end
+	def self.offset(profile, dist)
+		sub_profile_a = []
+		sub_profile_b = []
+		items = profile.items
+		items.each.with_index do |item, i|
+			if (join_arcs = add_if_is_tangent(items[i - 1], items[i]))
+				join_arcs.each do |join_arc|
+					join_arc = join_arc.offset(dist)
+					#dist -= 0.025
+					sub_profile_a << join_arc
+					sub_profile_b << join_arc.twin
+				end
+			end
+			item_off = item.offset(dist)
+			dist -= 0.025
+			sub_profile_a << item_off
+			sub_profile_b << item_off.twin
+		end
+		sub_profile_b.reverse!
+		return TwinableProfile.make_pair(sub_profile_a, sub_profile_b)
+	end
+	def self.prepare_for_offset(profile)
+		sub_profile_a = []
+		sub_profile_b = []
+		items = profile.items
+		items.each.with_index do |item, i|
+			if (join_arcs = add_if_is_tangent(items[i - 1], items[i]))
+				join_arcs.each do |join_arc|
+					sub_profile_a << join_arc
+					sub_profile_b << join_arc.twin
+				end
+			end
+			sub_profile_a << item
+			sub_profile_b << item.twin
+		end
+		sub_profile_b.reverse!
+		return TwinableProfile.make_pair(sub_profile_a, sub_profile_b)
+	end
+	def self.just_offset(profile, dist)
+		sub_profile_a = []
+		sub_profile_b = []
+		items = profile.items
+		items.each.with_index do |item, i|
+			item_off = item.offset(dist)
+			sub_profile_a << item_off
+			sub_profile_b << item_off.twin
+		end
+		sub_profile_b.reverse!
+		return TwinableProfile.make_pair(sub_profile_a, sub_profile_b)
+	end
+	def self.neighbor_curves(profile)
+		#profile = prepare_for_offset(profile)
+		#profile = just_offset(profile, 0.05)
+		a, dbg = VoronoiDiagramConstructor.neighbor_curves(profile)
+		return dbg
+	end
+end
+
+class SegmentRef
+	class ReversedSegRef
+		def offset(dist)
+			return @seg.offset(-dist).twin
+		end
+	end
+	def offset(dist)
+		return @seg.offset(dist)
+	end
+end
+
+class QuadrentArc
+	def offset(dist)
+		q = QuadrentArc.new()
+		q.c, q.r, q.sta, q.eda = @c, @r + dist, @sta, @eda
+
+		if q.r < 0
+			if (q.sta < Q2)
+				q.sta += Q2
+				q.eda += Q2
+			else
+				q.sta -= Q2
+				q.eda -= Q2
+			end
+			q.r = -q.r
+			puts "I was flipped"
+		end
+		return SegmentRef.make_pair(q)
+	end
+end
+
+class Line2d
+	def offset(dist)
+		ptoff = (@st - @ed).rot90.normalize().scale(dist)
+		return SegmentRef.make_pair(Line2d.new(@st + ptoff, @ed + ptoff))
+	end
+end
+
+class OffsetGenerator
+	def initialize()
+
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/profile_utils.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/profile_utils.rb
new file mode 100755
index 0000000..ac66386
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/profile_utils.rb
@@ -0,0 +1,136 @@
+require_relative "entities.rb"
+
+class SegmentRef
+	class ReversedSegRef
+		attr_accessor :twin, :seg
+		def initialize(seg)
+			@seg = seg
+		end
+		def tangent_at(a) -@seg.tangent_at(a) end
+		def st() @seg.ed end
+		def ed() @seg.st end
+		def sta() @seg.eda end 
+		def eda() @seg.sta end 
+		def is_twin() true end
+	end
+	attr_accessor :twin, :seg
+	def initialize(seg)
+		@seg = seg
+	end
+	def tangent_at(a) @seg.tangent_at(a) end
+	def sta() @seg.sta end 
+	def eda() @seg.eda end 
+	def st() @seg.st end
+	def ed() @seg.ed end
+	def is_twin() false end
+	def self.make_pair(seg)
+		ref = SegmentRef.new(seg)
+		tref = ReversedSegRef.new(seg)
+		ref.twin = tref
+		tref.twin = ref
+		return ref
+	end
+end
+
+class Oriented
+	def self.import_2d(items2d)
+		all_items = []
+		items2d.each do |item|
+			item.to_oriented(all_items)
+		end
+		return all_items.collect { |item| SegmentRef.make_pair(item) }
+	end
+end
+
+class QuadrentArc
+	attr_accessor :c, :r, :sta, :eda
+	def self.make(c, r, sta, eda)
+		q = QuadrentArc.new()
+		q.c, q.r, q.sta, q.eda = c, r, sta, eda
+		return q
+	end
+	def pt_for(a) @c + Point2d.new(@r * Math.cos(a), @r * Math.sin(a)) end
+	def tangent_at(a)
+		return Point2d.new(-Math.sin(a), Math.cos(a))
+	end
+	def midpoint_and_tangent()
+		a = (@sta + @eda) / 2
+		return [pt_for(a), tangent_at(a)]
+	end
+	def st() pt_for(@sta) end
+	def ed() pt_for(@eda) end
+end
+
+class Line2d
+	def midpoint_and_tangent()
+		return (@st + @ed).scale(0.5), (@ed - @st).normalize()
+	end
+	def sta() return 0.0 end
+	def eda() return 1.0 end
+	def tangent_at(a)
+		return (@ed - @st).normalize()
+	end
+end
+
+Q0 = 0
+Q1 = Math::PI / 2
+Q2 = 2 * Q1
+Q3 = 3 * Q1
+Q4 = 4 * Q1
+
+class Arc2d
+	def emit_arcs(st, ed, output)
+#		puts "emitting arcs: #{st} -> #{ed}"
+		return if st == ed
+
+=begin
+		4.times do |i|
+			iSt = i * Q1
+			iEd = (i + 1) * Q1
+			if (st < iEd && ed >= iSt)
+				ast = st < iSt ? iSt : st
+				aed = ed < iEd ? ed : iEd
+				if (aed > ast)
+					output.push QuadrentArc.make(@c, @r, ast, aed)
+				end
+			end
+		end
+=end
+		
+	end
+	def to_oriented(output)
+		ed, st = @eda, @sta
+		ed += Math::PI * 2 if (ed < st)
+		dt = ((ed - st) / Q1).ceil().to_i
+		dx = (ed - st) / dt
+		ast = st
+		dt.times do
+			aed = ast + dx
+			output.push QuadrentArc.make(@c, @r, ast, aed)
+			ast = aed
+		end
+		return
+		if (@sta > @eda)
+#			puts "wrapped arcs: #{@sta} -> #{@eda}"
+			emit_arcs(Q0, @eda, output)
+			emit_arcs(@sta, Q4, output)
+		else
+			emit_arcs(@sta, @eda, output)
+		end
+	end
+end
+
+class CircleProfile2d
+	def to_oriented(output)
+		output.push QuadrentArc.make(@c, @r, Q0, Q1)
+		output.push QuadrentArc.make(@c, @r, Q1, Q2)
+		output.push QuadrentArc.make(@c, @r, Q2, Q3)
+		output.push QuadrentArc.make(@c, @r, Q3, Q4)
+	end
+end
+
+class Line2d
+	def to_oriented(output)
+		output.push self
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/timed_view.rb b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/timed_view.rb
new file mode 100755
index 0000000..7b8f442
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/dxf_to_gcode/timed_view.rb
@@ -0,0 +1,83 @@
+require "gtk3"
+
+require "cairo"
+require "gui/gui_lib/ev_window.rb"
+require "gui/gtk_utils/pango_inspect.rb"
+require "gui/gtk_utils/clipping.rb"
+require "tokens/tokenizer_lib_base.rb"
+
+class TimeSeriesViewer
+	class Point2d
+		init_fields :x, :y
+		def -(o) Point2d.new(@x - o.x, @y - o.y) end
+		def +(o) Point2d.new(@x + o.x, @y + o.y) end
+	end
+	def set_scale(scale) @scale = scale end
+	def initialize()
+		@scale = 1.0
+		@xoff = 200
+		@yoff = 200
+	end
+	def draw_fn(&blk)
+		@draw_fn = blk
+	end
+  def resize(bbox)
+    @bbox = bbox
+  end
+	def scroll_event(ev)
+#		puts "#{ev.x} #{ev.y}"
+		#ev.x = @xoff + xcur * @scale
+		#ev.y = @yoff - ycur * @scale
+		xcur = (ev.x - @xoff) / @scale
+		ycur = (@yoff - ev.y) / @scale
+		if (ev.direction == Gdk::ScrollDirection::DOWN)
+			@scale *= 1.1
+		elsif (ev.direction == Gdk::ScrollDirection::UP)
+			@scale /= 1.1
+		end
+		@xoff = ev.x - xcur * @scale
+		@yoff = ev.y + ycur * @scale
+	end
+	def click_fn(&blk) @click_fn = blk end
+	def drag_fn(&blk) @drag_fn = blk end
+	def key_press_fn(&blk) @key_press_fn = blk end 
+	def key_release_fn(&blk) @key_release_fn = blk end 
+	def to_model(ev)
+		xcur = (ev.x - @xoff) / @scale
+		ycur = (@yoff - ev.y) / @scale
+		return Point2d.new(xcur, ycur)
+	end
+	def button_press_event(ev)
+		if (ev.button == 1)
+			@click_point = Point2d.new(ev.x, ev.y)
+			@click_fn.call(ev, to_model(ev)) if @click_fn
+		end
+	end
+	def button_release_event(ev)
+		@click_point = nil if (ev.button == 1)
+	end
+	def motion_notify_event(ev)
+		if @click_point
+			if !(@drag_fn && @drag_fn.call(to_model(ev)))
+				cp = Point2d.new(ev.x, ev.y)
+				delta = cp - @click_point
+				@click_point = cp
+				@xoff += delta.x
+				@yoff += delta.y
+			end
+		end
+	end
+  def key_press(ev)
+		@key_press_fn.call(ev) if @key_press_fn
+  end
+  def key_release(ev)
+		@key_release_fn.call(ev) if @key_release_fn
+  end
+	def draw(cr)
+		cr.set_source_rgb(0.0,0.1,0.1)
+		cr.paint()
+		cr.translate(@xoff, @yoff)
+		cr.set_source_rgb(0.0,1.0,0.1)
+		@draw_fn.call(cr, @scale) if (@draw_fn)			
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_file_format.rb b/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_file_format.rb
new file mode 100755
index 0000000..59e36bf
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_file_format.rb
@@ -0,0 +1,157 @@
+require "tokens/tokenizer_lib_base.rb"
+
+require_relative "simulate.rb"
+
+class GcodeInterpret
+	attr_accessor :stpt
+	attr_accessor :spindle_speed, :st_h
+	def initialize()
+		@cmds = []
+	end
+	def add_cmd(cmd)
+		@cmds << cmd
+	end
+end
+class UnparsedGcode
+	def initialize(view)
+		@view = view
+	end
+	def inspect() "<UnparsedGcode#{self.object_id}>" end
+end
+
+def expect_view_line(view, exp_line)
+	view.st += 1 while view[0].class == CommentLine
+	if !view[0].match_gcode(exp_line)
+		raise "unexpected: #{exp_line.inspect} vs #{view[0].inspect}"
+	end
+	view.st += 1
+end
+
+def expect_footer_view_line(view, exp_line)
+	view.ed -= 1 while view[-1].class == CommentLine
+	if !view[-1].match_gcode(exp_line)
+		raise "unexpected: #{exp_line.inspect} vs #{view[-1].inspect}"
+	end
+	view.ed -= 1
+end
+
+def expect_not_comment_lines(view, lines)
+	lines.split(/\n/).each do |ln|
+		expline = parse_gcode_line(ln)
+		expect_view_line(view, expline)
+	end
+end
+
+def expect_footer_not_comment_lines(view, lines)
+	lines.split(/\n/).reverse.each do |ln|
+		expline = parse_gcode_line(ln)
+		expect_footer_view_line(view, expline)
+	end
+end
+
+def get_spindle_speed(view)
+	view.st += 1 while view[0].class == CommentLine
+	words = view[0].words
+	raise "expected spindle speed, got: #{view[0].inspect}" if (words.length != 2 ||
+					words[0].code != "S" || words[1].code != "M" || words[1].number != "3")
+	view.st += 1
+	return words[0].number
+end
+
+def match_header(view)
+	expect_not_comment_lines(view, <<END1)
+%
+G90 G94 G17 G91.1
+G64 P0.001 Q0.001
+G20
+G53 G0 Z0.
+M9
+T1 M6
+END1
+# S19638 M3
+	speed = get_spindle_speed(view)
+expect_not_comment_lines(view, <<END1)
+G4 P10.
+G54
+M7
+END1
+ln = view[0]
+if (ln.words.length != 3 || ln.words[0].code != "G" ||
+	  ln.words[0].number != "0" || ln.words[1].code != "X" ||
+	  ln.words[2].code != "Y")
+	raise "expecting starting point. got #{ln.inspect} "
+end
+stx = ln.words[1].number
+sty = ln.words[2].number
+st_pt = Gcode::XYPoint.new(stx.to_f, sty.to_f)
+
+view.st += 1
+	expect_not_comment_lines(view, <<END1)
+G43 Z0.6 H1
+END1
+ln = view[0]
+if (ln.words.length != 2 || ln.words[0].code != "G" ||
+	  ln.words[0].number != "0" || ln.words[1].code != "Z")
+	raise "expecting starting height. got #{ln.inspect} "
+end
+st_h = ln.words[1].number
+
+view.st += 1
+#G0 Z#z_height
+return st_pt, speed, st_h
+end
+
+def match_footer(view)
+#	expect_footer_not_comment_lines(view, <<END1)
+#M9
+#END1
+	
+expect_footer_not_comment_lines(view, <<END1)
+M30
+%
+END1
+ln = view[-1]
+if (ln.words.length == 2)
+	expline = parse_gcode_line("G53 Z0.")
+	expect_footer_view_line(view, expline)
+else
+	expline = parse_gcode_line("G53 G0 Z0.")
+	expect_footer_view_line(view, expline)
+end
+
+end
+
+def extract_lines(view, interpret, state)
+	view.lines do |ln|
+		begin
+			if ln.class != CommentLine
+				line_cmd, state = state.emit_state_update(ln)
+				if (line_cmd)
+					interpret.add_cmd(line_cmd)
+				end
+			end
+		rescue Exception => e
+			$stderr.puts("Parse problem #{ln.filename}:#{ln.line_number}: #{ln.emit()}")
+			raise e
+		end
+	end
+end
+
+def interpret_gcode(gcode_file, do_not_validate = false)
+	interpret = GcodeInterpret.new()
+	view = GcodeFileView.new(gcode_file)
+	st_pt, speed, st_h = match_header(view)
+	match_footer(view)
+
+	interpret.stpt = st_pt 
+	interpret.st_h = st_h
+	interpret.spindle_speed = speed
+
+	if (do_not_validate)
+		interpret.add_cmd(UnparsedGcode.new(view))
+	else
+		state = GcodeState.init_defaults(st_pt)
+		extract_lines(view, interpret, state)
+	end
+	return interpret
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_layout.rb b/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_layout.rb
new file mode 100755
index 0000000..da9f48f
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/gcode/gcode_layout.rb
@@ -0,0 +1,263 @@
+require_relative "simulate.rb"
+require_relative "../point2d.rb"
+
+module Gcode
+	class Motion
+		def emit_gcode(f, state)
+			state = state.change_arc_plane(f, @arc_plane)
+			state, cmd_tag = state.change_mach_state(f, @mach_state)
+			f.puts emit_gcode_cmd(f, state) + cmd_tag
+			state.motion_type = self.class
+			return state
+		end
+	end
+	class XYZPoint
+		def to_s()
+			"<#{@x.inspect}, #{@y.inspect}, #{@z.inspect}>"
+		end
+	end
+	class Arc < Motion
+		def emit_gcode(f, state)
+			state = state.change_arc_plane(f, @arc_plane)
+			state, cmd_tag = state.change_mach_state(f, @mach_state)
+			f.puts emit_gcode_cmd(f, state) + cmd_tag
+			state.motion_type = self.class
+			return state
+		end
+	end
+	class CCWArc < Arc
+		def emit_gcode_cmd(f, state)
+			cmd = ""
+			cmd += "G3 " if state.motion_type != self.class
+			state.assert_cur_pt(@st)
+			da = state.update_arc_center(@c)
+			cmd += state.update_pos(@ed)
+			cmd += da
+		end
+		def inspect
+			"CCWArc #{@st} -> #{@ed} c:#{@c}"
+		end
+	end
+	class CWArc < Arc
+		def emit_gcode_cmd(f, state)
+			cmd = ""
+			cmd += "G2 " if state.motion_type != self.class
+			state.assert_cur_pt(@st)
+			da = state.update_arc_center(@c)
+			cmd += state.update_pos(@ed)
+			cmd += da
+		end
+		def inspect
+			"CWArc #{@st} -> #{@ed} c:#{@c}"
+		end
+	end
+	class Rapid < Motion
+		def emit_gcode_cmd(f, state)
+			cmd = ""
+			cmd += "G0 " if state.motion_type != self.class
+			state.assert_cur_pt(@st)
+			cmd += state.update_pos(@ed)
+		end
+		def inspect
+			"Rapid #{@st} -> #{@ed}"
+		end
+	end
+	class Linear < Motion
+		def emit_gcode_cmd(f, state)
+			cmd = ""
+			cmd += "G1 " if state.motion_type != self.class
+			state.assert_cur_pt(@st)
+			cmd += state.update_pos(@ed)
+		end
+		def inspect
+			"Linear #{@st} -> #{@ed}"
+		end
+	end
+end
+
+class GcodeState
+	class MachineState
+		def change_mach_state(f, o)
+			cmd_tag = ""
+			if @feed_rate != o.feed_rate && o.feed_rate
+				cmd_tag = "F#{o.feed_rate} "
+				@feed_rate = o.feed_rate
+			end
+			tmist = o.mist || @mist
+			tflood = o.flood || @flood
+			if (tmist == :off && @mist != :off) || (tflood == :off && @flood != :off)
+				f.puts("M9")
+				@mist = :off
+				@flood = :off
+			end
+			if tmist == :on && @mist == :off
+				f.puts("M7")
+				@mist = :on
+			end
+			if tflood == :on && @flood == :off
+				f.puts("M8")
+				@flood = :on
+			end
+      if o.cutter_comp != @cutter_comp
+        if o.cutter_comp == nil
+          f.puts("G40")
+        elsif o.cutter_comp == :right || o.cutter_comp == :left
+          code = "2"
+          code = "1" if o.cutter_comp == :left
+          if o.cutter_diameter == nil
+            throw Exception.new("missing cutter diameter")
+          end
+          f.puts("G4#{code} D#{o.cutter_diameter}")
+          @cutter_diameter = o.cutter_diameter
+        else
+          throw Exception.new("invalid_cutter_comp #{o.cutter_comp}")
+        end
+        @cutter_comp = o.cutter_comp
+      end
+      if @cutter_diameter != o.cutter_diameter
+        if o.cutter_diameter != nil
+          f.puts("D#{o.cutter_diameter}")
+        end
+        @cutter_diameter = o.cutter_diameter
+      end
+			return cmd_tag
+		end
+	end
+	def comp_d(d1, d2)
+		return d2 && (d1 != d2)
+	end
+	def assert_cur_pt(pt)
+		puts pt.inspect
+		if comp_d(pt.x, @cur_pt.x) || 
+			 comp_d(pt.y, @cur_pt.y) ||
+			 comp_d(pt.z, @cur_pt.z)
+			raise "problem! <#{pt.x}, #{pt.y}, #{pt.z}> " + 
+			    "vs cur:<#{@cur_pt.x}, #{@cur_pt.y}, #{@cur_pt.z}>"
+		end
+	end
+	def fmt_float(f)
+		"%f"%f
+	end
+	def update_pos(pt)
+		cmd = ""
+		cmd += "X#{fmt_float(pt.x)} " if (pt.x && @cur_pt.x != pt.x)
+		cmd += "Y#{fmt_float(pt.y)} " if (pt.y && @cur_pt.y != pt.y)
+		cmd += "Z#{fmt_float(pt.z)} " if (pt.z && @cur_pt.z != pt.z)
+		@cur_pt.x = pt.x || @cur_pt.x
+		@cur_pt.y = pt.y || @cur_pt.y
+		@cur_pt.z = pt.z || @cur_pt.z
+		return cmd
+	end
+	def update_arc_center(pt)
+		cmd = ""
+		ap = @arc_plane
+		cmd += "I#{fmt_float(pt.x - @cur_pt.x)} " if (pt.x && ap.has_x)# != @arc_center.x && pt.x)
+		cmd += "J#{fmt_float(pt.y - @cur_pt.y)} " if (pt.y && ap.has_y)# != @arc_center.y && pt.y)
+		cmd += "K#{fmt_float(pt.z - @cur_pt.z)} " if (pt.z && ap.has_z)# != @arc_center.z && pt.z)
+		@arc_center.x = pt.x || @arc_center.x
+		@arc_center.y = pt.y || @arc_center.y
+		@arc_center.z = pt.z || @arc_center.z
+		return cmd
+	end
+	def change_arc_plane(f, arc_plane)
+		return self if (arc_plane == @arc_plane)
+		if (Gcode::XYPlane == arc_plane)
+			f.puts("G17")
+		elsif (Gcode::XZPlane == arc_plane)
+			f.puts("G18")
+		elsif (Gcode::YZPlane == arc_plane)
+			f.puts("G19")
+		end
+		@arc_plane = arc_plane
+		return self
+	end
+	def change_mach_state(f, mach_state)
+		cmd_tag = @mach_state.change_mach_state(f, mach_state)
+		return self, cmd_tag
+	end
+end
+
+class UnparsedGcode
+	def emit_gcode(f, state)
+		@view.lines do |ln|
+			if (ln.class != CommentLine)
+				f.puts(ln.emit())
+			end
+		end
+		return state
+	end
+end
+
+class GcodeInterpret
+	def emit_gcode(f, state)
+		f.puts(<<END)
+S#{@spindle_speed} M3
+G0 X#{@stpt.x} Y#{@stpt.y}
+G43 Z0.6 H1
+G0 Z#{@st_h}
+END
+		state.cur_pt.x = @stpt.x
+		state.cur_pt.y = @stpt.y
+		state.cur_pt.z = nil
+		@cmds.each do |cmd|
+			puts cmd.inspect
+			state = cmd.emit_gcode(f, state)
+		end
+		return state
+	end
+end
+
+class GcodeLayout
+	def initialize()
+		@items = []
+		@start
+	end
+	class Pos
+		def initialize(gcode, pos, rot)
+			@gcode = gcode
+			@pos = pos
+			@rot = rot
+		end
+		def spindle_speed() @gcode.spindle_speed end
+		def emit_gcode(f, state)
+			f.puts(<<END)
+G10 L2 P9 X[#5221+#{@pos.x}] Y[#5222+#{@pos.y}] Z[#5223] R#{@rot * 180.0 / Math::PI}
+G59.3
+END
+			
+			return @gcode.emit_gcode(f, state)
+		end
+	end
+
+	def add_item(gcode, pos, rot)
+		@items << Pos.new(gcode, pos, rot)
+	end
+
+	def write_gcode(fname)
+		state = GcodeState.init_defaults(Gcode::XYZPoint.new(0,0,0))
+		File.open(fname, "w+") { |f|
+		f.puts(<<END)
+%
+G90 G94 G17 G91.1
+G64 P0.001 Q0.001
+G20
+G53 G0 Z0.
+(BORE8)
+M9
+T1 M6
+S#{@items[0].spindle_speed} M3
+G4 P10.
+G54
+M7
+END
+@items.each do |item|
+	state = item.emit_gcode(f, state)
+end
+		f.puts(<<END)
+M9
+G53 G0. Z0.
+M30
+END
+		}
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/gcode/parse.rb b/manufacturing/TravisProg1/lib_files/geometry/gcode/parse.rb
new file mode 100755
index 0000000..69d50e0
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/gcode/parse.rb
@@ -0,0 +1,109 @@
+class GcodeLine
+	attr_accessor :words, :line_number, :filename
+	def initialize(words)
+		@words = words
+	end
+	def match_gcode(o)
+		return false if self.class != o.class
+		return false if @words.length != o.words.length
+		@words.length.times do |i|
+			return false if @words[i] != o.words[i]
+		end
+		return true
+	end
+	def emit() "#{@words.collect {|word| word.emit()}.join(" ") }" end
+end
+
+class GcodePercentLine
+	attr_accessor :line_number, :filename
+	def match_gcode(o) return self.class == o.class end
+end
+
+class CommentLine
+	attr_accessor :line_number, :filename
+	attr_accessor :cmt
+	def initialize(cmt) @cmt = cmt end
+end
+
+class GcodeWord
+	attr_accessor :code, :number
+	def inspect() "#{@code.inspect}#{@number.inspect}" end
+	def initialize(code, number)
+		@code, @number = code, number
+	end
+	def emit() "#{@code}#{@number}" end
+	def ==(o)
+		@code == o.code && @number == o.number
+	end
+end
+
+class GcodeFile
+	attr_accessor :lines
+	def initialize()
+		@lines = []
+	end
+	def add_line(ln) @lines << ln end
+	def [](i) return @lines[i] end
+	def gcode_file() self end
+	def st() 0 end
+	def ed() @lines.length end
+end
+
+class GcodeFileView
+	def initialize(gcodef)
+		@gcodef = gcodef.gcode_file()
+		@st = gcodef.st()
+		@ed = gcodef.ed()
+	end
+	def st() @st end
+	def st=(v) @st = v end
+	def ed() @ed end
+	def ed=(v) @ed = v end
+	def gcode_file() @gcodef end
+	def length() @ed - @st end
+	def [](i)
+		l = length()
+		return nil if (i < -l || i >= l)
+		return @gcodef[i >= 0 ? i + @st : @ed + i]
+	end
+	def lines(&blk)
+		return Enumerator.new do |y|
+			(@st...@ed).each do |i|
+				y << @gcodef[i]
+			end
+		end if !blk
+		lines.each(&blk)
+		self
+	end
+end
+
+def parse_gcode_line(line)
+	line_no_space = line.gsub(/\s/,"")
+	if line_no_space[0] == "(" && line_no_space[-1] == ")"
+		#puts "comment_line: #{line.inspect}"
+		return CommentLine.new(line.chomp)
+	elsif (line_no_space == "%")
+		#puts "percent_line: \"%\""
+		return GcodePercentLine.new()
+	else
+		wl = line_no_space.scan(/[A-Za-z][0-9.\-]*/)
+		raise "Invalid line: #{line_no_space}"if (wl.join() != line_no_space)
+		#puts wl.inspect + " vs " + line.inspect
+		return GcodeLine.new(wl.collect { |w| GcodeWord.new(w[0], w[1..-1])})
+	end
+end
+
+def load_gcode(fname)
+	i = 0
+	fout = GcodeFile.new()
+  File.readlines(fname).each do |line|
+		line = parse_gcode_line(line) 
+		i += 1
+		if (line)
+			line.line_number = i
+			line.filename = fname
+			fout.add_line(line)
+		end
+	end
+	return fout
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/gcode/simulate.rb b/manufacturing/TravisProg1/lib_files/geometry/gcode/simulate.rb
new file mode 100755
index 0000000..2d53d10
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/gcode/simulate.rb
@@ -0,0 +1,255 @@
+
+
+module Gcode
+	class CmdLineView
+		def initialize(line)
+			@word_mapping = {}
+			line.words.each do |word|
+				(@word_mapping[word.code] ||= []) << word.number
+			end
+			#puts @word_mapping.inspect
+		end
+		def del_code(code)
+			l = @word_mapping.delete(code)
+			return l if !l
+			raise "expected less than one word of type: #{code}" if (l.length > 1)
+			return l[0]
+		end
+		def del_code_list(code)
+			l = @word_mapping.delete(code) || []
+			return l
+		end
+		def get(code)
+			return @word_mapping[code]
+		end
+		def assert_empty()
+			raise "word list not empty #{@word_mapping}" if @word_mapping.length != 0
+		end
+		def format_as_gcode()
+			return @word_mapping.collect do |k, v|
+				v.collect {|a| "#{k}#{a}"}.join(" ")
+			end.join(" ")
+		end
+	end
+	class XYPoint
+		init_fields :x, :y
+		def +(o) XYPoint.new(@x + o.x, @y + o.y) end
+		def -(o) XYPoint.new(@x - o.x, @y - o.y) end
+		def /(o) XYPoint.new(@x / o, @y / o) end
+		def *(o) XYPoint.new(@x * o, @y * o) end
+		def mag_sqr() return @x * @x + @y * @y end
+		def dot(o) @x * o.x + @y * o.y end
+		def cross(o) @x * o.y - @y * o.x end
+		def ==(o) self.class == o.class && @x == o.x && @y == o.y end
+	end
+	class XYZPoint
+		init_fields :x, :y, :z
+		def -@() XYZPoint.new(-@x, -@y, -@z) end
+		def ==(o) o.class == XYZPoint && @x == o.x && @y == o.y && @z == o.z && @x && @y && @z end
+	end
+	class ArcPlane
+		attr_accessor :twin
+		init_fields :vect
+		def self.make(pt)
+			a = ArcPlane.new(pt)
+			b = ArcPlane.new(-pt)
+			a.twin = b
+			b.twin = a
+			return a
+		end
+		def has_x() @vect.x == 0 end
+		def has_y() @vect.y == 0 end
+		def has_z() @vect.z == 0 end
+	end
+	XYPlane = ArcPlane.make(XYZPoint.new(0, 0, 1)) 
+	XZPlane = ArcPlane.make(XYZPoint.new(0, 1, 0)) 
+	YZPlane = ArcPlane.make(XYZPoint.new(1, 0, 0)) 
+	class Motion
+	end
+	class Arc < Motion
+	end
+	class CCWArc < Arc
+		init_fields :c, :st, :ed, :arc_plane, :mach_state
+		def self.state_construct(s1, s2)
+			self.new(s2.arc_center, s1.cur_pt, s2.cur_pt, s2.arc_plane, s2.mach_state())
+		end
+	end
+	class CWArc < Arc
+		init_fields :c, :st, :ed, :arc_plane, :mach_state
+		def self.state_construct(s1, s2)
+			self.new(s2.arc_center, s1.cur_pt, s2.cur_pt, s2.arc_plane, s2.mach_state())
+		end
+	end
+	class Rapid < Motion
+		init_fields :st, :ed, :arc_plane, :mach_state
+		def self.state_construct(s1, s2)
+			self.new(s1.cur_pt, s2.cur_pt, s2.arc_plane, s2.mach_state())
+		end
+	end
+	class Linear < Motion
+		init_fields :st, :ed, :arc_plane, :mach_state
+		def self.state_construct(s1, s2)
+			self.new(s1.cur_pt, s2.cur_pt, s2.arc_plane, s2.mach_state())
+		end
+	end
+end
+
+class GcodeState
+	class MachineState
+		init_fields :feed_rate, :mist, :flood, :cutter_diameter, :cutter_comp
+		def self.init_defaults()
+			return self.new(nil, nil, nil, nil, nil)
+		end
+		def update_mach_state(view)
+			feed_rate = view.del_code("F") || @feed_rate
+			cutter_diameter = view.del_code("D") || @cutter_diameter
+			mist, flood = @mist, @flood
+			view.del_code_list("M").each do |code|
+				if (code == "7")
+					mist = :on
+				elsif (code == "8")
+					flood = :on
+				elsif (code == "9")
+					flood = :off
+					mist = :off
+				else
+					raise "unknown code: #{code}"
+				end
+			end
+			MachineState.new(feed_rate, mist, flood, cutter_diameter, @cutter_comp)
+		end
+    def set_cutter_comp(new_comp)
+			MachineState.new(@feed_rate, @mist, @flood, @cutter_diameter, new_comp)
+    end
+	end
+	attr_accessor :cur_pt, :arc_plane, :arc_center, :mach_state, :motion_type
+	def initialize(cur_pt, arc_plane, arc_center, motion_type, mach_state)
+		@cur_pt = cur_pt
+		@arc_plane = arc_plane
+		@arc_center = arc_center
+		@motion_type = motion_type
+		@mach_state = mach_state
+	end
+	def mach_state() @mach_state end
+	def self.init_defaults(st_pt)
+		z = st_pt.respond_to?(:z) ? st_pt.z : nil
+		st_pt = Gcode::XYZPoint.new(st_pt.x, st_pt.y, z)
+		cent = Gcode::XYZPoint.new(nil, nil, nil)
+		mach_state = MachineState.init_defaults()
+		return self.new(st_pt, Gcode::XYPlane, cent, nil, mach_state)
+	end
+end
+
+class G98ModalMotion
+	def initialize(view)
+		@view = view
+	end
+	def emit_gcode(f, state)
+		f.puts "#{@view.format_as_gcode}"
+		return state
+	end
+end
+
+class G80ModalMotion
+	def initialize()
+	end
+	def emit_gcode(f, state)
+		f.puts "G80"
+		state.motion_type = nil
+		return state
+	end
+end
+
+class ExpectClearModalMotion
+	def initialize(pstate)
+		@pstate = pstate
+	end
+	def emit_state_update(gcode_line)
+		view = Gcode::CmdLineView.new(gcode_line)
+		init_g_codes = view.del_code_list("G")
+		if (init_g_codes.empty?())
+			return G98ModalMotion.new(view), self
+		end
+		if (init_g_codes != ["80"])
+			raise "G98 supposed to be followed by G80"
+		end
+		view.assert_empty()
+		return G80ModalMotion.new(), @pstate
+	end
+end
+
+class GcodeState
+	def pt_to_f(v)
+		return nil if !v
+		raise "not a float: #{v}" if (!v =~ /-?[0-9]+\.[0-9]*/)
+		return v.to_f
+	end
+	def get_npoint(gcode_line)
+		xval = pt_to_f(gcode_line.del_code("X"))
+		yval = pt_to_f(gcode_line.del_code("Y"))
+		zval = pt_to_f(gcode_line.del_code("Z"))
+		return Gcode::XYZPoint.new(xval ? xval : @cur_pt.x, 
+															 yval ? yval : @cur_pt.y, 
+															 zval ? zval : @cur_pt.z) 
+	end
+	def get_center(gcode_line)
+		xval = pt_to_f(gcode_line.del_code("I"))
+		yval = pt_to_f(gcode_line.del_code("J"))
+		zval = pt_to_f(gcode_line.del_code("K"))
+		return Gcode::XYZPoint.new(xval ? xval + @cur_pt.x : @arc_center.x, 
+															 yval ? yval + @cur_pt.y : @arc_center.y, 
+															 zval ? zval + @cur_pt.z : @arc_center.z) 
+	end
+	def get_arc_plane()
+		return @arc_plane
+	end
+	def construct_update_cmd(old_state)
+		if (old_state.cur_pt == self.cur_pt)
+			return nil
+		else
+			return @motion_type.state_construct(old_state, self)
+		end
+	end
+	def emit_state_update(gcode_line)
+		view = Gcode::CmdLineView.new(gcode_line)
+		init_g_code = (view.get("G") || [])[0]
+		if (init_g_code == "98") || (init_g_code == "83")
+			return G98ModalMotion.new(view), ExpectClearModalMotion.new(self)
+		end
+		npoint = get_npoint(view)
+		center = get_center(view)
+		mach_state = @mach_state.update_mach_state(view)
+		codes = view.del_code_list("G")
+		arc_plane = @arc_plane
+		motion_type = @motion_type
+		codes.each do |code|
+			if (code == "0")
+				motion_type = Gcode::Rapid
+			elsif (code == "1")
+				motion_type = Gcode::Linear
+			elsif (code == "2")
+				motion_type = Gcode::CWArc
+			elsif (code == "3")
+				motion_type = Gcode::CCWArc
+			elsif (code == "40")
+        mach_state = mach_state.set_cutter_comp(nil)
+			elsif (code == "41")
+        mach_state = mach_state.set_cutter_comp(:left)
+			elsif (code == "42")
+        mach_state = mach_state.set_cutter_comp(:right)
+			elsif (code == "17")
+				arc_plane = Gcode::XYPlane 
+			elsif (code == "18")
+				arc_plane = Gcode::XZPlane 
+			elsif (code == "19")
+				arc_plane = Gcode::YZPlane 
+			else
+				raise "unknown code: #{code}"
+			end
+		end
+		view.assert_empty()
+		nstate = self.class.new(npoint, arc_plane, center, motion_type, mach_state)
+		updated_cmd = nstate.construct_update_cmd(self)
+		return updated_cmd, nstate
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/placement/click_tester.rb b/manufacturing/TravisProg1/lib_files/geometry/placement/click_tester.rb
new file mode 100755
index 0000000..0840638
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/placement/click_tester.rb
@@ -0,0 +1,147 @@
+require "geometry/bool_ops/dxf_offset.rb"
+IntegerScale = 2**20
+class ProfileCrossing
+	class Entity
+		attr_accessor :st, :ed
+		def initialize(st, ed) @st = st; @ed = ed; end
+		def check_query(query)
+			q = Point2di.new((query.x * IntegerScale).to_i, (query.y * IntegerScale).to_i)
+			st = Point2di.new((@st.x * IntegerScale).to_i, (@st.y * IntegerScale).to_i)
+			ed = Point2di.new((@ed.x * IntegerScale).to_i, (@ed.y * IntegerScale).to_i)
+			return 0 if st.y == ed.y
+			dir = 1
+			st, ed, dir = ed, st, -1 if (st.y > ed.y)
+			return 0 if (st.y > q.y || ed.y <= q.y)
+
+			ttm = (q - st).cross(ed - st)
+			return ttm > 0 ? dir : 0 
+		end
+	end
+	def add_only_drawable(drawable)
+		@only_drawable << drawable
+	end
+	attr_accessor :filename
+	def initialize()
+		@profile_entity = []
+		@only_drawable = []
+	end
+	def query_crossings(query)
+		crossings = 0
+		@profile_entity.each do |entity|
+			crossings += entity.check_query(query)
+		end
+		return crossings
+	end
+	def emit(st, ed)
+		@profile_entity << Entity.new(st, ed)
+	end
+	def draw_to(cr, scale)
+		item = @profile_entity[0]
+		@profile_entity.each do |item|
+			cr.move_to(item.st.x * scale, -item.st.y * scale)
+			cr.line_to(item.ed.x * scale, -item.ed.y * scale)
+			cr.stroke()
+		end
+		cr.set_source_rgb(1.0, 0.5, 0.0)
+		@only_drawable.each do |item|
+			item.items.each { |sitem| sitem.draw_to(cr, scale) }
+		end
+	end
+end
+
+class Line2d
+	def emit_rev_crossing(st, crossing)
+		crossing.emit(st, @st)
+	end
+	def emit_crossing(st, crossing)
+		crossing.emit(st, @ed)
+	end
+end
+
+class QuadrentArc
+	def emit_rev_crossing(st, crossing)
+		crossing.emit(st, self.st())
+	end
+	def emit_crossing(st, crossing)
+		crossing.emit(st, self.ed())
+	end
+end
+
+class SegmentRef
+	class ReversedSegRef
+		def emit_crossing(st, crossing)
+			@seg.emit_rev_crossing(st, crossing)
+		end
+	end
+	def emit_crossing(st, crossing)
+		@seg.emit_crossing(st, crossing)
+	end
+end
+
+class TwinableProfile
+	def to_crossing(crossing)
+		@items.length.times do |i|
+			item = @items[i] 
+			item.emit_crossing(@items[i - 1].ed, crossing)
+		end
+	end
+end
+
+
+class Shape
+	# not correct if there is only ever two points. This is silly though.
+	def to_crossing(scale, pm, res = 1)
+		res_sqr = res * res
+		st = @segs[0].st
+		n = 0
+		@segs.each do |seg|
+			v = (seg.ed - st).mag_sqr.to_i
+			if (v > res_sqr)
+				st = seg.ed
+				n += 1
+			end
+		end
+		raise "problem" if n <= 2
+		dnom = scale ** -1
+		@segs.each do |seg|
+			v = (seg.ed - st).mag_sqr.to_i
+			if (v > res_sqr)
+				ed = seg.ed
+				pm.emit(ed * dnom, st * dnom)
+			end
+			st = seg.ed
+		end
+	end
+end
+
+def offset_dxf(dxf, scale, resolution, pm)
+	swl_p = offset_dxf_integer(dxf, IntegerScale, 2**15)
+end
+
+obj_name = (ARGV[0] || "9971-15-P-0015_transmition_plate.dxf")
+class PMLoader
+	def initialize()
+		@cache = {}
+	end
+	def direct_load(fname)
+		items = get_entities(fname)
+		items2d = items.collect { |item| item.to_2d() }
+
+		items2d_orient = Oriented.import_2d(items2d)
+		items2d_orient = SweepLine.stitch_curves(items2d_orient)
+		items = items2d_orient
+
+		scale = IntegerScale
+		swl_p = offset_dxf_integer(items, scale, 2**15)
+		pm = ProfileCrossing.new()
+		pm.filename = fname
+		swl_p.to_crossing(scale, pm)
+		for item in items
+			pm.add_only_drawable(item)
+		end
+		return pm
+	end
+	def load_pm(fname)
+		@cache[fname] ||= direct_load(fname)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/placement/drag_drop_rot_handler.rb b/manufacturing/TravisProg1/lib_files/geometry/placement/drag_drop_rot_handler.rb
new file mode 100755
index 0000000..719a76b
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/placement/drag_drop_rot_handler.rb
@@ -0,0 +1,72 @@
+class DragDropRotHandler
+	def initialize(layout)
+		@layout = layout
+		@picked = nil
+		@click_point = nil
+		@rot_point = nil
+		@ctrl_held = false
+		@a_held = false
+	end
+	def add_item_fn(&blk) @add_item_fn = blk end
+	def click(ev, cp)
+		if @a_held
+			@add_item_fn.call(cp) if @add_item_fn
+			@picked = nil
+			return
+		end
+		if @ctrl_held
+			@click_point = nil
+			if (!@rot_point)
+				@picked = @layout.pick(cp)
+				@rot_point = @picked ? cp : nil
+			else
+				@rot_drag_point = cp
+			end
+		else
+			@picked = @layout.pick(cp)
+			@click_point = cp
+		end
+	end
+	def drag(cp)
+		if @picked
+			if @click_point
+				delta = (cp - @click_point)
+				@picked.update(delta)
+				@click_point = cp
+			elsif @rot_drag_point
+				a1 = @rot_drag_point - @rot_point
+				a2 = cp - @rot_point
+				dang = Math.atan2(a2.y, a2.x) - Math.atan2(a1.y, a1.x)
+				@picked.rotate(@rot_point, dang)
+				@rot_drag_point = cp
+			end
+		end
+		return @picked ? true : nil
+	end
+	def key_release_fn(kv)
+		if (Gdk::Keyval::KEY_Control_L == kv.keyval || Gdk::Keyval::KEY_Control_R == kv.keyval)
+			@rot_point = nil
+			@rot_drag_point = nil
+			@ctrl_held = false
+		end
+		if (Gdk::Keyval::KEY_a == kv.keyval)
+			@a_held = false
+		end
+	end
+	def key_press_fn(kv)
+		if (Gdk::Keyval::KEY_Control_L == kv.keyval || Gdk::Keyval::KEY_Control_R == kv.keyval)
+			@ctrl_held = true
+		end
+		if (Gdk::Keyval::KEY_a == kv.keyval)
+			@a_held = true
+		end
+	end
+	def draw_scaled(cr, scale)
+		if @rot_point
+			cr.set_source_rgb(1.0, 0.5, 0.0)
+			draw_crosshair(cr, scale, @rot_point)
+		end
+		@layout.draw_to(cr, scale)
+	end
+	attr_accessor :layout
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/placement/sheet.rb b/manufacturing/TravisProg1/lib_files/geometry/placement/sheet.rb
new file mode 100755
index 0000000..1861834
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/placement/sheet.rb
@@ -0,0 +1,151 @@
+require "data/codecs/data_view.rb"
+require "data/codecs/serialize_buffer.rb"
+class SheetLayout
+	class BBox
+		attr_accessor :min, :max
+		def initialize()
+			reset()
+		end
+		def reset()
+			inf = Float::INFINITY
+			@min = Point2d.new(inf, inf)
+			@max = Point2d.new(-inf, -inf)
+		end
+		def add_point(pt)
+			@min.x = pt.x if pt.x < @min.x
+			@min.y = pt.y if pt.y < @min.y
+			@max.x = pt.x if pt.x > @max.x
+			@max.y = pt.y if pt.y > @max.y
+		end
+	end
+	class Pos
+		attr_accessor :c, :rot, :item
+		def initialize(c, rot, item)
+			@c, @rot, @item = c, rot, item
+			# @bbox = BBox.new()
+		end
+		def draw_to(cr, scale)
+			cr.save()
+			cr.translate(c.x * scale, -c.y * scale)
+			cr.rotate(-@rot)
+			@item.draw_to(cr, scale)
+			cr.restore()
+		end
+		def query_crossings(q)
+			q = q - @c
+			c = Math.cos(-@rot)
+			s = Math.sin(-@rot)
+			q = Point2d.new(q.x * c - q.y * s, q.x * s + q.y * c)
+			@item.query_crossings(q)
+		end
+		def update(delta)
+			@c += delta
+		end
+		def rotate(cent, da)
+			creal = cent
+			d1 = @c - cent 
+			c = Math.cos(-@rot)
+			s = Math.sin(-@rot)
+			d1 = Point2d.new(d1.x * c - d1.y * s, d1.x * s + d1.y * c)
+
+			@rot += da
+
+			c = Math.cos(@rot)
+			s = Math.sin(@rot)
+			dc = Point2d.new(c * d1.x - d1.y * s, d1.x * s + d1.y * c)
+			@c = creal + dc
+		end
+	end
+
+	def initialize()
+		@items = []
+		@selected = nil
+	end
+
+	attr_accessor :selected
+	def delete(item) @items.delete(item) end
+
+	def items() @items end
+
+	def add_item(item, c, rot)
+		@items << Pos.new(c, rot, item)
+	end
+	def pick(q)
+		@selected = nil
+		@items.each do |item|
+			cp_count = item.query_crossings(q)
+			return (@selected = item) if cp_count == 1
+		end
+		return nil
+	end
+	def draw_to(cr, scale, fixed = nil)
+		@items.each do |item|
+			(@selected == item ? Color::Red : Color::Green).set(cr)
+			Color::Blue.set(cr) if (fixed)
+			item.draw_to(cr, scale)
+		end
+	end
+end
+
+def draw_crosshair(cr, scale, pt)
+	cr.move_to(pt.x * scale + 5, -pt.y * scale + 5)
+	cr.line_to(pt.x * scale - 5, -pt.y * scale - 5)
+	cr.stroke()
+	cr.move_to(pt.x * scale + 5, -pt.y * scale - 5)
+	cr.line_to(pt.x * scale - 5, -pt.y * scale + 5)
+	cr.stroke()
+end
+
+class Point2d
+	def write_to(ser)
+		ser.write_f64(@x)
+		ser.write_f64(@y)
+	end
+	def self.load_from(ser)
+		self.new(ser.parse_f64(), ser.parse_f64())
+	end
+end
+
+class SheetLayout
+	class Pos
+		def write_to(ser)
+			ser.write_f64(@c.x)
+			ser.write_f64(@c.y)
+			ser.write_f64(@rot)
+			ser.code_string(@item.filename)
+		end
+	end
+	def write_to(ser)
+		ser.push()
+		@items.each do |item|
+			item.write_to(ser)
+		end
+		ser.pop()
+	end
+	def self.load_file(fname, pm_loader)
+		layout = self.new()
+		reader = StreamReader.new(File.open(fname, "rb").read())
+		reader.push()
+		while (reader.not_done())
+			pt = Point2d.load_from(reader)
+			ang = reader.parse_f64()
+			ofname = reader.parse_string()
+			pm = pm_loader.load_pm(ofname)
+			layout.add_item(pm, pt, ang)
+			#puts "<#{pt.x}, #{pt.y}> #{ang} #{ofname.inspect}"
+		end
+		reader.pop()
+		return layout
+	end
+end
+
+def write_out_sheet(sheet, fname)
+	ser = SerializeBuffer::Writer.new()
+	sheet.write_to(ser)
+	data = ser.data()
+
+	len = SerializeBuffer.get_concat_length(data)
+	buffer = OutputView.new(len)
+	SerializeBuffer.concat_output(buffer, data)
+	File.open(fname, "w+") { |f| f.write(buffer.val) }
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/point2d.rb b/manufacturing/TravisProg1/lib_files/geometry/point2d.rb
new file mode 100755
index 0000000..146b0aa
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/point2d.rb
@@ -0,0 +1,41 @@
+class Point2d
+	attr_accessor :x, :y
+	def initialize(x, y) @x, @y = x, y end
+	def inspect() "<#{@x}, #{@y}>" end
+	def to_s() "#<Point2d:#{@x} #{@y}>" end
+	def +(other) Point2d.new(@x + other.x, @y + other.y) end
+	def -(other) Point2d.new(@x - other.x, @y - other.y) end
+	def scale(d)
+		return Point2d.new(@x * d, @y * d)
+	end
+	def -@()
+		return Point2d.new(-@x, -@y)
+	end
+	def dot(o)
+		return o.x * @x + o.y * @y
+	end
+	def area(o)
+		return o.x * @y - o.y * @x
+	end
+	def normalize()
+		d = Math.sqrt(@x * @x + @y * @y)
+		return Point2d.new(@x / d, @y / d)
+	end
+	def mag() Math.sqrt(@x * @x + @y * @y) end
+	def mag_sqr() @x * @x + @y * @y end
+	def div(d) Point2d.new(@x / d, @y / d) end
+
+	def comp_h(o)
+		a = @x <=> o.x
+		return a if a != 0
+		return @y <=> o.y
+	end
+	def comp_v(o)
+		a = @y <=> o.y
+		return a if a != 0
+		return @x <=> o.x
+	end
+	def rot90()
+		return Point2d.new(-@y, @x)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/drag_window.rb b/manufacturing/TravisProg1/lib_files/geometry/router/drag_window.rb
new file mode 100755
index 0000000..7c31248
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/drag_window.rb
@@ -0,0 +1,275 @@
+require "geometry/placement/sheet.rb"
+require "geometry/placement/drag_drop_rot_handler.rb"
+require "geometry/placement/click_tester.rb"
+
+module Router
+	class HoleSpec
+		def self.direct_load()
+			r = HoleSpec::Radius
+			pts = 16.times.collect do |i|
+				th = i * Math::PI * 2 / 16.0
+				Point2d.new(Math.cos(th) * r, Math.sin(th) * r)
+			end
+			pm = ProfileCrossing.new()
+			pm.filename = self
+			16.times do |i|
+				pm.emit(pts[i], pts[i - 1])
+			end
+			return pm
+		end
+	end
+	class GcodeSpec
+		def direct_load()
+			part_spec = self
+			fname = part_spec.dxf_name()
+			items = get_entities(fname)
+			items2d = items.collect { |item| item.to_2d() }
+
+			items2d_orient = Oriented.import_2d(items2d)
+			items = [TwinableProfile.new(items2d_orient)]
+			#items2d_orient = SweepLine.stitch_curves(items2d_orient)
+#			items = items2d_orient
+
+			scale = IntegerScale
+			swl_p = offset_dxf_integer(items, scale, 2**16, 0.25)
+#			scale_points(swl_p, 1, 1)
+			pm = ProfileCrossing.new()
+			pm.filename = part_spec
+			swl_p.to_crossing(scale, pm)
+			for item in items
+				pm.add_only_drawable(item)
+			end
+			return pm
+		end
+	end
+	class SheetCut
+		def add_originable_points(lst)
+			@layout.items.each do |item|
+				lst << item.c if (item.item.filename == HoleSpec)
+			end
+		end
+	end
+	class SheetInfo
+		def add_originable_points(lst)
+			lst << Point2d.new(0, 0)
+			lst << Point2d.new(@w, 0)
+			lst << Point2d.new(0, @h)
+			lst << Point2d.new(@w, @h)
+		end
+	end
+	class MetalSheet
+		def originable_points()
+			lst = []
+			@cuts.each do |cut|
+				cut.add_originable_points(lst)
+			end
+			@sheet_info.add_originable_points(lst)
+			return lst
+		end
+	end
+end
+
+class OriginSelector
+	def initialize(sheet)
+		@points = sheet.originable_points()
+		@selected = nil
+	end
+	attr_accessor :selected
+	def pick(q)
+		@points.each do |point|
+			dist = (point - q)
+			dist = dist.x * dist.x + dist.y * dist.y
+			if dist < 4 * Router::HoleSpec::Radius ** 2
+				@selected = point
+			end
+		end
+		return nil
+	end
+	def draw_scaled(cr, scale)
+		if (@selected)
+			cr.set_source_rgb(1.0, 0.5, 0.5)
+			draw_crosshair(cr, scale, @selected)
+		end
+	end
+end
+
+class PMLoader
+	def initialize()
+		@cache = {}
+	end
+	def load_pm(part_spec)
+		return nil if (part_spec == nil)
+		@cache[part_spec] ||= part_spec.direct_load()
+	end
+	def load_pm_string(fname)
+		puts "loading part: #{fname}"
+		load_pm(@parts_library.realize(fname))
+	end
+	attr_accessor :parts_library
+end
+
+class DragWindow
+	attr_accessor :bbox
+	def resize(bbox)
+		@bbox = bbox
+	end
+	def add_item(layout, cp)
+		if (@adder && c_item = @adder.current_item())
+			layout.add_item(@pm_loader.load_pm(c_item), cp, 0)
+#				@pm_loader.load_pm("/home/parker/repo/geometry/gcode/sample/gcode.dxf"), cp, 0)
+		end
+	end
+	def set_active_adder(adder)
+		@adder = adder
+	end
+	def set_sheet(sheet)
+		@sheet = sheet
+		@sheet.active_cut_change() { |cut|
+			update_sheet_layout(cut.layout)
+		}
+	end
+	def update_origin_picker(origin_picker)
+		if (@drag_drop_handler)
+			@drag_drop_handler.layout.selected = nil
+		end
+		@origin_picker = origin_picker 
+		ts = self
+		@drag_drop_handler = nil
+		ts.click_fn() do |ev, cp|
+			@origin_picker.pick(cp)
+		end
+		ts.drag_fn() do |cp|
+		end
+		ts.key_press_fn() do |kv|
+		end
+		ts.key_release_fn() do |kv|
+		end
+		ts.draw_fn() do |cr, scale|
+			@origin_picker.draw_scaled(cr, scale)
+			GC.start()
+		end
+	end
+	def update_sheet_layout(layout)
+		ts = self
+		drag_drop_handler = DragDropRotHandler.new(layout)
+		@drag_drop_handler = drag_drop_handler
+		drag_drop_handler.add_item_fn() do |cp|
+			add_item(layout, cp)
+		end
+		ts.click_fn() do |ev, cp|
+			drag_drop_handler.click(ev, cp)
+		end
+		ts.drag_fn() do |cp|
+			drag_drop_handler.drag(cp)
+		end
+		ts.key_press_fn() do |kv|
+			drag_drop_handler.key_press_fn(kv)
+		end
+		ts.key_release_fn() do |kv|
+			drag_drop_handler.key_release_fn(kv)
+		end
+		ts.draw_fn() do |cr, scale|
+			drag_drop_handler.draw_scaled(cr, scale)
+			GC.start()
+		end
+	end
+	def set_scale(scale) @scale = scale end
+	def initialize(pm_loader)
+		@pm_loader = pm_loader
+		@scale = 20.0
+		@xoff = 200
+		@yoff = 200
+	end
+	def take_focus(ev) self end
+	def draw_fn(&blk)
+		@draw_fn = blk
+	end
+	def scroll_event(ev, cur)
+#		puts "#{ev.x} #{ev.y}"
+		#ev.x = @xoff + xcur * @scale
+		#ev.y = @yoff - ycur * @scale
+		xcur = (ev.x - @xoff) / @scale
+		ycur = (@yoff - ev.y) / @scale
+		if (ev.direction == Gdk::ScrollDirection::DOWN)
+			@scale *= 1.1
+		elsif (ev.direction == Gdk::ScrollDirection::UP)
+			@scale /= 1.1
+		end
+		@xoff = ev.x - xcur * @scale
+		@yoff = ev.y + ycur * @scale
+	end
+  def key_press(ev)
+		@key_press_fn.call(ev) if @key_press_fn
+  end
+  def key_release(ev)
+		@key_release_fn.call(ev) if @key_release_fn
+  end
+	def click_fn(&blk) @click_fn = blk end
+	def drag_fn(&blk) @drag_fn = blk end
+	def key_press_fn(&blk) @key_press_fn = blk end 
+	def key_release_fn(&blk) @key_release_fn = blk end 
+	def to_model(ev)
+		xcur = (ev.x - @xoff) / @scale
+		ycur = (@yoff - ev.y) / @scale
+		return Point2d.new(xcur, ycur)
+	end
+	def button_press_event(ev, cp)
+		if (ev.button == 1)
+			@click_point = Point2d.new(cp.x, cp.y)
+			@click_fn.call(ev, to_model(cp)) if @click_fn
+		end
+	end
+	def button_release_event(ev, pt)
+		@click_point = nil if (ev.button == 1)
+	end
+	def motion_notify_event(ev, cp)
+		if @click_point
+			if !(@drag_fn && @drag_fn.call(to_model(cp)))
+				delta = cp - @click_point
+				@click_point = cp
+				@xoff += delta.x
+				@yoff += delta.y
+			end
+		end
+	end
+	def draw(cr)
+		if (@sheet)
+			cr.set_source_rgb(0.0,0.1,0.1)
+			cr.paint()
+			cr.translate(@xoff, @yoff)
+			cr.set_source_rgb(0.0,1.0,0.1)
+			@sheet.draw_others(cr, @scale, @drag_drop_handler ? @drag_drop_handler.layout : nil)
+			@draw_fn.call(cr, @scale) if (@draw_fn)			
+		end
+	end
+end
+module Router
+	class MetalSheet
+		def draw_others(cr, scale, layout)
+			@sheet_info.draw_bounds(cr, scale)
+			@cuts.each do |cut|
+				cut.layout.draw_to(cr, scale, cut != @active_cut) if (cut.layout != layout)
+			end
+		end
+	end
+	class SheetInfo
+		def draw_bounds(cr, scale)
+			cr.set_source_rgb(1.0, 0.5, 0.0)
+			bounds = sheet_dxf()
+			if bounds
+				bounds.each do |bound|
+					bound.draw_to(cr, scale) #.each { |sitem| sitem.draw_to(cr, scale) }
+				end
+				return
+			end
+			w, h = @w * scale, @h * scale
+			cr.move_to(0, 0)
+			cr.line_to(w, 0)
+			cr.line_to(w, -h)
+			cr.line_to(0, -h)
+			cr.line_to(0, 0)
+			cr.stroke()
+		end
+	end
+end
+
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/emit_gcode.rb b/manufacturing/TravisProg1/lib_files/geometry/router/emit_gcode.rb
new file mode 100755
index 0000000..ee55371
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/emit_gcode.rb
@@ -0,0 +1,67 @@
+require "geometry/gcode/parse.rb"
+require "geometry/gcode/gcode_file_format.rb"
+require "geometry/gcode/gcode_layout.rb"
+
+class GcodeLoader
+	def initialize() @cache = {} end
+	def load_pm(fname)
+		@cache[fname] ||= interpret_gcode(load_gcode(fname))
+	end
+end
+
+module Router
+	class HoleSpec
+		def self.emit_gcode_group(item, gcode_group)
+			gcode_group.add_hole(item)
+		end
+	end
+	class GcodeSpec
+		def emit_gcode_group(item, gcode_group)
+			@gcode_files.each do |gcode_file|
+				ext = gcode_file.basename.to_path[0...-gcode_file.extname.length].split("_")[-1]
+				gcode_group.add_part(ext, gcode_file, item)
+			end
+			gcode_group
+		end
+	end
+	class GcodeGrouping
+		def initialize()
+			@suffix_groups = {}
+			@holes = []
+		end
+		def emit_group(group, fname, origin)
+			loader = GcodeLoader.new()
+			lay = GcodeLayout.new()
+			group.each do |pos, gcode|
+				lay.add_item(loader.load_pm(gcode), pos.c - origin, pos.rot)
+			end
+			lay.write_gcode(fname)
+		end
+		def emit_holes(fname, sheet_info, origin)
+			loader = GcodeLoader.new()
+			lay = GcodeLayout.new()
+			hole_fname = sheet_info.hole_file()
+			@holes.each do |pos|
+				lay.add_item(loader.load_pm(hole_fname), pos.c - origin, pos.rot)
+			end
+			lay.write_gcode(fname)
+		end
+		def groups() @suffix_groups end
+		def add_part(ext, gcode_file, item)
+			(@suffix_groups[ext] ||= []) << [item, gcode_file]
+		end
+		def add_hole(hole)
+			@holes << hole
+		end
+		def has_holes() @holes.length > 0 end
+	end
+	class SheetCut
+		def make_gcode_groupings()
+			gcode_group = GcodeGrouping.new()
+			@layout.items.each do |item|
+				item.item.filename.emit_gcode_group(item, gcode_group)
+			end
+			return gcode_group
+		end
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/file_formats.rb b/manufacturing/TravisProg1/lib_files/geometry/router/file_formats.rb
new file mode 100755
index 0000000..2a2c3d4
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/file_formats.rb
@@ -0,0 +1,52 @@
+module Router
+	class GcodeSpec
+		def serialize(ser)
+			ser.write_varint(0)
+			ser.code_string(full_path())
+		end
+	end
+	class HoleSpec
+		def self.serialize(ser)
+			ser.write_varint(1)
+		end
+	end
+	class SheetCut
+		def serialize(ser)
+			ser.push()
+			@layout.items.each { |a|
+				ser.write_f64(a.c.x)
+				ser.write_f64(a.c.y)
+				ser.write_f64(a.rot)
+				a.item.filename.serialize(ser)
+			}
+			ser.pop()
+		end
+		def self.load(skel, pm_loader)
+			reader = StreamReader.new(File.open(skel.fname, "rb").read())
+			reader.push()
+			while (reader.not_done())
+				pt = Point2d.load_from(reader)
+				ang = reader.parse_f64()
+				t = reader.parse_varint()
+				pm = nil
+				error_spec = nil
+				if (t == 0) # GcodeSpec
+					pm_fname = reader.parse_string()
+					pm = pm_loader.load_pm_string(Pathname.new(pm_fname))
+					error_spec = "Could not load #{pm_fname}" if !pm
+				elsif (t == 1) # HoleSpec
+					pm = pm_loader.load_pm(HoleSpec)
+					error_spec = "Could not load #{HoleSpec}" if !pm
+				else
+					raise "error loading: #{skel.fname}"
+				end
+				# Comment the following line out to enable skipping and deleting of unknown parts.
+				raise error_spec if error_spec
+				if (pm)
+					skel.layout.add_item(pm, pt, ang)
+				end
+			end
+			reader.pop()
+		end
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/metal_sheet.rb b/manufacturing/TravisProg1/lib_files/geometry/router/metal_sheet.rb
new file mode 100755
index 0000000..c3cd21e
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/metal_sheet.rb
@@ -0,0 +1,154 @@
+require "json"
+module Router
+	class SheetCut
+		attr_accessor :layout, :fname
+		def initialize(fname)
+			@fname = fname
+			@layout = SheetLayout.new()
+		end
+		def save_changes()
+			ser = SerializeBuffer::Writer.new()
+			serialize(ser)
+			data = ser.data
+			len = SerializeBuffer.get_concat_length(data)
+			buffer = OutputView.new(len)
+			SerializeBuffer.concat_output(buffer, data)
+			File.open(@fname, "wb+") { |f| f.write(buffer.val) }
+		end
+		def inspect()
+			lns = @layout.items.collect { |a|
+				a.item.filename.name()
+			}
+			return "<SheetCut: #{@fname}, #{lns.inspect}>"
+		end
+		def layout() @layout end
+	end
+	class MetalSheet
+		def initialize(sheet_info, sheet_folder)
+			@sheet_folder = sheet_folder
+			@cuts = []
+			@sheet_info = sheet_info
+			@active_cut = nil
+		end
+		def sheet_info() @sheet_info end
+		def load_cut(fname, pm_loader)
+			puts "loading cut: #{fname}"
+			cut = SheetCut.new(fname)
+			SheetCut.load(cut, pm_loader)
+			@cuts << cut
+		end
+		def cuts() @cuts end
+		def inspect() "<MetalSheet: #{@sheet_folder}, #{@sheet_info}, #{@cuts.inspect}>" end
+		def add_cut(name)
+			@cuts << cut = SheetCut.new(@sheet_folder + "#{name}.cut")
+			cut
+		end
+		def save_changes()
+			@cuts.each do |cut|
+				cut.save_changes()
+			end
+		end
+		def active_cut_change(&blk)
+			@active_cut_change = blk
+			@active_cut_change.call(@active_cut) if @active_cut
+		end
+		def sheet_name() @sheet_folder.basename.to_path end
+		def active_cut() @active_cut end
+		def set_active(cut)
+			@active_cut_change.call(cut) if @active_cut_change
+			@active_cut = cut
+		end
+		def empty?() @cuts.empty?() end
+	end
+	class MetalSheetLoader
+		def initialize(sheet, cuts)
+			@sheet_name = sheet
+			@sheet = SheetInfo.parse(sheet)
+			@cuts = cuts
+		end
+		def sheet_name()
+			@sheet_name.dirname.basename.to_path
+		end
+		def self.make(folder)
+			c = folder.children.select(&:file?)
+			sheets = [] 
+			cuts = []
+			c.each do |child|
+				sheets << child if (child.extname == ".sheet")
+				cuts << child if (child.extname == ".cut")
+			end
+			return nil if (sheets.length != 1)
+			return MetalSheetLoader.new(sheets[0], cuts)
+		end
+		def open(pm_loader)
+			ms = MetalSheet.new(@sheet, @sheet_name.dirname)
+			@cuts.each do |cut|
+				ms.load_cut(cut, pm_loader)
+			end
+			if (active_fname = @sheet.active())
+				ms.cuts.each do |cut|
+					if (cut.fname.to_path == active_fname)
+						ms.set_active(cut)
+					end
+				end
+			end
+			return ms
+		end
+	end
+	class SheetDeltaLayout
+	end
+
+	class SheetList < Array
+		def initialize(folder)
+			folder.children.collect do |child|
+				c = MetalSheetLoader.make(child)
+				self << c if c
+			end
+			@open_sheets = {}
+		end
+		def open_sheet(i, parts_lib)
+			sheet_load = self[i]
+			name = sheet_load.sheet_name
+			return @open_sheets[name] ||= sheet_load.open(parts_lib)
+		end
+	end
+	class SheetInfo
+		def initialize(fname, json, w, h)
+			@fname = fname
+			@json = json
+			@w, @h = w, h
+		end
+		def self.parse(fname)
+			json = JSON.parse(File.read(fname))
+			w = json["w"]
+			h = json["h"]
+			self.new(fname, json, w.to_f, h.to_f)
+		end
+		def set_active(cut)
+			@json["active"] = cut.fname.to_path
+			File.open(@fname, "w+") { |f| f.write(JSON.dump(@json)) }
+		end
+		def active()
+			@json["active"]
+		end
+		def sheet_dxf()
+			fname = @json["sheet_dxf"]
+			return nil if !fname
+			@sheet_dxf ||= (
+			items = get_entities(fname)
+			items2d = items.collect { |item| item.to_2d() }
+
+			items2d_orient = Oriented.import_2d(items2d)
+#			items2d_orient = SweepLine.stitch_curves(items2d_orient)
+			items2d_orient
+			)
+		end
+		def hole_file()
+			hf = @json["hole_file"]
+			raise "sheet: #{@fname} missing hole filename" if !hf
+			return hf
+		end
+	end
+end
+
+require_relative "file_formats.rb"
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/nester.rb b/manufacturing/TravisProg1/lib_files/geometry/router/nester.rb
new file mode 100755
index 0000000..be6ba88
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/nester.rb
@@ -0,0 +1,46 @@
+require_relative "parts_library.rb"
+require_relative "metal_sheet.rb"
+require_relative "view.rb"
+
+module Router
+	def self.do_nesting()
+		parts_lib = PartsLibrary.new(Pathname.new("./parts_library"))
+		sheets = SheetList.new(Pathname.new("./sheets"))
+		puts sheets.inspect
+		view = RouterLayoutView.new(parts_lib, sheets)
+		view = ClickTypeSplitView.new(view)
+		EvWindow.run(view, 1000, 640)
+	end
+end
+
+# Folder organized list of parts files (folders binding DXF to GCODE).
+#
+# Stories:
+# 
+# Add Part to Delta Cut File:
+#  - select part
+#  - click to place
+#
+# From Any View:
+#  - Save Stuff. [Breadcrums: Go to view selection step; Bac]
+#
+# Views:
+# 
+# List of Sheets:
+#  - Click sheet name -> sheet-edit
+#  
+# Edit Sheet:
+#  - Add New Delta Sheet Cut. Add new part. Delete selected part. Generate Gcode.
+#  - Edit notes.
+#  - Select particular part.
+#  - Switch to different Delta Sheet Cut. (list of delta-sheet cuts).
+#
+# TODO(parker):
+#  - List + delete items
+#  - Serialize active cut choice.
+#  - Scrolling on lists that might grow too big.
+#  - Visual save indicator.
+#  - Error mechanism.
+# DONE
+#  - Select + emit non-zero origin.
+#  - Select active cut. List cuts.
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/parts_library.rb b/manufacturing/TravisProg1/lib_files/geometry/router/parts_library.rb
new file mode 100755
index 0000000..ea0dd7a
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/parts_library.rb
@@ -0,0 +1,111 @@
+require "fileutils"
+require "pathname"
+require "set"
+
+module Router
+	class GcodeSpec
+		class SpecError
+			def initialize(msg)
+				@msg = msg
+			end
+			def msg() @msg end
+		def is_error() true end
+		end
+		def is_error() false end
+		def initialize(folder, dxf, gcode_files)
+			@folder = folder
+			@dxf = dxf
+			@gcode_files = gcode_files
+			puts self.inspect()
+		end
+		def dxf_name() @folder + @dxf end
+		def inspect()
+			"<Part: #{@folder.to_path.inspect}, #{@dxf.inspect}, #{@gcode_files.inspect}>"
+		end
+		def name()
+			@folder.dirname.to_path.inspect
+		end
+		def full_path() @folder.to_path end
+		def self.make(folder, children)
+			# puts "#{folder.inspect} -> #{children.inspect}"
+			dxfs = []
+			ngcs = []
+			children.each do |child|
+				dxfs << child if (child.extname == ".dxf")
+				dxfs << child if (child.extname == ".DXF")
+				ngcs << child if (child.extname == ".ngc")
+			end
+			return nil if (dxfs.length == 0 && ngcs.length == 0)
+			return SpecError.new("#{folder.inspect} #{dxfs.length} dxfs!") if (dxfs.length != 1)
+			return SpecError.new("#{folder.inspect} no ngcs!") if (ngcs.length == 0)
+			return GcodeSpec.new(folder, dxfs[0].basename.to_path, ngcs)
+		end
+	end
+	class HoleSpec
+		Radius = 0.25
+	end
+	class PartsLibrary
+		def fname() @folder.basename.to_path end
+		def initialize(folder)
+			@folder = folder
+			@sub_libs_by_name = {}
+			rescan()
+		end
+		def rescan()
+			#puts "entering #{fname()}"
+			c = @folder.children
+			bad = c.select() { |a| !(a.directory? || a.file?) }
+			raise "bad filenames of #{fname()} -> #{bad}" if (!bad.empty?()) 
+			@spec = GcodeSpec.make(@folder, c.select(&:file?))
+			@sub_libs = [] 
+			c.select(&:directory?).each do |d| 
+				subl = PartsLibrary.new(d)
+				if subl.non_empty()
+					if subl2 = @sub_libs_by_name[subl.fname]
+						subl2.rescan()
+						subl = subl2
+					else
+						puts "storing: #{subl.fname.inspect}"
+						@sub_libs_by_name[subl.fname] = subl
+					end
+					@sub_libs << subl
+				end
+			end
+		end
+		def sub_libs() @sub_libs end
+		def spec() @spec end
+		def non_empty()
+			@spec || @sub_libs.length != 0
+		end
+		def realize(fname)
+			# puts "fname: #{fname.inspect} #{@folder.inspect}"
+			deltp = fname.relative_path_from(@folder)
+			key = get_key_from(deltp)
+			# puts "fname: #{deltp} -> key: #{key}"
+			if (key)
+				#puts "key: #{key.inspect}"
+				target = @sub_libs_by_name[key]
+				if (target)
+					return @sub_libs_by_name[key].realize(fname)
+				else
+					puts "cannot find #{key} in #{@sub_libs_by_name.keys.inspect}"
+					return nil
+				end
+			else
+				if (@spec.is_error)
+					puts "realizing error parts library spec: #{fname} err: #{@spec.msg}"
+				end
+				return (!@spec || @spec.is_error) ? nil : @spec
+			end
+			#raise "problem!"
+		end
+		def get_key_from(deltp)
+			goal = Pathname.new(".")
+			return nil if (deltp == goal)
+			while deltp.dirname != goal
+				deltp = deltp.dirname
+			end
+			return deltp.to_path
+		end
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/geometry/router/view.rb b/manufacturing/TravisProg1/lib_files/geometry/router/view.rb
new file mode 100755
index 0000000..59737b5
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/geometry/router/view.rb
@@ -0,0 +1,851 @@
+require "gui/gtk_utils/colors.rb"
+
+require "gtk3"
+require "pathname"
+
+require "cairo"
+require "gui/gui_lib/ev_window.rb"
+require "gui/gtk_utils/pango_inspect.rb"
+require "gui/gtk_utils/clipping.rb"
+require "tokens/tokenizer_lib_base.rb"
+require "gui/gui_lib/line_edit.rb"
+require_relative "drag_window.rb"
+require_relative "emit_gcode.rb"
+
+def get_cursor_extents()
+  lay = get_desc_pango()
+  lay.set_text(" ")
+  extents = lay.pixel_extents[1]
+  return [extents.width, extents.height]
+end
+
+CursorExtents = get_cursor_extents()
+class StringEditView
+	attr_accessor :bbox
+	def resize(bbox) @bbox = bbox end
+	def xoff() @xoff || 2 end
+	def set_xoff(xoff) @xoff = xoff; self end
+	def initialize(txt)
+		@lay = get_desc_pango()
+		@txt = txt
+		@cursor = 0
+		update_txt()
+	end
+	def update_txt() @lay.set_text(@txt) end
+	def draw(cr)
+		y = 0
+		x = xoff
+		Color::Yellow.set(cr)
+		cr.update_pango_layout(@lay)
+
+		if (cursor)
+			attrs = Pango::AttrList.new()
+			attrs.insert(Color::Black.pattr_fg(cursor, cursor + 1))
+			p = @lay.index_to_pos(cursor)
+			Color::Yellow.set(cr)
+			ox, oy = x + p.x / Pango::SCALE, y + p.y / Pango::SCALE
+			if (@txt.length == cursor)
+				cr.rectangle(ox, oy, CursorExtents[0], CursorExtents[1])
+			else
+				p2 = @lay.index_to_pos(cursor + 1)
+				cr.rectangle(ox, oy, (p2.x - p.x) / Pango::SCALE, CursorExtents[1])
+			end
+			cr.fill()
+			@lay.attributes = attrs
+		end
+
+		cr.move_to(x, y)
+		cr.show_pango_layout(@lay)
+		@lay.attributes = nil if (cursor)
+		return y + height
+	end
+	def cursor() @cursor end
+	def key_press(ev)
+		cursor = [@cursor]
+		TextEditEvent.key_press(self, @txt, ev, cursor)
+		@cursor = cursor[0]
+	end
+	def button_press_event(ev, pt)
+		@cursor = get_cursor_x(pt.x)
+	end
+	def get_cursor_x(x)
+		x -= xoff
+		inside, index, trailing = @lay.xy_to_index(x * Pango::SCALE, 0)
+		index += 1 if index + 1 <= @txt.length && x > 0 && !inside
+		return index
+	end
+	def take_focus(ev) self end
+	def height() h = @lay.pixel_extents[1].height end
+end
+
+def rounded_rect(cr, x, y, w, h, r)
+	cr.move_to(x + r, y)
+  	cr.line_to(x, y + r) # should be arc
+	cr.line_to(x, y + h - r)
+  	cr.line_to(x + r, y + h) # should be arc
+	cr.line_to(x + w - r, y + h)
+		cr.line_to(x + w, y + h - r) # should be arc
+	cr.line_to(x + w, y + r)
+		cr.line_to(x + w - r, y) # should be arc
+	cr.line_to(x + r, y)
+end
+
+class ActionButton
+	def initialize(txt, obj, *action_args)
+		@lay = get_desc_pango()
+		@lay.set_text(txt)
+		@obj = obj
+		@action = action_args
+	end
+	Highlight = Color.new(0, 0, 0.75)
+	Normal = Color.new(0.0, 0, 1.0)
+	def draw(cr)
+		w = 0
+		(@hi ? Highlight : Normal).set(cr)
+		rounded_rect(cr, 2, 2, width() - 4, height() - 4, 3)
+		cr.fill()
+		Color::Yellow.set(cr)
+		cr.move_to(10, 5)
+		cr.show_pango_layout(@lay)
+	end
+	def button_press_event(ev, pt)
+		@obj.send(*@action)
+	end
+	def motion_notify_event(ev, pt) @hi = true end
+	def exit_notify_event() @hi = false end
+	def height() @lay.pixel_extents[1].height + 10 end
+	def width() @lay.pixel_extents[1].width + 20 end
+	attr_accessor :bbox
+	def resize(bbox) @bbox = bbox end
+end
+
+class VerticalPacker
+	class Padding
+		def just_padding() true end
+		def xoff() nil end
+		def draw(cr) end
+		def height() @height end
+		def initialize(h) @height = h end
+	end
+	def initialize()
+		@non_pad_children = []
+		@children = []
+	end
+	def reset()
+		@non_pad_children = []
+		@children = []
+	end
+	def height()
+		h = 0
+		@children.each { |child| h += child.height }
+		return h
+	end
+	def width()
+		w = 0
+		@children.each { |child| w = [w, child.width].max() }
+		return w
+	end
+	attr_accessor :bbox
+	def resize(bbox)
+		@bbox = bbox
+		w = @bbox.w
+		y = 0 
+		@children.each do |item|
+			h = item.height
+			if !(item.respond_to?(:just_padding) && item.just_padding())
+				item.resize(SClipBBox.new(0, y, w, h))
+			end
+			y += h
+		end
+	end
+	def children() @non_pad_children end
+	def add(item)
+		raise 'adding null item' if (!item)
+		@children.push(item)
+		if !(item.respond_to?(:just_padding) && item.just_padding())
+			@non_pad_children.push(item)
+		end
+	end
+	def draw(cr)
+		cr.save()
+		y = 0
+		@children.each do |child|
+			child.draw(cr)
+			cr.translate(0, child.height)
+		end
+		cr.restore()
+	end
+end
+class EmptyView
+	def self.bbox() SClipBBox::InvalidBBox end 
+	def self.resize(bbox) end
+	def self.draw(cr)
+	end
+end
+class SplitScreen
+	attr_accessor :bbox
+	def initialize(lhs, rhs, split)
+		@children = [lhs, rhs]
+		@split = split
+	end
+	def lhs() @children[0] end
+	def rhs() @children[1] end
+	def lhs=(child)
+		@children[0] = child
+		resize(@bbox) if @bbox
+	end
+	def rhs=(child)
+		@children[0] = child
+		resize(@bbox) if @bbox
+	end
+	def resize(bbox)
+		@bbox = bbox
+		a = bbox.dup()
+		a.y = a.x = 0
+		b = a.dup()
+		b.x = @split
+		a.w = @split
+		b.w -= @split
+		@children[0].resize(a)
+		@children[1].resize(b)
+	end
+	def children()
+		@children
+	end
+	def draw(cr)
+		@children.each do |child|
+			child.bbox.enter(cr) {
+				child.draw(cr)
+			}
+		end
+	end
+end
+
+class TextWindow
+	attr_accessor :bbox
+	def resize(bbox) @bbox = bbox end
+	def initialize()
+		@lay = get_desc_pango()
+		@lay.set_text("hello world")
+	end
+	def draw(cr)
+		cr.set_source_rgb(0.0,0.3,0.1)
+		cr.paint()
+		cr.set_source_rgb(1.0,1.0,0.0)
+		cr.update_pango_layout(@lay)
+		cr.move_to(2, 2)
+		cr.show_pango_layout(@lay)
+	end
+end
+
+class MyEventWrapper
+  attr_accessor :ev, :x, :y
+  def initialize(ev)
+    @ev = ev
+    @x = ev.x
+    @y = ev.y
+  end
+end
+
+class ClickTypeSplitView
+	def initialize(tree)
+		@tree = tree
+	end
+  def resize(bbox)
+    @bbox = bbox
+		@tree.resize(bbox.dup())
+  end
+	def event_recurse(ev, evnt_m, tree)
+		x, y = ev.x, ev.y
+		if tree.respond_to?(evnt_m)
+			tree.send(evnt_m, ev.ev, Point2d.new(ev.x, ev.y))
+			return tree, 0, 0
+		end
+		return nil, 0, 0 if !tree.respond_to?(:children)
+		tree.children.each do |c|
+			if (c.bbox.contain(x, y))
+				ev.x -= c.bbox.x
+				ev.y -= c.bbox.y
+		#		puts "event_recurse: #{c.bbox.x}, #{c.bbox.y}"
+				v, dx, dy = event_recurse(ev, evnt_m, c)
+				return v, dx + c.bbox.x, dy + c.bbox.y if v
+				ev.x,ev.y = x,y
+			end
+		end
+		return nil, 0, 0
+	end
+	def scroll_event(ev)
+		event_recurse(MyEventWrapper.new(ev), :scroll_event, @tree)
+	end
+	def button_press_event(ev)
+		return if ev.time == @last_click_time
+		@last_clicker, @lcx, @lcy = event_recurse(MyEventWrapper.new(ev), :button_press_event, @tree) 
+		if @last_clicker && @last_clicker.respond_to?(:take_focus)
+			@focus = @last_clicker.take_focus(ev)
+		end
+		@last_click_time = ev.time
+	end
+	def button_release_event(ev)
+    pt = Point2d.new(ev.x, ev.y)
+		if @last_clicker && @last_clicker.respond_to?(:button_release_event)
+      pt = Point2d.new(ev.x, ev.y)
+			pt.x -= @last_clicker.bbox.x
+			pt.y -= @last_clicker.bbox.y
+			@last_clicker.button_release_event(ev, pt)
+		end
+		@last_clicker = nil
+	end
+	def motion_notify_event(ev)
+		if @last_clicker && @last_clicker.respond_to?(:motion_notify_event)
+      pt = Point2d.new(ev.x, ev.y)
+			pt.x -= @lcx
+			pt.y -= @lcy
+			@last_clicker.motion_notify_event(ev, pt)
+		else
+			obj, dx, dy = event_recurse(MyEventWrapper.new(ev), :motion_notify_event, @tree)
+			if (@exit_notify && obj != @exit_notify)
+				@exit_notify.exit_notify_event()
+				@exit_notify = nil
+			end
+			@exit_notify = obj if (obj.respond_to?(:exit_notify_event))
+		end
+	end
+  def key_press(ev)
+		if @focus && @focus.respond_to?(:key_press)
+			@focus.key_press(ev)
+		end
+  end
+  def key_release(ev)
+		if @focus && @focus.respond_to?(:key_release)
+			@focus.key_release(ev)
+		end
+  end
+	def draw(cr)
+		@tree.draw(cr)
+	end
+end
+
+class VerticalPackerPlan < Array
+	def make(obj)
+		pck = VerticalPacker.new()
+		self.each do |item|
+			if (item.is_a?(Symbol))
+				pck.add(obj.send(item))
+			else
+				pck.add(item)
+			end
+		end
+		return pck
+	end
+end
+
+module DelegateToLayout
+	def height() @layout.height end
+	def width() @layout.width end
+	def resize(bbox)
+		@layout.resize(bbox)
+	end
+	def bbox() @layout ? @layout.bbox : nil end
+	def children() @layout.children end
+	def draw(cr) @layout.draw(cr) end
+end
+
+module Router
+	class ConstStringLineView
+		def initialize(str)
+			@lay = get_desc_pango()
+			@lay.set_text(str)
+		end
+		def draw_to(cr, x, y)
+			#cr.set_source_rgb(1.0,1.0,0.0)
+			cr.move_to(x, y)
+			cr.update_pango_layout(@lay)
+			cr.show_pango_layout(@lay)
+			return y + height()
+		end
+		def height() @lay.pixel_extents[1].height end
+	end
+	class ConstStringView
+		def initialize(str, col = nil)
+			@col = col || Color::Yellow
+			@lay = get_desc_pango()
+			@lay.set_text(str)
+		end
+		def reset_text(txt) @lay.set_text(txt) end
+		def just_padding() return true end
+		attr_accessor :xoff
+		def set_xoff(xoff) @xoff = xoff; self end
+		def draw(cr)
+			y = 0
+			@col.set(cr)
+			cr.move_to(@xoff || 0, y)
+			cr.update_pango_layout(@lay)
+			cr.show_pango_layout(@lay)
+			return y + height()
+		end
+		def height() @lay.pixel_extents[1].height end
+	end
+	class ListView
+		HoverC = Color::White
+		DullC = Color::Yellow
+		attr_accessor :bbox
+		def resize(bbox) @bbox = bbox end
+		def initialize(list, to_viewable, on_item_click)
+			@on_item_click = on_item_click
+			@views = list.collect(&to_viewable)
+		end
+		def draw_to(cr, x, y)
+			@views.each.with_index do |item, i|
+				(i == @hover_i ? HoverC : DullC).set(cr)
+				y = item.draw_to(cr, x, y)
+			end
+			return y
+		end
+		def height() @height ||= @views.collect(&:height).inject(0, &:+) end
+		def button_press_event(ev, cur)
+			y = cur.y
+			@views.each.with_index do |view, i|
+				if (view.height() > y && y > 0)
+					@on_item_click.call(i)
+				end
+				y -= view.height()
+			end
+		end
+		def motion_notify_event(ev, cur)
+			y = cur.y
+			@views.each.with_index do |view, i|
+				if (view.height() > y && y > 0)
+					@hover_i = i
+				end
+				y -= view.height()
+			end
+		end
+	end
+	class SheetListView
+		Sheets = ConstStringLineView.new("Select Sheet:")
+		attr_accessor :bbox
+		def resize(bbox)
+			@bbox = bbox
+      y = @scroll
+			y += 2 + Sheets.height + 5
+			@lv.bbox = SClipBBox.new(0, y, @bbox.w, h = @lv.height())
+			y += h
+		end
+		def children() [@lv] end
+		def initialize(sheets, master)
+			@sheets = sheets
+			@lv = ListView.new(@sheets, lambda {|a| ConstStringLineView.new("sheet #{a.sheet_name}") },lambda { |i|
+				master.set_sheet_view(master.open_sheet(i))
+			})
+      @scroll = 0
+		end
+    def scroll_event(ev, pt)
+      return if !ev.state.shift_mask?
+		  if (ev.direction == Gdk::ScrollDirection::DOWN)
+			  @scroll -= (Sheets.height * 3).to_i
+	   	elsif (ev.direction == Gdk::ScrollDirection::UP)
+			  @scroll += (Sheets.height * 3).to_i
+		  end
+      resize(@bbox)
+    end
+		def draw(cr)
+			x, y = 2, 2 + @scroll
+			Color::Yellow.set(cr)
+			y = Sheets.draw_to(cr, x, y) 
+			@lv.draw_to(cr, x + 5, y + 5)
+		end
+	end
+	class RouterLayoutView
+		include DelegateToLayout
+		def initialize(parts_lib, sheets)
+			@parts_lib = parts_lib
+			@sheets = sheets
+			@pm_loader = PMLoader.new()
+			@pm_loader.parts_library = @parts_lib
+			@layout = SplitScreen.new(EmptyView, @drag_window = DragWindow.new(@pm_loader), 400)
+			set_sheet_list_view()
+
+#			set_sheet_view(sheet = open_sheet(0))
+#			if sheet.cuts.length == 0
+#				@layout.lhs.cut_name = "cut1"
+#				@layout.lhs.add_cut()
+#			else
+#				sheet.set_active(sheet.cuts[0])
+#			end
+		end
+		def open_sheet(i)
+			@sheets.open_sheet(i, @pm_loader)
+		end
+		def set_sheet_list_view()
+			@layout.lhs = SheetListView.new(@sheets, self)
+		end
+		def set_sheet_view(sheet)
+			SheetView.make(sheet, @parts_lib, self).config_main_view(self)
+		end
+		def drag_window() @drag_window end
+		def set_active_view(view)
+			@layout.lhs = view
+		end
+	end
+	class SheetView
+		include DelegateToLayout
+		def initialize(sheet, parts_lib, master_view)
+			@master_view = master_view
+			@sheet, @parts_lib = sheet, parts_lib
+			@layout = LayoutPlan.make(self)
+			@add_entity_view = AddEntityView.new(self, @parts_lib)
+			@add_hole_view = AddHoleView.new(self)
+		end
+		def config_main_view(main_view)
+			if @sheet.empty?()
+				main_view.set_active_view(NewSheetCutView.new(self))
+			else
+				main_view.set_active_view(self)
+			end
+			main_view.drag_window.set_sheet(@sheet)
+		end
+		def self.make(sheet, parts_lib, master_view)
+			sheet_view = self.new(sheet, parts_lib, master_view)
+		end
+		def add_cut(cut_name)
+			set_active_cut(@sheet.add_cut(cut_name))
+			@active_cut_selector.set_current_cut()
+			@active_cut_selector.update_cut_list()
+		end
+		def set_active_cut(cut)
+			@sheet.set_active(cut)
+			@sheet.sheet_info.set_active(cut)
+			@master_view.set_active_view(self)
+		end
+		def enter_add_cut_view()
+			@master_view.set_active_view(NewSheetCutView.new(self))
+		end
+		def add_sheet_cmds()
+			return (V2HLayout
+				.add(ActionButton.new("Add Hole", self, :add_hole))
+				.add(ActionButton.new("Add Item", self, :add_entity))
+				.add(ActionButton.new("Del Select", self, :del_select))
+			)
+		end
+		def add_rescan_cmds()
+			return (V2HLayout
+				.add(ActionButton.new("Rescan parts_library", self, :rescan))
+			)
+		end
+		def rescan()
+			@parts_lib.rescan()
+			@add_entity_view = AddEntityView.new(self, @parts_lib)
+		end
+		def del_select()
+			active_cut = @sheet.active_cut
+			return puts "no active cut." if (!active_cut)
+			layout = active_cut.layout
+			select = layout.selected
+			return puts "nothing selected." if (!select)
+			layout.delete(select)
+			layout.selected = nil
+		end
+		def select_active_cut()
+			@active_cut_selector ||= ActiveCutSelector.new(self, @sheet)
+		end
+		def add_cut_cmds()
+			return (V2HLayout
+				.add(ActionButton.new("Emit Gcode", self, :emit_gcode))
+				.add(ActionButton.new("Save", self, :save_all_changes))
+				.add(ActionButton.new("Add Cut", self, :enter_add_cut_view))
+			)
+		end
+		def emit_gcode()
+			if (@sheet.active_cut == nil)
+				puts "first select active cut before emitting gcode."
+				return
+			end
+			@master_view.set_active_view(EmitGcodeView.new(@sheet, self, @master_view))
+		end
+		def save_all_changes()
+			@sheet.save_changes()
+		end
+		def add_hole()
+			@master_view.set_active_view(@add_hole_view)
+			@master_view.drag_window.set_active_adder(@add_hole_view)
+		end
+		def add_entity()
+			@master_view.set_active_view(@add_entity_view)
+			@master_view.drag_window.set_active_adder(@add_entity_view)
+		end
+		def done_adding_items()
+			@master_view.set_active_view(self)
+			@master_view.drag_window.set_active_adder(nil)
+		end
+		def show_sheet_name()
+			ConstStringView.new("Editing Sheet: #{@sheet.sheet_name}").set_xoff(2)
+		end
+
+		LayoutPlan = VerticalPackerPlan.new([
+			VerticalPacker::Padding.new(2),
+			:show_sheet_name,
+			:add_sheet_cmds,
+			:add_rescan_cmds,
+#			VerticalPacker::Padding.new(5),
+			VerticalPacker::Padding.new(5),
+			:add_cut_cmds,
+			:select_active_cut,
+		])
+	end
+	class NewSheetCutView
+		include DelegateToLayout
+		attr_accessor :cut_name
+		def initialize(sheet_view)
+			@sheet_view = sheet_view
+			@cut_name = ""
+			@layout = LayoutPlan.make(self)
+		end
+		def add_cut()
+			if @cut_name.length == 0
+				puts "error! cut filename is empty"
+				return
+			end
+			@sheet_view.add_cut(@cut_name)
+		end
+		def sheet_name_edit()
+			return StringEditView.new(@cut_name)
+		end
+		def add_cut_button()
+			return V2HLayout.add(ActionButton.new("Add Sheet Cut", self, :add_cut))
+		end
+		LayoutPlan = VerticalPackerPlan.new([
+			VerticalPacker::Padding.new(2),
+			ConstStringView.new("Adding new sheet cut. Enter filename:").set_xoff(2),
+			:sheet_name_edit,
+			:add_cut_button,
+		])
+	end
+	class AddEntityView
+		include DelegateToLayout
+		def initialize(sheet_view, parts_lib)
+			@sheet_view, @parts_lib = sheet_view, parts_lib
+			@part_selector = PartSelectorView.new(@parts_lib, self)
+			@layout = LayoutPlan.make(self)
+		end
+		def part_selector()
+			@part_selector
+		end
+		def add_items() @sheet_view.done_adding_items() end
+		def to_sheet_view()
+			return V2HLayout.add(ActionButton.new("Done Adding Items", self, :add_items))
+		end
+		def current_item
+			@part_selector.current_item()
+		end
+		LayoutPlan = VerticalPackerPlan.new([
+			:to_sheet_view,
+			VerticalPacker::Padding.new(2),
+			ConstStringView.new("Adding new part by clicking\n while holding a:").set_xoff(2),
+			:part_selector,
+			#:sheet_name_edit,
+			#:add_cut_button,
+		])
+	end
+	class AddHoleView
+		include DelegateToLayout
+		def initialize(sheet_view)
+			@sheet_view = sheet_view
+			@layout = LayoutPlan.make(self)
+		end
+		def current_item()
+			HoleSpec
+		end
+		def done_adding_holes() @sheet_view.done_adding_items() end
+		def to_sheet_view()
+			return V2HLayout.add(ActionButton.new("Done Adding Holes", self, :done_adding_holes))
+		end
+		LayoutPlan = VerticalPackerPlan.new([
+			:to_sheet_view,
+			VerticalPacker::Padding.new(2),
+			ConstStringView.new("Adding new hole by clicking\n while holding a.").set_xoff(2),
+			#:sheet_name_edit,
+			#:add_cut_button,
+		])
+	end
+	class EmitGcodeView
+		include DelegateToLayout
+		def initialize(sheet, sheet_view, master_view)
+			@sheet = sheet
+			@sheet_view = sheet_view
+			@master_view = master_view
+			cut_fname = @sheet.active_cut.fname.basename
+			cut_name = cut_fname.to_path[0...-(cut_fname.extname.length)]
+			@fname = "output/#{Time.new().strftime("%Y%m%d/#{cut_name}.ngc")}"
+			Pathname.new(@fname).dirname.mkpath
+			@gcode_groups = @sheet.active_cut.make_gcode_groupings()
+			#@origin_status = ConstStringView.new("origin currently unset.").set_xoff(2),
+			@layout = LayoutPlan.make(self)
+			@origin_picker = OriginSelector.new(@sheet)
+			@master_view.drag_window.update_origin_picker(@origin_picker)
+		end
+		def done_adding_holes()
+			@sheet_view.done_adding_items()
+			@master_view.drag_window().update_sheet_layout(@sheet.active_cut.layout)
+		end
+		def to_sheet_view()
+			return V2HLayout.add(ActionButton.new("Done Emitting Gcode", self, :done_adding_holes))
+		end
+		def select_origin()
+			return V2HLayout.add(ActionButton.new("Set Origin", self, :do_select_origin))
+		end
+		def do_select_origin()
+			if (!@origin_picker.selected)
+				puts "could not set origin"
+				return
+			end
+			# TODO(parker): this might just need to be deleted.
+		end
+		def emit_group(group)
+			origin = @origin_picker.selected
+			return puts "origin not set!" if (!origin)
+			@gcode_groups.emit_group(group, @fname, origin)
+		end
+		def emit_holes()
+			origin = @origin_picker.selected
+			return puts "origin not set!" if (!origin)
+			@gcode_groups.emit_holes(@fname, @sheet.sheet_info, origin) 
+		end
+		def list_gcode_groups()
+			@list_gcode_groups ||= (
+				vp = VerticalPacker.new();
+				vp.add(V2HLayout.add(ActionButton.new("Emit holes", self, :emit_holes))) if (@gcode_groups.has_holes());
+				@gcode_groups.groups.each do |name, group|
+					vp.add(V2HLayout.add(ActionButton.new("Emit #{name.inspect}", self, :emit_group, group)));
+				end
+				vp;
+			)
+			return @list_gcode_groups
+		end
+		def gcode_name_edit()
+			return StringEditView.new(@fname)
+		end
+		LayoutPlan = VerticalPackerPlan.new([
+			:to_sheet_view,
+			VerticalPacker::Padding.new(2),
+			ConstStringView.new("Set Gcode filename:").set_xoff(2),
+			:gcode_name_edit,
+			#:select_origin,
+			ConstStringView.new("Select Gcode grouping to emit.").set_xoff(2),
+			:list_gcode_groups
+			#:sheet_name_edit,
+			#:add_cut_button,
+		])
+	end
+	class ActiveCutSelector
+		include DelegateToLayout
+		def initialize(sheet_view, sheet)
+			@sheet_view = sheet_view
+			@sheet = sheet
+			@current_cut = ConstStringView.new(" ").set_xoff(2)
+			@cut_list = VerticalPacker.new()
+			set_current_cut()
+			update_cut_list()
+			@layout = LayoutPlan.make(self)
+		end
+		def set_current_cut()
+			if (cut = @sheet.active_cut)
+				@current_cut.reset_text("current cut: #{cut.fname.basename.to_path}")
+			else
+				@current_cut.reset_text("no active cut. Select One.")
+			end
+		end
+		def select_cut(cut)
+			@sheet_view.set_active_cut(cut)
+			set_current_cut()
+		end
+		def update_cut_list()
+			@cut_list.reset()
+			@sheet.cuts.each do |cut|
+				@cut_list.add(V2HLayout.add(ActionButton.new(
+					"cut: #{cut.fname.basename.to_path}", self, :select_cut, cut)))
+			end
+			@sheet_view.resize(@sheet_view.bbox) if @sheet_view.bbox
+		end
+		attr_accessor :current_cut, :cut_list
+		LayoutPlan = VerticalPackerPlan.new([
+			:current_cut,
+			#:cut_list,
+		])
+	end
+	class PartSelectorView
+		include DelegateToLayout
+		def initialize(parts_lib, to_update)
+			@to_update = to_update
+			@parts_lib = parts_lib
+			@parts_lib_stack = [@parts_lib]
+			make_layout()
+		end
+		def current_item()
+			spec = @parts_lib.spec
+			return (!spec || spec.is_error) ? nil : spec
+		end
+		def up_dir()
+			@parts_lib_stack.pop()
+			@parts_lib = @parts_lib_stack[-1]
+			update_layout()
+		end
+		def down_dir(sub_lib)
+			@parts_lib = sub_lib
+			@parts_lib_stack.push(sub_lib)
+			update_layout()
+		end
+		def update_layout()
+			make_layout()
+			@to_update.resize(@to_update.bbox) if @to_update.bbox
+		end
+		def make_layout()
+			packer = VerticalPacker.new()
+			if (@parts_lib_stack.length > 1)
+				packer.add(V2HLayout.add(ActionButton.new("..", self, :up_dir)))
+			end
+			@parts_lib.sub_libs.each do |sub_lib|
+				packer.add(V2HLayout.add(ActionButton.new(sub_lib.fname(), self, :down_dir, sub_lib)))
+			end
+			if spec = @parts_lib.spec()
+				if (spec.is_error)
+					packer.add(ConstStringView.new("error: #{spec.msg}").set_xoff(2))
+				else
+					packer.add(ConstStringView.new("cur_part: #{spec.name}").set_xoff(2))
+				end
+			elsif (@parts_lib_stack.length > 1)
+				packer.add(ConstStringView.new("current folder: #{@parts_lib.fname()}").set_xoff(2))
+			end
+			@layout = packer
+		end
+		#def resize(bbox) @bbox = bbox end
+	end
+end
+class V2HLayout
+	def self.add(item) self.new().add(item) end
+	def initialize()
+		@items = []
+		@height = 0
+	end
+	def add(item)
+		@items << item
+		@height = [@height, item.height].max()
+		self
+	end
+	def children() @items end
+	def height() @height end
+	def resize(bbox)
+		@bbox = bbox
+		x = 0
+		@items.each do |item|
+			w = item.width
+			item.resize(SClipBBox.new(x, 0, w, @height))
+			x += w
+		end
+	end
+	attr_accessor :bbox
+	def draw(cr)
+		@items.each do |item|
+			item.bbox.enter(cr) { item.draw(cr) }
+		end
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gtk_utils/clipping.rb b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/clipping.rb
new file mode 100755
index 0000000..9378adb
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/clipping.rb
@@ -0,0 +1,26 @@
+class SClipBBox
+	attr_accessor :x, :y, :w, :h
+	def initialize(x, y, w, h) @x = x; @y = y; @w = w; @h = h; end
+	def ex() @x + @w end
+	def ey() @y + @h end
+	def ex=(v) @x = v - @w end
+	def ey=(v) @y = v - @h end
+	def sy=(y) @h += (@y - y); @y = y end
+	def sx=(x) @w += (@x - x); @x = x end
+	def hy=(y) @h = (y - @y) end
+	def wx=(x) @w = (x - @x) end
+	def contain(x, y)
+		(@x <= x && @y <= y && x < @x + @w && y < @y + @h)
+	end
+	def enter(cr, &blk)
+		cr.save()
+		cr.rectangle(@x, @y, @w, @h)
+		cr.translate(@x, @y)
+		cr.clip()
+		clipr = cr.get_clip_rectangle()[1]
+		blk.call() if (clipr.width != 0 && clipr.height != 0)
+		cr.restore()
+		return self
+	end
+	InvalidBBox = SClipBBox.new(0,0,0,0)
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gtk_utils/colors.rb b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/colors.rb
new file mode 100755
index 0000000..7f01146
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/colors.rb
@@ -0,0 +1,38 @@
+class Color
+	def initialize(r, g, b)
+		@r, @g, @b = r, g, b
+	end 
+	def set(cr) 
+		cr.set_source_rgb(@r, @g, @b) 
+	end 
+	def hex_code()
+		r = (@r * 255).to_i.to_s(16).rjust(2, "0")
+		g = (@g * 255).to_i.to_s(16).rjust(2, "0")
+		b = (@b * 255).to_i.to_s(16).rjust(2, "0")
+		"##{r}#{g}#{b}"
+	end 
+	def code_255()
+		return 16 + 36*(@r * 5).to_i + 6*(@g * 5).to_i + (@b * 5).to_i
+	end 
+	def pattr_fg(st, ed)
+		attr = Pango::AttrForeground.new(65535 * @r, 65535 * @g, 65535 * @b)
+		attr.start_index = st
+		attr.end_index = ed
+		return attr
+	end
+	def pattr_bg(st, ed)
+		attr = Pango::AttrBackground.new(65535 * @r, 65535 * @g, 65535 * @b)
+		attr.start_index = st
+		attr.end_index = ed
+		return attr
+	end
+	Yellow = self.new(1.0, 1.0, 0.0)
+	Red = self.new(1.0, 0.0, 0.0)
+	Green = self.new(0.0, 1.0, 0.0)
+	Blue = self.new(0.0, 0.0, 1.0)
+	Pink = self.new(1.0, 0.5, 0.5)
+	Gray = self.new(0.5, 0.5, 0.5)
+	Teal = self.new(0.5, 0.5, 1.0)
+	Black = self.new(0.0, 0.0, 0.0)
+	White = self.new(1.0, 1.0, 1.0)
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_inspect.rb b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_inspect.rb
new file mode 100755
index 0000000..ed7310a
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_inspect.rb
@@ -0,0 +1,58 @@
+require "pango"
+
+require_relative "pango_tools.rb"
+
+class Pango::Layout
+	def inspect()
+		ls = self.lines()
+		txt = "pango_layout {\n"
+		ls.each do |line|
+			txt += "  #{line.display_line(font_description)}\n"
+		end
+		return txt + "}"
+	end
+end
+class Pango::LayoutLine
+  def inspect() "#<pango_line: #{display_line(layout.font_description)}>" end
+	def display_line(desc)
+		"[ #{runs.collect { |rn| rn.display_run(desc)}.join(", ")}]"
+	end
+end
+FontTable = {}
+def get_font_mapping(i)
+	if (FontTable.length == 0)
+		l = Gdk.default_root_window.create_cairo_context.create_pango_layout
+		l.set_font_description(Desc)
+		" ".upto("~") do |c|
+			l.set_text(c)
+			g = l.lines[0].runs[0].glyphs.glyphs[0][0].glyph
+			puts g.inspect
+			puts g - c.ord
+			puts "#{g} => #{c.ord}"
+			FontTable[g] = c
+		end
+		txt = " ".upto("~").to_a.join()
+		l.set_text(txt)
+		#puts c.inspect
+	end
+	return FontTable[i]
+end
+class Pango::GlyphItem
+	def display_run(desc)
+    throw Exception.new("Glyph item is buggy.")
+		self.glyphs.glyphs.collect do |glyph, i|
+			t = glyph.glyph
+			#puts t.inspect
+			#puts (t + 29).chr.inspect
+			#get_font_mapping(t.chr)
+			(t - 1).chr
+
+#			(glyph.glyph + 29).chr
+		end.join()
+	end
+end
+class Pango::Rectangle
+	def inspect()
+		"#<Pango::Rectangle: #{x}, #{y}, #{width} x #{height}>"
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_tools.rb b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_tools.rb
new file mode 100755
index 0000000..650a91e
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gtk_utils/pango_tools.rb
@@ -0,0 +1,28 @@
+ENV["FONTCONFIG_FILE"] = "/tmp/fontcfg"
+require "gtk3"
+$layout_creator = Pango::Context.new()
+$layout_creator.font_map = Pango::CairoFontMap.default
+Desc = Pango::FontDescription.new(
+	RUBY_PLATFORM =~ /linux/ ? "Fixed Medium Semi-Condensed 10"
+#: "Lucida Console Normal 9")
+: "Lucida Sans Unicode Normal 12")
+
+def get_default_pango()
+	return Pango::Layout.new($layout_creator)
+end
+def update_pango_context(cr)
+	cr.update_pango_context($layout_creator)
+end
+
+def get_desc_pango()
+	p = get_default_pango()
+	p.set_font_description(Desc)
+	return p
+end
+
+def get_text_desc_pango(txt)
+	p = get_default_pango()
+	p.set_font_description(Desc)
+	p.set_text(txt)
+	return p
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gui_lib/ev_window.rb b/manufacturing/TravisProg1/lib_files/gui/gui_lib/ev_window.rb
new file mode 100755
index 0000000..a2dd95d
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gui_lib/ev_window.rb
@@ -0,0 +1,112 @@
+require "gtk3"
+
+require "gui/gtk_utils/pango_tools.rb"
+require "gui/gtk_utils/colors.rb"
+require "gui/gtk_utils/clipping.rb"
+
+class EventWindow
+	def initialize(wi = 640, hi = 480)
+		w = Gtk::Window.new("Window name")
+		w.set_size_request(wi, hi)
+		w.move(800, 0)
+		w.add(@d = Gtk::DrawingArea.new())
+		w.signal_connect("delete_event") { Gtk.main_quit }
+		@needs_draw = false
+		# Keep the last 2 because of ref-counting issues.
+		# I suspect that the update_pango_context which stores a
+		# reference to the cairo_context in the pango context no longer
+		# maintains that reference.
+		# This is a problem currently for ruby version:
+		# ruby 2.3.3p222 (2016-11-21 revision 56859) [x64-mingw32]
+		# and gems:
+		#  gtk3-3.1.0-x64-mingw32
+		#  cairo-gobject-3.1.0-x64-mingw32
+		#  cairo-1.15.4-x64-mingw32
+		#  pango-3.1.0-x64-mingw32
+		#  Sat 01/14/2017
+		# Parker, Michael, & Travis
+		@leak_cr = []
+		@d.signal_connect("draw") { |w, cr| 
+			# @leak_cr.push(cr)
+			# @leak_cr.unshift if (@leak_cr.length() > 2)
+			update_pango_context(cr)
+			Color.new(0.1,0.1,0.1).set(cr)
+			cr.paint()
+			draw(cr)
+#			obj.draw(cr)
+			@needs_draw = true
+		}
+		GC.start()
+		@d.set_events(Gdk::EventMask::ALL_EVENTS_MASK &
+             		 ~Gdk::EventMask::SMOOTH_SCROLL_MASK)
+
+		@d.set_can_focus(true)
+		@d.set_can_default(true)
+		@d.set_focus(true)
+
+		[["button_press_event", :button_press_event], 
+	 		["button_release_event", :button_release_event], 
+			["motion_notify_event", :motion_notify_event],
+			["key_press_event", :key_press],
+			["key_release_event", :key_release],
+			["scroll_event", :scroll_event],
+			["configure_event", :configure_event],
+		].each do |evname, evtype|
+			if respond_to?(evtype)
+				@d.signal_connect(evname) { |w, e|
+					send(evtype, e)
+				}
+			elsif has_event_delegate(evtype)
+				@d.signal_connect(evname) { |w, e|
+					ev_delegate_send(evtype, e)
+				}
+			end
+		end
+		w.show_all()
+	end
+	def has_event_delegate(evtype) return false end
+	def redraw()
+		if (@needs_draw)
+			@d.queue_draw()
+			@needs_draw = false
+		end
+	end
+	attr_accessor :widget
+end
+
+class EvWindow < EventWindow
+	def initialize(obj, *args)
+		@obj = obj
+		super(*args)
+	end
+	def has_event_delegate(evtype)
+		@obj.respond_to?(evtype)# || @obj.has_event_delegate(evtype)
+	end
+	def ev_delegate_send(evtype, ev)
+		@obj.send(evtype, ev)
+		redraw()
+	end
+	def configure_event(ev)
+		changed = false
+		if (@bbox == nil)
+			changed = true
+			@bbox = SClipBBox.new(0,0,ev.width, ev.height) 
+		end
+		if @bbox.w != ev.width
+			@bbox.w = ev.width
+			changed = true
+		end
+		if @bbox.h != ev.height
+			@bbox.h = ev.height
+			changed = true
+		end
+		@obj.resize(@bbox) if changed && @obj.respond_to?(:resize)
+	end
+	def draw(cr)
+		@obj.draw(cr)
+	end
+	def self.run(w, *args)
+		self.new(w, *args)
+		Gtk.main()
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gui_lib/line_edit.rb b/manufacturing/TravisProg1/lib_files/gui/gui_lib/line_edit.rb
new file mode 100755
index 0000000..3e9b654
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gui_lib/line_edit.rb
@@ -0,0 +1,285 @@
+require "gtk3"
+
+require "gui/gtk_utils/colors.rb"
+require "gui/gtk_utils/pango_inspect.rb"
+require "gui/gtk_utils/clipping.rb"
+require_relative "text_edit_event.rb"
+
+class ExpLine
+	def initialize(txt)
+		@lay = get_desc_pango()
+		@txt = txt
+		update_txt()
+	end
+	def empty() return @txt.length == 0 end
+	def draw(cr, cursor = nil, x = 0, y = 0)
+		Color::Yellow.set(cr)
+		cr.update_pango_layout(@lay)
+
+		if (cursor)
+			attrs = Pango::AttrList.new()
+			attrs.insert(Color::Black.pattr_fg(cursor, cursor + 1))
+			@lay.attributes = attrs
+			p = @lay.index_to_pos(cursor)
+			Color::Yellow.set(cr)
+			ox, oy = x + p.x / Pango::SCALE, y + p.y / Pango::SCALE
+			cr.rectangle(ox, oy, 6, 13)
+			cr.fill()
+		end
+
+		cr.move_to(x, y)
+		cr.show_pango_layout(@lay)
+		@lay.attributes = nil if (cursor)
+	end
+
+	def height() return @lay.pixel_extents[1].height end
+
+	def update_txt() @lay.set_text(@txt) end
+
+	def key_press(ev, cursor)
+		TextEditEvent.key_press(self, @txt, ev, cursor) 
+	end
+	def get_cursor_xpos(cursor)
+		p = @lay.index_to_pos(cursor)
+		return p.x / Pango::SCALE + (@xoff || 0)
+	end
+	def get_cursor_x(x)
+		x -= (@xoff || 0)
+		inside, index, trailing = @lay.xy_to_index(x * Pango::SCALE, 0)
+		index += 1 if index + 1 <= @txt.length && x > 0 && !inside
+		return index
+	end
+	def get_bbox(x, y)
+		ext = @lay.pixel_extents[1]
+		return SClipBBox.new(x + @xoff, y, ext.width + 6, ext.height)
+	end
+	attr_accessor :cursor, :xoff
+end
+
+class GenericFormattedLine
+	def draw(cr, cursor = nil, x = 0, y = 0)
+		Color::Yellow.set(cr)
+		cr.update_pango_layout(@lay)
+
+		if (cursor)
+			cursor = cursor[0] if cursor.class != Integer
+			attrs = Pango::AttrList.new()
+			attrs.insert(Color::Black.pattr_fg(cursor, cursor + 1))
+			@lay.attributes = attrs
+			p = @lay.index_to_pos(cursor)
+			Color::Yellow.set(cr)
+			ox, oy = x + p.x / Pango::SCALE, y + p.y / Pango::SCALE
+			cr.rectangle(ox, oy, 6, 13)
+			cr.fill()
+		end
+
+		cr.move_to(x, y)
+		cr.show_pango_layout(@lay)
+		@lay.attributes = nil if (cursor)
+	end
+	def height() return @lay.pixel_extents[1].height end
+	def key_press(ev, cursor)
+		cur = cursor[0]
+		g = 0
+		items = get_items()
+		items.each do |prefix, p, i|
+			g += prefix.length
+			cur = cursor[0] - g
+			if cur <= p.length
+				curref = [cur]
+				if !TextEditEvent.key_press(self, p, ev, curref)
+					k = ev.keyval
+					if (k == Gdk::Keyval::KEY_Right && i < (items.length - 1))
+						cursor[0] = g + p.length + items[i + 1][0].length
+					elsif (k == Gdk::Keyval::KEY_Left && i > 0)
+						cursor[0] = g - prefix.length
+					else
+						return false
+					end
+					return true
+				end
+				cursor[0] = g + curref[0]
+				return true
+			end
+			g += p.length
+		end
+		return false
+	end
+	def get_cursor_xpos(cursor)
+		p = @lay.index_to_pos(cursor)
+		return p.x / Pango::SCALE + (@xoff || 0)
+	end
+	def cap_length(index, len)
+		index = 0 if index < 0
+		index = len if index >= len
+		return index
+	end
+	def get_cursor_x(x)
+		x -= (@xoff || 0)
+		inside, index, trailing = @lay.xy_to_index(x * Pango::SCALE, 0)
+		items = get_items()
+		index += 1 if !inside
+		g = 0
+		items.each do |prefix, p, i|
+			next_prefix = items.length - 1 > i ? items[i + 1][0] : nil
+
+			g += prefix.length
+			if (!next_prefix || index - g < p.length + 1 + next_prefix.length / 2)
+				return g + cap_length(index - g, p.length)
+			end
+			g += p.length
+		end
+	end
+	def get_bbox(x, y)
+		ext = @lay.pixel_extents[1]
+		return SClipBBox.new(x + @xoff, y, ext.width + 6, ext.height)
+	end
+	attr_accessor :cursor, :xoff
+end
+
+class ForStartLine < GenericFormattedLine
+	A = "for ("
+	B = "; "
+	C = "; "
+	D = "):"
+	def initialize(a, b, c)
+		@a = a
+		@b = b
+		@c = c
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A + @a + B + @b + C + @c + D)
+	end
+	def get_items()
+		return [[A, @a, 0], [B, @b, 1], [C, @c, 2]]
+	end
+end
+
+class IfStartLine < GenericFormattedLine
+	A = "if ("
+	D = "):"
+	def initialize(a)
+		@a = a
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A + @a + D)
+	end
+	def get_items()
+		return [[A, @a, 0]]
+	end
+end
+
+class ElseIfStartLine < GenericFormattedLine
+	A = "else if ("
+	D = "):"
+	def initialize(a)
+		@a = a
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A + @a + D)
+	end
+	def get_items()
+		return [[A, @a, 0]]
+	end
+end
+class ElseStartLine < GenericFormattedLine
+	A = "else:"
+	def initialize()
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A)
+	end
+	def key_press(ev, cursor) return false end
+	def get_cursor_x(x) return A.length end
+end
+
+class FunctionStartLine < GenericFormattedLine
+	A = "function "
+	B = "("
+	C = ") -> "
+	D = ":"
+	def initialize(fn)
+		@name = fn.name
+		@args = fn.args
+		@ret_t = fn.ret_t
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A + @name + B + @args + C + @ret_t + D)
+	end
+	def get_items()
+		return [[A, @name, 0], [B, @args, 1], [C, @ret_t, 2]]
+	end
+end
+
+class ClassStartLine < GenericFormattedLine
+	A = "class "
+	B = ":"
+	def initialize(cls)
+		@name = cls.name
+		@lay = get_desc_pango()
+		update_txt()
+	end
+	def update_txt()
+		@lay.set_text(A + @name + B)
+	end
+	def get_items()
+		return [[A, @name, 0]]
+	end
+end
+
+def compile_suite_to(suite, lines, xoff = 0)
+	suite.each do |item|
+		item.compile_to(lines, xoff)
+	end
+end
+
+
+
+class ExpStmt
+	def compile_to(lines, xoff)
+		lines << exp = ExpLine.new(@exp)
+		exp.xoff = xoff
+	end
+end
+
+class IfStmt
+	def compile_to(lines, xoff)
+		i = 0
+		while (i < @suites.length || i < @conds.length)
+			suite = @suites[i] ||= []
+			cond = @conds[i]
+			if i == 0
+				lines << if_stmt = IfStartLine.new(cond)
+				if_stmt.xoff = xoff
+			else
+				if (cond)
+					lines << if_stmt = ElseIfStartLine.new(cond)
+					if_stmt.xoff = xoff
+				else
+					lines << if_stmt = ElseStartLine.new()
+					if_stmt.xoff = xoff
+				end
+			end
+			compile_suite_to(suite, lines, xoff + 10)
+			i += 1
+		end
+	end
+end
+
+class ForStmt
+	def compile_to(lines, xoff)
+		lines << f_stmt = ForStartLine.new(@a, @b, @c)
+		f_stmt.xoff = xoff
+		compile_suite_to(@suite, lines, xoff + 10)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/gui/gui_lib/text_edit_event.rb b/manufacturing/TravisProg1/lib_files/gui/gui_lib/text_edit_event.rb
new file mode 100755
index 0000000..261572c
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/gui/gui_lib/text_edit_event.rb
@@ -0,0 +1,29 @@
+class TextEditEvent
+	def self.key_press(obj, txt, ev, cursor)
+		k = ev.keyval
+    if (k >= 32 && k <= 126) 
+      txt.insert(cursor[0], k.chr)
+			obj.update_txt()
+			cursor[0] += 1
+    elsif k == Gdk::Keyval::KEY_Right && cursor[0] < txt.length
+     	cursor[0] += 1
+    elsif k == Gdk::Keyval::KEY_BackSpace && cursor[0] > 0
+      cursor[0] -= 1
+			txt.slice!(cursor[0], 1)
+			obj.update_txt()
+    elsif k == Gdk::Keyval::KEY_Delete && cursor[0] < txt.length
+			txt.slice!(cursor[0], 1)
+			obj.update_txt()
+    elsif k == Gdk::Keyval::KEY_Left && cursor[0] > 0
+      cursor[0] -= 1
+		elsif k == Gdk::Keyval::KEY_Home
+			cursor[0] = 0
+		elsif k == Gdk::Keyval::KEY_End
+			cursor[0] = txt.length
+		else
+			return false
+		end
+		return true
+	end
+end
+
diff --git a/manufacturing/TravisProg1/lib_files/tokens/tokenizer_lib_base.rb b/manufacturing/TravisProg1/lib_files/tokens/tokenizer_lib_base.rb
new file mode 100755
index 0000000..02b7543
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/tokens/tokenizer_lib_base.rb
@@ -0,0 +1,148 @@
+class StringBuffer
+	def is_buffered(); true;	end
+	def initialize(string)
+		@string = string
+    @line = 1
+    @col_nums = []
+    @col = 0
+		@i = 0
+	end
+  def lineno()
+    return @line
+  end
+  def filename
+    return @filename || "<string>"
+  end
+  def pos()
+    "#{filename()}:#{lineno()},#{@col}"
+  end
+  def pop_char()
+    val = @string[@i,1]
+		@i += 1
+    @col_nums[@line] = @col += 1
+    if(val == "\n")
+      @line += 1
+      @col = 0
+    end
+    return val
+  end
+  def unpop_char(char)
+		@i -= 1
+    if(char == "\n")
+      @line -= 1
+      @col = @col_nums[@line]
+    end
+    @col -= 1
+  end
+end
+class BufferedReader
+	def is_buffered(); true;	end
+  def initialize(file)
+    @file = file
+    @line = 1
+    @chars = []
+    @col_nums = []
+    @col = 0
+		@i = 0
+		@charbuf_i = 1e9
+		@ti = 0
+  end
+	def path()
+		@file.path
+	end
+  def filename
+    return File.basename(@file.path)
+  end
+  def pos()
+    "#{filename()}:#{lineno()},#{@col}"
+  end
+  def clear_comment()
+    @file.gets
+    @line += 1
+  end
+  def lineno()
+    return @line
+    return @file.lineno + 1
+  end
+	def read_next_char()
+		if @charbuf_i >= 2048
+			@charbuf = @file.read(2048)
+			@ti += 2048
+			@charbuf_i = 0
+		end
+		return nil if @charbuf == nil
+		val = @charbuf[@charbuf_i]
+		@charbuf_i += 1
+		return val
+	end
+  def pop_char()
+		@i += 1
+    val = @chars.pop() || read_next_char()
+    @col_nums[@line] = @col += 1
+    if(val == "\n")
+      @line += 1
+      @col = 0
+    end
+
+    return val
+  end
+  def unpop_char(char)
+		@i -= 1
+    if(char == "\n")
+      @line -= 1
+      @col = @col_nums[@line]
+    end
+    @col -= 1
+    @chars.push(char)
+  end
+	def get_data()
+		ie = @i - @ti + 2048
+		is = @old_i - @ti + 2048
+		if (is > 0)
+			return @charbuf[is...ie]
+		end
+		#puts "get_data: #{is} #{ie}"
+		@file.seek(@old_i, IO::SEEK_SET)
+		#@chars = []
+		val = @file.read(@i - @old_i)
+		@file.seek(@ti, IO::SEEK_SET)
+		return val
+	end
+	def mark_data()
+		@old_i = @i - 1
+	end
+end
+require "yaml"
+class TokensConvert
+	def self.unEscapeString(str)
+		return YAML.load(%Q(---\n#{str}\n))
+	end
+	def self.toFloat(str)
+		return str.to_f
+	end
+	def self.toInteger(str)
+		return str.to_i
+	end
+end
+class Module
+	def init_fields(*fields)
+		fn = "def initialize(#{fields.join(",")})\n"
+		fn += "super()\n"
+		fields.each do |fname|
+			fn += "@#{fname} = #{fname}\n"
+		end
+		fn += "end\n"
+		self.class_eval(fn)
+		attr_accessor(*fields)
+	end
+	def init_fields_ext(*fields)
+		fn = "def initialize(#{fields.join(",")})\n"
+		fields.each do |fname|
+			fn += "@#{fname} = #{fname}\n"
+		end
+		fn += "initialize_extended()"
+		fn += "end\n"
+		self.class_eval(fn)
+		attr_accessor(*fields)
+	end
+end
diff --git a/manufacturing/TravisProg1/lib_files/tools/repo_utils/unrepoize.rb b/manufacturing/TravisProg1/lib_files/tools/repo_utils/unrepoize.rb
new file mode 100755
index 0000000..4c39eb4
--- /dev/null
+++ b/manufacturing/TravisProg1/lib_files/tools/repo_utils/unrepoize.rb
@@ -0,0 +1,35 @@
+def unrepoize(extern_path, target)
+	if ARGV[0] == "-unrepoize"
+		$LOAD_PATH[0] = "~/repo"
+		require 'fileutils'
+		ARGV.slice!(0..0)
+		extern_path = File.expand_path(extern_path)
+		repo_path = File.expand_path("~/repo")
+		target = extern_path + target
+		exp = [File.expand_path("~/repo/tools/repo_utils/unrepoize.rb")]
+		
+#		puts target
+		$unrepoize_handler = Proc.new() {
+			(exp + $").each do |f|
+				#f = String.new(f)
+				#puts f.inspect, f.class
+#				FileUtils.mkpath(target)
+				if (f.start_with?(repo_path) && !f.start_with?(extern_path))
+					sf = f
+					tf = target + f[repo_path.length..-1]
+					FileUtils.mkpath(File.dirname(tf))
+					FileUtils.cp(sf, tf)
+					#puts "unrepoize #{tf}"
+				else
+					#puts "skipping: #{f.inspect}"
+				end
+			end
+		}
+	end
+end
+
+def unrepoize_finish()
+	if $unrepoize_handler
+	 	$unrepoize_handler.call()
+	end
+end
diff --git a/manufacturing/TravisProg1/travis_prog.rb b/manufacturing/TravisProg1/travis_prog.rb
new file mode 100755
index 0000000..d0ad046
--- /dev/null
+++ b/manufacturing/TravisProg1/travis_prog.rb
@@ -0,0 +1,3 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__) + "/lib_files")
+require "geometry/router/nester.rb"
+Router::do_nesting()