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.
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
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
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…
: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…
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
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
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!