Create Section Plane Programmatically?

ruby
sectionplanes

#1

I would like to do the following programmatically in Ruby:

  • select a section plane by choosing one with a specific name
  • create a group of section lines using Sketchup’s native “Create Group from Slice” feature or similar
  • name the group something predictable for later use/retrieval
  • fill all of the sub group object, presumably by looping through points and tracing or something

The API however, only seems to offer very remedial methods for dealing with Section Planes… getting.setting the name, determining if its active, etc.

I think I can handle most of the grunt work but does anyone know how I could access or at least simulate the Create Group from Slice method?


How to export model part above the ground plane only
#2

See previous topic thread …


Section Fill Control
How to break, exit, abort, fail, raise or whatever in a Script
#3

Thanks Dan but I was looking for “under the hood” info on Section Planes. Nonetheless, I muddled through so below is my Script.

The idea here was to do sort of a Skalp Lite that makes a very remedial Section Plane we can use to distinguish between existing (EXIST) and proposed (PROP) geometry in our floor plans. We do a lot of renovation work and we can use a dedicated section view to overlay views to that end.

Obviously, we are working in a controlled environment where we can define and anticipate the names of groups, scenes, layers, etc. Something commercial like Skalp has to deal with many possible scenarios so needs to be much more sophisticated but also HEAVY. We had problems with it bogging down. Staff was going crazy.

One slightly hacky thing I had to do was to create a temporary circle entity we use as the cutting plane. Ideally, I would have just selected the Section Plane and used that to run intersect_with but that did not seem to be working out. Input welcome.

Anyway, hopefully this helps someone with Section Plane.

require 'sketchup.rb'

# TODO:
# remove inner faces - if some areas are completely enclosed, we will get multiple Z Section faces
# abort, exit, break, etc. for various cancels or fails

# REQUIRES:
# layers with names DEMO|EXIST|PROP 
# groups in the model with instance names DEMO|EXIST|PROP that are the target objects to be cut
# section planes at different (at least one) levels with the name format "P" (for "plan") + "LEVEL_NUMBER" (P1,P2,P3 and so on)
# a folder containing styles named DEMO.style|EXIST.style|PROP.style
# a file name z_section_materials.skp containing materials DEMO|EXIST|PROP
# a component named "VIEWPORT" we use to set the view boundaries

module ZCode
module ZSectionModule

#################################################################

def self.z_section()

    # start undo tracking
    Sketchup.active_model.start_operation('ZSection', true)
    
    # ask user for target level
    prompts = ["PHASE:","LEVEL:"]
    defaults = ["DEMO","1"]
    list = ["DEMO|EXIST|PROP","1|2"]
    response = UI.inputbox(prompts, defaults, list, "Z SECTION")

    # catch "Cancel" button clicks
    if(response == false)
        return nil
    else
        $target_phase = response[0]
        $target_level = response[1]
    end
    
    #set current layer to default
    Sketchup.active_model.active_layer = Sketchup.active_model.layers["Layer0"]
    
    # make sure the target layer is on
    if(Sketchup.active_model.layers["#{$target_phase}"])
        Sketchup.active_model.layers["#{$target_phase}"].visible = true
    else
        UI.messagebox('There are no matching layers in the model')
        return nil
    end
    
    # get objects to be cut
    $group_to_be_sectioned = false
    Sketchup.active_model.entities.grep(Sketchup::Group).each{|g|
    
        # look for target group
        if(g.name == $target_phase)
            $group_to_be_sectioned = g
            if(g.hidden?)
                g.hidden = false
            end
            
        # or delete prior sections
        elsif(g.name == "#{$target_phase}#{$target_level}_CUT")
            g.erase!
        end
        
    }
    
    # catch missing group errors
    if($group_to_be_sectioned == false)
        UI.messagebox('There are no matching groups in the model')
        return nil
    end
    
    # get section plane which is always named P for "plan level"
    Sketchup.active_model.entities.grep(Sketchup::SectionPlane).each{|plane|
        if(plane.name == "P#{$target_level}")
            $su_section_plane = plane.get_plane
        end
    }

    # get section plane elevation relative to origin
    section_z = ORIGIN.project_to_plane($su_section_plane)

    # define parameters of a circle large enough to capture anything in the model
    $radius = [Sketchup.active_model.bounds.width,Sketchup.active_model.bounds.depth].max
    center_xy = Sketchup.active_model.bounds.center
    $center = [center_xy[0],center_xy[1],section_z[2]]

    # create a group to receive the section circle geometry
    z_section_circle = Sketchup.active_model.entities.add_group
    z_section_circle.name = "Z_SECTION_CIRCLE"
    z_section_circle.transformation = ORIGIN
    
    # add the section geometry to the group
    z_section_circle.entities.add_circle($center, Z_AXIS, $radius)
    
    # fill in the section circle from its points
    z_section_circle.entities.grep(Sketchup::Edge).each{|e|
        e.find_faces
    }
    
    # create a group within the appropriate target group to receive the section geometry
    $z_section_container = Sketchup.active_model.entities.add_group
    $z_section_container_trans = $z_section_container.transformation
    $z_section_container.name = "#{$target_phase}#{$target_level}_CUT"
    $z_section_container.transformation = ORIGIN
    $z_section_container.layer = "#{$target_phase}"
    
    # create section points and edges
    z_section_circle.entities.intersect_with(
        false,
        z_section_circle.transformation,
        $z_section_container,
        $z_section_container.transformation,
        false,
        $group_to_be_sectioned
    )

    # fill in the section plane from the section points
    $z_section_container.entities.grep(Sketchup::Edge).each{|e|
        e.find_faces
    }

    #remove inner faces
    $z_section_container.entities.grep(Sketchup::Face).each{|f|
        f.loops.each{|l|
            if(l.outer?)
                #outer_loop_array < l
            else
                #hole_array < l
                #l.face.erase!
            end
        }
    }
    #test_array = face_a.loops[i].edges - face_b.outer_loop.edges

    # load default materials from file
    material_path = File.join(ENV['HOME'],'Dropbox/support/_sketchup/plugins/z_section_materials.skp')
    Sketchup.active_model.definitions.load material_path

    #apply a material to the section face
    $z_section_container.material = $target_phase
    
    #delete the section circle
    z_section_circle.erase!
    
    # zoom to the extents of viewport
    z_zoom()
    
    # load styles from file and set current style
    style_path = File.join(ENV['HOME'],"Dropbox/support/_sketchup/styles/#{$target_phase}.style")
    Sketchup.active_model.styles.add_style(style_path, true) # true sets loaded style as active
    #Sketchup.active_model.styles.selected_style = Sketchup.active_model.styles["#{$target_phase}"] 
    
    # hide everything but the new $z_section_container
    Sketchup.active_model.entities.each{|all_groups|
        if(all_groups.is_a?(Sketchup::Group))
            if(all_groups.name != "#{$target_phase}#{$target_level}_CUT")
                all_groups.hidden = true
            end
        elsif(all_groups.is_a?(Sketchup::ComponentInstance))
            if(all_groups.definition.name != "VIEWPORT")
                all_groups.hidden = true
            end
        else
            all_groups.hidden = true
        end
    }

    # hide all non-target layers
    Sketchup.active_model.layers.each{|l|
        if((l.name != $target_phase)&&(l.name != "Layer0"))
            l.visible = false
        end
    }

    #create a new scene
    new_page = Sketchup.active_model.pages.add("#{$target_phase}#{$target_level}_CUT")
    
    #hide $z_section_container on all other scenes
    Sketchup.active_model.pages.each{|op|
        op.delay_time = 0
        if(new_page != op)
            Sketchup.active_model.pages.selected_page = op
            $z_section_container.hidden = true
            op.update(16)
        end
    }
    
    #delete the old scene
    Sketchup.active_model.pages.each{|p|
        p.delay_time = 0
        if((new_page != p)&&(p.name == "#{$target_phase}#{$target_level}_CUT"))
            Sketchup.active_model.pages.erase(p)
        end
    }
    
    #set the active page to the one just created
    Sketchup.active_model.pages.selected_page = Sketchup.active_model.pages["#{$target_phase}#{$target_level}_CUT"]
    
    # end undo tracking
    Sketchup.active_model.commit_operation

end #def

#################################################################

def self.z_zoom()

    # orient camera to plan view
    z_plan_camera()
    
    $viewport = false
    Sketchup.active_model.entities.grep(Sketchup::ComponentInstance).each{|comp_instance|
        if(comp_instance.definition.name == "VIEWPORT")
            $viewport = comp_instance
            if($viewport.definition.hidden?)
                $viewport.definition.hidden = false
            end
        end
    }
    if($viewport)
        #print "viewport found - zooming to viewport\n"
        Sketchup.active_model.active_view.zoom($viewport)
    else
        #print "no viewport found - zooming to section extents\n"
        Sketchup.active_model.active_view.zoom($z_section_container)
    end

end #def

#################################################################

def self.z_plan_camera()

    camera = Sketchup::Camera.new
    status = camera.perspective = false
    eye = [0,0,20000]
    camera.set(eye, ORIGIN, Y_AXIS)
    Sketchup.active_model.active_view.camera = camera

end #def

#################################################################

end # ZSectionModule
end # end of Module ZCode

# menus/toolbars ##############################################################

if( not file_loaded?("z_section.rb") )
    
    # create menu items
    UI.menu("Extensions").add_item("Z SECTION") {
        ZCode::ZSectionModule::z_section()
    }
    UI.menu("Extensions").add_item("Z ZOOM") {
        ZCode::ZSectionModule::z_zoom()
    }

    # create toolbar buttons
    toolbar = UI::Toolbar.new "Z_Tools"

    # set the path where icons are stored
    icon_path = File.join(ENV['HOME'],'Dropbox/support/_sketchup/plugins/')

    # create z_section button
    command_z_section = UI::Command.new("Z Section") {
      ZCode::ZSectionModule::z_section()
    }
    command_z_section.small_icon = "#{icon_path}z_section-small.png"
    command_z_section.large_icon = "#{icon_path}z_section-large.png"
    command_z_section.tooltip = "Z Section"
    command_z_section.status_bar_text = "Create or update Z Section"
    command_z_section.menu_text = "Z_Section"
    toolbar = toolbar.add_item command_z_section
    
    # create z_zoom button
    command_z_zoom = UI::Command.new("Z Zoom") {
      ZCode::ZSectionModule::z_zoom()
    }
    command_z_zoom.small_icon = "#{icon_path}z_zoom-small.png"
    command_z_zoom.large_icon = "#{icon_path}z_zoom-large.png"
    command_z_zoom.tooltip = "Z Zoom"
    command_z_zoom.status_bar_text = "Zoom to viewport in plan orientation"
    command_z_zoom.menu_text = "Z_Zoom"
    toolbar = toolbar.add_item command_z_zoom
    
    # show the Z_Tools toolbar
    toolbar.show
    
end
file_loaded("z_section.rb")

#4

Don’t take over patterns that you see elsewhere or in other languages (the others apparently do it like that, so it must be done like that) without understanding what it means and whether it is applicable (or needed) in the context where you want to use it.

This should be target_phase. Variables starting with a $ are global variables . In SketchUp extensions, global variables are almost “forbidden” because

  • they pollute the global namespace
  • they can have side effects on other extension using the same variables (it breaks other extensions)
  • they can break your own extension or cause hard to reproduce bugs if the behavior of your extension depends not only on the parameters that you explicitely pass to it, but also on global application state (values of global variables that were left over from previous invocations).

Don’t repeat yourself (DRY). Always save a result in a reference/variable when you want to reuse the exact same result (What if a method does not always return the same result? Can we be sure that always the same model is active?).

model = Sketchup.active_model
model.active_layer = model.layers["Layer0"]

Stylewise, you don’t really need parentheses around conditions (better: if response == false or if !response).
Otherwise I like your use of comments!


#5

Will do! Thanks so much for the tips!


#6

I believe if I have several defs in a single module that all need to access Sketchup.active_model I should used @model = Sketchup.active_model correct?


#7

@ variables are meant to capture “state”, that is values that are needed to remember the the history of the object so as to define how it will respond to future methods. The downside of overusing them is that you need to know when and why they were last set, and if they really aren’t essential to state that can be ambiguous. In particular they shouldn’t be used to pass values around the side instead of explicitly in method parameters. They also shouldn’t be used for optimization, eg to avoid calling Sketchup.active_model again on a different method or module.


#8

So to summarize (because I don’t understand all of that), I should call

model = Sketchup.active_model

inside each def correct?


#9
model = Sketchup.active_model
@faces = model.number_faces
# you may want to update the face count, but not change model...
def count_faces(model)
 @faces = model.number_faces
end

john


#10

Here’s an example of a legitimate use: if you set @model in an object’s initialize method and assume that the object is unalterably tied to that model for the rest of the object’s life. The danger in that case is to be certain that the object doesn’t outlive the active model. This especially true on Mac, where there can be more than one model open at the same time in the same session of SketchUp. What happens if the user switches to a different active model? Is there any way that now or in the future some method of the object could be called without switching @model to the new active model? You will need to keep this in mind while writing and maintaining your code.

In addition, @ variables are not “local”. That is, the interpreter has to look them up each time they are used. It isn’t clear that looking them up is all that much faster than invoking a library method to fetch the value such as Sketchup.active_model. You can be fooling yourself if you think this will make any detectable difference in the performance of your code!


#11

Not worried about performance, just code clarity and really scripting convenience. In PHP you can set a global and be confident that it will be available to you until it is killed or updated. Seems weird that it’s frowned upon in Ruby or at least SU Ruby.

My other thought is, why WOULDN’T you want your model var to ALWAYS re-update to the current model? If I’m not worried about performance, at least then you can be confident you are working on the thing sitting in front of the user.

I guess we’re getting in the weeds :grinning:


#12

Not a question of availability but of correctness. There is nothing that automatically manages the value of a variable to keep it current. So you have to be sure your code can never arrive at a place where it is not clear when the value of a shared variable (whether @ or global) was set. I don’t know PHP, but due to a lot of history with bugs due to unintended interaction with other code, global variables are considered to be a bad thing in most modern programming languages.


#13

In normal Ruby, a script runs alone in the Ruby process, completes, and Ruby is unloaded, so using globals is not as much a problem (but still can be if gems or libraries are loaded and used.)

In an embedded Ruby environment, the global objectspace is shared amonst ALL libraires classes and modules, whether they are Ruby Core or custom by individual companies and authors. It is not weird at all to expect and require all authors to write and execute ALL of their custom code WITHIN their own namespace modules, submodules and classes.

Note that many of the example extensions break this rule, but were written long ago before clashing of variables was understood by the community. Money has never been released to fix those examples. (They should be converted to use a state variable inside the extension’s namespace.)

You DO. But you can call the update within the parameter list that you pass to the method definitions (_not “defs”) … ie … using John’s example …

count_faces( Sketchup.active_model) if condition

… but if you will be calling a series of methods each using the active model, you can do as John showed, assigned a temporary local model variable, and use it in each subsequent method call.


#14

OK, I get that. It seems overly redundant/inefficient to be constantly referring to the active model, especially if it needs to be included as a def argument and (as you suggest in another post about exit) routines need to be broken up into very small chunks. That’s a lot of repetitive model calling in the argument space when ideally, it would just be there waiting for you to access.

The idea that a module is a little sandbox where I can’t screw up someone else’s scripts is great so perhaps there is . way that vars are at least “global to the module” or something?

Maybe the active model example is confusing. If I have a var called color = red and I want to refer to it in the 12 or so defs I need to create to make a complete routine, how can I “always within the module” have access to that color var? I seem to get scolded every time I use @ instance variables or (heaven forbid) @@ class variables that seem to do just that - always be available.

OK that sounds like I just don’t know enough about all the bad situations that may arise from using @ that now I’m thoroughly discouraged from using them!

I’ll also have to add that anything I am working on is for me alone and never will see the open world where some of these commercial level protocols are really important. Just some leisurely coding. I understand however that most Sages here are indeed trying to code some Fort Knox level stuff!


#15

YES. Module variables are @@vars. They are global to the module that they are defined in, however they must be declared (created) before they are ever used as a method parameter or within any expression being evaluated. So it is good to init them at the top of a module. (I usual do so just below local constant definitions.) Ex:

module SoAndSo

  # boolean module var:
  @@save_settings = true unless defined?(@@save_settings)

  # non-boolean module vars:
  @@max_pages ||= 5
  @@my_color  ||= Sketchup::Color.new("Red")
  @@my_color_name ||= "red"

end

This works as long as the module is not used as a mixin module to modify classes, because then the @@var(s) would become shared class variables, which have have a weird global inherited shared behavior that can trip up the unwary.

You should not be. There are two ways to use them.

  1. What you might be missing is that, … because a module is an instance of class Module it can have instance (@var) variables unique to that module alone. The Class class is a subclass of class Module, which adds instantiation and automatic initialization behavior (to classes) that can be used to create and initialize instance (@var) variables. But you can also do it manually by creating a method definition (call it “init” or “setup” or “reset”, whatever) that initializes all the @vars for a module, and then as the last statement in the module body before it’s end keyword, call this custom “init” method and all the @vars will now exist.

  2. The other way is to write a class definition (inside your plugin sub-module) which holds state including what model the class’ instance was instantiated for. In this class scenario it is common to have a @model instance variable referencing the model. Not common yet on Windows because only 1 model is ever open, but more common on Mac where multiple models can be open at the same time, and the user can switch between them whenever they wish. (There is now an observer callback that can be used to know when the user switches models on the Mac.)


It sounds to me like you have not read all the basic introductory “primers” on all the Core Ruby index pages. They are the pages in the “doc” subfolders here (from globals on down) …
http://ruby-doc.org/core-2.2.4/index.html


Also see my learning Ruby helpers for all the downloadable and online books and tutorials. The stuff is free. Read it. Print out chapters and leave in the toilet as study material.


Lastly, the SketchUp team has example extension consisting of files for you to study. You should do this as well. (They are called “Example Scripts” and “Utilities Tools” and are on their page in the Extension Warehouse.)


#16

OK Dan, sounds like I have a lot of homework to do! I’ll get to it. Thanks for your help!