Remove inner faces programmatically

I am trying to analyze a given series of nested faces in order to remove some of them. I want to determine which is the outermost face, then alternately erase, save, erase, save… subsequent nested faces. Some faces will be isolated and will be saved no matter what. A test case is shown in the image below.

In order to find out which faces are nested within what other faces, I am analyzing each face and its neighboring inner and outer loops.

I think this is the way to go anyway.

The idea is that:

  • I can compare the outer loop of one face to all of the inner loops in the collection to determine if one face is nested within another
  • A face with an inner loop and an outer loop that exists nowhere else in the collection must be the outermost
  • A face with no inner loop and an outer loop that is some other face’s inner loop is the innermost
  • A face with no inner loop and an outer loop that exists nowhere else in the collection is isolated

Based on this information, I think I can assign face nesting levels starting with nesting level 1 at the outermost. Then I can delete all faces with even nesting levels.

I am using a hash to collect info about the faces and add various keys as I determine them. In the code below, I only have added "solo" => true but other stats like "isolated" => true will be added as well.

Does this make sense?

The problem I’m having is comparing the loops. I understand that the loops themselves cannot be compared because they are different entities when considered from different neighboring faces. The edges, however, ought to be geometrically similar, giving me some way to compare their Sketchup::Edges arrays.

That does not appear to be working. See the line

if(!faces_collection[i]["inner"] - this_array["outer"])

below.

Here is what I have so far:

def self.z_remove_inner_faces(z_section_face_group)

  # create empty face_info for recording info about the faces
  faces_collection = []

  # loop through faces and collect info about each
  z_section_face_group.entities.grep(Sketchup::Face).each_with_index{|f, index|

    #store the face in a hash
    this_face = {
      "face_ent" => f
    }

    # then loop face loops and record if inner or outer
    # only edges can be compared because loops will appear different
    f.loops.each{|l|
      if(l.outer?)
          this_face["outer"] = l.edges
      else
          this_face["inner"] = l.edges
      end
    }

    # push the face_info into the faces_collection
    faces_collection << this_face

  }

  # loop all faces in the collection to inspect them
  faces_collection.each_with_index{|this_array, index|

    print "\ninspecting face array #{index}\n--------------------\n"

    #print "searching for edge: #{this_array["outer"]}\n"

    # set some tracking vars
    is_solo = false

    # loop all other faces in the collection for comparison
    faces_collection.length.times{|i|

      if(index != i) # no need to compare array to iteself
      
        print "comparing to face array #{i}\n"

        if(faces_collection[i]["inner"] == this_array["outer"])
        #if(!faces_collection[i]["inner"] - this_array["outer"])
            print "FOUND! face #{this_array["face_ent"]} is inside face #{faces_collection[i]["face_ent"]}\n"
        end

      end

      # if a face has no inner loop AND its outer loop does not exist elsewhere in the collection, is a solo face
      if((!this_array.key?("inner")) && (!faces_collection[i].key(this_array["outer"])))
          is_solo = true
      end

    }

    # at this point this_face has been compared to all others so some conclusions can be made
    if(is_solo)
        this_array["solo"] = true
        print "This face is solo!\n"
    end
  }    

  # report current state of things
  faces_collection.each{|i|
      print "---------------------\n"
      #print "#{i}\n"
      i.each do |key, value|
          print "#{key}: #{value}\n\n"
      end 
  }

end #def
1 Like

I’ve done something similar to find edges within a model.
For each edge, count the number of faces. If it is exactly one, then the edge is on the outside of the face.
Those faces could then be stored for removal.

def EdgeSelect::selectGroupEdges(group)
	group.entities.each { |e|
		if e.class == Sketchup::Edge
			edge = e
			faces = edge.faces
			if faces.length == 1
				pos1 = edge.vertices[0].position
				pos2 = edge.vertices[1].position
				pos1.transform! group.transformation
				pos2.transform! group.transformation
				#edge2 = Sketchup::Edge.new(pos1,pos2)
				edges = Sketchup.active_model.active_entities.add_edges pos1,pos2
				Sketchup.active_model.selection.add edges
			end
		end
	}
end

If you are sure that your model is conform (in perfect rings), that is

  • faces have only one or two loops (one outer loop, and one or zero inner loop)
  • inner loops have all their edges connected to the same face(s)

…then, you can simply take an arbitrary edge on each loop (say the one with index 0), and use the topological relation giving the faces of this edge.

Here is the kind of corresponding code

EDIT: Now tested

#Take a collection of faces in rings (or isolated) and identify those to be deleted
def ring_faces_to_be_erased(lst_faces)
	#Find the outer faces: several loops, but edges on the outer loop are a border (1 face only)
	lst_outer_faces = lst_faces.find_all { |face| face.loops.length == 2 && face.loops[0].edges[0].faces.length == 1 }
	
	#Recursion from outer faces
	hsh_faces_to_be_erased = {}
	lst_outer_faces.each do |face|
		erase = false
		while face.loops.length == 2			
			#Take an edge on the inner loop and get the inner face on this edge
			face = face.loops[1].edges[0].faces.find { |f| f != face }
			
			#Swap the erase status and declare face to be erased
			erase = !erase
			hsh_faces_to_be_erased[face.entityID] = face if erase
		end	
	end
	
	#List of faces to be erased
	hsh_faces_to_be_erased.values
end

@hank,

when trying to work out what I need ruby to find, I often write a fairly simple, easy to change, script and add some ‘visual coding’ to see what the code is doing…

I create a simple model to test on, and re-run, modify, re-run until I think I’ve covered all my edge cases…

then I re-write as defs , classes or modules etc…

cull_2D_faces.rb (643 Bytes)
cull_2D_faces.skp (22.7 KB)

john

Wait… what!? What are you doing? …pasting screen actions into a Ruby Console? You can do that!?? HOW?

Ruby Console is a ‘multiline’ input box [always has been on a mac]…

so, you can copy paste scripts directly into it…

BUT, it is displayed as a ‘single line’ input field, because that’s all that the Windoze version could do…

I modified the nib file for SU v7 and have been using that ever since… [as do many other mac coders]

I have offered it to you a number of times…

it makes it very easy to test things out, as the file I attached shows…

john

Oh THAT’s what you were talking about… I see. I would love to try it. Is it available in the extension warehouse?

curious about…

sleep 0.2

to allow you to see what is happening (in human time)?

1 Like

:refresh actually allows you to see it and :sleep set the duration [0.25 is normally what I’d use], in that one I use selection as a instead of creating holder arrays, but it quick…

no, that would be against the terms of service, but ‘they’ don’t seem to mind me privately sharing it…

john

OK, trying to understand what these lines mean…

select the face edges

sel.add([face] << face.edges) && next if face.outer_loop.edges[0].faces.length == 1

if there is only one outer loop edge?

sel.add([face] << face.edges) if  (sel.to_a - face.edges).size == sel.size

ok no idea!

sel.add([face] << face.edges) unless  sel.include?(face.edges[0])

if there is at least one edge?

Is that right?

hi hank, I added some comments that hopefully make it clearer…

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

# selection is a 'collection' that you can :add to, :remove from or :clear
# :remove is an alias for :add and :add is actually a toggle [adding something twice removes it...]
# :clear needs content or it will raise an error...
sel.clear unless sel.empty?

=begin
  # unused here,  I tested an example without faces and kept it in the snippet...
  # :grep(){} is used to add faces to the found edges...
  ents.grep(Sketchup::Edge){ |e| e.find_faces }
  # showtime: I'm only using this to observe what the code does...
  sleep 0.2
  view.refresh
=end

# :grep returns an array 
faces = ents.grep(Sketchup::Face)

# it could also have been passed the next block directly i.e. ents.grep(Sketchup::Face){ |face| ....}
# but we need a face array for the 'cull' at the end...
faces.each{|face|
  # sel.add(Array.new.push(face, face.edges))
  # a face has an :outer_loop that has an Jedges which have :faces 
  # if any edge in an :outer_loop has only one face, then it's added to the selection and we skip to the :next 
  sel.add([face] << face.edges) && next if face.outer_loop.edges[0].faces.length == 1
 
  # showtime
  sleep 0.2
  view.refresh
 
  # same push but only if [our collection] - [new content] dosen't reduce the size
  # if it did it indicates it's already in there, i.e. from another face...
  sel.add([face] << face.edges) if  (sel.to_a - face.edges).size == sel.size
 
  # showtime
  sleep 0.2
  view.refresh
 
  # only if a random edge isn't already in the collection...
  # you could check all, but any single one works quicker...
  # without :next on the previuos this 'double' check will 'toggle' :add 
  # i.e. it will remove that last item unless it meats both conditions
  sel.add([face] << face.edges) unless  sel.include?(face.edges[0])
 
  # showtime
  sleep 0.2
  view.refresh
}

# hopefully the rest is now understandable...
model.start_operation('cull')
faces.each do |face|
  next if sel.include?(face)
  face.erase!
end
model.commit_operation

john

1 Like

As the old saying goes, “there is more than one way to skin a cat”.

My solution

mod = Sketchup.active_model
ent = mod.active_entities
sav = []
# find a new face whose outer loop is not shared and has an inner loop
# get the other face sharing the inner loop
# erase and repeat
begin
  fac = ent.grep(Sketchup::Face).find{|f|
  !sav.include?(f) && f.outer_loop.edges[0].faces.length==1 && f.loops[1]}
  if fac
    sav << fac
    nxt = (fac.loops[1].edges[0].faces - [fac])[0]
    nxt.erase! if nxt
  end
end while fac
2 Likes

SUPER helpful… I think i get everything for the most part. Honestly this is magical to me! Your code is so short and efficient it is mind-blowing.

Anyway, I got a slightly different result where one of the inner faces remains in the selection…

@sdmitch

That worked too! Wow!

that works here, was that the .rb or the commented code?

the commented code works on that for me…

I may have modified the conditionals, after I posted the file…

I was more surprised it worked if I drew a random line across it…

random_line

it’s meant to a ‘playground’, and I never claimed it would work :wink:

john

1 Like

I tried again and it worked! I must have messed something up in the comments where I was adding so notes.

I know! pretty cool! I would have thought that would short circuit things too!

My solution fails when the line is drawn because that eliminates all inner loops.

Hi @john_drivenupthewall can you explain this snippet to me?

sel.add([face] << face.edges)

I am trying to adapt your solution to the situation where instead of a selection collection, faces are processed “behind the scenes” using an array of faces to save called faces_to_save similar to @sdmitch’s solution.

Something like?

faces_to_save << face.edges

but that does not actually work and ALL faces end up getting erased.

I even tried

faces_to_save << ([face] << face.edges)

but not working either so obviously I just don’t understand what is happening at that line!

baby steps are what you need…

so, you want to replace sel with an array…

change the line sel = model.selection to >> sel = [] # an empty array

use find and replace on the name sel and replace with to_go # or whatever

test the code and it still all works…

john

OMG that is tooo easy! Ha ha. I thought sel being a collection versus and array would require a complete re-think.

Gonna give it a go tonight!

Thanks!