How to make a selected face be truly flat

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. Fitting a plane to many points in 3D

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