9 November 2005
Ruby 3D Point

I need to do some 3D structure analysis for a contract. While I already have a substantial amount of Java code that I could use, I’ve decided to build up some capabilities for the work in Ruby.

The first object I put together for this is Point3, a cartesian 3D point, and a basis for most of the work to follow. Instances of the class can be created from coordinates or other Point3s, and can be added, subtracted, multiplied, divided, negated, tested for equality and printed.

# This modifies to objects add 3D point duck classifying.

class Object

  # Returns true if the instance has 3D cartesian coordinates, ie. responds
  # to x, y and z.
  def point3_like?
    (respond_to? :x) && (respond_to? :y) && (respond_to? :z)
  end

  # Raises an TypeError with a human-readable message when the instance src
  # cannot be converted to an instance of type dest.
  def raise_no_conversion(src,dest=self)
    dest = dest.class.name unless dest.instance_of? String
    raise TypeError, "Cannot convert '#{src.class.name}' to #{dest}"
  end

end
# Point3 represents a 3D point in cartesian coordinates

class Point3

  # the signed distance from the 3D origin along the x axis.
  attr_accessor :x
  # the signed distance from the 3D origin along the y axis.
  attr_accessor :y
  # the signed distance from the 3D origin along the z axis.
  attr_accessor :z

  # Returns the origin of 3D space, where x, y, and z all have zero values.
  def Point3.origin
    @@origin3
  end

  # Creates and returns a Point3 instance.
  def initialize(x=0, y=0, z=0)
    set x, y, z
  end

  # Returns a string representation of the instance.
  def to_s
    "Point3: x #{x}  y #{y}  z #{z}"
  end

  # Sets the coordinate values of the instance. When
  # * x is Numeric, the arguments are interpreted as coordinates.
  # * x responds like a Point3, its cartesian coordinates are assigned.
  # * otherwise a TypeError is raised.
  # The modified instance is returned.
  def set(x=0, y=0, z=0)
    if x.kind_of? Numeric
      @x, @y, @z = 1.0*x, 1.0*y, 1.0*z
    elsif x.point3_like?
      set x.x, x.y, x.z
    else
      raise_no_conversion x
    end
    self
  end

  # Returns true if the coordinates of the instance are equal to the
  # coordinates of the given point.
  def ==(point3)
    (@x == point3.x) && (@y == point3.y) && (@z == point3.z)
  end

  # Returns a copy of point3 with the given cartesian coordinates:
  # * x is Numeric, the arguments are copied as the coordinates.
  # * x responds like a Point3, its coordinates are copied.
  # * otherwise a TypeError is raised.
  def point3(x=self.x, y=self.y, z=self.z)
    Point3.new(x,y,z)
  end

  # Returns the 3D distance from the instance to point3.
  def distance_to(point3)
    Math.sqrt(((@x-point3.x)**2)+((@y-point3.y)**2)+((@z-point3.z)**2))
  end

  # Returns the 3D distance from the instance to the origin.
  def modulus
    distance_to(origin)
  end

  # Returns the dot product of the vectors represented by the instance and
  # point3, with common endpoints at the origin.
  def dot(point3)
    (@x*point3.x)+(@y*point3.y)+(@z*point3.z)
  end

  # Returns a new Point3 instance whose coordinates are the original
  # instance's mirrored through the origin.
  def mirror
    point3.mirror!
  end

  # Returns the modified instance whose coordinates have been mirrored through
  # the origin.
  def mirror!
    set(-@x, -@y, -@z)
  end

  # Returns a new Point3 instance whose coordinates are the original
  # instance's with the given amounts added:
  # * x is Numeric, the arguments are added to the coordinates.
  # * x responds like a Point3, its cartesian coordinates are added.
  # * otherwise a TypeError is raised.
  def add(x=0,y=0,z=0)
    point3.add!(x,y,z)
  end

  alias + add

  # Returns the modified instance with the arguments added.
  def add!(x=0,y=0,z=0)
    if x.kind_of? Numeric
      set(@x+x, @y+y, @z+z)
    elsif x.point3_like?
      add! x.x, x.y, x.z
    else
      raise_no_conversion x
    end
  end

  # Returns a new Point3 instance whose coordinates are the original
  # instance's with the given amounts subtracted:
  # * x is Numeric, the arguments are subtracted from the coordinates.
  # * x responds like a Point3, its cartesian coordinates are subtracted.
  # * otherwise a TypeError is raised.
  def subtract(x=0,y=0,z=0)
    point3.subtract!(x,y,z)
  end

  alias - subtract

  # Returns the modified instance with the arguments subtracted.
  def subtract!(x=0,y=0,z=0)
    if x.kind_of? Numeric
      add!(-x, -y, -z)
    elsif x.point3_like?
      subtract! x.x, x.y, x.z
    else
      raise_no_conversion x
    end
  end

  # Returns a new Point3 instance whose coordinates are the original
  # instance's multiplied by the given amounts:
  # * x is Numeric, the coordinates are multiplied by the arguments.
  # * x responds like a Point3, the instance's coordinates are multiplied by x's coordinates.
  # * otherwise a TypeError is raised.
  def multiply(x=1, y=1, z=1)
    point3.multiply!(x,y,z)
  end

  alias * multiply

  # Returns the modified instance as multiplied by the arguments.
  def multiply!(x=1, y=1, z=1)
    if x.kind_of? Numeric
      set(@x*x, @y*y, @z*z)
    elsif x.point3_like?
      multiply! x.x, x.y, x.z
    else
      raise_no_conversion x
    end
  end

  # Returns a new Point3 instance whose coordinates are the original
  # instance's multiplied by the scalar.
  # * scalar is Numeric, the arguments are multiplied by the coordinates.
  # * x responds like a Point3, the instance is multiplied by the scalar.
  # * otherwise a TypeError is raised.
  def scale(scalar=1)
    point3.scale!(scalar)
  end

  # Returns the modified instance as multiplied by the scalar.
  def scale!(scalar=1)
    if scalar.kind_of? Numeric
      multiply! scalar, scalar, scalar
    elsif scalar.point3_like?
      multiply! scalar
    else
      raise_no_conversion scalar
    end
  end

  # Returns a new Point3 instance representing the unit vector (with the same
  # direction as the original instance, but whose length is 1.)
  def unit(x=1, y=1, z=1)
    point3.unit!
  end

  # Returns the modified instance as the unit vector.
  def unit!(x=1, y=1, z=1)
    scale!(1/modulus)
  end

  # Returns a new Point3 instance that is the cross product of the given
  # arguments treated as vectors with endpoints at the origin:
  # * x is Numeric, the cross product of the instance with the arguments.
  # * x responds like a Point3,
  #   * y is Numeric, the cross product of the instance with x's coordinates.
  #   * y responds like a Point3, the cross product of x with y.
  # * otherwise a TypeError is raised.
  def cross(x=1/sqrt3, y=1/sqrt3, z=1/sqrt3)
    point3.cross!(x,y,z)
  end

  # Returns the modified instance as the cross product.
  def cross!(x=1/@@sqrt3, y=1/@@sqrt3, z=1/@@sqrt3)
    if x.kind_of? Numeric
      set((@y*z)-(@z*y), (@z*x)-(@x*z), (@x*y)-(@y*x))
    elsif x.point3_like?
      if y.kind_of? Numeric
        cross! x.x, x.y, x.z
      elsif y.point3_like?
        set(x).cross!(y)
      else
        raise_no_conversion y
      end
    else
      raise_no_conversion x
    end
  end

  # Returns a new Point3 instance that is a distance d from the instance along
  # the line to the Point3 e. If normalized is true, the d argument specifies
  # the fraction of the distance from the instance (being 0) to e (being 1).
  # If normalize is false, the d argument specifies an absolute distance.
  def to_along (e, d, normalize=true)
    scalar = normalize ? (distance_to e) : 1
    point3(e).subtract!(self).unit!.scale!(d*scalar).add(self)
  end

  # The 3D origin
  @@origin3 = Point3.new

  @@sqrt3 = Math.sqrt 3

end

More to come…