├── .editorconfig ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── examples ├── 01_hello_cube │ ├── README.md │ ├── ex_hello_cube.rb │ └── ex_hello_cube │ │ └── main.rb ├── 02_custom_tool │ ├── README.md │ ├── ex_custom_tool.rb │ └── ex_custom_tool │ │ └── main.rb ├── 03_press_drag_release_tool │ ├── README.md │ ├── ex_press_drag_release_tool.rb │ └── ex_press_drag_release_tool │ │ └── main.rb ├── 04_vcb_tool │ ├── README.md │ ├── ex_vcb_tool.rb │ └── ex_vcb_tool │ │ └── main.rb ├── 05_tool_inference_lock │ ├── README.md │ ├── ex_tool_inference_lock.rb │ └── ex_tool_inference_lock │ │ ├── inference_lock_helper.rb │ │ └── main.rb ├── 99_hello_donut │ ├── README.md │ ├── ex_hello_donut.rb │ └── ex_hello_donut │ │ └── main.rb ├── 99_hello_sphere │ ├── README.md │ ├── ex_hello_sphere.rb │ └── ex_hello_sphere │ │ └── main.rb ├── 99_in_tool_selection │ ├── README.md │ ├── ex_in_tool_selection.rb │ └── ex_in_tool_selection │ │ ├── inference_lock_helper.rb │ │ └── main.rb ├── 99_license │ ├── README.md │ ├── ex_hello_license.rb │ └── ex_hello_license │ │ ├── extension_info.txt │ │ └── main.rb └── 99_sphere_tool │ ├── README.md │ ├── ex_sphere_tool.rb │ └── ex_sphere_tool │ └── main.rb ├── load_tutorials.rb └── tutorials ├── 01_hello_cube ├── README.md ├── tut_hello_cube.rb └── tut_hello_cube │ └── main.rb └── 02_custom_tool ├── README.md ├── tut_custom_tool.rb └── tut_custom_tool └── main.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # If your editor doesn't support editor configs out of the box, there usually 4 | # exists an extension for it. Short of that, configure your editor accordingly. 5 | 6 | root = true 7 | 8 | [*] 9 | indent_style = space 10 | indent_size = 2 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | max_line_length = 80 14 | 15 | [*.{js,rb}] 16 | charset = utf-8 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Listen for rdebug-ide", 10 | "type": "Ruby", 11 | "request": "attach", 12 | "cwd": "${workspaceRoot}", 13 | "remoteHost": "127.0.0.1", 14 | "remotePort": "7000", 15 | "remoteWorkspaceRoot": "${workspaceRoot}" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Debug in SketchUp 2018", 8 | "type": "shell", 9 | "command": "skippy", 10 | "args": [ 11 | "sketchup:debug", 12 | "2018" 13 | ], 14 | "problemMatcher": [] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Trimble Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started & Tutorials 2 | 3 | ## Example Extensions 4 | 5 | We often find that we need a level of training that goes well beyond the basics, 6 | but is more informative than just reading the API documentation. Look no 7 | further! Check out our Example Extensions section for examples of fully 8 | functioning SketchUp Extensions, complete with comments and helpful hints. 9 | 10 | ### How Do I Use These Tutorials? 11 | 12 | Hopefully you can use these examples however you want. The Example code is 13 | available 3 different ways. You can: 14 | 15 | 1. Read the [Example Extensions tutorials on the SketchUp Developer website](https://developer.sketchup.com/developers/example-extensions). 16 | Read through the step by step tutorial of the code and comments and try to 17 | follow along by building the same extension yourself. Use these examples to 18 | help learn better SketchUp API usage. 19 | 2. Fork the [fully commented examples from Github](https://github.com/SketchUp/sketchup-ruby-api-tutorials/tree/main/tutorials). 20 | Get the code right on your machine, with all the comments for easy access. 21 | Use it as a quick reference to comments, code snippets, etc. 22 | 3. Fork the [non-commented version from Github](https://github.com/SketchUp/sketchup-ruby-api-tutorials/tree/main/examples). 23 | This is handy for people who just want to look at the code quickly and move 24 | on. You can copy and paste chunks of code easily from the non-commented 25 | samples without the verbose comments getting in the way. 26 | 27 | Depending what you are trying to achieve, there is a method to get there easily. 28 | Just get all the code at once, and pull out what you need. Or follow along line 29 | by line, tutorial style. The end goal is for everyone to gain access to solid 30 | code examples that help improve their SketchUp extensions. 31 | 32 | ## Loading Directly from the Repository 33 | 34 | If you clone this repository to your computer you can load the files directly 35 | from where you cloned them using a proxy loading script: 36 | 37 | ```ruby 38 | # Create a file in your Plugins folder with these lines: 39 | $LOAD_PATH << 'some\path\to\sketchup-ruby-api-tutorials' 40 | require 'load_tutorials.rb' 41 | ``` 42 | 43 | That snippet will take care of loading the examples and tutorials. 44 | 45 | If you have examples of your own that you think might be useful open a Pull 46 | Request. Follow the pattern of the existing examples. 47 | 48 | You can use `Examples.reload` while you work to reload all the files. 49 | 50 | # Development Guides 51 | 52 | Also make sure to check out the [Wiki section](https://github.com/SketchUp/sketchup-ruby-api-tutorials/wiki) 53 | for guides on how to setup your project for extension development, IDE setup 54 | and more. 55 | -------------------------------------------------------------------------------- /examples/01_hello_cube/README.md: -------------------------------------------------------------------------------- 1 | # Hello Cube 2 | 3 | This demonstrates the basic structure of a SketchUp extension. 4 | 5 | * Registers an extension with SketchUp. 6 | * Adds menu item to trigger a command. 7 | * Creates a group with a cube. 8 | 9 | A detailed tutorial of this example is located under tutorials/01_hello_cube. 10 | -------------------------------------------------------------------------------- /examples/01_hello_cube/ex_hello_cube.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'extensions.rb' 6 | 7 | module Examples 8 | module HelloCube 9 | 10 | unless file_loaded?(__FILE__) 11 | ex = SketchupExtension.new('Hello Cube', 'ex_hello_cube/main') 12 | ex.description = 'SketchUp Ruby API example creating a cube.' 13 | ex.version = '1.0.0' 14 | ex.copyright = 'Trimble Navigations © 2016' 15 | ex.creator = 'SketchUp' 16 | Sketchup.register_extension(ex, true) 17 | file_loaded(__FILE__) 18 | end 19 | 20 | end # module HelloCube 21 | end # module Examples 22 | -------------------------------------------------------------------------------- /examples/01_hello_cube/ex_hello_cube/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module HelloCube 8 | 9 | def self.create_cube 10 | model = Sketchup.active_model 11 | model.start_operation('Create Cube', true) 12 | group = model.active_entities.add_group 13 | entities = group.entities 14 | points = [ 15 | Geom::Point3d.new(0, 0, 0), 16 | Geom::Point3d.new(1.m, 0, 0), 17 | Geom::Point3d.new(1.m, 1.m, 0), 18 | Geom::Point3d.new(0, 1.m, 0) 19 | ] 20 | face = entities.add_face(points) 21 | face.pushpull(-1.m) 22 | model.commit_operation 23 | end 24 | 25 | unless file_loaded?(__FILE__) 26 | menu = UI.menu('Plugins') 27 | menu.add_item('01 Create Cube Example') { 28 | self.create_cube 29 | } 30 | file_loaded(__FILE__) 31 | end 32 | 33 | end # module HelloCube 34 | end # module Examples 35 | -------------------------------------------------------------------------------- /examples/02_custom_tool/README.md: -------------------------------------------------------------------------------- 1 | # Custom Tool 2 | 3 | This demonstrates the basics for making custom tools. It's a simplified version 4 | of the SketchUp Line Tool. 5 | 6 | * Defines a custom tool class. 7 | * Takes user input to draw edges. 8 | 9 | A detailed tutorial of this example is located under tutorials/02_custom_tool. 10 | -------------------------------------------------------------------------------- /examples/02_custom_tool/ex_custom_tool.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate how to create a custom Ruby tool that lets the user pick 5 | # points in the model to draw edges. 6 | 7 | require 'sketchup.rb' 8 | require 'extensions.rb' 9 | 10 | module Examples 11 | module CustomTool 12 | 13 | unless file_loaded?(__FILE__) 14 | ex = SketchupExtension.new('Custom Tool', 'ex_custom_tool/main') 15 | ex.description = 'SketchUp Ruby API example creating a custom tool.' 16 | ex.version = '1.0.0' 17 | ex.copyright = 'Trimble Navigations © 2016' 18 | ex.creator = 'SketchUp' 19 | Sketchup.register_extension(ex, true) 20 | file_loaded(__FILE__) 21 | end 22 | 23 | end # module CustomTool 24 | end # module Examples 25 | -------------------------------------------------------------------------------- /examples/02_custom_tool/ex_custom_tool/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module CustomTool 8 | 9 | class LineTool 10 | 11 | def activate 12 | @mouse_ip = Sketchup::InputPoint.new 13 | @picked_first_ip = Sketchup::InputPoint.new 14 | update_ui 15 | end 16 | 17 | def deactivate(view) 18 | view.invalidate 19 | end 20 | 21 | def resume(view) 22 | update_ui 23 | view.invalidate 24 | end 25 | 26 | def suspend(view) 27 | view.invalidate 28 | end 29 | 30 | def onCancel(reason, view) 31 | reset_tool 32 | view.invalidate 33 | end 34 | 35 | def onMouseMove(flags, x, y, view) 36 | if picked_first_point? 37 | @mouse_ip.pick(view, x, y, @picked_first_ip) 38 | else 39 | @mouse_ip.pick(view, x, y) 40 | end 41 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 42 | view.invalidate 43 | end 44 | 45 | def onLButtonDown(flags, x, y, view) 46 | if picked_first_point? && create_edge > 0 47 | # When the user have picked a start point and then picks another point 48 | # we create an edge and try to create new faces from that edge. 49 | # Like the native tool we reset the tool if it created new faces. 50 | reset_tool 51 | else 52 | # If no face was created we let the user chain new edges to the last 53 | # input point. 54 | @picked_first_ip.copy!(@mouse_ip) 55 | end 56 | 57 | update_ui 58 | view.invalidate 59 | end 60 | 61 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 62 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 63 | # with your own custom cursor bitmap: 64 | # 65 | # CURSOR_PENCIL = UI.create_cursor(cursor_path, 0, 0) 66 | CURSOR_PENCIL = 632 67 | def onSetCursor 68 | # Note that `onSetCursor` is called frequently so you should not do much 69 | # work here. At most you switch between different cursor representing 70 | # the state of the tool. 71 | UI.set_cursor(CURSOR_PENCIL) 72 | end 73 | 74 | def draw(view) 75 | draw_preview(view) 76 | @mouse_ip.draw(view) if @mouse_ip.display? 77 | end 78 | 79 | def getExtents 80 | bounds = Geom::BoundingBox.new 81 | bounds.add(picked_points) 82 | bounds 83 | end 84 | 85 | private 86 | 87 | def update_ui 88 | if picked_first_point? 89 | Sketchup.status_text = 'Select end point.' 90 | else 91 | Sketchup.status_text = 'Select start point.' 92 | end 93 | end 94 | 95 | def reset_tool 96 | @picked_first_ip.clear 97 | update_ui 98 | end 99 | 100 | def picked_first_point? 101 | @picked_first_ip.valid? 102 | end 103 | 104 | def picked_points 105 | points = [] 106 | points << @picked_first_ip.position if picked_first_point? 107 | points << @mouse_ip.position if @mouse_ip.valid? 108 | points 109 | end 110 | 111 | def draw_preview(view) 112 | points = picked_points 113 | return unless points.size == 2 114 | view.set_color_from_line(*points) 115 | view.line_width = 1 116 | view.line_stipple = '' 117 | view.draw(GL_LINES, points) 118 | end 119 | 120 | # Returns the number of created faces. 121 | def create_edge 122 | model = Sketchup.active_model 123 | model.start_operation('Edge', true) 124 | edge = model.active_entities.add_line(picked_points) 125 | num_faces = edge.find_faces || 0 # API returns nil instead of 0. 126 | model.commit_operation 127 | 128 | num_faces 129 | end 130 | 131 | end # class LineTool 132 | 133 | 134 | def self.activate_line_tool 135 | Sketchup.active_model.select_tool(LineTool.new) 136 | end 137 | 138 | unless file_loaded?(__FILE__) 139 | menu = UI.menu('Plugins') 140 | menu.add_item('02 Custom Tool Example') { 141 | self.activate_line_tool 142 | } 143 | file_loaded(__FILE__) 144 | end 145 | 146 | end # module CustomTool 147 | end # module Examples 148 | -------------------------------------------------------------------------------- /examples/03_press_drag_release_tool/README.md: -------------------------------------------------------------------------------- 1 | # Press+Drag+Release Tool 2 | 3 | This demonstrates how a tool can be improved to feel more SketchUp like by 4 | supporting optional press+drag+release drawing style, in addition to the normal 5 | click+move+click. 6 | 7 | This is a continuation of "02_custom_tool". 8 | -------------------------------------------------------------------------------- /examples/03_press_drag_release_tool/ex_press_drag_release_tool.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate how to improve a custom Ruby tool to allow optional 5 | # press+drag+release drawing style. 6 | 7 | require 'sketchup.rb' 8 | require 'extensions.rb' 9 | 10 | module Examples 11 | module PressDragReleaseTool 12 | 13 | unless file_loaded?(__FILE__) 14 | ex = SketchupExtension.new('Press+Drag+Release Tool', 'ex_press_drag_release_tool/main') 15 | ex.description = 'SketchUp Ruby API example for press+drag+release drawing style.' 16 | ex.version = '1.0.0' 17 | ex.copyright = 'Trimble Inc © 2021' 18 | ex.creator = 'SketchUp' 19 | Sketchup.register_extension(ex, true) 20 | file_loaded(__FILE__) 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/03_press_drag_release_tool/ex_press_drag_release_tool/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module PressDragReleaseTool 8 | 9 | class LineTool 10 | 11 | # Threshold in logical screen pixels for when the mouse is considered to 12 | # be dragged. 13 | DRAG_THRESHOLD = 10 14 | 15 | def activate 16 | @mouse_ip = Sketchup::InputPoint.new 17 | @picked_first_ip = Sketchup::InputPoint.new 18 | 19 | # Track where mouse was pressed down so we can compare its position when 20 | # later released. 21 | @mouse_down = ORIGIN 22 | 23 | update_ui 24 | end 25 | 26 | def deactivate(view) 27 | view.invalidate 28 | end 29 | 30 | def resume(view) 31 | update_ui 32 | view.invalidate 33 | end 34 | 35 | def suspend(view) 36 | view.invalidate 37 | end 38 | 39 | def onCancel(reason, view) 40 | reset_tool 41 | view.invalidate 42 | end 43 | 44 | def onMouseMove(flags, x, y, view) 45 | if picked_first_point? 46 | @mouse_ip.pick(view, x, y, @picked_first_ip) 47 | 48 | # Print the length of the previewed line to the measurement bar. 49 | # Printing a Length object (as opposed to a float) automatically 50 | # formats it according to the model units. 51 | Sketchup.vcb_value = @mouse_ip.position.distance(@picked_first_ip.position) 52 | else 53 | @mouse_ip.pick(view, x, y) 54 | end 55 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 56 | view.invalidate 57 | end 58 | 59 | def onLButtonDown(flags, x, y, view) 60 | # Track where in screen space mouse is pressed down. 61 | @mouse_down = Geom::Point3d.new(x, y) 62 | 63 | if picked_first_point? && create_edge > 0 64 | # When the user have picked a start point and then picks another point 65 | # we create an edge and try to create new faces from that edge. 66 | # Like the native tool we reset the tool if it created new faces. 67 | reset_tool 68 | else 69 | # If no face was created we let the user chain new edges to the last 70 | # input point. 71 | @picked_first_ip.copy!(@mouse_ip) 72 | end 73 | 74 | update_ui 75 | view.invalidate 76 | end 77 | 78 | def onLButtonUp(flags, x, y, view) 79 | if @mouse_down.distance([x, y]) > DRAG_THRESHOLD 80 | # Mouse is released far enough away from where it was pressed to be 81 | # considered to be dragged there. 82 | 83 | create_edge 84 | 85 | # When drag-drawing, it feels odd to chain line drawing and we 86 | # consistently reset the tool instead. 87 | # You can try only calling this method if create_edge returns a 88 | # positive number, to experience the other behavior. 89 | reset_tool 90 | end 91 | end 92 | 93 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 94 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 95 | # with your own custom cursor bitmap: 96 | # 97 | # CURSOR_PENCIL = UI.create_cursor(cursor_path, 0, 0) 98 | CURSOR_PENCIL = 632 99 | 100 | def onSetCursor 101 | # Note that `onSetCursor` is called frequently so you should not do much 102 | # work here. At most you switch between different cursor representing 103 | # the state of the tool. 104 | UI.set_cursor(CURSOR_PENCIL) 105 | end 106 | 107 | def draw(view) 108 | draw_preview(view) 109 | @mouse_ip.draw(view) if @mouse_ip.display? 110 | end 111 | 112 | def getExtents 113 | bounds = Geom::BoundingBox.new 114 | bounds.add(picked_points) 115 | bounds 116 | end 117 | 118 | private 119 | 120 | def update_ui 121 | if picked_first_point? 122 | Sketchup.status_text = 'Select end point.' 123 | else 124 | Sketchup.status_text = 'Select start point.' 125 | end 126 | 127 | Sketchup.vcb_label = 'Length' 128 | end 129 | 130 | def reset_tool 131 | @picked_first_ip.clear 132 | update_ui 133 | end 134 | 135 | def picked_first_point? 136 | @picked_first_ip.valid? 137 | end 138 | 139 | def picked_points 140 | points = [] 141 | points << @picked_first_ip.position if picked_first_point? 142 | points << @mouse_ip.position if @mouse_ip.valid? 143 | points 144 | end 145 | 146 | def draw_preview(view) 147 | points = picked_points 148 | return unless points.size == 2 149 | view.set_color_from_line(*points) 150 | view.line_width = 1 151 | view.line_stipple = '' 152 | view.draw(GL_LINES, points) 153 | end 154 | 155 | # Returns the number of created faces. 156 | def create_edge 157 | model = Sketchup.active_model 158 | model.start_operation('Edge', true) 159 | edge = model.active_entities.add_line(picked_points) 160 | num_faces = edge.find_faces || 0 # API returns nil instead of 0. 161 | model.commit_operation 162 | 163 | num_faces 164 | end 165 | 166 | end # class LineTool 167 | 168 | 169 | def self.activate_line_tool 170 | Sketchup.active_model.select_tool(LineTool.new) 171 | end 172 | 173 | unless file_loaded?(__FILE__) 174 | menu = UI.menu('Plugins') 175 | menu.add_item('03 Press+Drag+Release Tool Example') { 176 | self.activate_line_tool 177 | } 178 | file_loaded(__FILE__) 179 | end 180 | 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /examples/04_vcb_tool/README.md: -------------------------------------------------------------------------------- 1 | # Measurement Bar Tool 2 | 3 | This demonstrates how a tool can be improved to feel more SketchUp like by 4 | taking text input in the measurement bar (aka VCB). 5 | 6 | This is a continuation of "03_press_drag_release". 7 | -------------------------------------------------------------------------------- /examples/04_vcb_tool/ex_vcb_tool.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate how to improve a custom Ruby tool to allow precise text input 5 | # in the measurement bar (aka VCB). 6 | 7 | require 'sketchup.rb' 8 | require 'extensions.rb' 9 | 10 | module Examples 11 | module VCBTool 12 | 13 | unless file_loaded?(__FILE__) 14 | ex = SketchupExtension.new('Measurement Bar', 'ex_vcb_tool/main') 15 | ex.description = 'SketchUp Ruby API example of tool Measurement Bar support.' 16 | ex.version = '1.0.0' 17 | ex.copyright = 'Trimble Inc © 2021' 18 | ex.creator = 'SketchUp' 19 | Sketchup.register_extension(ex, true) 20 | file_loaded(__FILE__) 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/04_vcb_tool/ex_vcb_tool/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module VCBTool 8 | 9 | class LineTool 10 | 11 | # Threshold in logical screen pixels for when the mouse is considered to 12 | # be dragged. 13 | DRAG_THRESHOLD = 10 14 | 15 | def activate 16 | @mouse_ip = Sketchup::InputPoint.new 17 | @picked_first_ip = Sketchup::InputPoint.new 18 | 19 | # Track where mouse was pressed down so we can compare its position when 20 | # later released. 21 | @mouse_down = ORIGIN 22 | 23 | update_ui 24 | end 25 | 26 | def deactivate(view) 27 | view.invalidate 28 | end 29 | 30 | # Enable editing in the measurement bar when the first point has been 31 | # picked. 32 | def enableVCB? 33 | picked_first_point? 34 | end 35 | 36 | def resume(view) 37 | update_ui 38 | view.invalidate 39 | end 40 | 41 | def suspend(view) 42 | view.invalidate 43 | end 44 | 45 | def onCancel(reason, view) 46 | reset_tool 47 | view.invalidate 48 | end 49 | 50 | def onMouseMove(flags, x, y, view) 51 | if picked_first_point? 52 | @mouse_ip.pick(view, x, y, @picked_first_ip) 53 | 54 | # Print the length of the previewed line to the measurement bar. 55 | # Printing a Length object (as opposed to a float) automatically 56 | # formats it according to the model units. 57 | Sketchup.vcb_value = @mouse_ip.position.distance(@picked_first_ip.position) 58 | else 59 | @mouse_ip.pick(view, x, y) 60 | end 61 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 62 | view.invalidate 63 | end 64 | 65 | def onLButtonDown(flags, x, y, view) 66 | # Track where in screen space mouse is pressed down. 67 | @mouse_down = Geom::Point3d.new(x, y) 68 | 69 | if picked_first_point? && create_edge > 0 70 | # When the user have picked a start point and then picks another point 71 | # we create an edge and try to create new faces from that edge. 72 | # Like the native tool we reset the tool if it created new faces. 73 | reset_tool 74 | else 75 | # If no face was created we let the user chain new edges to the last 76 | # input point. 77 | @picked_first_ip.copy!(@mouse_ip) 78 | end 79 | 80 | update_ui 81 | view.invalidate 82 | end 83 | 84 | def onLButtonUp(flags, x, y, view) 85 | if @mouse_down.distance([x, y]) > DRAG_THRESHOLD 86 | # Mouse is released far enough away from where it was pressed to be 87 | # considered to be dragged there. 88 | 89 | create_edge 90 | 91 | # When drag-drawing, it feels odd to chain line drawing and we 92 | # consistently reset the tool instead. 93 | # You can try only calling this method if create_edge returns a 94 | # positive number, to experience the other behavior. 95 | reset_tool 96 | end 97 | end 98 | 99 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 100 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 101 | # with your own custom cursor bitmap: 102 | # 103 | # CURSOR_PENCIL = UI.create_cursor(cursor_path, 0, 0) 104 | CURSOR_PENCIL = 632 105 | 106 | def onSetCursor 107 | # Note that `onSetCursor` is called frequently so you should not do much 108 | # work here. At most you switch between different cursor representing 109 | # the state of the tool. 110 | UI.set_cursor(CURSOR_PENCIL) 111 | end 112 | 113 | # Listen to measurement bar text input. 114 | def onUserText(text, view) 115 | begin 116 | distance = text.to_l 117 | rescue ArgumentError 118 | UI.messagebox('Invalid length') 119 | return 120 | end 121 | direction = picked_points[1] - picked_points[0] 122 | end_point = picked_points[0].offset(direction, distance) 123 | @mouse_ip = Sketchup::InputPoint.new(end_point) 124 | 125 | # If faces are created when creating the edge, reset the tool. 126 | # Otherwise keep drawing edges. 127 | if create_edge > 0 128 | reset_tool 129 | else 130 | @picked_first_ip.copy!(@mouse_ip) 131 | end 132 | 133 | view.invalidate 134 | end 135 | 136 | def draw(view) 137 | draw_preview(view) 138 | @mouse_ip.draw(view) if @mouse_ip.display? 139 | end 140 | 141 | def getExtents 142 | bounds = Geom::BoundingBox.new 143 | bounds.add(picked_points) 144 | bounds 145 | end 146 | 147 | private 148 | 149 | def update_ui 150 | if picked_first_point? 151 | Sketchup.status_text = 'Select end point.' 152 | else 153 | Sketchup.status_text = 'Select start point.' 154 | end 155 | 156 | Sketchup.vcb_label = 'Length' 157 | end 158 | 159 | def reset_tool 160 | @picked_first_ip.clear 161 | update_ui 162 | end 163 | 164 | def picked_first_point? 165 | @picked_first_ip.valid? 166 | end 167 | 168 | def picked_points 169 | points = [] 170 | points << @picked_first_ip.position if picked_first_point? 171 | points << @mouse_ip.position if @mouse_ip.valid? 172 | points 173 | end 174 | 175 | def draw_preview(view) 176 | points = picked_points 177 | return unless points.size == 2 178 | view.set_color_from_line(*points) 179 | view.line_width = 1 180 | view.line_stipple = '' 181 | view.draw(GL_LINES, points) 182 | end 183 | 184 | # Returns the number of created faces. 185 | def create_edge 186 | model = Sketchup.active_model 187 | model.start_operation('Edge', true) 188 | edge = model.active_entities.add_line(picked_points) 189 | num_faces = edge.find_faces || 0 # API returns nil instead of 0. 190 | model.commit_operation 191 | 192 | num_faces 193 | end 194 | 195 | end # class LineTool 196 | 197 | 198 | def self.activate_line_tool 199 | Sketchup.active_model.select_tool(LineTool.new) 200 | end 201 | 202 | unless file_loaded?(__FILE__) 203 | menu = UI.menu('Plugins') 204 | menu.add_item('04 Measurement Bar Tool Example') { 205 | self.activate_line_tool 206 | } 207 | file_loaded(__FILE__) 208 | end 209 | 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /examples/05_tool_inference_lock/README.md: -------------------------------------------------------------------------------- 1 | # Tool Inference Lock 2 | 3 | This demonstrates how a tool can be improved to feel more SketchUp like by 4 | locking the inference with Shift and arrow keys. 5 | 6 | This is a continuation of "04_tool_vcb_support". 7 | -------------------------------------------------------------------------------- /examples/05_tool_inference_lock/ex_tool_inference_lock.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate how to add inference locking to a tool. 5 | 6 | require 'sketchup.rb' 7 | require 'extensions.rb' 8 | 9 | module Examples 10 | module ToolInferenceLock 11 | 12 | unless file_loaded?(__FILE__) 13 | ex = SketchupExtension.new('Tool Inference Lock', 'ex_tool_inference_lock/main') 14 | ex.description = 'SketchUp Ruby API example of tool inference lock.' 15 | ex.version = '1.0.0' 16 | ex.copyright = 'Trimble Inc © 2021' 17 | ex.creator = 'SketchUp' 18 | Sketchup.register_extension(ex, true) 19 | file_loaded(__FILE__) 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/05_tool_inference_lock/ex_tool_inference_lock/inference_lock_helper.rb: -------------------------------------------------------------------------------- 1 | module Examples 2 | module ToolInferenceLock 3 | # Helper object to lock tool inference using Shift or arrow keys. 4 | class InferenceLockHelper 5 | # Create InferenceLockHelper object. 6 | def initialize 7 | @axis_lock = nil 8 | @mouse_x = nil 9 | @mouse_y = nil 10 | end 11 | 12 | # Call this method from +onKeyDown+. 13 | # 14 | # After calling this method, also re-pick your input point, as the 15 | # inference lock may have changed. 16 | # 17 | # @param key [Integer] 18 | # @param view [Sketchup::View] 19 | # @param active_ip [Sketchup::InputPoint] 20 | # The InputPoint currently used to pick points. 21 | # @param reference_ip [Sketchup::InputPoint] 22 | # An InputPoint marking where the current operation was started. 23 | def on_keydown(key, view, active_ip, reference_ip = active_ip) 24 | if key == CONSTRAIN_MODIFIER_KEY 25 | try_lock_constraint(view, active_ip) 26 | else 27 | try_lock_axis(key, view, reference_ip) 28 | end 29 | end 30 | 31 | # Call this method from +onKeyUp+. 32 | # 33 | # After calling this method, also re-pick your input point, as the 34 | # inference lock may have changed. 35 | # 36 | # @param key [Integer] 37 | # @param view [Sketchup::View] 38 | def on_keyup(key, view) 39 | return unless key == CONSTRAIN_MODIFIER_KEY 40 | return if @axis_lock 41 | 42 | # Calling this method with no argument unlocks inference. 43 | view.lock_inference 44 | end 45 | 46 | # Remove any inference locking. 47 | # This should typically be called when the user resets the tool. 48 | # Removing inference locking using Shift and arrow keys is handled by 49 | # +handle_keydown+ and +handle_keyup+. 50 | def unlock 51 | @axis_lock = nil 52 | # Calling this method with no argument unlocks inference. 53 | Sketchup.active_model.active_view.lock_inference 54 | end 55 | 56 | private 57 | 58 | # Try picking a constraint lock. 59 | # 60 | # @param view [Sketchup::View] 61 | # @param active_ip [Sketchup::InputPoint] 62 | # The InputPoint currently used to pick points. 63 | def try_lock_constraint(view, active_ip) 64 | return if @axis_lock 65 | return unless active_ip.valid? 66 | 67 | view.lock_inference(active_ip) 68 | end 69 | 70 | # Try picking an axis lock for given keycode. 71 | # 72 | # @param key [Integer] 73 | # @param view [Sketchup::View] 74 | # @param reference_ip [Sketchup::InputPoint] 75 | # An InputPoint marking where the current operation was started. 76 | def try_lock_axis(key, view, reference_ip) 77 | return unless reference_ip.valid? 78 | 79 | case key 80 | when VK_RIGHT 81 | lock_inference_axis([reference_ip.position, view.model.axes.xaxis], view) 82 | when VK_LEFT 83 | lock_inference_axis([reference_ip.position, view.model.axes.yaxis], view) 84 | when VK_UP 85 | lock_inference_axis([reference_ip.position, view.model.axes.zaxis], view) 86 | end 87 | end 88 | 89 | # Unlock inference lock to axis, if there is any. 90 | # 91 | # @param view [Sketchup::view] 92 | def unlock_axis(view) 93 | # Any inference lock not done with `lock_inference_axis` should be kept. 94 | # This method is only concerned with inference locks to the axes. 95 | return unless @axis_lock 96 | 97 | @axis_lock = nil 98 | # Calling this method with no argument unlocks inference. 99 | view.lock_inference 100 | end 101 | 102 | # Lock inference to an axis or unlock if already locked to that very axis. 103 | # 104 | # @param line [Array<(Geom::Point3d, Geom::Vector3d)>] 105 | # @param view [Sketchup::View] 106 | def lock_inference_axis(line, view) 107 | return unlock_axis(view) if line == @axis_lock 108 | 109 | @axis_lock = line 110 | view.lock_inference( 111 | Sketchup::InputPoint.new(line[0]), 112 | Sketchup::InputPoint.new(line[0].offset(line[1])) 113 | ) 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /examples/05_tool_inference_lock/ex_tool_inference_lock/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'ex_tool_inference_lock/inference_lock_helper' 6 | 7 | module Examples 8 | module ToolInferenceLock 9 | 10 | class LineTool 11 | 12 | # Threshold in logical screen pixels for when the mouse is considered to 13 | # be dragged. 14 | DRAG_THRESHOLD = 10 15 | 16 | def activate 17 | @mouse_ip = Sketchup::InputPoint.new 18 | @picked_first_ip = Sketchup::InputPoint.new 19 | @mouse_position = ORIGIN 20 | @mouse_down = ORIGIN 21 | @distance = 0 22 | 23 | # Create a InferenceLockHelper object to use in this tool. 24 | # See inference_lock_helper.rb for details. 25 | @inference_lock_helper = InferenceLockHelper.new 26 | 27 | update_ui 28 | end 29 | 30 | def deactivate(view) 31 | @inference_lock_helper.unlock 32 | view.invalidate 33 | end 34 | 35 | def enableVCB? 36 | picked_first_point? 37 | end 38 | 39 | def resume(view) 40 | update_ui 41 | view.invalidate 42 | end 43 | 44 | def suspend(view) 45 | view.invalidate 46 | end 47 | 48 | def onCancel(reason, view) 49 | reset_tool 50 | view.invalidate 51 | end 52 | 53 | def onKeyDown(key, _repeat, _flags, view) 54 | @inference_lock_helper.on_keydown(key, view, @mouse_ip, @picked_first_ip) 55 | pick_mouse_position(view) 56 | end 57 | 58 | def onKeyUp(key, _repeat, _flags, view) 59 | @inference_lock_helper.on_keyup(key, view) 60 | pick_mouse_position(view) 61 | end 62 | 63 | def onMouseMove(flags, x, y, view) 64 | # Memorize mouse positions so we can do the InputPoint picking again 65 | # when inference lock changes. 66 | @mouse_position = Geom::Point3d.new(x, y, 0) 67 | 68 | # Breaking out the normal mouse move logic to a method that can also be 69 | # called when inference lock changes. 70 | pick_mouse_position(view) 71 | end 72 | 73 | def onLButtonDown(flags, x, y, view) 74 | @mouse_down = Geom::Point3d.new(x, y) 75 | 76 | if picked_first_point? && create_edge > 0 77 | # When the user have picked a start point and then picks another point 78 | # we create an edge and try to create new faces from that edge. 79 | # Like the native tool we reset the tool if it created new faces. 80 | reset_tool 81 | else 82 | # If no face was created we let the user chain new edges to the last 83 | # input point. 84 | @picked_first_ip.copy!(@mouse_ip) 85 | end 86 | 87 | update_ui 88 | view.invalidate 89 | end 90 | 91 | def onLButtonUp(flags, x, y, view) 92 | if @mouse_down.distance([x, y]) > DRAG_THRESHOLD 93 | create_edge 94 | reset_tool 95 | end 96 | end 97 | 98 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 99 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 100 | # with your own custom cursor bitmap: 101 | # 102 | # CURSOR_PENCIL = UI.create_cursor(cursor_path, 0, 0) 103 | CURSOR_PENCIL = 632 104 | 105 | def onSetCursor 106 | # Note that `onSetCursor` is called frequently so you should not do much 107 | # work here. At most you switch between different cursor representing 108 | # the state of the tool. 109 | UI.set_cursor(CURSOR_PENCIL) 110 | end 111 | 112 | def onUserText(text, view) 113 | begin 114 | distance = text.to_l 115 | rescue ArgumentError 116 | UI.messagebox('Invalid length') 117 | return 118 | end 119 | direction = picked_points[1] - picked_points[0] 120 | end_point = picked_points[0].offset(direction, distance) 121 | @mouse_ip = Sketchup::InputPoint.new(end_point) 122 | 123 | # If faces are created when creating the edge, reset the tool. 124 | # Otherwise keep drawing edges. 125 | if create_edge > 0 126 | reset_tool 127 | else 128 | @picked_first_ip.copy!(@mouse_ip) 129 | end 130 | 131 | view.invalidate 132 | end 133 | 134 | def draw(view) 135 | draw_preview(view) 136 | 137 | view.line_width = 1 138 | @mouse_ip.draw(view) if @mouse_ip.display? 139 | end 140 | 141 | def getExtents 142 | bounds = Geom::BoundingBox.new 143 | bounds.add(picked_points) 144 | bounds 145 | end 146 | 147 | private 148 | 149 | # This is the input point picking logic you typically see in OnMouseMove. 150 | # This code is broken out as a separate method to be able to update the 151 | # input points when the inference lock changes. 152 | def pick_mouse_position(view) 153 | if picked_first_point? 154 | @mouse_ip.pick(view, @mouse_position.x, @mouse_position.y, @picked_first_ip) 155 | @distance = @mouse_ip.position.distance(@picked_first_ip.position) 156 | update_ui 157 | else 158 | @mouse_ip.pick(view, @mouse_position.x, @mouse_position.y) 159 | end 160 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 161 | view.invalidate 162 | end 163 | 164 | def update_ui 165 | # We specifically do not bloat the status text with all the arrow keys 166 | # and Shift key, as those are more or less universal in SketchUp. 167 | # Adding too much text just makes the status bar hard to read. 168 | # However, the instructor should mention these. 169 | if picked_first_point? 170 | Sketchup.status_text = 'Select end point.' 171 | Sketchup.vcb_value = @distance 172 | else 173 | Sketchup.status_text = 'Select start point.' 174 | end 175 | 176 | Sketchup.vcb_label = 'Length' 177 | end 178 | 179 | def reset_tool 180 | @picked_first_ip.clear 181 | 182 | # Unlock inference when resetting the tool. 183 | # If the user isn't happy with the starting point, they likely aren't 184 | # happy with any inference lock either. 185 | @inference_lock_helper.unlock 186 | 187 | update_ui 188 | end 189 | 190 | def picked_first_point? 191 | @picked_first_ip.valid? 192 | end 193 | 194 | def picked_points 195 | points = [] 196 | points << @picked_first_ip.position if picked_first_point? 197 | points << @mouse_ip.position if @mouse_ip.valid? 198 | points 199 | end 200 | 201 | def draw_preview(view) 202 | points = picked_points 203 | return unless points.size == 2 204 | 205 | # When inference is locked, draw the lines 3 pixels wide. 206 | # This subtly but clearly the line now "snaps" into a certain direction. 207 | view.line_width = view.inference_locked? ? 3 : 1 208 | 209 | view.set_color_from_line(*points) 210 | view.draw(GL_LINES, points) 211 | end 212 | 213 | # Returns the number of created faces. 214 | def create_edge 215 | model = Sketchup.active_model 216 | model.start_operation('Edge', true) 217 | edge = model.active_entities.add_line(picked_points) 218 | num_faces = edge.find_faces || 0 # API returns nil instead of 0. 219 | model.commit_operation 220 | 221 | # Remove any inference lock once an edge is drawn. 222 | # Typically you don't want more than one edge locked to a single line or 223 | # plane. 224 | @inference_lock_helper.unlock 225 | 226 | num_faces 227 | end 228 | 229 | end # class LineTool 230 | 231 | 232 | def self.activate_line_tool 233 | Sketchup.active_model.select_tool(LineTool.new) 234 | end 235 | 236 | unless file_loaded?(__FILE__) 237 | menu = UI.menu('Plugins') 238 | menu.add_item('05 Tool Inference Lock Example') { 239 | self.activate_line_tool 240 | } 241 | file_loaded(__FILE__) 242 | end 243 | 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /examples/99_hello_donut/README.md: -------------------------------------------------------------------------------- 1 | # Hello Donut 2 | 3 | This examples demonstrates how to create a donut shape in SketchUp using 4 | FollowMe. 5 | 6 | -------------------------------------------------------------------------------- /examples/99_hello_donut/ex_hello_donut.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'extensions.rb' 6 | 7 | module Examples 8 | module HelloSphere 9 | 10 | unless file_loaded?(__FILE__) 11 | ex = SketchupExtension.new('Hello Donut', 'ex_hello_donut/main') 12 | ex.description = 'SketchUp Ruby API example creating a donut.' 13 | ex.version = '1.0.0' 14 | ex.copyright = 'Trimble Inc © 2018' 15 | ex.creator = 'SketchUp' 16 | Sketchup.register_extension(ex, true) 17 | file_loaded(__FILE__) 18 | end 19 | 20 | end # module HelloSphere 21 | end # module Examples 22 | -------------------------------------------------------------------------------- /examples/99_hello_donut/ex_hello_donut/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module HelloDonut 8 | 9 | def self.create_donut 10 | model = Sketchup.active_model 11 | model.start_operation('Create Donut', true) 12 | group = model.active_entities.add_group 13 | # Create a filled circle which later will be extruded along a path to 14 | # create a donut shape. 15 | num_segments = 24 16 | center_radius = 1.m 17 | thickness = 0.5.m 18 | origin = Geom::Point3d.new(0, center_radius, 0) 19 | circle = group.entities.add_circle(origin, X_AXIS, thickness, num_segments) 20 | face = group.entities.add_face(circle) 21 | # Create a temporary path for follow me to use to perform the revolve. 22 | # This path should not touch the face. 23 | path = group.entities.add_circle(ORIGIN, Z_AXIS, center_radius, num_segments) 24 | # This creates the donut. 25 | face.followme(path) 26 | # The temporary path is no longer needed. 27 | # group.entities.erase_entities(path) 28 | model.commit_operation 29 | end 30 | 31 | unless file_loaded?(__FILE__) 32 | menu = UI.menu('Plugins') 33 | menu.add_item('99 Create Donut Example') { 34 | self.create_donut 35 | } 36 | file_loaded(__FILE__) 37 | end 38 | 39 | end # module HelloDonut 40 | end # module Examples -------------------------------------------------------------------------------- /examples/99_hello_sphere/README.md: -------------------------------------------------------------------------------- 1 | # Hello Sphere 2 | 3 | This examples demonstrates how to create a sphere in SketchUp using FollowMe. 4 | 5 | -------------------------------------------------------------------------------- /examples/99_hello_sphere/ex_hello_sphere.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'extensions.rb' 6 | 7 | module Examples 8 | module HelloSphere 9 | 10 | unless file_loaded?(__FILE__) 11 | ex = SketchupExtension.new('Hello Sphere', 'ex_hello_sphere/main') 12 | ex.description = 'SketchUp Ruby API example creating a sphere.' 13 | ex.version = '1.0.0' 14 | ex.copyright = 'Trimble Inc © 2018' 15 | ex.creator = 'SketchUp' 16 | Sketchup.register_extension(ex, true) 17 | file_loaded(__FILE__) 18 | end 19 | 20 | end # module HelloSphere 21 | end # module Examples 22 | -------------------------------------------------------------------------------- /examples/99_hello_sphere/ex_hello_sphere/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module HelloSphere 8 | 9 | def self.create_sphere 10 | model = Sketchup.active_model 11 | model.start_operation('Create Sphere', true) 12 | group = model.active_entities.add_group 13 | # Create a filled circle which later will be revolved around itself to 14 | # create a sphere. 15 | num_segments = 48 16 | circle = group.entities.add_circle(ORIGIN, X_AXIS, 1.m, num_segments) 17 | face = group.entities.add_face(circle) 18 | face.reverse! 19 | # Create a temporary path for follow me to use to perform the revolve. 20 | # This path should not touch the face. 21 | path = group.entities.add_circle(ORIGIN, Z_AXIS, 2.m, num_segments) 22 | # This creates the sphere. 23 | face.followme(path) 24 | # The temporary path is no longer needed. 25 | group.entities.erase_entities(path) 26 | model.commit_operation 27 | end 28 | 29 | unless file_loaded?(__FILE__) 30 | menu = UI.menu('Plugins') 31 | menu.add_item('99 Create Sphere Example') { 32 | self.create_sphere 33 | } 34 | file_loaded(__FILE__) 35 | end 36 | 37 | end # module HelloSphere 38 | end # module Examples -------------------------------------------------------------------------------- /examples/99_in_tool_selection/README.md: -------------------------------------------------------------------------------- 1 | # In tool selection 2 | 3 | This demonstrates how a tool can include a selection stage if activated without 4 | a usable selection. This is how native Move, Scale and Rotate behaves. 5 | 6 | This approach can also be used for tools that only works on objects created 7 | by the same extension. For instance a road plugin could have a tool for 8 | adjusting the control points of your custom roads, and only be able to select 9 | Groups representing such roads. 10 | 11 | This example assumes you are already familiar with concepts from previous tool 12 | examples. 13 | -------------------------------------------------------------------------------- /examples/99_in_tool_selection/ex_in_tool_selection.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate an optional select stage in a tool. 5 | 6 | require 'sketchup.rb' 7 | require 'extensions.rb' 8 | 9 | module Examples 10 | module InToolSelection 11 | 12 | unless file_loaded?(__FILE__) 13 | ex = SketchupExtension.new('In Tool Selection', 'ex_in_tool_selection/main') 14 | ex.description = 'SketchUp Ruby API example of in tool selection.' 15 | ex.version = '1.0.0' 16 | ex.copyright = 'Trimble Inc © 2021' 17 | ex.creator = 'SketchUp' 18 | Sketchup.register_extension(ex, true) 19 | file_loaded(__FILE__) 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/99_in_tool_selection/ex_in_tool_selection/inference_lock_helper.rb: -------------------------------------------------------------------------------- 1 | module Examples 2 | module InToolSelection 3 | # Helper object to lock tool inference using Shift or arrow keys. 4 | class InferenceLockHelper 5 | # Create InferenceLockHelper object. 6 | def initialize 7 | @axis_lock = nil 8 | @mouse_x = nil 9 | @mouse_y = nil 10 | end 11 | 12 | # Call this method from +onKeyDown+. 13 | # 14 | # After calling this method, also re-pick your input point, as the 15 | # inference lock may have changed. 16 | # 17 | # @param key [Integer] 18 | # @param view [Sketchup::View] 19 | # @param active_ip [Sketchup::InputPoint] 20 | # The InputPoint currently used to pick points. 21 | # @param reference_ip [Sketchup::InputPoint] 22 | # An InputPoint marking where the current operation was started. 23 | def on_keydown(key, view, active_ip, reference_ip = active_ip) 24 | if key == CONSTRAIN_MODIFIER_KEY 25 | try_lock_constraint(view, active_ip) 26 | else 27 | try_lock_axis(key, view, reference_ip) 28 | end 29 | end 30 | 31 | # Call this method from +onKeyUp+. 32 | # 33 | # After calling this method, also re-pick your input point, as the 34 | # inference lock may have changed. 35 | # 36 | # @param key [Integer] 37 | # @param view [Sketchup::View] 38 | def on_keyup(key, view) 39 | return unless key == CONSTRAIN_MODIFIER_KEY 40 | return if @axis_lock 41 | 42 | # Calling this method with no argument unlocks inference. 43 | view.lock_inference 44 | end 45 | 46 | # Remove any inference locking. 47 | # This should typically be called when the user resets the tool. 48 | # Removing inference locking using Shift and arrow keys is handled by 49 | # +handle_keydown+ and +handle_keyup+. 50 | def unlock 51 | @axis_lock = nil 52 | # Calling this method with no argument unlocks inference. 53 | Sketchup.active_model.active_view.lock_inference 54 | end 55 | 56 | private 57 | 58 | # Try picking a constraint lock. 59 | # 60 | # @param view [Sketchup::View] 61 | # @param active_ip [Sketchup::InputPoint] 62 | # The InputPoint currently used to pick points. 63 | def try_lock_constraint(view, active_ip) 64 | return if @axis_lock 65 | return unless active_ip.valid? 66 | 67 | view.lock_inference(active_ip) 68 | end 69 | 70 | # Try picking an axis lock for given keycode. 71 | # 72 | # @param key [Integer] 73 | # @param view [Sketchup::View] 74 | # @param reference_ip [Sketchup::InputPoint] 75 | # An InputPoint marking where the current operation was started. 76 | def try_lock_axis(key, view, reference_ip) 77 | return unless reference_ip.valid? 78 | 79 | case key 80 | when VK_RIGHT 81 | lock_inference_axis([reference_ip.position, view.model.axes.xaxis], view) 82 | when VK_LEFT 83 | lock_inference_axis([reference_ip.position, view.model.axes.yaxis], view) 84 | when VK_UP 85 | lock_inference_axis([reference_ip.position, view.model.axes.zaxis], view) 86 | end 87 | end 88 | 89 | # Unlock inference lock to axis, if there is any. 90 | # 91 | # @param view [Sketchup::view] 92 | def unlock_axis(view) 93 | # Any inference lock not done with `lock_inference_axis` should be kept. 94 | # This method is only concerned with inference locks to the axes. 95 | return unless @axis_lock 96 | 97 | @axis_lock = nil 98 | # Calling this method with no argument unlocks inference. 99 | view.lock_inference 100 | end 101 | 102 | # Lock inference to an axis or unlock if already locked to that very axis. 103 | # 104 | # @param line [Array<(Geom::Point3d, Geom::Vector3d)>] 105 | # @param view [Sketchup::View] 106 | def lock_inference_axis(line, view) 107 | return unlock_axis(view) if line == @axis_lock 108 | 109 | @axis_lock = line 110 | view.lock_inference( 111 | Sketchup::InputPoint.new(line[0]), 112 | Sketchup::InputPoint.new(line[0].offset(line[1])) 113 | ) 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /examples/99_in_tool_selection/ex_in_tool_selection/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'ex_in_tool_selection/inference_lock_helper' 5 | 6 | module Examples 7 | module InToolSelection 8 | 9 | class ComponentMoveTool 10 | 11 | # Threshold in logical screen pixels for when the mouse is considered to 12 | # be dragged. 13 | DRAG_THRESHOLD = 10 14 | 15 | def initialize 16 | @mouse_ip = Sketchup::InputPoint.new 17 | @picked_first_ip = Sketchup::InputPoint.new 18 | @mouse_position = ORIGIN 19 | @mouse_down = ORIGIN 20 | @distance = 0 21 | 22 | # Create a InferenceLockHelper object to use in this tool. 23 | # See inference_lock_helper.rb for details. 24 | @inference_lock_helper = InferenceLockHelper.new 25 | 26 | # Empty the selection if the tool can't use it. 27 | # Otherwise keep this preselection. 28 | Sketchup.active_model.selection.clear unless valid_selection? 29 | 30 | # The tool selection is tracked separately from the SketchUp selection. 31 | # When we are in the selection stage we want to temporarily select 32 | # hovered entities for previewing them as selected, but our tool logic 33 | # treats the selection as empty until the user clicks. 34 | # 35 | # Native tools can preview entities as selected without actually 36 | # adding them to the selection, but the Ruby API doesn't support this. 37 | @selection = Sketchup.active_model.selection.to_a 38 | 39 | # Remember whether tool was activated with preselection or not as this 40 | # affect how the tool resets. 41 | @preselected = !Sketchup.active_model.selection.empty? 42 | 43 | # The entity being hovered when at the selection stage. 44 | @hovered_entity = nil 45 | 46 | # Whether the the tool is able to interact with the hovered entity. 47 | # This affects what mouse cursor the tool uses. 48 | @valid_hovered_entity = false 49 | end 50 | 51 | def activate 52 | update_ui 53 | end 54 | 55 | def deactivate(view) 56 | @inference_lock_helper.unlock 57 | view.model.abort_operation if @in_operation 58 | view.invalidate 59 | end 60 | 61 | def enableVCB? 62 | picked_first_point? 63 | end 64 | 65 | def resume(view) 66 | update_ui 67 | view.invalidate 68 | end 69 | 70 | def suspend(view) 71 | view.invalidate 72 | end 73 | 74 | def onCancel(reason, view) 75 | reset_tool 76 | view.invalidate 77 | end 78 | 79 | def onKeyDown(key, _repeat, _flags, view) 80 | @inference_lock_helper.on_keydown(key, view, @mouse_ip, @picked_first_ip) 81 | pick_mouse_position(view) 82 | end 83 | 84 | def onKeyUp(key, _repeat, _flags, view) 85 | @inference_lock_helper.on_keyup(key, view) 86 | pick_mouse_position(view) 87 | end 88 | 89 | def onMouseMove(flags, x, y, view) 90 | if @selection.empty? 91 | # If the tool selection is empty, try picking the hovered object. 92 | try_select_entity(view, x, y) 93 | end 94 | 95 | @mouse_position = Geom::Point3d.new(x, y, 0) 96 | pick_mouse_position(view) 97 | end 98 | 99 | def onLButtonDown(flags, x, y, view) 100 | @mouse_down = Geom::Point3d.new(x, y) 101 | 102 | # If tool selection is empty, select our hovered entity. 103 | # If we already have a selection, we use it. 104 | if @selection.empty? 105 | @selection = view.model.selection.to_a 106 | end 107 | 108 | return if @selection.empty? 109 | 110 | # For a Move or Rotate like tool, we can select a starting point in the 111 | # same mouse click as we select an entity. For Scale or other tools 112 | # where you need to select a handle, this logic is typically slightly 113 | # different, and separate click is needed. 114 | 115 | if !picked_first_point? 116 | start_move(view) 117 | else 118 | end_move(view) 119 | end 120 | 121 | update_ui 122 | view.invalidate 123 | end 124 | 125 | def onLButtonUp(flags, x, y, view) 126 | if @mouse_down.distance([x, y]) > DRAG_THRESHOLD 127 | end_move(view) 128 | end 129 | end 130 | 131 | # Here we have hard coded a special ID for the move cursor in SketchUp. 132 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 133 | # with your own custom cursor bitmap: 134 | # 135 | # CURSOR = UI.create_cursor(cursor_path, 0, 0) 136 | MOVE_CURSOR = 641 137 | INVALID_CURSOR = 663 138 | 139 | def onSetCursor 140 | # Note that `onSetCursor` is called frequently so you should not do much 141 | # work here. At most you switch between different cursor representing 142 | # the state of the tool. 143 | 144 | # Cursor depends on the tool state and hovered entity. 145 | # If the user is at the selection stage and hover a non-selectable 146 | # entity, they se an invalid-cursor. 147 | # Hovering empty space in SketchUp doesn't show the invalid-cursor, 148 | # even if empty space can't be selected. 149 | if !picked_first_point? && @hovered_entity && !@valid_hovered_entity 150 | UI.set_cursor(INVALID_CURSOR) 151 | else 152 | UI.set_cursor(MOVE_CURSOR) 153 | end 154 | end 155 | 156 | def onUserText(text, view) 157 | begin 158 | distance = text.to_l 159 | rescue ArgumentError 160 | UI.messagebox('Invalid length') 161 | return 162 | end 163 | direction = picked_points[0].vector_to(picked_points[1]) 164 | end_point = picked_points[0].offset(direction, distance) 165 | @mouse_ip = Sketchup::InputPoint.new(end_point) 166 | 167 | # Move the selected entities and exit the move stage. 168 | do_move(view) 169 | end_move(view) 170 | 171 | view.invalidate 172 | end 173 | 174 | def draw(view) 175 | draw_preview_line(view) 176 | 177 | view.line_width = 1 178 | 179 | # Draw inputpoint (including tooltip) so the user knows where exactly 180 | # where it is. 181 | # However, don't draw it when at the selection stage and not hovering 182 | # a selectable entity. Doing so would suggest you could click to select 183 | # a point and proceed to the next tool stage. 184 | if picked_first_point? || (@hovered_entity && @valid_hovered_entity) 185 | @mouse_ip.draw(view) 186 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 187 | end 188 | end 189 | 190 | def getExtents 191 | bounds = Geom::BoundingBox.new 192 | bounds.add(picked_points) unless picked_points.empty? 193 | bounds 194 | end 195 | 196 | private 197 | 198 | # Check if an entity is allowed to be selected by this tool. 199 | def valid_entity?(entity) 200 | # In this somewhat artificial example we only allow components to be 201 | # moved. In a real life example you'd typically filter entities by 202 | # other criteria. 203 | 204 | # To replicate native Move, Rotate and Scale tools, allow all 205 | # DrawingElements except Axes. 206 | ### !entity.is_a?(Sketchup::Axes) 207 | 208 | # Another typical use case is a custom tool that edits some property of 209 | # a custom objects, e.g. Bezier curve control points or wall corner 210 | # points. For this, you'd typically check for the entity being a group 211 | # and/or component, and if it has the attribute dictionary holding your 212 | # custom properties. 213 | ### entity.is_a?(Sketchup::Group) && entity.attribute_dictionary("TurboWall2000") 214 | 215 | entity.is_a?(Sketchup::ComponentInstance) 216 | end 217 | 218 | # Check if the selection as a whole is valid for this tool. 219 | def valid_selection? 220 | selection = Sketchup.active_model.selection 221 | 222 | selection.size == 1 && valid_entity?(selection.first) 223 | end 224 | 225 | # Try selecting the entity under the mouse cursor, if the tool allows it. 226 | def try_select_entity(view, x, y) 227 | view.model.selection.clear 228 | 229 | pickhelper = view.pick_helper(x, y) 230 | @hovered_entity = pickhelper.best_picked 231 | return unless @hovered_entity 232 | 233 | @valid_hovered_entity = valid_entity?(@hovered_entity) 234 | return unless @valid_hovered_entity 235 | 236 | view.model.selection.add(@hovered_entity) 237 | end 238 | 239 | # Start model operation and enter the tool move stage. 240 | def start_move(view) 241 | # Typically you'd just copy the active picking InputPoint state into 242 | # the reference InputPoint here. 243 | ### @picked_first_ip.copy!(@mouse_ip) 244 | # However, since we are moving the geometry that the InputPoint has 245 | # gotten its position from, this would also cause the reference 246 | # InputPoint to move. This would lead to the distance reported in the 247 | # VCB to be 0 and the preview line to be missing. 248 | # Instead just create a new InputPoint with a fixed location in space. 249 | # See https://github.com/SketchUp/api-issue-tracker/issues/618 250 | # See https://github.com/SketchUp/api-issue-tracker/issues/452 251 | @picked_first_ip = Sketchup::InputPoint.new(@mouse_ip.position) 252 | 253 | # Wrap model changes in an operation so it can be undone in one step. 254 | # Typically we disable drawing in operations to increase performance, 255 | # but here we want live updates when moving the mouse. 256 | view.model.start_operation("Move", false) 257 | 258 | # Track whether we are in an operation so we can abort it if the user 259 | # activates another tool. 260 | @in_operation = true 261 | 262 | # Track the mouse position between events so we can update the position 263 | # of the selected entities. 264 | @last_mouse_position = @mouse_ip.position 265 | end 266 | 267 | # Update the position of the selected entities. 268 | # Called on each mouse move or when entering a measurement as text. 269 | def do_move(view) 270 | return unless @last_mouse_position 271 | 272 | # Move entities the same distance mouse has moved since last move event. 273 | delta_move = @last_mouse_position.vector_to(@mouse_ip.position) 274 | @last_mouse_position = @mouse_ip.position 275 | 276 | view.model.active_entities.transform_entities(delta_move, @selection) 277 | end 278 | 279 | # Leave the tool move stage and close the operation. 280 | def end_move(view) 281 | view.model.commit_operation 282 | @in_operation = false 283 | 284 | @picked_first_ip.clear 285 | @inference_lock_helper.unlock 286 | 287 | # If tool was activated with a pre-selection, keep the selection and let 288 | # the user pick a starting point anywhere. Otherwise, drop the selection 289 | # and select whatever the user clicks when starting next move operation. 290 | reset_tool unless @preselected 291 | end 292 | 293 | def pick_mouse_position(view) 294 | if !picked_first_point? 295 | @mouse_ip.pick(view, @mouse_position.x, @mouse_position.y) 296 | else 297 | # If we could, we would want the InputPoint to ignore entities in 298 | # @selection. We are currently getting some quite strange behavior 299 | # when we get inference from the entities we are already moving. 300 | # For instance you can't always move an object away from the camera, 301 | # as the entities being moved is in front of the point you'd want to 302 | # pick. 303 | # https://github.com/SketchUp/api-issue-tracker/issues/452 304 | @mouse_ip.pick(view, @mouse_position.x, @mouse_position.y, @picked_first_ip) 305 | @distance = @mouse_ip.position.distance(@picked_first_ip.position) 306 | update_ui 307 | 308 | do_move(view) 309 | end 310 | view.invalidate 311 | end 312 | 313 | def update_ui 314 | if @selection.empty? 315 | Sketchup.status_text = 'Select a component.' 316 | elsif !picked_first_point? 317 | Sketchup.status_text = 'Select start point.' 318 | else 319 | Sketchup.status_text = 'Select end point.' 320 | 321 | # Only update the VCB text if is different from when last set. 322 | # This prevents the text the user is currently writing from being 323 | # overridden at key events affecting the inference. 324 | Sketchup.vcb_value = @distance unless @vcb_cache == @distance 325 | @vcb_cache = @distance 326 | end 327 | 328 | Sketchup.vcb_label = 'Distance' 329 | end 330 | 331 | def reset_tool 332 | @picked_first_ip.clear 333 | @inference_lock_helper.unlock 334 | Sketchup.active_model.selection.clear 335 | @selection.clear 336 | @preselected = false 337 | 338 | update_ui 339 | end 340 | 341 | def picked_first_point? 342 | @picked_first_ip.valid? 343 | end 344 | 345 | def picked_points 346 | points = [] 347 | points << @picked_first_ip.position if picked_first_point? 348 | points << @mouse_ip.position if @mouse_ip.valid? 349 | points 350 | end 351 | 352 | def draw_preview_line(view) 353 | points = picked_points 354 | return unless points.size == 2 355 | 356 | # Currently there is an API bug that makes the line stipple scale with 357 | # the line width, which produces an inconsistent look from that of 358 | # native tools. 359 | # https://github.com/SketchUp/api-issue-tracker/issues/229 360 | view.line_width = view.inference_locked? ? 3 : 1 361 | view.set_color_from_line(*points) 362 | view.line_stipple = "_" 363 | view.draw(GL_LINES, points) 364 | end 365 | 366 | end 367 | 368 | def self.activate_move_tool 369 | Sketchup.active_model.select_tool(ComponentMoveTool.new) 370 | end 371 | 372 | unless file_loaded?(__FILE__) 373 | menu = UI.menu('Plugins') 374 | menu.add_item('99 In Tool Selection') { 375 | self.activate_move_tool 376 | } 377 | file_loaded(__FILE__) 378 | end 379 | 380 | end 381 | end 382 | -------------------------------------------------------------------------------- /examples/99_license/README.md: -------------------------------------------------------------------------------- 1 | # Hello License 2 | 3 | This examples demonstrates how to implement license checks to your Ruby 4 | extension using the Ruby API. 5 | 6 | To use the License API you need to be a vendor on Extension Warehouse. The 7 | extension must also be distributed via Extension Warehouse for the license 8 | to work. 9 | 10 | Developers can obtain test licenses via Extension Warehouse when logged into 11 | their vendor account. 12 | -------------------------------------------------------------------------------- /examples/99_license/ex_hello_license.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'extensions.rb' 6 | 7 | module Examples 8 | module HelloLicense 9 | 10 | unless file_loaded?(__FILE__) 11 | ex = SketchupExtension.new('Hello License', 'ex_hello_license/main') 12 | ex.description = 'SketchUp Ruby API example using the License API.' 13 | ex.version = '1.0.0' 14 | ex.copyright = 'Trimble Inc © 2018' 15 | ex.creator = 'SketchUp' 16 | Sketchup.register_extension(ex, true) 17 | file_loaded(__FILE__) 18 | end 19 | 20 | end # module HelloLicense 21 | end # module Examples 22 | -------------------------------------------------------------------------------- /examples/99_license/ex_hello_license/extension_info.txt: -------------------------------------------------------------------------------- 1 | ID=6cce9800-40b0-4dd9-9671-8d55a05ae1e8 2 | VERSION_ID=ea2d6fa8-02b1-438f-b91a-0709889fa156 3 | -------------------------------------------------------------------------------- /examples/99_license/ex_hello_license/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module HelloLicense 8 | 9 | def self.create_cube 10 | # Performing a license check before executing the extension's commands. 11 | # The extension id is kept as a local variable as constants and 12 | # class/instance variables are too easy to tamper with. 13 | ext_id = '6cce9800-40b0-4dd9-9671-8d55a05ae1e8' 14 | license = Sketchup::Licensing.get_extension_license(ext_id) 15 | unless license.licensed? 16 | UI::messagebox('Could not obtain a valid license.') 17 | return 18 | end 19 | 20 | # If license is ok then normal execution will proceed: 21 | model = Sketchup.active_model 22 | model.start_operation('Create Cube', true) 23 | group = model.entities.add_group 24 | entities = group.entities 25 | points = [ 26 | Geom::Point3d.new(0, 0, 0), 27 | Geom::Point3d.new(1.m, 0, 0), 28 | Geom::Point3d.new(1.m, 1.m, 0), 29 | Geom::Point3d.new(0, 1.m, 0) 30 | ] 31 | face = entities.add_face(points) 32 | face.pushpull(-1.m) 33 | model.commit_operation 34 | end 35 | 36 | unless file_loaded?(__FILE__) 37 | # Menus and toolbars are always visible, regardless of the extension's 38 | # license state. This is to avoid confusing for the user. This require 39 | # the commands themselves to have a license check. As seen in the example 40 | # above, it's recommended to provide a message back to the user. 41 | menu = UI.menu('Plugins') 42 | menu.add_item('99 Create Cube Example') { 43 | self.create_cube 44 | } 45 | file_loaded(__FILE__) 46 | end 47 | 48 | # Fetching a license here so that it will be checked by SketchUp during 49 | # startup. This will include the extension in the dialog that warns about 50 | # missing licenses. 51 | ext_id = '6cce9800-40b0-4dd9-9671-8d55a05ae1e8' 52 | ext_lic = Sketchup::Licensing.get_extension_license(ext_id) 53 | 54 | end # module HelloLicense 55 | end # module Examples 56 | -------------------------------------------------------------------------------- /examples/99_sphere_tool/README.md: -------------------------------------------------------------------------------- 1 | # Sphere Tool 2 | 3 | This examples demonstrates how to take user input from picking in the model as 4 | well as VCB input. 5 | 6 | * Draws a mesh preview to the viewport. 7 | * Accept user input for distance and segment count. 8 | * Creates a sphere using FollowMe. 9 | -------------------------------------------------------------------------------- /examples/99_sphere_tool/ex_sphere_tool.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | require 'extensions.rb' 6 | 7 | module Examples 8 | module SphereToolExample 9 | 10 | unless file_loaded?(__FILE__) 11 | ex = SketchupExtension.new('Sphere Tool Example', 'ex_sphere_tool/main') 12 | ex.description = 'SketchUp Ruby API example creating a sphere.' 13 | ex.version = '1.0.0' 14 | ex.copyright = 'Trimble Inc © 2018' 15 | ex.creator = 'SketchUp' 16 | Sketchup.register_extension(ex, true) 17 | file_loaded(__FILE__) 18 | end 19 | 20 | end # module SphereToolExample 21 | end # module Examples 22 | -------------------------------------------------------------------------------- /examples/99_sphere_tool/ex_sphere_tool/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module SphereToolExample 8 | 9 | class SphereTool 10 | 11 | def activate 12 | @num_segments = 24 13 | @mouse_ip = Sketchup::InputPoint.new 14 | @picked_first_ip = Sketchup::InputPoint.new 15 | update_ui 16 | end 17 | 18 | def deactivate(view) 19 | view.invalidate 20 | end 21 | 22 | def resume(view) 23 | update_ui 24 | view.invalidate 25 | end 26 | 27 | def suspend(view) 28 | view.invalidate 29 | end 30 | 31 | def onCancel(reason, view) 32 | reset_tool 33 | view.invalidate 34 | end 35 | 36 | def onMouseMove(flags, x, y, view) 37 | if picked_first_point? 38 | @mouse_ip.pick(view, x, y, @picked_first_ip) 39 | else 40 | @mouse_ip.pick(view, x, y) 41 | end 42 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 43 | update_ui 44 | view.invalidate 45 | end 46 | 47 | def onLButtonDown(flags, x, y, view) 48 | if picked_first_point? 49 | create_sphere 50 | else 51 | @picked_first_ip.copy!(@mouse_ip) 52 | end 53 | update_ui 54 | view.invalidate 55 | end 56 | 57 | def enableVCB? 58 | picked_first_point? 59 | end 60 | 61 | def onUserText(text, view) 62 | # Check if it's adjustments to number of segments. 63 | if text.end_with?('s') 64 | on_segment_change_input(text) 65 | update_ui 66 | return 67 | end 68 | # Ensure that the center of the sphere has been picked. 69 | return unless picked_first_point? && @mouse_ip.valid? 70 | # Try to parse the user input - this might fail, so rescue from errors. 71 | begin 72 | radius = text.to_l 73 | # Ensure the sphere actually have a dimension. 74 | raise ArgumentError if radius == 0.to_l 75 | rescue ArgumentError 76 | UI.beep 77 | view.tooltip = 'Invalid length entered.' 78 | return 79 | end 80 | # Compute the new radius from the user input. 81 | origin = @picked_first_ip.position 82 | vector = origin.vector_to(@mouse_ip) 83 | vector.length = radius 84 | tangent = origin.offset(vector, radius) 85 | # Everything ready to create the sphere. 86 | create_sphere 87 | update_ui 88 | view.invalidate 89 | end 90 | 91 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 92 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 93 | # with your own custom cursor bitmap: 94 | # 95 | # CURSOR_SPHERE = UI.create_cursor(cursor_path, 0, 0) 96 | CURSOR_CIRCLE = 1457 97 | def onSetCursor 98 | # Note that `onSetCursor` is called frequently so you should not do much 99 | # work here. At most you switch between different cursor representing 100 | # the state of the tool. 101 | UI.set_cursor(CURSOR_CIRCLE) 102 | end 103 | 104 | def draw(view) 105 | draw_preview(view) 106 | @mouse_ip.draw(view) if @mouse_ip.display? 107 | end 108 | 109 | def getExtents 110 | sphere_bounds 111 | end 112 | 113 | private 114 | 115 | def update_ui 116 | if picked_first_point? 117 | Sketchup.status_text = 'Select the radius of sphere or enter value' 118 | Sketchup.vcb_label = 'Radius' 119 | Sketchup.vcb_value = picked_distance 120 | else 121 | Sketchup.status_text = 'Select centre of sphere' 122 | Sketchup.vcb_label = 'Radius' 123 | Sketchup.vcb_value = '' 124 | end 125 | end 126 | 127 | def reset_tool 128 | @picked_first_ip.clear 129 | update_ui 130 | end 131 | 132 | def on_segment_change_input(text) 133 | segments = text.to_i # .to_i will strip out the trailing "s". 134 | valid_range = (3..999) # Matching SketchUp 2018 for circle segments. 135 | if valid_range.include?(segments) 136 | @num_segments = segments 137 | true 138 | else 139 | min = valid_range.min 140 | mmax = valid_range.mmax 141 | message = "Curve segments must be in the range from #{min} to #{max}" 142 | UI.messagebox(message) 143 | false 144 | end 145 | end 146 | 147 | def picked_first_point? 148 | @picked_first_ip.valid? 149 | end 150 | 151 | def picked_points 152 | points = [] 153 | points << @picked_first_ip.position if picked_first_point? 154 | points << @mouse_ip.position if @mouse_ip.valid? 155 | points 156 | end 157 | 158 | def picked_distance 159 | if @picked_first_ip.valid? && @mouse_ip.valid? 160 | @picked_first_ip.position.distance(@mouse_ip) 161 | else 162 | 0.to_l 163 | end 164 | end 165 | 166 | def draw_preview(view) 167 | points = picked_points 168 | return unless points.size == 2 169 | draw_picked_points(view, points) 170 | draw_sphere(view, points) 171 | end 172 | 173 | def draw_picked_points(view, points) 174 | view.set_color_from_line(*points) 175 | view.line_width = 1 176 | view.line_stipple = '' 177 | view.draw(GL_LINES, points) 178 | end 179 | 180 | def draw_sphere(view, points) 181 | origin, tangent = points 182 | x_axis = origin.vector_to(tangent) 183 | # Remember to take into account that the input points could be the same. 184 | return unless x_axis.valid? 185 | radius = x_axis.length 186 | loops = sphere_preview_points(origin, x_axis, radius, @num_segments) 187 | view.drawing_color = 'purple' 188 | view.line_width = 1 189 | view.line_stipple = '' 190 | loops.each { |loop| 191 | view.draw(GL_LINE_LOOP, loop) 192 | } 193 | end 194 | 195 | def create_sphere 196 | origin, tangent = picked_points 197 | radius = origin.distance(tangent) 198 | model = Sketchup.active_model 199 | model.start_operation('Create Sphere', true) 200 | group = model.active_entities.add_group 201 | # Create a filled circle which later will be revolved around itself to 202 | # create a sphere. 203 | num_segments = @num_segments 204 | circle = group.entities.add_circle(origin, X_AXIS, radius, num_segments) 205 | face = group.entities.add_face(circle) 206 | face.reverse! 207 | # Create a temporary path for follow me to use to perform the revolve. 208 | # This path should not touch the face. 209 | path = group.entities.add_circle(origin, Z_AXIS, radius * 2, num_segments) 210 | # This creates the sphere. 211 | face.followme(path) 212 | # The temporary path is no longer needed. 213 | group.entities.erase_entities(path) 214 | model.commit_operation 215 | # Prepare to allow new input for new spheres. 216 | reset_tool 217 | end 218 | 219 | # Creates a boundingbox covering the sphere to be drawn. Instead of 220 | # feeding it all the points, just compute the extremes in XYZ directions 221 | # from the sphere center. 222 | def sphere_bounds 223 | bounds = Geom::BoundingBox.new 224 | if @picked_first_ip.valid? && @mouse_ip.valid? 225 | origin = @picked_first_ip.position 226 | x_axis = origin.vector_to(@mouse_ip) 227 | return bounds unless x_axis.valid? 228 | 229 | y_axis = x_axis.axes.x 230 | y_axis.length = x_axis.length 231 | z_axis = x_axis.axes.y 232 | z_axis.length = x_axis.length 233 | bounds.add(origin.offset(x_axis)) 234 | bounds.add(origin.offset(x_axis.reverse)) 235 | bounds.add(origin.offset(y_axis)) 236 | bounds.add(origin.offset(y_axis.reverse)) 237 | bounds.add(origin.offset(z_axis)) 238 | bounds.add(origin.offset(z_axis.reverse)) 239 | end 240 | bounds 241 | end 242 | 243 | def sphere_preview_points(origin, x_axis, radius, segments = 24) 244 | circle = circle3d(x_axis, radius, segments) 245 | tr_origin = Geom::Transformation.new(origin) 246 | rotation_step = 360.degrees / segments 247 | # Longitude lines (Vertical) 248 | loops = segments.times.map { |i| 249 | angle = rotation_step * i 250 | tr_rotation = Geom::Transformation.rotation(ORIGIN, Z_AXIS, angle) 251 | tr = tr_origin * tr_rotation 252 | circle.map { |point| point.transform(tr) } 253 | } 254 | # Latitude lines (Horizontal) 255 | latitudes = [] 256 | segments.times { |i| 257 | latitudes << loops.map { |loop| loop[i] } 258 | } 259 | loops.concat(latitudes) 260 | loops 261 | end 262 | 263 | def circle3d(normal, radius, segments = 24) 264 | points = circle2d(radius, segments = 24) 265 | tr = Geom::Transformation.new(ORIGIN, normal) 266 | points.map { |point| point.transform(tr) } 267 | end 268 | 269 | def circle2d(radius, segments = 24) 270 | segment_angle = 360.degrees / segments 271 | arc = [] 272 | (0..segments).each { |i| 273 | angle = segment_angle * i 274 | x = radius * Math.cos(angle) 275 | y = radius * Math.sin(angle) 276 | arc << Geom::Point3d.new(x, y, 0) 277 | } 278 | arc 279 | end 280 | 281 | end # class SphereTool 282 | 283 | 284 | def self.activate_sphere_tool 285 | Sketchup.active_model.select_tool(SphereTool.new) 286 | end 287 | 288 | unless file_loaded?(__FILE__) 289 | menu = UI.menu('Plugins') 290 | menu.add_item('99 Sphere Tool Example') { 291 | self.activate_sphere_tool 292 | } 293 | file_loaded(__FILE__) 294 | end 295 | 296 | end # module SphereToolExample 297 | end # module Examples 298 | -------------------------------------------------------------------------------- /load_tutorials.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | module Examples 5 | 6 | # Finds and returns the filename for each of the root .rb files in the 7 | # examples folder. 8 | # 9 | # @yield [String] filename 10 | # 11 | # @return [Array] files 12 | def self.rb_files(include_subfolders = false) 13 | examples_path = File.join(__dir__, 'examples') 14 | folders = include_subfolders ? '**' : '*' 15 | examples_pattern = File.join(examples_path, folders, '*.rb') 16 | Dir.glob(examples_pattern).each { |filename| 17 | yield filename 18 | } 19 | end 20 | 21 | # Utility method to mute Ruby warnings for whatever is executed by the block. 22 | def self.mute_warnings(&block) 23 | old_verbose = $VERBOSE 24 | $VERBOSE = nil 25 | result = block.call 26 | ensure 27 | $VERBOSE = old_verbose 28 | result 29 | end 30 | 31 | # Utility method to quickly reload the tutorial files. Useful for development. 32 | # 33 | # @return [Integer] Number of files reloaded. 34 | def self.reload 35 | self.mute_warnings do 36 | load __FILE__ 37 | files = self.rb_files(true) { |filename| 38 | load filename 39 | } 40 | puts "Reloaded #{files.size} files" if $VERBOSE 41 | files.size + 1 42 | end 43 | end 44 | 45 | # This runs when this file is loaded and adds the location of each of the 46 | # tutorials folders to the load path such that the tutorials can be loaded 47 | # into SketchUp directly from the repository. 48 | self.rb_files { |filename| 49 | begin 50 | path = File.dirname(filename) 51 | $LOAD_PATH << path 52 | require filename 53 | rescue LoadError => error 54 | warn "Failed to load: #{filename}" 55 | warn error.inspect 56 | warn error.description 57 | end 58 | } 59 | 60 | end # module Examples 61 | -------------------------------------------------------------------------------- /tutorials/01_hello_cube/README.md: -------------------------------------------------------------------------------- 1 | # Hello Cube 2 | 3 | This demonstrates the basic structure of a SketchUp extension. 4 | 5 | * Registers an extension with SketchUp. 6 | * Adds menu item to trigger a command. 7 | * Creates a group with a cube. 8 | 9 | The code for this tutorial also exist under examples/01_hello_cube with all the 10 | comments removed. 11 | -------------------------------------------------------------------------------- /tutorials/01_hello_cube/tut_hello_cube.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate the required pattern for Extensions to be accepted to the 5 | # Extension Warehouse. 6 | # 7 | # Key requirements: 8 | # 9 | # * Register a SketchupExtension instance in the root .rb file. 10 | # 11 | # * Do not load anything else in the root .rb file - it should _only_ do 12 | # registration. 13 | # 14 | # * Confine all code for the extension into its own namespace (module). 15 | # This means that one should never use global variables, global constants 16 | # or global methods. It also means that you should not modify the Ruby API or 17 | # SketchUp API. 18 | 19 | # These are needed because we use some methods defined in these files which are 20 | # not automatically loaded by SketchUp. 21 | # 22 | # * sketchup.rb is needed for `file_loaded?` and `file_loaded`. 23 | # 24 | # * extensions.rb is needed for the `SketchupExtension` class. 25 | require 'sketchup.rb' 26 | require 'extensions.rb' 27 | 28 | # In order to make sure your extension doesn't affect other installed extensions 29 | # you need to put all your extension code within your own module. 30 | # 31 | # We recommend a pattern similar to this for best flexibility: 32 | # 33 | # module PublisherName 34 | # module ExtensionName 35 | # # ... 36 | # end 37 | # end 38 | # 39 | # This allows you to put all your extensions into a single module representing 40 | # you as developer/company. 41 | module Examples 42 | module HelloCube 43 | 44 | # The use of `file_loaded?` here is to prevent the extension from being 45 | # registered multiple times. This can happen for a number of reasons when 46 | # the file is reloaded - either when debugging during development or 47 | # extension updates etc. 48 | # 49 | # The `__FILE__` constant is a "magic" Ruby constant that returns a string 50 | # with the path to the current file. You don't have to use this constant 51 | # with `file_loaded?` - you can use any unique string to represent this 52 | # file. But `__FILE__` is very convenient for this. 53 | unless file_loaded?(__FILE__) 54 | 55 | # Here we define the extension. The two arguments is the extension name 56 | # and the file that should be loaded when the extension is enabled. 57 | # 58 | # Note that the file loaded (tut_hello_cube/main) must be in a folder 59 | # named with the same base name as this root file. 60 | # 61 | # Another thing to notice is that we omitted the .rb file extension and 62 | # wrote `tut_hello_world/main` instead of `tut_hello_world/main.rb`. 63 | # SketchUp is smart enough to find the file regardless and this is 64 | # required if you decide to later encrypt the extension. 65 | ex = SketchupExtension.new('Hello Cube', 'tut_hello_cube/main') 66 | 67 | # Next we add some information to the extension. This isn't required, but 68 | # highly recommended as it helps the user when managing her installed 69 | # extensions. 70 | ex.description = 'SketchUp Ruby API example creating a cube.' 71 | ex.version = '1.0.0' 72 | ex.copyright = 'Trimble Navigations © 2016' 73 | ex.creator = 'SketchUp' 74 | 75 | # Finally we tell SketchUp to register this extension. Remember to always 76 | # set the second argument to true - this tells SketchUp to load the 77 | # extension by default. Otherwise the user has to manually enable the 78 | # extension after installation. 79 | Sketchup.register_extension(ex, true) 80 | 81 | # This is needed for the load guard to prevent the extension being 82 | # registered multiple times. 83 | file_loaded(__FILE__) 84 | end 85 | 86 | end # module HelloCube 87 | end # module Examples 88 | -------------------------------------------------------------------------------- /tutorials/01_hello_cube/tut_hello_cube/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module HelloCube 8 | 9 | # This method creates a simple cube inside of a group in the model. 10 | def self.create_cube 11 | # We need a reference to the currently active model. The SketchUp API 12 | # currently only lets you work on the active model. Under Windows there 13 | # will be only one model open at a time, but under OS X there might be 14 | # multiple models open. 15 | # 16 | # Beware that if there is no model open under OS X then `active_model` 17 | # will return nil. In this example we ignore that for simplicity. 18 | model = Sketchup.active_model 19 | 20 | # Whenever you make changes to the model you must take care to use 21 | # `model.start_operation` and `model.commit_operation` to wrap everything 22 | # into a single undo step. Otherwise the user risks not being able to 23 | # undo everything and she may loose work. 24 | # 25 | # Making sure your model changes are undoable in a single undo step is a 26 | # requirement of the Extension Warehouse submission quality checks. 27 | # 28 | # Note that the first argument name is a string that will be appended to 29 | # the Edit > Undo menu - so make sure you name your operations something 30 | # the users can understand. 31 | model.start_operation('Create Cube', true) 32 | 33 | # Creating a group via the API is slightly different from creating a 34 | # group via the UI. Via the UI you create the faces first, then group 35 | # them. But with the API you create the group first and then add its 36 | # content directly to the group. 37 | group = model.active_entities.add_group 38 | entities = group.entities 39 | 40 | # Here we define a set of 3d points to create a 1x1m face. Note that the 41 | # internal unit in SketchUp is inches. This means that regardless of the 42 | # model unit settings the 3d data is always stored in inches. 43 | # 44 | # In order to make it easier work with lengths the Numeric class has 45 | # been extended with some utility methods that let us write stuff like 46 | # `1.m` to represent a meter instead of `39.37007874015748`. 47 | points = [ 48 | Geom::Point3d.new(0, 0, 0), 49 | Geom::Point3d.new(1.m, 0, 0), 50 | Geom::Point3d.new(1.m, 1.m, 0), 51 | Geom::Point3d.new(0, 1.m, 0) 52 | ] 53 | 54 | # We pass the points to the `add_face` method and keep the returned 55 | # reference to the face as we want to keep working with it. 56 | # 57 | # Note that normally the orientation (its normal) is a result of the order 58 | # of the 3d points you use to create it. The exception is when you create 59 | # a face on the ground plane (all points with z == 0) then it will always 60 | # be face down. 61 | face = entities.add_face(points) 62 | 63 | # Here we invoke SketchUp's push-pull functionality on the face. But note 64 | # that we must use a negative number in order for it to extrude upwards 65 | # in the positive direction of the Z-axis. This is because SketchUp 66 | # forced this face on the ground place to be face down. 67 | face.pushpull(-1.m) 68 | 69 | # Finally we are done and we close the operation. In production you will 70 | # want to catch errors and abort to clean up if your function failed. 71 | # But for simplicity we won't do this here. 72 | model.commit_operation 73 | end 74 | 75 | # Here we add a menu item for the extension. Note that we again use a 76 | # load guard to prevent multiple menu items from accidentally being 77 | # created. 78 | unless file_loaded?(__FILE__) 79 | 80 | # We fetch a reference to the top level menu we want to add to. Note that 81 | # we use "Plugins" here which was the old name of the "Extensions" menu. 82 | # By using "Plugins" you remain backwards compatible. 83 | menu = UI.menu('Plugins') 84 | 85 | # We add the menu item directly to the root of the menu in this example. 86 | # But if you plan to add multiple items per extension we recommend you 87 | # group them into a sub-menu in order to keep things organized. 88 | menu.add_item('01 Create Cube Example') { 89 | self.create_cube 90 | } 91 | 92 | file_loaded(__FILE__) 93 | end 94 | 95 | end # module HelloCube 96 | end # module Examples 97 | -------------------------------------------------------------------------------- /tutorials/02_custom_tool/README.md: -------------------------------------------------------------------------------- 1 | # Custom Tool 2 | 3 | This demonstrates the basics for making custom tools. It's a simplified version 4 | of the SketchUp Line Tool. 5 | 6 | * Defines a custom tool class. 7 | * Takes user input to draw edges. 8 | 9 | The code for this tutorial also exist under examples/02_custom_tool with all the 10 | comments removed. 11 | -------------------------------------------------------------------------------- /tutorials/02_custom_tool/tut_custom_tool.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | # This demonstrate how to create a custom Ruby tool that lets the user pick 5 | # points in the model to create a cube. 6 | 7 | require 'sketchup.rb' 8 | require 'extensions.rb' 9 | 10 | module Examples 11 | module CustomTool 12 | 13 | unless file_loaded?(__FILE__) 14 | ex = SketchupExtension.new('Custom Tool', 'tut_custom_tool/main') 15 | ex.description = 'SketchUp Ruby API example creating a custom tool.' 16 | ex.version = '1.0.0' 17 | ex.copyright = 'Trimble Navigations © 2016' 18 | ex.creator = 'SketchUp' 19 | Sketchup.register_extension(ex, true) 20 | file_loaded(__FILE__) 21 | end 22 | 23 | end # module CustomTool 24 | end # module Examples 25 | -------------------------------------------------------------------------------- /tutorials/02_custom_tool/tut_custom_tool/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Trimble Inc 2 | # Licensed under the MIT license 3 | 4 | require 'sketchup.rb' 5 | 6 | module Examples 7 | module CustomTool 8 | 9 | # Custom tools create a class which responds to various callback methods 10 | # from SketchUp. 11 | class LineTool 12 | 13 | # The `actvate` method is called whenever the tool is activated. This is 14 | # where you initialize and prepare your tool. 15 | # Note that this is different from Ruby's `initialize` method. You can 16 | # reuse Tool instances in which case `initialize` would not be the best 17 | # place to set up the tool. 18 | def activate 19 | 20 | # We will need to sample 3d points from the model as the user interacts 21 | # with the tool and the model. For this we use InputPoint which also 22 | # adds some inference-magic. 23 | # We need to sample the 3d point under the mouse cursor and keep a 24 | # reference of what the user clicks on. 25 | @mouse_ip = Sketchup::InputPoint.new 26 | @picked_first_ip = Sketchup::InputPoint.new 27 | 28 | # We make sure to call our utility method update_ui that updates 29 | # the statusbar with instructions on how to use the tool. 30 | update_ui 31 | end 32 | 33 | # This is called when the user switches to a different tool. It's 34 | # recommended to always call view.invalidate in order to make sure we 35 | # clear out any custom drawings done to the viewport. Otherwise these 36 | # drawings might linger around for a moment, confusing the user. 37 | def deactivate(view) 38 | view.invalidate 39 | end 40 | 41 | # Tools can be temporarily suspended and resumed. One example of this is 42 | # when the user uses the Orbit tool by pressing the middle mouse button. 43 | # In order to make sure we update our statusbar text and custom viewport 44 | # drawing we need to do that here. 45 | def resume(view) 46 | update_ui 47 | view.invalidate 48 | end 49 | 50 | def suspend(view) 51 | view.invalidate 52 | end 53 | 54 | # Tools can be interrupted for various reasons. In this example tool we 55 | # simply reset it regardless, but if you need finer granularity you can 56 | # check the reason code. 57 | # 58 | # 0: The user canceled the current operation by hitting the escape key. 59 | # 1: The user re-selected the same tool from the toolbar or menu. 60 | # 2: The user did an undo while the tool was active. 61 | def onCancel(reason, view) 62 | reset_tool 63 | view.invalidate 64 | end 65 | 66 | def onMouseMove(flags, x, y, view) 67 | # We want to sample the 3d point under the cursor as the user moves it. 68 | if picked_first_point? 69 | # When the user picks a start point we use that while picking in 70 | # order for SketchUp to do it's inference magic. Note that if you 71 | # want to allow the user to lock inferencing you need to implement 72 | # `view.lock_inference`. This will be described in a later tutorial. 73 | @mouse_ip.pick(view, x, y, @picked_first_ip) 74 | else 75 | # When the user hasn't picked a start point yet we just use the 76 | # x and y coordinates of the cursor. 77 | @mouse_ip.pick(view, x, y) 78 | end 79 | # Here we let SketchUp display its inferencing similar to how the 80 | # native tools do it. 81 | view.tooltip = @mouse_ip.tooltip if @mouse_ip.valid? 82 | # Lastly we want to ensure we update the view. 83 | view.invalidate 84 | end 85 | 86 | # When the user clicks in the viewport we want to create edges based on 87 | # the input points we have collected. 88 | def onLButtonDown(flags, x, y, view) 89 | if picked_first_point? && create_edge > 0 90 | # When the user have picked a start point and then picks another point 91 | # we create an edge and try to create new faces from that edge. 92 | # Like the native tool we reset the tool if it created new faces. 93 | reset_tool 94 | else 95 | # If no face was created we let the user chain new edges to the last 96 | # input point. 97 | @picked_first_ip.copy!(@mouse_ip) 98 | end 99 | 100 | # As always we want to update the statusbar text and view. 101 | update_ui 102 | view.invalidate 103 | end 104 | 105 | # Here we have hard coded a special ID for the pencil cursor in SketchUp. 106 | # Normally you would use `UI.create_cursor(cursor_path, 0, 0)` instead 107 | # with your own custom cursor bitmap: 108 | # 109 | # CURSOR_PENCIL = UI.create_cursor(cursor_path, 0, 0) 110 | CURSOR_PENCIL = 632 111 | def onSetCursor 112 | # Note that `onSetCursor` is called frequently so you should not do much 113 | # work here. At most you switch between different cursors representing 114 | # the state of the tool. 115 | UI.set_cursor(CURSOR_PENCIL) 116 | end 117 | 118 | # The `draw` method is called every time SketchUp updates the viewport. 119 | # You should take care to do as little work in this method as possible. 120 | # If you need to calculate things to draw it is best to cache the data in 121 | # order to get better frame rates. 122 | def draw(view) 123 | draw_preview(view) 124 | @mouse_ip.draw(view) if @mouse_ip.display? 125 | end 126 | 127 | # When you use `view.draw` and draw things outside the boundingbox of 128 | # the existing model geometry you will see that things get clipped. 129 | # In order to make sure everything you draw is visible you must return 130 | # a boundingbox here which defines the 3d model space you draw to. 131 | def getExtents 132 | bounds = Geom::BoundingBox.new 133 | bounds.add(picked_points) 134 | bounds 135 | end 136 | 137 | # In this example we put all the logic in the tool class itself. For more 138 | # complex tools you probably want to move that logic into its own class 139 | # in order to reduce complexity. If you are familiar with the MVC pattern 140 | # then consider a tool class a controller - you want to keep it short and 141 | # simple. 142 | private 143 | 144 | def update_ui 145 | if picked_first_point? 146 | Sketchup.status_text = 'Select end point.' 147 | else 148 | Sketchup.status_text = 'Select start point.' 149 | end 150 | end 151 | 152 | def reset_tool 153 | @picked_first_ip.clear 154 | update_ui 155 | end 156 | 157 | def picked_first_point? 158 | @picked_first_ip.valid? 159 | end 160 | 161 | def picked_points 162 | points = [] 163 | points << @picked_first_ip.position if picked_first_point? 164 | points << @mouse_ip.position if @mouse_ip.valid? 165 | points 166 | end 167 | 168 | def draw_preview(view) 169 | points = picked_points 170 | return unless points.size == 2 171 | view.set_color_from_line(*points) 172 | view.line_width = 1 173 | view.line_stipple = '' 174 | view.draw(GL_LINES, points) 175 | end 176 | 177 | # Returns the number of created faces. 178 | def create_edge 179 | model = Sketchup.active_model 180 | model.start_operation('Edge', true) 181 | edge = model.active_entities.add_line(picked_points) 182 | num_faces = edge.find_faces || 0 # API returns nil instead of 0. 183 | model.commit_operation 184 | 185 | num_faces 186 | end 187 | 188 | end # class LineTool 189 | 190 | 191 | # A utility method to activate the tool. This is defined here for easy 192 | # reuse as well as making it easier to debug while developing. If this code 193 | # was directly in the `add_item` block and you needed to make changes to 194 | # how you activate the tool then it would not take effect until you 195 | # restarted SketchUp due to the load guard. 196 | def self.activate_line_tool 197 | Sketchup.active_model.select_tool(LineTool.new) 198 | end 199 | 200 | unless file_loaded?(__FILE__) 201 | menu = UI.menu('Plugins') 202 | menu.add_item('02 Custom Tool Example') { 203 | self.activate_line_tool 204 | } 205 | file_loaded(__FILE__) 206 | end 207 | 208 | end # module CustomTool 209 | end # module Examples 210 | --------------------------------------------------------------------------------