Why am I inconsistently getting my outer faces going the right direction

Actually this still isn’t working and the code you included produces this at the bottom.

I’ll update if I find a solution to my original problem.

SKETCHUP_CONSOLE.clear
Sketchup.file_new
Sketchup.debug_mode = true

# Default code, use or delete...
mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

group = ent.add_group
trimmer_group = ent.add_group
face = group.entities.add_face [0,0,0],[0,10,0],[10,10,0],[10,0,0]
face.reverse! unless face.normal.z == 1
edges_to_follow = face.outer_loop.edges
e1_dir = edges_to_follow[0].line[1]

min_v = [0,0,0]
d = 1
z = 1

cut_vector = [-d,d,-z]
up_vector = [d,d,z]
down_vector = [d,d,-z]

p " e1 direction = #{e1_dir}"

pts = [ min_v.offset(cut_vector),min_v.offset(down_vector),min_v.offset(up_vector)] 

# add cut face
cut = group.entities.add_face pts
p " cut direction = #{cut.normal}"
# make cut face follow edges
cut.reverse! if cut.normal.samedirection?(e1_dir)
p " cut direction = #{cut.normal}"

cut.followme edges_to_follow

group.entities.erase_entities(edges_to_follow)

Well It sure looks good to me. :wink:

What is the expectation ?

I would like it to be a solid triangular ring. Kinda like the picture below.


This still has it’s own issues though which I am working out.

I found this snippet which I will try and adapt for my program.

tr = group.transformation
group.entities.grep(Sketchup::Face).each { |face|
    vec = face.normal
    cen = face.bounds.center.transform!(tr) ### FIX
    ray = Sketchup.active_model.raytest([cen, vec])
    ### if it hits something and that is in the cube group then we reverse the face ?
    if ray && ray[1].include?(group)
        #UI.messagebox("reversed")
        face.reverse!
        face.material = 'red'
    else 
        #UI.messagebox("not reversed")
    end
}

Edit: forgot the snippet

Okay well that was easy. The face in the middle (intersecting face) is what prevents the result from becoming solid. Get rid of the face as in this “doit2” example, and you get the solid ring:

ef doit2
  mod = Sketchup.active_model # Open model
  mod.start_operation("Follow Me Test 2",true)
  #
  ###
    #
    ent = mod.entities # All entities in model
    #sel = mod.selection # Current selection

    #group = ent.add_group
    #face = group.entities.add_face [0,0,0],[0,10,0],[10,10,0],[10,0,0]
    #face.reverse! unless face.normal.z == 1
    pts = [[-1,-1,-1], [-1,12,-1], [12,12,-1], [12,-1,-1]]
    group = ent.add_group
    face_to_follow  = group.entities.add_face(pts)
    edges_to_follow = face_to_follow.outer_loop.edges
    
    #e1_dir = edges_to_follow[0].line[1]
    e1 = edges_to_follow[0]
    e1_dir = e1.start.position.vector_to(e1.end.position)

    min_v = [0,0,0]
    d = 1
    z = 1

    cut_vector = [-d,0,0]
    up_vector = [d,0,z]
    down_vector = [d,0,0]

    p " e1 direction = #{e1_dir}"

    #pts = [ min_v.offset(cut_vector),min_v.offset(down_vector),min_v.offset(up_vector)]
    pts = [ [-1,-1,0], [1,1,0], [1,1,1] ]

    # add cut face
    cut = group.entities.add_face pts
    p " cut direction = #{cut.normal}"
    # make cut face follow edges
    cut.reverse! if cut.normal.samedirection?(e1_dir)
    p " cut direction = #{cut.normal}"

    cut.followme edges_to_follow

    group.entities.erase_entities(edges_to_follow)
    #
  ###
  #
  mod.commit_operation
  #
end
1 Like

I see. That makes sense. The edges I am using in my main program are just edges (no face) so that shouldn’t be an issue at all.

1 Like

Just a fundamental difference… :stuck_out_tongue_winking_eye:
We always need to take care how the existing and newly created basic drawing elements merge/ split/interact… with each others, depends on the order of the creation too.

So if you don’t need a face, then why did you created it in your example?

If you create directly a curve istened, the order of edges is completely in “your hand”. Then you can avoid dealing with face orientation etc.

mod = Sketchup.active_model # Open model
ent = mod.entities # All entities in model
sel = mod.selection # Current selection

group = ent.add_group
trimmer_group = ent.add_group # BTW: What is this empty group for??
face = group.entities.add_face [0,0,0],[0,10,0],[10,10,0],[10,0,0]
face.reverse! unless face.normal.z == 1
edges_to_follow = face.outer_loop.edges
edges_to_follow = group.entities.add_curve([0,0,0],[0,10,0],[10,10,0],[10,0,0],[0,0,0])
e1_dir = edges_to_follow[0].line[1]

__
__

  • Tip 1
    Work with the copy of your existing model or part of it:
    If you do have an existing path in the motel you can manually select it then use it by sel defined on 4th line of you code. So the path can be:

edges_to_follow =sel

Or you can ensure if you will have edges only:

edges_to_follow =sel.grep(Sketchup::Edge)

so you can test it in a “real environment” and see what will happen…

  • Tip 2
    After ruming snippet from console (or from Code Editor) you can try use the undo (SU menu/toolbar or ctrl+z) to examine “step by step” what geometry has been created and how does it looks like…
    (Note: the code between #start_operation - #commit_operation will be handled as one undo)

Yeah, I realize that. The reason I made a face was for ease of making edges and I just copy pasted a square someone else had made. In my full program I can’t just select certain edges because the way they are produced is by following a set of faces. After creating the edge set I delete the faces and edges that aren’t being used. This is still a rough copy but it should give you an idea of how it works.

  # uses the given faces to produce edges that can be followed.
  def get_followme_edges(faces,v)
    p "get_followme_edges"
    
    # v is the minimum xy point
    raise "v is not Point3d and will not work!" unless v.class == Geom::Point3d
    
    # gets the active model
    model = Sketchup.active_model
    
    # find loops that make up faces
    loop_edges=faces.map(&:outer_loop).compact.flatten
    
    # find all the loop edges
    loop_edges.map!{|l| l.edges}.flatten!
    
    # find loop edges that are in each face
    unsorted_edges = loop_edges.find_all{|e| faces.map{|f| e.used_by?(f)}.count(true) <=1}
    
    # empty for adding ordered edges
    follow_edges = []
    
    # for debugging
    p "first edge"
    p "first start = #{[v]}"
    p "egde starts = #{unsorted_edges.map{|e| e.start.position}}"
    
    # find the first edge which starts at our given value v
    first_edge =  unsorted_edges.find{|e| e.start.position == v}
    
    # add that first edge to our empty follow_edges array
    follow_edges << first_edge
    
    # remove the first edge from the list of edges
    edge_list = unsorted_edges - [first_edge]
    p edge_list
    p "starts = #{edge_list.map{|e| e.start.position.to_s}}"
    p "ends = #{edge_list.map{|e| e.end.position.to_s}}"
    p "number_faces = #{edge_list.map{|e| e.faces.length}} "
    # get the first edge end position to use as the start for the next edge
    c_e = first_edge.end.position
    p c_e == edge_list.last.start.position
    # validation
    raise "c_e is nil!" if c_e == nil
    
    loop = 0
    
    # goes through each start and end and finds corresponding edges
    while c_e != v 
      # find the edge with the start position same as the end position of the prior edge. face length must be equal to 1 for it to be accepted
      new_edge = edge_list.find{|e| e.start.position == c_e && e.faces.length == 1}
      
      # in the case that our edge is reversed for some reason we can use the end position as the start and start position as end.
      if new_edge == nil && edge_list.length > 1
         p "c_e = #{c_e}"
        # make the new edge where the list 
        new_edge = edge_list.find{|e| e.end.position == c_e}
        raise "error in edge list" unless new_edge != nil
        
        # add the new edge to our sorted edges 
        follow_edges << new_edge
        
        # record new start position
        c_s = new_edge.end.position
        
        # record new end position
        c_e = new_edge.start.position
        
        # remove the edge from the edge list
        edge_list.delete(new_edge)
        
        
      
      # in the case the edge is not equal to nil
      elsif new_edge != nil
        
        # add our new edge to our sorted edges
        follow_edges << new_edge
        
        # record new start position
        c_s = new_edge.start.position
        
        # record new end position
        c_e = new_edge.end.position
        
        # delete the edge from the edge list 
        edge_list.delete(new_edge)
        
      else # finished finding edges
        break
      end
      loop += 1
      p "loop #{loop}"
    end
    p "get_followme_edges end"
    p "follow_edges = #{follow_edges}"
    return follow_edges
  end

But, I am not sure that #grep returns the edges in the proper path order.
They could be in random order in the returned array.

The docs for the Selection set say clearly …

Note that the order of entities ( selection[0] , selection[1] and so on) in the set is in no particular order and should not be assumed to be in the same order as the user selected the entities.


But … must comment out the #start_operation and #commit_operation calls so that the whole thing is not wrapped up within one undo.

@DanRathbun @dezmo
I have created a solution that works using some code I found here and I also added some code in to elucidate what exactly it is that I am doing. I haven’t tried it yet on multiple faces but this solution should work for anyone where the bottom face is on a single plane.

SKETCHUP_CONSOLE.clear
Sketchup.file_new
module Example
  extend self
  # Default code, use or delete...
  mod = Sketchup.active_model # Open model
  ent = mod.entities # All entities in model
  sel = mod.selection # Current selection

  # add first group
  group = ent.add_group

  # add face to first group and get the edges to be followed.
  face = group.entities.add_face [0,0,0],[0,10,0],[10,10,0],[10,0,0]
  face.reverse! unless face.normal.z == 1
  edges_to_follow = face.edges

  # from a different part of the program
  inversion = true

  # add group2
  group2 = ent.add_group
  face_g2 = group2.entities.add_face [[-1,-1,-1], [-1,11,-1], [11,11,-1], [11,-1,-1]]
  face_g2.reverse! unless face_g2.normal == 1
  face_g2.pushpull 2

  # find the e1_direction
  e1_dir = edges_to_follow[0].line[1]

  # move the edges out of the way.
  transform = Geom::Transformation.new([0,0,10])
  group.entities.transform_entities(transform, edges_to_follow)

  # minimum xy corner value
  min_v = [0,0,0]

  # various modifiers
  d = 1
  z = 1
  o = 0

  # vectors for cuts
  cut_vector = [-d,d,-z-o]
  up_vector = [d,d,z-o]
  down_vector = [d,d,-z-o]
  trim_vector = [-d,d,z-o]

  # add trimmer_group
  trimmer_group = ent.add_group
  trimmer_pts = [ min_v.offset(cut_vector),min_v.offset(trim_vector),min_v.offset(up_vector),min_v.offset(down_vector)]
  trim_face = trimmer_group.entities.add_face trimmer_pts


  p " trim_face direction = #{trim_face.normal}"
  trim_face.reverse! unless trim_face.normal.samedirection?(e1_dir)
  p " trim_face direction = #{trim_face.normal}"
  trim_face.followme edges_to_follow

  pts = [ min_v.offset(cut_vector),min_v.offset(down_vector),min_v.offset(up_vector)] 

  # add cut face
  cut = group.entities.add_face pts
  p " cut direction = #{cut.normal}"
  # make cut face follow edges
  cut.reverse! unless cut.normal.samedirection?(e1_dir)
  p " cut direction = #{cut.normal}"

  cut.followme edges_to_follow

  group.entities.erase_entities(edges_to_follow)

  def getOtherFace(e, f)
    f1 = e.faces[0] == f ? e.faces[1] : e.faces[0]
    return nil if f == f1
    return e.reversed_in?(f) == e.reversed_in?(f1) ? f1.reverse! : f1
  end

  def getShell(start_face)
    front_q = []  #unprocessed shell faces
    face_h  = {}  #hash with expanded faces
    edge_h  = {}  #Hash with expanded edges 
    shell_h = {}  #final shell

    #push start face
    front_q.push(start_face)
    face_h[start_face] = nil

    while front_q.length > 0 do 
      f = front_q.pop
      shell_h[f] = nil
      f.edges.each { |e|
        next if edge_h.has_key?(e) || e.faces.length <= 1 
        edge_h[e] = nil
        f1 = getOtherFace(e, f)                     
        unless f1 == nil || face_h.has_key?(f1)  
          front_q.push(f1)
          face_h[f1] = nil
        end
      }
    end

    return shell_h
  end

  # get all faces 
  c_faces = group.entities.grep(Sketchup::Face)
  if trimmer_group
    t_faces = trimmer_group.entities.grep(Sketchup::Face)
  end

  if inversion == true
    # for cut
    b_face = c_faces.find{|f| f.normal.parallel?(Z_AXIS)}
    b_face.reverse! unless b_face.normal.z == -1
    getShell(b_face)
    # for trimmer group.
    if trimmer_group
      b_face_t = t_faces.find{|f| f.normal.parallel?(Z_AXIS) && [1,2,4].include?(f.classify_point(min_v.offset(cut_vector)))}
      b_face_t.reverse! unless b_face_t.normal.z == -1
      getShell(b_face_t)
    end
  else 
    # for cut
    t_face = c_faces.find{|f| f.normal.parallel?(Z_AXIS)}
    t_face.reverse! unless t_face.normal.z == 1
    getShell(t_face)
    # for trimmer_group
    if trimmer_group
      b_face_t = t_faces.find{|f| f.normal.parallel?(Z_AXIS) && [1,2,4].include?(f.classify_point(min_v.offset(cut_vector)))}
      b_face_t.reverse! unless b_face_t.normal.z == 1
      getShell(b_face_t)
    end
  end

  g2 = trimmer_group.subtract(group2)
  group.outer_shell(g2)
end

It is really weird to have code within a module that is not wrapped up in a method.
This code gets run when the module is first evaluated, and then never again.

For this reason, we call it “run once code” and it is usually for metaprogramming.
Ie, it sets up constants, or module vars, or UI elements that only get defined once.

For test code, it is always best to wrap it up in methods, and have some command method to fire off the example. (As I did above with the “doit” method calls. Sometimes I call this method simply “go”.)