Home | Ruby | Graphinity |     Share This Page

#!/usr/bin/ruby -w

=begin
/***************************************************************************
 *   Copyright (C) 2008, Paul Lutus                                        *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
=end

require 'graphinityui_ui'

require 'graphinityhelp'

include Math # this allows use of math functions without "Math." prefix

PROGRAM_VERSION = "2.4"

class Graphinity < GraphinityuiGlade
   attr_reader :program_title
   attr_reader :ini_file
   attr_accessor :graphic_pane
   attr_reader :config
   attr_reader :mouse_x_pos
   attr_reader :mouse_y_pos

   def initialize(path,xxx,name)
      super(path,xxx,name)
      @graphic_pane = nil
      @config = nil
      get_widgets()
      @mouse_press_x = nil
      @mouse_x_pos = nil
      @mouse_y_pos = nil
      @program_name = self.class.name
      @program_title = @program_name + " " + PROGRAM_VERSION
      GraphinityUI().set_title(@program_title)
      @anaglyphComboManager = ComboBoxManager.new(anaglyphicComboBox(),[ "None","Black","White" ])
      @config = Configuration.new
      @conf_handler = ConfigurationHandler.new(@config,self)
      @ini_file = @conf_handler.ini_file
      @help_engine = GraphinityHelp.new(self)
      fd = Pango::FontDescription.new("monospace,normal,12")
      helpTextEdit().modify_font(fd)
      helpTextEdit().modify_bg(Gtk::STATE_NORMAL,Gdk::Color.new(255,255,255))
      @graph_engine_2d = GraphEngine2d.new(self)
      @graph_engine_3d = GraphEngine3d.new(self)
      load_conf_values()
      [ graphicPane1(), graphicPane2()].each do |pane|
         pane.signal_connect("expose_event") do
            draw_image
         end
      end
      switch_tabs(controlTabWidget().page)
      set_panes_tooltip(twoThreeTabWidget().page)
   end

   def get_widgets()
      @glade.widget_names.each do |name|
         # create accessor methods for each defined widget
         eval("def #{name}() return @glade.get_widget(\"#{name}\") end")
         # associate events with actions
         w = @glade.get_widget("#{name}")
         if(w.class.name =~ /Gtk::Entry/)
            w.signal_connect('activate') { draw_image(true) }
            # don't scramble equations with mouse wheel
            unless(name =~ /equation/i)
               w.signal_connect('scroll_event') { |wid,evt| mouse_scroll_event(wid,evt) }
            end
         end
      end
   end

   def switch_tabs(m)
      case m
      when 0 then @graphic_pane = graphicPane1()
      when 1 then @graphic_pane = graphicPane2()
      when 2 then force_focus(helpSearchLineEdit())
      end
      draw_image if m < 2
   end

   def current_mode()
      return twoThreeTabWidget().page
   end

   # a hack to allow a reliable focus shift

   def force_focus(obj)
      Thread.new { sleep 0.1; obj.grab_focus }
   end

   def beep
      Gdk.beep
   end

   def status_bar(s)
      statusBarLabel().text = s
   end

   def Graphinity::message_dialog(window,message,inquiry = false)
      if inquiry
         dlg = Gtk::MessageDialog.new(nil,
         Gtk::MessageDialog::MODAL,
         Gtk::MessageDialog::QUESTION,
         Gtk::MessageDialog::BUTTONS_YES_NO,
         message)
      else # just an alert
         dlg = Gtk::MessageDialog.new(nil,
         Gtk::MessageDialog::MODAL,
         Gtk::MessageDialog::INFO,
         Gtk::MessageDialog::BUTTONS_OK,
         message)
      end
      dlg.set_title(window.class.name)
      response = dlg.run
      dlg.destroy
      return response == Gtk::MessageDialog::RESPONSE_YES || response == Gtk::MessageDialog::RESPONSE_OK
   end

   def create_control_array
      @control_array = [
         [:app_xpos,@app_x],
         [:app_ypos,@app_y],
         [:app_xsize,@app_width],
         [:app_ysize,@app_height],
         [:anaglyph_mode,anaglyphicComboBox()],
         [:equation_2d,equation2DLineEdit()],
         [:equation_3d,equation3DLineEdit()],
         [:x_axis_label,xLabelLineEdit()],
         [:y_axis_label,yLabelLineEdit()],
         [:chart_title,chartTitleLineEdit()],
         [:x_nums,xIndexCheckBox()],
         [:y_nums,yIndexCheckBox()],
         [:border ,borderCheckBox()],
         [:x_min2d,xMin2DLineEdit()],
         [:x_max2d,xMax2DLineEdit()],
         [:y_min2d,yMin2DLineEdit()],
         [:y_max2d,yMax2DLineEdit()],
         [:x_min3d,xMin3DLineEdit()],
         [:x_max3d,xMax3DLineEdit()],
         [:y_min3d,yMin3DLineEdit()],
         [:y_max3d,yMax3DLineEdit()],
         [:z_min3d,zMin3DLineEdit()],
         [:z_max3d,zMax3DLineEdit()],
         [:x_grid_steps,xGridStepsLineEdit()],
         [:y_grid_steps,yGridStepsLineEdit()],
         [:plot_steps_2d,plotSteps2DLineEdit()],
         [:plot_steps_3d,plotSteps3DLineEdit()],
         [:control_var_a,controlALineEdit()],
         [:control_var_b,controlBLineEdit()],
         [:control_var_c,controlCLineEdit()],
         [:line_thickness,lineThicknessLineEdit()],
         [:current_control_tab,controlTabWidget()],
         [:current_display_tab,twoThreeTabWidget()]
      ]
   end

   def save_control(val,con)
      result = -1
      case con.class.to_s
      when "Gtk::ComboBox" then result = con.active
      when "Gtk::CheckButton" then result = con.active?
      when "Gtk::SpinButton" then result = con.value
      when "Gtk::Notebook" then result = con.page
      when "Fixnum" then result = con
      when "Float" then result = con
      when "Gtk::Entry" then result = con.text
      end
      return result
   end

   def load_control(val,con)
      case con.class.to_s
      when "Gtk::ComboBox" then con.active = val.to_i
      when "Gtk::CheckButton" then con.active = val
      when "Gtk::SpinButton" then con.value = val
      when "Gtk::Notebook" then con.page = val.to_i
      when "Fixnum" then con = val.to_i
      when "Float" then con = val.to_f
      when "Gtk::Entry" then con.text = val.to_s
      end
   end

   def get_app_dims
      @app_width,@app_height = GraphinityUI().window.size
      @app_x,@app_y = GraphinityUI().window.position
   end

   def save_conf_values()
      get_app_dims
      create_control_array
      @control_array.each do |item|
         r = item[0].to_s + "="
         v = @config.send(item[0])
         @config.send(r, save_control(v,item[1]))
      end
      @conf_handler.writeConfig
   end

   def load_conf_values()
      get_app_dims
      create_control_array
      @conf_handler.readConfig
      @control_array.each do |item|
         load_control(@config.send(item[0]),item[1])
      end
      if(@config.app_xpos != -1)
         GraphinityUI().move(@config.app_xpos,@config.app_ypos)
         GraphinityUI().resize(@config.app_xsize,@config.app_ysize)
      end
   end

   def set_panes_tooltip(pane)
      tip = (pane != 1)?"Drag mouse = data, right-click = copy image to clipboard":\
      "Drag mouse to rotate, zoom with wheel, right-click = copy image to clipboard"
      graphicPane1().set_tooltip_text(tip)
      graphicPane2().set_tooltip_text(tip)
   end

   # mode 0 = 2d equation entered
   # mode 1 = 3d equation entered

   def draw_image(reset = false,mode = -1)
      if(mode >= 0)
         @config.graphic_mode = mode
      end
      if @graphic_pane && @graph_engine_2d && @graph_engine_3d
         case @config.graphic_mode
         when 0
            @graph_engine_2d.graph_equation(equation2DLineEdit(),"|x,y,a,b,c|",reset)
         when 1
            @graph_engine_3d.graph_equation(equation3DLineEdit(),"|x,y,z,a,b,c|",reset)
         end
      end
   end

   def Graphinity::col_to_rgb(col)
      return col.red,col.green,col.blue
   end

   def Graphinity::rgb_to_col(r,g,b)
      return Gdk::Color.new(r,g,b)
   end

   def Graphinity::fixnum_to_col(n)
      b = n & 255
      n >>= 8
      g = n & 255
      n >>= 8
      r = n & 255
      return Gdk::Color.new(r <<= 8,g <<= 8,b <<= 8)
   end

   def Graphinity::col_to_fixnum(col)
      result = (((col.red >> 8) & 255) << 16) | (((col.green >> 8) & 255) << 8) | ((col.blue >> 8) & 255)
      return result
   end

   def setColor(col)
      dialog = Gtk::ColorSelectionDialog.new("Color Selection")
      init = Graphinity::fixnum_to_col(col)
      dialog.colorsel.current_color = init
      result = dialog.run
      if result = Gtk::Dialog::RESPONSE_OK
         col = dialog.colorsel.current_color
      end
      dialog.destroy
      return Graphinity::col_to_fixnum(col)
   end

   # override parent class close method

   def close(*x)
      save_conf_values
      Gtk.main_quit
   end

   # use mouse motion to rotate 3D drawing

   def mouse_move_event (e)
      if(@mouse_press_x)
         @mouse_x_pos = e.x
         @mouse_y_pos = e.y
         if(current_mode() == 1)
            dx = (e.y - @mouse_press_x) / 2.0
            dy = (e.x - @mouse_press_y) / 2.0
            @config.rotx = (@mouse_press_rx - dx) % 360
            @config.roty = (@mouse_press_ry - dy) % 360
            xa = @config.rotx.to_i
            xa = -360 + xa if xa > 180
            s = sprintf("x = %3d°, y = %3d°",xa,@config.roty.to_i)
            status_bar(s)
         end
         draw_image
      end
   end

   # mouse right-click

   def mouse_context(e)
      if(@graph_engine_2d && @graph_engine_3d)
         case @config.graphic_mode
         when 0
            map = @graph_engine_2d.pixmap
         when 1
            map = @graph_engine_3d.pixmap
         end
         alloc = graphic_pane().allocation
         xsize = alloc.width
         ysize = alloc.height
         buf = Gdk::Pixbuf.from_drawable(nil,map,0,0,xsize,ysize)
         Gtk::Clipboard.get(GraphinityUI().display,Gdk::Selection::CLIPBOARD).image = buf
         status_bar("Image copied to clipboard.")
      end
   end

   # start mouse action

   def mouse_press_event (e)
      if(e.button == 3)
         mouse_context(e)
      else
         # set up to control rotation
         # by dragging mouse
         @mouse_x_pos = e.x
         @mouse_y_pos = e.y
         @mouse_press_rx = @config.rotx
         @mouse_press_ry = @config.roty
         @mouse_press_x = e.y
         @mouse_press_y = e.x
      end
      draw_image
   end

   # stop mouse action

   def mouse_release_event (e)
      @mouse_press_x = nil
      @mouse_x_pos = nil
      draw_image
   end

   def mouse_scroll_event (w,e)
      # get mouse wheel delta
      delta = (e.direction == Gdk::EventScroll::DOWN)?-1:1
      if(w.class.name =~ /EventBox/)
         @config.drawing_scale *= 1.0 + (delta * 0.1)
      elsif(w.class.name =~ /Entry/)
         s = w.text
         if(s =~ /\d/) # if some numeric digits
            sv = s.sub(%r{[-+\.\d]+(.*)},"\\1")
            nv = s.to_f
            w.text = (nv+delta).to_s + sv
         end
      end
      draw_image
   end

   # event handlers

   def on_GraphinityUI_delete_event(widget, arg0)
      close
   end
   def on_helpSearchLineEdit_key_release_event(widget, arg0)
      ss = helpSearchLineEdit().text
      @help_engine.search(ss)
   end
   def on_controlTabWidget_switch_page(widget, arg0,arg1)
      switch_tabs(arg1)
   end
   def on_borderCheckBox_toggled(widget)
      draw_image
   end
   def on_yIndexCheckBox_toggled(widget)
      draw_image(true)
   end
   def on_xIndexCheckBox_toggled(widget)
      draw_image(true)
   end
   def on_anaglyphicComboBox_changed(widget)
      if @config
         @config.anaglyph_mode = widget.active
      end
      draw_image(true)
   end
   def on_quitPushButton_clicked(widget)
      close
   end
   def on_numberColorPushButton_clicked(widget)
      @config.number_color = setColor(@config.number_color)
      draw_image
   end
   def on_borderColorPushButton_clicked(widget)
      @config.border_color = setColor(@config.border_color)
      draw_image
   end
   def on_textColorPushButton_clicked(widget)
      @config.text_color = setColor(@config.text_color)
      draw_image
   end
   def on_backgroundColorPushButton_clicked(widget)
      @config.background_color = setColor(@config.background_color)
      draw_image
   end
   def on_plotColorPushButton_clicked(widget)
      @config.plot_color = setColor(@config.plot_color)
      draw_image
   end
   def on_gridColorPushButton_clicked(widget)
      @config.grid_color = setColor(@config.grid_color)
      draw_image
   end
   def on_twoThreeTabWidget_switch_page(widget, arg0, arg1)
      set_panes_tooltip(arg1)
      draw_image(true,arg1)
   end
   def on_eventbox1_scroll_event(widget, arg0)
      mouse_scroll_event(widget,arg0)
   end
   def on_eventbox1_button_press_event(widget, arg0)
      mouse_press_event(arg0)
   end
   def on_eventbox1_button_release_event(widget, arg0)
      mouse_release_event(arg0)
   end
   def on_eventbox1_motion_notify_event(widget, arg0)
      mouse_move_event(arg0)
   end
   def on_eventbox2_scroll_event(widget, arg0)
      mouse_scroll_event(widget,arg0)
   end
   def on_eventbox2_button_press_event(widget, arg0)
      mouse_press_event(arg0)
   end
   def on_eventbox2_button_release_event(widget, arg0)
      mouse_release_event(arg0)
   end
   def on_eventbox2_motion_notify_event(widget, arg0)
      mouse_move_event(arg0)
   end

end # class Graphinity

class ComboBoxManager
   def initialize(box,list,default = nil)
      @box = box
      @hash = {}
      index = 0
      # a placeholder item is required to get around
      # a bug in the Glade designer that won't create
      # a sane combobox without it. So first,
      # remove the placeholder item
      @box.remove_text(0)
      list.each do |item|
         @box.append_text(item)
         @hash[item] = index
         index += 1
      end
      if (@hash[default])
         @box.set_active(@hash[default])
      else
         @box.set_active(0)
      end
   end
   def active_string()
      return @box.active_text
   end
end

=begin
   The Configuration class defines program values
   to be read from and written to a configuration file.
=end

class Configuration
   attr_accessor :app_xpos
   attr_accessor :app_ypos
   attr_accessor :app_xsize
   attr_accessor :app_ysize
   attr_accessor :anaglyph_mode
   attr_accessor :graphic_mode
   attr_accessor :equation_2d
   attr_accessor :equation_3d
   attr_accessor :chart_title
   attr_accessor :x_axis_label
   attr_accessor :y_axis_label
   attr_accessor :plot_steps_2d
   attr_accessor :plot_steps_3d
   attr_accessor :line_thickness
   attr_accessor :x_nums
   attr_accessor :y_nums
   attr_accessor :border
   attr_accessor :x_min2d
   attr_accessor :x_max2d
   attr_accessor :y_min2d
   attr_accessor :y_max2d
   attr_accessor :x_min3d
   attr_accessor :x_max3d
   attr_accessor :y_min3d
   attr_accessor :y_max3d
   attr_accessor :z_min3d
   attr_accessor :z_max3d
   attr_accessor :x_grid_steps
   attr_accessor :y_grid_steps
   attr_accessor :control_var_a
   attr_accessor :control_var_b
   attr_accessor :control_var_c
   attr_accessor :rotx
   attr_accessor :roty
   attr_accessor :drawing_scale
   attr_accessor :border_color
   attr_accessor :grid_color
   attr_accessor :number_color
   attr_accessor :text_color
   attr_accessor :plot_color
   attr_accessor :background_color
   attr_accessor :current_control_tab
   attr_accessor :current_display_tab

   # default values

   def initialize
      @app_xpos = 100
      @app_ypos = 100
      @app_xsize = 600
      @app_ysize = 600
      @anaglyph_mode = 0
      @x_min2d = -3
      @x_max2d = 3
      @y_min2d = 0
      @y_max2d = 1
      @x_min3d = -3
      @x_max3d = 3
      @y_min3d = -0.25
      @y_max3d = 0.75
      @z_min3d = -3
      @z_max3d = 3
      @x_grid_steps = 8
      @y_grid_steps = 8
      @control_var_a = 1
      @control_var_b = 1
      @control_var_c = 1
      @rotx = -20.0
      @roty = -20.0
      @drawing_scale = 1.0
      @chart_title = "y = ?"
      @x_axis_label = "x"
      @y_axis_label = "y"
      @graphic_mode = 0
      @equation_2d = "E^-x^2"
      @equation_3d = "E^-(x^2+z^2)"
      @plot_steps_2d = 500
      @plot_steps_3d = 16
      @line_thickness = 1
      @border = true
      @x_nums = true
      @y_nums = true
      @border_color = 0x0
      @grid_color = 0xa0c0a0
      @number_color = 0x4040ff
      @text_color = 0x0
      @plot_color = 0xaa007f
      @background_color = 0xffffff
      @current_control_tab = 2
   end
end

# ConfigurationHandler reads and writes configuration

class ConfigurationHandler
   attr_reader :ini_file
   def initialize(conf,app)
      @conf = conf
      @prog_name = app.class.to_s
      @conf_path = File.join(ENV["HOME"], "." + @prog_name)
      @ini_file = @conf_path + "/" + @prog_name + ".ini"
      Dir.mkdir(@conf_path) unless FileTest.exists?(@conf_path)
   end
   def writeConfig
      file = File.new(@ini_file,"w")
      unless file.nil?
         @conf.instance_variables.sort.each { |x|
            xi = @conf.instance_variable_get(x)
            sx = x.sub(/@/,"")
            # escape strings
            if(xi.class == String)
               xi.gsub!(/\\/,"\\\\\\\\")
               xi.gsub!(/"/,"\\\"")
               xi = "\"#{xi}\""
            end
            file.write("#{sx}=#{xi}\n")
         }
         file.close()
      end
   end
   def readConfig
      if FileTest.exists?(@ini_file)
         file = File.new(@ini_file,"r")
         file.each { |line|
            line.strip!
            unless line =~ /^#/ # unless a comment line
               begin
                  @conf.instance_eval("@" + line)
               rescue Exception
               end
            end
         }
         file.close()
      end
   end
end

# a Cartesian 3D vector class
# with a number of important operator overrides

class Cart3
   attr_accessor :x,:y,:z
   def initialize(x = 0,y = 0,z = 0)
      if(x.class == self.class)
         @x = x.x
         @y = x.y
         @z = x.z
      else
         @x = x
         @y = y
         @z = z
      end
   end

   def -(e)
      Cart3.new(@x - e.x,@y - e.y,@z - e.z)
   end

   def +(e)
      Cart3.new(@x + e.x,@y + e.y,@z + e.z)
   end

   def *(e)
      if(e.class != self.class)
         # multiply by scalar
         Cart3.new(@x * e,@y * e,@z * e)
      else
         # multiply by vector
         Cart3.new(@x * e.x,@y * e.y,@z * e.z)
      end
   end

   def /(e)
      if(e.class != self.class)
         # divide by scalar
         Cart3.new(@x / e,@y / e,@z / e)
      else
         # divide by vector
         Cart3.new(@x / e.x,@y / e.y,@z / e.z)
      end
   end

   # sum of squares
   def sumsq
      @x*@x+@y*@y+@z*@z
   end

   def to_s
      "[#{CommonCode::fmt_num(@x)},#{CommonCode::fmt_num(@y)},#{CommonCode::fmt_num(@z)}]"
   end
end # class Cart3

# RotationMatrix performs 3D rotations and perspective

class RotationMatrix

   # perspective depth cue for 3D -> 2D transformation
   PerspectiveDepth = 8
   # empirical constant for anaglyphic rotation
   AnaglyphScale = 0.05

   RotationMatrix::ToRad = Math::PI / 180.0
   RotationMatrix::ToDeg = 180.0 / Math::PI

   # populate 3D matrix with values for x,y,z rotations

   def populate_matrix(xa,ya)

      # create trig values
      @sy = Math.sin(xa * RotationMatrix::ToRad);
      @cy = Math.cos(xa * RotationMatrix::ToRad);
      @sx = Math.sin(ya * RotationMatrix::ToRad);
      @cx = Math.cos(ya * RotationMatrix::ToRad);
   end

   # 3D -> 2D, add perspective cue,
   # perform anaglyphic perspective rotation if specified

   def convert_3d_to_2d(v,anaglyph_flag = 0)
      v.x = (v.x * (PerspectiveDepth + v.z))/PerspectiveDepth
      v.x += v.z * anaglyph_flag * AnaglyphScale if anaglyph_flag
      v.y = (v.y * (PerspectiveDepth + v.z))/PerspectiveDepth
   end

   # rotate a 3D point using matrix values

   def rotate(v)
      # borrowed from my "Apple World" 1979
      hf = (v.x * @sx - v.z * @cx)
      py = v.y * @cy + @sy * hf
      px = v.x * @cx + v.z * @sx
      pz = -v.y * @sy + @cy * hf
      v.x = px; v.y = py; v.z = pz
   end
end # class RotationMatrix


# parent class for the GraphEngine2d and GraphEngine3d classes

class GraphEngine
   attr_reader :pixmap
   def initialize(app)
      @parent = app
      @pixmap = nil
      @suspend_equation = false
      @old_xsize = -1
      @old_ysize = -1
      @text_color = nil
   end
   def ntrp(xa,xb,ya,yb,x)
      return 0 if(xb-xa == 0)
      return ((x-xa)/(xb-xa) * (yb-ya)) + ya;
   end
   def inside(x,a,b)
      return (x >= a && x <= b)
   end

   def max(*a)
      return a.max
   end

   def min(*a)
      return a.min
   end

   def error_dialog(title,msg)
      mb = Gtk::MessageDialog.new(@parent.GraphinityUI,
      Gtk::Dialog::DESTROY_WITH_PARENT,
      Gtk::MessageDialog::WARNING,
      Gtk::MessageDialog::BUTTONS_CLOSE,
      msg)
      mb.set_title(@parent.program_title + " " + title + " Error")
      mb.run
      mb.destroy
   end

   # deal with various common numerical entry errors
   def test_num(widget,s)
      r = s.gsub(/\.+/,".")
      r = r.gsub(/(^|\D+)(\.\d)/,"\\10\\2")
      if(r != s)
         widget.text = r
      end
      return r
   end

   def getSet(widget, default)
      a = @control_var_a
      b = @control_var_b
      c = @control_var_c
      s = widget.text.strip
      v = 0
      if s.size > 0
         s = test_num(widget,s)
         begin
            eval("v = #{s}.to_f")
         rescue Exception => err
            s = "0.0"
            widget.text = s
            @parent.status_bar("Numeric entry error")
            str = err.to_s.sub(/(.*)for.*/m,"\\1")
            error_dialog("Number entry",str)
         end
      else
         v = default
         widget.text = v.to_s
      end
      return v
   end

   def set_scale_vals()
      xa2d = -3.0
      xb2d = 3.0
      ya2d = -1.0
      yb2d = 1.0
      xa3d = -3.0
      xb3d = 3.0
      ya3d = -1.0
      yb3d = 1.0
      za3d = -3.0
      zb3d = 3.0
      xgs = 10
      ygs = 10
      ps2d = 500
      ps3d = 16
      ca = 1
      cb = 1
      cc = 1
      lt = 0
      @control_var_a = getSet(@parent.controlALineEdit,ca)
      @control_var_b = getSet(@parent.controlBLineEdit,cb)
      @control_var_c = getSet(@parent.controlCLineEdit,cc)
      @xa2d = getSet(@parent.xMin2DLineEdit,xa2d)
      @xb2d = getSet(@parent.xMax2DLineEdit,xb2d)
      @ya2d = getSet(@parent.yMin2DLineEdit,ya2d)
      @yb2d = getSet(@parent.yMax2DLineEdit,yb2d)
      @xa3d = getSet(@parent.xMin3DLineEdit,xa3d)
      @xb3d = getSet(@parent.xMax3DLineEdit,xb3d)
      @ya3d = getSet(@parent.yMin3DLineEdit,ya3d)
      @yb3d = getSet(@parent.yMax3DLineEdit,yb3d)
      @za3d = getSet(@parent.zMin3DLineEdit,za3d)
      @zb3d = getSet(@parent.zMax3DLineEdit,zb3d)
      @x_grid_steps = getSet(@parent.xGridStepsLineEdit,xgs)
      @y_grid_steps = getSet(@parent.yGridStepsLineEdit,ygs)
      @plot_steps_2d = getSet(@parent.plotSteps2DLineEdit,ps2d)
      @plot_steps_3d = getSet(@parent.plotSteps3DLineEdit,ps3d)
      @line_thickness = getSet(@parent.lineThicknessLineEdit,lt)
   end

   def create_font (font_desc)
      @pango_context = Gdk::Pango.context
      desc = Pango::FontDescription.new(font_desc)
      fm = @pango_context.get_metrics(desc)
      cw = fm.approximate_char_width / Pango::SCALE
      ch = (fm.ascent + fm.descent) / Pango::SCALE
      return desc,cw,ch
   end

   def set_sizes()
      alloc = @parent.graphic_pane().allocation
      xsize = alloc.width
      ysize = alloc.height
      if(xsize != @old_xsize || ysize != @old_ysize)
         @old_xsize = xsize
         @old_ysize = ysize
         @text_scale = (@old_xsize > @old_ysize)?(@old_ysize / 65.0):(@old_xsize / 65.0)
         @normal_font,@normal_char_width,@normal_char_height = create_font("Monospace, Normal, #{@text_scale}")
         @title_font,@title_char_width,@title_char_height = create_font("Monospace, Normal, #{1.5 * @text_scale}")
      end
      @bg_color = Graphinity::fixnum_to_col(@parent.config.background_color)
      if(@parent.borderCheckBox.active?)
         @internal_margin = 10
         @draw_border = true
      else
         @internal_margin = 8
         @draw_border = false
      end
      @chart_title = @parent.chartTitleLineEdit.text.strip
      set_scale_vals
      @line_thickness = (@line_thickness < 0)?0:(@line_thickness)
   end

   def graph_equation(widget,templ,reset)
      if(reset)
         @suspend_equation = false
         # if mouse is not down
         if(!@parent.mouse_x_pos)
            @parent.status_bar("OK")
         end
      end
      s = widget.text
      if(s.size > 0)
         if(!@suspend_equation)
            begin
               s = test_num(widget,s)
               # allow "^" as power symbol
               s.gsub!(/\^/,"**")
               @gproc = eval("Proc.new { #{templ} #{s} }")
            rescue Exception => err
               if(!@suspend_equation)
                  @suspend_equation = true
                  @parent.status_bar("Equation error")
                  str = err.to_s.sub(/.*(syntax.*)/m,"\\1")
                  error_dialog("Equation",str)
               end
            end
         end
      end
      draw_image()
   end

   def draw_line(gc,sx,sy,ex,ey)
      # guard against numeric values
      # beyond the range of an integer
      begin
         @pixmap.draw_line(gc,sx,sy,ex,ey)
      rescue
      end
   end

   def fill_block(gc,color,sx,sy,wx,wy)
      gc.fill = Gdk::GC::Fill::SOLID
      gc.rgb_fg_color = color
      @pixmap.draw_rectangle(gc,true,sx,sy,wx+2,wy+2)
   end

   def draw_text(text,gc,x,y,font_desc,color = @text_color)
      gc.rgb_fg_color = color
      layout = Pango::Layout.new(Gdk::Pango.context)
      layout.font_description = font_desc
      layout.text = text
      @pixmap.draw_layout(gc,x,y,layout)
   end
end

class GraphEngine3d < GraphEngine

   def initialize(app)
      super(app)
      @rotator = RotationMatrix.new
      @old_plot_steps_3d = -1
   end

   def draw_graph(gc,anaglyph_flag,color)
      @rotator.populate_matrix(@parent.config.rotx,@parent.config.roty)
      zi = 0
      gc.rgb_fg_color = color
      # declare array for precomputing drawing points
      if(@plot_steps_3d != @old_plot_steps_3d)
         @plot_steps_3d = (@plot_steps_3d <= 0)?1:@plot_steps_3d
         @point_array = Array.new(@plot_steps_3d+1) { Array.new(@plot_steps_3d+1) { [] } }
         @old_plot_steps_3d = @plot_steps_3d
      end
      # 1. fill the array with X,Z data
      while(zi <= @plot_steps_3d && !@suspend_equation)
         mz = ntrp(0,@plot_steps_3d,-1.0,1.0,zi.to_f)
         z = ntrp(0,@plot_steps_3d,@za3d,@zb3d,zi.to_f)
         xi = 0
         while(xi <= @plot_steps_3d && !@suspend_equation)
            mx = ntrp(0,@plot_steps_3d,-1.0,1.0,xi.to_f)
            x = ntrp(0,@plot_steps_3d,@xa3d,@xb3d,xi.to_f)
            y = 0
            begin
               y = @gproc.call(x,y,z,@control_var_a,@control_var_b,@control_var_c)
            rescue Exception => err
               if !@suspend_equation
                  s = err.to_s.sub(/(.*)for.*/,"\\1")
                  error_dialog("Execution",s)
               end
               y = 0
               @suspend_equation = true
            end
            if(!@suspend_equation)
               my = ntrp(@ya3d,@yb3d,-0.5,0.5,y)
               v = Cart3.new(mx,my,mz) * @parent.config.drawing_scale
               @rotator.rotate(v)
               @rotator.convert_3d_to_2d(v,anaglyph_flag)
               @point_array[xi][zi] = [ @x_screen_center + (v.x * @screen_scale),
               @y_screen_center - (v.y * @screen_scale) ]
            end
            xi += 1
         end
         zi += 1
      end # fill precomputing array
      if(!@suspend_equation)
         # 2. draw the precomputed array contents twice,
         # two orthogonal sets of lines
         oxa = oya = 0
         oxb = oyb = 0
         0.upto(@plot_steps_3d) do |yi|
            0.upto(@plot_steps_3d) do |xi|
               xa,ya = @point_array[yi][xi]
               xb,yb = @point_array[xi][yi]
               if(xi > 0)
                  draw_line(gc,oxa,oya,xa,ya)
                  draw_line(gc,oxb,oyb,xb,yb)
               end
               oxa,oya = xa,ya
               oxb,oyb = xb,yb
            end
         end
      end # draw precomputed results
   end

   def choose_anaglyph_color(color,black,white)
      case @parent.config.anaglyph_mode
      when 1
         color = white
      when 2
         color = black
      end
      return color
   end

   def draw_image()
      set_sizes
      @x_screen_center = @old_xsize / 2
      @y_screen_center = @old_ysize / 2
      @screen_scale = (@x_screen_center > @y_screen_center)?@y_screen_center:@x_screen_center
      @chart_title.gsub!(/\?/,@parent.equation3DLineEdit.text)
      return unless @parent.graphic_pane.window
      # use double-buffering
      @pixmap = Gdk::Pixmap.new(@parent.graphic_pane.window,@old_xsize,@old_ysize,-1)
      gc = Gdk::GC.new(@pixmap)
      gc.set_line_attributes(@line_thickness,Gdk::GC::LineStyle::SOLID,Gdk::GC::CapStyle::ROUND,Gdk::GC::JoinStyle::MITER)
      red = Gdk::Color.parse("red")
      cyan = Gdk::Color.parse("cyan")
      black = Gdk::Color.parse("black")
      white = Gdk::Color.parse("white")
      @gc_text = Gdk::GC.new(@pixmap)
      @text_color = Graphinity::fixnum_to_col(@parent.config.text_color)
      #puts "anaglyph mode: #{@parent.config.anaglyph_mode}"
      gc.function = Gdk::GC::COPY
      case @parent.config.anaglyph_mode
      when 0 # no anaglyphic display
         fill_block(gc,@bg_color,0,0,@old_xsize,@old_ysize)
         color = Graphinity::fixnum_to_col(@parent.config.plot_color)
         draw_graph(gc,0,color)
         title_color = Graphinity::fixnum_to_col(@parent.config.text_color)
      when 1 # anaglyphic black
         fill_block(gc,black,0,0,@old_xsize,@old_ysize)
         gc.function = Gdk::GC::OR
         draw_graph(gc,1,red)
         draw_graph(gc,-1,cyan)
         title_color = white
      when 2 # anaglyphic white
         fill_block(gc,white,0,0,@old_xsize,@old_ysize)
         gc.function = Gdk::GC::AND
         draw_graph(gc,1,cyan)
         draw_graph(gc,-1,red)
         title_color = black
      end
      # the default operator
      gc.function = Gdk::GC::COPY
      if(@draw_border)
         color = Graphinity::fixnum_to_col(@parent.config.border_color)
         color = choose_anaglyph_color(color,black,white)
         case @parent.config.anaglyph_mode
         when 1
            color = white
         when 2
            color = black
         end
         gc.rgb_fg_color = color
         @pixmap.draw_rectangle(gc,false,0,0,@old_xsize-1,@old_ysize-1)
      end
      if(@chart_title.size > 0)
         color = Graphinity::fixnum_to_col(@parent.config.text_color)
         color = choose_anaglyph_color(color,black,white)
         gc.rgb_fg_color = color
         x = (@old_xsize - @chart_title.size * @title_char_width) / 2.0
         y = @internal_margin
         draw_text(@chart_title,@gc_text,x,y,@title_font,color)
      end
      # copy to display
      @parent.graphic_pane.window.draw_drawable(gc,@pixmap,0,0,0,0,@old_xsize,@old_ysize)
   end
end # class GraphEngine3d

class GraphEngine2d < GraphEngine

   def initialize(app)
      super(app)
   end

   def format_index_num(n)
      return sprintf("%g",n)
   end

   def draw_grid(gc)
      gridCol = Graphinity::fixnum_to_col(@parent.config.grid_color)
      if(@draw_border)
         borderColor = Graphinity::fixnum_to_col(@parent.config.border_color)
         gc.rgb_fg_color = borderColor;
         @pixmap.draw_rectangle(gc,false,0,0,@old_xsize-1,@old_ysize-1)
      end
      gc.rgb_fg_color = gridCol
      y = 0
      while (y <= @y_grid_steps)
         gy = ntrp(0,@y_grid_steps.to_f,@draw_ya,@draw_yb,y)
         draw_line(gc,@draw_xa,gy,@draw_xb,gy)

         if(@show_y_nums)
            sv = ntrp(0,@y_grid_steps.to_f,@ya2d,@yb2d,y)
            ss = format_index_num(sv)
            right_just = ss.size * @normal_char_width + @internal_margin
            draw_text(ss,@gc_text,@draw_xa - right_just,gy-@normal_char_height/2,@normal_font,@number_color)
         end
         y += 1
      end

      x = 0
      while (x <= @x_grid_steps)
         gx = ntrp(0,@x_grid_steps.to_f,@draw_xa,@draw_xb,x)
         draw_line(gc,gx,@draw_ya,gx,@draw_yb)
         if(@show_x_nums)
            sv = ntrp(0,@x_grid_steps.to_f,@xa2d,@xb2d,x)
            ss = format_index_num(sv)
            center_bias = ss.size * @normal_char_width / 2
            draw_text(ss,@gc_text,gx-center_bias,@draw_ya+@normal_char_height/2,@normal_font,@number_color)
         end
         x += 1
      end

      if(@show_x_nums && @x_axis_label.size > 0)
         draw_text(@x_axis_label,@gc_text,@draw_xb+@internal_margin,@draw_ya+@normal_char_height/2,@normal_font)
      end
      if(@show_y_nums && @y_axis_label.size > 0)
         draw_text(@y_axis_label,@gc_text,@draw_xa-@internal_margin-@y_axis_label.size * @normal_char_width,@draw_yb-@internal_margin-@title_char_height,@normal_font)
      end
      if(@chart_title.size > 0)
         x = draw_x = (((@draw_xb - @draw_xa) - @chart_title.size * @title_char_width) / 2.0) + @draw_xa
         y = @draw_yb-@internal_margin-@title_char_height
         draw_text(@chart_title,@gc_text,x,y,@title_font)
      end

   end

   def draw_graph(gc)
      first = true
      ogx=ogy=0
      plotCol = Graphinity::fixnum_to_col(@parent.config.plot_color)
      gc.rgb_fg_color= plotCol
      steps = @plot_steps_2d
      steps = (steps < 8.0)?8.0:(steps)
      if(steps > 0 && defined? @gproc)
         xi = 0
         while(xi <= steps && !@suspend_equation)
            x = ntrp(0,steps,@xa2d,@xb2d,xi)
            y = 0
            begin
               y = @gproc.call(x,y,@control_var_a,@control_var_b,@control_var_c)
            rescue Exception => err
               if !@suspend_equation
                  s = err.to_s.sub(/(.*)for.*/,"\\1")
                  error_dialog("Execution",s)
               end
               y = 0
               @suspend_equation = true
            end
            begin
               gx = ntrp(@xa2d,@xb2d,@draw_xa,@draw_xb,x)
               gy = ntrp(@ya2d,@yb2d,@draw_ya,@draw_yb,y)
               unless first
                  if((inside(ogx,@draw_xa,@draw_xb) \
                  && inside(gx,@draw_xa,@draw_xb)) \
                  || (inside(ogy,@draw_yb,@draw_ya) \
                  && inside(gy,@draw_yb,@draw_ya)))
                     draw_line(gc,ogx,ogy,gx,gy)
                  end
               end
            rescue
               # errors converting huge floats to integers
            end
            ogx = gx;ogy = gy;
            first = false
            xi += 1
         end
         mx = @parent.mouse_x_pos
         my = @parent.mouse_y_pos
         if(mx) # if mouse is in play
            if (mx >= 0 && mx < @old_xsize &&
               my >= 0 && my < @old_ysize)
               x = ntrp(@draw_xa,@draw_xb,@xa2d,@xb2d,mx.to_f)
               posx = ntrp(@draw_xa,@draw_xb,0.0,1.0,mx.to_f)
               y = 0
               begin
                  y = @gproc.call(x,y,@control_var_a,@control_var_b,@control_var_c)
               rescue
                  y = 0
               end
               gy = ntrp(@ya2d,@yb2d,@draw_ya,@draw_yb,y)
               posy = ntrp(@ya2d,@yb2d,0,1,y)
               lineCol = Graphinity::fixnum_to_col(@parent.config.text_color)
               gc.rgb_fg_color= lineCol
               draw_line(gc,mx,@draw_ya,mx,@draw_yb)
               draw_line(gc,@draw_xa,gy,@draw_xb,gy)
               s = "Data: x = #{sprintf("%g",x)}, y = #{sprintf("%g",y)}"
               offset = @normal_char_height/2
               w = s.size * @normal_char_width
               h = @normal_char_height
               # position text in a suitable quadrant
               dispx = (posx < 0.5)?mx+offset:mx-w-offset
               dispy = (posy < 0.5)?gy-offset-h:gy-offset+h
               draw_text(s,@gc_text,dispx,dispy,@normal_font)
            end
         end
      end
   end

   def draw_image()
      set_sizes
      @pango_context = Gdk::Pango.context
      return unless @parent.graphic_pane.window
      # use double-buffering
      @pixmap = Gdk::Pixmap.new(@parent.graphic_pane.window,@old_xsize,@old_ysize,-1)
      gc = Gdk::GC.new(@pixmap)
      gc.set_line_attributes(@line_thickness,Gdk::GC::LineStyle::SOLID,Gdk::GC::CapStyle::ROUND,Gdk::GC::JoinStyle::MITER)
      @gc_text = Gdk::GC.new(@pixmap)
      @text_color = Graphinity::fixnum_to_col(@parent.config.text_color)
      @number_color = Graphinity::fixnum_to_col(@parent.config.number_color)
      @gc_text.rgb_fg_color = Graphinity::fixnum_to_col(@parent.config.text_color)
      # figure out margins
      @chart_title.gsub!(/\?/,@parent.equation2DLineEdit.text)
      @x_axis_label = @parent.xLabelLineEdit.text.strip
      @y_axis_label = @parent.yLabelLineEdit.text.strip
      @show_x_nums = @parent.xIndexCheckBox.active?
      @show_y_nums = @parent.yIndexCheckBox.active?
      # compute main title margin
      if(@chart_title.size > 0)
         @main_title_margin = @internal_margin + @title_char_height
      else
         @main_title_margin = 0
      end
      # compute Y axis title margin
      if(@y_axis_label.size > 0)
         @y_axis_label_margin = @internal_margin + @normal_char_height
         @title_left_margin = @normal_char_width * @y_axis_label.size/2
      else
         @y_axis_label_margin = 0
         @title_left_margin = 0
      end
      @top_margin = max(@main_title_margin,@y_axis_label_margin)

      # compute X axis title margin
      if(@x_axis_label.size > 0)
         @x_axis_label_margin = @internal_margin + @normal_char_width * @x_axis_label.size
      else
         @x_axis_label_margin = 0
      end

      # Noooo! Pleaseeee!
      if(@x_grid_steps == 0 || @y_grid_steps == 0)
         @x_grid_steps = 10
         @y_grid_steps = 10
         @parent.xGridStepsLineEdit.text = @x_grid_steps.to_s
         @parent.yGridStepsLineEdit.text = @y_grid_steps.to_s
      end

      # compute left margin
      s1 = format_index_num(@xa2d).size
      if(@show_y_nums)
         # must avoid cases of unexpectedly wide numbers
         sa = []
         0.upto(@y_grid_steps) do |y|
            gy = ntrp(0,@y_grid_steps,@ya2d,@yb2d,y.to_f)
            s = format_index_num(gy)
            sa << s.size
         end
         sc = sa.max
         @num_left_margin = @internal_margin + sc * @normal_char_width
      elsif(@show_x_nums)
         @num_left_margin = @internal_margin + s1 * @normal_char_width
      else
         @num_left_margin = 0
      end
      # compute bottom_margin
      if(@show_x_nums)
         @bottom_margin = @normal_char_height + @internal_margin
      elsif(@show_y_nums)
         @bottom_margin = @normal_char_height/2 + @internal_margin
      else
         @bottom_margin = 0
      end

      @left_margin = max(@title_left_margin,@num_left_margin)

      # compute number right margin
      if(@show_x_nums)
         @num_right_margin = @internal_margin + format_index_num(@xb2d).size * @normal_char_width/2
      else
         @num_right_margin = 0
      end

      # compute text right margin
      if(@x_axis_label.size > 0)
         @title_right_margin = @internal_margin + @normal_char_width * @x_axis_label.size
      else
         @title_right_margin = 0
      end
      @right_margin = max(@num_right_margin,@title_right_margin)

      # assign the values
      @draw_xa = @internal_margin + @left_margin
      @draw_xb = @old_xsize - @right_margin - @internal_margin
      @draw_ya = @old_ysize - @bottom_margin - @internal_margin
      @draw_yb = @internal_margin + @top_margin
      gc.function = Gdk::GC::COPY
      fill_block(gc,@bg_color,0,0,@old_xsize,@old_ysize)
      draw_grid(gc)
      draw_graph(gc)
      # copy to display
      @parent.graphic_pane.window.draw_drawable(gc,@pixmap,0,0,0,0,@old_xsize,@old_ysize)
   end
end # class GraphEngine2d

# Main program
if __FILE__ == $0
   # Set values as your own application.
   PROG_PATH = "graphinityui.glade"
   PROG_NAME = "Graphinity"
   Graphinity.new(PROG_PATH, nil, PROG_NAME)
   Gtk.main
end

Home | Ruby | Graphinity |     Share This Page