Apparent random behaviour in intersect_with

I have noticed an apperently random behaviour in intersect_with. I am probably making a mistake somewhere, but I honestly can’t see where.

In my plugin I need to intersect a few groups (tools) with one group containing only one face (aoi[0]). I want the intersection to appear on the face inside the group aoi[0].

Open this model (I still cannot upload on the forum for some reason) and then execute this script from the console.

mod = Sketchup.active_model
ent = mod.active_entities

10.times do
  aois = []
  ent.grep(Sketchup::Group).each do |e|
    if e.attribute_dictionary("RadCalc_Entities") != nil
      aois << e if e.attribute_dictionary("RadCalc_Entities")["EntityType"] == "area_of_interest"
    end
  end

  intersect_entities = ["fixed_surroundings", "far_buildings", "new_buildings", "obstructions", "baffles", "shading"]

  tools = []
  ent.grep(Sketchup::Group).each do |e|
    if e.attribute_dictionary("RadCalc_Entities") != nil
      tools << e if intersect_entities.include?(e.attribute_dictionary("RadCalc_Entities")["EntityType"])
    end
  end

  mod.start_operation("Intersect")
  tools.each do |tool|
    #  tool.material = "red"
    #  puts "Before using tool #{tool} -- no faces in aoi: #{aois[0].entities.grep(Sketchup::Face).size}"
    tool.entities.intersect_with false, tool.transformation, aois[0].entities, aois[0].transformation, false, aois[0]
    #  tool.material = nil
  end
  puts "Faces in aoi: #{aois[0].entities.grep(Sketchup::Face).size}"
  mod.abort_operation
end
nil

The script intersects all the tools with the aois[0] group, counts the number of faces in the group after the interesection and then aborts the operation (equivalent to undo in the UI).

To my surprise, the number of faces keeps changing. For example I get

Faces in aoi: 29
Faces in aoi: 26

Faces in aoi: 27
Faces in aoi: 26
Faces in aoi: 27
Faces in aoi: 26
Faces in aoi: 26
Faces in aoi: 27
Faces in aoi: 31
Faces in aoi: 31

I have also tested it by undoing manually at each step and the bahviour is the same.

Also, and this is what made me start the investigation, the large face within the group disappears!

Finally, if you run the script (inside the times loop) only once you the face disappears, but if you start to delete the groups outside the central circle at random, and then you put them back again with undo, everything works as expected. :open_mouth:

Any idea? Have I made some silly mistake that I can’t see?

You are doing the intersect_with twice, but only once inside the start_operation block, so the abort_operation only cancels that intersect, the previous one outside of the block is not ‘undone’, hence the confusion…

Hmm. I double checked to be sure that I had pasted the right code. This is the only place where I use intersect_with

mod.start_operation("Intersect")
tools.each do |tool|
  #  tool.material = "red"
  #  puts "Before using tool #{tool} -- no faces in aoi: #{aois[0].entities.grep(Sketchup::Face).size}"
  tool.entities.intersect_with false, tool.transformation, aois[0].entities, aois[0].transformation, false, aois[0]
  #  tool.material = nil
end
puts "Faces in aoi: #{aois[0].entities.grep(Sketchup::Face).size}"
mod.abort_operation

Unless I have completely misunderstood how intersect_with works (completely possible :smile: ), I am executing it for each tool in the tools array.

Even if I execute it once, there is still the issue that one of the faces disappears.

You are correct - I misread your code.
So back to square one…

:thinking: Could be that you get the elements ordered in arrays differently every time you are collecting…

What happens if you “merge” the collections of faces and groups into one grep method (BTV. the grep have its own iteration method, so do not need to call the each on it…)

  intersect_entities = ["fixed_surroundings", "far_buildings", "new_buildings", "obstructions", "baffles", "shading"]
  aois = []
  tools = []
  ent.grep(Sketchup::Group) do |e|
    if !e.attribute_dictionary("RadCalc_Entities").nil?
      aois << e if e.attribute_dictionary("RadCalc_Entities")["EntityType"] == "area_of_interest"
      tools << e if intersect_entities.include?(e.attribute_dictionary("RadCalc_Entities")["EntityType"])
    end
  end

Please try to share your model…

Same issue if I combine the collections in one grep. Even if the order was different, I would expect the same result in the end given that the geometries have not changed. Thanks for the tip about grep BTW.

The link to the model was in the message, but it is from my dropbox. Here it is for convenience

I don’t think intersect is the problem. If you count the number of edges produced by the intersection you probably get the same number every time. The problem is when Sketchup tries to merge the edges with the existing geometry (in order to produce new faces). This process has always been unreliable and prone to strange inconsistencies especially in cases where there are lots of internal faces.

You can try to run edge.find_faces on every edge in aois[0] after the intersection but I’m not sure that helps…

Thanks for the suggestion @CAUL, but unfortunately it did not help, as you suspected. What did help was to get rid of the faces that I do not need directly inside the loop. In my plugin, those tools are buildings and the aois are external spaces on which I perform some analysis, so the faces inside a building were removed after the intersection anyway.

I have also added a check for boundng box intersection before intersect_with to speed up the process.

This is the updated code if you are interested. Thanks to @jimhami42 for the code to calculate the centroid. It saved me a bit of time.

module RG
  mod = Sketchup.active_model
  ent = mod.active_entities

  def self.get_centroid(pts)
    total_area = 0
    total_centroids = Geom::Vector3d.new(0, 0, 0)
    third = Geom::Transformation.scaling(1.0 / 3.0)
    npts = pts.length
    vec1 = Geom::Vector3d.new(pts[1].x - pts[0].x, pts[1].y - pts[0].y, pts[1].z - pts[0].z)
    vec2 = Geom::Vector3d.new(pts[2].x - pts[0].x, pts[2].y - pts[0].y, pts[2].z - pts[0].z)
    ref_sense = vec1.cross vec2
    for i in 0...(npts - 2)
      vec1 = Geom::Vector3d.new(pts[i + 1].x - pts[0].x, pts[i + 1].y - pts[0].y, pts[i + 1].z - pts[0].z)
      vec2 = Geom::Vector3d.new(pts[i + 2].x - pts[0].x, pts[i + 2].y - pts[0].y, pts[i + 2].z - pts[0].z)
      vec = vec1.cross vec2
      area = vec.length / 2.0
      if (ref_sense.dot(vec) < 0)
        area *= -1.0
      end
      total_area += area
      centroid = (vec1 + vec2).transform(third)
      t = Geom::Transformation.scaling(area)
      total_centroids += centroid.transform(t)
    end
    c = Geom::Transformation.scaling(1.0 / total_area)
    return Geom::Point3d.new((total_centroids.transform!(c) + Geom::Vector3d.new(pts[0].x, pts[0].y, pts[0].z)).to_a)
  end

  #10.times do
  aois = []
  tools = []

  intersect_entities = ["fixed_surroundings", "far_buildings", "new_buildings", "obstructions", "baffles", "shading"]

  ent.grep(Sketchup::Group) do |e|
    unless e.attribute_dictionary("RadCalc_Entities").nil?
      aois << e if e.attribute_dictionary("RadCalc_Entities")["EntityType"] == "area_of_interest"
      tools << e if intersect_entities.include?(e.attribute_dictionary("RadCalc_Entities")["EntityType"])
    end
  end

  mod.start_operation("Intersect")
  tools.each do |tool|
    unless (tool.bounds.intersect(aois[0].bounds)).empty?
      tool.entities.intersect_with false, tool.transformation, aois[0].entities, aois[0].transformation, false, aois[0]

      aois[0].entities.grep(Sketchup::Face) do |face|
        center = get_centroid(face.outer_loop.vertices.map { |v| v.position }).transform(aois[0].transformation)
        ray = [center, Z_AXIS]
        item = mod.raytest(ray, false)
        aois[0].entities.erase_entities(face) unless item.nil?
      end
    end
  end
  puts "Faces in aoi: #{aois[0].entities.grep(Sketchup::Face).size}"
  mod.commit_operation
  #end
end

The stray edges are removed later. If you test it you will see some left over faces. These are concave faces with the centroid outside the face. I am now looking for an algorithm to find an internal point (any) for concave polygons.

Thanks all for the various suggestions.

There are two methods in the API for this:

  1. Geom.point_in_polygon_2D(point, polygon, check_border)
  2. Face.classify_point(point)

Thanks. Yep, I was aware of these methods. However, they assume you have a point to check to begin with. I need to find that point.

An approach could be to start a triangulation and get the centroid of the first triangle found. I think this could be achieved by finding the first ear of the polygon, but I have just started the investigation :slight_smile:

EDIT
Probably an easier approach using the API is to get, within the same loop, the Geom::PolygonMesh associated to the face (face.mesh) and take the centroid of the first polygon of the mesh (a triangle).

This may be what you’re looking for. Code from Eneroth;s Solid Tools. eneroth-solid-tools/solid_operations.rb at master · Eneroth3/eneroth-solid-tools · GitHub

Method: 2

 # Find an arbitrary point at a face.
  #
  # @param face [Sketchup::Face]
  #
  # @return [Geom::Point3d, nil] nil is returned for zero area faces.
  def self.point_at_face(face)
    # Sometimes invalid faces gets created when intersecting.
    # These are removed when validity check run.
    return if face.area.zero?

    # PolygonMesh.polygon_points in rare situations return points on a line,
    # which would lead to a point on the edge boundary being returned rather
    # than one within face.
    index = 1
    begin
      points = face.mesh.polygon_points_at(index)
      index += 1
    end while points[0].on_line?(points[1], points[2])

    Geom.linear_combination(
      0.5,
      Geom.linear_combination(0.5, points[0], 0.5, points[1]),
      0.5,
      points[2]
    )
  end
  private_class_method :point_at_face

Method 1 (the old way)

    # Internal: Find arbitrary point inside face, not on its edge or corner.
    # face - The face to find a point in.
    # Returns a Point3d object.
    def self.point_in_face(face)
      # Sometimes invalid faces gets created when intersecting.
      # These are removed when validity check run.
      return if face.area == 0

      # First find centroid and check if is within face (not in a hole).
      centroid = face.bounds.center
      return centroid if face.classify_point(centroid) == Sketchup::Face::PointInside
	
      # Find points by combining 3 adjacent corners.
      # If middle corner is convex point should be inside face (or in a hole).
      face.vertices.each_with_index do |v, i|
        c0 = v.position
        c1 = face.vertices[i-1].position
        c2 = face.vertices[i-2].position
        p  = Geom.linear_combination(0.9, c0, 0.1, c2)
        p  = Geom.linear_combination(0.9, p,  0.1, c1)
        return p if face.classify_point(p) == Sketchup::Face::PointInside
      end
		  warn "Algorithm failed to find an arbitrary point on face."
      nil
    end

Thanks @sWilliams. Also quite a few useful tools in the repo!

I went for a different approach that seems to work. After the main loop, I am left only with concave faces, I think. So I do another loop


  aoi[0].entities.grep(Sketchup::Face) do |face|
    mesh = face.mesh
    first_polygon = mesh.polygons.first
    center = get_centroid(first_polygon.map{|vi| mesh.point_at(vi.abs)}).transform(aoi.transformation)
    ray = [center, Z_AXIS]
    item = mod.raytest(ray, false)
    aoi.entities.erase_entities(face) unless item.nil?
  end