Smoothing edges when creating faces in a loop

Hello,

I’m trying to find a way to smooth edges when creating faces in a nested loop. The add_face method im using is in the snippet below, but in general, the coordinates are read in seqence from a data array, and triangular faces are created to build a mesh as in the example image.

pt0 = [ ]
pt0[0] = [ax, ay, az]
pt0[1] = [bx, by, bz]
pt0[2] = [cx, cy, cz]

face = entities.add_face(pt0)

The smooth = true seems only to apply to edges. Chat GPT by the way was useless and ‘thinks’ that you can use face.smooth = true to smooth all edges, or even entities.add_smoothed_face(). Both of which return ‘undefined method’ errors.

I feel I may need a different method completely?

Thank you,

Jack

Use this documentation instated:

In your particular question:

You are right, the methods for edge is here:

(BTW, I believe the #soft= method what you are lokniig for. )

The #smooth= method is used to set the edge to be smooth.
A smooth edge will cause the shading between connected faces to blend to a smooth transition. The edge will still be visible.

The #soft= method is used to set the edge to be soft.
A soft edge will cause the connected faces to be treated as a surface. This means that if you have Hidden Geometry off and select one face it will also select all faces connected with soft edges. A soft edge will also appear hidden.


You need to determine the edges array that bound the face you created, and soften and/or smooth them.

The Face #edges method is used to get an array of edges that bound the face.
Conceptually (you may need a condition to filter out which edges you want to soften) :

face = entities.add_face(pt0)
face.edges.each{|edge|
  edge.soft = true
}

__
Perhaps you can collect all the edges after the mesh is created, then soften the non boundary edges only.
edge.soft = true if edge.faces.size > 1

2 Likes

Thank you so much. First I think I’ll try to select and soften the edges as each face is created… and if that doesn’t work do it at the end as you also suggest. Your “if edge.faces.size > 1” is interesting. I’ll dig more to understand it fully. I’ll keep you posted, thanks again.

You can save yourself some tedium …

pt0 = [
  [ax, ay, az],
  [bx, by, bz],
  [cx, cy, cz]
]

face = entities.add_face(pt0)

The Edge#faces method returns an Array and it’s #size method returns the number of elements in the array.

So the full expression is a boolean test whether the edge is shared between 2 or more faces.

The subpart edge.faces.size is known as call chaining where multiple methods are called, each upon the result of the previous method call.

FYI, the SketchUp API Array documentation only lists the extra methods that the API adds to the Ruby Core Array class. For all the core methods, you need to refer to core Ruby docs …

  • Array
  • Enumerable is mixed into most all other collection classes, both core and of the SketchUp API
2 Likes

Testing for when to soften (and not) becomes easier if you use a Range for columns and rows.

Here is an example of using Range#first and Range#last comparisons as the argument to Edge#soft=

  • ASIDE: I suppose since you know the dimensions of the grid, (ie: the values of first and last,) you can do the same test without ranges.

  • EDITED: to use a, b, c and d for the point references. Now also smooths non-boundary edges.

def calc_points(col, row)
  [
    Geom::Point3d.new(col, row, 0)
    Geom::Point3d.new(col + 1, row, 0)
    Geom::Point3d.new(col + 1, row + 1, 0)
    Geom::Point3d.new(col, row + 1, 0)
  ]
end ### calc_points()

def build_grid( row_num = 20, col_num = 20 )

  model = Sketchup.active_model
  model.start_operation('Create Grid', true)

  ments = model.entities
  group = ments.add_group
  entities = group.entities
  entities.add_cpoint(ORIGIN)

  # Define ranges for rows and columns:
  rows = 0..row_num-1
  cols = 0..col_num-1

  # Create triangulated grid with soft+smooth internal edges.
  for row in rows
    for col in cols
      #  d +--5--+ c
      #    | \   |
      #    3 2,6 4
      #    |   \ |
      #  a +--1--+ b

      a, b, c, d = calc_points(col, row)

      edge1 = entities.add_line(a, b)
      edge2 = entities.add_line(b, d)
      edge3 = entities.add_line(d, a)

      edge4 = entities.add_line(b, c)
      edge5 = entities.add_line(c, d)
      edge6 = entities.add_line(d, b)

      # Soften & smooth the bottom edge if not in first row:
      edge1.soft= edge1.smooth= row != rows.first
      # Always soften & smooth the dialognal edge:
      edge2.soft= edge2.smooth= true
      # Soften & smooth the left edge if not in first column:
      edge3.soft= edge3.smooth= col != cols.first

      # Soften & smooth the right edge if not in last column:
      edge4.soft= edge4.smooth= col != cols.last
      # Soften & smooth the top edge if not in last row:
      edge5.soft= edge5.smooth= row != rows.last
      # Always soften & smooth the dialognal edge:
      edge6.soft= edge6.smooth= true

      face1 = entities.add_face(edge1, edge2, edge3)
      face2 = entities.add_face(edge4, edge5, edge6)

    end # each col
  end # each row

  model.commit_operation
end ### build_grid()
1 Like

Wow, so your first and simplest approach works a treat, as each triangular face is created, only edges shared by >1 faces are made soft, and as the mesh is built up, every edge is treated correctly. As an aside, for this specific application, the model data is a grid, with each sector having four coordinates a, b, c and d, and as they may not be coplanar, two triangles are generated within each iteration of the loop. Even when the sector infill is set at less than 100% (eg 70%) the method is perfect.

What I also worked out is that if you soften AND smooth the edges you get the best result. In the SU render below, the top is ‘soft’, middle ‘soft & smooth’ and the bottom ‘smooth’. Interestingly, the Enscape render below responds only to ‘smooth’, ‘soft’ does not affect how the faces are rendered.

Thank you so much Dan.

pt0 = []
pt0[0] = [ax, ay, az]
pt0[1] = [bx, by, bz]
pt0[2] = [cx, cy, cz]

face = entities.add_face(pt0)

face.edges.each{|edge|
edge.soft = true if edge.faces.size > 1
edge.smooth = true if edge.faces.size > 1
}

pt1 = []
pt1[0] = [ax, ay, az]
pt1[1] = [dx, dy, dz]
pt1[2] = [bx, by, bz]

face = entities.add_face(pt1)
face.edges.each{|edge|
edge.soft = true if edge.faces.size > 1
edge.smooth = true if edge.faces.size > 1
}

1 Like

Thanks Dan, yes, I now recall the ‘size’ call from my Ruby tutorials. How can you not adore what Ruby can do with arrays. And your pt0 definition is much less tedious, I agree!

Jack.

Yes, I knew this would work, but looking at a mesh we can see that only the edges of faces along the boundary of the mesh need testing. It is also plain to see that the number of edges greatly outnumbers that of faces.

It is a great waste of processing time to call #faces on every edge in the mesh. Each time this is done a new Ruby array (which wraps a new C array) must be created, just to test it’s size.

So the rhetorical question: … Why do this needless testing for all those interior edges that will always need to be softened and smoothed?

This is the impetus for the above example.

Okay I edited the above example to use a, b, c and d for the sector point references.
Also moved point calculation into a separate method

I had left this out for simplicity, but also edited the example to also smooth non-boundary edges.

1 Like

Ok, so what I see here is the mesh/grid being built sector by sector with edges and the edges softened/smoothed before the faces are applied. I might be wrong, but it looks like there may be some duplication within each sector and from sector to sector. The first sector in the first row for example includes four edges between the corner points plus one diagonal, so 5 edges. The next sector shares the last edge of the previous sector, so adds 4 more edges. The second sector of the second row would only be adding 3 new edges, as there are two adjacent sectors, and so on. Interestingly this is why I opted to build the mesh with faces and not edges.

This is further complicated should I set the parameters to only partially fill the sector (as in renders below), so maybe the duplication is essential to cover both scenarios with one method?

The way the data array is generated makes it fairly easy to determine if any edge is a boundary edge or not if the sector is fully filled. It’s like this:

[
[x1, y1, z1, x2, y2, z2, x3, y3, z3 …],
[x1, y1, z1, x2, y2, z2, x3, y3, z3 …],
[x1, y1, z1, x2, y2, z2, x3, y3, z3 …]
.
.
]

The number of row and columns are variable values.

I’m totally with you on getting to the most resource efficient method, as sometimes the meshes can have 10s of thousands of faces. 128r x 128c for example is 33k triangles!

It doesn’t matter. There will always be coincident edges that SketchUp will need to merge. Ie, creating faces also creates an edge loop around each face. (There can be no face without edges to define them.)

The only other option for meshes if you are still on SU2017 is the Geom::PolygonMesh class.

But since it is a virtual class you cannot soften or smooth edges until the mesh data is added to the entities as real geometry.
This is done by passing soften and smooth flags to the Entities#add_faces_from_mesh method.

2 Likes

Thank you Dan, all noted. Your assistance has been amazing. Regards, Jack.

1 Like

You might want to look at this reference page on the various ways you can create geometry in SketchUp as well:

2 Likes

Thank you!