How to make a selected face be truly flat

I’ve been tracking down an issue, that I should write up about sometime, because the initial symptoms are wild and confusing. I did finally figure out what the real issue was, and I’m trying to find an extension that could take care of the underlying issue. I’m avoiding telling what the original symptoms were, it might send you off into the hours I spent before I saw what was the important part!

So, SketchUp considers something to be coplanar if all of the internal points are on the same plane, but there is a tolerance to that, and imported geometry may have a smaller tolerance. This comes up with importing CAD, and there are extensions such as Eneroth Flatten to Plane to help with that. For the problem I’m solving the selected face may not be on any axis, so the flattening direction is the normal of the face I suppose. That might be as little as a one line change in the flatten to plane code.

But, I think it flattens back to zero, and not just back to the edges of the selected face. Here are manual steps that achieve exactly what I want to do:

  1. Double click down to the geometry.
  2. Use the Axes tool to set the axes so that red is inferred from whatever direction the face goes left, green from the direction it goes down, leaving blue to be coming straight out of the face.
  3. Use Eneroth Flatten to Plane.
  4. Right-click, Reset the axes.

One extension that comes close to that is Move to Plane (TT, inside Architect Tools). It does show up the internal difficult triangles, but in my test case there isn’t a target plane, all possible planes are not really flat.

If Move to Plane could be told to consider what SketchUp sees as one face to be the plane to move to, that might do what I need.

A perfect extension would take the entire model and go down to every face that SketchUp sees, and does a move to plane of the internal geometry that was imported, based on the plane that is the single face SketchUp is seeing.

Is there already an extension, or are any of you intrigued enough to modify existing code?

Just a few questions for this “non-planar scenario” …

Was the “Merge coplanar faces” option selected during import, or does it matter if it is checked or not ?

Does the this send action work for the whole model or just what is selected, and does it work for your case ?

Sketchup.send_action("fixNonPlanarFaces:")

Thank you for the idea. In both cases I get a no problems found message, which makes sense, because SketchUp sees the face as being coplanar. The example I’m working on is a tiny part of a large model, that was started a long time ago. Don’t know what import options were selected.

An easy way to show the problem cases is to export as Collada and bring that back in. Then you can use the tape measure to see the real coordinates. This screenshot shows two parts of the same face, both of which should have been at 0.0", but are at 0.000250" and 0.000318", enough of a difference to cause problems, but not enough to seem non-coplanar.

I’ve been trying other extensions. Came across the @TIG Flatten to Plane extension. It’s the same as Move to Plane, but it uses Work Plane as a way to specify the plane to flatten to. I seem to have to reinstall it each session for some reason, but the bigger problem is that it puts the flattened points into a group. To recreate the original face I need to paint bucket sample the existing face, delete it, explode the flattened group, draw a line, erase the line, then paint bucket fill the new face.

It’s also not quite right as a solution because the points that are now fixed are sometimes in use by other connected faces which would leave some number of line endings not quite touching the flattened points. I read a comment in the SketchUcation forum, that referred to right-clicking and choosing Flatten Faces. I couldn’t find that option, but it does sound close to what I’m trying to do.

Another extension I tried was FredoScale. Unfortunately it doesn’t see the small differences in the coordinates, and so the box scaling option doesn’t give me middle handle to use in order to scale it to zero. If I push pull the face out a little it does then work well, but I still have to do the repair work to get the face and material back.

If I can track down the extension that has the right click, flatten faces option, that might do what I need.

In any case, finding extensions that almost do what I need gives me hope.

Vertex tools, adjust the Gizmo to the angle you want and hit the Make Planar button.

Colin,
I’ve needed this function in the last week or so and decided to make a first try at writing a solution. The code is based around Eneroth’s flatten to plane, a bit of clean up code from Dan Rathbun, with maths from Emil Ernerfeldt. http://www.ilikebigbits.com/2015_03_04_plane_from_points.html

Usage:

  • Select any set of faces and edges
  • Click on the Extensions menu > Flatten to Calculated Plane

The selected geometry will be collapsed onto a plane that represents a ‘best fit’ to the selected entities.

Any edges in the original selection that end up dividing coplanar face will be deleted.

The extension
sw_flatten_to_calculated_plane.rbz (2.4 KB)

A test file that mimics your initial question
start box.skp (74.4 KB)

For the Rubyists among us

####################
# based on Eneroth's Flatten to Plane Extension
# https://extensions.sketchup.com/pl/content/eneroth-flatten-plane

module SW
module FlattenToCalculatedPlane

  def self.purge_invalid_texts(entities)
    entities.grep(Sketchup::Text) { |t| t.erase! if t.point.to_a.any?(&:nan?) }
    nil
  end

  # Constructs a plane from a collection of points -
  # so that the summed squared distance to all points is minimzized,returns a plane ie. [point, vector]
  # Ideas and impplementation from: http://www.ilikebigbits.com/2017_09_25_plane_from_points_2.html
  def self.plane_from_points(points)
    return false if points.size < 3 # At least three points required
    sum = Geom::Point3d.new(0,0,0)
    
    points.each {|pt| sum += pt.to_a } # The + operate doesn't convert to vector from a point
    tr = Geom::Transformation.scaling(1.0/points.size)
    centroid = sum.transform(tr)
    
    # Calc full 3x3 covariance matrix, excluding symmetries:
    xx = xy = xz = yy = yz = zz = 0.0
    points.each {|p|
      r = p - centroid.to_a;
      xx += r.x * r.x;
      xy += r.x * r.y;
      xz += r.x * r.z;
      yy += r.y * r.y;
      yz += r.y * r.z;
      zz += r.z * r.z;
    }
      
    xx /= points.size
    xy /= points.size
    xz /= points.size
    yy /= points.size
    yz /= points.size
    zz /= points.size

    weighted_dir = Geom::Vector3d.new(0,0,0)
   
    det_x = yy*zz - yz*yz
    axis_dir =  [ det_x, xz*yz - xy*zz, xy*yz - xz*yy]
    weight = det_x * det_x
    weight = -weight if weighted_dir.dot(axis_dir) < 0.0
    tr = Geom::Transformation.scaling(weight)
    weighted_dir += axis_dir.transform!(tr)
   
    det_y = xx*zz - xz*xz
    axis_dir = [ xz*yz - xy*zz, det_y, xy*xz - yz*xx]
    weight = det_y * det_y;
    weight = -weight if weighted_dir.dot(axis_dir) < 0.0
    tr = Geom::Transformation.scaling(weight)
    weighted_dir += axis_dir.transform!(tr)
    
    det_z = xx*yy - xy*xy
    axis_dir = [ xy*yz - xz*yy, xy*xz - yz*xx, det_z]
    weight = det_z * det_z;
    weight = -weight if weighted_dir.dot(axis_dir) < 0.0
    tr = Geom::Transformation.scaling(weight)
    weighted_dir += axis_dir.transform!(tr)
    
    normal = weighted_dir.normalize
    
    return false if !normal.valid?
    [centroid, normal]
    
  end

  # remove edges from coplanar face (only from the slected entities)
  # https://forums.sketchup.com/t/deleting-redundant-edges-from-a-solid/104585/3
  # From: Dan Rathbun
  def self.remove_coplanar(ents)
    edge_list = ents.grep(Sketchup::Edge)
    redundant = edge_list.find_all do |e|
      next false unless e.faces.size == 2
      vector = e.faces.first.normal
      e.faces.all? { |f| f.normal.parallel?(vector) }
    end

    ents[0].model.active_entities.erase_entities(redundant) unless redundant.empty?
  end
    
  # Flatten to plane, Code adapted from Eneroth
  def self.flatten_to_plane(ents, remove = false)
    # If curves are not exploded moving one vertex will also move its neighbours,
    # causing a very unpredictable result.
    curves = ents.select { |e| e.respond_to?(:curve) }.flat_map(&:curve).compact.uniq
    curves.each { |c| c.edges.first.explode_curve }

    vertices = ents.select { |e| e.respond_to?(:vertices) }.flat_map(&:vertices).uniq
    original_points = vertices.map(&:position)
    
    plane = plane_from_points(original_points)
    return if !plane
    #p plane ### diagnostics
     
    vectors = original_points.map { |p| p.project_to_plane(plane) - p }
    ents.first.parent.entities.transform_by_vectors(vertices, vectors)
    
    #remove coplanar
    remove_coplanar(ents) if remove == true

    # Using transform_by_vectors on several vertices at once may cause 2D texts
    # from going mad with NAN coordinates and break SketchUp rendering.
    # Aka the "Zoom Extents" bug.
    purge_invalid_texts(ents.first.parent.entities) if ents.size > 0

    nil
  end

  def self.flatten_to__calculated_plane_operation
    model = Sketchup.active_model
    model.start_operation("Flatten to Plane", true)
    flatten_to_plane(model.selection, true) if model.selection.size != 0
    model.commit_operation

    nil
  end

  unless file_loaded?(__FILE__)
    file_loaded(__FILE__)
    menu = UI.menu("Plugins")
    menu.add_item(EXTENSION.name) { flatten_to__calculated_plane_operation }
  end
end
end

1 Like

Thanks Box and sWilliam for those two suggestions. Vertex Tools does fix things too, but not quite as easily as sw_flatten_to_calculated_plane!

Can I use the extension to help solve the big model problem one of our customers is having?

The underlying problem is that SketchUp’s tolerance for drawing new faces is smaller than its tolerance for what is coplanar. This means that an apparently completely flat surface gains extra faces when you try to break the existing face, if the face is made from imported geometry. SketchUp ought to fix the locations of points that are deemed to create a coplanar face, so that mathematically it is coplanar, and not just visually.

Here is a video of me trying to split the single face into two parts, only ending up with two new faces exactly on top of the existing face. The extension quickly fixes that.

I have no objection to my lil’ part helping others. It’s why I posted it. It was basically just converting pseudo code description of the task into concise Ruby code as possible.

But it may suffer from the same “SketchUp has differing behavior” syndrome.
My lil method uses the API’s idea of parallel vectors (within the API tolerance) to decide if faces are coplanar. I seem to recall a topic thread on this that TT weighed in on. It was about the API’s lack of a “coplanar” test method for the API.

I think SW gets around this by first doing what TT suggests, so that by the time he calls my method the faces are truly coplanar, so the normal vector compare can be trusted.

Tried that… trouble is that you run into scenario where the small adjustments to the vertices will trigger autofold on connected faces.

Any any script that tries to move a vertex with 100% accuracy needs to use a scaling transformation instead of a translation transformation. If you use entities.transform_by_vectors then any vectors with magniture less than 1/1000 will be ignored since SU will consider it close enough. The only thing that works to move a vertex a small amount is to use a scaling transformation of magnitude 0 and origin at the location you want to move the vertex.

1 Like

I think that I would be ok with auto fold. You could then adjust the affected face.

The important thing would be that coplanar faces match with coplanar edges/

But autofold would make the new faces not planar. So you still have issues working with the model.

Auto fold wouldn’t affect the face you are asking to have coplanar edges, but it could cause a face that shares one edge to suddenly have two faces. But that might be ok, so long as they are not seen as being coplanar to each other.

Reducing the threshold of what is considered coplanar would help to at least point out the problem cases.