Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Friday, 25 April 2014

Experiments with JRuby Integration (for ruby-processing)

Here is the Vec2 class (I wanted to have Vec2D but that doesn't play too well with JRuby, well at least not for me), very loosely based on yokolets Fraction example.
package processing.vec2;

import org.apache.commons.math3.util.FastMath;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFloat;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

/**
 *
 * @author Martin Prout
 */
@JRubyClass(name = "Processing::Vec2")
public class Vec2 extends RubyObject {

    static final double EPSILON = 1.0e-04; // matches processing.org EPSILON
    double jx = 0;
    double jy = 0;

    /**
     *
     * @param context
     * @param klazz
     * @param args optional (no args jx = 0, jy = 0)
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "new", meta = true, rest = true)
    public static IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
        Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
        vec2.init(context, args);
        return vec2;
    }

    /**
     *
     * @param runtime
     * @param klass
     */
    public Vec2(Ruby runtime, RubyClass klass) {
        super(runtime, klass);
    }

    void init(ThreadContext context, IRubyObject[] args) {
        if (Arity.checkArgumentCount(context.getRuntime(), args, Arity.OPTIONAL.getValue(), 2) == 2) {
            jx = (Double) args[0].toJava(Double.class);
            jy = (Double) args[1].toJava(Double.class);
        }
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "x")
    public RubyFloat getX(ThreadContext context) {
        return context.getRuntime().newFloat(jx);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "y")
    public RubyFloat getY(ThreadContext context) {
        return context.getRuntime().newFloat(jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "set_x")
    public RubyFloat setX(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        jx = scalar;
        return context.getRuntime().newFloat(jx);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "set_y")
    public RubyFloat setY(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        jy = scalar;
        return context.getRuntime().newFloat(jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dist")
    public IRubyObject dist(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        double result = FastMath.sqrt((jx - b.jx) * (jx - b.jx) + (jy - b.jy) * (jy - b.jy));
        return context.getRuntime().newFloat(result);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dist_squared")
    public IRubyObject dist_squared(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        double result = (jx - b.jx) * (jx - b.jx) + (jy - b.jy) * (jy - b.jy);
        return context.getRuntime().newFloat(result);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "cross")
    public IRubyObject cross(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        return context.getRuntime().newFloat(jx * b.jy - jy * b.jx);
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "dot")
    public IRubyObject dot(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        return context.getRuntime().newFloat(jx * b.jx + jy * b.jy);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "add")
    public IRubyObject add(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx + b.jx),
            context.getRuntime().newFloat(jy + b.jy)};
        return Vec2.rbNew(context, other.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "sub")
    public IRubyObject sub(ThreadContext context, IRubyObject other) {
        Vec2 b = (Vec2) other.toJava(Vec2.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx - b.jx),
            context.getRuntime().newFloat(jy - b.jy)};
        return Vec2.rbNew(context, other.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "mult")
    public IRubyObject mult(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        RubyFloat[] input = {context.getRuntime().newFloat(jx * scalar),
            context.getRuntime().newFloat(jy * scalar)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param other
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "div")
    public IRubyObject div(ThreadContext context, IRubyObject other) {
        double scalar = (Double) other.toJava(Double.class);
        if (FastMath.abs(scalar) < Vec2.EPSILON) {
            return this;
        }
        RubyFloat[] input = {context.getRuntime().newFloat(jx / scalar),
            context.getRuntime().newFloat(jy / scalar)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "heading")
    public IRubyObject heading(ThreadContext context) {
        return context.getRuntime().newFloat(FastMath.atan2(-jy, jx) * -1.0);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "mag_squared")
    public IRubyObject mag_squared(ThreadContext context) {
        return context.getRuntime().newFloat(jx * jx + jy * jy);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "mag")
    public IRubyObject mag(ThreadContext context) {
        return context.getRuntime().newFloat(FastMath.hypot(jx, jy));
    }

    /**
     *
     * @param context
     * @param scalar
     * @return
     * @todo have optional conditional block (evaluate to boolean)
     */
    @JRubyMethod(name = "set_mag")
    public IRubyObject set_mag(ThreadContext context, IRubyObject scalar) {
        double new_mag = (Double) scalar.toJava(Double.class);
        double current = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        jx *= new_mag / current;
        jy *= new_mag / current;
        return this;
    }

    /**
     *
     * @param context
     * @return this as a ruby object
     */
    @JRubyMethod(name = "normalize!")
    public IRubyObject normalize_bang(ThreadContext context) {
        double mag = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        jx /= mag;
        jy /= mag;
        return this;
    }

    /**
     *
     * @param context
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "normalize")
    public IRubyObject normalize(ThreadContext context) {
        double mag = FastMath.sqrt(FastMath.pow(jx, 2) + FastMath.pow(jy, 2));
        RubyFloat[] input = {context.getRuntime().newFloat(jx / mag),
            context.getRuntime().newFloat(jy / mag)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     * Example of a regular ruby class method
     *
     * @param context
     * @param klazz the klazz of object we are creating
     * @param other input angle in radians
     * @return new Vec2 object (ruby)
     */
    @JRubyMethod(name = "from_angle", meta = true)
    public static IRubyObject from_angle(ThreadContext context, IRubyObject klazz, IRubyObject other) {
        final double scalar = (Double) other.toJava(Double.class);
        RubyFloat[] args = {context.getRuntime().newFloat(FastMath.cos(scalar)),
            context.getRuntime().newFloat(FastMath.sin(scalar))};
        Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
        vec2.init(context, args);
        return vec2;
    }

    /**
     *
     * @param context
     * @param other
     * @return this Vec2 object rotated
     */
    @JRubyMethod(name = "rotate!")
    public IRubyObject rotate_bang(ThreadContext context, IRubyObject other) {
        double theta = (Double) other.toJava(Double.class);
        double x = (jx * FastMath.cos(theta) - jy * FastMath.sin(theta));
        double y = (jx * FastMath.sin(theta) + jy * FastMath.cos(theta));
        jx = x;
        jy = y;
        return this;
    }

    /**
     *
     * @param context
     * @param other
     * @return a new Vec2 object rotated
     */
    @JRubyMethod(name = "rotate")
    public IRubyObject rotate(ThreadContext context, IRubyObject other) {
        double theta = (Double) other.toJava(Double.class);
        double x = (jx * FastMath.cos(theta) - jy * FastMath.sin(theta));
        double y = (jx * FastMath.sin(theta) + jy * FastMath.cos(theta));
        RubyFloat[] input = {context.getRuntime().newFloat(x),
            context.getRuntime().newFloat(y)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param args
     * @return as a new Vec2 object (ruby)
     */
    @JRubyMethod(name = "lerp", rest = true)
    public IRubyObject lerp(ThreadContext context, IRubyObject[] args) {
        Arity.checkArgumentCount(context.getRuntime(), args, 2, 2);
        Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
        double scalar = (Double) args[1].toJava(Double.class);
        assert (scalar >= 0 && scalar < 1.0) :
                "Lerp value " + scalar + " out of range 0 .. 1.0";
        double x0 = jx + (vec.jx - jx) * scalar;
        double y0 = jx + (vec.jx - jx) * scalar;
        RubyFloat[] input = {context.getRuntime().newFloat(x0),
            context.getRuntime().newFloat(y0)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @param args
     * @return this
     */
    @JRubyMethod(name = "lerp!", rest = true)
    public IRubyObject lerp_bang(ThreadContext context, IRubyObject[] args) {
        Arity.checkArgumentCount(context.getRuntime(), args, 2, 2);
        Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
        double scalar = (Double) args[1].toJava(Double.class);
        assert (scalar >= 0 && scalar < 1.0) :
                "Lerp value " + scalar + " out of range 0 .. 1.0";
        double x0 = jx + (vec.jx - jx) * scalar;
        double y0 = jx + (vec.jx - jx) * scalar;
        jx = x0;
        jy = y0;
        return this;
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "copy")
    public IRubyObject copy(ThreadContext context) {
        double x0 = jx;
        double y0 = jy;
        RubyFloat[] input = {context.getRuntime().newFloat(x0),
            context.getRuntime().newFloat(y0)};
        return Vec2.rbNew(context, this.getMetaClass(), input);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "to_a")
    public IRubyObject toArray(ThreadContext context) {
        RubyFloat[] output = {context.getRuntime().newFloat(jx), context.getRuntime().newFloat(jy)};
        return context.getRuntime().newArray(output);
    }

    /**
     *
     * @param context
     * @return
     */
    @JRubyMethod(name = "to_s")
    public IRubyObject to_s(ThreadContext context) {
        return context.getRuntime().newString(String.format("Vec2(x = %4.4f, y = %4.4f)", jx, jy));
    }

    /**
     *
     * @return
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 53 * hash + (int) (Double.doubleToLongBits(this.jx) ^ (Double.doubleToLongBits(this.jx) >>> 32));
        hash = 53 * hash + (int) (Double.doubleToLongBits(this.jy) ^ (Double.doubleToLongBits(this.jy) >>> 32));
        return hash;
    }

    /**
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Vec2 other = (Vec2) obj;
        if (Double.doubleToLongBits(this.jx) != Double.doubleToLongBits(other.jx)) {
            return false;
        }
        return (Double.doubleToLongBits(this.jy) == Double.doubleToLongBits(other.jy));
    }

    /**
     *
     * @param context
     * @param other
     * @return
     */
    @JRubyMethod(name = "almost_eql?")
    public IRubyObject almost_eql_p(ThreadContext context, IRubyObject other) {
        Vec2 v = (other instanceof Vec2) ? (Vec2) other.toJava(Vec2.class) : null;
        IRubyObject result = (v == null) ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : (FastMath.abs(jx - v.jx) > Vec2.EPSILON)
                ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : (FastMath.abs(jy - v.jy) > Vec2.EPSILON)
                ? RubyBoolean.newBoolean(context.getRuntime(), false)
                : RubyBoolean.newBoolean(context.getRuntime(), true);
        return result; // return false as default unless not null && values equal
    }

}

Here is the Vec2Service class
package processing.vec2;
import java.io.IOException;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.BasicLibraryService;


public class Vec2Service implements BasicLibraryService {
    @Override
    public boolean basicLoad(final Ruby runtime) throws IOException {
        RubyModule processing = runtime.defineModule("Processing");
        RubyModule vec2Module = processing.defineModuleUnder("Vec2");
        RubyClass vec2 = vec2Module.defineClassUnder(Vec2.class.getSimpleName(), runtime.getObject(), VEC_ALLOCATOR);
        vec2.defineAnnotatedMethods(Vec2.class);
        return true;
    }

    private static final ObjectAllocator VEC_ALLOCATOR = new ObjectAllocator() {
        @Override
        public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
            return new Vec2(runtime, klazz);
        }
    };
}

Here is the Rakefile jruby -S rake compile
# -*- ruby -*-
require 'java'
require 'rake/javaextensiontask'

Rake::JavaExtensionTask.new('processing/vec2') do |ext|
  jars = FileList['lib/*.jar']
  ext.classpath = jars.map {|x| File.expand_path x}.join ':'
  ext.name = 'processing/vecmath'
  ext.debug=true
  ext.source_version='1.7'
  ext.target_version='1.7'
end

Here is vec2.rb
require_relative './vecmath'
require 'processing/vec2/vec2'

module Processing
  module Vec2
    class Vec2
      alias_method :*, :mult
      alias_method :/, :div
      alias_method :+, :add
      alias_method :-, :sub
      alias_method :==, :eql?
    end
  end
end

Here are some sample usages:-
require 'java'
require_relative '../lib/processing_vecmath'
include Processing::Vec2
vec = Vec2.new(0, 0)
vec1 = Vec2.new(0, 0)
vec2 = Vec2.new(3.0, 4.0)
vec3 =Vec2.new(1.00000000000, 1.000000000000)
puts vec3.normalize
puts "mult #{vec3}"
puts "add #{vec2 + vec3}"
puts "original #{vec2}"
puts vec == vec1
puts vec2.almost_eql? Vec2.new(3.0, 4.0)
puts vec.almost_eql? Vec2.new(0, 0)
puts vec.dist(vec2)
puts vec2.mag

Here is the output:-
Vec2(x = 0.7071, y = 0.7071)
mult Vec2(x = 1.0000, y = 1.0000)
add Vec2(x = 4.0000, y = 5.0000)
original Vec2(x = 3.0000, y = 4.0000)
true
true
true
5.0
5.0
Interesting working in Netbeans the rake build script was unecessary, but size of compiled jar was much smaller with the script (however I had linked jruby-9000 in Netbeans). See repository on github for Vec3 and spec tests.

No comments:

Post a Comment

Followers

Blog Archive

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2