class MeshNormalAnalyzer

Public Class Methods

new(mesh) click to toggle source
# File lib/ruby3mf/mesh_normal_analyzer.rb, line 3
def initialize(mesh)
  @vertices = []
  @intersections = []

  vertices_node = mesh.css("vertices")
  vertices_node.children.each do |vertex_node|
    if vertex_node.attributes.count > 0
      x = vertex_node.attributes['x'].to_s.to_f
      y = vertex_node.attributes['y'].to_s.to_f
      z = vertex_node.attributes['z'].to_s.to_f
      @vertices << [x, y, z]
    end
  end

  @triangles = []
  triangles_node = mesh.css("triangles")
  triangles_node.children.each do |triangle_node|
    if triangle_node.attributes.count > 0
      v1 = triangle_node.attributes['v1'].to_s.to_i
      v2 = triangle_node.attributes['v2'].to_s.to_i
      v3 = triangle_node.attributes['v3'].to_s.to_i
      @triangles << [v1, v2, v3]
    end
  end
end

Public Instance Methods

compare_normals(triangle, hit_direction) click to toggle source
# File lib/ruby3mf/mesh_normal_analyzer.rb, line 98
def compare_normals(triangle, hit_direction)
  oriented_normal = cross_product(
      vector_to(triangle[0], triangle[1]),
      vector_to(triangle[1], triangle[2]))

  angle = angle_between(oriented_normal, hit_direction)

  angle < Math::PI / 2.0
end
found_inward_triangle() click to toggle source
# File lib/ruby3mf/mesh_normal_analyzer.rb, line 29
def found_inward_triangle
  # Trace a ray toward the center of the vertex points.  This will hopefully
  # maximize our chances of hitting the object's trianges on the first try.
  center = point_cloud_center(@vertices)

  @point = [0.0, 0.0, 0.0]
  @direction = vector_to(@point, center)

  # Make sure that we have a reasonably sized direction.
  # Might end up with a zero length vector if the center is also
  # at the origin.
  if magnitude(@direction) < 0.1
    @direction = [0.57, 0.57, 0.57]
  end

  # make the direction a unit vector just to make the
  # debug info easier to understand
  @direction = normalize(@direction)

  attempts = 0
  begin
    # Get all of the intersections from the ray and put them in order of distance.
    # The triangle we hit that's farthest from the start of the ray should always be
    # a triangle that points away from us (otherwise we would hit a triangle even
    # further away, assuming the mesh is closed).
    #
    # One special case is when the set of triangles we hit at that distance is greater
    # than one.  In that case we might have hit a "corner" of the model and so we don't
    # know which of the two (or more) points away from us.  In that case, cast a random
    # ray from the center of the object and try again.

    @triangles.each do |tri|
      v1 = @vertices[tri[0]]
      v2 = @vertices[tri[1]]
      v3 = @vertices[tri[2]]

      process_triangle(@point, @direction, [v1, v2, v3])
    end

    if @intersections.count > 0
      # Sort the intersections so we can find the hits that are furthest away.
      @intersections.sort! {|left, right| left[0] <=> right[0]}

      max_distance = @intersections.last[0]
      furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}

      # Print out the hits
      # furthest_hits.each {|hit| puts hit[1].to_s}

      found_good_hit = furthest_hits.count == 1
    end

    if found_good_hit
      outside_triangle = furthest_hits.last[2]
    else
      @intersections = []
      attempts = attempts + 1

      target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
      @point = center
      @direction = normalize(vector_to(@point, target))
    end
  end until found_good_hit || attempts >= 10

  # return true if we hit a triangle with an inward pointing normal
  # (according to counter-clockwise normal orientation)
  found_good_hit && !compare_normals(outside_triangle, @direction)
end
intersect(point, direction, triangle) click to toggle source
# File lib/ruby3mf/mesh_normal_analyzer.rb, line 121
def intersect(point, direction, triangle)
  v0 = triangle[0]
  v1 = triangle[1]
  v2 = triangle[2]

  return [false, 0] if v0.nil? || v1.nil? || v2.nil?

  e1 = vector_to(v0, v1)
  e2 = vector_to(v0, v2)

  h = cross_product(direction, e2)
  a = dot_product(e1, h)

  if a.abs < 0.00001
    return false, 0
  end

  f = 1.0/a
  s = vector_to(v0, point)
  u = f * dot_product(s, h)

  if u < 0.0 || u > 1.0
    return false, 0
  end

  q = cross_product(s, e1)
  v = f * dot_product(direction, q)

  if v < 0.0 || u + v > 1.0
    return false, 0
  end

  t = f * dot_product(e2, q)
  [t > 0, t]
end
process_triangle(point, direction, triangle) click to toggle source
# File lib/ruby3mf/mesh_normal_analyzer.rb, line 108
def process_triangle(point, direction, triangle)
  found_intersection, t = intersect(point, direction, triangle)

  if t > 0
    intersection = []
    intersection[0] = point[0] + t * direction[0]
    intersection[1] = point[1] + t * direction[1]
    intersection[2] = point[2] + t * direction[2]

    @intersections << [t, intersection, triangle]
  end
end