TRIM Tool Algorithm

I’ve had my trim tool for quite a while now. It is part of the Truss plugin but at some point I would like to split it out and just make it a stand alone plugin/utility (and at no cost to the user). This type of feature should really be part of the basic set of tools in SketchUp in my opinion. Essentially you pick a face (trimming plane) and then you pick an edge or part of the solid that you want to trim off.

It has worked reliably for a while now with only a few minor issues but recently it has come to my knowledge that it actually does not properly trim hollow sections, see image below:

As you can see with a hollow section the algorithm leaves a residual face which renders the entity a non-solid.

Strange that I have not noticed this before but it is probably due to the fact that I’ve primarily been occupied with trimming wood members which are rarely hollow sections like the rectangular steel tube shown.

It has been quite a while since I created this bit of code but here is the trimming method in its entirety:

    model = Sketchup.active_model
 	view = model.active_view
	entities = model.active_entities

	ph2 = view.pick_helper
 	ph2.do_pick(x, y)

 	ph2.count.times { |pick_path_index|
		@Pickdepth2 = ph2.depth_at(pick_path_index)
		@Pickpath2 = ph2.path_at(pick_path_index)
		@TRroof2 = ph2.transformation_at(pick_path_index)
 	}

	@pickcount2 = ph2.count
	best_entity = ph2.best_picked
	
	if @Pickpath2.nil?
		# Do nothing
	else
		pathsize2 = @Pickpath2.size - 1
	
		for step in 0..pathsize2
			entityi = @Pickpath2[step]
			typei = entityi.typename
			if (typei == "ComponentInstance") || (typei == "Group")
				manifoldi = entityi.manifold?
				
				# Selects Group or Component Instance that is a solid

				if manifoldi

					# Make Group or Component Instance Unique

					entityi = entityi.make_unique
					definitioni = entityi.definition
					namei = definitioni.name


					if typei == "ComponentInstance"
						@group_entities = definitioni.entities
					else
						@group_entities = entityi.entities
					end

					@group_trans = entityi.transformation

					@Intersectpoints = []
					@Intersectpoints2 = []
					@Newfacepoints = []
					@Newfacepoints2 = []
					@Interents = []
					counter0 = 0
					counter1 = 0
					
					#  Checks each entity (edge) in group to see if it intersects with trimming plane

					@group_entities.each do |ent|
						if ent.typename == "Edge"
							line = ent.line
							intersect_point = Geom.intersect_line_plane(line, @trim_plane)

							if intersect_point
					
								# puts  "Intersect point detected at #{intersect_point}"

								@Intersectpoints[counter0] = intersect_point
								@Intersectpoints2[counter0] = intersect_point.transform @TRroof2

								counter0 = counter0 + 1
	
								# Checks that intersection point is within bounds of edge
								
								usedbyedge = ent.bounds.contains?(intersect_point)
								
								if usedbyedge
									@Newfacepoints[counter1] = intersect_point
									@Interents[counter1] = ent
									@Newfacepoints2[counter1] = intersect_point.transform @TRroof2
									
									counter1 = counter1 + 1
								end
							end
						end
					end


					draw_status = model.start_operation('Trim')
							
					##########
					#
					# Creates Temp face for trimming
					#
	
					@temp_circle = entities.add_circle(@ctr_member_face,@nrm_member_face,500,10)
					@temp_face = entities.add_face(@temp_circle)


					######
					#
					# Intersects Group with Temp Face

					@group_entities.intersect_with(true, @TRroof2, @group_entities, @TRroof2, false, @temp_face)
	

					######
					#
					# Remove edges on other side of plane

					vts = @group_entities.grep(Sketchup::Edge).map{|e|e.vertices}.flatten.uniq
						
					pln = @temp_face.plane 
					proj_pt1 = @pts[1].project_to_plane(pln)
					vec_pt1 = @pts[1].vector_to proj_pt1
					nrm = vec_pt1

					remove = []

					vts.each{|v|
 						vp= v.position.transform(@TRroof2)
 						next if vp.on_plane?(pln)
 						next if vp.project_to_plane(pln).vector_to(vp).samedirection?(nrm)
 						remove << v.edges
					}

					if remove[0]
						remove.flatten!.uniq!.reverse!
						entities.erase_entities(remove)
					end
				

					######
					#
					# Adds new face to trimmed Group

					vts = @group_entities.grep(Sketchup::Edge).map{|e|e.vertices}.flatten.uniq
					vts.each{|v|
						vp= v.position.transform(@TRroof2)
 						next if !vp.on_plane?(pln)
						v.edges.each{|e| e.find_faces}
					}


					#######
					#
					# Erase Temp face and edges

					@temp_edges = @temp_face.edges
					entities.erase_entities(@temp_edges)

					draw_status = model.commit_operation
					
					break
				end
				
			else
				definitioni = "Not Defined"
				namei = "Not Defined"
				manifoldi = "Not Defined"

			end
			
		end
	end

At this point I am kind of stuck when it comes to dealing with this interior face or possibility of one or more interior faces. I’m throwing this out there since there may exist a simple solution to this problem that I am totally missing. My one idea is to check the trimmed group or component after the operation and check to see if it is still a manifold and then to apply corrective action if it is not.

The difficulties arise with this small chunk of code that is attempting to stitch the solid back together after removing the projecting edges:

######
#
# Adds new face to trimmed Group

vts = @group_entities.grep(Sketchup::Edge).map{|e|e.vertices}.flatten.uniq
vts.each{|v|
	vp= v.position.transform(@TRroof2)
 	next if !vp.on_plane?(pln)
	v.edges.each{|e| e.find_faces}
}

Specifically the line that utilizes the find_faces method. This line of code will add faces willy-nilly on the trimming plane with no regard for interior holes. The problem of course is how to know what is an interior hole and what isn’t and where to add the correct face. This has become an interesting problem…

After finding faces, you can delete any face where every edge (of that face) is shared by more than two faces. Yes, that was a goofy sentence. Here’s the code.

model = Sketchup.active_model
entities = model.active_entities

togo = []
entities.grep(Sketchup::Face).each{|face|
  togo << face if face.edges.all? {|e| e.faces.size > 2}
  }
  
togo.each{|f| f.erase!}

1 Like

I think you are on to something here. This would have never occurred to me.

Rather than looping through all of the faces of the group or component how about limiting it to just the new faces that were created on the trimming plane:

new_faces = @group_entities.grep(Sketchup::Face).select { |face| face.plane == pln}

Of course this doesn’t seem to work, I’m trying to check if each face is on the trimming plane (pln).

It would be nice if there was a method for a face to check if it was on a plane (ie. on_plane?)

There is, a face has a plane; you have a plane.
The face’s vertices points can be checked to see if they are on a plane etc…

As an aside why not find the vertices of all of the edges that fall on the ‘wrong’ side of the cutting ‘plane’ and then project each vertex-point perpendicular [or along the main connected ‘edge’ ?] onto the ‘plane’ until it’s then on the plane - you now have the vectors from the vertex-points to the plane as an array - so use entities.transform_by_vectors(vertex_array, vector_array) ?
Class: Sketchup::Entities — SketchUp Ruby API Documentation

1 Like

Yes, I can do that but then I would have to iterate through all the vertices of a face to check if it is on a plane since some may land on the plane and others not. A face is only on a plane if all its vertices are also on that plane. I was hoping to avoid more computational overhead if possible.

In my code block above I’m trying to check the equivalency of two planes but obviously that does not work. I’m wondering though if anyone has encountered this problem before and what is the solution? (compare two planes for equivalency)

A plane consists of a point and a vector, if another plane has the same two properties - then they are ‘equal’.
If another plane has the point, and its_vector==plane_vector.reverse - then then also ‘overlay’, without being ‘equal’ - since the planes’ normals don’t match.

1 Like

I get it. Now I can see why checking for plane to plane equivalency is a bit complicated. I’m sure there is a simple algorithm though if I think about it hard enough.

I suppose I could just check the normal vector of the face and compare it to the vector that defines the plane but this only establishes if the face is parallel to the plane and not on the actual plane.

You’d be better checking that the face vertices points are on the plane ?
Then checking the face normal and plane normal [and its reverse] for matches…

1 Like

At this point I think it is too much trouble (overhead) to limit the faces just to the trimming plane.

My final fix is:

                    ##########
					#
					# Check for Interior Faces
					
					if entityi.manifold?
						# Do nothing
					else
						interior_faces = []
						all_faces = @group_entities.grep(Sketchup::Face)
				
						all_faces.each{|face|
  							interior_faces << face if face.edges.all? {|e| e.faces.size > 2}
  						}
  
						interior_faces.each{|f| f.erase!}
					end

Thank-you sWilliams for providing the solution. Again, I would never have figured this one out so concisely, its genius.

Not in his case. face#plane returns an array of the 4 plane coefficients as floats.
This means that it is Array#==(other_ary) that does the test.

Equality — Two arrays are equal if they contain the same number of elements and if each element is equal to (according to Object#==) the corresponding element in other_ary .

However Float#== overrides BasicObject#==.

Returns true only if obj has the same value as float.

But the Ruby API did not override Float#==(obj)

This means the test will not use SketchUp’s internal tolerance.

To do so, you’d need to convert the floats to the API’s Length class, ie …

new_faces = @group_entities.grep(Sketchup::Face).select { |face|
  face.plane.map(&:to_l) == pln.map(&:to_l) 
}

Then the first test by Array#== verifys that each have 4 members and then passes each member pairs to Length#==.

But are coefficent tolerance the same as point tolerance ?

2 Likes

This is why I gave up on the face is on plane issue, this stuff is now above my pay grade.

Several of us have said for many years a Geom::Plane class would be beneficial for the API.

2 Likes

It’s good to know that I’m not the first who has come up against this.

My head hurts :slight_smile:

1 Like

Take 2 :pill: and try again in the morning. :wink:

1 Like

Here’s some simplified code to check only the faces on the cutting plane, and a skp file to test against

model = Sketchup.active_model
entities = model.active_entities

# Using the variable names from line 94 of your sample code

##########
#
# Creates Temp face for trimming
#

#@temp_circle = entities.add_circle(@ctr_member_face,@nrm_member_face,500,10)
#@temp_face = entities.add_face(@temp_circle)


@ctr_member_face = Geom::Point3d.new(8, 0, 0)
@nrm_member_face = Geom::Vector3d.new(1, 0, 0)

cutting_plane = [@ctr_member_face, @nrm_member_face]

faces = entities.select{|face| face.class == Sketchup::Face &&
  face.normal.parallel?(@nrm_member_face) &&
  face.vertices[0].position.on_plane?(cutting_plane)
  }
  
togo = faces.select{|face| face.edges.all? {|e| e.faces.size > 2}}
  
togo.each{|f| f.erase!}




tube.skp (122.6 KB)

1 Like

Perhaps to skip iterating edges …

@ctr_member_face = Geom::Point3d.new(8, 0, 0)
@nrm_member_face = Geom::Vector3d.new(1, 0, 0)

cutting_plane = [@ctr_member_face, @nrm_member_face]

togo = entities.grep(Sketchup::Face).find { |face|
  face.normal.parallel?(@nrm_member_face) &&
  face.vertices[0].position.on_plane?(cutting_plane) &&
  face.loops.size == 1
}

togo.erase! if togo # nil if not found
1 Like

Hmm, You can make this algorithm run fairly quickly if you only find_faces for the new edges that are created as a result of the intersect_with. To accomplish this we remember the array of Edges before and after the modifications.

This code runs in 100ms on the model below.

start = Time.now

model = Sketchup.active_model
entities = model.active_entities

model.start_operation('bobbit', true)

@ctr_member_face = Geom::Point3d.new(4, 0, 0)
@nrm_member_face = Geom::Vector3d.new(1, 0, 0)
cutting_plane = [@ctr_member_face, @nrm_member_face]

# Gather an array of edge entites at the start of this operation
edges_at_start = entities.grep(Sketchup::Edge)

# intersect with a temporary face
@temp_circle = entities.add_circle(@ctr_member_face,@nrm_member_face,500,12)
@temp_face = entities.add_face(@temp_circle)
entities.intersect_with(true, IDENTITY, entities, IDENTITY, false, @temp_face)
entities.erase_entities(@temp_circle)

# Erase the edges that are on the wrong side of the cutting plane.
# In this example I cheat and erase everything with vertex.x greater than 4
togo = entities.select{|ent|
    ent.class == Sketchup::Edge &&
    (ent.start.position.x > 4 || ent.end.position.x > 4)
  }
entities.erase_entities(togo)

# Gather an array of edges at the end of the intersection
edges_at_end = entities.select{|e| e.class == Sketchup::Edge}

# Find only the new edges, and find_faces for them
new_edges = edges_at_end - edges_at_start
p "Found #{new_edges.size} new edges"
model.selection.add(new_edges)

new_edges.each{|e| e.find_faces} # if e.faces.size < 2}

# delete faces that are not part of the solid object
faces = entities.select{|face| face.class == Sketchup::Face &&
  face.normal.parallel?(@nrm_member_face) &&
  face.vertices[0].position.on_plane?(cutting_plane)
  }
  
togo = faces.select{|face| face.edges.all? {|e| e.faces.size > 2}}
entities.erase_entities(togo)

p Time.now - start

And a model to test it with:
tube 2.skp (114.8 KB)

EDIT: I edited the code above to remove a flaw in the find_faces line.

1 Like

entities.grep(Sketchup::Edge) is much faster…

john

2 Likes