ruby3mf 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +13 -13
- data/.rspec +2 -2
- data/bin/batch.rb +3 -1
- data/bin/cli.rb +12 -12
- data/bin/console +14 -14
- data/bin/folder_test.sh +13 -13
- data/bin/suite_test.sh +41 -41
- data/lib/ruby3mf.rb +26 -26
- data/lib/ruby3mf/3MFcoreSpec_1.1.xsd.template +188 -188
- data/lib/ruby3mf/content_types.rb +77 -73
- data/lib/ruby3mf/document.rb +242 -242
- data/lib/ruby3mf/edge_list.rb +60 -60
- data/lib/ruby3mf/errors.yml +188 -188
- data/lib/ruby3mf/log3mf.rb +135 -135
- data/lib/ruby3mf/mesh_analyzer.rb +80 -80
- data/lib/ruby3mf/mesh_normal_analyzer.rb +218 -218
- data/lib/ruby3mf/model3mf.rb +117 -118
- data/lib/ruby3mf/relationships.rb +50 -50
- data/lib/ruby3mf/schema_files.rb +26 -26
- data/lib/ruby3mf/texture3mf.rb +29 -29
- data/lib/ruby3mf/thumbnail3mf.rb +21 -21
- data/lib/ruby3mf/version.rb +3 -3
- data/lib/ruby3mf/xml.xsd +286 -286
- data/lib/ruby3mf/xml_val.rb +68 -74
- data/ruby3mf.gemspec +32 -32
- data/suite.011917.out +236 -236
- metadata +3 -9
- data/foo/2D/ffffa2c3-ba74-4bea-a4d0-167a4211134d.model +0 -18747
- data/foo/3D/3dmodel.model +0 -40
- data/foo/3D/_rels/3dmodel.model.rels +0 -4
- data/foo/Thumbnails/ffffa6c3-ba74-4bea-a4d0-167a4211134d.model +0 -0
- data/foo/[Content_Types].xml +0 -7
- data/foo/_rels/.rels +0 -5
    
        data/lib/ruby3mf/log3mf.rb
    CHANGED
    
    | @@ -1,135 +1,135 @@ | |
| 1 | 
            -
            require 'singleton'
         | 
| 2 | 
            -
            require 'yaml'
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            # Example usage:
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            # Log3mf.context "box.3mf" do |l|
         | 
| 7 | 
            -
            #   --do some stuff here
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            #   l.context "[Content-Types].xml" do |l|
         | 
| 10 | 
            -
            #     -- try to parse file. if fail...
         | 
| 11 | 
            -
            #     l.log(:fatal_error, "couldn't parse XML") <<<--- THIS WILL GENERATE FATAL ERROR EXCEPTION
         | 
| 12 | 
            -
            #   end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
            #   l.context "examing Relations" do |l|
         | 
| 15 | 
            -
            #     l.log(:error, "a non-fatal error")
         | 
| 16 | 
            -
            #     l.log(:warning, "a warning")
         | 
| 17 | 
            -
            #     l.log(:info, "it is warm today")
         | 
| 18 | 
            -
            #   end
         | 
| 19 | 
            -
            # end
         | 
| 20 | 
            -
            #
         | 
| 21 | 
            -
            # Log3mf.to_json
         | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
            class Log3mf
         | 
| 25 | 
            -
              include Singleton
         | 
| 26 | 
            -
              include Interpolation
         | 
| 27 | 
            -
             | 
| 28 | 
            -
              LOG_LEVELS = [:fatal_error, :error, :warning, :info, :debug]
         | 
| 29 | 
            -
             | 
| 30 | 
            -
              SPEC_LINKS = {
         | 
| 31 | 
            -
                core: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf',
         | 
| 32 | 
            -
                material: 'http://3mf.io/wp-content/uploads/2015/04/3MFmaterialsSpec_1.0.1.pdf',
         | 
| 33 | 
            -
                production: 'http://3mf.io/wp-content/uploads/2016/07/3MFproductionSpec.pdf',
         | 
| 34 | 
            -
                slice: 'http://3mf.io/wp-content/uploads/2016/07/3MFsliceSpec.pdf',
         | 
| 35 | 
            -
                #opc: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-376,%20Fourth%20Edition,%20Part%202%20-%20Open%20Packaging%20Conventions.zip'
         | 
| 36 | 
            -
                opc: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf'
         | 
| 37 | 
            -
              }.freeze
         | 
| 38 | 
            -
             | 
| 39 | 
            -
              # Allows us to throw FatalErrors if we ever get errors of severity :fatal_error
         | 
| 40 | 
            -
              class FatalError < RuntimeError
         | 
| 41 | 
            -
              end
         | 
| 42 | 
            -
             | 
| 43 | 
            -
              def initialize
         | 
| 44 | 
            -
                @log_list = []
         | 
| 45 | 
            -
                @context_stack = []
         | 
| 46 | 
            -
                @ledger = []
         | 
| 47 | 
            -
                errormap_path = File.join(File.dirname(__FILE__), "errors.yml")
         | 
| 48 | 
            -
                @errormap = YAML.load_file(errormap_path)
         | 
| 49 | 
            -
              end
         | 
| 50 | 
            -
             | 
| 51 | 
            -
              def reset_log
         | 
| 52 | 
            -
                @log_list = []
         | 
| 53 | 
            -
                @context_stack = []
         | 
| 54 | 
            -
              end
         | 
| 55 | 
            -
             | 
| 56 | 
            -
              def self.reset_log
         | 
| 57 | 
            -
                Log3mf.instance.reset_log
         | 
| 58 | 
            -
              end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
              def context (context_description, &block)
         | 
| 61 | 
            -
                @context_stack.push(context_description)
         | 
| 62 | 
            -
                retval = block.call(Log3mf.instance)
         | 
| 63 | 
            -
                @context_stack.pop
         | 
| 64 | 
            -
                retval
         | 
| 65 | 
            -
              end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
              def self.context(context_description, &block)
         | 
| 68 | 
            -
                Log3mf.instance.context(context_description, &block)
         | 
| 69 | 
            -
              end
         | 
| 70 | 
            -
             | 
| 71 | 
            -
              def method_missing(name, *args, &block)
         | 
| 72 | 
            -
                if LOG_LEVELS.include? name.to_sym
         | 
| 73 | 
            -
                  if [:fatal_error, :error, :debug].include? name.to_sym
         | 
| 74 | 
            -
                    linenumber = caller_locations[0].to_s.split('/')[-1].split(':')[-2].to_s
         | 
| 75 | 
            -
                    filename = caller_locations[0].to_s.split('/')[-1].split(':')[0].to_s
         | 
| 76 | 
            -
                    options = {linenumber: linenumber, filename: filename}
         | 
| 77 | 
            -
                    # Mike: do not call error or fatal_error without an entry in errors.yml
         | 
| 78 | 
            -
                    raise "{fatal_}error called WITHOUT using error symbol from: #{filename}:#{linenumber}" if ( !(args[0].is_a? Symbol) && (name.to_sym != :debug) )
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                    puts "***** Log3mf.#{name} called from #{filename}:#{linenumber} *****" if $DEBUG
         | 
| 81 | 
            -
             | 
| 82 | 
            -
                    options = options.merge(args[1]) if args[1]
         | 
| 83 | 
            -
                    log(name.to_sym, args[0], options)
         | 
| 84 | 
            -
                  else
         | 
| 85 | 
            -
                    log(name.to_sym, *args)
         | 
| 86 | 
            -
                  end
         | 
| 87 | 
            -
                else
         | 
| 88 | 
            -
                  super
         | 
| 89 | 
            -
                end
         | 
| 90 | 
            -
              end
         | 
| 91 | 
            -
             | 
| 92 | 
            -
              def log(severity, message, options = {})
         | 
| 93 | 
            -
                error = @errormap.fetch(message.to_s) { {"msg" => message.to_s, "page" => nil} }
         | 
| 94 | 
            -
                options[:page] = error["page"] unless options[:page]
         | 
| 95 | 
            -
                options[:spec] = error["spec"] unless options[:spec]
         | 
| 96 | 
            -
                entry = {id: message,
         | 
| 97 | 
            -
                         context: "#{@context_stack.join("/")}",
         | 
| 98 | 
            -
                         severity: severity,
         | 
| 99 | 
            -
                         message: interpolate(error["msg"], options)}
         | 
| 100 | 
            -
                entry[:spec_ref] = spec_link(options[:spec], options[:page]) if (options && options[:page])
         | 
| 101 | 
            -
                entry[:caller] = "#{options[:filename]}:#{options[:linenumber]}" if (options && options[:filename] && options[:linenumber])
         | 
| 102 | 
            -
                @log_list << entry
         | 
| 103 | 
            -
                raise FatalError if severity == :fatal_error
         | 
| 104 | 
            -
              end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
              def count_entries(*levels)
         | 
| 107 | 
            -
                entries(*levels).count
         | 
| 108 | 
            -
              end
         | 
| 109 | 
            -
             | 
| 110 | 
            -
              def self.count_entries(*l)
         | 
| 111 | 
            -
                Log3mf.instance.count_entries(*l)
         | 
| 112 | 
            -
              end
         | 
| 113 | 
            -
             | 
| 114 | 
            -
              def entries(*levels)
         | 
| 115 | 
            -
                return @log_list if levels.size == 0
         | 
| 116 | 
            -
                @log_list.select { |i| levels.include? i[:severity] }
         | 
| 117 | 
            -
              end
         | 
| 118 | 
            -
             | 
| 119 | 
            -
              def self.entries(*l)
         | 
| 120 | 
            -
                Log3mf.instance.entries(*l)
         | 
| 121 | 
            -
              end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
              def spec_link(spec, page)
         | 
| 124 | 
            -
                spec = :core unless spec
         | 
| 125 | 
            -
                "#{SPEC_LINKS[spec]}#page=#{page}"
         | 
| 126 | 
            -
              end
         | 
| 127 | 
            -
             | 
| 128 | 
            -
              def to_json
         | 
| 129 | 
            -
                @log_list.to_json
         | 
| 130 | 
            -
              end
         | 
| 131 | 
            -
             | 
| 132 | 
            -
              def self.to_json
         | 
| 133 | 
            -
                Log3mf.instance.to_json
         | 
| 134 | 
            -
              end
         | 
| 135 | 
            -
            end
         | 
| 1 | 
            +
            require 'singleton'
         | 
| 2 | 
            +
            require 'yaml'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            # Example usage:
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            # Log3mf.context "box.3mf" do |l|
         | 
| 7 | 
            +
            #   --do some stuff here
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            #   l.context "[Content-Types].xml" do |l|
         | 
| 10 | 
            +
            #     -- try to parse file. if fail...
         | 
| 11 | 
            +
            #     l.log(:fatal_error, "couldn't parse XML") <<<--- THIS WILL GENERATE FATAL ERROR EXCEPTION
         | 
| 12 | 
            +
            #   end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            #   l.context "examing Relations" do |l|
         | 
| 15 | 
            +
            #     l.log(:error, "a non-fatal error")
         | 
| 16 | 
            +
            #     l.log(:warning, "a warning")
         | 
| 17 | 
            +
            #     l.log(:info, "it is warm today")
         | 
| 18 | 
            +
            #   end
         | 
| 19 | 
            +
            # end
         | 
| 20 | 
            +
            #
         | 
| 21 | 
            +
            # Log3mf.to_json
         | 
| 22 | 
            +
             | 
| 23 | 
            +
             | 
| 24 | 
            +
            class Log3mf
         | 
| 25 | 
            +
              include Singleton
         | 
| 26 | 
            +
              include Interpolation
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              LOG_LEVELS = [:fatal_error, :error, :warning, :info, :debug]
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              SPEC_LINKS = {
         | 
| 31 | 
            +
                core: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf',
         | 
| 32 | 
            +
                material: 'http://3mf.io/wp-content/uploads/2015/04/3MFmaterialsSpec_1.0.1.pdf',
         | 
| 33 | 
            +
                production: 'http://3mf.io/wp-content/uploads/2016/07/3MFproductionSpec.pdf',
         | 
| 34 | 
            +
                slice: 'http://3mf.io/wp-content/uploads/2016/07/3MFsliceSpec.pdf',
         | 
| 35 | 
            +
                #opc: 'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-376,%20Fourth%20Edition,%20Part%202%20-%20Open%20Packaging%20Conventions.zip'
         | 
| 36 | 
            +
                opc: 'http://3mf.io/wp-content/uploads/2016/03/3MFcoreSpec_1.1.pdf'
         | 
| 37 | 
            +
              }.freeze
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              # Allows us to throw FatalErrors if we ever get errors of severity :fatal_error
         | 
| 40 | 
            +
              class FatalError < RuntimeError
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def initialize
         | 
| 44 | 
            +
                @log_list = []
         | 
| 45 | 
            +
                @context_stack = []
         | 
| 46 | 
            +
                @ledger = []
         | 
| 47 | 
            +
                errormap_path = File.join(File.dirname(__FILE__), "errors.yml")
         | 
| 48 | 
            +
                @errormap = YAML.load_file(errormap_path)
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              def reset_log
         | 
| 52 | 
            +
                @log_list = []
         | 
| 53 | 
            +
                @context_stack = []
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              def self.reset_log
         | 
| 57 | 
            +
                Log3mf.instance.reset_log
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def context (context_description, &block)
         | 
| 61 | 
            +
                @context_stack.push(context_description)
         | 
| 62 | 
            +
                retval = block.call(Log3mf.instance)
         | 
| 63 | 
            +
                @context_stack.pop
         | 
| 64 | 
            +
                retval
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              def self.context(context_description, &block)
         | 
| 68 | 
            +
                Log3mf.instance.context(context_description, &block)
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
              def method_missing(name, *args, &block)
         | 
| 72 | 
            +
                if LOG_LEVELS.include? name.to_sym
         | 
| 73 | 
            +
                  if [:fatal_error, :error, :debug].include? name.to_sym
         | 
| 74 | 
            +
                    linenumber = caller_locations[0].to_s.split('/')[-1].split(':')[-2].to_s
         | 
| 75 | 
            +
                    filename = caller_locations[0].to_s.split('/')[-1].split(':')[0].to_s
         | 
| 76 | 
            +
                    options = {linenumber: linenumber, filename: filename}
         | 
| 77 | 
            +
                    # Mike: do not call error or fatal_error without an entry in errors.yml
         | 
| 78 | 
            +
                    raise "{fatal_}error called WITHOUT using error symbol from: #{filename}:#{linenumber}" if ( !(args[0].is_a? Symbol) && (name.to_sym != :debug) )
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    puts "***** Log3mf.#{name} called from #{filename}:#{linenumber} *****" if $DEBUG
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    options = options.merge(args[1]) if args[1]
         | 
| 83 | 
            +
                    log(name.to_sym, args[0], options)
         | 
| 84 | 
            +
                  else
         | 
| 85 | 
            +
                    log(name.to_sym, *args)
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                else
         | 
| 88 | 
            +
                  super
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              def log(severity, message, options = {})
         | 
| 93 | 
            +
                error = @errormap.fetch(message.to_s) { {"msg" => message.to_s, "page" => nil} }
         | 
| 94 | 
            +
                options[:page] = error["page"] unless options[:page]
         | 
| 95 | 
            +
                options[:spec] = error["spec"] unless options[:spec]
         | 
| 96 | 
            +
                entry = {id: message,
         | 
| 97 | 
            +
                         context: "#{@context_stack.join("/")}",
         | 
| 98 | 
            +
                         severity: severity,
         | 
| 99 | 
            +
                         message: interpolate(error["msg"], options)}
         | 
| 100 | 
            +
                entry[:spec_ref] = spec_link(options[:spec], options[:page]) if (options && options[:page])
         | 
| 101 | 
            +
                entry[:caller] = "#{options[:filename]}:#{options[:linenumber]}" if (options && options[:filename] && options[:linenumber])
         | 
| 102 | 
            +
                @log_list << entry
         | 
| 103 | 
            +
                raise FatalError if severity == :fatal_error
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              def count_entries(*levels)
         | 
| 107 | 
            +
                entries(*levels).count
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              def self.count_entries(*l)
         | 
| 111 | 
            +
                Log3mf.instance.count_entries(*l)
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
              def entries(*levels)
         | 
| 115 | 
            +
                return @log_list if levels.size == 0
         | 
| 116 | 
            +
                @log_list.select { |i| levels.include? i[:severity] }
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
              def self.entries(*l)
         | 
| 120 | 
            +
                Log3mf.instance.entries(*l)
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
              def spec_link(spec, page)
         | 
| 124 | 
            +
                spec = :core unless spec
         | 
| 125 | 
            +
                "#{SPEC_LINKS[spec]}#page=#{page}"
         | 
| 126 | 
            +
              end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
              def to_json
         | 
| 129 | 
            +
                @log_list.to_json
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
              def self.to_json
         | 
| 133 | 
            +
                Log3mf.instance.to_json
         | 
| 134 | 
            +
              end
         | 
| 135 | 
            +
            end
         | 
| @@ -1,80 +1,80 @@ | |
| 1 | 
            -
            class MeshAnalyzer
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              def self.validate_object(object, includes_material)
         | 
| 4 | 
            -
                Log3mf.context "verifying object" do |l|
         | 
| 5 | 
            -
                  children = object.children.map { |child| child.name }
         | 
| 6 | 
            -
                  have_override = object.attributes["pid"] or object.attributes["pindex"]
         | 
| 7 | 
            -
                  l.error :object_with_components_and_pid if have_override && children.include?("components")
         | 
| 8 | 
            -
                end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                Log3mf.context "validating geometry" do |l|
         | 
| 11 | 
            -
                  list = EdgeList.new
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                  # if a triangle has a pid, then the object needs a pid
         | 
| 14 | 
            -
                  has_triangle_pid = false
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                  meshs = object.css('mesh')
         | 
| 17 | 
            -
                  meshs.each do |mesh|
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                    num_vertices = mesh.css("vertex").count
         | 
| 20 | 
            -
                    triangles = mesh.css("triangle")
         | 
| 21 | 
            -
                    l.error :not_enough_triangles if triangles.count < 4
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                    if triangles
         | 
| 24 | 
            -
                      triangles.each do |triangle|
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                        v1 = triangle.attributes["v1"].to_s.to_i
         | 
| 27 | 
            -
                        v2 = triangle.attributes["v2"].to_s.to_i
         | 
| 28 | 
            -
                        v3 = triangle.attributes["v3"].to_s.to_i
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                        l.error :invalid_vertex_index if [v1, v2, v3].select{|vertex| vertex >= num_vertices}.count > 0
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                        unless includes_material
         | 
| 33 | 
            -
                          l.context "validating property overrides" do |l|
         | 
| 34 | 
            -
                            property_overrides = []
         | 
| 35 | 
            -
                            property_overrides << triangle.attributes['p1'].to_s.to_i if triangle.attributes['p1']
         | 
| 36 | 
            -
                            property_overrides << triangle.attributes['p2'].to_s.to_i if triangle.attributes['p2']
         | 
| 37 | 
            -
                            property_overrides << triangle.attributes['p3'].to_s.to_i if triangle.attributes['p3']
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                            property_overrides.reject! { |prop| prop.nil? }
         | 
| 40 | 
            -
                            l.error :has_base_materials_gradient unless property_overrides.uniq.size <= 1
         | 
| 41 | 
            -
                          end
         | 
| 42 | 
            -
                        end
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                        if v1 == v2 || v2 == v3 || v3 == v1
         | 
| 45 | 
            -
                          l.error :non_distinct_indices
         | 
| 46 | 
            -
                        end
         | 
| 47 | 
            -
             | 
| 48 | 
            -
                        list.add_edge(v1, v2)
         | 
| 49 | 
            -
                        list.add_edge(v2, v3)
         | 
| 50 | 
            -
                        list.add_edge(v3, v1)
         | 
| 51 | 
            -
                        unless has_triangle_pid
         | 
| 52 | 
            -
                          has_triangle_pid = triangle.attributes["pid"] != nil
         | 
| 53 | 
            -
                        end
         | 
| 54 | 
            -
                      end
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                      if has_triangle_pid && !(object.attributes["pindex"] && object.attributes["pid"])
         | 
| 57 | 
            -
                        l.error :missing_object_pid
         | 
| 58 | 
            -
                      end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                      result = list.verify_edges
         | 
| 61 | 
            -
                      if result == :bad_orientation
         | 
| 62 | 
            -
                        l.error :resource_3dmodel_orientation
         | 
| 63 | 
            -
                      elsif result == :hole
         | 
| 64 | 
            -
                        l.error :resource_3dmodel_hole
         | 
| 65 | 
            -
                      elsif result == :nonmanifold
         | 
| 66 | 
            -
                        l.error :resource_3dmodel_nonmanifold
         | 
| 67 | 
            -
                      end
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                    end
         | 
| 70 | 
            -
                  end
         | 
| 71 | 
            -
                end
         | 
| 72 | 
            -
              end
         | 
| 73 | 
            -
             | 
| 74 | 
            -
              def self.validate(model_doc, includes_material)
         | 
| 75 | 
            -
                model_doc.css('model/resources/object').select { |object| ['model', 'solidsupport', ''].include?(object.attributes['type'].to_s) }.each do |object|
         | 
| 76 | 
            -
                  validate_object(object, includes_material)
         | 
| 77 | 
            -
                end
         | 
| 78 | 
            -
              end
         | 
| 79 | 
            -
             | 
| 80 | 
            -
            end
         | 
| 1 | 
            +
            class MeshAnalyzer
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              def self.validate_object(object, includes_material)
         | 
| 4 | 
            +
                Log3mf.context "verifying object" do |l|
         | 
| 5 | 
            +
                  children = object.children.map { |child| child.name }
         | 
| 6 | 
            +
                  have_override = object.attributes["pid"] or object.attributes["pindex"]
         | 
| 7 | 
            +
                  l.error :object_with_components_and_pid if have_override && children.include?("components")
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                Log3mf.context "validating geometry" do |l|
         | 
| 11 | 
            +
                  list = EdgeList.new
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # if a triangle has a pid, then the object needs a pid
         | 
| 14 | 
            +
                  has_triangle_pid = false
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  meshs = object.css('mesh')
         | 
| 17 | 
            +
                  meshs.each do |mesh|
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    num_vertices = mesh.css("vertex").count
         | 
| 20 | 
            +
                    triangles = mesh.css("triangle")
         | 
| 21 | 
            +
                    l.error :not_enough_triangles if triangles.count < 4
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    if triangles
         | 
| 24 | 
            +
                      triangles.each do |triangle|
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                        v1 = triangle.attributes["v1"].to_s.to_i
         | 
| 27 | 
            +
                        v2 = triangle.attributes["v2"].to_s.to_i
         | 
| 28 | 
            +
                        v3 = triangle.attributes["v3"].to_s.to_i
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                        l.error :invalid_vertex_index if [v1, v2, v3].select{|vertex| vertex >= num_vertices}.count > 0
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                        unless includes_material
         | 
| 33 | 
            +
                          l.context "validating property overrides" do |l|
         | 
| 34 | 
            +
                            property_overrides = []
         | 
| 35 | 
            +
                            property_overrides << triangle.attributes['p1'].to_s.to_i if triangle.attributes['p1']
         | 
| 36 | 
            +
                            property_overrides << triangle.attributes['p2'].to_s.to_i if triangle.attributes['p2']
         | 
| 37 | 
            +
                            property_overrides << triangle.attributes['p3'].to_s.to_i if triangle.attributes['p3']
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                            property_overrides.reject! { |prop| prop.nil? }
         | 
| 40 | 
            +
                            l.error :has_base_materials_gradient unless property_overrides.uniq.size <= 1
         | 
| 41 | 
            +
                          end
         | 
| 42 | 
            +
                        end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        if v1 == v2 || v2 == v3 || v3 == v1
         | 
| 45 | 
            +
                          l.error :non_distinct_indices
         | 
| 46 | 
            +
                        end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                        list.add_edge(v1, v2)
         | 
| 49 | 
            +
                        list.add_edge(v2, v3)
         | 
| 50 | 
            +
                        list.add_edge(v3, v1)
         | 
| 51 | 
            +
                        unless has_triangle_pid
         | 
| 52 | 
            +
                          has_triangle_pid = triangle.attributes["pid"] != nil
         | 
| 53 | 
            +
                        end
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                      if has_triangle_pid && !(object.attributes["pindex"] && object.attributes["pid"])
         | 
| 57 | 
            +
                        l.error :missing_object_pid
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      result = list.verify_edges
         | 
| 61 | 
            +
                      if result == :bad_orientation
         | 
| 62 | 
            +
                        l.error :resource_3dmodel_orientation
         | 
| 63 | 
            +
                      elsif result == :hole
         | 
| 64 | 
            +
                        l.error :resource_3dmodel_hole
         | 
| 65 | 
            +
                      elsif result == :nonmanifold
         | 
| 66 | 
            +
                        l.error :resource_3dmodel_nonmanifold
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              def self.validate(model_doc, includes_material)
         | 
| 75 | 
            +
                model_doc.css('model/resources/object').select { |object| ['model', 'solidsupport', ''].include?(object.attributes['type'].to_s) }.each do |object|
         | 
| 76 | 
            +
                  validate_object(object, includes_material)
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
            end
         | 
| @@ -1,219 +1,219 @@ | |
| 1 | 
            -
            class MeshNormalAnalyzer
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              def initialize(mesh)
         | 
| 4 | 
            -
                @vertices = []
         | 
| 5 | 
            -
                @intersections = []
         | 
| 6 | 
            -
             | 
| 7 | 
            -
                vertices_node = mesh.css("vertices")
         | 
| 8 | 
            -
                vertices_node.children.each do |vertex_node|
         | 
| 9 | 
            -
                  if vertex_node.attributes.count > 0
         | 
| 10 | 
            -
                    x = vertex_node.attributes['x'].to_s.to_f
         | 
| 11 | 
            -
                    y = vertex_node.attributes['y'].to_s.to_f
         | 
| 12 | 
            -
                    z = vertex_node.attributes['z'].to_s.to_f
         | 
| 13 | 
            -
                    @vertices << [x, y, z]
         | 
| 14 | 
            -
                  end
         | 
| 15 | 
            -
                end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                @triangles = []
         | 
| 18 | 
            -
                triangles_node = mesh.css("triangles")
         | 
| 19 | 
            -
                triangles_node.children.each do |triangle_node|
         | 
| 20 | 
            -
                  if triangle_node.attributes.count > 0
         | 
| 21 | 
            -
                    v1 = triangle_node.attributes['v1'].to_s.to_i
         | 
| 22 | 
            -
                    v2 = triangle_node.attributes['v2'].to_s.to_i
         | 
| 23 | 
            -
                    v3 = triangle_node.attributes['v3'].to_s.to_i
         | 
| 24 | 
            -
                    @triangles << [v1, v2, v3]
         | 
| 25 | 
            -
                  end
         | 
| 26 | 
            -
                end
         | 
| 27 | 
            -
              end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
              def found_inward_triangle
         | 
| 30 | 
            -
                # Trace a ray toward the center of the vertex points.  This will hopefully
         | 
| 31 | 
            -
                # maximize our chances of hitting the object's trianges on the first try.
         | 
| 32 | 
            -
                center = point_cloud_center(@vertices)
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                @point = [0.0, 0.0, 0.0]
         | 
| 35 | 
            -
                @direction = vector_to(@point, center)
         | 
| 36 | 
            -
             | 
| 37 | 
            -
                # Make sure that we have a reasonably sized direction.
         | 
| 38 | 
            -
                # Might end up with a zero length vector if the center is also
         | 
| 39 | 
            -
                # at the origin.
         | 
| 40 | 
            -
                if magnitude(@direction) < 0.1
         | 
| 41 | 
            -
                  @direction = [0.57, 0.57, 0.57]
         | 
| 42 | 
            -
                end
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                # make the direction a unit vector just to make the
         | 
| 45 | 
            -
                # debug info easier to understand
         | 
| 46 | 
            -
                @direction = normalize(@direction)
         | 
| 47 | 
            -
             | 
| 48 | 
            -
                attempts = 0
         | 
| 49 | 
            -
                begin
         | 
| 50 | 
            -
                  # Get all of the intersections from the ray and put them in order of distance.
         | 
| 51 | 
            -
                  # The triangle we hit that's farthest from the start of the ray should always be
         | 
| 52 | 
            -
                  # a triangle that points away from us (otherwise we would hit a triangle even
         | 
| 53 | 
            -
                  # further away, assuming the mesh is closed).
         | 
| 54 | 
            -
                  #
         | 
| 55 | 
            -
                  # One special case is when the set of triangles we hit at that distance is greater
         | 
| 56 | 
            -
                  # than one.  In that case we might have hit a "corner" of the model and so we don't
         | 
| 57 | 
            -
                  # know which of the two (or more) points away from us.  In that case, cast a random
         | 
| 58 | 
            -
                  # ray from the center of the object and try again.
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  @triangles.each do |tri|
         | 
| 61 | 
            -
                    v1 = @vertices[tri[0]]
         | 
| 62 | 
            -
                    v2 = @vertices[tri[1]]
         | 
| 63 | 
            -
                    v3 = @vertices[tri[2]]
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                    process_triangle(@point, @direction, [v1, v2, v3])
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
             | 
| 68 | 
            -
                  if @intersections.count > 0
         | 
| 69 | 
            -
                    # Sort the intersections so we can find the hits that are furthest away.
         | 
| 70 | 
            -
                    @intersections.sort! {|left, right| left[0] <=> right[0]}
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                    max_distance = @intersections.last[0]
         | 
| 73 | 
            -
                    furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                    # Print out the hits
         | 
| 76 | 
            -
                    # furthest_hits.each {|hit| puts hit[1].to_s}
         | 
| 77 | 
            -
             | 
| 78 | 
            -
                    found_good_hit = furthest_hits.count == 1
         | 
| 79 | 
            -
                  end
         | 
| 80 | 
            -
             | 
| 81 | 
            -
                  if found_good_hit
         | 
| 82 | 
            -
                    outside_triangle = furthest_hits.last[2]
         | 
| 83 | 
            -
                  else
         | 
| 84 | 
            -
                    @intersections = []
         | 
| 85 | 
            -
                    attempts = attempts + 1
         | 
| 86 | 
            -
             | 
| 87 | 
            -
                    target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
         | 
| 88 | 
            -
                    @point = center
         | 
| 89 | 
            -
                    @direction = normalize(vector_to(@point, target))
         | 
| 90 | 
            -
                  end
         | 
| 91 | 
            -
                end until found_good_hit || attempts >= 10
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                # return true if we hit a triangle with an inward pointing normal
         | 
| 94 | 
            -
                # (according to counter-clockwise normal orientation)
         | 
| 95 | 
            -
                found_good_hit && !compare_normals(outside_triangle, @direction)
         | 
| 96 | 
            -
              end
         | 
| 97 | 
            -
             | 
| 98 | 
            -
              def compare_normals(triangle, hit_direction)
         | 
| 99 | 
            -
                oriented_normal = cross_product(
         | 
| 100 | 
            -
                    vector_to(triangle[0], triangle[1]),
         | 
| 101 | 
            -
                    vector_to(triangle[1], triangle[2]))
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                angle = angle_between(oriented_normal, hit_direction)
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                angle < Math::PI / 2.0
         | 
| 106 | 
            -
              end
         | 
| 107 | 
            -
             | 
| 108 | 
            -
              def process_triangle(point, direction, triangle)
         | 
| 109 | 
            -
                found_intersection, t = intersect(point, direction, triangle)
         | 
| 110 | 
            -
             | 
| 111 | 
            -
                if t > 0
         | 
| 112 | 
            -
                  intersection = []
         | 
| 113 | 
            -
                  intersection[0] = point[0] + t * direction[0]
         | 
| 114 | 
            -
                  intersection[1] = point[1] + t * direction[1]
         | 
| 115 | 
            -
                  intersection[2] = point[2] + t * direction[2]
         | 
| 116 | 
            -
             | 
| 117 | 
            -
                  @intersections << [t, intersection, triangle]
         | 
| 118 | 
            -
                end
         | 
| 119 | 
            -
              end
         | 
| 120 | 
            -
             | 
| 121 | 
            -
              def intersect(point, direction, triangle)
         | 
| 122 | 
            -
                v0 = triangle[0]
         | 
| 123 | 
            -
                v1 = triangle[1]
         | 
| 124 | 
            -
                v2 = triangle[2]
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                return [false, 0] if v0.nil? || v1.nil? || v2.nil?
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                e1 = vector_to(v0, v1)
         | 
| 129 | 
            -
                e2 = vector_to(v0, v2)
         | 
| 130 | 
            -
             | 
| 131 | 
            -
                h = cross_product(direction, e2)
         | 
| 132 | 
            -
                a = dot_product(e1, h)
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                if a.abs < 0.00001
         | 
| 135 | 
            -
                  return false, 0
         | 
| 136 | 
            -
                end
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                f = 1.0/a
         | 
| 139 | 
            -
                s = vector_to(v0, point)
         | 
| 140 | 
            -
                u = f * dot_product(s, h)
         | 
| 141 | 
            -
             | 
| 142 | 
            -
                if u < 0.0 || u > 1.0
         | 
| 143 | 
            -
                  return false, 0
         | 
| 144 | 
            -
                end
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                q = cross_product(s, e1)
         | 
| 147 | 
            -
                v = f * dot_product(direction, q)
         | 
| 148 | 
            -
             | 
| 149 | 
            -
                if v < 0.0 || u + v > 1.0
         | 
| 150 | 
            -
                  return false, 0
         | 
| 151 | 
            -
                end
         | 
| 152 | 
            -
             | 
| 153 | 
            -
                t = f * dot_product(e2, q)
         | 
| 154 | 
            -
                [t > 0, t]
         | 
| 155 | 
            -
              end
         | 
| 156 | 
            -
            end
         | 
| 157 | 
            -
             | 
| 158 | 
            -
            # Various utility functions
         | 
| 159 | 
            -
             | 
| 160 | 
            -
            def cross_product(a, b)
         | 
| 161 | 
            -
              result = [0, 0, 0]
         | 
| 162 | 
            -
              result[0] = a[1]*b[2] - a[2]*b[1]
         | 
| 163 | 
            -
              result[1] = a[2]*b[0] - a[0]*b[2]
         | 
| 164 | 
            -
              result[2] = a[0]*b[1] - a[1]*b[0]
         | 
| 165 | 
            -
             | 
| 166 | 
            -
              result
         | 
| 167 | 
            -
            end
         | 
| 168 | 
            -
             | 
| 169 | 
            -
            def dot_product(a, b)
         | 
| 170 | 
            -
              a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
         | 
| 171 | 
            -
            end
         | 
| 172 | 
            -
             | 
| 173 | 
            -
            def vector_to(a, b)
         | 
| 174 | 
            -
              [b[0] - a[0], b[1] - a[1], b[2] - a[2]]
         | 
| 175 | 
            -
            end
         | 
| 176 | 
            -
             | 
| 177 | 
            -
            def magnitude(a)
         | 
| 178 | 
            -
              Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
         | 
| 179 | 
            -
            end
         | 
| 180 | 
            -
             | 
| 181 | 
            -
            def equal(a, b)
         | 
| 182 | 
            -
              (a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
         | 
| 183 | 
            -
            end
         | 
| 184 | 
            -
             | 
| 185 | 
            -
            def angle_between(a, b)
         | 
| 186 | 
            -
              cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
         | 
| 187 | 
            -
              Math.acos(cos_theta)
         | 
| 188 | 
            -
            end
         | 
| 189 | 
            -
             | 
| 190 | 
            -
            def normalize(a)
         | 
| 191 | 
            -
              length = magnitude(a)
         | 
| 192 | 
            -
              [a[0]/length, a[1]/length, a[2]/length]
         | 
| 193 | 
            -
            end
         | 
| 194 | 
            -
             | 
| 195 | 
            -
            def point_cloud_center(vertices)
         | 
| 196 | 
            -
              if vertices.count < 1
         | 
| 197 | 
            -
                return [0, 0, 0]
         | 
| 198 | 
            -
              end
         | 
| 199 | 
            -
             | 
| 200 | 
            -
              vertex = vertices[0]
         | 
| 201 | 
            -
              min_x = max_x = vertex[0]
         | 
| 202 | 
            -
              min_y = max_y = vertex[1]
         | 
| 203 | 
            -
              min_z = max_z = vertex[2]
         | 
| 204 | 
            -
             | 
| 205 | 
            -
              vertices.each do |vertex|
         | 
| 206 | 
            -
                x = vertex[0]
         | 
| 207 | 
            -
                y = vertex[1]
         | 
| 208 | 
            -
                z = vertex[2]
         | 
| 209 | 
            -
             | 
| 210 | 
            -
                min_x = x if x < min_x
         | 
| 211 | 
            -
                max_x = x if x > max_x
         | 
| 212 | 
            -
                min_y = y if y < min_y
         | 
| 213 | 
            -
                max_y = y if y > max_y
         | 
| 214 | 
            -
                min_z = z if z < min_z
         | 
| 215 | 
            -
                max_z = z if z > max_z
         | 
| 216 | 
            -
              end
         | 
| 217 | 
            -
             | 
| 218 | 
            -
              [(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
         | 
| 1 | 
            +
            class MeshNormalAnalyzer
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              def initialize(mesh)
         | 
| 4 | 
            +
                @vertices = []
         | 
| 5 | 
            +
                @intersections = []
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                vertices_node = mesh.css("vertices")
         | 
| 8 | 
            +
                vertices_node.children.each do |vertex_node|
         | 
| 9 | 
            +
                  if vertex_node.attributes.count > 0
         | 
| 10 | 
            +
                    x = vertex_node.attributes['x'].to_s.to_f
         | 
| 11 | 
            +
                    y = vertex_node.attributes['y'].to_s.to_f
         | 
| 12 | 
            +
                    z = vertex_node.attributes['z'].to_s.to_f
         | 
| 13 | 
            +
                    @vertices << [x, y, z]
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                @triangles = []
         | 
| 18 | 
            +
                triangles_node = mesh.css("triangles")
         | 
| 19 | 
            +
                triangles_node.children.each do |triangle_node|
         | 
| 20 | 
            +
                  if triangle_node.attributes.count > 0
         | 
| 21 | 
            +
                    v1 = triangle_node.attributes['v1'].to_s.to_i
         | 
| 22 | 
            +
                    v2 = triangle_node.attributes['v2'].to_s.to_i
         | 
| 23 | 
            +
                    v3 = triangle_node.attributes['v3'].to_s.to_i
         | 
| 24 | 
            +
                    @triangles << [v1, v2, v3]
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def found_inward_triangle
         | 
| 30 | 
            +
                # Trace a ray toward the center of the vertex points.  This will hopefully
         | 
| 31 | 
            +
                # maximize our chances of hitting the object's trianges on the first try.
         | 
| 32 | 
            +
                center = point_cloud_center(@vertices)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                @point = [0.0, 0.0, 0.0]
         | 
| 35 | 
            +
                @direction = vector_to(@point, center)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                # Make sure that we have a reasonably sized direction.
         | 
| 38 | 
            +
                # Might end up with a zero length vector if the center is also
         | 
| 39 | 
            +
                # at the origin.
         | 
| 40 | 
            +
                if magnitude(@direction) < 0.1
         | 
| 41 | 
            +
                  @direction = [0.57, 0.57, 0.57]
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                # make the direction a unit vector just to make the
         | 
| 45 | 
            +
                # debug info easier to understand
         | 
| 46 | 
            +
                @direction = normalize(@direction)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                attempts = 0
         | 
| 49 | 
            +
                begin
         | 
| 50 | 
            +
                  # Get all of the intersections from the ray and put them in order of distance.
         | 
| 51 | 
            +
                  # The triangle we hit that's farthest from the start of the ray should always be
         | 
| 52 | 
            +
                  # a triangle that points away from us (otherwise we would hit a triangle even
         | 
| 53 | 
            +
                  # further away, assuming the mesh is closed).
         | 
| 54 | 
            +
                  #
         | 
| 55 | 
            +
                  # One special case is when the set of triangles we hit at that distance is greater
         | 
| 56 | 
            +
                  # than one.  In that case we might have hit a "corner" of the model and so we don't
         | 
| 57 | 
            +
                  # know which of the two (or more) points away from us.  In that case, cast a random
         | 
| 58 | 
            +
                  # ray from the center of the object and try again.
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  @triangles.each do |tri|
         | 
| 61 | 
            +
                    v1 = @vertices[tri[0]]
         | 
| 62 | 
            +
                    v2 = @vertices[tri[1]]
         | 
| 63 | 
            +
                    v3 = @vertices[tri[2]]
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    process_triangle(@point, @direction, [v1, v2, v3])
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  if @intersections.count > 0
         | 
| 69 | 
            +
                    # Sort the intersections so we can find the hits that are furthest away.
         | 
| 70 | 
            +
                    @intersections.sort! {|left, right| left[0] <=> right[0]}
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    max_distance = @intersections.last[0]
         | 
| 73 | 
            +
                    furthest_hits = @intersections.select{|hit| (hit[0]-max_distance).abs < 0.0001}
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    # Print out the hits
         | 
| 76 | 
            +
                    # furthest_hits.each {|hit| puts hit[1].to_s}
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    found_good_hit = furthest_hits.count == 1
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  if found_good_hit
         | 
| 82 | 
            +
                    outside_triangle = furthest_hits.last[2]
         | 
| 83 | 
            +
                  else
         | 
| 84 | 
            +
                    @intersections = []
         | 
| 85 | 
            +
                    attempts = attempts + 1
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    target = [Random.rand(10)/10.0, Random.rand(10)/10.0, Random.rand(10)/10.0]
         | 
| 88 | 
            +
                    @point = center
         | 
| 89 | 
            +
                    @direction = normalize(vector_to(@point, target))
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end until found_good_hit || attempts >= 10
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                # return true if we hit a triangle with an inward pointing normal
         | 
| 94 | 
            +
                # (according to counter-clockwise normal orientation)
         | 
| 95 | 
            +
                found_good_hit && !compare_normals(outside_triangle, @direction)
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              def compare_normals(triangle, hit_direction)
         | 
| 99 | 
            +
                oriented_normal = cross_product(
         | 
| 100 | 
            +
                    vector_to(triangle[0], triangle[1]),
         | 
| 101 | 
            +
                    vector_to(triangle[1], triangle[2]))
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                angle = angle_between(oriented_normal, hit_direction)
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                angle < Math::PI / 2.0
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              def process_triangle(point, direction, triangle)
         | 
| 109 | 
            +
                found_intersection, t = intersect(point, direction, triangle)
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                if t > 0
         | 
| 112 | 
            +
                  intersection = []
         | 
| 113 | 
            +
                  intersection[0] = point[0] + t * direction[0]
         | 
| 114 | 
            +
                  intersection[1] = point[1] + t * direction[1]
         | 
| 115 | 
            +
                  intersection[2] = point[2] + t * direction[2]
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  @intersections << [t, intersection, triangle]
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              def intersect(point, direction, triangle)
         | 
| 122 | 
            +
                v0 = triangle[0]
         | 
| 123 | 
            +
                v1 = triangle[1]
         | 
| 124 | 
            +
                v2 = triangle[2]
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                return [false, 0] if v0.nil? || v1.nil? || v2.nil?
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                e1 = vector_to(v0, v1)
         | 
| 129 | 
            +
                e2 = vector_to(v0, v2)
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                h = cross_product(direction, e2)
         | 
| 132 | 
            +
                a = dot_product(e1, h)
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                if a.abs < 0.00001
         | 
| 135 | 
            +
                  return false, 0
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                f = 1.0/a
         | 
| 139 | 
            +
                s = vector_to(v0, point)
         | 
| 140 | 
            +
                u = f * dot_product(s, h)
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                if u < 0.0 || u > 1.0
         | 
| 143 | 
            +
                  return false, 0
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                q = cross_product(s, e1)
         | 
| 147 | 
            +
                v = f * dot_product(direction, q)
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                if v < 0.0 || u + v > 1.0
         | 
| 150 | 
            +
                  return false, 0
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                t = f * dot_product(e2, q)
         | 
| 154 | 
            +
                [t > 0, t]
         | 
| 155 | 
            +
              end
         | 
| 156 | 
            +
            end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
            # Various utility functions
         | 
| 159 | 
            +
             | 
| 160 | 
            +
            def cross_product(a, b)
         | 
| 161 | 
            +
              result = [0, 0, 0]
         | 
| 162 | 
            +
              result[0] = a[1]*b[2] - a[2]*b[1]
         | 
| 163 | 
            +
              result[1] = a[2]*b[0] - a[0]*b[2]
         | 
| 164 | 
            +
              result[2] = a[0]*b[1] - a[1]*b[0]
         | 
| 165 | 
            +
             | 
| 166 | 
            +
              result
         | 
| 167 | 
            +
            end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
            def dot_product(a, b)
         | 
| 170 | 
            +
              a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
         | 
| 171 | 
            +
            end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
            def vector_to(a, b)
         | 
| 174 | 
            +
              [b[0] - a[0], b[1] - a[1], b[2] - a[2]]
         | 
| 175 | 
            +
            end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
            def magnitude(a)
         | 
| 178 | 
            +
              Math.sqrt(a[0]*a[0] + a[1]*a[1] + a[2]*a[2])
         | 
| 179 | 
            +
            end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
            def equal(a, b)
         | 
| 182 | 
            +
              (a[0] - b[0]).abs < 0.0001 && (a[1] - b[1]).abs < 0.0001 && (a[2] - b[2]).abs < 0.0001
         | 
| 183 | 
            +
            end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
            def angle_between(a, b)
         | 
| 186 | 
            +
              cos_theta = dot_product(a, b) / (magnitude(a) * magnitude(b))
         | 
| 187 | 
            +
              Math.acos(cos_theta)
         | 
| 188 | 
            +
            end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            def normalize(a)
         | 
| 191 | 
            +
              length = magnitude(a)
         | 
| 192 | 
            +
              [a[0]/length, a[1]/length, a[2]/length]
         | 
| 193 | 
            +
            end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
            def point_cloud_center(vertices)
         | 
| 196 | 
            +
              if vertices.count < 1
         | 
| 197 | 
            +
                return [0, 0, 0]
         | 
| 198 | 
            +
              end
         | 
| 199 | 
            +
             | 
| 200 | 
            +
              vertex = vertices[0]
         | 
| 201 | 
            +
              min_x = max_x = vertex[0]
         | 
| 202 | 
            +
              min_y = max_y = vertex[1]
         | 
| 203 | 
            +
              min_z = max_z = vertex[2]
         | 
| 204 | 
            +
             | 
| 205 | 
            +
              vertices.each do |vertex|
         | 
| 206 | 
            +
                x = vertex[0]
         | 
| 207 | 
            +
                y = vertex[1]
         | 
| 208 | 
            +
                z = vertex[2]
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                min_x = x if x < min_x
         | 
| 211 | 
            +
                max_x = x if x > max_x
         | 
| 212 | 
            +
                min_y = y if y < min_y
         | 
| 213 | 
            +
                max_y = y if y > max_y
         | 
| 214 | 
            +
                min_z = z if z < min_z
         | 
| 215 | 
            +
                max_z = z if z > max_z
         | 
| 216 | 
            +
              end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
              [(min_x + max_x) / 2.0, (min_y + max_y) / 2.0, (min_z + max_z) / 2.0]
         | 
| 219 219 | 
             
            end
         |