End of input error

There’s no end for the if statement near the top.

1 Like

move the handles into the module?

john

1 Like

Now I’m getting variable errors when I run it…any idea? Inputbox appears and data is able to be imputed, but once ok is clicked I get these errors

Error: #<NameError: undefined local variable or method `sketchup_extension’ for CaydenWilson::ProTrim:Module>

:22:in `' :10:in `' :9:in `' SketchUp:1:in `eval'

Doesn’t seem to be an issue…but I’ll try it! :wink:

UI.Notification is designed to be used with reference to a SketchupExtension object. The first argument to UI.Notification.new (named sketchup_extension) must be a reference to a SketchupExtension object that your code has created and registered with the Sketchup engine. Consult the API documentation about SketchupExtension.

Side note: to avoid the forum markup engine mangling error messages, put triple backtick on the lines before and after where you paste the error messages. The < and > characters otherwise are taken as markup tags and create a mess.

2 Likes

Perfect! Didn’t know I should do that.

By the way, I agree with @john_drivenupthewall that the “handles” should be inside the module, not at the global level. You never know when global variables will collide with ones someone else defined!

Best they would be in a method. You won’t be able to access these local variables anyways from other methods.

Create a method to wrap all orphan code.

module CaydenWilson
  module ProTrim
    def init
      model = Sketchup.active_model # Use spaces around operators
      selection = model.selection # Use the same model reference
      # …
      prompts = ["Trim Height:            ", "Trim Thickness:", "Quarter Round:", "R:", "G:", "B:"]
      # …
      notification.show
    end
    # …
    self.init # Instead of calling it here, you can do this in a menu item's command handler.
  end
end

Then call the method.

Now you have more flexibility and control to run this code when you need it. You probably don’t want to force this notification onto the user when SketchUp starts.

1 Like

Yes, and I’ve told Cayden previously not to use UI::Notification objects.
They are not designed to be used for workflow prompts (and their display time cannot be controlled [yet.])
Cayden should use normal modal messageboxes instead.

(Cayden, FYI, modal means that the messagebox or dialog seizes the focus and does not release it until the user dismisses the window.)


With regard to the SketchupExtension object, it’s registrar file is a separate file that goes in the “Plugins” folder. All your extensions other files go in a subfolder that has the same name as the registrar file.
If the extension registrar file is "CaydenWilson_ProTrim.rb" then your extension subfolder must be named "CaydenWilson_ProTrim".

The handles are not an issue because your code is not using them.

The most prime rule for coding in a shared ObjectSpace is that ALL YOUR CODE MUST be within your toplevel namespace module (ie, the CaydenWilson module in the above example,) and any code specific to a CERTAIN EXTENSION should ALL be within that extension’s submodule (in the example the CaydenWilson::ProTrim submodule.)

Even the lonely …

require "sketchup.rb"

… method call can be within your modules. It is a frivolous call because the SketchUp engine loads it (and the other "Tools" subfolder files "extensions.rb" and "langhandler.rb") during the startup cycle ever since the release of SketchUp 2014. These loads happen before any extensions ever begin to load.

You do not “run” code like this (ie, a module definition) repeatedly in an event-driven programming environment. Instead the module is loaded at startup and waits for an event to fire off the display of the inputbox.

The event will be the user either choosing a command from a menu or clicking a toolbar button. So this means you need a “eval once” block at the bottom of your module that creates the UI elements (your extension’s submenu and /or toolbar and the command objects that will be attached to the menu items and toolbar buttons.)

    if !@loaded
      submenu = UI.menu("Extensions").add_submenu("ProTrim")
      cmd = UI::Command.new("Build Trim") { build_trim_command() }
      submenu.add_item(cmd)
      # Set the @loaded var to true so this block is only evaluated ONCE:
      @loaded = true
    end

Such a block that evaluates only once, will prevent multiple submenus, menu items and toolbars, etc., from getting created, if you tweak your code and need to reload it during development.

Reference class documentation for:


So as shown in the eval once block above, you need a command method that will be fired by the user when they start your command. I called it "build_trim_command()"but you can name it what you like.

Put the inputbox code inside this method.

Also I’ve told you repeatedly, you must always check that the return value from the input box is “truthy” (ie only two things in Ruby are falsehoods and these are false and nil. Everything else evals booleanwise as a truth.)
So this means that if the user cancels the inpubox then the variable input will eval as a falsehood.
If the user enters values (or accepts the default values) and clicks OK, then input will be an array instance and eval as a truth, so your subsequent code can go ahead and treat it as an array of values.
A simple one liner inserted just after the call to UI.inpubox would be …

return unless input

It tests that input evals as true (and therefore cannot be false or nil,) and so must be an array instance. If the user has cancelled the inputbox, your code should assume the user wishes to cancel the command, and so the return statement will exit the method, and nothing will happen.

Some extensions do display a “Command Cancelled” messagebox, just so the user knows their cancel has worked and they know nothing in they model changed. (Your code, your choice.)


Lastly, in order for your methods to call each other without qualification, you need to extend the module with itself. Put this statement at the top of the module.

extend self
1 Like

Sorry about the notification being in there…I edited the file and forgot to save it, and then didn’t fix that. I think I’ve fixed the code to what you’ve been saying…I just need to figure out how to execute the face build/follow me if the user clicks OK in the messagebox. Also, I keep getting an error on my toolbar section…and my icons do not appear on my toolbar or as an option to add to my toolbar. Thanks for your help!

module CaydenWilson
  module ProTrim

    extend self

    def init
      #Loading in handles
      model=Sketchup.active_model
      selection=Sketchup.active_model.selection
      entities=Sketchup.active_model.entities
      materials=Sketchup.active_model.materials
      layer_array=Sketchup.active_model.layers

      require 'sketchup.rb'
      require 'extensions.rb'

      pro_trim=SketchupExtension.new('ProTrim", "ProTrim/base.rb"')
      pro_trim.version='1.0'
      Sketchup.register_extension(pro_trim, true)

      self.init
    end

    def get_points(axis, height, thickness)
      case axis
      when X_AXIS
        [ [0,0,0], [0,0,height], [thickness,0,0], [thickness,0,height] ]
      when Y_AXIS
        [ [0,0,0], [0,0,height], [0,thickness,0], [0,thickness,height] ]
      end
    end

    def create_trim_material(materials)
      trim_material=Sketchup.active_model.materials.add "Trim"
      trim_material.color=[red, green, blue]
      trim_group.material=trim_material
    end

    def build_trim(axis, selection, height, thickness)
      #Input to gather data for trim
      prompts=["Trim Height:            ", "Trim Thickness:", "Quarter Round:", "R:", "G:", "B:"]
      defaults=[5.0,0.5,"No",255.0,255.0,255.0]
      list=["","","Yes|No","","",""]
      input=UI.inputbox prompts, defaults, list, "ProTrim"
      return unless input
        height,thickness,quarter_round,red,green,blue=input
      if quarter_round=='Yes'
        quarter_round_width=0.75
        quarter_round_height=0.75
      end
      message="Select edges for your trim path."
      result=UI.messagebox(message, MB_OKCANCEL)
      if result==IDCANCEL
        return
      end
      pts=get_points(axis, height, thickness)
      #Creating a face using array
      face=entities.add_face(pts)
      edges = face.edges
      connected=face.all_connected
      face.back_material = "Trim"
      material = trim.back_material
      face.material = trim
      #Selecting edges (path) to extrude on
      face.followme( selection.grep(Sketchup::Edge) )
    end

    def create_trim_layer(layer_array, entities)
      new_layer=model.layers.add ("Trim")
      model.active_layer=new_layer
      name=trim
      visable=True
    end
    if !@loaded
      submenu = UI.menu("Extensions").add_submenu("ProTrim")
      toolbar=UI::Toolbak.new "ProTrim"
      cmd = UI::Command.new("Build Trim") { init() }
      cmd.small_icon="IconSmall.png"
      cmd.large_icon="IconLarge.png"
      cmd.menu_text="ProTrim"
      toolbar.add_item cmd
      toolbar.show
      submenu.add_item(cmd)
      # Set the @loaded var to true so this block is only evaluated ONCE:
      @loaded = true
    end
  end
end

Once again you are listening … reread what was written.

1 Like

There is NO class named UI::Toolbak … it’s UI::Toolbar.

You may need to use absolute paths to the icon files, or relative paths from the Plugins folder. I can’t remember which.

The Ruby global function __FILE__ returns the absolute path to the file it is called from.
The Ruby global function __dir__ returns the absolute directory path where the file it’s called from is located.
So if the icon files are in the same folder …

cmd.small_icon = File.join(__dir__,"IconSmall.png")

If they were in an "images" subfolder from where the file is …

cmd.small_icon = File.join(__dir__,"images","IconSmall.png")

cmd = UI::Command.new("Build Trim") { init() }

Andreas gave you some general advice that does not apply in this case.
You don’t need anything in the init() method. Delete it.

And (as said above) the following …

module CaydenWilson
  module ProTrim
    pro_trim = SketchupExtension.new('ProTrim", "CaydenWilson_ProTrim/base.rb"')
    pro_trim.version = '1.0'
    pro_trim.description = 'Professioanl Trim Building extension.'
    pro_trim.creator = 'Cayden Wilson'
    pro_trim.copyright = '2020'
    Sketchup.register_extension(pro_trim, true)
  end
end

… all goes into a registrar file named "CaydenWilson_ProTrim.rb" outside your extension subfolder, which must be named "CaydenWilson_ProTrim".

The registrar file gets installed into the “Plugins” folder, and is automatically run by SketchUp at startup to register your extension.

1 Like

Do not insert spaces between a method name and it’s argument list. This will usually cause a warning.

There is no good reason for Ruby code to change the active layer (tag). ALL geometric primitives should always be untagged (associated with “Layer0”.) Only the trim groups or components should be assigned to other layer/tags.


Also you are not helping the interpreter or US to read your code by omitting spaces before and after operators.

1 Like

Once the method is called you have infinite recursion… but it is never called.

1 Like

I fixed all the spaces for better readability…Its been a bad habit of mine not using them previously. Also, I am going to look into creating a group for the trim, which has always been my intention.

When you say I don’t need anything in the init method, do you mean to delete the handles or do you mean to just remove the init method?

Yes remove the whole method. Why would you need a method if it’s not needed or it’s empty of code ?

1 Like

WHoops! I made an error and left out the word no from the previous post. (Now fixed.)

1 Like

I’m still working on the group for the trim, but does this look better? I also have fixed the folder structure and naming, created the register file as shown, and placed icons in a “Icons” folder. Thanks!

module CaydenWilson
  module ProTrim

    extend self

    #Loading in handles
    model = Sketchup.active_model
    selection = Sketchup.active_model.selection
    entities = Sketchup.active_model.entities
    materials = Sketchup.active_model.materials
    layer_array = Sketchup.active_model.layers

    require 'sketchup.rb'
    require 'extensions.rb'

    def get_points(axis, height, thickness)
      case axis
      when X_AXIS
        [ [0,0,0], [0,0,height], [thickness,0,0], [thickness,0,height] ]
      when Y_AXIS
        [ [0,0,0], [0,0,height], [0,thickness,0], [0,thickness,height] ]
      end
    end

    def create_trim_material(materials)
      trim_material = Sketchup.active_model.materials.add "Trim"
      trim_material.color = [red, green, blue]
      trim_group.material = trim_material
    end

    def build_trim(axis, selection, height, thickness)
      #Input to gather data for trim
      prompts = ["Trim Height:            ", "Trim Thickness:", "Quarter Round:", "R:", "G:", "B:"]
      defaults = [5.0,0.5,"No",255.0,255.0,255.0]
      list = ["","","Yes|No","","",""]
      input = UI.inputbox prompts, defaults, list, "ProTrim"
      return unless input
        height,thickness,quarter_round,red,green,blue=input
      if quarter_round == 'Yes'
        quarter_round_width = 0.75
        quarter_round_height = 0.75
      end
      message = "Select edges for your trim path."
      result = UI.messagebox(message, MB_OKCANCEL)
      if result == IDCANCEL
        return
      end
      pts=get_points(axis, height, thickness)
      #Creating a face using array
      face = entities.add_face(pts)
      edges = face.edges
      connected = face.all_connected
      face.back_material = "Trim"
      material = trim.back_material
      face.material = trim
      #Selecting edges (path) to extrude on
      face.followme( selection.grep(Sketchup::Edge) )
    end

    def create_trim_layer(layer_array, entities)
      new_layer = model.layers.add("Trim")
      model.active_layer = new_layer
      name = trim
      visable = True
    end
    if !@loaded
      submenu = UI.menu("Extensions").add_submenu("ProTrim")
      toolbar=UI::Toolbar.new "ProTrim"
      cmd = UI::Command.new("Build Trim") { init() }
      cmd.small_icon = File.join(__dir__, "Icons", "IconSmall.png")
      cmd.large_icon = File.join(__dir__, "Icons", "IconLarge.png")
      cmd.menu_text = "ProTrim"
      toolbar.add_item cmd
      toolbar.show
      submenu.add_item(cmd)
      # Set the @loaded var to true so this block is only evaluated ONCE:
      @loaded = true
    end
  end
end

Firstly, many of the examples you might see, are very old and violate good programming style rules that have been accepted since.)

It looks a little better, but you’ve still missed a few places without spaces around =.

(This line [above] is also indented, but should not be.)


Secondly, whenever you call a method upon an object with dot notation, you should use parenthesis around the argument list. (The call to the UI::Toolbar.new constructor [above] is a prime example where parenthesis should always be used.)

Only global methods from module Kernel, BasicObject, and Object are allowed generally to omit parenthesis if readability permits.

It has also been proposed that private method calls of classes Module and Class, from within a module or class definition where the receiver is self or implied to be the module or class itself, can also omit parenthesis.

(This rule is referred to as “methods having keyword status” [reserved word == keyword].)
However, in my opinion, you should never be ridiculed for using parenthesis even when allowed to omit them. (The foremost Ruby coding style guide will take the opposite tack and will produce rule violations if parenthesis are used when they can be omitted.)


At the module level, insert a blank line before blocks like this and methods (or other blocks.)


spacing for readability is paramount for argument lists, arrays, hashes and expressions.
(Arrays or argument lists of numbers tend to blur together at the end of a long day.) Ie:

defaults = [5.0, 0.5, "No", 255.0, 255.0, 255.0]

As said above … You do not need to require dependent files, if this file does not use them.
And since SketchUp 2014, the SketchUp load cycle will load the 3 files from it’s "Tools" subfolder, and your code does not really need to require them anyway. IE, the calls …

    require 'sketchup.rb'
    require 'extensions.rb'

… are not explicitly needed because they will always already be loaded by the time your module is being loaded, but more so because this file doesn’t call any feature defined by those files.


And, … the code that was in the body of the init() method, again is not being used, so delete …

    #Loading in handles
    model = Sketchup.active_model
    selection = Sketchup.active_model.selection
    entities = Sketchup.active_model.entities
    materials = Sketchup.active_model.materials
    layer_array = Sketchup.active_model.layers

Local variables at the module level are unusable by the module’s methods anyway.
They’d need to be @@vars or @vars in order for any of the methods to access them.

But such assignments would need to be dynamic, because users close models and open other models. So assigning a set of “@handles” would be done at the last second, probably as the first step in a command, so that your code is sure that the handles are for objects in the active model.

A note about entities. Always prefer to work with the active_entities rather than assume the user always wants to work at the top level of the model. Ie, instead of …

    @entities = Sketchup.active_model.entities

… use …

    @entities = Sketchup.active_model.active_entities

… should be …

      result = UI.messagebox(message, MB_OKCANCEL)
      return if result == IDCANCEL

… or even better

      result = UI.messagebox(message, MB_OKCANCEL)
      return unless result == IDOK

This is called a statement with a conditional expression in modifier position.
Note also that the return statement can have an expression to return immediately following the return. Ex:

      result = UI.messagebox(message, MB_OKCANCEL)
      return false unless result == IDOK

As said above, don’t change the USER’s active layer/tag. Ruby API code doesn’t need to do this anyway as code would likely always be creating geometry inside a group or component’s definition’s entities collection (associated with “Layer0”/“Untagged”,) then creating an instance in the model and afterward setting the instance’s layer/tag.

Besides this workflow, changing the active Layer/Tag will fire off any LayersObserver objects that are active by other extensions. This is not something your code needs to have happening.

In addition the 3 local variable assignments in the method are unneeded of do nothing. This method is really a one-liner …

    # Create (if necessary) and return a reference to the "Trim" layer/tag.
    def create_trim_layer(model, tagname = 'Trim')
      model.layers[tagname] || model.layers.add(tagname)
    end

… this above example allows localized language layer/tag names to be passed into the method.
(Not all users run SketchUp in English.)