Highlight detect tool - nested instances

Could anyone help me with my highlight detect tool, please.

The issue lies with the nested components, which are being drawn in the wrong position. I believe it has something to do with the transformations. Notice that my code contains two ‘draw’ methods where I attempted to fix this. Comment and uncomment them to test as needed.
Steps for testing:

  1. Place the script in the ‘Plugins’ folder of SketchUp.
  2. Go to the Extensions menu and click on ‘Highlight Tool with Hidden Detection 2’.
  3. Hover the mouse over the component in the model I sent as an example. (Notice that there are nested components; try to target them).
module MyHighlightTool
  module HighlightTool
    class Highlighter
      def activate
        @hovered_entity = nil
        @bounds_color_visible = Sketchup::Color.new(0, 255, 0, 128) # Semi-transparent green
        @bounds_color_hidden = Sketchup::Color.new(255, 0, 0, 128)  # Semi-transparent red
      end
  
      def deactivate(view)
        clear_highlight(view)
      end
  
      def onMouseMove(flags, x, y, view)
        model = Sketchup.active_model
        active_entities = model.active_entities
        entities = active_entities.grep(Sketchup::ComponentInstance).concat(active_entities.grep(Sketchup::Group))

        unless @hovered_entity.nil?
          definition_entities = @hovered_entity.definition.entities
          entities = definition_entities.grep(Sketchup::ComponentInstance).concat(definition_entities.grep(Sketchup::Group))
        end
  
        process_entities(entities, x, y, view)
      end

      def process_entities(entities, x, y, view)
        return if entities.nil?

        # Converts mouse position to a 3D radius
        ray = view.pickray(x, y)
  
        # Checks all entities (visible and hidden)
        closest_entity, closest_distance = nil, Float::INFINITY
        entities.each do |entity|
          next unless entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
  
          # Ignore excluded entities or entities without valid bounds
          next unless entity.valid? && entity.bounds
  
          # Checks if the radius reaches the entity's bounds
          hit_point = bounds_ray_intersection(entity.bounds, ray)
          if hit_point
            distance = hit_point.distance(ray[0]) # Calculates the distance between the origin of the ray and the hit
            if distance < closest_distance
              closest_entity = entity
              closest_distance = distance
            end
          end
        end
  
        # Updates the entity under the mouse
        if closest_entity != @hovered_entity
          @hovered_entity = closest_entity
          view.invalidate
        end
      end

      def onLButtonDown(flags, x, y, view)
        puts "onLButtonDown: flags = #{flags}"
        puts "                   x = #{x}"
        puts "                   y = #{y}"
        puts "                view = #{view}"
        print
      end

      def print
        unless @hovered_entity.nil?
          puts @hovered_entity
          puts @hovered_entity.is_a?(Sketchup::ComponentInstance)
            if @hovered_entity.name.nil? || @hovered_entity.name.empty?
              puts @hovered_entity.definition.name
              return
            end

            puts @hovered_entity.name
        end
      end
  
      def draw(view)
        return unless @hovered_entity
  
        bounds = @hovered_entity.bounds
        color = @hovered_entity.visible? ? @bounds_color_visible : @bounds_color_hidden
  
        # Draw filled faces
        draw_filled_faces(view, bounds, color)
      end

      # def draw(view)
      #   return unless @hovered_entity
      
      #   # We obtain the global transformation of the entity
      #   global_transformation = @hovered_entity.transformation
      
      #   bounds = @hovered_entity.bounds
      #   color = @hovered_entity.visible? ? @bounds_color_visible : @bounds_color_hidden
      
      #   # Transforms the bounding box coordinates to global space
      #   transformed_bounds = transform_bounds(bounds, global_transformation)
      
      #   # Draws filled faces in global space
      #   draw_filled_faces(view, transformed_bounds, color)
      # end

      def transform_bounds(bounds, transformation)
        transformed_corners = (0..7).map do |i|
          bounds.corner(i).transform(transformation)
        end
      
        transformed_bounds = Geom::BoundingBox.new
        transformed_corners.each { |corner| transformed_bounds.add(corner) }
        transformed_bounds
      end
  
      private
  
      # Checks the intersection between the radius and the planes of the BoundingBox faces
      def bounds_ray_intersection(bounds, ray)
        planes = bounding_box_planes(bounds)
        planes.each do |plane|
          hit_point = Geom.intersect_line_plane(ray, plane)
          # Checks if the intersection point is inside the BoundingBox
          return hit_point if hit_point && bounds.contains?(hit_point)
        end
        nil
      end
  
      # Generates BoundingBox plans
      def bounding_box_planes(bounds)
        [
          [bounds.corner(0), bounds.corner(1), bounds.corner(2)], # Face inferior
          [bounds.corner(4), bounds.corner(5), bounds.corner(6)], # Face superior
          [bounds.corner(0), bounds.corner(4), bounds.corner(5)], # Face lateral 1
          [bounds.corner(1), bounds.corner(5), bounds.corner(6)], # Face lateral 2
          [bounds.corner(2), bounds.corner(6), bounds.corner(7)], # Face lateral 3
          [bounds.corner(3), bounds.corner(7), bounds.corner(4)]  # Face lateral 4
        ]
      end
  
      # Draw filled faces
      def draw_filled_faces(view, bounds, color)
        view.drawing_color = color
  
        # Defines the faces of the BoundingBox
        faces = [
          [bounds.corner(0), bounds.corner(2), bounds.corner(3), bounds.corner(1)], # Lower face
          [bounds.corner(4), bounds.corner(5), bounds.corner(7), bounds.corner(6)], # Top face
          [bounds.corner(0), bounds.corner(3), bounds.corner(7), bounds.corner(4)], # Side face 1 left
          [bounds.corner(1), bounds.corner(5), bounds.corner(6), bounds.corner(2)], # Side face 1 right
          [bounds.corner(0), bounds.corner(4), bounds.corner(5), bounds.corner(1)], # Side face 3 front
          [bounds.corner(3), bounds.corner(2), bounds.corner(6), bounds.corner(7)]  # Side face 3 bck
        ]
  
        # Draws each face as a polygon
        faces.each do |face|
          view.draw(GL_POLYGON, face)
        end
      end
  
      # Clears the highlight
      def clear_highlight(view)
        @hovered_entity = nil
        view.invalidate
      end
    end

    unless file_loaded?(__FILE__)
      UI.menu("Plugins").add_item("Highlight Tool with Hidden Detection 2") {
        Sketchup.active_model.select_tool(Highlighter.new)
      }
      file_loaded(__FILE__)
    end
  end
end

Thank you in advance to everyone who can help…
highlight_tool.skp (14.8 KB)

highlight_tool_hidden_detection.rb (4.4 KB)

Please quote code correctly on the forum:

I have moved the topic to the correct category.

Please correct your post, and make sure waiting until the attachment is fully uploaded.

Ok, fixed

Thanks. But the attachments are still not there.

Sorry, I fixed it once more…

You are assuming we know what your tool does. We do not.
I suggest editing the first post and adding a comment in the code for the Highlighter class.

Other than this, my first reaction is going to be:

Why are you using View#pickray instead of Sketchup::PickHelper ?

My tool attempts to detect instances of components or groups, whether nested or not. When hovering the mouse over them, it draws the bounding box in the view according to the component’s boundingbox.
I’ve added images and step-by-step instructions to help you understand how it works and what is going wrong.

The issue I’m trying to fix is that the bounding boxes are positioned incorrectly if the component is moved off the origin axis (0).

Yes, of course it does.

Calling transformation does not always return a global transform. You will only get a global transform if the parent context is the active context.

If you are at the top-level of the model or in a grandparent context (of some level) … then you will need to get a combined transform to transform the bounding corners.

You can do this by building a InstancePath and using it’s #transformation method.

Or, as said previously, use a PickHelper and use it’s #transformation_at method, instead of View#pickray.

By the way I think @thomthom may already have an example of bounds highlighting either on GitHub or BitBucket.

Thanks for your time @DanRathbun

This was the version that got the closest, but it still has position errors.
You can see there is a method called collect_occurences, where I retrieve the instance path to use with Sketchup::InstancePath.new(instance_paths). However, as I mentioned, the positions are incorrect, and I really find it quite challenging to get the transformations right.
I also tried a version using PickHelper, but it made the situation worse.
I tried looking through @thomthom’s repositories, but not in depth. If you recall the name of the repo or tool and could share it, it would help me a lot.

module MyHighlightTool
  module HighlightTool
    class Highlighter2
      def activate
        @hovered_entity = nil
        @bounds_color_visible = Sketchup::Color.new(0, 255, 0, 128) # Semi-transparent green
        @bounds_color_hidden = Sketchup::Color.new(255, 0, 0, 128)  # Semi-transparent red
      end
  
      def deactivate(view)
        clear_highlight(view)
      end
  
      def onMouseMove(flags, x, y, view)
        model = Sketchup.active_model
        active_entities = model.active_entities
        entities = active_entities.grep(Sketchup::ComponentInstance).concat(active_entities.grep(Sketchup::Group))

        unless @hovered_entity.nil?
          definition_entities = @hovered_entity.definition.entities
          entities = definition_entities.grep(Sketchup::ComponentInstance).concat(definition_entities.grep(Sketchup::Group))
        end
  
        process_entities(entities, x, y, view)
      end

      def process_entities(entities, x, y, view)
        return if entities.nil?

        # Converts mouse position to a 3D radius
        ray = view.pickray(x, y)
  
        # Checks all entities (visible and hidden)
        closest_entity, closest_distance = nil, Float::INFINITY
        entities.each do |entity|
          next unless entity.is_a?(Sketchup::ComponentInstance) || entity.is_a?(Sketchup::Group)
  
          # Ignore excluded entities or entities without valid bounds
          next unless entity.valid? && entity.bounds
  
          # Checks if the radius reaches the entity's bounds
          hit_point = bounds_ray_intersection(entity.bounds, ray)
          if hit_point
            distance = hit_point.distance(ray[0]) # Calculates the distance between the origin of the ray and the hit
            if distance < closest_distance
              closest_entity = entity
              closest_distance = distance
            end
          end
        end
  
        # Updates the entity under the mouse
        if closest_entity != @hovered_entity
          @hovered_entity = closest_entity
          view.invalidate
        end
      end

      def onLButtonDown(flags, x, y, view)
        puts "onLButtonDown: flags = #{flags}"
        puts "                   x = #{x}"
        puts "                   y = #{y}"
        puts "                view = #{view}"
        print
      end

      def print
        unless @hovered_entity.nil?
          puts @hovered_entity
          puts @hovered_entity.is_a?(Sketchup::ComponentInstance)
            if @hovered_entity.name.nil? || @hovered_entity.name.empty?
              puts @hovered_entity.definition.name
              return
            end

            puts @hovered_entity.name
        end
      end
  
      # def draw(view)
      #   return unless @hovered_entity
  
      #   bounds = @hovered_entity.bounds
      #   color = @hovered_entity.visible? ? @bounds_color_visible : @bounds_color_hidden
  
      #   # Draw filled faces
      #   draw_filled_faces(view, bounds, color)
      # end

      def draw(view)
        return unless @hovered_entity
      
        # We obtain the global transformation of the entity
        # global_transformation = @hovered_entity.transformation
        instance_paths = collect_occurences(@hovered_entity).flatten!
        instance_path = Sketchup::InstancePath.new(instance_paths)
        global_transformation = instance_path.transformation
      
        bounds = @hovered_entity.bounds
        color = @hovered_entity.visible? ? @bounds_color_visible : @bounds_color_hidden
      
        # Transforms the bounding box coordinates to global space
        transformed_bounds = transform_bounds(bounds, global_transformation)
      
        # Draws filled faces in global space
        draw_filled_faces(view, transformed_bounds, color)
      end

      def transform_bounds(bounds, transformation)
        transformed_corners = (0..7).map do |i|
          bounds.corner(i).transform(transformation)
        end
      
        transformed_bounds = Geom::BoundingBox.new
        transformed_corners.each { |corner| transformed_bounds.add(corner) }
        transformed_bounds
      end
  
      def collect_occurences(instance)
        instance_paths = []
        queue = [[instance]]
        until queue.empty?
          path = *queue.shift
          outer = path.first
          if outer.parent.is_a?(Sketchup::Model)
            instance_paths << path
          else
            outer.parent.instances.each do |uncle|
              queue << [uncle] + path
            end
          end
        end
        instance_paths
      end

      private
  
      # Checks the intersection between the radius and the planes of the BoundingBox faces
      def bounds_ray_intersection(bounds, ray)
        planes = bounding_box_planes(bounds)
        planes.each do |plane|
          hit_point = Geom.intersect_line_plane(ray, plane)
          # Checks if the intersection point is inside the BoundingBox
          return hit_point if hit_point && bounds.contains?(hit_point)
        end
        nil
      end
  
      # Generates BoundingBox plans
      def bounding_box_planes(bounds)
        [
          [bounds.corner(0), bounds.corner(1), bounds.corner(2)], # Face inferior
          [bounds.corner(4), bounds.corner(5), bounds.corner(6)], # Face superior
          [bounds.corner(0), bounds.corner(4), bounds.corner(5)], # Face lateral 1
          [bounds.corner(1), bounds.corner(5), bounds.corner(6)], # Face lateral 2
          [bounds.corner(2), bounds.corner(6), bounds.corner(7)], # Face lateral 3
          [bounds.corner(3), bounds.corner(7), bounds.corner(4)]  # Face lateral 4
        ]
      end
  
      # Draw filled faces
      def draw_filled_faces(view, bounds, color)
        view.drawing_color = color
  
        # Defines the faces of the BoundingBox
        faces = [
          [bounds.corner(0), bounds.corner(2), bounds.corner(3), bounds.corner(1)], # Lower face
          [bounds.corner(4), bounds.corner(5), bounds.corner(7), bounds.corner(6)], # Top face
          [bounds.corner(0), bounds.corner(3), bounds.corner(7), bounds.corner(4)], # Side face 1 left
          [bounds.corner(1), bounds.corner(5), bounds.corner(6), bounds.corner(2)], # Side face 1 right
          [bounds.corner(0), bounds.corner(4), bounds.corner(5), bounds.corner(1)], # Side face 3 front
          [bounds.corner(3), bounds.corner(2), bounds.corner(6), bounds.corner(7)]  # Side face 3 bck
        ]
  
        # Draws each face as a polygon
        faces.each do |face|
          view.draw(GL_POLYGON, face)
        end
      end
  
      # Clears the highlight
      def clear_highlight(view)
        @hovered_entity = nil
        view.invalidate
      end
    end

    unless file_loaded?(__FILE__)
      UI.menu("Plugins").add_item("Highlight Tool with Hidden Detection 2") {
        Sketchup.active_model.select_tool(Highlighter2.new)
      }
      file_loaded(__FILE__)
    end
  end
end

A web search finds it easily:

1 Like

One problem with your tool class is that it does not set the cursor properly.

      def onSetCursor
        UI.set_cursor(633) # SketchUp's Select tool cursor
      end

Again, your code has no comments explaining what your methods do (or try to do.) Comments, comments, comments. Every method should have a comment. I suggest you use YARD comments.

In order to build instance paths, you must collect the path nodes as you drill downward. You cannot find an instance path from the leaf back upwards.

1 Like

What @DanRathbun wrote about bottom-up building of instance paths is because a nested instance is in the entities collection of a component definition. Except if there is only one instance of that parent definition, it is impossible to know which instance of the parent you are interested in based on the nested one. Each instance will have its own distinct transformation, so this picky point matters in general.

2 Likes

I think I remembered why I used View#pickray.
I need to detect hidden components as well, and SketchUp’s PickHelper method does not select hidden entities.
That’s why I iterated through all entities in the model using model.active_entities or the definition’s entities, checking if the ray intersects the bounds of each iterated entity.

I tried to do what it said in this post.
https://forums.sketchup.com/t/how-to-get-components-global-coordination/46983/2

Well, keep at it until you understand what to do then. I myself do not have the time to debug your code, much less code from an old topic post.

If anyone knows how to solve this problem, I’m willing to pay as long as it’s not too expensive.
This issue with transformations just doesn’t click for me. Even using Sketchup::InstancePath, nothing worked.
And even AI has reached a point where it doesn’t know anymore.

Try my code

      def find_transformation(object)
        # 'Path must include object to find the right transformation'
        path = [object]

        # 'Find all objects in active path'
        objects = find_parents(path, object)

        # Reverse to get the right active path because
        # active path must from model to deepest entity
        # then multiple transformation

        active_path = objects.reverse

        tr = Geom::Transformation.new
        active_path.each do |o|
          tr *= o.transformation
        end

        # p 'Return'
        tr
      end

      def find_parents(path, entity)
        parent = entity.parent
        return path if parent.is_a?(Sketchup::Model)

        # Find the instance that contains this entity
        instance = parent.instances.find { |inst| inst.definition.entities.include?(entity) }

        # Handle the case where no instance is found (which might indicate an error)
        if instance
          path << instance
          find_parents(path, instance)
        else
          path # Return the path even if an instance is not found
        end
      end
1 Like

Do you actually need to highlight bounding boxes? What is the problem you’re trying to solve? Are you trying to hover around and find hidden geometry? If you could get this to work, when you hover over something like a triangle/pyramid, you’d see a ‘box’. If you want to highlight faces, you could take a look at Dan’s Face Sniffer and Kostya’s Highlighter.

[Example] A simple FaceSniffer tool extension - Developers / Ruby API - SketchUp Community

Code improvements - Developers / Ruby API - SketchUp Community

EDIT - looks like Trinh beat me too it!

1 Like