Creating multiple holes using Ruby API causes slowness


#1

I am trying to write a Ruby program that needs to create several holes. I start with a wide flat box (a sheet of plywood) and create hundreds of holes in that box using the Group.subtract method.

This gets me the result I want (i.e. the right holes are getting created in the right places), but as I add more holes, the run time is increasing dramatically. Are there any tips or tricks that I can use to reduce the run time for my program?


#2

can you add a working sample of the code your using?

it a bit hard to guess…

john


#3

Here is my code. Just copy and paste into the Ruby Console:

class Playfield
    def initialize()
        @width = 20.25
        @depth = 42.0
        @thickness = 17.0/32.0
        @wall_thickness = 0.5
        @wall_height = 1.125
    end

    def rot(degrees)
        Geom::Transformation.rotation(Geom::Point3d.new, Geom::Vector3d.new(0, 0, 1), degrees.degrees)
    end

    def trans(x, y, z)
        Geom::Transformation.translation(Geom::Vector3d.new(x, y, z))
    end

    def left()
        Geom::Transformation.new
    end

    def right()
        Geom::Transformation.scaling(-1, 1, 1)
    end

    def create_base()
        create_floor()
        create_wall(0, 73.0/16.0, @wall_thickness, @depth - @wall_thickness)
        create_wall(0, @depth - @wall_thickness, @width, @depth)
        create_wall(@width - @wall_thickness, 17.0/4.0, @width, @depth - @wall_thickness)
        create_wall(@width - @wall_thickness - 11.0/8.0 - @wall_thickness, 7.5, @width - @wall_thickness - 11.0/8.0, 18.0)
    end

    def create_wall(x1, y1, x2, y2)
        # TODO: Create screw holes
        entities = Sketchup.active_model.active_entities.add_group().entities

        pt1 = [x1, y1, 0.0]
        pt2 = [x1, y2, 0.0]
        pt3 = [x2, y2, 0.0]
        pt4 = [x2, y1, 0.0]
        new_face = entities.add_face pt1, pt2, pt3, pt4
        new_face.pushpull -@wall_height
    end

    def create_floor()
        @floor = Sketchup.active_model.active_entities.add_group()
        entities = @floor.entities

        pt1 = [0.0, 0.0, 0.0]
        pt2 = [@width, 0.0, 0.0]
        pt3 = [@width, @depth, 0.0]
        pt4 = [0.0, @depth, 0.0]
        face = entities.add_face pt1, pt2, pt3, pt4
        face.pushpull @thickness
    end

    def add_hole_from_edges(hole, edges)
        face = hole.entities.add_face edges
        face.pushpull @thickness
        @floor = hole.subtract @floor
    end

    def create_circular_hole(t, r)
        hole = Sketchup.active_model.active_entities.add_group()
        entities = hole.entities

        centerpoint = Geom::Point3d.new
        # Create a circle perpendicular to the normal or Z axis
        normal = Geom::Vector3d.new 0,0,1
        edges = entities.add_circle t * centerpoint, normal, r

        add_hole_from_edges hole, edges
    end

    def join_arcs(entities, arcs)
        edges = []
        (0 .. arcs.length - 2).each do |i|
            edges += entities.add_edges arcs[i].last.end, arcs[i+1].first.start
            edges += arcs[i]
        end
        edges += entities.add_edges arcs.last.last.end, arcs.first.first.start
        edges += arcs.last
        return edges
    end

    def create_round_ended_hole(t, h, w)
        hole = Sketchup.active_model.active_entities.add_group()
        entities = hole.entities

        centerpoint = Geom::Point3d.new 0, w / 2, 0
        # Create a circle perpendicular to the normal or Z axis
        normal = Geom::Vector3d.new(0,0,1)
        xaxis = t * Geom::Vector3d.new(1,0,0)

        bottom_arc = entities.add_arc t * centerpoint, xaxis, normal, w/2.0, 180.0.degrees, 360.0.degrees
        top_arc = entities.add_arc t * trans(0, h - w, 0) * centerpoint, xaxis, normal, w/2.0, 0.0.degrees, 180.0.degrees

        add_hole_from_edges hole, join_arcs(entities, [bottom_arc, top_arc]) 
    end

    def add_ball_trough(t)
        hole = Sketchup.active_model.active_entities.add_group()
        entities = hole.entities

        t2 = t * rot(29.2)
        normal = Geom::Vector3d.new(0,0,1)
        xaxis = t2 * Geom::Vector3d.new(1,0,0)

        right_arc = entities.add_arc t2 * Geom::Point3d.new, xaxis, normal, 5.0/8.0, -90.0.degrees, 90.0.degrees
        top_arc = entities.add_arc t2 * Geom::Point3d.new(-33.0/4.0, 7.0/16.0, 0.0), xaxis, normal, 3.0/16.0, 90.0.degrees, 180.0.degrees
        bottom_arc = entities.add_arc t2 * Geom::Point3d.new(-33.0/4.0, -7.0/16.0, 0.0), xaxis, normal, 3.0/16.0, 180.0.degrees, 270.0.degrees

        add_hole_from_edges hole, join_arcs(entities, [right_arc, top_arc, bottom_arc])
    end

    def create_pilot_hole(t)
        # TODO: pilot holes should not be full depth
        create_circular_hole(t, 1.0/32)
    end

    def create_t_nut_hole(t)
        create_circular_hole(t, 7.0/64.0)
    end

    def create_lamp_hole(t)
        create_circular_hole(t, 0.25)
    end

    def install_component(t, component)
#        component = Sketchup.active_model.definitions.load "Z:\\home\\peter\\MEGA\\Pinball\\SketchUp\\" + component + ".skp"
#        Sketchup.active_model.active_entities.add_instance(component, t)
    end

    def add_post(t)
        create_pilot_hole(t)
        install_component(t, "Star_Post_1-1'16_-03-8319-13")
    end

    def add_rollover_switch(t)
        install_component(t * trans(0, 0, -@thickness), "Rollover_Switch_and_Bracket_A-12688")
        create_round_ended_hole(t, 25.0/16.0, 3.0/16.0)
        create_pilot_hole(t * trans(31.0/64.0, -29.0/64.0, 0))
        create_pilot_hole(t * trans(31.0/64.0, -53.0/64.0, 0))
    end

    def add_slingshot(t)
        create_round_ended_hole(t, 1.0, 1.0/2.0)
        create_circular_hole(t * trans(-1.0, 3.0/8.0, 0.0), 1.0/4.0)
        create_circular_hole(t * trans(1.0, 3.0/8.0, 0.0), 1.0/4.0)
    end

    def add_flipper_constellation(t, side)
        # Flipper drill template
        install_component(t * side * trans(0, 0, -@thickness), "Flipper\ Assy\ -\ Williams\ A-15205\ \(Left\)")
        [-17.0/32.0, -5.0/32.0, 89.0/32.0, 101.0/32.0].each do |x|
            [-17.0/8.0, 43.0/32.0].each do |y|
                create_pilot_hole(t * side * trans(x, y, 0.0))
            end
        end

        # Flipper bat
        create_circular_hole(t * side, 0.25)
        install_component(t * side * rot(145.0), "flipper")

        # Inlane guide
        t2 = t * side * trans(-2.137, 1.563, 0.0) * rot(325)
        create_pilot_hole(t2)
        create_pilot_hole(t2 * trans(-13.0/8.0, 0, 0))
        create_pilot_hole(t2 * trans(13.0/8.0, 0, 0))
        create_pilot_hole(t2 * trans(-131.0/32.0, 89.0/32.0, 0)) # TODO: This hole isn't perfect
        install_component(t2 * trans(0.0, 0.0, 0.53125), "Inlane_williams_plastic")

        # Inlane switch
        add_rollover_switch(t * side * trans(-3.142, 4.406, 0) * side)

        # Outlane switch
        add_rollover_switch(t * side * trans(-4.590, 4.406, 0) * side)

        t3 = t * side * trans(-1.654, 5.030, 0.0) * rot(291.2)
        add_slingshot(t3 * side)
        add_post(t3 * trans(1.919, 0.200, 0.0))
        add_post(t3 * trans(0.596, -0.696, 0.0))
        add_post(t3 * trans(-0.396, -0.425, 0.0))
        add_post(t3 * trans(-1.884, 0.339, 0.0))
    end

    def add_guide(t)
        install_component(t * trans(0, 0, 1.25) * rot(90), "Lane_Guide_03-8318-25")

        create_lamp_hole(t)
        add_post(t * trans(0, 1.25/2, 0))
        add_post(t * trans(0, -1.25/2, 0))
    end

    def add_pop_bumper(t)
        # Ring and rod holes
        create_circular_hole(t * trans(11.0/16.0, 0.0, 0.0), 3.0/16.0)
        create_circular_hole(t * trans(-11.0/16.0, 0.0, 0.0), 3.0/16.0)
        
        # Skirt shaft hole
        create_circular_hole(t, 11.0/32.0)

        # Lamp lead holes
        t2 = t * rot(45)
        create_circular_hole(t2 * trans(0.0, 11.0/32.0, 0.0), 3.0/16.0)
        create_circular_hole(t2 * trans(0.0, -11.0/32.0, 0.0), 3.0/16.0)

        # Coil bracket (hammer screw) holes
        create_circular_hole(t * trans(0.0, 17.0/16.0, 0.0), 3.0/64.0)
        create_circular_hole(t * trans(1.0, 7.0/16.0, 0.0), 3.0/64.0)
        create_circular_hole(t * trans(-1.0, 7.0/16.0, 0.0), 3.0/64.0)

        # Mounting pilot holes
        create_pilot_hole(t * trans(5.0/16.0, 5.0/16.0, 0.0))
        create_pilot_hole(t * trans(-5.0/16.0, -5.0/16.0, 0.0))

        # Spoon switch bracket holes
        create_pilot_hole(t * trans(-3.0/8.0, -29.0/16.0, 0))
        create_pilot_hole(t * trans(-3.0/8.0, -35.0/16.0, 0))

        # Drill template
        install_component(t * trans(0, 0, -@thickness), "Pop\ Bumper\ Assembly\ Williams\ Bally")

        # Pop bumper
        install_component(t * rot(90) * trans(0, 0, 1.0/16.0), "pop-bumper")
    end
end

Sketchup.active_model.active_entities.each { |it| Sketchup.active_model.active_entities.erase_entities it }

playfield = Playfield.new()
playfield.create_base()

playfield.add_ball_trough(Geom::Transformation.translation(Geom::Vector3d.new(281.0/16.0, 47.0/8.0, 0)))

playfield.add_flipper_constellation Geom::Transformation.translation(Geom::Vector3d.new(5.635, 6.701, 0)), playfield.left
playfield.add_flipper_constellation Geom::Transformation.translation(Geom::Vector3d.new(12.479, 6.701, 0)), playfield.right

x = 36.0
(0..2).each do
    playfield.add_guide Geom::Transformation.translation(Geom::Vector3d.new(x/16.0, 585.0/16.0, 0))
    x += 17
    playfield.add_rollover_switch Geom::Transformation.translation(Geom::Vector3d.new(x/16.0, 573.0/16.0, 0))
    x += 17
end
playfield.add_guide Geom::Transformation.translation(Geom::Vector3d.new(x/16.0, 585.0/16.0, 0))

playfield.add_pop_bumper Geom::Transformation.translation(Geom::Vector3d.new(39.0/16.0, 525.0/16.0, 0))
playfield.add_pop_bumper Geom::Transformation.translation(Geom::Vector3d.new(114.0/16.0, 522.0/16.0, 0))
playfield.add_pop_bumper Geom::Transformation.translation(Geom::Vector3d.new(72.0/16.0, 467.0/16.0, 0))

Sketchup.send_action("viewTop:")
Sketchup.send_action("viewZoomExtents:")

If I comment out the body of the add_hole_from_edges method, the code runs much faster, but creates no holes…

The goal of this program is to produce model of a pinball playfield that I can send to a CNC mill and produce an actual playfield.


#4

it’s running at 129.545646 seconds on my mac [but could be made quicker]…

oddly, one of the favourite things I have ever made was a bespoke pinball machine demonstrating the human digestive system for a children’s museum…

the ball was bounced around the in the lower intestine before spiralling down into a toilet pan when it got past the flippers…

I’ll have a look at the code over the weekend if I get time…

john


#5

Well the first issue is that none of your geometry creation is wrapped in an undo operation (which has the UI refresh suppressed.)

See:


#6

Is there some suggestion about how or where I should use these undo methods? I tried wrapping the script with a start/commit but that only dramatically increased the run time.


#7

I’ve done some analysis by commenting out certain parts of the code. If I comment out everything after the ball trough. the code runs in 0.02s. If I add one flipper constellation (it doesn’t matter which) the run time increases to about 4 seconds. If I add the second constellation the run time increases to 26 seconds.

I really think that the run time increases disproportionately with the number of individual subtract operations. Is there any way that I could ‘collect’ all the holes in a data structure and perform only one subtract?


#8

Did you try by setting the second argument (disable_ui) to true?


#9

See: http://www.thomthom.net/thoughts/2014/06/sketchup-extensions-ux-dont-break-undo/

Basically the start and commit are block opener and block closer calls used like a begin and end. (I personally always indent the code in between to show it’s a block of code wrapped within one undo operation.)

Only actual model entity creation (or modification) code statements need be wrapped within the undo block. Often at the beginning of methods, there may be code that runs to determine IF some model geometry needs to be changed or created. When not, a conditional expression causes a “bailout” (early return) from the method, before a call to a block of undoable code.


#10

So, I carved the code down a bit, so now it only builds the top left most quadrant (pop bumpers and lane guides) as a test.

The code without calls to the undo methods runs in 26 seconds.

Then I added:

Sketchup.active_model.start_operation('Draw item', true)

before, and

Sketchup.active_model.commit_operation

after drawing each item (the floor, each pop bumper, each post, each lane guide, and each rollover switch).

and the runtime went up to 47 seconds.

I am not worried about being able to undo anything. The purpose of the code is to generate a playfield based on a ruby description.

I am pretty sure it has to do with the holes, and the work that SketchUp has to do in order to figure out their geometry. If I comment out the subtract operation, the code runs in 0.23 of a second.


#11

The start and commit operation is mean to be at the beginning and end of your bulk operation. The disable_ui flag will suppress a lot of the notifications that goes to the UI that would otherwise slow things down.