├── src └── setup.bat ├── skippy.json ├── .yardopts ├── .gitignore ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .editorconfig ├── modules ├── tool_constants.rb ├── view_constants.rb ├── boundingbox_constants.rb ├── object_utils.rb ├── skippylib.rb ├── pick_helper.rb ├── color.rb ├── geometry.rb ├── plane.rb ├── platform.rb ├── resource.rb ├── image.rb ├── uv │ └── mapping.rb ├── image_rep.rb ├── command.rb ├── line.rb ├── boundingbox.rb ├── uv.rb ├── drawing_cache.rb ├── drawing_helper.rb └── c_extension_manager.rb ├── Gemfile ├── .solargraph.yml ├── tools ├── run_coverage.rb ├── generate_testup_coverage.rb └── coverage.rb ├── yard-tools ├── versions │ └── fulldoc │ │ └── text │ │ └── setup.rb └── coverage │ └── fulldoc │ └── text │ └── setup.rb ├── .rubocop_todo.yml ├── LICENSE ├── .rubocop.yml ├── tests └── TT_Lib │ ├── TC_Geometry.rb │ ├── TC_UV.rb │ ├── TC_Color.rb │ ├── coverage.manifest │ ├── TC_UVQ.rb │ └── TC_Line.rb ├── README.md └── .rubocop_new_cops.yml /src/setup.bat: -------------------------------------------------------------------------------- 1 | mklink /D .\modules ..\modules 2 | -------------------------------------------------------------------------------- /skippy.json: -------------------------------------------------------------------------------- 1 | { 2 | "library": true, 3 | "name": "tt-lib", 4 | "version": "3.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "tt-lib³" 2 | --no-private 3 | --tag sketchup:SketchUp 4 | --markup markdown 5 | modules/**/*.rb 6 | - 7 | README.md 8 | LICENSE 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Archives 2 | *.rbz 3 | 4 | # RuboCop 5 | .rubocop-https-*-yml 6 | 7 | # YARD 8 | /.yardoc 9 | /doc 10 | 11 | # SimpleCov 12 | /coverage 13 | 14 | # Symlinks 15 | /src/modules 16 | 17 | # Bundler 18 | Gemfile.lock 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solargraph.diagnostics": true, 3 | "files.exclude": { 4 | "src/modules/**": true 5 | }, 6 | "cSpell.words": [ 7 | "Sketchup", 8 | "boundingbox", 9 | "messagebox", 10 | "polyline", 11 | "rubocop", 12 | "subclassing" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; Indicate this is the root of the project. 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{html,js,md,rb}] 11 | charset = utf-8 12 | 13 | [*.{md,txt}] 14 | max_line_length = 80 15 | -------------------------------------------------------------------------------- /modules/tool_constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module ToolConstants 6 | 7 | # Constants for Tool.onCancel 8 | 9 | REASON_ESC = 0 10 | REASON_REACTIVATE = 1 11 | REASON_UNDO = 2 12 | 13 | end 14 | end # module 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | group :development do 6 | gem 'minitest' 7 | gem 'rubocop', '>= 0.82', '< 2.0' 8 | gem 'rubocop-minitest' 9 | gem 'rubocop-sketchup', '~> 1.4.0' 10 | gem 'sketchup-api-stubs' 11 | gem 'skippy', '~> 0.5.2.a' 12 | gem 'yard', '~> 0.9.16' 13 | end 14 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | require_paths: 2 | - "C:/Program Files/SketchUp/SketchUp 2023/Tools" 3 | - "C:/Program Files/SketchUp/SketchUp 2022/Tools" 4 | - "C:/Program Files/SketchUp/SketchUp 2021/Tools" 5 | - "C:/Program Files/SketchUp/SketchUp 2020/Tools" 6 | - "C:/Program Files/SketchUp/SketchUp 2019/Tools" 7 | - src 8 | 9 | require: 10 | - sketchup-api-stubs 11 | 12 | reporters: 13 | - rubocop 14 | -------------------------------------------------------------------------------- /modules/view_constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module ViewConstants 6 | 7 | # Constants for Sketchup::View.draw_points 8 | 9 | DRAW_OPEN_SQUARE = 1 10 | DRAW_FILLED_SQUARE = 2 11 | DRAW_PLUS = 3 12 | DRAW_CROSS = 4 13 | DRAW_STAR = 5 14 | DRAW_OPEN_TRIANGLE = 6 15 | DRAW_FILLED_TRIANGLE = 7 16 | 17 | end 18 | end # module 19 | -------------------------------------------------------------------------------- /tools/run_coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # TODO: Support Mac 4 | # TODO: Support argument for SketchUp version. 5 | 6 | solution_path = File.expand_path('..', __dir__) 7 | 8 | sketchup = 'C:\\Program Files\\SketchUp\\SketchUp 2018\\SketchUp.exe' 9 | script = File.join(__dir__, 'coverage.rb') 10 | 11 | command = %("#{sketchup}" -RubyStartup "#{script}") 12 | 13 | Dir.chdir(solution_path) do 14 | id = spawn(command) 15 | Process.detach(id) 16 | end 17 | -------------------------------------------------------------------------------- /modules/boundingbox_constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module BoundingBoxConstants 6 | 7 | # Constants for Geom::BoundingBox.corner 8 | 9 | BOTTOM_FRONT_LEFT = 0 10 | BOTTOM_FRONT_RIGHT = 1 11 | BOTTOM_BACK_RIGHT = 2 12 | BOTTOM_BACK_LEFT = 3 13 | 14 | TOP_FRONT_LEFT = 4 15 | TOP_FRONT_RIGHT = 5 16 | TOP_BACK_RIGHT = 6 17 | TOP_BACK_LEFT = 7 18 | 19 | end 20 | end # module 21 | -------------------------------------------------------------------------------- /tools/generate_testup_coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | solution_path = File.expand_path('..', __dir__) 6 | tests_path = File.join(solution_path, 'tests', 'TT_Lib') 7 | 8 | command = %(yardoc -t coverage -f text -p yard-tools) 9 | 10 | Dir.chdir(solution_path) do 11 | system(command) 12 | end 13 | 14 | source = File.join(solution_path, 'coverage.manifest') 15 | target = tests_path 16 | FileUtils.move(source, target, force: true, verbose: true) 17 | -------------------------------------------------------------------------------- /yard-tools/versions/fulldoc/text/setup.rb: -------------------------------------------------------------------------------- 1 | # TODO: Copied from sketchup-yard-template. Convert into reusable gem. 2 | 3 | require 'set' 4 | 5 | include Helpers::ModuleHelper 6 | 7 | def init 8 | find_all_versions 9 | end 10 | 11 | 12 | def all_objects 13 | run_verifier(Registry.all) 14 | end 15 | 16 | 17 | def find_all_versions 18 | versions = Set.new 19 | all_objects.each { |object| 20 | version_tag = object.tag(:since) 21 | versions << version_tag.text if version_tag 22 | } 23 | puts versions.sort.join("\n") 24 | exit # Avoid the YARD summary 25 | end 26 | -------------------------------------------------------------------------------- /.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 | "name": "Listen for rdebug-ide", 9 | "type": "Ruby", 10 | "request": "attach", 11 | // "preLaunchTask": "Debug in SketchUp 2017", 12 | "cwd": "${workspaceRoot}", 13 | "remoteHost": "127.0.0.1", 14 | "remotePort": "7000", 15 | "remoteWorkspaceRoot": "${workspaceRoot}" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /modules/object_utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module ObjectUtils 6 | 7 | private 8 | 9 | # @return [String] 10 | # @since 3.0.0 11 | def object_id_hex 12 | format('0x%.16x', id: self.object_id) 13 | end 14 | 15 | # @param [Hash] extra 16 | # @return [String] 17 | # @since 3.0.0 18 | def inspect_object(extra = {}) 19 | meta = extra.map { |k, v| "#{k}: #{v}" }.join(' ') 20 | meta = " #{meta}" unless meta.empty? 21 | hex_id = object_id_hex 22 | "#<#{self.class.name}:#{hex_id}#{meta}>" 23 | end 24 | 25 | end # module 26 | end # module 27 | -------------------------------------------------------------------------------- /modules/skippylib.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # tt-lib³ is a a `skippy` library for SketchUp. 4 | # 5 | # Pick and choose what modules and classes you need for your SketchUp extension 6 | # project. 7 | # 8 | # You can use `skippy` to automate the management of your library dependency or 9 | # you can manually copy+paste what you need. 10 | # 11 | # @sketchup 2014 12 | module SkippyLib 13 | 14 | # @private 15 | # @since 3.0.0 16 | def self.reload 17 | original_verbose = $VERBOSE 18 | $VERBOSE = nil 19 | load __FILE__ # rubocop:disable SketchupSuggestions/FileEncoding 20 | pattern = "#{__dir__}/**/*.rb" 21 | Dir.glob(pattern) { |file| 22 | load file 23 | } 24 | ensure 25 | $VERBOSE = original_verbose 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /modules/pick_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module PickHelper 6 | 7 | # @param [Sketchup::View] view 8 | def self.new(view) 9 | # TODO: Don't do this! `Sketchup::PickHelper` objects are reused. Cannot 10 | # use `extend` as it with modify the global persistent object. 11 | raise NotImplementedError 12 | # pick_helper = view.pick_helper 13 | # pick_helper.extend(self) 14 | # pick_helper 15 | end 16 | 17 | # Return unique set of leaves from the pick paths. 18 | # @return [Array] 19 | def leaves 20 | all_picked.uniq 21 | end 22 | 23 | # @return [Array>] 24 | def paths 25 | Array.new(count) { |i| path_at(i) } 26 | end 27 | 28 | end 29 | end # module 30 | -------------------------------------------------------------------------------- /.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 2017", 8 | "type": "shell", 9 | "command": "skippy", 10 | "args": [ 11 | "sketchup:debug", 12 | "2017" 13 | ], 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "label": "Debug in SketchUp 2018", 18 | "type": "shell", 19 | "command": "skippy", 20 | "args": [ 21 | "sketchup:debug", 22 | "2018" 23 | ], 24 | "problemMatcher": [] 25 | }, 26 | { 27 | "label": "Debug in SketchUp 2019", 28 | "type": "shell", 29 | "command": "skippy", 30 | "args": [ 31 | "sketchup:debug", 32 | "2019" 33 | ], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /modules/color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modules/color' 4 | 5 | module SkippyLib 6 | # @since 3.0.0 7 | class Color < Sketchup::Color 8 | 9 | # @since 3.0.0 10 | alias r red 11 | 12 | # @since 3.0.0 13 | alias g green 14 | 15 | # @since 3.0.0 16 | alias b blue 17 | 18 | # @since 3.0.0 19 | alias a alpha 20 | 21 | # @since 3.0.0 22 | def grayscale? 23 | red == green && green == blue 24 | end 25 | 26 | # @return [Integer] Value between 0 - 255 27 | # @since 3.0.0 28 | def luminance 29 | # Colorimetric conversion to grayscale. 30 | # Original: 31 | # http://forums.sketchucation.com/viewtopic.php?t=12368#p88865 32 | # (red * 0.3) + (green * 0.59) + (blue * 0.11) 33 | # Current: https://stackoverflow.com/a/596243/486990 34 | # => https://www.w3.org/TR/AERT/#color-contrast 35 | ((red * 299) + (green * 587) + (blue * 114)) / 1000 36 | end 37 | 38 | end 39 | end # module 40 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-12-12 17:33:07 +0100 using RuboCop version 0.61.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: CountComments, ExcludedMethods. 11 | Metrics/MethodLength: 12 | Exclude: 13 | - 'modules/c_extension_manager.rb' 14 | 15 | # Offense count: 1 16 | # Cop supports --auto-correct. 17 | # Configuration parameters: EnforcedStyle. 18 | # SupportedStyles: implicit, explicit 19 | Style/RescueStandardError: 20 | Exclude: 21 | - 'modules/c_extension_manager.rb' 22 | 23 | # Offense count: 1 24 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 25 | # URISchemes: http, https 26 | Layout/LineLength: 27 | Exclude: 28 | - 'modules/c_extension_manager.rb' 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2023 Thomas Thomassen 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 | -------------------------------------------------------------------------------- /modules/geometry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module Geometry 6 | 7 | extend self 8 | 9 | # @overload midpoint(edge) 10 | # @param [Sketchup::Edge] edge 11 | # @return [Geom::Point3d] 12 | # 13 | # @overload midpoint(point1, point2) 14 | # @param [Geom::Point3d] point1 15 | # @param [Geom::Point3d] point2 16 | # @return [Geom::Point3d] 17 | # 18 | # @since 3.0.0 19 | def mid_point(*args) 20 | case args.size 21 | when 1 # Edge 22 | points = args.first.vertices.map(&:position) 23 | when 2 # Points 24 | points = args 25 | else 26 | raise ArgumentError, "wrong number of arguments (#{args.size} for 1..2)" 27 | end 28 | Geom.linear_combination(0.5, points.first, 0.5, points.last) 29 | end 30 | 31 | # @param [Array] points 32 | # @param [Geom::Vector3d] vector 33 | # @return [Array] 34 | # @since 3.0.0 35 | def offset_points(points, vector) 36 | points.map { |point| point.offset(vector) } 37 | end 38 | 39 | end # class 40 | end # module 41 | -------------------------------------------------------------------------------- /yard-tools/coverage/fulldoc/text/setup.rb: -------------------------------------------------------------------------------- 1 | # TODO: Copied from sketchup-yard-template. Convert into reusable gem. 2 | 3 | require 'fileutils' 4 | require 'set' 5 | 6 | include Helpers::ModuleHelper 7 | 8 | MANIFEST_FILENAME = 'coverage.manifest'.freeze 9 | 10 | def init 11 | generate_manifest 12 | end 13 | 14 | 15 | def namespace_objects 16 | run_verifier(Registry.all(:class, :module)) 17 | end 18 | 19 | 20 | def generate_manifest 21 | puts "Generating #{MANIFEST_FILENAME}..." 22 | methods = Set.new 23 | namespace_objects.each do |object| 24 | run_verifier(object.meths).each { |method| 25 | # TODO(thomthom): Currently the manifest doesn't distinguish between 26 | # class and instance methods. This should be addressed, but we need to 27 | # update TestUp to handle this first. 28 | methods << "#{method.namespace}.#{method.name}" 29 | } 30 | end 31 | manifest = methods.sort.join("\n") 32 | # TODO(thomthom): Add an option for the output path so this can be put 33 | # directly into the Ruby API test folder. 34 | manifest_path = File.join(Dir.pwd, MANIFEST_FILENAME) 35 | puts manifest_path 36 | File.write(manifest_path, manifest) 37 | end 38 | -------------------------------------------------------------------------------- /modules/plane.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | class Plane < Array 6 | 7 | # @param [Geom::Point3d] point 8 | # @param [Geom::Vector3d] vector 9 | # @since 3.0.0 10 | def initialize(point, vector) 11 | super() 12 | push(point) 13 | push(vector) 14 | end 15 | 16 | # Hide Array modifiers. 17 | private :push, :<<, :pop, :shift, :unshift, :[]=, :concat 18 | private :delete, :delete_at, :delete_if, :drop, :drop_while, :fill 19 | private :collect!, :compact!, :flatten!, :map!, :normalize!, :offset!, 20 | :reject!, :reverse!, :rotate!, :select!, :shuffle!, :slice!, :sort!, 21 | :sort_by!, :transform!, :uniq! 22 | 23 | # Hide SketchUp API extension to the Array class that doesn't make sense for 24 | # this class. 25 | private :x, :y, :z 26 | private :cross, :dot 27 | private :distance, :distance_to_line, :distance_to_plane 28 | private :offset, :vector_to 29 | private :normalize 30 | private :on_line?, :on_plane? 31 | 32 | # @since 3.0.0 33 | def inspect 34 | "#{self.class.name}#{super}" 35 | end 36 | 37 | end 38 | end # module 39 | -------------------------------------------------------------------------------- /modules/platform.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module Platform 6 | 7 | # @since 3.0.0 8 | def self.mac? 9 | Sketchup.platform == :platform_osx 10 | end 11 | 12 | # @since 3.0.0 13 | def self.win? 14 | Sketchup.platform == :platform_win 15 | end 16 | 17 | # @since 3.0.0 18 | POINTER_SIZE = ['a'].pack('P').size * 8 19 | 20 | # @since 3.0.0 21 | KEY = (self.mac? ? 'win' : 'osx').freeze 22 | 23 | # @since 3.0.0 24 | ID = "#{KEY}#{POINTER_SIZE}" 25 | 26 | # @since 3.0.0 27 | NAME = (self.mac? ? 'macOS' : 'Windows').freeze 28 | 29 | # @since 3.0.0 30 | IS_MAC = self.mac? 31 | 32 | # @since 3.0.0 33 | IS_WIN = self.win? 34 | 35 | # @return [String] 36 | # @since 3.0.0 37 | def self.temp_path 38 | paths = [ 39 | Sketchup.temp_dir, 40 | ENV.fetch('TMPDIR', nil), 41 | ENV.fetch('TMP', nil), 42 | ENV.fetch('TEMP', nil), 43 | ] 44 | temp = paths.find { |path| File.exist?(path) } 45 | raise 'Unable to locate temp directory' if temp.nil? 46 | 47 | File.expand_path(temp) 48 | end 49 | 50 | end # class 51 | end # module 52 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-minitest 3 | - rubocop-sketchup 4 | 5 | inherit_from: 6 | - https://raw.githubusercontent.com/SketchUp/rubocop-sketchup/master/sketchup-style.yml 7 | - .rubocop_todo.yml 8 | - .rubocop_new_cops.yml 9 | 10 | AllCops: 11 | DisplayCopNames: true 12 | DisplayStyleGuide: true 13 | ExtraDetails: true 14 | Exclude: 15 | - src/*/vendor/**/* 16 | - yard-tools/**/* 17 | SketchUp: 18 | SourcePath: modules 19 | TargetSketchUpVersion: 2014 20 | Exclude: 21 | - skippy/**/* 22 | - tests/**/* 23 | - tools/**/* 24 | TargetRubyVersion: 2.0 25 | 26 | 27 | # Doesn't apply to this project because it's not an extension. 28 | SketchupRequirements/FileStructure: 29 | Enabled: false 30 | 31 | SketchupRequirements/SketchupExtension: 32 | Enabled: false 33 | 34 | 35 | Naming/AccessorMethodName: 36 | Exclude: 37 | - modules/command.rb # This method reads clearer with set_ 38 | 39 | Naming/ClassAndModuleCamelCase: 40 | Exclude: 41 | - tests/**/* 42 | 43 | Naming/FileName: 44 | Exclude: 45 | - tests/**/* 46 | 47 | Naming/MethodName: 48 | Exclude: 49 | - tests/**/* 50 | 51 | 52 | # These hides methods of the parent class. 53 | Style/AccessModifierDeclarations: 54 | Exclude: 55 | - modules/line.rb 56 | - modules/plane.rb 57 | -------------------------------------------------------------------------------- /modules/resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sketchup.require 'modules/platform' 4 | 5 | module SkippyLib 6 | # @since 3.0.0 7 | module Resource 8 | 9 | # The supported file format for vector icons depend on the platform. 10 | # @since 3.0.0 11 | VECTOR_FILETYPE = (Platform.mac? ? 'pdf' : 'svg').freeze 12 | 13 | # Provide the path to a bitmap icon, which will be used for older SketchUp 14 | # versions that doesn't support vector formats. 15 | # 16 | # For versions that do support vector formats it will return the path with 17 | # the file extension that is compatible with the running OS. 18 | # 19 | # If the vector variant doesn't exist the original path will be returned. 20 | # 21 | # @param [String] path 22 | # @return [String] 23 | # @since 3.0.0 24 | def self.icon_path(path) 25 | return path unless Sketchup.version.to_i > 15 26 | 27 | vector_icon = self.vector_path(path) 28 | File.exist?(vector_icon) ? vector_icon : path 29 | end 30 | 31 | # @param [String] path 32 | # @return [String] 33 | # @since 3.0.0 34 | def self.vector_path(path) 35 | dir = File.dirname(path) 36 | basename = File.basename(path, '.*') 37 | File.join(dir, "#{basename}.#{VECTOR_FILETYPE}") 38 | end 39 | 40 | end # module 41 | end # module 42 | -------------------------------------------------------------------------------- /modules/image.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | module Image 6 | 7 | # This module is deliberately not a mix-in. 8 | 9 | # @param [Sketchup::Image] image 10 | # @return [Sketchup::Material] 11 | # @sketchup 2018 12 | # @since 3.0.0 13 | def self.clone_material(image, name: nil, force: false) 14 | definition = self.definition(image) 15 | material_name = name || "Copy of #{definition.name}" 16 | model = image.model 17 | # Reuse existing materials if it exists. Only checking for existing names. 18 | if force 19 | material = model.materials[material_name] 20 | return material if material 21 | end 22 | # Note that materials.add create unique names on demand. 23 | material = model.materials.add(material_name) 24 | # rubocop:disable SketchupSuggestions/Compatibility 25 | material.texture = image.image_rep 26 | # rubocop:enable SketchupSuggestions/Compatibility 27 | material 28 | end 29 | 30 | # @param [Sketchup::Image] image 31 | # @return [Sketchup::ComponentDefinition] 32 | # @since 3.0.0 33 | def self.definition(image) 34 | image.model.definitions.find { |definition| 35 | definition.image? && definition.instances.include?(image) 36 | } 37 | end 38 | 39 | end # module 40 | end # module 41 | -------------------------------------------------------------------------------- /modules/uv/mapping.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # Simple structure to make it more readable to build the UV mapping array 5 | # for `Sketchup::Face#position_material`. 6 | # 7 | # @example 8 | # mapping = UVMapping.new 9 | # mapping.add(points[0], UV.new(0, 0)) 10 | # mapping.add(points[1], UV.new(1, 0)) 11 | # mapping.add(points[2], UV.new(1, 1)) 12 | # mapping.add(points[3], UV.new(0, 1)) 13 | # face.position_material(material, mapping.to_a, true) 14 | # 15 | # @since 3.0.0 16 | class UVMapping 17 | 18 | # @since 3.0.0 19 | def initialize 20 | @mapping = [] 21 | end 22 | 23 | # @param [Geom::Point3d] model_point 24 | # @param [UV] uv 25 | # @return [nil] 26 | # @since 3.0.0 27 | def add(model_point, uv) 28 | @mapping << model_point 29 | @mapping << uv 30 | nil 31 | end 32 | 33 | # @return [Array] 34 | # @since 3.0.0 35 | def to_a 36 | @mapping 37 | end 38 | 39 | # @return [Array] 40 | # @since 3.0.0 41 | def to_ary 42 | # TODO: Remove this? This object doesn't really behave like an array. 43 | # It was added as an attempt to avoid calling .to_a when passing on to 44 | # face.position_material. 45 | @mapping 46 | end 47 | 48 | end # class 49 | end # module 50 | -------------------------------------------------------------------------------- /tools/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | puts 'COVERAGE: Preparing to run coverage tests...' 4 | 5 | puts "COVERAGE: Working Directory: #{Dir.pwd} (#{Dir.getwd})" 6 | 7 | solution_path = File.expand_path('..', __dir__) 8 | ruby_source_path = File.join(solution_path, 'src') 9 | ruby_tests_path = File.join(solution_path, 'tests') 10 | test_suite_path = File.join(ruby_tests_path, 'TT_Lib') 11 | 12 | puts 'COVERAGE: Loading SimpleCov...' 13 | 14 | $LOAD_PATH << ruby_source_path 15 | pattern = "#{ruby_source_path}/modules/**/*.rb" 16 | 17 | # https://github.com/colszowka/simplecov 18 | require 'simplecov' 19 | SimpleCov.root(solution_path) 20 | SimpleCov.start do 21 | track_files pattern 22 | 23 | add_filter '/tests/' 24 | add_filter 'skippylib.rb' 25 | 26 | # TODO: Set up groups. 27 | # add_group 'Debugging', 'src/tt_shadow_texture/debugging' 28 | # add_group 'Image', 'src/tt_shadow_texture/image' 29 | # add_group 'Shadows', /\/shadow_/ 30 | end 31 | 32 | # Using a timer to allow SketchUp to fully boot up before running the tests. 33 | done = false 34 | UI.start_timer(0.0, false) { 35 | next if done 36 | 37 | done = true 38 | 39 | puts 'COVERAGE: Loading TestUp...' 40 | 41 | require 'testup' 42 | # TODO: Check TestUp version. Requires 2.3 or newer. 43 | 44 | puts 'COVERAGE: Running tests...' 45 | 46 | TestUp::API.run_suite_without_gui(test_suite_path) 47 | 48 | puts 'COVERAGE: Terminating...' 49 | 50 | Sketchup.active_model.close(true) # Force close the test model. 51 | Sketchup.quit 52 | } 53 | -------------------------------------------------------------------------------- /tests/TT_Lib/TC_Geometry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'testup/testcase' 4 | 5 | require 'modules/geometry' 6 | 7 | module SkippyLib 8 | class TC_Geometry < TestUp::TestCase 9 | 10 | def setup 11 | # ... 12 | end 13 | 14 | def teardown 15 | # ... 16 | end 17 | 18 | 19 | def test_mixin 20 | klass = Class.new do 21 | include Geometry 22 | end 23 | klass.is_a?(Geometry) 24 | klass.new.mid_point([0, 0, 0], [2, 2, 2]) 25 | end 26 | 27 | 28 | def test_mid_point_points 29 | point1 = Geom::Point3d.new(1, 2, 4) 30 | point2 = Geom::Point3d.new(4, 12, 8) 31 | mid_point = Geometry.mid_point(point1, point2) 32 | assert_kind_of(Geom::Point3d, mid_point) 33 | assert_equal(Geom::Point3d.new(2.5, 7, 6), mid_point) 34 | end 35 | 36 | def test_mid_point_edge 37 | model = start_with_empty_model 38 | point1 = Geom::Point3d.new(1, 2, 4) 39 | point2 = Geom::Point3d.new(4, 12, 8) 40 | edge = model.entities.add_line(point1, point2) 41 | mid_point = Geometry.mid_point(edge) 42 | assert_kind_of(Geom::Point3d, mid_point) 43 | assert_equal(Geom::Point3d.new(2.5, 7, 6), mid_point) 44 | end 45 | 46 | def test_mid_point_too_few_arguments 47 | assert_raises(ArgumentError) do 48 | Geometry.mid_point 49 | end 50 | end 51 | 52 | def test_mid_point_too_many_arguments 53 | assert_raises(ArgumentError) do 54 | Geometry.mid_point([0, 0, 0], [1, 1, 1], [2, 2, 2]) 55 | end 56 | end 57 | 58 | 59 | def test_offset_points 60 | points = [ 61 | Geom::Point3d.new(0, 0, 0), 62 | Geom::Point3d.new(2, 5, 8), 63 | Geom::Point3d.new(6, 2, 3), 64 | ] 65 | vector = Geom::Vector3d.new(1, 2, -2) 66 | result = Geometry.offset_points(points, vector) 67 | expected = [ 68 | Geom::Point3d.new(1, 2, -2), 69 | Geom::Point3d.new(3, 7, 6), 70 | Geom::Point3d.new(7, 4, 1), 71 | ] 72 | assert_kind_of(Array, result) 73 | result.each { |point| 74 | assert_kind_of(Geom::Point3d, point) 75 | } 76 | assert_equal(expected, result) 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /modules/image_rep.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sketchup.require 'modules/platform' 4 | 5 | module SkippyLib 6 | # @sketchup 2018 7 | # @since 3.0.0 8 | module ImageRepHelper 9 | 10 | extend self 11 | 12 | # @param [Integer] width 13 | # @param [Integer] height 14 | # @param [Array] colors 15 | # @return [Sketchup::ImageRep] 16 | # @since 3.0.0 17 | def self.colors_to_image_rep(width, height, colors) 18 | row_padding = 0 19 | bits_per_pixel = 32 20 | pixel_data = self.colors_to_32bit_bytes(colors) 21 | # rubocop:disable SketchupSuggestions/Compatibility 22 | image_rep = Sketchup::ImageRep.new 23 | # rubocop:enable SketchupSuggestions/Compatibility 24 | image_rep.set_data(width, height, bits_per_pixel, row_padding, pixel_data) 25 | image_rep 26 | end 27 | 28 | # From C API documentation on SUColorOrder 29 | # 30 | # > SketchUpAPI expects the channels to be in different orders on 31 | # > Windows vs. Mac OS. Bitmap data is exposed in BGRA and RGBA byte 32 | # > orders on Windows and Mac OS, respectively. 33 | # 34 | # @param [Array] color 35 | # @return [Array(Integer, Integer, Integer, Integer)] RGBA/BGRA 36 | # @since 3.0.0 37 | def self.color_to_32bit(color) 38 | r, g, b, a = color.to_a 39 | Platform::IS_WIN ? [b, g, r, a] : [r, g, b, a] 40 | end 41 | 42 | # @param [Array] colors 43 | # @return [String] Binary byte string of raw image data. 44 | # @since 3.0.0 45 | def self.colors_to_32bit_bytes(colors) 46 | colors.map { |color| self.color_to_32bit(color) }.flatten.pack('C*') 47 | end 48 | 49 | # @param [Array] color 50 | # @return [Array(Integer, Integer, Integer)] RGBA/BGRA 51 | # @since 3.0.0 52 | def self.color_to_24bit(color) 53 | self.color_to_32bit(color)[0, 3] 54 | end 55 | 56 | # @param [Array] colors 57 | # @return [String] Binary byte string of raw image data. 58 | # @since 3.0.0 59 | def self.colors_to_24bit_bytes(colors) 60 | colors.map { |color| self.color_to_24bit(color) }.flatten.pack('C*') 61 | end 62 | 63 | end 64 | end # module 65 | -------------------------------------------------------------------------------- /tests/TT_Lib/TC_UV.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'testup/testcase' 4 | 5 | require 'modules/uv' 6 | 7 | module SkippyLib 8 | class TC_UV < TestUp::TestCase 9 | 10 | def setup 11 | # ... 12 | end 13 | 14 | def teardown 15 | # ... 16 | end 17 | 18 | 19 | def test_initialize 20 | uv = UV.new(0.5, 0.25) 21 | assert_kind_of(UV, uv) 22 | assert_kind_of(Geom::Point3d, uv) 23 | assert_equal(0.5, uv.u) 24 | assert_equal(0.25, uv.v) 25 | assert_equal(1.0, uv.q) 26 | end 27 | 28 | def test_initialize_too_many_arguments 29 | assert_raises(ArgumentError) do 30 | UV.new(0.5, 0.25, 0.5) 31 | end 32 | end 33 | 34 | 35 | def test_from_uvq 36 | uvq = UVQ.new(1.5, 2.0, 0.5) 37 | uv = UV.from_uvq(uvq) 38 | assert_kind_of(UV, uv) 39 | assert_equal(3.0, uv.u) 40 | assert_equal(4.0, uv.v) 41 | assert_equal(1.0, uv.q) 42 | end 43 | 44 | def test_from_uvq_point3d 45 | uvq_point = Geom::Point3d.new(1.5, 2.0, 0.5) 46 | uv = UV.from_uvq(uvq_point) 47 | assert_kind_of(UV, uv) 48 | assert_equal(3.0, uv.u) 49 | assert_equal(4.0, uv.v) 50 | assert_equal(1.0, uv.q) 51 | end 52 | 53 | 54 | def test_u 55 | uv = UV.new(0.5, 0.25) 56 | assert_equal(0.5, uv.u) 57 | end 58 | 59 | 60 | def test_v 61 | uv = UV.new(0.5, 0.25) 62 | assert_equal(0.25, uv.v) 63 | end 64 | 65 | 66 | def test_q 67 | uvq = UV.new(0.5, 0.25) 68 | assert_equal(1.0, uvq.q) 69 | end 70 | 71 | 72 | def test_to_uvq 73 | uv = UV.new(0.5, 0.25) 74 | uvq = uv.to_uvq 75 | assert_kind_of(UVQ, uvq) 76 | assert_equal(0.5, uvq.u) 77 | assert_equal(0.25, uvq.v) 78 | assert_equal(1.0, uvq.q) 79 | end 80 | 81 | 82 | def test_to_s 83 | uv = UV.new(0.5, 0.25) 84 | result = uv.to_s 85 | assert_kind_of(String, result) 86 | assert_equal('UV(0.5, 0.25)', result) 87 | end 88 | 89 | 90 | def test_inspect 91 | uv = UV.new(0.5, 0.25) 92 | result = uv.inspect 93 | assert_kind_of(String, result) 94 | assert_equal('SkippyLib::UV(0.5, 0.25, 1.0)', result) 95 | end 96 | 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /modules/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sketchup.require 'modules/resource' 4 | 5 | module SkippyLib 6 | # A wrapper on top of `UI::Command` which will automatically pick the 7 | # appropriate vector file format alternative for icon resources. 8 | # 9 | # @since 3.0.0 10 | module Command 11 | 12 | # Allows for an error handler to be configured for when commands raises an 13 | # error. Useful for providing general feedback to the user or error loggers. 14 | # 15 | # @since 3.0.0 16 | def self.set_error_handler(&block) 17 | @error_handler = block 18 | end 19 | 20 | # @param [String] title 21 | # @since 3.0.0 22 | def self.new(title, &block) 23 | # SketchUp allocate the object by implementing `new` - probably part of 24 | # older legacy implementation when that was the norm. Because of that the 25 | # class cannot be sub-classed directly. This module simulates the 26 | # interface for how UI::Command is created. `new` will create an instance 27 | # of UI::Command but mix itself into the instance - effectively 28 | # subclassing it. (yuck!) 29 | command = UI::Command.new(title) { 30 | begin 31 | block.call 32 | rescue Exception => e # rubocop:disable Lint/RescueException 33 | if @error_handler.nil? 34 | raise 35 | else 36 | @error_handler.call(e) 37 | end 38 | end 39 | } 40 | command.extend(self) 41 | command.instance_variable_set(:@proc, block) 42 | command 43 | end 44 | 45 | # @return [Proc] 46 | # @since 3.0.0 47 | def proc 48 | @proc 49 | end 50 | 51 | # @since 3.0.0 52 | def invoke 53 | @proc.call 54 | end 55 | 56 | # Sets the large icon for the command. Provide the full path to the raster 57 | # image and the method will look for a vector variant in the same folder 58 | # with the same basename. 59 | # 60 | # @param [String] path 61 | # @since 3.0.0 62 | def large_icon=(path) 63 | super(Resource.icon_path(path)) 64 | end 65 | 66 | # @see #large_icon 67 | # 68 | # @param [String] path 69 | # @since 3.0.0 70 | def small_icon=(path) 71 | super(Resource.icon_path(path)) 72 | end 73 | 74 | end # module 75 | end # module 76 | -------------------------------------------------------------------------------- /tests/TT_Lib/TC_Color.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'testup/testcase' 4 | 5 | require 'modules/color' 6 | 7 | module SkippyLib 8 | class TC_Color < TestUp::TestCase 9 | 10 | def setup 11 | # ... 12 | end 13 | 14 | def teardown 15 | # ... 16 | end 17 | 18 | 19 | def test_initialize 20 | color = Color.new(64, 128, 192, 32) 21 | assert_kind_of(Color, color) 22 | assert_kind_of(Sketchup::Color, color) 23 | assert_equal(64, color.red) 24 | assert_equal(128, color.green) 25 | assert_equal(192, color.blue) 26 | assert_equal(32, color.alpha) 27 | end 28 | 29 | 30 | def test_grayscale_Query_is_grayscale 31 | color = Color.new(64, 64, 64, 255) 32 | assert(color.grayscale?) 33 | end 34 | 35 | def test_grayscale_Query_is_transparent_grayscale 36 | color = Color.new(64, 64, 64, 32) 37 | assert(color.grayscale?) 38 | end 39 | 40 | def test_grayscale_Query_is_not_grayscale 41 | color = Color.new(64, 128, 192, 255) 42 | refute(color.grayscale?) 43 | end 44 | 45 | 46 | def test_luminance 47 | color = Color.new(64, 128, 192) 48 | assert_equal(116, color.luminance) 49 | end 50 | 51 | def test_luminance_transparent 52 | color = Color.new(64, 128, 192, 32) 53 | assert_equal(116, color.luminance) 54 | end 55 | 56 | def test_luminance_black 57 | color = Color.new(0, 0, 0) 58 | assert_equal(0, color.luminance) 59 | end 60 | 61 | def test_luminance_white 62 | color = Color.new(255, 255, 255) 63 | assert_equal(255, color.luminance) 64 | end 65 | 66 | def test_luminance_semi_dark 67 | color = Color.new(40, 20, 30) 68 | assert_equal(27, color.luminance) 69 | end 70 | 71 | def test_luminance_semi_light 72 | color = Color.new(210, 220, 230) 73 | assert_equal(218, color.luminance) 74 | end 75 | 76 | 77 | def test_r 78 | color = Color.new(64, 128, 192, 32) 79 | assert_equal(64, color.r) 80 | end 81 | 82 | 83 | def test_g 84 | color = Color.new(64, 128, 192, 32) 85 | assert_equal(128, color.g) 86 | end 87 | 88 | 89 | def test_b 90 | color = Color.new(64, 128, 192, 32) 91 | assert_equal(192, color.b) 92 | end 93 | 94 | 95 | def test_a 96 | color = Color.new(64, 128, 192, 32) 97 | assert_equal(32, color.a) 98 | end 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /modules/line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | # @since 3.0.0 5 | class Line < Array 6 | 7 | # @param [Geom::Point3d] point 8 | # @param [Geom::Vector3d] vector 9 | # @since 3.0.0 10 | def initialize(point, vector) 11 | super() 12 | # Not making type check or type conversion due to performance. 13 | push(point) 14 | push(vector) 15 | end 16 | 17 | # @return [Geom::Vector3d] 18 | # @since 3.0.0 19 | def direction 20 | @direction ||= direction_internal 21 | end 22 | 23 | # @return [Array(Geom::Point3d, Geom::Vector3d)] 24 | # @since 3.0.0 25 | def to_a 26 | [at(0).clone, direction] 27 | end 28 | 29 | # @since 3.0.0 30 | def valid? 31 | valid_point3d?(at(0)) && (valid_point3d?(at(1)) || valid_vector3d?(at(1))) 32 | end 33 | 34 | # @return [String] 35 | # @since 3.0.0 36 | def inspect 37 | "#{self.class.name}#{super}" 38 | end 39 | 40 | # Hide Array modifiers. 41 | private :push, :<<, :pop, :shift, :unshift, :[]=, :concat 42 | private :delete, :delete_at, :delete_if, :drop, :drop_while, :fill 43 | private :collect!, :compact!, :flatten!, :map!, :normalize!, :offset!, 44 | :reject!, :reverse!, :rotate!, :select!, :shuffle!, :slice!, :sort!, 45 | :sort_by!, :transform!, :uniq! 46 | 47 | # Hide SketchUp API extension to the Array class that doesn't make sense for 48 | # this class. 49 | private :x, :y, :z 50 | private :cross, :dot 51 | private :distance, :distance_to_line, :distance_to_plane 52 | private :offset, :vector_to 53 | private :normalize 54 | private :on_line?, :on_plane? 55 | 56 | private 57 | 58 | # @return [Geom::Vector3d] 59 | def direction_internal 60 | if at(1).is_a?(Geom::Vector3d) 61 | at(1).normalize 62 | elsif at(1).is_a?(Array) 63 | Geom::Vector3d.new(at(1)).normalize 64 | else 65 | at(0).vector_to(at(1)).normalize 66 | end 67 | end 68 | 69 | # @param [Geom::Point3d, Array(Numeric, Numeric, Numeric)] object 70 | def valid_point3d?(object) 71 | object.is_a?(Geom::Point3d) || valid_triple?(object) 72 | end 73 | 74 | # @param [Geom::Vector3d, Array(Numeric, Numeric, Numeric)] object 75 | def valid_vector3d?(object) 76 | object.is_a?(Geom::Vector3d) || valid_triple?(object) 77 | end 78 | 79 | # @param [Array(Numeric, Numeric, Numeric)] object 80 | def valid_triple?(object) 81 | object.is_a?(Array) && object.size == 3 && object.all? { |item| 82 | item.is_a?(Numeric) 83 | } 84 | end 85 | 86 | end 87 | end # module 88 | -------------------------------------------------------------------------------- /modules/boundingbox.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sketchup.require 'modules/boundingbox_constants' 4 | Sketchup.require 'modules/object_utils' 5 | 6 | module SkippyLib 7 | # This class is different from `Geom::BoundingBox` because it represent the 8 | # orientation in model space. The visible boundingbox one see in the viewport. 9 | # 10 | # @since 3.0.0 11 | class BoundingBox 12 | 13 | include BoundingBoxConstants 14 | include ObjectUtils 15 | 16 | # @since 3.0.0 17 | attr_reader :points 18 | 19 | # @param [Array] points 0, 4 or 8 3d points. 20 | # @since 3.0.0 21 | def initialize(points) 22 | unless [0, 4, 8].include?(points.size) 23 | raise ArgumentError, "Expected 0, 4 or 8 points (#{points.size} given)" 24 | end 25 | 26 | @points = points 27 | end 28 | 29 | 30 | # @since 3.0.0 31 | def empty? 32 | @points.empty? 33 | end 34 | 35 | 36 | # @since 3.0.0 37 | def is_2d? 38 | @points.size == 4 39 | end 40 | 41 | # @since 3.0.0 42 | def is_3d? 43 | @points.size == 8 44 | end 45 | 46 | 47 | # @since 3.0.0 48 | def have_area? # rubocop:disable Naming/PredicateName 49 | x_axis.valid? && y_axis.valid? 50 | end 51 | 52 | # @since 3.0.0 53 | def have_volume? # rubocop:disable Naming/PredicateName 54 | x_axis.valid? && y_axis.valid? && z_axis.valid? 55 | end 56 | 57 | 58 | # @since 3.0.0 59 | def width 60 | x_axis.length 61 | end 62 | 63 | # @since 3.0.0 64 | def height 65 | y_axis.length 66 | end 67 | 68 | # @since 3.0.0 69 | def depth 70 | z_axis.length 71 | end 72 | 73 | 74 | # @since 3.0.0 75 | def origin 76 | @points[BOTTOM_FRONT_LEFT] 77 | end 78 | 79 | 80 | # @since 3.0.0 81 | def x_axis 82 | @points[BOTTOM_FRONT_LEFT].vector_to(@points[BOTTOM_FRONT_RIGHT]) 83 | end 84 | 85 | # @since 3.0.0 86 | def y_axis 87 | @points[BOTTOM_FRONT_LEFT].vector_to(@points[BOTTOM_BACK_LEFT]) 88 | end 89 | 90 | # @since 3.0.0 91 | def z_axis 92 | @points[BOTTOM_FRONT_LEFT].vector_to(@points[TOP_FRONT_LEFT]) 93 | end 94 | 95 | 96 | # @since 3.0.0 97 | def draw(view) 98 | view.draw(GL_LINE_LOOP, @points[0..3]) 99 | if is_3d? 100 | view.draw(GL_LINE_LOOP, @points[4..7]) 101 | connectors = [ 102 | @points[0], @points[4], 103 | @points[1], @points[5], 104 | @points[2], @points[6], 105 | @points[3], @points[7], 106 | ] 107 | view.draw(GL_LINES, connectors) 108 | end 109 | end 110 | 111 | end # class 112 | end # module 113 | -------------------------------------------------------------------------------- /modules/uv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SkippyLib 4 | 5 | # Alias to make code more readable. 6 | # 7 | # The SketchUp Ruby API deal with `Geom::Point3d` objects when it comes to 8 | # UV mapping. But this can make it hard to read the intent of the code when 9 | # everything is a `Geom::Point3d`. 10 | # 11 | # @since 3.0.0 12 | class UV < Geom::Point3d 13 | 14 | # @param [UVQ, Geom::Point3d] uvq 15 | # @since 3.0.0 16 | def self.from_uvq(uvq) 17 | self.new(uvq.x / uvq.z, uvq.y / uvq.z) 18 | end 19 | 20 | # @param [Float] x 21 | # @param [Float] y 22 | # @since 3.0.0 23 | def initialize(x, y) 24 | super(x, y, 1.0) 25 | end 26 | 27 | # @since 3.0.0 28 | alias u x 29 | 30 | # @since 3.0.0 31 | alias v y 32 | 33 | # @since 3.0.0 34 | alias q z 35 | 36 | # @return [UVQ] 37 | # @since 3.0.0 38 | def to_uvq 39 | UVQ.new(x, y) 40 | end 41 | 42 | # return [String] 43 | # @since 3.0.0 44 | def to_s 45 | "#{self.class.name.split('::').last}(#{x.to_f}, #{y.to_f})" 46 | end 47 | 48 | # return [String] 49 | # @since 3.0.0 50 | def inspect 51 | "#{self.class.name}(#{x.to_f}, #{y.to_f}, #{z.to_f})" 52 | end 53 | 54 | end 55 | 56 | 57 | # Alias to make code more readable. 58 | # 59 | # The SketchUp Ruby API deal with `Geom::Point3d` objects when it comes to 60 | # UV mapping. But this can make it hard to read the intent of the code when 61 | # everything is a `Geom::Point3d`. 62 | # 63 | # @since 3.0.0 64 | class UVQ < Geom::Point3d 65 | 66 | # @param [UV, Geom::Point3d] uv 67 | # @since 3.0.0 68 | def self.from_uv(uv) 69 | unless uv.z == 0.0 || uv.z == 1.0 # rubocop:disable Lint/FloatComparison 70 | raise ArgumentError, "Q is not 1.0, was: #{uv.z.to_f}" 71 | end 72 | 73 | self.new(uv.x, uv.y) 74 | end 75 | 76 | # @param [Float] x 77 | # @param [Float] y 78 | # @param [Float] z 79 | # @since 3.0.0 80 | def initialize(x, y, z = 1.0) 81 | super(x, y, z) 82 | end 83 | 84 | # @since 3.0.0 85 | alias u x 86 | 87 | # @since 3.0.0 88 | alias v y 89 | 90 | # @since 3.0.0 91 | alias q z 92 | 93 | # @return [UV] 94 | # @since 3.0.0 95 | def to_uv 96 | UV.from_uvq(self) 97 | end 98 | 99 | # return [String] 100 | # @since 3.0.0 101 | def to_s 102 | "#{self.class.name.split('::').last}(#{x.to_f}, #{y.to_f}, #{z.to_f})" 103 | end 104 | 105 | # return [String] 106 | # @since 3.0.0 107 | def inspect 108 | "#{self.class.name}(#{x.to_f}, #{y.to_f}, #{z.to_f})" 109 | end 110 | 111 | end # class 112 | 113 | end # module 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tt-lib³ 2 | 3 | A [skippy](https://github.com/thomthom/skippy) library for SketchUp 4 | 5 | ## Beware: Work in Progress! 6 | 7 | This project is in very early stage of development. Experimental. Pre-alpha. Anything might change at any given time until a stable release has been announced. 8 | 9 | ## Intent 10 | 11 | This is a general purpose Ruby library for SketchUp extensions. 12 | 13 | Previous incarnations of tt-lib was distributed as an `.rbz`, identically to any normal extension. By it self it did nothing, but a number of extensions depended on it to be installed. This was convent for development, but not for maintenance or end users. 14 | 15 | ### Long Term 16 | 17 | The long term goal for tt-lib³ is to become a [skippy](https://github.com/thomthom/skippy) library. Skippy will - in time - manage library dependencies for SketchUp extension projects; Installing, updating etc. While making sure the consumed libraries are embedded into the extension package and namespace. 18 | 19 | ### Short Term 20 | 21 | Until skippy is more mature, this library can be used as a resource and a reference point for small utility classes and methods useful in common SketchUp extension development. 22 | 23 | ## Design Philosophy 24 | 25 | ### Short and Sweet 26 | 27 | The library will consist of a number of small generic classes and modules designed for their own specific purpose. 28 | 29 | Objects that holds state are implemented at classes, while stateless functionality is put into mix-in-able modules. 30 | 31 | ### Mix-in-able modules 32 | 33 | Modules in this library `include self`, so you can use their methods by calling them on the module itself, or by including it into your current scope. 34 | 35 | Example: (As a mix-in) 36 | 37 | ```ruby 38 | require 'example/vendor/tt-lib/drawing_helper' 39 | 40 | module Example 41 | class CustomTool 42 | 43 | include DrawingHelper 44 | 45 | def draw(view) 46 | draw_points(view, points, size) 47 | draw_edges(view, edges, 2, 'red') 48 | end 49 | 50 | end 51 | end 52 | ``` 53 | 54 | Example: (As a separate module reference) 55 | 56 | ```ruby 57 | require 'example/vendor/tt-lib/drawing_helper' 58 | 59 | module Example 60 | class CustomTool 61 | 62 | def draw(view) 63 | DrawingHelper.draw_points(view, points, size) 64 | DrawingHelper.draw_edges(view, edges, 2, 'red') 65 | end 66 | 67 | end 68 | end 69 | ``` 70 | 71 | ### Tested 72 | 73 | Each module, class and methods should have tests that verify their behavior. 74 | 75 | ### [Documented](https://www.rubydoc.info/github/thomthom/tt-lib/master) 76 | 77 | Each module, class and methods should have [documentation](https://www.rubydoc.info/github/thomthom/tt-lib/master) for what it does, it's parameters, it's types and examples. 78 | 79 | ### Consistent 80 | 81 | RuboCop enforces coding style and linting for code consistency. 82 | -------------------------------------------------------------------------------- /tests/TT_Lib/coverage.manifest: -------------------------------------------------------------------------------- 1 | SkippyLib::BoundingBox.depth 2 | SkippyLib::BoundingBox.draw 3 | SkippyLib::BoundingBox.empty? 4 | SkippyLib::BoundingBox.have_area? 5 | SkippyLib::BoundingBox.have_volume? 6 | SkippyLib::BoundingBox.height 7 | SkippyLib::BoundingBox.initialize 8 | SkippyLib::BoundingBox.is_2d? 9 | SkippyLib::BoundingBox.is_3d? 10 | SkippyLib::BoundingBox.origin 11 | SkippyLib::BoundingBox.points 12 | SkippyLib::BoundingBox.width 13 | SkippyLib::BoundingBox.x_axis 14 | SkippyLib::BoundingBox.y_axis 15 | SkippyLib::BoundingBox.z_axis 16 | SkippyLib::CExtensionManager.initialize 17 | SkippyLib::CExtensionManager.inspect 18 | SkippyLib::CExtensionManager.prepare_path 19 | SkippyLib::CExtensionManager.to_s 20 | SkippyLib::Color.a 21 | SkippyLib::Color.b 22 | SkippyLib::Color.g 23 | SkippyLib::Color.grayscale? 24 | SkippyLib::Color.luminance 25 | SkippyLib::Color.r 26 | SkippyLib::Command.invoke 27 | SkippyLib::Command.large_icon= 28 | SkippyLib::Command.new 29 | SkippyLib::Command.proc 30 | SkippyLib::Command.set_error_handler 31 | SkippyLib::Command.small_icon= 32 | SkippyLib::DrawingCache.clear 33 | SkippyLib::DrawingCache.initialize 34 | SkippyLib::DrawingCache.inspect 35 | SkippyLib::DrawingCache.method_missing 36 | SkippyLib::DrawingCache.render 37 | SkippyLib::DrawingCache.respond_to_missing? 38 | SkippyLib::DrawingHelper.adjust_to_pixel_grid 39 | SkippyLib::DrawingHelper.draw2d_point 40 | SkippyLib::DrawingHelper.draw_edges 41 | SkippyLib::DrawingHelper.draw_points 42 | SkippyLib::DrawingHelper.offset_toward_camera 43 | SkippyLib::DrawingHelper.screen_point 44 | SkippyLib::Geometry.mid_point 45 | SkippyLib::Geometry.offset_points 46 | SkippyLib::Image.clone_material 47 | SkippyLib::Image.definition 48 | SkippyLib::ImageRepHelper.color_to_24bit 49 | SkippyLib::ImageRepHelper.color_to_32bit 50 | SkippyLib::ImageRepHelper.colors_to_24bit_bytes 51 | SkippyLib::ImageRepHelper.colors_to_32bit_bytes 52 | SkippyLib::ImageRepHelper.colors_to_image_rep 53 | SkippyLib::Line.direction 54 | SkippyLib::Line.initialize 55 | SkippyLib::Line.inspect 56 | SkippyLib::Line.to_a 57 | SkippyLib::Line.valid? 58 | SkippyLib::PickHelper.leaves 59 | SkippyLib::PickHelper.new 60 | SkippyLib::PickHelper.paths 61 | SkippyLib::Plane.initialize 62 | SkippyLib::Plane.inspect 63 | SkippyLib::Platform.mac? 64 | SkippyLib::Platform.temp_path 65 | SkippyLib::Platform.win? 66 | SkippyLib::Resource.icon_path 67 | SkippyLib::Resource.vector_path 68 | SkippyLib::UV.from_uvq 69 | SkippyLib::UV.initialize 70 | SkippyLib::UV.inspect 71 | SkippyLib::UV.q 72 | SkippyLib::UV.to_s 73 | SkippyLib::UV.to_uvq 74 | SkippyLib::UV.u 75 | SkippyLib::UV.v 76 | SkippyLib::UVMapping.add 77 | SkippyLib::UVMapping.initialize 78 | SkippyLib::UVMapping.to_a 79 | SkippyLib::UVMapping.to_ary 80 | SkippyLib::UVQ.from_uv 81 | SkippyLib::UVQ.initialize 82 | SkippyLib::UVQ.inspect 83 | SkippyLib::UVQ.q 84 | SkippyLib::UVQ.to_s 85 | SkippyLib::UVQ.to_uv 86 | SkippyLib::UVQ.u 87 | SkippyLib::UVQ.v -------------------------------------------------------------------------------- /tests/TT_Lib/TC_UVQ.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'testup/testcase' 4 | 5 | require 'modules/uv' 6 | 7 | module SkippyLib 8 | class TC_UVQ < TestUp::TestCase 9 | 10 | def setup 11 | # ... 12 | end 13 | 14 | def teardown 15 | # ... 16 | end 17 | 18 | 19 | def test_initialize_three_floats 20 | uvq = UVQ.new(0.5, 0.25, 0.75) 21 | assert_kind_of(UVQ, uvq) 22 | assert_kind_of(Geom::Point3d, uvq) 23 | assert_equal(0.5, uvq.u) 24 | assert_equal(0.25, uvq.v) 25 | assert_equal(0.75, uvq.q) 26 | end 27 | 28 | def test_initialize_two_floats 29 | uvq = UVQ.new(0.5, 0.25) 30 | assert_kind_of(UVQ, uvq) 31 | assert_kind_of(Geom::Point3d, uvq) 32 | assert_equal(0.5, uvq.u) 33 | assert_equal(0.25, uvq.v) 34 | assert_equal(1.0, uvq.q) 35 | end 36 | 37 | def test_initialize_too_many_arguments 38 | assert_raises(ArgumentError) do 39 | UV.new(0.5, 0.25, 0.5, 0.75) 40 | end 41 | end 42 | 43 | 44 | def test_from_uv 45 | uv = UV.new(1.5, 2.0) 46 | uvq = UVQ.from_uv(uv) 47 | assert_kind_of(UVQ, uvq) 48 | assert_equal(1.5, uvq.u) 49 | assert_equal(2.0, uvq.v) 50 | assert_equal(1.0, uvq.q) 51 | end 52 | 53 | def test_from_uv_point3d 54 | uv_point = Geom::Point3d.new(1.5, 2.0) 55 | uvq = UVQ.from_uv(uv_point) 56 | assert_kind_of(UVQ, uvq) 57 | assert_equal(1.5, uvq.u) 58 | assert_equal(2.0, uvq.v) 59 | assert_equal(1.0, uvq.q) 60 | end 61 | 62 | def test_from_uv_point3d_where_q_is_not_one 63 | uv_point = Geom::Point3d.new(1.5, 2.0, 0.5) 64 | assert_raises(ArgumentError) do 65 | UVQ.from_uv(uv_point) 66 | end 67 | end 68 | 69 | 70 | def test_u 71 | uvq = UVQ.new(0.5, 0.25, 0.75) 72 | assert_equal(0.5, uvq.u) 73 | end 74 | 75 | 76 | def test_v 77 | uvq = UVQ.new(0.5, 0.25, 0.75) 78 | assert_equal(0.25, uvq.v) 79 | end 80 | 81 | 82 | def test_q 83 | uvq = UVQ.new(0.5, 0.25, 0.75) 84 | assert_equal(0.75, uvq.q) 85 | end 86 | 87 | 88 | def test_to_uv 89 | uvq = UVQ.new(1.5, 2.0, 0.5) 90 | uv = uvq.to_uv 91 | assert_kind_of(UV, uv) 92 | assert_equal(3.0, uv.u) 93 | assert_equal(4.0, uv.v) 94 | assert_equal(1.0, uv.q) 95 | end 96 | 97 | 98 | def test_to_s 99 | uvq = UVQ.new(0.5, 0.25, 0.75) 100 | result = uvq.to_s 101 | assert_kind_of(String, result) 102 | assert_equal('UVQ(0.5, 0.25, 0.75)', result) 103 | end 104 | 105 | 106 | def test_inspect 107 | uvq = UVQ.new(0.5, 0.25, 0.75) 108 | result = uvq.inspect 109 | assert_kind_of(String, result) 110 | assert_equal('SkippyLib::UVQ(0.5, 0.25, 0.75)', result) 111 | end 112 | 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /modules/drawing_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sketchup.require 'modules/object_utils' 4 | 5 | module SkippyLib 6 | # Caches drawing instructions so complex calculations for generating the 7 | # GL data can be reused. 8 | # 9 | # Redirect all `Sketchup::View` commands to a {DrawingCache} object and call 10 | # `#render` in a Tool's `#draw` event. 11 | # 12 | # @example 13 | # class ExampleTool 14 | # def initialize(model) 15 | # @draw_cache = DrawCache.new(model.active_view) 16 | # end 17 | # def deactivate(view) 18 | # @draw_cache.clear 19 | # end 20 | # def resume(view) 21 | # view.invalidate 22 | # end 23 | # def onLButtonUp(flags, x, y, view) 24 | # point = Geom::Point3d.new(x, y, 0) 25 | # view.draw_points(point, 10, 1, 'red') 26 | # view.invalidate 27 | # end 28 | # def draw(view) 29 | # @draw_cache.render 30 | # end 31 | # end 32 | # 33 | # @since 3.0.0 34 | class DrawingCache 35 | 36 | include ObjectUtils 37 | 38 | # @param [Sketchup::View] view 39 | # @since 3.0.0 40 | def initialize(view) 41 | @view = view 42 | @commands = [] 43 | end 44 | 45 | # Clears the cache. All drawing instructions are removed. 46 | # 47 | # @return [Nil] 48 | # @since 3.0.0 49 | def clear 50 | @commands.clear 51 | nil 52 | end 53 | 54 | # Draws the cached drawing instructions. 55 | # 56 | # @return [Sketchup::View] 57 | # @since 3.0.0 58 | def render 59 | view = @view 60 | @commands.each { |command| 61 | view.send(*command) 62 | } 63 | view 64 | end 65 | 66 | # Cache drawing commands and data. These methods received the finished 67 | # processed drawing data that will be executed when {#render} is called. 68 | [ 69 | :draw, 70 | :draw2d, 71 | :draw_line, 72 | :draw_lines, 73 | :draw_points, 74 | :draw_polyline, 75 | :draw_text, 76 | :drawing_color=, 77 | :line_stipple=, 78 | :line_width=, 79 | :set_color_from_line, 80 | ].each { |symbol| 81 | define_method(symbol) { |*args| 82 | @commands << args.unshift(__method__) 83 | @commands.size 84 | } 85 | } 86 | 87 | # Pass through methods to `Sketchup::View` so that the drawing cache object 88 | # can easily replace `Sketchup::View` objects in existing codes. 89 | def method_missing(method, *args) 90 | @view.respond_to?(method) ? @view.send(method, *args) : super 91 | end 92 | 93 | def respond_to_missing?(method, *) 94 | @view.respond_to?(method) | super 95 | end 96 | 97 | # @return [String] 98 | # @since 3.0.0 99 | def inspect 100 | inspect_object(commands: @commands.size) 101 | end 102 | 103 | end # class 104 | end # module 105 | -------------------------------------------------------------------------------- /tests/TT_Lib/TC_Line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'testup/testcase' 4 | 5 | require 'modules/line' 6 | 7 | module SkippyLib 8 | class TC_Line < TestUp::TestCase 9 | 10 | def setup 11 | # ... 12 | end 13 | 14 | def teardown 15 | # ... 16 | end 17 | 18 | 19 | def test_initialize_point_vector 20 | point = Geom::Point3d.new(10, 20, 30) 21 | vector = Geom::Vector3d.new(3, 2, 1) 22 | line = Line.new(point, vector) 23 | assert_kind_of(Line, line) 24 | assert_kind_of(Array, line) 25 | assert_equal([point, vector.normalize], line.to_a) 26 | end 27 | 28 | def test_initialize_two_points 29 | point1 = Geom::Point3d.new(10, 20, 30) 30 | point2 = Geom::Point3d.new(15, 30, 55) 31 | vector = Geom::Vector3d.new(5, 10, 25).normalize 32 | line = Line.new(point1, point2) 33 | assert_kind_of(Line, line) 34 | assert_kind_of(Array, line) 35 | assert_equal([point1, vector], line.to_a) 36 | end 37 | 38 | 39 | def test_direction 40 | point1 = Geom::Point3d.new(10, 20, 30) 41 | point2 = Geom::Point3d.new(15, 30, 55) 42 | vector = Geom::Vector3d.new(5, 10, 25).normalize 43 | line = Line.new(point1, point2) 44 | result = line.direction 45 | assert_kind_of(Geom::Vector3d, result) 46 | assert_equal(vector, result) 47 | end 48 | 49 | def test_direction_array_vector 50 | point = Geom::Point3d.new(10, 20, 30) 51 | vector = Geom::Vector3d.new(1, 2, 3) 52 | # This case is a little ambiguous, as array can substitute both points and 53 | # vectors. #direction will assume vector. 54 | # TODO: Check how SketchUp deal with this. 55 | line = Line.new(point, [1, 2, 3]) 56 | result = line.direction 57 | assert_kind_of(Geom::Vector3d, result) 58 | assert_equal(vector.normalize, result) 59 | end 60 | 61 | def test_direction_cached_value 62 | point1 = Geom::Point3d.new(10, 20, 30) 63 | point2 = Geom::Point3d.new(15, 30, 55) 64 | line = Line.new(point1, point2) 65 | result1 = line.direction 66 | result2 = line.direction 67 | assert_equal(result1.object_id, result2.object_id) 68 | end 69 | 70 | 71 | def test_valid_Query_valid_point_vector 72 | point = Geom::Point3d.new(10, 20, 30) 73 | vector = Geom::Vector3d.new(1, 2, 3) 74 | line = Line.new(point, vector) 75 | assert(line.valid?) 76 | end 77 | 78 | def test_valid_Query_valid_array_point_vector 79 | vector = Geom::Vector3d.new(1, 2, 3) 80 | line = Line.new([10, 20, 30], vector) 81 | assert(line.valid?) 82 | end 83 | 84 | def test_valid_Query_valid_point_array_vector 85 | point = Geom::Point3d.new(10, 20, 30) 86 | line = Line.new(point, [1, 2, 3]) 87 | assert(line.valid?) 88 | end 89 | 90 | def test_valid_Query_invalid 91 | point = Geom::Point3d.new(10, 20, 30) 92 | line = Line.new(point, nil) 93 | refute(line.valid?) 94 | end 95 | 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /modules/drawing_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'modules/view_constants' 4 | 5 | module SkippyLib 6 | # @since 3.0.0 7 | module DrawingHelper 8 | 9 | extend self 10 | 11 | # @param [Array] points 12 | # @param [Boolean] centre_pixel 13 | # @return [Array] 14 | # @since 3.0.0 15 | def adjust_to_pixel_grid(points, centre_pixel: false) 16 | points.map { |point| 17 | point.x = point.x.to_i 18 | point.y = point.y.to_i 19 | point.z = point.z.to_i 20 | if centre_pixel 21 | point.x -= 0.5 22 | point.y -= 0.5 23 | point.z -= 0.5 24 | end 25 | point 26 | } 27 | end 28 | 29 | # @param [Sketchup::View] view 30 | # @param [Geom::Point3d] point 31 | # @return [Geom::Point3d] 32 | # @since 3.0.0 33 | def offset_toward_camera(view, point) 34 | # Model.pixels_to_model converts argument to integers. So in order to get 35 | # a fraction we compute that from the result. 36 | size = view.pixels_to_model(1, point) * 0.01 37 | point.offset(view.camera.direction.reverse!, size) 38 | end 39 | 40 | # Ensures the given screen point is adjusted to the screen pixel grid. 41 | # Drawing odd width lines require the pixel coordinate to be in the centre 42 | # of the pixel in order for it to be drawn crisp when anti-aliasing is 43 | # enabled. 44 | # 45 | # @since 3.0.0 46 | def screen_point(screen_point, line_width) 47 | odd_line_width = line_width.to_i.odd? 48 | adjustment = odd_line_width ? 0.5 : 0.0 49 | corrected_point = screen_point.clone 50 | corrected_point.x = corrected_point.x.to_i + adjustment 51 | corrected_point.y = corrected_point.y.to_i + adjustment 52 | corrected_point.z = 0.0 53 | corrected_point 54 | end 55 | 56 | # @param [Sketchup::View] view 57 | # @param [Enumerable] edges 58 | # @param [Integer] width 59 | # @param [Sketchup::Color] color 60 | # @since 3.0.0 61 | def draw_edges(view, edges, width = 1, color = nil) 62 | color ||= view.model.rendering_options['ForegroundColor'] 63 | points = edges.map { |edge| 64 | edge.vertices.map { |vertex| 65 | offset_toward_camera(view, vertex.position) 66 | } 67 | } 68 | points.flatten! 69 | view.drawing_color = color 70 | view.line_width = width 71 | view.line_stipple = '' 72 | view.draw(GL_LINES, points) 73 | nil 74 | end 75 | 76 | # Because the SU API doesn't let one set the Point size and style when 77 | # drawing 3D points the vertices are simulated by using `GL_LINES` instead. 78 | # There is a slight overhead by generating the new points like this, but 79 | # it's the only solution at the moment. 80 | # 81 | # @param [Sketchup::View] view 82 | # @param [Enumerable] points 83 | # @param [Integer] size 84 | # @return [Nil] 85 | # @since 3.0.0 86 | def draw_points(view, points, size) 87 | return nil if points.empty? 88 | 89 | segments = [] 90 | # Draw each point as a line segment. It's currently a workaround as the 91 | # SketchUp Ruby API doesn't give enough control for drawing points. 92 | camera_right = view.camera.direction.axes.x 93 | camera_left = camera_right.reverse 94 | toward_camera = view.camera.direction.reverse 95 | # Offset the points a little bit so they don't disappear into the 96 | # mesh. The offset distance is picked by experimenting with what looked 97 | # good. It might not scale properly for all sizes. 98 | screen_offset_distance = (size / 3.0).to_i 99 | points.each { |point| 100 | point = point.position if point.is_a?(Sketchup::Vertex) 101 | # Calculate the offset position for the vertex relative to the camera. 102 | offset_distance = view.pixels_to_model(screen_offset_distance, point) 103 | camera_point = point.offset(toward_camera, offset_distance) 104 | # Calculate the world coordinates for the vertex in order to draw it in 105 | # the given screen size. 106 | offset = view.pixels_to_model(size / 2.0, camera_point) 107 | segments << camera_point.offset(camera_left, offset) 108 | segments << camera_point.offset(camera_right, offset) 109 | } 110 | view.line_stipple = '' 111 | view.line_width = size 112 | view.draw(GL_LINES, segments) 113 | nil 114 | end 115 | 116 | # @todo Change this to accept an array of vertices/points. 117 | # 118 | # Draw 2D squares which are adjusted to the pixel grid. 119 | # 120 | # @param [Sketchup::View] view 121 | # @param [Enumerable] point 122 | # @param [Integer] size 123 | # @return [Nil] 124 | # @since 3.0.0 125 | def draw2d_point(view, point, size) 126 | half_size = size / 2.0 127 | # rubocop:disable Layout/SpaceInsideArrayLiteralBrackets 128 | points = [ 129 | point.offset([-half_size, -half_size, 0]), 130 | point.offset([ half_size, -half_size, 0]), 131 | point.offset([ half_size, half_size, 0]), 132 | point.offset([-half_size, half_size, 0]), 133 | ] 134 | # rubocop:enable Layout/SpaceInsideArrayLiteralBrackets 135 | points = adjust_to_pixel_grid(points) 136 | view.draw2d(GL_QUADS, points) 137 | nil 138 | end 139 | 140 | end # class 141 | end # module 142 | -------------------------------------------------------------------------------- /modules/c_extension_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | 5 | Sketchup.require 'modules/object_utils' 6 | Sketchup.require 'modules/platform' 7 | 8 | module SkippyLib 9 | # Loads the appropriate C Extension loader after ensuring the appropriate 10 | # version has been copied from the staging area. 11 | # 12 | # @since 3.0.0 13 | class CExtensionManager 14 | 15 | include ObjectUtils 16 | 17 | class IncompatibleVersion < RuntimeError; end 18 | 19 | # Acceptable versions: 20 | # * Major.Minor.Revision 21 | # * Major.Minor.Revision.Build 22 | # @private 23 | VERSION_PATTERN = /\d+\.\d+\.\d+(?:.\d+)?$/.freeze 24 | 25 | # The `library_path` argument should point to the path where a 'stage' 26 | # folder is located with the following folder structure: 27 | # 28 | # + 29 | # +-+ stage 30 | # +-+ 1.8 31 | # | +-+ HelloWorld.so 32 | # | + HelloWorld.bundle 33 | # +-+ 2.0 34 | # +-+ HelloWorld.so 35 | # + HelloWorld.bundle 36 | # 37 | # The appropriate file will be copied on demand to a folder structure like: 38 | # `///HelloWorld.so` 39 | # 40 | # When a new version is deployed the files will be copied again from the 41 | # staging area to a new folder named with the new extension version. 42 | # 43 | # The old versions are cleaned up if possible. This attempt is done upon 44 | # each time {#prepare_path} is called. 45 | # 46 | # This way the C extensions can be updated because they are never loaded 47 | # from the staging folder directly. 48 | # 49 | # @param [SketchupExtension] extension The extension version. 50 | # @param [String] library_path The location where the C Extensions are 51 | # located. 52 | # @since 3.0.0 53 | def initialize(extension, library_path) 54 | # ENV, __FILE__, $LOAD_PATH, $LOADED_FEATURE and more might return an 55 | # encoding different from UTF-8 under Windows. It's often ASCII-US or 56 | # ASCII-8BIT. If the developer has derived from these strings the encoding 57 | # sticks with it and will often lead to errors further down the road when 58 | # trying to load the files. To work around this the path is attempted to 59 | # be relabeled as UTF-8 if we can produce a valid UTF-8 string. 60 | # I'm forcing an encoding instead of converting because the encoding label 61 | # of the strings seem to be consistently mislabeled - the data is in 62 | # fact UTF-8. 63 | if library_path.respond_to?(:encoding) 64 | test_path = library_path.dup.force_encoding('UTF-8') 65 | library_path = test_path if test_path.valid_encoding? 66 | end 67 | 68 | unless version =~ VERSION_PATTERN 69 | raise ArgumentError, 'Version must be in "X.Y.Z" format' 70 | end 71 | unless File.directory?(library_path) 72 | raise IOError, "Stage path not found: #{library_path}" 73 | end 74 | 75 | @extension = extension 76 | @path = library_path 77 | @stage = File.join(library_path, 'stage') 78 | @target = File.join(library_path, extension.version) 79 | end 80 | 81 | # Copies the necessary C Extension libraries to a version dependent folder 82 | # from where they can be loaded. This will allow the SketchUp RBZ installer 83 | # to update the extension without running into errors when trying to 84 | # overwrite files from previous installation. 85 | # 86 | # @return [String] The path where the extensions are located. 87 | # @since 3.0.0 88 | def prepare_path 89 | ruby_version = RUBY_VERSION.match(/(\d+\.\d+)\.\d/)[1] 90 | stage_path = File.join(@stage, ruby_version, Platform::ID) 91 | target_path = File.join(@target, ruby_version, Platform::ID) 92 | fallback = false 93 | 94 | begin 95 | # Copy files if target doesn't exist. 96 | unless File.directory?(stage_path) 97 | raise IncompatibleVersion, "Staging directory not found: #{stage_path}" 98 | end 99 | 100 | unless File.directory?(target_path) 101 | FileUtils.mkdir_p(target_path) 102 | end 103 | stage_content = Dir.entries(stage_path) 104 | target_content = Dir.entries(target_path) 105 | unless (stage_content - target_content).empty? 106 | FileUtils.copy_entry(stage_path, target_path) 107 | end 108 | 109 | # Clean up old versions. 110 | filter = File.join(@path, '*') 111 | Dir.glob(filter).each { |entry| 112 | next unless File.directory?(entry) 113 | next if [@stage, @target].include?(entry) 114 | next unless entry =~ VERSION_PATTERN 115 | 116 | begin 117 | FileUtils.rm_r(entry) 118 | rescue 119 | puts "#{@extension.name} - Unable to clean up: #{entry}" 120 | end 121 | } 122 | rescue Errno::EACCES 123 | if fallback 124 | UI.messagebox( 125 | "Failed to load #{@extension.name}. Missing permissions to " \ 126 | 'Plugins and temp folder.' 127 | ) 128 | raise 129 | else 130 | # Even though the temp folder contains the username, it appear to be 131 | # returned in DOS 8.3 format which Ruby 1.8 can open. Fall back to 132 | # using the temp folder for these kind of systems. 133 | puts "#{@extension.name} - Unable to access: #{target_path}" 134 | short_name = @extension.name.gsub(/[^A-Za-z0-9_-]/, '') 135 | temp_lib_path = File.join(Platform.temp_path, short_name) 136 | target_path = File.join(temp_lib_path, 137 | @extension.version, 138 | ruby_version, 139 | Platform::ID) 140 | puts "#{@extension.name} - Falling back to: #{target_path}" 141 | fallback = true 142 | retry 143 | end 144 | end 145 | 146 | target_path 147 | end 148 | 149 | # @return [String] 150 | # @since 3.0.0 151 | def inspect 152 | inspect_object 153 | end 154 | alias to_s inspect 155 | 156 | end # class 157 | 158 | end # module 159 | -------------------------------------------------------------------------------- /.rubocop_new_cops.yml: -------------------------------------------------------------------------------- 1 | Gemspec/DeprecatedAttributeAssignment: # new in 1.30 2 | Enabled: true 3 | Gemspec/DevelopmentDependencies: # new in 1.44 4 | Enabled: true 5 | Gemspec/RequireMFA: # new in 1.23 6 | Enabled: true 7 | Layout/LineContinuationLeadingSpace: # new in 1.31 8 | Enabled: true 9 | Layout/LineContinuationSpacing: # new in 1.31 10 | Enabled: true 11 | Layout/LineEndStringConcatenationIndentation: # new in 1.18 12 | Enabled: true 13 | Layout/SpaceBeforeBrackets: # new in 1.7 14 | Enabled: true 15 | Lint/AmbiguousAssignment: # new in 1.7 16 | Enabled: true 17 | Lint/AmbiguousOperatorPrecedence: # new in 1.21 18 | Enabled: true 19 | Lint/AmbiguousRange: # new in 1.19 20 | Enabled: true 21 | Lint/ConstantOverwrittenInRescue: # new in 1.31 22 | Enabled: true 23 | Lint/DeprecatedConstants: # new in 1.8 24 | Enabled: true 25 | Lint/DuplicateBranch: # new in 1.3 26 | Enabled: true 27 | Lint/DuplicateMagicComment: # new in 1.37 28 | Enabled: true 29 | Lint/DuplicateMatchPattern: # new in 1.50 30 | Enabled: true 31 | Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 32 | Enabled: true 33 | Lint/EmptyBlock: # new in 1.1 34 | Enabled: true 35 | Lint/EmptyClass: # new in 1.3 36 | Enabled: true 37 | Lint/EmptyInPattern: # new in 1.16 38 | Enabled: true 39 | Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 40 | Enabled: true 41 | Lint/LambdaWithoutLiteralBlock: # new in 1.8 42 | Enabled: true 43 | Lint/MixedCaseRange: # new in 1.53 44 | Enabled: true 45 | Lint/NoReturnInBeginEndBlocks: # new in 1.2 46 | Enabled: true 47 | Lint/NonAtomicFileOperation: # new in 1.31 48 | Enabled: true 49 | Lint/NumberedParameterAssignment: # new in 1.9 50 | Enabled: true 51 | Lint/OrAssignmentToConstant: # new in 1.9 52 | Enabled: true 53 | Lint/RedundantDirGlobSort: # new in 1.8 54 | Enabled: true 55 | Lint/RedundantRegexpQuantifiers: # new in 1.53 56 | Enabled: true 57 | Lint/RefinementImportMethods: # new in 1.27 58 | Enabled: true 59 | Lint/RequireRangeParentheses: # new in 1.32 60 | Enabled: true 61 | Lint/RequireRelativeSelfPath: # new in 1.22 62 | Enabled: true 63 | Lint/SymbolConversion: # new in 1.9 64 | Enabled: true 65 | Lint/ToEnumArguments: # new in 1.1 66 | Enabled: true 67 | Lint/TripleQuotes: # new in 1.9 68 | Enabled: true 69 | Lint/UnexpectedBlockArity: # new in 1.5 70 | Enabled: true 71 | Lint/UnmodifiedReduceAccumulator: # new in 1.1 72 | Enabled: true 73 | Lint/UselessRescue: # new in 1.43 74 | Enabled: true 75 | Lint/UselessRuby2Keywords: # new in 1.23 76 | Enabled: true 77 | Metrics/CollectionLiteralLength: # new in 1.47 78 | Enabled: true 79 | Naming/BlockForwarding: # new in 1.24 80 | Enabled: true 81 | Security/CompoundHash: # new in 1.28 82 | Enabled: true 83 | Security/IoMethods: # new in 1.22 84 | Enabled: true 85 | Style/ArgumentsForwarding: # new in 1.1 86 | Enabled: true 87 | Style/ArrayIntersect: # new in 1.40 88 | Enabled: true 89 | Style/CollectionCompact: # new in 1.2 90 | Enabled: true 91 | Style/ComparableClamp: # new in 1.44 92 | Enabled: true 93 | Style/ConcatArrayLiterals: # new in 1.41 94 | Enabled: true 95 | Style/DataInheritance: # new in 1.49 96 | Enabled: true 97 | Style/DirEmpty: # new in 1.48 98 | Enabled: true 99 | Style/DocumentDynamicEvalDefinition: # new in 1.1 100 | Enabled: true 101 | Style/EmptyHeredoc: # new in 1.32 102 | Enabled: true 103 | Style/EndlessMethod: # new in 1.8 104 | Enabled: true 105 | Style/EnvHome: # new in 1.29 106 | Enabled: true 107 | Style/ExactRegexpMatch: # new in 1.51 108 | Enabled: true 109 | Style/FetchEnvVar: # new in 1.28 110 | Enabled: true 111 | Style/FileEmpty: # new in 1.48 112 | Enabled: true 113 | Style/FileRead: # new in 1.24 114 | Enabled: true 115 | Style/FileWrite: # new in 1.24 116 | Enabled: true 117 | Style/HashConversion: # new in 1.10 118 | Enabled: true 119 | Style/HashExcept: # new in 1.7 120 | Enabled: true 121 | Style/IfWithBooleanLiteralBranches: # new in 1.9 122 | Enabled: true 123 | Style/InPatternThen: # new in 1.16 124 | Enabled: true 125 | Style/MagicCommentFormat: # new in 1.35 126 | Enabled: true 127 | Style/MapCompactWithConditionalBlock: # new in 1.30 128 | Enabled: true 129 | Style/MapToHash: # new in 1.24 130 | Enabled: true 131 | Style/MapToSet: # new in 1.42 132 | Enabled: true 133 | Style/MinMaxComparison: # new in 1.42 134 | Enabled: true 135 | Style/MultilineInPatternThen: # new in 1.16 136 | Enabled: true 137 | Style/NegatedIfElseCondition: # new in 1.2 138 | Enabled: true 139 | Style/NestedFileDirname: # new in 1.26 140 | Enabled: true 141 | Style/NilLambda: # new in 1.3 142 | Enabled: true 143 | Style/NumberedParameters: # new in 1.22 144 | Enabled: true 145 | Style/NumberedParametersLimit: # new in 1.22 146 | Enabled: true 147 | Style/ObjectThen: # new in 1.28 148 | Enabled: true 149 | Style/OpenStructUse: # new in 1.23 150 | Enabled: true 151 | Style/OperatorMethodCall: # new in 1.37 152 | Enabled: true 153 | Style/QuotedSymbols: # new in 1.16 154 | Enabled: true 155 | Style/RedundantArrayConstructor: # new in 1.52 156 | Enabled: true 157 | Style/RedundantConstantBase: # new in 1.40 158 | Enabled: true 159 | Style/RedundantCurrentDirectoryInPath: # new in 1.53 160 | Enabled: true 161 | Style/RedundantDoubleSplatHashBraces: # new in 1.41 162 | Enabled: true 163 | Style/RedundantEach: # new in 1.38 164 | Enabled: true 165 | Style/RedundantFilterChain: # new in 1.52 166 | Enabled: true 167 | Style/RedundantHeredocDelimiterQuotes: # new in 1.45 168 | Enabled: true 169 | Style/RedundantInitialize: # new in 1.27 170 | Enabled: true 171 | Style/RedundantLineContinuation: # new in 1.49 172 | Enabled: true 173 | Style/RedundantRegexpArgument: # new in 1.53 174 | Enabled: true 175 | Style/RedundantRegexpConstructor: # new in 1.52 176 | Enabled: true 177 | Style/RedundantSelfAssignmentBranch: # new in 1.19 178 | Enabled: true 179 | Style/RedundantStringEscape: # new in 1.37 180 | Enabled: true 181 | Style/ReturnNilInPredicateMethodDefinition: # new in 1.53 182 | Enabled: true 183 | Style/SelectByRegexp: # new in 1.22 184 | Enabled: true 185 | Style/SingleLineDoEndBlock: # new in 1.57 186 | Enabled: true 187 | Style/StringChars: # new in 1.12 188 | Enabled: true 189 | Style/SwapValues: # new in 1.1 190 | Enabled: true 191 | Style/YAMLFileRead: # new in 1.53 192 | Enabled: true 193 | Minitest/AssertInDelta: # new in 0.10 194 | Enabled: true 195 | Minitest/AssertKindOf: # new in 0.10 196 | Enabled: true 197 | Minitest/AssertOperator: # new in 0.32 198 | Enabled: true 199 | Minitest/AssertOutput: # new in 0.10 200 | Enabled: true 201 | Minitest/AssertPathExists: # new in 0.10 202 | Enabled: true 203 | Minitest/AssertPredicate: # new in 0.18 204 | Enabled: true 205 | Minitest/AssertRaisesCompoundBody: # new in 0.21 206 | Enabled: true 207 | Minitest/AssertRaisesWithRegexpArgument: # new in 0.22 208 | Enabled: true 209 | Minitest/AssertSame: # new in 0.26 210 | Enabled: true 211 | Minitest/AssertSilent: # new in 0.10 212 | Enabled: true 213 | Minitest/AssertWithExpectedArgument: # new in 0.11 214 | Enabled: true 215 | Minitest/AssertionInLifecycleHook: # new in 0.10 216 | Enabled: true 217 | Minitest/DuplicateTestRun: # new in 0.19 218 | Enabled: true 219 | Minitest/EmptyLineBeforeAssertionMethods: # new in 0.23 220 | Enabled: true 221 | Minitest/LifecycleHooksOrder: # new in 0.28 222 | Enabled: true 223 | Minitest/LiteralAsActualArgument: # new in 0.10 224 | Enabled: true 225 | Minitest/MultipleAssertions: # new in 0.10 226 | Enabled: true 227 | Minitest/NonPublicTestMethod: # new in 0.27 228 | Enabled: true 229 | Minitest/RefuteInDelta: # new in 0.10 230 | Enabled: true 231 | Minitest/RefuteKindOf: # new in 0.10 232 | Enabled: true 233 | Minitest/RefuteOperator: # new in 0.32 234 | Enabled: true 235 | Minitest/RefutePathExists: # new in 0.10 236 | Enabled: true 237 | Minitest/RefutePredicate: # new in 0.18 238 | Enabled: true 239 | Minitest/RefuteSame: # new in 0.26 240 | Enabled: true 241 | Minitest/ReturnInTestMethod: # new in 0.31 242 | Enabled: true 243 | Minitest/SkipEnsure: # new in 0.20 244 | Enabled: true 245 | Minitest/SkipWithoutReason: # new in 0.24 246 | Enabled: true 247 | Minitest/TestFileName: # new in 0.26 248 | Enabled: true 249 | Minitest/TestMethodName: # new in 0.10 250 | Enabled: true 251 | Minitest/UnreachableAssertion: # new in 0.14 252 | Enabled: true 253 | Minitest/UnspecifiedException: # new in 0.10 254 | Enabled: true 255 | Minitest/UselessAssertion: # new in 0.26 256 | Enabled: true 257 | --------------------------------------------------------------------------------