SketchUp Trim Extension

I have recently been developing an extension that creates base trim, casing, and crown molding. I have so far gotten this little bit to work…though I have much more code in my files. I understand this probably isn’t the best way to write this, but it runs so I figured I’d post something that works.

plugins_menu = UI.menu("Plugins")
submenu = plugins_menu.add_submenu("ProTrim")
submenu.add_item("Create Base Trim") {

#Gathering user input
prompts = ["Trim Height:                        ", "Trim Thickness: ", "Quarter Round: "]
defaults = [5.0,0.5, "No"]
list = ["","", "Yes|No"]
input = UI.inputbox(prompts, defaults, list, "ProTrim")
height = input[0]
thickness = input[1]
quarter_round = input[2]

model = Sketchup.active_model
entities = model.active_entities
selection = model.selection

if quarter_round == 'Yes'
  pts = [ [0, 0, 0], [thickness, 0, 0], [thickness+0.75,0,0], [thickness+0.75,0,0.75], [thickness,0,0.75], [thickness, 0, height], [0, 0, height] ]
  # Add the face to the entities in the model
  face = entities.add_face(pts)
  connected = face.all_connected
  face.followme(selection.grep(Sketchup::Edge))
elsif quarter_round == 'No'
  pts = [ [0, 0, 0], [thickness, 0, 0], [thickness, 0, height], [0, 0, height] ]
  # Add the face to the entities in the model
  face = entities.add_face(pts)
  connected = face.all_connected
  face.followme(selection.grep(Sketchup::Edge))
end
}

There’s a couple of issues that I have with this…

A. While it creates trim, it only creates it on lines connected to the origin. I need it to build trim on any given coordinates in the model that are 99.9% of the time not connected to the origin.

B. I want to add an UI.messagebox before the input box that instructs the user to select a path of edges to extrude the trim profile on. Here is the code for that.

message = "Please select edges for your trim path."
result = UI.messagebox( message, MB_OKCANCEL)
return false if result == IDCANCEL

However this does not allow the user to select any edges while it is active, and therefore you cannot select a path to extrude trim on.

C. Another feature I want is the ability to extrude the profile on two different paths as seen below. How do I tell my code to do this. I assume the profile will have to be built twice in order to do so.

D. @DanRathbun I know you’ve been instructing me to use methods such as def build_trim, however, I am unable to execute those after I click OK on my input box. I have seen other code with a start_operation, is this what I should use to achieve this? If so, how do I use it to instruct the program to use this. I haven’t found it in the API guide.

Here is what my tool does so far.

Thank you for your help and support guys! It means a lot to me!

  • CW

The SketchUp API documentation is a DSL Reference (that only extends the core features of the Ruby programming language.)

It is not a learning resource and it’s code examples are notorious for being both worthless and containing errors.

To learn to write good Ruby code that works, you need to read good books on Programming Ruby.
My Ruby Learning Resources lists has a list for books that you can download for free. Then you can read them in your spare time, … in study hall, on the school bus, sitting on the toilet, etc.

As I told you in your last topic, … generating trim profiles with code is the hard way to do this.

Your challenge here (ie, multiple followme-extrusion paths) is a big tipoff that your trim profile face will need to be within component instances that are placed (using a geometric transformation) at the beginning of each trim path.

Except this code only works from within a method definition. The statement with the reserved word return causes the code execution to “return” from this statement, to the point in the code where this method was called.

So your main issue is not organizing your code correctly into modules and methods.

I think it is time you reviewed what I’ve written in your previous topic threads.
(Go through your avatar menu, via your profile and go to your activity and topics.)

I believe I gave you the answer in another topic thread. Your UI command would first check the model selection for a pre-selection of edges. IF the selection was empty, then and only then would you display the messagebox telling the user to pre-pick the trim path(s). When the user dismisses the messagebox (doesn’t matter how, IDOK or IDCANEL… doesn’t matter) your command should exit by simply a return statement from your command method …

    def create_base_trim
      model = Sketchup.active_model
      selection = model.selection
      if selection.empty? || selection.grep(Sketchup::Edge).empty?
        UI.messagebox(
          "Please select edges for your trim path(s) and restart command."
        )
        return false
      end
      # The command continues ...
      edges = selection.grep(Sketchup::Edge)
      # Next determine the number of edge paths in edges.
      # More code as needed ...
    end

You never run UI object creation more than once per session.

This means that …

must be wrapped within a conditional statement that will only run once when your extension loads.
Ie, usually at the very end of the extension submodule definition, or at the end of the last file of an extension in the load order …

    if !@loaded
      plugins_menu = UI.menu("Plugins")
      submenu = plugins_menu.add_submenu("ProTrim")
      submenu.add_item("Create Base Trim") { create_base_trim() }
      # Add other menu items here ...
      @loaded = true
    end

Notice, how I show that you should always only call a command method from within the menu item (or UI::Command) proc. The reason is that Proc objects are a snapshot of the current code environment. During development, you’ll be tweaking your methods and reloading the code to redefine those methods. But you cannot redefine a UI object proc without closing SketchUp and restarting the whole thing which is way too time consuming.

This is why UI object creation is always within a conditional block that ensures menu items, commands, toolbars and toolbar buttons are created once once per session. If you don’t do this, extra menu items with the same name get created in the menus.

And since you should only create them once, and will need to be able to update what menu and toolbar commands do … then you must put the working code inside command method(s) and only have the UI command proc(s) call the appropriate method.

I know I’ve showed you how to correctly set up your code before,
because you almost got it correct here in this post

Topic: End of input error, post 22
… except for the bit about calling the uneeded “init()” method.

I don’t understand how you could go from that very well formed code (in that post) to what you posted above, which is not extension code.

No this will not solve the issues you are having as they are issues with code organization which is not SketchUp API specific. (It’s generic programming practice you are having difficulty with.)

The Sketchup::Model#start_operation() paradigm is something you will need to employ at some point in your coding. (Basically anytime your code modifies the model you’ll need to wrap the code within an undo operation.)

If you’d like to use a block form method within your extension submodule, then something like this …

    def with_undo(model, opname, disable_ui = true)
      return unless block_given?
      model.start_operation(opname, disable_ui)
      yield
      model.commit_operation
    end

… and then whenever your code modifies the model you can use a method like this above …

    def extrude_base_trim(trimdef, transgrp, edges)
      model = Sketchup.active_model
      ents = model.active_entities
      grp = nil
      with_undo(model,"Add Base Trim") do
        grp = ents.add_group # added at ORIGIN
        inst = grp.entities.add_instance(trimdef,IDENTITY)
        grp.transform!(transgrp)
        inst.explode
        face = grp.entities.grep(Sketchup::Face).first
        face.followme(edges)
        # If all went well, the group should contain the extruded trim
      end
      return grp
    end

The transgrp transformation would need to be a combination translation (to the start of the first edge in the trim path array) and a rotation to align the group (and therefore the profile instance within it) to be perpendicular to the trim path. You may also need to reverse the instance’s face so that the back face is headed toward the trim path.
The trimdef is a reference to a preloaded component definition containing a trim profile face.
The edges are an array of one of the trim paths.

Keep in mind it’s an example and might need tweaking for your own use.

2 Likes
#ProTrim

#Developer: Cayden Wilson

#For feature requests or bug reports, please contact me at
#caydenwilson017@gmail.com
#Thank you for choosing ProTrim!


module CaydenWilson
  module ProTrim

    extend self

    #Only loading UI object creation once
    if !@loaded
      plugins_menu = UI.menu("Plugins")
      submenu = plugins_menu.add_submenu("ProTrim")
      submenu.add_item("Create Base Trim") {create_base_trim(height, thickness, quarter_round, entities, trimdef, transgroup, edges)}
      @loaded = true
    end

    #Ability to undo what the extension created
    def undo(model, opname, disable_ui = true)
      return unless block_given?
      model.start_operation(opname, disable_ui)
      yield
      model.commit_operation
    end

    def create_base_trim(height, thickness, quarter_round, entities, trimdef, transgroup, edges)
      #Gathering user input
      prompts = ["Trim Height:                        ", "Trim Thickness: ", "Quarter Round: "]
      defaults = [5.0, 0.5,  "No"]
      list = ["", "", "Yes|No"]
      input = UI.inputbox(prompts, defaults, list, "ProTrim")
      height = input[0]
      thickness = input[1]
      quarter_round = input[2]

      #Checking if edges exist in the selection - returns false if no edges are selected and closes program
      if selection.empty? || selection.grep(Sketchup::Edge).empty?
        UI.messagebox("Please select edges for your trim path(s) and restart ProTrim.")
        return false
      end

      #Loading in handles
      model = Sketchup.active_model
      @entities = model.active_entities
      selection = Sketchup.active_model.selection

      #Creating a group for the trim
      group = nil
      undo(mode, "Create Base Trim") do
        group = entities.add_group #added at model origin
        instance = group.entities.add_instance(trimdef, IDENTITY)
        group.transform!(transgroup)
        instance.explode
        face = group.entities.grep(Sketchup::Face).first
        face.followme(edges)
      end
      return group

      #Determines if trim is built with or without quarter round
      if quarter_round == 'Yes'
        pts = [ [0, 0, 0], [thickness, 0, 0], [thickness+0.75,0,0], [thickness+0.75,0,0.75], [thickness,0,0.75], [thickness, 0, height], [0, 0, height] ]
        # Add the face to the entities in the model
        face = entities.add_face(pts)
        connected = face.all_connected
        face.followme(selection.grep(Sketchup::Edge))
      elsif quarter_round == 'No'
        pts = [ [0, 0, 0], [thickness, 0, 0], [thickness, 0, height], [0, 0, height] ]
        # Add the face to the entities in the model
        face = entities.add_face(pts)
        connected = face.all_connected
        face.followme(selection.grep(Sketchup::Edge))
      end
    end
  end
end

Does this fix some of the organization issues. I have an error that when I try to run the program from the menu it does not load…no error message shows up, and in the ruby console, it simply says true.

I looked and think I need to use the translation for what I’m trying to achieve.

vector = Geom::Vector3d.new(0, 1, 0)
tr = Geom::Transformation.translation(vector)

I want my vector to be at the start of each selected path, how would I do that. Is there a way to find the start point of a selection (with edges)?

Here’s the goal of the program.

  1. You launch the program from the menu and a message appears instructing you to select a path or paths to extrude a trim profile on. The path is made of edges.

  2. The program builds the trim profile based on the information in an input box.

  3. The trim is able to be created on separate paths that may run on different axis or at angles.

  4. The program can set the material of the new trim.

  5. The program takes the new geometry created and places it into a group, which is assigned to a new layer corresponding with the trim in the model.

You are still not following my advice.

  1. I clearly showed above that the selection check for edges is the first thing that should happen in the command method.
    Ie, there is no point in subjecting the user to enter trim options if the command is going to have to exit because they’ve not selected any edge path(s).
    And … re:
    #Checking if edges exist in the selection - returns false if no edges are selected and closes program
    … it’s too long (lines should be 80 characters of less,) and it doesn’t “close the program”, … it exits your command method which to the enduser appears as a command cancel.
    (I’m also not fond of omitting a space after the # and the beginning of a comment. (Readability)

  2. You’ve misplaced the creation of the model and selection reference. (Ie, the if selection.empty? ||… conditional will raise a NameError exception as the reference selection is not yet defined until lower in your code.)

  3. I’ve told you repeatedly, that the results from calling UI.inputbox must be checked for a boolean falsity, as this means the user has canceled the inputbox. (Refer to previous topic threads.)

  4. I carefully explained above that the UI command procs are a snapshot and should be executed last. But you’ve pasted them in first, before even a method that the proc calls is even defined.

How could my post be a solution if you do not follow the advice ?


Endusers are not likely to be reading your code, and if this will be a commerical extension then you’ll probably encrypt all the .rb files in your extension subfolder to .rbe format. (This means endusers couldn’t read anything in the files anyway.)

Contact and support information would go in a help file and/or the unencrypted extension registrar script that goes one folder up in the “Plugins” folder. You could also put this in the extension object’s description field and the users could see it in the Extension Manager.
But most coders would put it html help files that are distributed along with your extension.


Back to the creation of the menu item command.

You are not running the program from the menu item. (SketchUp is the application program.)
Your code is executing just what the proc attached to the menu item does, which is a command (consisting of methods.)

The following will not work, as NONE of the following local reference objects have been created at the extension module level within the menu item command proc:
height, thickness, quarter_round, entities, trimdef, transgroup, edges

    #Only loading UI object creation once
    if !@loaded
      plugins_menu = UI.menu("Plugins")
      submenu = plugins_menu.add_submenu("ProTrim")
      submenu.add_item("Create Base Trim") {create_base_trim(height, thickness, quarter_round, entities, trimdef, transgroup, edges)}
      @loaded = true
    end

Secondly, (again) please stop making us and yourself scroll horizontally to read code.
Blocks, literal Arrays and Hashes, and method argument lists can span multiple lines.
You put the opening delimiter on the first line, indent the internal lines, and put the non-indented closing delimiter on the last line.

calling_some_method(
  height, thickness, quarter_round, entities, trimdef, transgroup, edges
)

# A literal array:
  prompts = [
    "Trim Height:                        ",
    "Trim Thickness: ",
    "Quarter Round: "
  ]

# A block
undo(model,"Nifty Command") {
  # a bunch of statements or a long method call
}

I told you what kind of transformation you will need. It is actually 2 (or more) transformations multiplied together.

In this snippet … vector is the equivalent of the global vector Y_AXIS.
But is also a unit vector. SketchUp and it’s APIs use inches internally regardless of what the model’s display units are set to.
So your vector used as a translational transform will result in something being moved 1 inch in the Y direction. This is not really what you need.

Your code will need to split the edges array into path arrays, with the edges in start to end order.
Then for each path array, get the start vertex from the start edge, and get it’s point position, get the vector from ORIGIN to this point and use that for the translational part of the transform …

paths.each |path| do
  start_edge = path.first
  point = start_edge.start.position
  vector = ORIGIN.vector_to(point)
  vt = Geom::Transformation.translation(vector)
  # The followme tool needs the face to be perpendicular
  # to the starting edge of the extrusion path.
  path_vector = point.vector_to(start_edge.end.position)
  # face_vector is probably one of the global axis vectors or it's reverse
  face_vector = Y_AXIS
  rt = Geom::Transformation.rotation(
    ORIGIN,
    Z_AXIS,
    face_vector.angle_between(path_vector)
  )
  transgrp = rt * vt
  extrude_base_trim(trimdef, transgrp, path)
end

Again, this is a simple example for a possibility. It assumes all trim paths will be perpendicular to the Z_AXIS (parallel to the ground plane,) which might not always be true. Some trim runs diagonally up along stairwells, and not all edges where ceiling meets walls are horizontal for crown molding trim.

Also the face_vector is only known to you, as I don’t know in what plane you plan to draw your trim face profiles. If you draw them on the XY ground plane, then you’ll also need to rotate the instances 90 degrees to stand them up. So I’d suggest draw upon the XZ plane with the front face toward the screen. The front face’s normal vector pointing toward you, it’s reverse pointing in the Y_AXIS direction.

There may bee an issue with the vector.angle_between method. It may not give angles greater than 180 degrees.

The other thing that I’ve left out is the determination of which side on the edge path to position the face group on. Your code will need to check the faces that adjoin the edge for their normal vector which would indicate a direction pointing inward toward the center of the room. (Ie, the reversed vector from the wall faces normals would point inside the wall.)

Yes, it is an exercise in iteration. I believe it has been covered here in the past.
Regardless if you can find the topic, you iterate through the edge objects until you find the ones that have a start or end vertex used only by one edge.

I’ll leave that as an exercise for you to solve.

1 Like

Again … extension not program.

And as I’ve already explained you shouldn’t do this. Well you can, but you would have to implement a custom path selection tool. Others have done this in the past. But it just makes more work for you to do (and much more for you to learn) in the very beginning.

I strongly suggest just begin with the pre-pick path paradigm … and later on when your better at coding this, you can try to tackle a custom tool class. It is way beyond your abilities and I’ll not be attempting to help you with a tool at this time.

Also adding in a bunch more work for you, and making your extension less flexible. (It wouldn’t allow users to specify custom trim profiles. Which would mean users would look elsewhere for a trim extension.)

Again, tailor made for using component or group instances. Transformed to the start of the paths.

Easily done after the trim is extruded, just set the material upon the group enclosing the extruded trim.

This is butt-backward. As I’ve attempted to show in the examples above, the extension would create an empty group and either … insert a component instance into the group from a library and then explode it … or just draw the face geometry inside the group.

You must do it in this order because if you don’t the trim profile face and later it’s extrusion would interact with other loose geometry in whatever entities context is active.

Assigning the group to use a layer/tag is not a problem. It can be done at nay point after the creation of the group.

1 Like

I think I’ve cleaned it up a bit…

module CaydenWilson
  module ProTrim

    extend self

    #Ability to undo what the extension created
    def undo(model, opname, disable_ui = true)
      return unless block_given?
      model.start_operation(opname, disable_ui)
      yield
      model.commit_operation
    end

    def create_base_trim()
      #Checking if edges exist in the selection
      # returns false if no edges are selected and cancels command
      if selection.empty? || selection.grep(Sketchup::Edge).empty?
        UI.messagebox("Please select edges for your trim path(s) 
        and restart ProTrim.")
        return false
      end

      #Gathering user input
      prompts = ["Trim Height:                        ",
      "Trim Thickness: ", "Quarter Round: "]
      defaults = [5.0, 0.5,  "No"]
      list = ["", "", "Yes|No"]
      input = UI.inputbox(prompts, defaults, list, "ProTrim")
      return unless input
      height = input[0]
      thickness = input[1]
      quarter_round = input[2]

      #Loading in handles
      model = Sketchup.active_model
      @entities = model.active_entities
      selection = Sketchup.active_model.selection

      #Creating a group for the trim
      group = nil
      undo(mode, "Create Base Trim") do
        group = entities.add_group #added at model origin
        instance = group.entities.add_instance(trimdef, IDENTITY)
        group.transform!(transgroup)
        vector = Geom::Vector3d.new(0,0,1)
        translate = Geom::Transformation.translation[]
        instance.explode
        face = group.entities.grep(Sketchup::Face).first
        face.followme(edges)
      end
      return group

      #Determines if trim is built with or without quarter round
      if quarter_round == 'Yes'
        pts = [ [0, 0, 0], [thickness, 0, 0], [thickness+0.75,0,0],
        [thickness+0.75,0,0.75], [thickness,0,0.75], 
        [thickness, 0, height], [0, 0, height] ]
        # Add the face to the entities in the model
        face = entities.add_face(pts)
        connected = face.all_connected
        face.followme(selection.grep(Sketchup::Edge))
      elsif quarter_round == 'No'
        pts = [ [0, 0, 0], [thickness, 0, 0],
        [thickness, 0, height], [0, 0, height] ]
        # Add the face to the entities in the model
        face = entities.add_face(pts)
        connected = face.all_connected
        face.followme(selection.grep(Sketchup::Edge))
      end
    end
    #Only loading UI object creation once
    if !@loaded
      plugins_menu = UI.menu("Plugins")
      submenu = plugins_menu.add_submenu("ProTrim")
      submenu.add_item("Create Base Trim") {create_base_trim()}
      @loaded = true
    end
  end
end

For some reason my methods never are defined when I go to use the program…do you know why this may be?

I didn’t move the def undo(), should I?

  1. So the user will infer to select edges?
  2. I plan to add an option where users can save their own profiles into a folder in the extension, and they can select that profile from a drop down menu.
  3. I will add the group first…sorry about that…I’m used to creating a group after making geometry in SU
  4. At this point I’m just trying to get this to work so I can therefore build onto it…I’m really hoping it’ll run soon.

Again … see issue number 2 (above.)

Ugh! I give up Cayden.

I’ll point you again toward the free books. You MUST learn the basics of computer programming and then core Ruby syntax and code organization.

I am not willing to waste anymore time. A forum like this is good for explaining a specific issue or code challenge, but not good at all to teach basic programming. And I certainly do not have the time to type an entire book (although it seems I have over the past several topics.)

I’d suggest you go back through what I wrote in your various topic threads and cut and paste what I wrote into a kind of notebook.

It doesn’t matter where that method is located, as it will not get called until runtime.

You would have help files that tell them to pre-select edge paths, or you’d use a UI::Command object that has a tooltip and/or status text telling them to do a prepick. (The bonus is that this object can be used for a toolbar button as well as a menu item.)

What’s good for the goose, is good for the gander.

Which is why I suggested the pre-pick paradigm as you are a long way from being able to code a custom SketchUp selection tool.

Good luck to ya’.