Ruby API pushpull behavior - cannot replicate manual behavior


#1

Hi all,

I’m unable to replicate the standard push/pull behavior with the API.

I create a face on the surface of a cube and then issue the following commands:

face.pushpull(-0.5, false)

I thought that this would then intrude into the cube. But what it instead does, is create a new solid which intersects the cube. I then cannot select the new solid, probably because it’s intersecting.

This is not the behavior when I do this with the push/pull tool. What could be the problem?

This code is run when a face of a solid is selected.

Complete code located below:

def convert_to_units(elem)

  converted_elem = elem.cm.to_inch


  return converted_elem

end


def create_sticker
  model = Sketchup.active_model
  entities = model.entities
  
  #create the sticker square: should be 5 cm by 5 cm

  sticker_dim = convert_to_units(5)
  pts = []
  pts[0] = [-sticker_dim/2.0, -sticker_dim/2.0, 0]
  pts[1] = [sticker_dim/2.0, -sticker_dim/2.0, 0]
  pts[2] = [sticker_dim/2.0, sticker_dim/2.0, 0]
  pts[3] = [-sticker_dim/2.0, sticker_dim/2.0, 0]

  selection = model.selection

  selection.each do |phys_sel| 
    
    if phys_sel.typename == 'Face'

      #get the bounding box:
      bounds = phys_sel.bounds
      center_of_face = bounds.center

        face = entities.add_face pts
        
        target_normal = phys_sel.normal
        current_normal = face.normal

        #first, translation
        translation = Geom::Transformation.new bounds.center

        entities.transform_entities(translation, face)
        
        #then, rotation
        point = bounds.center
        axis = current_normal.cross(target_normal)
        angle = Math::acos(current_normal.dot(target_normal))
        
        if axis.length > 0.0
        
          rotation = Geom::Transformation.rotation point, axis, angle
        
          entities.transform_entities(rotation, face)
          
        elsif current_normal.z == -1
          #in this case we need to rotate by 180 degrees
          rotation = Geom::Transformation.rotation point, (Geom::Vector3d.new 1,0,0), Math::PI
          entities.transform_entities(rotation, face)

        end

        face.pushpull(-0.5, false) #make a new face

    end

  end

end

# Add a menu item to launch our plugin.
UI.menu("PlugIns").add_item("Create stickers") {
  create_sticker
}

#2

Please post not the complete code, but the code of only the problem (see mcve). Everything unnecessary (like materials, or unused code, 2nd line) costs us more time to understand.

Apart from that, some tips that will improve your coding skills:

  • Use sensible variable names. If elem is a number or length and not an entity or other element, call it like that, to avoid errors by you and misunderstandings by people who read or work from your code.

  • Don’t use string names for type comparison (phys_sel.typename == 'Face'). Thomas has written an in depth explanation.

  • Don’t use external coupling. That is when one method accesses a global (or almost global) variable under the assumption that either another method has set it up with a correct value, or that the user has prepared it correctly.
    By accessing Sketchup.active_model within a method, you assume that the user has not focussed another model. By accessing the selection, you assume that the user has selected appropriate entities. Better is to pass in these dependencies (model, or an array of entities) as method parameters.

  • By default, SketchUp’s internal entities are already inch (and this is not going to change as long as this API exists). All other “units” are methods that convert from that unit to inch. So actually when you pass to any method that accepts lengths 1, it is the same as 1.to_inch, and 2.5.cm is the same as 2.5.cm.to_inch.

  • Ruby Style is indentation of 2 spaces (which would also increase readability).


#3

Hi Aerilius,

The reason I have put so much code down is because I am not sure where exactly the problem is - although I think the problem is with the pushpull statement, it could be that a prior transformation error is causing me to get strange behavior. I want feedback if I am solving my problem completely wrong. I posted the line that seems to not be working correctly at the top. I am certain that if I had just posted that one line, someone would have posted “looks fine to me, can you post the rest of the code?” I am trying to mitigate that step. As a relatively novice user, I am not sure where the problem will be, so please understand why I am posting the full code. This script is quite short

I removed the materials stuff, which seemed to be the primary extraneous stuff that was bothering you.

I will keep some of your other suggestions in mind for future refactoring of the code. I have changed the indentation to 2 spaces.


#4

Yes, and exactly that is the idea of figuring out a MCVE, it’s a compromise: You exclude all possibilities (like materials) that you are sure don’t cause the issue, but you include enough to reproduce the issue (which the single line wouldn’t).

  • By leaving more things out and testing you can often solve a problem on your own.
  • Another approach is to set your testing goal lower: Instead of expecting that all the code will behave like SketchUp’s pushpull, divide and conquer the problem by testing if parts of the code do the expected (partial) step. One can log properties to the ruby console with puts or check if conditions (is the face oriented as expected, etc.).

Thanks for editing, I’ll take a deeper look later (I’m in a hurry).


#5

(Guess) Try reversing the face before push-pulling ?


#6

Please, I am new to the Sketchup API, and this is the simplest I can reduce the problem to where I am sure that the problem is not elsewhere.

I have spent a few hours now trying to debug this, and left something like 10 extraneous lines. I don’t want to sound like a jerk, and am very grateful for any actual help, but can we please get off trying to explain to me debugging 101 like I am new to programming?


#7

Thank you for the suggestion, but I have tried this in various permutations, and it has not helped. My best guess is that somehow the face is not aligned with the surface, but I don’t know how/why this would be.


#8

From your code it is impossible to determine what is the selection when you start. That is, what is the content of your model and what have you selected? This might have a bearing on your problem. For example, if the “cube” is a Group or ComponentInstance that you open for edit and select a Face, then the new face you create is in the model’s Entities collection, not the Group’s definition’s collection and they will not intersect, either before or after you pushpull.


#9

Hi slbaumgartner,

Good point, I will edit my post to clarify, but I am selecting a face of a solid object (a cube in my tests).

This sounds like it is likely the problem. Can you please elaborate so I understand for the future how the intersection works? Perhaps you can provide a very brief example?


#10

A primary mission of Groups and Components is to isolate their geometry from the rest of the model. To do this, their definitions each have their own Entities collection, separate from the general Entities collection of the model. Things placed in different Entities collections do not interact. Your code creates your new Face in the model’s Entities collection, so if that is not where the selection lives, they are isolated from each other. When you say that your cube is a “solid object” that sounds like it must be a Group of ComponentInstance, as SketchUp does not apply the label “solid” to ungrouped geometry.


#11

Okay, I understand, thank you. What is the correct solution then? How do I access the group to which the selected face belongs?


#12

I think you need to transform the points array before using add_face. Then the added face will intersect the selected face.

Not sure why adding the face, then transforming it does not trigger an intersect. It seems like it should work the way you have it.


#13

use

face.parent.definition.entities

not model.entities


#14

Attempting to add it to the phys_sel, the physically selected face

phys_sel.parent.definition.entities returns:

Error: #<NoMethodError: undefined method `definition' for #<Sketchup::Model:0x0000000d800c98>>
<main>:73:in `block in create_tag'
<main>:52:in `each'
<main>:52:in `create_tag'
<main>:in `<main>'
SketchUp:1:in `eval'

#15

well, that says the selected face isn’t in a Group or ComponentInstance, which begs the question why do you say that the selected face is in a solid? If you could post the model and indicate what you are selecting it would help a lot!


#16

I am simply drawing a square on the screen and pulling it upward using the GUI. Then I click on one of the faces to select it. Then I run this script. I will post a picture shortly but literally that’s all I’m doing.


#17

An MCVE, including all geometry and everything that we need to run it (The code you gave did just nothing if we didn’t have your selection :anguished: ).

def create_sticker(model, phys_sels)
  entities = model.entities
  
  # create the sticker square: should be 5 cm by 5 cm

  sticker_dim = 5.cm
  pts = []
  pts[0] = [-sticker_dim/2.0, -sticker_dim/2.0, 0]
  pts[1] = [sticker_dim/2.0, -sticker_dim/2.0, 0]
  pts[2] = [sticker_dim/2.0, sticker_dim/2.0, 0]
  pts[3] = [-sticker_dim/2.0, sticker_dim/2.0, 0]

  phys_sels.each{ |phys_sel| 
    next unless phys_sel.is_a?(Sketchup::Face)

    # get the bounding box:
    bounds = phys_sel.bounds
    center_of_face = bounds.center
    face = entities.add_face pts
    target_normal = phys_sel.normal
    current_normal = face.normal

    # first, translation
    translation = Geom::Transformation.new bounds.center
    entities.transform_entities(translation, face)

    unless current_normal.parallel?(target_normal)
      # then, rotation
      point = bounds.center
      axis = current_normal.cross(target_normal)
      angle = Math::acos(current_normal.dot(target_normal))
      if axis.valid? # Not null vector, actually the if clause is not needed because the cross product of non-parallel vectors is never the null vector.
        rotation = Geom::Transformation.rotation point, axis, angle
        entities.transform_entities(rotation, face)
      end
    end
    face.pushpull(0.5, false) # make a new face
  }
end

# Set up the model to reproduce the problem.
model = Sketchup.active_model
face = model.entities.add_face([20,150,40], [20,50,40], [70,50,40], [70,150,40])
create_sticker(model, [face])

I hope I got the example right. To an example belongs also an expectation what it should do, and an observeration of what it does wrong. I’m not sure exactly what it should achieve:

  • You want to draw an extruded tile facing outwards of an arbitrarily oriented face. Considering that SketchUp’s default behavior for drawing faces is odd (if face in x,y plane, then normal downwards [0,0,-1] ), we either have to reverse the face or pushpull into the opposite direction.
  • Should the sticker be connected to the face, or should it be in its own group? Currently, the base face won’t properly intersect (merge) your selected face, probably due to numerical precision errors of the transformations.
  • Would it be ok (or better?) if we directly draw the sticker in its final orientation on the face? You would first calculate an axes transformation, then apply it to each point in pts, then draw the face and push-pull it.

The menu registration would look like this:

unless file_loaded?(__FILE__)
  # Add a menu item to launch our plugin.
  UI.menu("PlugIns").add_item("Create stickers") {
    # Always rescue and log possible exceptions from actions launched through
    # the UI or Ruby Tools. Otherwise we won't notice failures.
    begin
      model = Sketchup.active_model
      create_sticker(model, model.selection.to_a)
    rescue Exception => e
      puts e.message
      puts e.backtrace
    end
  }
  file_loaded(__FILE__)
end

#18

Winner!! This solved it. Although I’m not 100% sure why this is required.


#19

Well, it’s a better idea to add geometry exactly where you need it rather than to add geometry at the Origin and then transform it into position. You won’t always know what may already exist at the Origin, or if you are unintentionally modifying it. Eventually you will want to use Groups and Components to keep geometry separate, as slbaumgartner started to mention.

The Ruby API methods generally behave similarly to their comparable user actions. The method Entities.add_face will automatically intersect with any other loose geometry the new face overlaps. Just like drawing overlapping shapes using SketchUp’s Rectangle tool. Eventually you will get into using Groups and components to keep geomtry separated. slbaumg

What is less clear is that if transforming a face to a position overlapping another face is supposed to trigger the same automatic intersection. Through experimentation and observation, it appears not to trigger an automatic intersect of entities. The behavior could be a bug, or could be as designed.


#20

[quote=“aespielberg, post:14, topic:13646”]
phys_sel.parent.definition.entities
[/quote] returns an error, because an object’s parent will either be a model or a definition of a component or group, it will not ‘have’ a definition !

ants = phys_sel.parent.entities

Will return the entities-collection associated with the selection’s parent.
But since you can only select objects in the active_entities context, it’s the same as:

ants = Sketchup.active_model.active_entities

Which will be either the model’s own entities-collection or an entities-collection belonging to a group or component-definition…