How to add and pushpull a circle on an existing face


#1

Hi,

For several days I have tried to solve this problem:

Is it possible to pushpull a circle on an existing face?

model = Sketchup.active_model 
ents = @model.active_entities

ents.each do |o|
  o.erase!
end

ptsf = [[-1000,0,0], [-1000,1000,0], [-2000,1000,0], [-2000,0,0]]

ents.add_face ptsf

# pushpull circle on face - not working
centerpoint = [-1500,500,0]
vector = [0,0,1]
radius = 300

circle = ents.add_circle centerpoint, vector, radius
		
face = ents.add_face circle

face.pushpull 200

It creates the Error: #<NoMethodError: undefined method `pushpull’ for nil:NilClass>

The endresult should be something like this:

image


#2

This gives certainly Error: #<NoMethodError: undefined method 'active_entities' for nil:NilClass>.
Do not use instance variables when you don’t need them or don’t know how to use them. The @ determines the type of variable and an instance variable is only meant to be used within a class instance (and declared in the initializemethod). If you do not use classes, you do not need it.


Recommended is to use round parentheses around the arguments of every method call. It is not required, but good style, makes it easier to recognize what is a method and avoids special cases where the precendence of subsequent method calls is ambiguous when they are only separated by spaces.


After reading an error message like this, the subsequent step is to become curious why the object on which pushpull has been called is nil. Therefore you need to break before doing the pushpull and inspect the value of the reference face (e.g. with puts('face', face.inspect)).
Apparently ents.add_face circle has returned nil. This happens when a face in the same place already exists. The circle face already exists because it has been created automatically out of the square when you inserted the circle edges that partition the square into two faces.

This is a tricky part of trying to use the API like you would model manually. The add_circle method creates edges and returns them, but it triggers also automatic merging and splitting which causes SketchUp to create a circle face, but it is not returned as a reference.
But in order to continue your script, you need a reference and you need to be sure that you find the reference for the right entity (you cannot rely on guessing, programming must always be unambiguous).
Methods with side effects (automatic merging and splitting) are a bit indeterministic.

A work-around is to compare a snapshot of the existing entities before and after your operation and find the reference to the circle face this way. When thinking of work-arounds we need to ensure that there are no risky assumptions that may not always be true. This approach works here

  1. when we can assume that exactly one circle face is created successfully (e.g. we must know no there are no existing edges that causes it to split into two or more circle sections with additional split edges).
  2. we know there is no concurrency like a parallel process that interferes and add/remove entities between our snapshots.
entities_before = ents.to_a
circle_edges = ents.add_circle(centerpoint, vector, radius)
circle_face = (ents.to_a - entities_before - circle_edges).first

unless circle_face.nil?
  circle_face.pushpull(200)
end

An alternative is therefore to avoid operations that cause merging and splitting. When using the API, it is better to think in a different paradigm, not how to modify geometry like you would do with the mouse, but how to compute all the vertex positions of the final geometry. All experienced SketchUp developers prefer to compute the points using virtual geometry and trigonometry or vector math (Geom::Point3d, Math.sin or Geom::Transformation.rotation) and then add a face for these circle points using add_face.


#3

Ok - thank you. I think I will do that - compute all the points in the circle.


#4

A possibly irrelevant question: do you realize that coordinates in SketchUp Ruby API code are taken to be inches unless you explicitly specify units (e.g. 1000.mm) regardless of the model units you have set? The to_l method will convert value into a Length in the model’s units, but will rescale it to keep the equivalent to the original inches (e.g. 10.to_l => 25.4cm) I ask because the values in your snippet are suspiciously large (thousands) if taken to be inches.

This might not matter in your specific case, but you should also be aware that in SketchUp a “circle” is actually a regular polygon with some attached metadata that remembers the mathematical parameters of the true circle. The metadata is created during the add_circle method. There is no way to generate or attach it to a polygon after the fact. So, don’t expect to treat the result of building it via @Aerilius excellent suggestion to respond to circle-related (ArcCurve) methods.


#5

Found a possible solution:

The circle variable is an Array of Edges. So it iterates over all the edges and grap the first vertex and push into the cv_arr

centerpoint = [-750,500,0]
vector = [0,0,1]
radius = 300

circle = ents.add_circle centerpoint, vector, radius
		
cv_arr = []
circle.each do |c|
  cv_arr << c.vertices[0]
end

face = ents.add_face cv_arr

face.pushpull 200

Yes I do.


#6

That works because you didn’t first draw the ptsf face, so the circle is just that with no face in it. It lets the add_face succeed because there is no prior duplicate face (try breaking up the code right after the add_circle call and you will see this is true).


#7

Well actually I did so the code should look like this - And it seems to work:

model = Sketchup.active_model 
ents = model.active_entities

ents.each do |o|
  o.erase!
end

ptsf = [[-1000,0,0], [-1000,1000,0], [-2000,1000,0], [-2000,0,0]]

ents.add_face ptsf

# pushpull circle on face
centerpoint = [-750,500,0]
vector = [0,0,1]
radius = 300

circle = ents.add_circle centerpoint, vector, radius
		
cv_arr = []
circle.each do |c|
  cv_arr << c.vertices[0]
end

face = ents.add_face cv_arr

face.pushpull 200


#8
cv_arr = []
circle.each do |c|
  cv_arr << c.vertices[0]
end

What you are wanting to do, is “iterate the circle edges, and collect all the edge vertice arrays, flatten the array of nested arrays, then uniquify the array because there will be two references to every vertex in the array” …

cv_arr = circle.map(&:vertices).flatten.uniq

ADD: You could also likely do …

cv_arr = circle.map(&:vertices).map(&:first)

[ fixed examples ]


Here is some sample code to study …

circle_edges = ents.add_circle( centerpoint, vector, radius )

circle_curve = circle_edges.first.curve
circle_curve_vertices = circle_curve.vertices

faces_common_to_circle = circle_edges.first.faces

circle_face = faces_common_to_circle.find do |face|
  face.outer_loop.vertices == circle_curve_vertices
end

#9

Undoubtly more beautiful - after some googling I came to the conclusion that it may be like this:
(not :& but &: )

cv_arr = circle.map(&:vertices).map(&:first)

So thank you!

The last part looks interesting, but I seems unnecessary in my case?!..


#10

My BAD! Sorry. I’ve noticed more and more that the signals from my brain to my fingers are slower on one side, so I am often transposing letters and having to back up and fix spelling.

And I often mistype these shorthand method calls. I’ll try to member that method names are usually referred to in Ruby by symbols. (Spock voice: “remember…”) [ I corrected the above examples.]

Well if you know that the outer face has exactly 4 vertices you can pick the face that has more than 4.

But you need to find the face that add_circle creates somehow. And it is better to learn how to code for any scenario rather than a specific one. You just know that things will vary in the future.