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