Maximum Distance Bisector

about 500 lines of tk codes for Bisector CRUD

#!/usr/bin/env ruby
require 'tk'
PntRad = 3    # Point radius of TkcOval
ScrHgt = 600  # Screen Hight

class Geom
  Infinity = 10*ScrHgt
  @@canvas = Hash.new
  attr_accessor :g

  def initialize
    @g = nil
  end
  
  def delete
    if @g.class <= Array
      @g.each {|g| delete_geom g}
    else
      @g.destroy
    end
  end
  
  def has? g
    @g === g
  end
  
  def []=(*args)
    if @g.class <= Array
      @g.each {|g| g.[]= *args}
    else
      @g.[]= *args
    end
  end
    
  def [](arg)
    if @g.class <= Array
      @g.each {|g| g.[] arg}
    else
      @g.[] arg
    end
  end

  def Geom.[]=(key, val)
    @@canvas[key] = val
  end
  
  def Geom.[](key)
    @@canvas[key]
  end
  
  def Geom.size
    @@canvas.size
  end
    
end


class Point < Geom
  attr_accessor :x, :y, :coords, :size

  def initialize *args
    super()
    if args.length > 1 && args[0].kind_of?(Fixnum) && args[1].kind_of?(Fixnum)
      @x, @y = args.shift, args.shift
    elsif args.length > 0 && args[0].kind_of?(Array) && !args[0].empty?
      pnt = args.shift
      @x = pnt[0]
      @y = pnt[1]
    else
      @x, @y = Infinity, Infinity
    end
    # default style
    args << {:fill=>nil}        unless args.find {|e| e.class <= Hash && e.has_key?(:fill)}
    args << {:outline=>'black'} unless args.find {|e| e.class <= Hash && e.has_key?(:outline)}
    h = args.find {|e| e.class <= Hash && e.has_key?(:size)}
    if h
      @size = h[:size]
    else
      args << {:size=>PntRad}
      @size = PntRad
    end
    @g = add_point @x, @y, *args
    Geom[@g] = self
  end

  def set *args
    if args.length > 1 && args[0].kind_of?(Fixnum) && args[1].kind_of?(Fixnum)
      @x, @y = args.shift, args.shift
    elsif args.length > 0 && args[0].kind_of?(Array) && !args[0].empty?
      pnt = args.shift
      @x = pnt[0]
      @y = pnt[1]
    else
      @x, @y = Infinity, Infinity
    end
    set_point @g, @x, @y, @size, *args
  end
    
  def coords= p
    @x, @y = p
    set_point @g, @x, @y, @size
  end

  def coords
    [@x, @y]
  end

  def size= rad
    @size = rad
    set_point @g, @x, @y, @size
  end

  def size
    @size
  end

  def move x, y
    @g.move x, -y
    @x += x
    @y += y
  end
end


class Line < Geom
  attr_accessor :p0, :p1, :coords

  def initialize *args
    super()
    if !args.empty? && args[0].class <= Array &&
    args[0][0].kind_of?(Fixnum) && args[0][1].kind_of?(Fixnum)
      @p0 = args.shift
    elsif !args.empty? && args[0].class <= Point 
      @p0 = args.shift.coords
    else
      @p0 = [0, 0]
    end
    if !args.empty? && args[0].class <= Array &&
    args[0][0].kind_of?(Fixnum) && args[0][1].kind_of?(Fixnum)
      @p1 = args.shift
    elsif !args.empty? && args[0].class <= Point 
      @p1 = args.shift.coords
    else
      @p1 = [0, 0]
    end
    @g = add_line @p0[0], @p0[1], @p1[0], @p1[1], *args
    Geom[@g] = self
  end

  def set x0, y0, x1, y1, *args
    @p0 = [x0, y0]
    @p1 = [x1, y1]
    set_line @g, x0, y0, x1, y1 *args
  end
    
  def coords= args
    @p0, @p1 = args
    set_line @g, @p0[0], @p0[1], @p1[0], @p1[1] 
  end
  
  def coords
    [@p0, @p1]
  end
  
  def move x, y
    @g.move x, -y
    @p0[0] += x
    @p0[1] += y
    @p1[0] += x
    @p1[1] += y
  end
end

def delete_geom(g)
  if g.class <= Array
    g.each {|e| delete_geom e}
  else
    g.destroy
  end
end

def add_point(x, y, *rest)
  pnt = TkcOval.new($canvas, 0, 0, 0, 0)
  rest << {:fill=>'red'} unless rest.find do |e| 
    e.class <= Hash && e.has_key?(:fill)
  end
  if rad = rest.find {|e| e.class <= Hash && e.has_key?(:size)}
    r = rad[:size]
  else
    r = PntRad
  end
  set_point(pnt, x, y, r, *rest)
end

def set_point(pnt, x, y, r, *rest)
  pnt.coords = x-r, ScrHgt-y-r, x+r, ScrHgt-y+r
  rest.each {|h| h.each {|k,v| pnt[k]=v unless k == :size}}
  pnt
end

def add_line(x0, y0, x1, y1, *rest)
  line = TkcLine.new($canvas, 0, 0, 0, 0)
  rest << {:fill=>'blue'} unless rest.find do |e| 
    e.class <= Hash && e.has_key?(:fill)
  end
  set_line(line, x0, y0, x1, y1, *rest)
end

def set_line(line, x0, y0, x1, y1, *rest)
  line.coords = x0, ScrHgt-y0, x1, ScrHgt-y1
  rest.each {|h| h.each {|k,v| line[k]=v}}
  line
end

def add_text(x, y, msg, *rest)
  text = TkcText.new($canvas, 0, 0, *rest)
  rest << {:fill=>'black'} unless rest.find do |e| 
    e.class <= Hash && e.has_key?(:fill)
  end
  rest << {:font=>'Courier -24 bold'} unless rest.find do |e| 
    e.class <= Hash && e.has_key?(:font)
  end
  set_text(text, x, y, msg, *rest)
end

def set_text(text, x, y, msg, *rest)
  text.coords x, ScrHgt - y
  text[:text] = msg
  rest.each{|h| h.each{|k,v| text[k]=v}}
  text
end

def find_geoms tag
  $canvas.find_withtag(tag).map{|e| Geom[e]}
end

def lincomb base, vec, len
  [base[0] + vec[0]*len, base[1] + vec[1]*len]
end

def dist p0, p1
  [(p0.x - p1.x).abs, (p0.y - p1.y).abs].max
end

class Bisector < Geom
  Len = ScrHgt
  attr_accessor :p0, :p1, :bd, :d0, :d1, :dd, :q0, :q1, :bs, :w0, :w1

  def initialize p0, p1, *args
    # default style
    args << {:tags=>$bsectag} unless args.find {|e| e.class <= Hash && e.has_key?(:tags)}
    args << {:fill=>'gray75'} unless args.find {|e| e.class <= Hash && e.has_key?(:fill)}
    @bs = Line.new *args
    args.delete_if {|e| e.has_key?(:tags)}
    @w0 = Line.new *args
    @w1 = Line.new *args
    set p0, p1, *args
    @g = [@bs, @w0, @w1].map! {|e| Geom[e.g] = self; e.g}
  end
  
  def set p0, p1, *args
    @p0 = p0
    @p1 = p1
    bisect
  end

  def delete
    @bs.delete
    @w0.delete
    @w1.delete
    super
  end
  
  def bisect
    wingbases @p0, @p1
    @bs.coords = @q0, @q1
    wingdirs @p0.x-@p1.x, @p0.y-@p1.y
    @w0.coords = @q0, lincomb(@q0, @d0, Len)
    @w1.coords = @q1, lincomb(@q1, @d1, Len)
  end

  def wingbases p0, p1
    xmax = [p0.x, p1.x].max
    xmin = [p0.x, p1.x].min
    xdif = (p0.x - p1.x).abs
    xmid = (p0.x + p1.x)/2
    ymax = [p0.y, p1.y].max
    ymin = [p0.y, p1.y].min
    ydif = (p0.y - p1.y).abs
    ymid = (p0.y + p1.y)/2
    if xdif < ydif
      @q0 = [xmin + ydif/2, ymid]
      @q1 = [xmax - ydif/2, ymid]
      @dd = [1, 0]
    elsif xdif > ydif
      @q0 = [xmid, ymin + xdif/2]
      @q1 = [xmid, ymax - xdif/2]
      @dd = [0, 1]
    else #xdif == ydif
      @q0 = @q1 = [xmid, ymid]
      @dd = [0, 0]
    end
  end

  def wingdirs dx, dy
    if dx == 0
      @d0, @d1 = [1, 0], [-1, 0]
    elsif dy == 0
      @d0, @d1 = [0, 1], [0, -1]
    elsif dx*dy < 0       # i.e. near -45 deg
      @d0, @d1 = [1, 1], [-1, -1]
    elsif dx.abs < dy.abs # i.e. near +45 deg and steep
      @d0, @d1 =  [1, -1], [-1, 1]
    else # dx.abs >= dy.abs i.e. near +45 deg and flat 
      @d0, @d1 = [-1, 1], [1, -1]
    end
    if @dd[0] == 0 and @dd[1] == 0
      @dd = @d0
    end
  end

  def belong_to? p
    p === @p0 || p === @p1
  end
  
  def distance
    dist @p0, @p1
  end
  
end

def reset_bisector s0, s1, *args
  find_geoms($bsectag).map{|b|
    b.delete if b.belong_to?(s0) && b.belong_to?(s1)
  }
  Bisector.new s0, s1, *args
end


if __FILE__ == $0
def init *args
  root = TkRoot.new *args
  $canvas = TkCanvas.new(root) { 
      width  ScrHgt
      height ScrHgt
      borderwidth 1
      relief  'sunken'
  }
  $canvas.pack
  $seltag = TkcTag.new($canvas)
  $status = add_text 250, 20, 
    "Mouse buttons: Left=>Drag, Middle=>Delete, Right=>Add\n",
    :font=>'Courier -12'
end

def run
  msgopt = {
    :icon=>'warning', :message=>'Realy quit?', 
    :type=>'okcancel', :default=>'ok', :title=>'WARNING'
  }
  TkButton.new(:text=>'Quit') {|b|
    command proc{ exit if Tk.messageBox(msgopt.merge(:parent=>b)) == 'ok'}
    pack :expand=>false, :anchor=>:e
  }
  Tk.mainloop
end

def scrumble_bsec
  period = 10 # ms
  ntimes = 200
  bsecs = find_geoms $bsectag
  sites = find_geoms $sitetag
  n = sites.length
  i = 0
  TkAfter.new(period, ntimes,
    proc{
      sites.each_with_index {|p, j|
        if j == rand(n) # or j == i%n
          a = rand 20
          b = rand 20
          case rand 100
          when 1..80
            a *= Math.sin(i%11? i/29: -i/31)
            b *= Math.cos(i%13? i/47: -i/43)
          when 81..90
            p.move b, -a
          else
            p.move -a, b
          end
          bsecs.each {|b| if b.belong_to? p then b.bisect end}
        end
        i += 1        
      }
    }
  ).start
end

def bind_bsec
  all = TkcTagAll.new($canvas)
  cur = TkcTagCurrent.new($canvas)
  mark = [0, 0] # canvas coordinates (upside down)
  target = nil  # a selected Geom object
  asite = nil   # a site that has the selected 'target'
  bsecs = nil   # surrounding bisectors that belong to the site
  all.bind 'ButtonPress-1',
    proc{|x,y|
      if cur.find[0].class <= TkcOval
        target = cur.find[0]
        mark = [x, y]
        asite = find_geoms($sitetag).find{|p| p.has? target}
        bsecs = find_geoms($bsectag).select{|b| b.belong_to? asite}
      end
    }, '%x %y'
  all.bind 'Button1-Motion',
    proc {|x,y|
      if target
        asite.move x-mark[0], mark[1]-y
        bsecs.each {|b| b.bisect} unless bsecs.empty?
        mark = [x, y]
      end
    }, '%x %y'
  all.bind 'ButtonRelease-1', proc{target = nil}
  all.bind 'ButtonPress-2',
    proc{
      item = Geom[cur.find.first]
      if item == nil or item.class <= Bisector
        Tk.messageBox({
          :icon=>'warning', :message=>'Can\'t delete bisectors', 
          :type=>'okcancel', :default=>'ok', :title=>'WARNING'
        })
      else
        item.delete
        show_stat
      end
    }
  $canvas.bind 'ButtonPress-3', proc{|x,y| add_site x, ScrHgt-y}, '%x %y'
  TkButton.new(:text=>'Scrumble') {|b|
    command proc{ yield }
    pack :expand=>false, :anchor=>:center
  }
end

def color r, g, b
  colors = ['00', '33', '66', '99', 'cc', 'ff']
  c = '#' + colors[r] + colors[g]  + colors[b]
end

def random_color
  color rand(5), rand(5), rand(5)
end

def add_site x, y
  sites = find_geoms $sitetag
  p = Point.new x, y, :tags=>$sitetag, :outline=>'black', :size=>6
  p.g[:fill] = random_color
  sites.map{|q| reset_bisector p, q, :fill=>'blue'}
  show_stat
  p
end

def show_stat
  bsecs = find_geoms $bsectag
  sites = find_geoms $sitetag
  $status[:text] = "Mouse buttons: Left=>Drag, Middle=>Delete, Right=>Add\n" +
    "# of sites: #{sites.size},  # of bisectors #{bsecs.size}"
end

class Point
  def delete
    find_geoms($bsectag).map{|b| b.delete if b.belong_to? self}
    super
  end
end

init 'title'=>"Bisectors"
$sitetag = TkcTag.new($canvas)
$bsectag = TkcTag.new($canvas)
s0 = add_site 200, 200
s1 = add_site 400, 250
s2 = add_site 350, 450
bind_bsec { scrumble_bsec }
run
end