Outerloop of a group with several faces inside and holes

How can I obtain the outer loop from a group or component that contains more than one coplanar face like this?

coplanar faces.skp (218.5 KB)

With this code:

instance = Sketchup.active_model.selection.first
totalArea = 0
facesInInstance = instance.definition.entities.grep(Sketchup::Face)

facesInInstance.each do |unitarea|
  totalArea += unitarea.area
end
puts facesInInstance.count
puts Sketchup.format_area(totalArea)

I get the sum of all the faces, but if I obtain the outer loop of each face and remove all duplicates, I still end up with the inner loop. Any ideas?

1 Like

Just “brainstorming” here …

If we assume that the cumulative length of the outer perimeter edges will always be a larger value than any of the interior sum lengths of inner loops, then a comparison can be made to get the outer perimeter.

After collecting all edges and filtering out duplicates …

Step 2 would be to divide the array of all edges into a nested array of loops. In this case a loop is a collection of edges that are connected via shared vertices.

Step 3 would be Interating the loops array to find the longest loop.

Probably not foolproof, as you might have a inner loop that meanders and has more edges and so longer length than the outer perimeter.

1 Like

Another idea …

Having done step 2 resulting in a nested array of loops, step 3 might be to determine which loop edges do not share an internal face. These would be the inner loops to be skipped leaving the outer perimeter loop.

The idea would be to geometrically sniff out what is “inside” a loop as opposed to “outside” a loop.
A space inside an inner loop is a space shared by the edges that bound that space. The trick is to somehow determine this. Perhaps firing rays from the midpoint of the loop edges to see if they hit one of the other edges?

Thanks @DanRathbun . I think first idea should work. I’ll try tomorrow.

An alternative is to fill the holes with edge.find_faces and then gather all of the edges that are only connected to a single face.

model = Sketchup.active_model
ents = model.active_entities
sel = model.selection

model.start_operation("dummy", true)

  grp = ents.grep(Sketchup::Group)[0] # grab the first group in the entities 
  edges = grp.entities.grep(Sketchup::Edge)
  
  # fill the holes and return any edges that continue to have only one face
  outer_loop = edges.select { | edge | 
    edge.find_faces if edge.faces.size == 1 
    edge.faces.size == 1 # return true for the outer_loop edges
  }
  
model.abort_operation

sel.clear
sel.add(outer_loop)
puts outer_loop

Good solution, I had not thought about the option of doing a process and interrupting it.

This will be the final result (Although I should also consider the option that the chosen loop is not the appropriate one for what you comment later)

module Rtches
  module AreaAndOuterPerimeter
    extend self
    # Find loops
    def find_loops(edges)
      loops = []
      workingEdges = edges.dup # Array copy to work with

      until workingEdges.empty?
        loop_edges = []
        edge = workingEdges.shift
        loop_edges << edge

        # connectedEdges
        current_vertex = edge.start
        loop_completed = false

        while !loop_completed
          connected_edge = workingEdges.find do |e|
            e.start == current_vertex || e.end == current_vertex
          end

          if connected_edge
            loop_edges << connected_edge
            workingEdges.delete(connected_edge)
            current_vertex = (connected_edge.start == current_vertex) ? connected_edge.end : connected_edge.start
            loop_completed = true if current_vertex == loop_edges.first.start
          else
            # No edges connected
            break
          end
        end

        # loop to array of loops
        loops << loop_edges unless loop_edges.empty?
      end

      loops
    end

    instance = Sketchup.active_model.selection.first
    allEdges = instance.definition.entities.grep(Sketchup::Edge)
    facesInInstance = instance.definition.entities.grep(Sketchup::Face)

    totalArea = 0
    facesInInstance.each do |unitarea|
      totalArea += unitarea.area
    end

    puts "Total Area "  + Sketchup.format_area(totalArea)

    conssideredEdges = allEdges.select{|numberOfFaces| numberOfFaces.faces.count == 1}

    # Finding loops
    loops = find_loops(conssideredEdges)

    #loops perimeters
    outer_perimeter = 0
    loops.each do |l|
      perimeter = 0
      l.each do |le|
        perimeter += le.length
      end
      perimeter >= outer_perimeter ? outer_perimeter = perimeter : outer_perimeter
    end
    puts "Outer Loop Length " + Sketchup.format_length(outer_perimeter)

  end
end

Now, it is also best to pass the parent’s transformation (or any cumulative transformation of the instance path) into the #area method so that instance scaling is applied to the face area values.

Indeed, you’re absolutely right. In fact, in the rest of the extensions I have considered it.

Thank you @DanRathbun

1 Like

Althougth @sWilliams solution is very tempting and easy to implement I think that aborting the process could be problematic somtimes so, I finally will implement the option with both loops. Thnaks to both @DanRathbun and @sWilliams