Problem with use of raytest and classify_point methods

Context: I have a model that has multiple faces parallel to (but below) the XY plane (constant negative z-values). The faces may have materials. Similarly, the face edges may have (different) materials as well. All coplanar faces are grouped (one group for each z-value). Next, I also have edges without faces connected. These edges may be connected as curves, and each curve (or set of connected edges) will also be in a plane parallel to the XY-plane. Each curve or set of co-planar edges is also grouped. All these groups (either faces with edges, or just ‘edges’) may be further grouped into other groups (with a multilevel group structure). Ultimately, they all roll-up to a single top level group. At initial creation time, each of these groups may have transformations applied.

Problem: For a given point in the Z=0 plane (x, y, 0), I need to know the material (if any) of the point with the same x and y coordinates of the face (or face edge) with the highest value of z (thus the least negative value).

My solution so far: I think the problem could/should be solved with a combination of the raytest and classify_point methods in the Ruby API. So, I cast a ray downwards from the origin at (x, y, 0), and take the point hit by the ray to derive the material at that particular x/y coordinate. This seems to work ok most of the time (when the hit point is either inside a face or when the hitpoint is on a curve/edge set without faces). However, there seems to be ‘random’ behavior if the hitpoint is on the edge of a group that contains a face. In this case, it doesn’t seem to follow a systematic pattern whether the path of the raytest (i.e. the second return value - array - of the model.raytest) returns the face or the edge. Sometimes, it seems to return the face, sometimes it seems to return the edge… In such cases, I need to know the material of the edge (not the face). ..

I try to handle this (if a face is returned) with the face.classify_point method. This method returns whether the hitpoint is ‘inside’ the face, at the ‘edge’ of a face, at a vertex of a face, or outside the face… So, for my requirement, this should allow to retrieve the edge (if the hitpoint is on the edge, but the raytest has returned the face entity).

My problem is that this doesn’t seem to work. The face.classify method systematically returns that the hitpoint is outside the face (return value 32). This is despite the fact that the raytest has found the hitpoint as the intersection of the ray with the face… From online research on this forum, I understand that there is a difference in coordinate systems used by the raytest and classify_point methods. The solution should be to apply an inverse transformation of the group to the hitpoint. I’ve done that, but the issue remains (classify_point keeps returning 32…). One thing that’s not clear for me is: which group (of the multilevel group hierarchy returned by raytest) should be taken for the transformation inverse. I tried with the different levels of the hierarchy, but it seems to make no difference…

Can somebody with more experience than me have a look at this, and let me know what I’m doing wrong? Especially, the ‘inverse transformation’ step is something that I’m not very sure about…

My (simplified version of the) code is shown below:

        pnt_data.each do |e, data|                                             # Run the downward raytest for point
          ray_origin        = data[:point].offset(Geom::Vector3d.new(0, 0, -1), Z)# Apply small offset in z-direction to point (avoid self-hitting or hitting the canvas)
          while true
            ray             = [ray_origin, Geom::Vector3d.new(0, 0, -1)]        # Ray points downwards in Z direction
            hit             = model.raytest(ray, true)                          # Hidden geometry is not considered
            if hit
              hit_point     = hit[0]                                            # The hit_point is the intersection of the ray with the faces or (curve)/edges
              hit_entity    = hit[1].last
              ray_origin    = hit_point.offset(Geom::Vector3d.new(0, 0, -1), Z) # Re-initialize the ray_origin to a point just below the current hit_point to search for the next hit
              hit_mtl       = hit_entity.material
              alpha         = hit_mtl.nil? ? 1.0 : hit_mtl.alpha                # Determine the alpha value of the hit entity material
              if hit_entity.is_a?(Sketchup::Face)                               # If the hit_entity is a face, 2 scenario's are in principle possible: either the edge is a face boundary or not (i.e. inside). Due to design vertices are not possible...
                tr = hit[1][-2].transformation
                check_pnt   = hit_point.transform (tr.inverse)
                pnt_face    = hit_entity.classify_point(check_pnt)
                case pnt_face
                  when Sketchup::Face::PointInside                              # The hit_point is strictly inside the face area
                  when Sketchup::Face::PointOnEdge                              # The hit_point is on one of the face edges
                    hit_entity  = hit_entity.edges.first                        # Replace the 'face' as hit_entity by the 'first face edge' as hit entity
                    hit_mtl     = hit_entity.material                           # Replace the hit_mtl with the edge material! First edge: all face edges have identical materials
                    alpha       = hit_mtl.nil? ? 1.0 : hit_mtl.alpha            # Determine the alpha value of the edge material
                  else                                                          # Should normally never happen, but happens all the time....
                    break
                end
              else                                                              # The ray hit is with an edge in ref_obj. This branch is valid for lineart edges
              end
            else                                                                # No furter hits are found: break
              break                                                             # Quit the while loop --> process next point
            end
          end
        end

I could fix the problem. As mentioned in my initial message, I assumed the root issue could be related to the inverse transformation that needs to be applied for the hitpoint. I tried upfront with all transformations of the different levels in the group hierarchy (but on an individual group basis).

The solution is however to multiply the transformations for all levels of the group hierarchy (top-down order) and then calculate the inverse of this transformation product. Next, apply this inversed transformation to the hitpoint, and the classify_point method returns correctly whether the point is on the edge, or inside the face (or any of the other possibilities, like on a vertex).

I just update my previous message in case it would be helpful for other people.

In the case where you need to find the closest edge of a raytest hit-point:

test = model.raytest(test_pt, vector)
obj = test[1].last
hit_pt = test[0]
matl = if obj.is_a?(Sketchup::Face)
  edge = obj.edges.min { |edge| hit_pt.distance_to_line(edge.line) }
  edge.material
elsif obj.is_a?(Sketchup::Edge)
  obj.material
else
  nil
end

So, … also you can use Geom::Point3d#distance_to_line to find when the distance is 0.

This might simplify your code as the distance does not need to be transformed into nested contexts. Even though the hit-point is in coordinates of the active context and the actual coordinate of the object hit in it’s internal transform, the distances are just a length value. You’d be looking for a distance of zero in the case an edge was hit.

As always with floating point, test for “close enough” rather than exactly zero.

Yep. To use SketchUp’s internal tolerance:

hit_pt.distance_to_line(edge.line).to_l == 0.0

Uses Numeric#to_l to convert the Float into a Length object and uses it’s == method.