geom_craft 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2c55f5ca6a18a1d30f105f666b6aa1d51f97ef06f376af7b4aea7a9c4b2448ab
4
+ data.tar.gz: e07e3b8f910da37f28b60f69f4dac4b19259fc5738b1c04941c07a97aca5d789
5
+ SHA512:
6
+ metadata.gz: be621512a1a79272ea25a9ad4ca1208e51ffbea198f0fdbc285669aa2abc56b3422682fb9e3052cd163309620626189f36cca19990853837e1cf2cc9fe1e840b
7
+ data.tar.gz: 9597e85fc040319961b90217a8ed18024190efba7f99020e26ff7bbf55b9e64990556f6cf207a7bdd83282db8c5de5e59c83918a97ccc749fb99ec0f0f9e58a2
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /*.html
19
+ *.ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2025 akicho8
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # GeomCraft
2
+
3
+ ## Introduction
4
+
5
+ This is a Ruby library inspired by the Rect and 2D vector libraries used in the Rust Nannou framework.
6
+
7
+ ## Installation
8
+
9
+ Install as a standalone gem
10
+
11
+ ```shell
12
+ $ gem install geom_craft
13
+ ```
14
+
15
+ Or install within application using Gemfile
16
+
17
+ ```shell
18
+ $ bundle add geom_craft
19
+ $ bundle install
20
+ ```
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task :default => :spec
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "geom_craft/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "geom_craft"
7
+ spec.version = GeomCraft::VERSION
8
+ spec.authors = ["akicho8"]
9
+ spec.email = ["akicho8@gmail.com"]
10
+ spec.description = %q{This is a Ruby library inspired by the Rect and 2D vector libraries used in the Rust Nannou framework}
11
+ spec.summary = %q{This is a Ruby library inspired by the Rect and 2D vector libraries used in the Rust Nannou framework}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "rake"
21
+ spec.add_development_dependency "rspec"
22
+ spec.add_development_dependency "test-unit"
23
+ end
@@ -0,0 +1,13 @@
1
+ module GeomCraft
2
+ module MathExt
3
+ # https://github.com/godotengine/godot/blob/44e399ed5fa895f760b2995e59788bdb49782666/core/math/math_funcs.cpp#L120
4
+ def snapped(value, step)
5
+ if step.nonzero?
6
+ value = (value / step + 0.5).floor * step
7
+ end
8
+ value
9
+ end
10
+ end
11
+ end
12
+
13
+ Math.extend(GeomCraft::MathExt)
@@ -0,0 +1,44 @@
1
+ module GeomCraft
2
+ module AngleConvert
3
+ TAU = 2 * Math::PI
4
+ ONE_TURN_DEGREES = 360.0
5
+
6
+ def rad_to_deg
7
+ self * ONE_TURN_DEGREES / TAU
8
+ end
9
+
10
+ def rad_to_turn
11
+ self / TAU
12
+ end
13
+
14
+ def rad_to_rad
15
+ self
16
+ end
17
+
18
+ def deg_to_rad
19
+ self * TAU / ONE_TURN_DEGREES
20
+ end
21
+
22
+ def deg_to_turn
23
+ self / ONE_TURN_DEGREES
24
+ end
25
+
26
+ def deg_to_deg
27
+ self
28
+ end
29
+
30
+ def turn_to_deg
31
+ self * ONE_TURN_DEGREES
32
+ end
33
+
34
+ def turn_to_rad
35
+ self * TAU
36
+ end
37
+
38
+ def turn_to_turn
39
+ self
40
+ end
41
+ end
42
+ end
43
+
44
+ Numeric.include(GeomCraft::AngleConvert)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
4
+ require path
5
+ end
6
+
7
+ # copy from /opt/rbenv/versions/3.4.2/lib/ruby/gems/3.4.0/gems/activesupport-8.0.2/lib/active_support/core_ext.rb
@@ -0,0 +1,55 @@
1
+ module GeomCraft
2
+ # 普通は rand の実行回数が少ないこっちを使う
3
+ # 平均(mean) で 標準偏差が 1.0 (7割が±1.0の範囲にあるという意味)
4
+ class RandNorm
5
+ class << self
6
+ def call(...)
7
+ @instance ||= new(...)
8
+ @instance.call(...)
9
+ end
10
+ end
11
+
12
+ def initialize(mu: 0.0, sigma: 1.0, random: Random.new)
13
+ @mu = mu
14
+ @sigma = sigma
15
+ @random = random
16
+ end
17
+
18
+ def call(mu: @mu, sigma: @sigma, random: @random)
19
+ if @next_value
20
+ v = @next_value
21
+ @next_value = nil
22
+ else
23
+ r1 = random.rand
24
+ r2 = random.rand
25
+ a = Math.sqrt(-2 * Math.log(r1))
26
+ b = 2 * Math::PI * r2
27
+ x = a * Math.cos(b) # ここで1つ取れる
28
+ y = a * Math.sin(b) # まとめて2つ目も取れる
29
+ @next_value = y # 次に返すように取っておく
30
+ v = x
31
+ end
32
+ mu + sigma * v
33
+ end
34
+ end
35
+ end
36
+
37
+ if $0 == __FILE__
38
+ @rand_norm = GeomCraft::RandNorm.new
39
+ def rnorm(...)
40
+ @rand_norm.call(...)
41
+ end
42
+
43
+ n = 1000000
44
+ list = n.times.collect { rnorm(mu: 70, sigma: 10) }
45
+
46
+ # 配列から標準偏差の取得
47
+ avg = list.sum.fdiv(n)
48
+ sd = Math.sqrt(list.collect { |v| (v - avg)**2 }.sum.fdiv(n))
49
+ sd # => 9.996427943190731
50
+
51
+ # 1,2,3倍はそれぞれ 68.26% 95.44% 99.74% になるか?
52
+ list.count { |v| (-(sd*1)..(sd*1)).include?(v - avg) }.fdiv(n) # => 0.682338
53
+ list.count { |v| (-(sd*2)..(sd*2)).include?(v - avg) }.fdiv(n) # => 0.954698
54
+ list.count { |v| (-(sd*3)..(sd*3)).include?(v - avg) }.fdiv(n) # => 0.997286
55
+ end
@@ -0,0 +1,462 @@
1
+ # https://zenn.dev/megeton/articles/0ace7ce5d23f48
2
+ # https://docs.rs/nannou/0.18.1/nannou/geom/range/struct.Range.html
3
+
4
+ module GeomCraft
5
+ class Range2
6
+ class << self
7
+ def [](...)
8
+ new(...)
9
+ end
10
+
11
+ def one; 1.0; end
12
+ def zero; 0.0; end
13
+
14
+ def from_pos_and_len(pos, len)
15
+ half_len = len / 2.0
16
+ start = pos - half_len
17
+ _end = pos + half_len
18
+ new(start, _end)
19
+ end
20
+
21
+ def map_range(val, in_min, in_max, out_min, out_max)
22
+ (val.to_f - in_min) / (in_max - in_min) * (out_max - out_min) + out_min
23
+ end
24
+
25
+ def clamp(value, start, _end)
26
+ if start <= _end
27
+ if value < start
28
+ start
29
+ elsif value > _end
30
+ _end
31
+ else
32
+ value
33
+ end
34
+ else
35
+ if value < _end
36
+ _end
37
+ elsif value > start
38
+ start
39
+ else
40
+ value
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ attr_accessor :start
47
+ attr_accessor :end
48
+
49
+ def initialize(start, _end)
50
+ @start = start
51
+ @end = _end
52
+ end
53
+
54
+ def magnitude
55
+ @end - @start
56
+ end
57
+
58
+ def length
59
+ mag = magnitude
60
+ if mag < self.class.zero
61
+ -mag
62
+ else
63
+ mag
64
+ end
65
+ end
66
+
67
+ def middle
68
+ (@end + @start).fdiv(2)
69
+ end
70
+
71
+ def invert
72
+ self.class.new(@end, @start)
73
+ end
74
+
75
+ def map_value(value, other)
76
+ self.class.map_range(value, @start, @end, other.start, other.end)
77
+ end
78
+
79
+ def lerp(amount)
80
+ @start + (@end - @start) * amount
81
+ end
82
+
83
+ def shift(amount)
84
+ self.class.new(@start + amount, @end + amount)
85
+ end
86
+
87
+ def direction
88
+ if @start < @end
89
+ self.class.one
90
+ elsif @start > @end
91
+ -self.class.one
92
+ else
93
+ self.class.zero
94
+ end
95
+ end
96
+
97
+ def absolute
98
+ if @start > @end
99
+ invert
100
+ else
101
+ self
102
+ end
103
+ end
104
+
105
+ def max(other)
106
+ start = [@start, @end, other.start, other.end].min
107
+ _end = [@start, @end, other.start, other.end].max
108
+ self.class.new(start, _end)
109
+ end
110
+
111
+ def overlap(other)
112
+ a = absolute
113
+ other = other.absolute
114
+ start = [a.start, other.start].max
115
+ _end = [a.end, other.end].min
116
+ magnitude = _end - start
117
+ if magnitude >= self.class.zero
118
+ self.class.new(start, _end)
119
+ end
120
+ end
121
+
122
+ def max_directed(other)
123
+ if @start <= @end
124
+ max(other)
125
+ else
126
+ max(other).invert
127
+ end
128
+ end
129
+
130
+ def contains?(pos)
131
+ a = absolute
132
+ a.start <= pos && pos <= a.end
133
+ end
134
+
135
+ def round(...); self.class.new(@start.round(...), @end.round(...)); end
136
+ def floor(...); self.class.new(@start.floor(...), @end.floor(...)); end
137
+ def ceil(...); self.class.new(@start.ceil(...), @end.ceil(...)); end
138
+ def truncate(...); self.class.new(@start.truncate(...), @end.truncate(...)); end
139
+
140
+ def pad_start(pad); self.class.new(@start + (@start <= @end ? pad : -pad), @end); end
141
+ def pad_end(pad); self.class.new(@start, @end + (@start <= @end ? -pad : pad)); end
142
+ def pad(pad); pad_start(pad).pad_end(pad); end
143
+ def pad_ends(start, _end); pad_start(start).pad_end(_end); end
144
+
145
+ def clamp_value(value)
146
+ self.class.clamp(value, @start, @end)
147
+ end
148
+
149
+ def stretch_to_value(value)
150
+ if @start <= @end
151
+ if value < @start
152
+ self.class.new(value, @end)
153
+ elsif value > @end
154
+ self.class.new(@start, value)
155
+ else
156
+ self
157
+ end
158
+ else
159
+ if value < @end
160
+ self.class.new(@start, value)
161
+ elsif value > @start
162
+ self.class.new(value, @end)
163
+ else
164
+ self
165
+ end
166
+ end
167
+ end
168
+
169
+ def same_direction?(other)
170
+ self_direction = @start <= @end
171
+ other_direction = other.start <= other.end
172
+ self_direction == other_direction
173
+ end
174
+
175
+ def align_start_of(other)
176
+ if same_direction?(other)
177
+ diff = other.start - @start
178
+ else
179
+ diff = other.start - @end
180
+ end
181
+ shift(diff)
182
+ end
183
+
184
+ def align_end_of(other)
185
+ if same_direction?(other)
186
+ diff = other.end - @end
187
+ else
188
+ diff = other.end - @start
189
+ end
190
+ shift(diff)
191
+ end
192
+
193
+ def align_middle_of(other)
194
+ diff = other.middle - middle
195
+ shift(diff)
196
+ end
197
+
198
+ def align_after(other)
199
+ if same_direction?(other)
200
+ diff = other.end - @start
201
+ else
202
+ diff = other.end - @end
203
+ end
204
+ shift(diff)
205
+ end
206
+
207
+ def align_before(other)
208
+ if self.same_direction?(other)
209
+ diff = other.start - @end
210
+ else
211
+ diff = other.start - @start
212
+ end
213
+ shift(diff)
214
+ end
215
+
216
+ def align_to(align, other)
217
+ public_send("align_#{align}_of", other)
218
+ end
219
+
220
+ def closest_edge(scalar)
221
+ if scalar < @start
222
+ start_diff = @start - scalar
223
+ else
224
+ start_diff = scalar - @start
225
+ end
226
+ if scalar < @end
227
+ end_diff = @end - scalar
228
+ else
229
+ end_diff = scalar - @end
230
+ end
231
+ if start_diff <= end_diff
232
+ :start
233
+ else
234
+ :end
235
+ end
236
+ end
237
+
238
+ ################################################################################
239
+
240
+ def to_a
241
+ [@start, @end]
242
+ end
243
+
244
+ def ==(other)
245
+ self.class == other.class && @start == other.start && @end == other.end
246
+ end
247
+
248
+ def eql?(other)
249
+ self.class == other.class && @start == other.start && @end == other.end
250
+ end
251
+
252
+ def hash
253
+ self.class.hash ^ @start.hash ^ @end.hash
254
+ end
255
+
256
+ def <=>(other)
257
+ [self.class, @start, @end] <=> [other.class, other.start, other.end]
258
+ end
259
+
260
+ def inspect
261
+ "(#{@start} -> #{@end})"
262
+ end
263
+
264
+ def to_s
265
+ "(#{@start} -> #{@end})"
266
+ end
267
+ end
268
+ end
269
+
270
+ if $0 == __FILE__
271
+ Range2 = GeomCraft::Range2
272
+
273
+ require "rspec/autorun"
274
+
275
+ RSpec.configure do |config|
276
+ config.expect_with :test_unit
277
+ end
278
+
279
+ describe Range2 do
280
+ it "new" do
281
+ assert { Range2.new(1, 2) == Range2.new(1, 2) }
282
+ end
283
+
284
+ it "pos を中心に半径 len / 2 の幅とする (重要)" do
285
+ assert { Range2::from_pos_and_len(100.0, 10.0) == Range2.new(95.0, 105.0) }
286
+ end
287
+
288
+ it "それぞれの値" do
289
+ assert { Range2.new(100.0, 200.0).start == 100.0 }
290
+ assert { Range2.new(100.0, 200.0).middle == 150.0 }
291
+ assert { Range2.new(100.0, 200.0).end == 200.0 }
292
+ end
293
+
294
+ describe "ベクトルの強さと長さ" do
295
+ it "強さ (end - start)" do
296
+ assert { Range2.new(100, -200).magnitude == -300 }
297
+ end
298
+
299
+ it "長さ (強さの絶対値)" do
300
+ assert { Range2.new(100, -200).length == 300 }
301
+ end
302
+ end
303
+
304
+ it "小数の補正" do
305
+ assert { Range2.new(0.4, 0.5).round == Range2.new(0, 1) }
306
+ assert { Range2.new(0.4, 0.5).floor == Range2.new(0, 0) }
307
+ assert { Range2.new(0.4, 0.5).ceil == Range2.new(1, 1) }
308
+ assert { Range2.new(0.4, 0.5).truncate == Range2.new(0, 0) }
309
+ end
310
+
311
+ describe "範囲" do
312
+ it "OR (向きを破壊する)" do
313
+ a = Range2.new(5.0, 3.0)
314
+ b = Range2.new(4.0, 6.0)
315
+ assert { a.max(b) == Range2.new(3.0, 6.0) }
316
+ end
317
+
318
+ it "OR (向きを維持する)" do
319
+ a = Range2.new(5.0, 3.0)
320
+ b = Range2.new(4.0, 6.0)
321
+ assert { a.max_directed(b) == Range2.new(6.0, 3.0) }
322
+ end
323
+
324
+ it "AND (向きを破壊する)" do
325
+ a = Range2.new(5.0, 3.0)
326
+ b = Range2.new(4.0, 6.0)
327
+ assert { a.overlap(b) == Range2.new(4.0, 5.0) }
328
+ end
329
+ end
330
+
331
+ describe "向き" do
332
+ it "現在の向きを返す" do
333
+ assert { Range2.new(0, 10).direction == 1.0 }
334
+ assert { Range2.new(10, 0).direction == -1.0 }
335
+ assert { Range2.new(10, 10).direction == -0.0 }
336
+ end
337
+
338
+ it "向きが同じか?" do
339
+ a = Range2.new(1, 2)
340
+ b = Range2.new(3, 4)
341
+ assert { a.same_direction?(b) == true }
342
+ end
343
+
344
+ it "向きを反転する" do
345
+ assert { Range2.new(0, 100).invert == Range2.new(100, 0) }
346
+ end
347
+
348
+ it "正の向きにする" do
349
+ assert { Range2.new(10, 0).absolute == Range2.new(0, 10) }
350
+ end
351
+ end
352
+
353
+ describe "スケーリング" do
354
+ it "map_value" do
355
+ a = Range2.new(0.0, 1.0)
356
+ b = Range2.new(0.0, 100.0)
357
+ assert { a.map_value(0.9, b) == 90.0 }
358
+ end
359
+
360
+ it "元の範囲が 0..1 の場合 lerp 使うと簡潔に書ける" do
361
+ b = Range2.new(0.0, 100.0)
362
+ assert { b.lerp(0.9) == 90.0 }
363
+ end
364
+ end
365
+
366
+ describe "指定の軸で整列する" do
367
+ it "相手の左端に揃える" do
368
+ a = Range2.new(0, 100)
369
+ b = Range2.new(50, 100)
370
+ assert { a.align_start_of(b) == Range2.new(50, 150) }
371
+ end
372
+
373
+ it "相手の右端に揃える" do
374
+ a = Range2.new(0, 50)
375
+ b = Range2.new(0, 100)
376
+ assert { a.align_end_of(b) == Range2.new(50, 100) }
377
+ end
378
+
379
+ it "相手の中央に揃える" do
380
+ a = Range2.new(0.0, 50.0)
381
+ b = Range2.new(0.0, 100.0)
382
+ assert { a.align_middle_of(b) == Range2.new(25.0, 75.0) }
383
+ end
384
+
385
+ it "相手のどこかに揃える" do
386
+ a = Range2.new(0.0, 5.0)
387
+ b = Range2.new(10.0, 20.0)
388
+ assert { a.align_to(:start, b) == Range2.new(10.0, 15.0) }
389
+ assert { a.align_to(:end, b) == Range2.new(15.0, 20.0) }
390
+ assert { a.align_to(:middle, b) == Range2.new(12.5, 17.5) }
391
+ end
392
+ end
393
+
394
+ describe "横に並べる" do
395
+ it "相手の左隣り並べる" do
396
+ a = Range2.new(0.0, 5.0)
397
+ b = Range2.new(0.0, 10.0)
398
+ assert { a.align_after(b) == Range2.new(10.0, 15.0) }
399
+ end
400
+
401
+ it "相手の右隣り並べる" do
402
+ a = Range2.new(0.0, 5.0)
403
+ b = Range2.new(0.0, 0.0)
404
+ assert { a.align_before(b) == Range2.new(-5.0, 0.0) }
405
+ end
406
+ end
407
+
408
+ describe "Edge を寄せる (サイズが変わる)" do
409
+ it "左端を内側に寄せる" do
410
+ assert { Range2.new(10, 0).pad_start(3) == Range2.new(7, 0) }
411
+ end
412
+
413
+ it "右端を内側に寄せる" do
414
+ assert { Range2.new(10, 0).pad_end(3) == Range2.new(10, 3) }
415
+ end
416
+
417
+ it "両端を内側に寄せる" do
418
+ assert { Range2.new(10, 0).pad(3) == Range2.new(7, 3) }
419
+ end
420
+
421
+ it "両端を内側に寄せる (個別指定)" do
422
+ assert { Range2.new(10, 0).pad_ends(3, 4) == Range2.new(7, 4) }
423
+ end
424
+ end
425
+
426
+ it "この範囲に含むか?" do
427
+ assert { Range2.new(1, 2).contains?(2) == true }
428
+ end
429
+
430
+ it "対象を補正する" do
431
+ assert { Range2.new(10, 0).clamp_value(-1) == 0 }
432
+ assert { Range2.new(10, 0).clamp_value(11) == 10 }
433
+ end
434
+
435
+ it "ずらす (サイズ不変)" do
436
+ assert { Range2.new(2, 3).shift(10) == Range2.new(12, 13) }
437
+ end
438
+
439
+ it "近い方の端を引き伸ばす" do
440
+ assert { Range2.new(10, 20).stretch_to_value(5) == Range2.new(5, 20) }
441
+ assert { Range2.new(10, 20).stretch_to_value(25) == Range2.new(10, 25) }
442
+ end
443
+
444
+ it "範囲内を指定した場合は何も変化しない" do
445
+ assert { Range2.new(10, 20).stretch_to_value(15) == Range2.new(10, 20) }
446
+ end
447
+
448
+ it "値に近い方の Edge を返す" do
449
+ assert { Range2.new(0.0, 10.0).closest_edge(4.0) == :start }
450
+ assert { Range2.new(0.0, 10.0).closest_edge(6.0) == :end }
451
+ end
452
+
453
+ it "==" do
454
+ assert { Range2.new(1, 2) == Range2.new(1, 2) }
455
+ end
456
+ end
457
+ end
458
+ # >> ................................
459
+ # >>
460
+ # >> Finished in 0.02209 seconds (files took 0.08981 seconds to load)
461
+ # >> 32 examples, 0 failures
462
+ # >>