Weird raytest

I tried to find similar topics on the forum but I couldn’t find any.
Why is this? Am I doing something wrong? Is this really a reytest problem? Is there a way around it?
In the tool I am trying to find distances to neighboring faces via raytest. It works fine, but if there are tangent faces, reytest ignores them and shows the distance to the next one. What is the problem?

      def calculate_face_distance(face, transformation)
        return nil unless face&.valid?
        centroid = face.bounds.center.transform(transformation)
        normal = face.normal.transform(transformation)
        ray = [centroid, normal]
        hit = @model.raytest(ray, false)
        return nil unless hit
        hit_point = hit[0]
        distance = centroid.distance(hit_point)
        [distance, @ray_length].min
      end

      def find_adjacent_faces
        @adjacent_faces = []
        return unless @selected_face&.valid? && @hovered_entity&.valid?

        entities = if @hovered_entity.is_a?(Sketchup::Group)
          @hovered_entity.entities
        elsif @hovered_entity.is_a?(Sketchup::ComponentInstance)
          @hovered_entity.definition.entities
        else
          @model.entities
        end

        edges = @selected_face.edges
        @adjacent_faces = []
        edges.each do |edge|
          edge.faces.each do |face|
            next if face == @selected_face
            next unless face_belongs_to_entity?(face, [@hovered_entity, face], @hovered_entity)
            @adjacent_faces << { face: face, transformation: @selected_face_transformation }
          end
        end
      end

A few general issues:

  • There is no comments at all in the code snippets.

    • Each method should have a description stating it’s purpose
    • and even better if the arguments and return type are specified with YARD notation.
  • In find_adjacent_faces():

    • @adjacent_faces is being referenced to [] twice
    • entities is being referenced, but not used

So for example, in calculate_face_distance(), I have no idea why you are using a normal vector for the face that is perpendicular to all the edges, for the ray direction. (Lack of commentary does not tell me what the transformation argument is doing.)

Also it is not clear what geometric context the tool would be in when at the time the ray’s are fired.

If within the same edit context than the face (selected) perhaps #raytest does not hit edges in the same context? (Guessing.) Could be by design or could be a bug.

For the one edge that is 50cm(?) did the ray first hit the face’s edge and then you had to refire the ray from that point in the same ray direction to get the distance?

Dan, thanks for your interest. Here is a short test code. Check out the results.


model = Sketchup.active_model
entities = model.entities

# Clear the model for a clean test
entities.clear!

# --- Create Cube 1 (100x100x100 mm, base at Z = 0) ---
group1 = entities.add_group
group1_entities = group1.entities

cube1_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face1 = group1_entities.add_face(cube1_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face1.normal.z < 0
  face1.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face1.pushpull(100.mm)

# --- Create Cube 2 (100x100x100 mm, base at Z = 100 mm) ---
group2 = entities.add_group
group2_entities = group2.entities

cube2_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face2 = group2_entities.add_face(cube2_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face2.normal.z < 0
  face2.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face2.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group2.transform!(Geom::Transformation.translation([0, 0, 100.mm]))

# --- Raytest from each face of Cube 1 ---
faces = group1.entities.grep(Sketchup::Face)

faces.each_with_index do |face, index|
  next unless face.valid?

  centroid = face.bounds.center # Center point of the face
  normal = face.normal          # Normal vector of the face
  ray = [centroid, normal]      # Ray from the face center in the direction of the normal

  hit = model.raytest(ray, false) # Perform ray test, ignore hidden geometry

  if hit
    hit_point = hit[0]
    distance = centroid.distance(hit_point)
    puts "Face #{index + 1}: Distance to nearest object: #{(distance.to_mm * 10).round / 10.0} mm"
  else
    puts "Face #{index + 1}: No intersection found"
  end
end

I’ve occasionally gotten weird results from raytest. For example, recently when I shot a ray from the center of a face in the reverse normal direction it returned a hit on that same face. Completely killed its usefulness for what I was trying to do!

2 Likes

I thought I’d find a way to get around this) But it’s no use.
Hits to emit a ray, then return it, find the face it returns to and compare the distances between the centers of the faces. If the distance is very small, then these are tangent faces. This also does not work, unfortunately.

model = Sketchup.active_model
entities = model.entities

# Clear the model for a clean test
entities.clear!

# --- Create Cube 1 (100x100x100 mm, base at Z = 0) ---
group1 = entities.add_group
group1_entities = group1.entities

cube1_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face1 = group1_entities.add_face(cube1_points)

# Ensure the face normal is pointing upwards (Z > 0)
face1.reverse! if face1.normal.z < 0

# Push/pull the face to create a cube of height 100 mm
face1.pushpull(100.mm)

# --- Create Cube 2 (100x100x100 mm, base at Z = 100 mm) ---
group2 = entities.add_group
group2_entities = group2.entities

cube2_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face2 = group2_entities.add_face(cube2_points)

# Ensure the face normal is pointing upwards (Z > 0)
face2.reverse! if face2.normal.z < 0

# Push/pull the face to create a cube of height 100 mm
face2.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group2.transform!(Geom::Transformation.translation([0, 0, 100.mm]))

# --- Raytest from each face of Cube 1 ---
faces = group1.entities.grep(Sketchup::Face)

faces.each_with_index do |face, index|
  next unless face.valid?

  centroid = face.bounds.center
  normal = face.normal
  ray = [centroid, normal]

  hit = model.raytest(ray, false)

  puts "Face #{index + 1} of Cube 1 →"
  
  if hit
    hit_point = hit[0]
    distance = centroid.distance(hit_point)
    puts " ↳ Ray hit at: [#{hit_point.x.to_mm.round(6)}, #{hit_point.y.to_mm.round(6)}, #{hit_point.z.to_mm.round(6)}] мм"
    puts " ↳ Distance: #{distance.to_mm.round(6)} мм"

    puts " ↳ Centroid Z: #{centroid.z.to_mm.round(6)}"
    puts " ↳ Hit Point Z: #{hit_point.z.to_mm.round(6)}"
    puts " ↳ Delta Z: #{(hit_point.z - centroid.z).to_mm.round(6)} мм"
  else
    puts " ↳ Ray did not hit anything ❌"
  end
end

A slightly different idea is to shoot a ray back from whatever is hit with the first ray test and check if the hit is in the same container.

Note: I’ve added a third cube to your example.

model = Sketchup.active_model
sel = model.selection
entities = model.entities

# Clear the model for a clean test
entities.clear!

# --- Create Cube 1 (100x100x100 mm, base at Z = 0) ---
group1 = entities.add_group
group1_entities = group1.entities

cube1_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face1 = group1_entities.add_face(cube1_points)

# Ensure the face normal is pointing upwards (Z > 0)

if face1.normal.z < 0
  face1.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face1.pushpull(100.mm)

# --- Create Cube 2 (100x100x100 mm, base at Z = 100 mm) ---
group2 = entities.add_group
group2_entities = group2.entities

cube2_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face2 = group2_entities.add_face(cube2_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face2.normal.z < 0
  face2.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face2.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group2.transform!(Geom::Transformation.translation([0, 0, 100.mm]))


# --- Create Cube 3 (100x100x100 mm, base at x = 200 mm) ---
group3 = entities.add_group
group3_entities = group3.entities

cube3_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face3 = group3_entities.add_face(cube3_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face3.normal.z < 0
  face3.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face3.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group3.transform!(Geom::Transformation.translation([50.mm, 300.mm, 0]))



##########################################
# --- Raytest from each face of Cube 1 ---
##########################################

faces = group1.entities.grep(Sketchup::Face)

faces.each_with_index { |face, index|
  next unless face.valid?
  next if face.hidden? # ignore hidden faces

  # hide this face from the ray test
  face.hidden = true
  
  centroid = face.bounds.center # Center point of the face
  normal = face.normal          # Normal vector of the face

  ray = [centroid, normal]      # Ray from the new centroid in the direction of the normal

  hit = model.raytest(ray) # Perform ray test, ignore hidden geometry

  if hit
    # record the distance
    distance = centroid.distance(hit[0])

    # ray test in the reverse direction
    reverse_ray = [hit[0], normal.reverse]
    reverse_hit = model.raytest(reverse_ray) 
    container = reverse_hit[1][0]
    
    # if the reverse ray hits the same container
    # there is a coincident faces and or edge pair
    distance = 0 if container == hit[1][0]

    puts "Face #{index + 1}: Distance to nearest object: #{(distance.to_mm * 10).round / 10.0} mm"
   
  else
    puts "Face #{index + 1}: No intersection found"
  end

  # unhide this face from the ray test
  face.hidden = false
  
}
nil

Results -

Face 1: No intersection found
Face 2: Distance to nearest object: 0.0 mm
Face 3: No intersection found
Face 4: No intersection found
Face 5: No intersection found
Face 6: Distance to nearest object: 200.0 mm
nil
2 Likes

Thank you. Perfectly) Good idea.

1 Like

I did today, finally. I got the result you have been seeing … face2 (the top face) goes through and returns 100.mm.

I then did: group2.move!([0,0,10.mm]) and reran the group1 each faces iteration loop and this time face2 was 10.mm from the “hit point”.

I believe this should be a “quirk” (thinking that it was purposefully coded this way.) : What context are the points in for this method? If they are in world (model) coordinates, then tangent faces would return the firing point of the ray and we’d never be able to fire a ray in that direction.
But the workaround (when the hit point == firing point) would have been to hide the face at the leaf of the returned instance path and then fire the ray again to go through the other object. It seems like the raytest was coded to instead ignore zero distance “hits” and treat the touching face as if it were hidden.

At this point perhaps open an issue in the GitHub API tracker and ask for a new named argument to modify the behavior of the test.


Aside: (distance.to_mm * 10).round / 10.0

The #to_mm method returns Float which takes decimal places argument to it’s #round method.
So: distance.to_mm.round(1)

1 Like

Unfortunately, I don’t know how to professionally formulate a query so that programmers can understand it ). I’m just self-taught and doing this out of curiosity. But it would be useful to have such an option.

I cannot myself log into GitHub now since they went to 2FA. I need to get a FIDO key.

Sorry, I’m just trying to understand this. Why doesn’t this work in a shared parent group of 4?
Or @Dan Rathbun?

model = Sketchup.active_model
sel = model.selection
entities = model.entities

# Clear the model for a clean test
entities.clear!

# Create parent Group 4
group4 = entities.add_group
group4_entities = group4.entities

# --- Create Cube 1 (100x100x100 mm, base at Z = 0) ---
group1 = group4_entities.add_group
group1_entities = group1.entities

cube1_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face1 = group1_entities.add_face(cube1_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face1.normal.z < 0
  face1.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face1.pushpull(100.mm)

# --- Create Cube 2 (100x100x100 mm, base at Z = 100 mm) ---
group2 = group4_entities.add_group
group2_entities = group2.entities

cube2_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face2 = group2_entities.add_face(cube2_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face2.normal.z < 0
  face2.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face2.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group2.transform!(Geom::Transformation.translation([0, 0, 100.mm]))

# --- Create Cube 3 (100x100x100 mm, base at x = 200 mm) ---
group3 = group4_entities.add_group
group3_entities = group3.entities

cube3_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face3 = group3_entities.add_face(cube3_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face3.normal.z < 0
  face3.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face3.pushpull(100.mm)

# Move the third cube group to position
group3.transform!(Geom::Transformation.translation([50.mm, 300.mm, 0]))

# --- Raytest from each face of Cube 1 within Group 4 ---
faces = group1.entities.grep(Sketchup::Face)

faces.each_with_index { |face, index|
  next unless face.valid?
  next if face.hidden? # ignore hidden faces

  # hide this face from the ray test
  face.hidden = true
  
  centroid = face.bounds.center # Center point of the face
  normal = face.normal          # Normal vector of the face

  ray = [centroid, normal]      # Ray from the centroid in the direction of the normal

  # Perform ray test using model, ignore hidden geometry
  hit = model.raytest(ray)

  if hit
    # record the distance
    distance = centroid.distance(hit[0])

    # ray test in the reverse direction
    reverse_ray = [hit[0], normal.reverse]
    reverse_hit = model.raytest(reverse_ray) 
    container = reverse_hit[1][0] if reverse_hit
    
    # if the reverse ray hits the same container
    # there is a coincident faces and or edge pair
    distance = 0 if container == hit[1][0]

    puts "Face #{index + 1}: Distance to nearest object: #{distance.to_mm.round(1)} mm"
   
  else
    puts "Face #{index + 1}: No intersection found"
  end

  # unhide this face from the ray test
  face.hidden = false
}
nil

Face 1: No intersection found

Face 2: Distance to nearest object: 0.0 mm

Face 3: No intersection found

Face 4: No intersection found

Face 5: No intersection found

Face 6: Distance to nearest object: 0.0 mm

The ‘path’ to the hit face is a little more complicated than I demonstrated in the code above. Try this.

model = Sketchup.active_model
sel = model.selection
entities = model.entities

# Clear the model for a clean test
entities.clear!

# Create parent Group 4
puts group4 = entities.add_group
group4_entities = group4.entities

# --- Create Cube 1 (100x100x100 mm, base at Z = 0) ---
group1 = group4_entities.add_group
group1_entities = group1.entities

cube1_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face1 = group1_entities.add_face(cube1_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face1.normal.z < 0
  face1.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face1.pushpull(100.mm)

# --- Create Cube 2 (100x100x100 mm, base at Z = 100 mm) ---
group2 = group4_entities.add_group
group2_entities = group2.entities

cube2_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face2 = group2_entities.add_face(cube2_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face2.normal.z < 0
  face2.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face2.pushpull(100.mm)

# Move the second cube group upwards by 100 mm so it sits exactly on top of cube 1
group2.transform!(Geom::Transformation.translation([0, 0, 100.mm]))

# --- Create Cube 3 (100x100x100 mm, base at x = 200 mm) ---
group3 = group4_entities.add_group
group3_entities = group3.entities

cube3_points = [
  Geom::Point3d.new(0, 0, 0),
  Geom::Point3d.new(0, 100.mm, 0),
  Geom::Point3d.new(100.mm, 100.mm, 0),
  Geom::Point3d.new(100.mm, 0, 0)
]
face3 = group3_entities.add_face(cube3_points)

# Ensure the face normal is pointing upwards (Z > 0)
if face3.normal.z < 0
  face3.reverse!
end

# Push/pull the face to create a cube of height 100 mm
face3.pushpull(100.mm)

# Move the third cube group to position
group3.transform!(Geom::Transformation.translation([50.mm, 300.mm, 0]))

# --- Raytest from each face of Cube 1 within Group 4 ---
faces = group1.entities.grep(Sketchup::Face)

faces.each_with_index { |face, index|
  next unless face.valid?
  next if face.hidden? # ignore hidden faces

  # hide this face from the ray test
  face.hidden = true
  
  centroid = face.bounds.center # Center point of the face
  normal = face.normal          # Normal vector of the face

  ray = [centroid, normal]      # Ray from the centroid in the direction of the normal

  # Perform ray test using model, ignore hidden geometry
  hit = model.raytest(ray)

  if hit
    hit_point = hit[0]
    hit_container = hit[1][-2]
    
    # record the distance
    distance = centroid.distance(hit_point)

    # ray test in the reverse direction
    reverse_ray = [hit_point, normal.reverse]
    reverse_hit = model.raytest(reverse_ray) 

    # raytest returns an array containing the hit point followed by the path (an array) to the 
    # the hit entity. The second to last item in the array is the parent of the hit entity
    reverse_container = reverse_hit[1][-2] if reverse_hit
    
    # if the reverse ray hits the same container
    # there is/are coincident faces and/or edge pairs
    distance = 0 if reverse_container == hit_container

    puts "Face #{index + 1}: Distance to nearest object: #{distance.to_mm.round(1)} mm"
   
  else
    puts "Face #{index + 1}: No intersection found"
  end

  # unhide this face from the ray test
  face.hidden = false
}
nil
1 Like