vector2d 2.3.0 → 3.0.0
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/.github/workflows/build.yml +1 -1
- data/.gitignore +3 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +1 -1
- data/.yardopts +11 -0
- data/CHANGELOG.md +216 -0
- data/Gemfile +5 -1
- data/Gemfile.lock +35 -5
- data/README.md +325 -12
- data/Rakefile +18 -0
- data/benchmark/vector_comparison.rb +129 -0
- data/lib/vector2d/angles.rb +315 -0
- data/lib/vector2d/arithmetic.rb +97 -0
- data/lib/vector2d/comparison.rb +188 -0
- data/lib/vector2d/componentwise.rb +239 -0
- data/lib/vector2d/constructors.rb +137 -0
- data/lib/vector2d/conversions.rb +139 -0
- data/lib/vector2d/coordinates.rb +22 -0
- data/lib/vector2d/deprecation.rb +16 -0
- data/lib/vector2d/dimensions.rb +297 -0
- data/lib/vector2d/interpolation.rb +191 -0
- data/lib/vector2d/lengths.rb +284 -0
- data/lib/vector2d/matrix_interop.rb +93 -0
- data/lib/vector2d/parsing.rb +142 -0
- data/lib/vector2d/projection.rb +208 -0
- data/lib/vector2d/version.rb +2 -1
- data/lib/vector2d.rb +172 -60
- data/spec/lib/vector2d/angles_spec.rb +441 -0
- data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
- data/spec/lib/vector2d/comparison_spec.rb +358 -0
- data/spec/lib/vector2d/componentwise_spec.rb +300 -0
- data/spec/lib/vector2d/constructors_spec.rb +299 -0
- data/spec/lib/vector2d/conversions_spec.rb +234 -0
- data/spec/lib/vector2d/dimensions_spec.rb +820 -0
- data/spec/lib/vector2d/interpolation_spec.rb +322 -0
- data/spec/lib/vector2d/lengths_spec.rb +457 -0
- data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
- data/spec/lib/vector2d/parsing_spec.rb +329 -0
- data/spec/lib/vector2d/projection_spec.rb +306 -0
- data/spec/lib/vector2d_documentation_spec.rb +38 -0
- data/spec/lib/vector2d_immutability_spec.rb +263 -0
- data/spec/lib/vector2d_spec.rb +90 -50
- data/spec/lib/vector2d_subclassing_spec.rb +210 -0
- data/spec/lib/vector2d_tags_spec.rb +43 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/doc_examples/comments.rb +65 -0
- data/spec/support/doc_examples/markdown.rb +50 -0
- data/spec/support/doc_examples.rb +94 -0
- data/spec/support/doc_tags.rb +151 -0
- data/spec/support/shared_examples/class_preserving_method.rb +10 -0
- data/spec/support/shared_examples/deprecated_method.rb +25 -0
- data/spec/support/shared_examples/parsed_vector.rb +11 -0
- data/vector2d.gemspec +2 -1
- metadata +42 -13
- data/.travis.yml +0 -10
- data/lib/vector2d/calculations.rb +0 -141
- data/lib/vector2d/coercions.rb +0 -64
- data/lib/vector2d/fitting.rb +0 -60
- data/lib/vector2d/properties.rb +0 -46
- data/lib/vector2d/transformations.rb +0 -98
- data/spec/lib/vector2d/coercions_spec.rb +0 -59
- data/spec/lib/vector2d/fitting_spec.rb +0 -70
- data/spec/lib/vector2d/properties_spec.rb +0 -47
- data/spec/lib/vector2d/transformations_spec.rb +0 -139
data/spec/spec_helper.rb
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocExamples
|
|
4
|
+
# The examples in the doc comments under lib/. An example is an
|
|
5
|
+
# indented line inside a comment, and one comment is one block, named
|
|
6
|
+
# after the method it sits above.
|
|
7
|
+
#
|
|
8
|
+
# Tag blocks are skipped. A block runs from the first tag at the left
|
|
9
|
+
# margin of the comment to the next blank comment line, so the type
|
|
10
|
+
# lists in @param and @return are not mistaken for examples.
|
|
11
|
+
module Comments
|
|
12
|
+
COMMENT = /\A\s*#(?:\s|\z)/
|
|
13
|
+
INDENTED = /\A\s*#\s{3}(\s*\S.*)\z/
|
|
14
|
+
DEFINITION = /\A\s*def\s+(self\.)?([^\s(]+)/
|
|
15
|
+
TAG = /\A\s*#\s@/
|
|
16
|
+
BLANK = /\A\s*#\s*\z/
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def blocks
|
|
20
|
+
Dir[File.join(ROOT, "lib/**/*.rb")].flat_map { |path| parse(path) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse(path)
|
|
26
|
+
chunks = File.readlines(path, chomp: true).each_with_index.to_a
|
|
27
|
+
.chunk { |line, _| line.match?(COMMENT) }.to_a
|
|
28
|
+
chunks.each_with_index.filter_map do |(comment, lines), index|
|
|
29
|
+
block(path, lines, chunks.dig(index + 1, 1)) if comment
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def block(path, lines, following)
|
|
34
|
+
DocExamples.block(path.delete_prefix("#{ROOT}/"), lines.first[1] + 1,
|
|
35
|
+
name(following), code(lines))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def code(lines)
|
|
39
|
+
untagged(lines).filter_map do |line, index|
|
|
40
|
+
body = line[INDENTED, 1]
|
|
41
|
+
[body, index] if body
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Drops the tag blocks, each running from its first tag to the
|
|
46
|
+
# next blank comment line.
|
|
47
|
+
def untagged(lines)
|
|
48
|
+
tagged = false
|
|
49
|
+
lines.reject do |line, _|
|
|
50
|
+
tagged = true if line.match?(TAG)
|
|
51
|
+
tagged &&= !line.match?(BLANK)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The method a doc comment sits above, for naming the example.
|
|
56
|
+
def name(following)
|
|
57
|
+
code = Array(following).map(&:first).find { |line| !line.strip.empty? }
|
|
58
|
+
match = code&.match(DEFINITION)
|
|
59
|
+
return "the examples" unless match
|
|
60
|
+
|
|
61
|
+
"#{match[1] ? '.' : '#'}#{match[2]}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DocExamples
|
|
4
|
+
# The examples in the fenced Ruby blocks in the README.
|
|
5
|
+
#
|
|
6
|
+
# One section, from a heading to the next, is one block. The fenced
|
|
7
|
+
# blocks under a heading share a binding, the way the README reads,
|
|
8
|
+
# where a vector set up in one paragraph is used by the next.
|
|
9
|
+
module Markdown
|
|
10
|
+
FILE = "README.md"
|
|
11
|
+
HEADING = /\A##\s+(.+)\z/
|
|
12
|
+
FENCE = /\A```/
|
|
13
|
+
RUBY_FENCE = /\A```ruby\s*\z/
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def blocks
|
|
17
|
+
lines = File.readlines(File.join(ROOT, FILE), chomp: true)
|
|
18
|
+
sections(lines.each_with_index.to_a)
|
|
19
|
+
.filter_map { |section| block(section) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def sections(lines)
|
|
25
|
+
lines.slice_before { |line, _| line.match?(HEADING) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def block(section)
|
|
29
|
+
DocExamples.block(FILE, section.first[1] + 1, name(section),
|
|
30
|
+
code(section))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def name(section)
|
|
34
|
+
heading = section.first[0][HEADING, 1]
|
|
35
|
+
heading ? "the #{heading} section" : "the examples"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The lines inside the ```ruby fences, with the blank ones dropped.
|
|
39
|
+
def code(section)
|
|
40
|
+
fenced(section).reject { |line, _| line.strip.empty? }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fenced(lines)
|
|
44
|
+
lines.slice_before { |line, _| line.match?(FENCE) }
|
|
45
|
+
.select { |chunk| chunk.first[0].match?(RUBY_FENCE) }
|
|
46
|
+
.flat_map { |chunk| chunk.drop(1) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
# Extracts the runnable examples from the documentation.
|
|
6
|
+
#
|
|
7
|
+
# The doc comments under lib/ and the fenced Ruby blocks in the README
|
|
8
|
+
# are read the same way. Lines carrying a "# =>" marker assert on the
|
|
9
|
+
# value of the expression to their left, the rest are setup, and the
|
|
10
|
+
# examples in one block share a binding:
|
|
11
|
+
#
|
|
12
|
+
# v = Vector2d(2, 3)
|
|
13
|
+
# v.length # => 3.6055..
|
|
14
|
+
#
|
|
15
|
+
# A marker on a line of its own applies to the line above it. A
|
|
16
|
+
# trailing ".." stands in for any digits, and an exception class means
|
|
17
|
+
# the expression is expected to raise. An example spanning several
|
|
18
|
+
# lines, like a class definition, is evaluated as one expression.
|
|
19
|
+
module DocExamples
|
|
20
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
21
|
+
MARKER = /\A(.*?)\s*#\s*=>\s*(.+)\z/
|
|
22
|
+
EXCEPTION = /\A(?:[A-Z]\w*::)*[A-Z]\w*Error\z/
|
|
23
|
+
HASH_ROCKET = /:(\w+)=>/
|
|
24
|
+
|
|
25
|
+
# A single line of an example. Setup lines have no expectation.
|
|
26
|
+
Example = Struct.new(:location, :code, :expected) do
|
|
27
|
+
def expectation? = !expected.nil?
|
|
28
|
+
|
|
29
|
+
# Appends a line to an example that is not a complete expression yet.
|
|
30
|
+
def continue(line, expected)
|
|
31
|
+
self.code = "#{code}\n#{line}"
|
|
32
|
+
self.expected = expected
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def raises? = expected.match?(EXCEPTION)
|
|
36
|
+
|
|
37
|
+
def exception = Object.const_get(expected)
|
|
38
|
+
|
|
39
|
+
def pattern
|
|
40
|
+
escaped = Regexp.escape(normalize(expected))
|
|
41
|
+
Regexp.new("\\A#{escaped.gsub('\.\.') { '\d*' }}\\z")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspected(value) = normalize(value.inspect)
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Values are compared with whitespace removed, so the documentation
|
|
49
|
+
# is free to align the markers, and with hashes in the {key: value}
|
|
50
|
+
# form Ruby 3.4 and up print.
|
|
51
|
+
def normalize(inspected)
|
|
52
|
+
inspected.delete(" ").gsub(HASH_ROCKET) { "#{Regexp.last_match(1)}:" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The examples from a single doc comment or README section.
|
|
57
|
+
Block = Struct.new(:file, :line, :name, :examples)
|
|
58
|
+
|
|
59
|
+
class << self
|
|
60
|
+
def blocks = Comments.blocks + Markdown.blocks
|
|
61
|
+
|
|
62
|
+
# A block built from its code lines, or nil if none of them assert
|
|
63
|
+
# on anything.
|
|
64
|
+
def block(file, line, name, lines)
|
|
65
|
+
examples = examples(file, lines)
|
|
66
|
+
return unless examples.any?(&:expectation?)
|
|
67
|
+
|
|
68
|
+
Block.new(file, line, name, examples)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def examples(file, lines)
|
|
74
|
+
lines.each_with_object([]) do |(body, index), found|
|
|
75
|
+
append(found, "#{file}:#{index + 1}", body)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def append(found, location, body)
|
|
80
|
+
expression, expected = body.match(MARKER)&.captures
|
|
81
|
+
if expected && expression.empty?
|
|
82
|
+
found.last.expected = expected
|
|
83
|
+
elsif continues?(found.last)
|
|
84
|
+
found.last.continue(expression || body, expected)
|
|
85
|
+
else
|
|
86
|
+
found << Example.new(location, expression || body, expected)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def continues?(example) = example && !complete?(example.code)
|
|
91
|
+
|
|
92
|
+
def complete?(code) = !Ripper.sexp(code).nil?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yard"
|
|
4
|
+
|
|
5
|
+
# The type vocabulary the doc tags are written in.
|
|
6
|
+
#
|
|
7
|
+
# YARD tags are the only place a type is declared, so a union that
|
|
8
|
+
# loses a member in one comment is a silent error. These are the
|
|
9
|
+
# canonical spellings, and the checks below hold every tag to them.
|
|
10
|
+
module DocTags
|
|
11
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
12
|
+
|
|
13
|
+
# Coordinates are the concrete real classes. Numeric is wrong: to_i,
|
|
14
|
+
# to_f and nan? are defined on these four, not on Numeric.
|
|
15
|
+
COORDINATE = %w[Integer Float Rational BigDecimal].freeze
|
|
16
|
+
|
|
17
|
+
# Everything Vector2d.parse accepts, written out once in the
|
|
18
|
+
# +coercible+ macro in lib/vector2d.rb.
|
|
19
|
+
COERCIBLE = %w[Vector2d Array String Hash Integer Float Rational
|
|
20
|
+
BigDecimal ::Vector ::Matrix].freeze
|
|
21
|
+
|
|
22
|
+
# The members of the parse union that are not coordinates. Naming
|
|
23
|
+
# two of them is what marks a parameter as taking the whole union.
|
|
24
|
+
SHAPES = %w[Vector2d Array String Hash ::Vector ::Matrix].freeze
|
|
25
|
+
|
|
26
|
+
# Numeric spans Complex, which is not a coordinate, and misses the
|
|
27
|
+
# methods the coordinate types are used for.
|
|
28
|
+
FORBIDDEN = %w[Numeric].freeze
|
|
29
|
+
TYPE_NAME = /(?:::)?[A-Z]\w*/
|
|
30
|
+
TAG = /\A\s*#\s@/
|
|
31
|
+
DEPRECATED = /\A\s*#\s@deprecated\b/
|
|
32
|
+
CONTINUATION = /\A\s*#\s{2,}\S/
|
|
33
|
+
COMMENT = /\A\s*#(?:\s|\z)/
|
|
34
|
+
|
|
35
|
+
# One tag, with its type list flattened to the names it mentions.
|
|
36
|
+
Tag = Struct.new(:path, :kind, :name, :types) do
|
|
37
|
+
def to_s = [path, "@#{kind}", name].compact.join(" ")
|
|
38
|
+
|
|
39
|
+
def count(union) = (union & types).size
|
|
40
|
+
|
|
41
|
+
def missing(union) = union - types
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
def methods
|
|
46
|
+
load_registry
|
|
47
|
+
YARD::Registry.all(:method)
|
|
48
|
+
.select { |m| m.visibility == :public }
|
|
49
|
+
.sort_by(&:path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Every parameter the method takes, keywords included.
|
|
53
|
+
def parameters(method)
|
|
54
|
+
method.parameters.map { |name, _| name.to_s.delete("*&:") }
|
|
55
|
+
.reject(&:empty?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The parameters with no @param, counting the ones inside an
|
|
59
|
+
# @overload.
|
|
60
|
+
def undocumented_parameters(method)
|
|
61
|
+
documented = method.tags(:param) +
|
|
62
|
+
method.tags(:overload).flat_map { |o| o.tags(:param) }
|
|
63
|
+
parameters(method) - documented.map(&:name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tags(method)
|
|
67
|
+
collect(method, method) +
|
|
68
|
+
method.tags(:overload).flat_map { |o| collect(method, o) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The tags naming more than one coordinate type, which have to
|
|
72
|
+
# name all four. One on its own is a type in its own right, as the
|
|
73
|
+
# Integer #ceil rounds to is.
|
|
74
|
+
def coordinate_tags(method)
|
|
75
|
+
tags(method).select { |tag| tag.count(COORDINATE) > 1 }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The tags taking anything Vector2d.parse accepts, which have to
|
|
79
|
+
# name the whole union.
|
|
80
|
+
def parse_union_tags(method)
|
|
81
|
+
tags(method).select do |tag|
|
|
82
|
+
tag.kind == :param && tag.count(SHAPES) > 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Methods warning about their own deprecation without saying so in
|
|
87
|
+
# a tag.
|
|
88
|
+
def untagged_deprecations
|
|
89
|
+
load_registry
|
|
90
|
+
YARD::Registry.all(:method).select do |method|
|
|
91
|
+
method.name != :warn_deprecated &&
|
|
92
|
+
method.source.to_s.include?("warn_deprecated") &&
|
|
93
|
+
method.tag(:deprecated).nil?
|
|
94
|
+
end.map(&:path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Comment lines that come after the tag block of their doc
|
|
98
|
+
# comment. Tags close a comment, and the example extractor reads
|
|
99
|
+
# them as running to the end of it, so prose after them would go
|
|
100
|
+
# missing and type lists before them would be read as code.
|
|
101
|
+
# @deprecated is the exception, and opens a comment.
|
|
102
|
+
def prose_after_tags
|
|
103
|
+
sources.flat_map do |file, lines|
|
|
104
|
+
comment_blocks(lines).flat_map { |block| stray(file, block) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def comment_blocks(lines)
|
|
111
|
+
lines.each_with_index.chunk { |line, _| line.match?(COMMENT) }
|
|
112
|
+
.select(&:first).map(&:last)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def stray(file, block)
|
|
116
|
+
start = block.index { |line, _| line.match?(TAG) && !line.match?(DEPRECATED) }
|
|
117
|
+
return [] unless start
|
|
118
|
+
|
|
119
|
+
block[start..]
|
|
120
|
+
.reject { |line, _| line.match?(TAG) || line.match?(CONTINUATION) }
|
|
121
|
+
.map { |_, index| "#{file}:#{index + 1}" }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def collect(method, holder)
|
|
125
|
+
%i[param return].flat_map do |kind|
|
|
126
|
+
holder.tags(kind).map do |tag|
|
|
127
|
+
Tag.new(method.path, kind, tag.name,
|
|
128
|
+
Array(tag.types).join(",").scan(TYPE_NAME).uniq)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sources
|
|
134
|
+
files.map { |path| [path.delete_prefix("#{ROOT}/"), File.readlines(path, chomp: true)] }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# lib/vector2d.rb first, so the macros it defines are known by the
|
|
138
|
+
# time the modules using them are parsed.
|
|
139
|
+
def files
|
|
140
|
+
Dir[File.join(ROOT, "lib/**/*.rb")].sort_by { |path| [path.count("/"), path] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def load_registry
|
|
144
|
+
return if @loaded
|
|
145
|
+
|
|
146
|
+
YARD::Registry.clear
|
|
147
|
+
YARD.parse(files, [], YARD::Logger::ERROR)
|
|
148
|
+
@loaded = true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples "a class preserving method" do |method, *args|
|
|
4
|
+
describe "##{method}" do
|
|
5
|
+
it "returns an instance of the receiver's class" do
|
|
6
|
+
expect(vector.public_send(method, *args))
|
|
7
|
+
.to be_an_instance_of(vector.class)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.shared_examples "a deprecated method" do |name, replacement|
|
|
4
|
+
around do |example|
|
|
5
|
+
enabled = Warning[:deprecated]
|
|
6
|
+
Warning[:deprecated] = true
|
|
7
|
+
example.run
|
|
8
|
+
Warning[:deprecated] = enabled
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "warns that the method is deprecated" do
|
|
12
|
+
expect { vector }
|
|
13
|
+
.to output(/Vector2d##{name} is deprecated/).to_stderr
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "points at the replacement" do
|
|
17
|
+
expect { vector }
|
|
18
|
+
.to output(/#{Regexp.escape(replacement)}/).to_stderr
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "is silent when deprecation warnings are disabled" do
|
|
22
|
+
Warning[:deprecated] = false
|
|
23
|
+
expect { vector }.not_to output.to_stderr
|
|
24
|
+
end
|
|
25
|
+
end
|
data/vector2d.gemspec
CHANGED
|
@@ -20,10 +20,11 @@ Gem::Specification.new do |s|
|
|
|
20
20
|
.map { |f| File.basename(f) }
|
|
21
21
|
s.require_paths = ["lib"]
|
|
22
22
|
|
|
23
|
-
s.required_ruby_version = ">= 3.
|
|
23
|
+
s.required_ruby_version = ">= 3.4.0"
|
|
24
24
|
|
|
25
25
|
s.metadata = {
|
|
26
26
|
"bug_tracker_uri" => "https://github.com/elektronaut/vector2d/issues",
|
|
27
|
+
"changelog_uri" => "https://github.com/elektronaut/vector2d/blob/main/CHANGELOG.md",
|
|
27
28
|
"documentation_uri" => "https://www.rubydoc.info/gems/vector2d",
|
|
28
29
|
"rubygems_mfa_required" => "true",
|
|
29
30
|
"source_code_uri" => "https://github.com/elektronaut/vector2d"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vector2d
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Inge Jørgensen
|
|
@@ -23,7 +23,7 @@ files:
|
|
|
23
23
|
- ".release-please-manifest.json"
|
|
24
24
|
- ".rubocop.yml"
|
|
25
25
|
- ".tool-versions"
|
|
26
|
-
- ".
|
|
26
|
+
- ".yardopts"
|
|
27
27
|
- CHANGELOG.md
|
|
28
28
|
- CODE_OF_CONDUCT.md
|
|
29
29
|
- CONTRIBUTING.md
|
|
@@ -32,27 +32,56 @@ files:
|
|
|
32
32
|
- LICENSE
|
|
33
33
|
- README.md
|
|
34
34
|
- Rakefile
|
|
35
|
+
- benchmark/vector_comparison.rb
|
|
35
36
|
- lib/vector2d.rb
|
|
36
|
-
- lib/vector2d/
|
|
37
|
-
- lib/vector2d/
|
|
38
|
-
- lib/vector2d/
|
|
39
|
-
- lib/vector2d/
|
|
40
|
-
- lib/vector2d/
|
|
37
|
+
- lib/vector2d/angles.rb
|
|
38
|
+
- lib/vector2d/arithmetic.rb
|
|
39
|
+
- lib/vector2d/comparison.rb
|
|
40
|
+
- lib/vector2d/componentwise.rb
|
|
41
|
+
- lib/vector2d/constructors.rb
|
|
42
|
+
- lib/vector2d/conversions.rb
|
|
43
|
+
- lib/vector2d/coordinates.rb
|
|
44
|
+
- lib/vector2d/deprecation.rb
|
|
45
|
+
- lib/vector2d/dimensions.rb
|
|
46
|
+
- lib/vector2d/interpolation.rb
|
|
47
|
+
- lib/vector2d/lengths.rb
|
|
48
|
+
- lib/vector2d/matrix_interop.rb
|
|
49
|
+
- lib/vector2d/parsing.rb
|
|
50
|
+
- lib/vector2d/projection.rb
|
|
41
51
|
- lib/vector2d/version.rb
|
|
42
52
|
- release-please-config.json
|
|
43
|
-
- spec/lib/vector2d/
|
|
44
|
-
- spec/lib/vector2d/
|
|
45
|
-
- spec/lib/vector2d/
|
|
46
|
-
- spec/lib/vector2d/
|
|
47
|
-
- spec/lib/vector2d/
|
|
53
|
+
- spec/lib/vector2d/angles_spec.rb
|
|
54
|
+
- spec/lib/vector2d/arithmetic_spec.rb
|
|
55
|
+
- spec/lib/vector2d/comparison_spec.rb
|
|
56
|
+
- spec/lib/vector2d/componentwise_spec.rb
|
|
57
|
+
- spec/lib/vector2d/constructors_spec.rb
|
|
58
|
+
- spec/lib/vector2d/conversions_spec.rb
|
|
59
|
+
- spec/lib/vector2d/dimensions_spec.rb
|
|
60
|
+
- spec/lib/vector2d/interpolation_spec.rb
|
|
61
|
+
- spec/lib/vector2d/lengths_spec.rb
|
|
62
|
+
- spec/lib/vector2d/matrix_interop_spec.rb
|
|
63
|
+
- spec/lib/vector2d/parsing_spec.rb
|
|
64
|
+
- spec/lib/vector2d/projection_spec.rb
|
|
65
|
+
- spec/lib/vector2d_documentation_spec.rb
|
|
66
|
+
- spec/lib/vector2d_immutability_spec.rb
|
|
48
67
|
- spec/lib/vector2d_spec.rb
|
|
68
|
+
- spec/lib/vector2d_subclassing_spec.rb
|
|
69
|
+
- spec/lib/vector2d_tags_spec.rb
|
|
49
70
|
- spec/spec_helper.rb
|
|
71
|
+
- spec/support/doc_examples.rb
|
|
72
|
+
- spec/support/doc_examples/comments.rb
|
|
73
|
+
- spec/support/doc_examples/markdown.rb
|
|
74
|
+
- spec/support/doc_tags.rb
|
|
75
|
+
- spec/support/shared_examples/class_preserving_method.rb
|
|
76
|
+
- spec/support/shared_examples/deprecated_method.rb
|
|
77
|
+
- spec/support/shared_examples/parsed_vector.rb
|
|
50
78
|
- vector2d.gemspec
|
|
51
79
|
homepage: https://github.com/elektronaut/vector2d
|
|
52
80
|
licenses:
|
|
53
81
|
- MIT
|
|
54
82
|
metadata:
|
|
55
83
|
bug_tracker_uri: https://github.com/elektronaut/vector2d/issues
|
|
84
|
+
changelog_uri: https://github.com/elektronaut/vector2d/blob/main/CHANGELOG.md
|
|
56
85
|
documentation_uri: https://www.rubydoc.info/gems/vector2d
|
|
57
86
|
rubygems_mfa_required: 'true'
|
|
58
87
|
source_code_uri: https://github.com/elektronaut/vector2d
|
|
@@ -63,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
63
92
|
requirements:
|
|
64
93
|
- - ">="
|
|
65
94
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: 3.
|
|
95
|
+
version: 3.4.0
|
|
67
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
97
|
requirements:
|
|
69
98
|
- - ">="
|
data/.travis.yml
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class Vector2d
|
|
4
|
-
module Calculations
|
|
5
|
-
module ClassMethods
|
|
6
|
-
# Calculates cross product of two vectors.
|
|
7
|
-
#
|
|
8
|
-
# v1 = Vector2d(2, 1)
|
|
9
|
-
# v2 = Vector2d(2, 3)
|
|
10
|
-
# Vector2d.cross_product(v1, v2) # => 4
|
|
11
|
-
#
|
|
12
|
-
def cross_product(vector1, vector2)
|
|
13
|
-
(vector1.x * vector2.y) - (vector1.y * vector2.x)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Calculates dot product of two vectors.
|
|
17
|
-
#
|
|
18
|
-
# v1 = Vector2d(2, 1)
|
|
19
|
-
# v2 = Vector2d(2, 3)
|
|
20
|
-
# Vector2d.dot_product(v1, v2) # => 10
|
|
21
|
-
#
|
|
22
|
-
def dot_product(vector1, vector2)
|
|
23
|
-
(vector1.x * vector2.x) + (vector1.y * vector2.y)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Calculates angle between two vectors in radians.
|
|
27
|
-
#
|
|
28
|
-
# v1 = Vector2d(2, 3)
|
|
29
|
-
# v2 = Vector2d(4, 5)
|
|
30
|
-
# Vector2d.angle_between(v1, v2) # => 0.0867..
|
|
31
|
-
#
|
|
32
|
-
def angle_between(vector1, vector2)
|
|
33
|
-
one = vector1.normalized? ? vector1 : vector1.normalize
|
|
34
|
-
two = vector2.normalized? ? vector2 : vector2.normalize
|
|
35
|
-
Math.acos(dot_product(one, two))
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Multiplies vectors.
|
|
40
|
-
#
|
|
41
|
-
# Vector2d(1, 2) * Vector2d(2, 3) # => Vector2d(2, 6)
|
|
42
|
-
# Vector2d(1, 2) * 2 # => Vector2d(2, 4)
|
|
43
|
-
#
|
|
44
|
-
def *(other)
|
|
45
|
-
calculate_each(:*, other)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Divides vectors.
|
|
49
|
-
#
|
|
50
|
-
# Vector2d(4, 2) / Vector2d(2, 1) # => Vector2d(2, 2)
|
|
51
|
-
# Vector2d(4, 2) / 2 # => Vector2d(2, 1)
|
|
52
|
-
#
|
|
53
|
-
def /(other)
|
|
54
|
-
calculate_each(:/, other)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Adds vectors.
|
|
58
|
-
#
|
|
59
|
-
# Vector2d(1, 2) + Vector2d(2, 3) # => Vector2d(3, 5)
|
|
60
|
-
# Vector2d(1, 2) + 2 # => Vector2d(3, 4)
|
|
61
|
-
#
|
|
62
|
-
def +(other)
|
|
63
|
-
calculate_each(:+, other)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Subtracts vectors.
|
|
67
|
-
#
|
|
68
|
-
# Vector2d(2, 3) - Vector2d(2, 1) # => Vector2d(0, 2)
|
|
69
|
-
# Vector2d(4, 3) - 1 # => Vector2d(3, 2)
|
|
70
|
-
#
|
|
71
|
-
def -(other)
|
|
72
|
-
calculate_each(:-, other)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Calculates the distance between two vectors.
|
|
76
|
-
#
|
|
77
|
-
# v1 = Vector2d(2, 3)
|
|
78
|
-
# v2 = Vector2d(5, 6)
|
|
79
|
-
# v1.distance(v2) # => 1.4142..
|
|
80
|
-
#
|
|
81
|
-
def distance(other)
|
|
82
|
-
Math.sqrt(squared_distance(other))
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Calculate squared distance between vectors.
|
|
86
|
-
#
|
|
87
|
-
# v1 = Vector2d(2, 3)
|
|
88
|
-
# v2 = Vector2d(5, 6)
|
|
89
|
-
# v1.distance_squared(v2) # => 18
|
|
90
|
-
#
|
|
91
|
-
def squared_distance(other)
|
|
92
|
-
v, = coerce(other)
|
|
93
|
-
dx = v.x - x
|
|
94
|
-
dy = v.y - y
|
|
95
|
-
(dx * dx) + (dy * dy)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Dot product of this vector and another vector.
|
|
99
|
-
#
|
|
100
|
-
# v1 = Vector2d(2, 1)
|
|
101
|
-
# v2 = Vector2d(2, 3)
|
|
102
|
-
# v1.dot_product(v2) # => 10
|
|
103
|
-
#
|
|
104
|
-
def dot_product(other)
|
|
105
|
-
v, = coerce(other)
|
|
106
|
-
self.class.dot_product(self, v)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Cross product of this vector and another vector.
|
|
110
|
-
#
|
|
111
|
-
# v1 = Vector2d(2, 1)
|
|
112
|
-
# v2 = Vector2d(2, 3)
|
|
113
|
-
# v1.cross_product(v2) # => 4
|
|
114
|
-
#
|
|
115
|
-
def cross_product(other)
|
|
116
|
-
v, = coerce(other)
|
|
117
|
-
self.class.cross_product(self, v)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Angle in radians between this vector and another vector.
|
|
121
|
-
#
|
|
122
|
-
# v1 = Vector2d(2, 3)
|
|
123
|
-
# v2 = Vector2d(4, 5)
|
|
124
|
-
# v1.angle_between(v2) # => 0.0867..
|
|
125
|
-
#
|
|
126
|
-
def angle_between(other)
|
|
127
|
-
v, = coerce(other)
|
|
128
|
-
self.class.angle_between(self, v)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
private
|
|
132
|
-
|
|
133
|
-
def calculate_each(method, other)
|
|
134
|
-
v, = coerce(other)
|
|
135
|
-
self.class.new(
|
|
136
|
-
x.send(method, v.x),
|
|
137
|
-
y.send(method, v.y)
|
|
138
|
-
)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|