gosling 2.1.0 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6522b7771bdf2e2b85a0bf3cbf7f440497d21a91
4
- data.tar.gz: e0d6aa8c63c4361aeb7dffb1f25c6310bb050730
3
+ metadata.gz: f40ad4e1b06a4aeb3653172b748bb6b9cff0aaec
4
+ data.tar.gz: '0249b7dbe396d152de3791ad3e5548ffad9c8ece'
5
5
  SHA512:
6
- metadata.gz: a1646556b4e6765ceda7ede9e7c7447cd71eda9af4eb510bd7413e9ba8e9116c4c4177e9b8096c3f2fc81584c0bcb310ab6caf00691d951f1b549b5ffb98eeb8
7
- data.tar.gz: 719da8786b573ae344338586f2c53ebc0bc41987d392fc0383b218fc4408e94f5f411ffc3a506bf78a9958eae033ae34db14385db3c4702f380e6e312250d753
6
+ metadata.gz: 05a6efbacdee880176e86660408c0d005d0e32e86e1eca91a213e1552dfdbccfe40edf43d167669e0684690d2280299ce49448d2f3719b7604bb037c683e6a49
7
+ data.tar.gz: b5670a5dace288c80bb8ba339cbba83b171d1e0ec8d104b2ba327792e550dc4957783ec0ee710940bc45c115af4f40d958bf77563b7711b6a248fd31ed5a933b
data/lib/gosling/actor.rb CHANGED
@@ -139,12 +139,19 @@ module Gosling
139
139
  # be skipped and not drawn.
140
140
  #
141
141
  def draw(matrix = nil)
142
- matrix ||= Snow::Mat3.new
143
- transform = to_matrix * matrix
142
+ transform = MatrixCache.instance.get
143
+ if matrix
144
+ to_matrix.multiply(matrix, transform)
145
+ else
146
+ transform.set(to_matrix)
147
+ end
148
+
144
149
  render(transform) if @is_visible
145
150
  if @are_children_visible
146
151
  @children.each { |child| child.draw(transform) }
147
152
  end
153
+ ensure
154
+ MatrixCache.instance.recycle(transform)
148
155
  end
149
156
 
150
157
  ##
@@ -209,12 +216,11 @@ module Gosling
209
216
  # space of its root ancestor).
210
217
  #
211
218
  def get_global_transform(out = nil)
219
+ out ||= Snow::Mat3.new
212
220
  if parent
213
- out ||= Snow::Mat3.new
214
221
  to_matrix.multiply(parent.get_global_transform, out)
215
- out
216
222
  else
217
- to_matrix
223
+ out.set(to_matrix)
218
224
  end
219
225
  end
220
226
 
@@ -222,9 +228,13 @@ module Gosling
222
228
  # Returns the global x/y position of this actor (where it is relative to its root ancestor). This value is calculated
223
229
  # using the Actor's center (see Transformable#center).
224
230
  #
225
- def get_global_position
226
- tf = get_global_transform
227
- Transformable.transform_point(tf, center)
231
+ def get_global_position(out = nil)
232
+ tf = MatrixCache.instance.get
233
+ get_global_transform(tf)
234
+ out ||= Snow::Vec3.new
235
+ Transformable.transform_point(tf, center, out)
236
+ ensure
237
+ MatrixCache.instance.recycle(tf)
228
238
  end
229
239
 
230
240
  ##
@@ -288,6 +298,19 @@ module Gosling
288
298
  def render(matrix)
289
299
  end
290
300
 
301
+ def fill_polygon(vertices)
302
+ (2...vertices.length).each do |i|
303
+ v0 = vertices[0]
304
+ v1 = vertices[i - 1]
305
+ v2 = vertices[i]
306
+ @window.draw_triangle(
307
+ v0[0].to_f, v0[1].to_f, @color,
308
+ v1[0].to_f, v1[1].to_f, @color,
309
+ v2[0].to_f, v2[1].to_f, @color,
310
+ )
311
+ end
312
+ end
313
+
291
314
  ##
292
315
  # Internal use only. See #add_child and #remove_child.
293
316
  #
@@ -34,9 +34,10 @@ module Gosling
34
34
  ##
35
35
  # Returns the angle's corresponding unit vector times this circle's radius.
36
36
  #
37
- def get_point_at_angle(radians)
37
+ def get_point_at_angle(radians, out = nil)
38
38
  raise ArgumentError.new("Expected Numeric, but received #{radians.inspect}!") unless radians.is_a?(Numeric)
39
- Snow::Vec3[Math.cos(radians) * @radius, Math.sin(radians) * @radius, 0]
39
+ out ||= Snow::Vec3.new
40
+ out.set(Math.cos(radians) * @radius, Math.sin(radians) * @radius, 0)
40
41
  end
41
42
 
42
43
  ##
@@ -48,23 +49,17 @@ module Gosling
48
49
 
49
50
  private
50
51
 
52
+ # TODO: keep a cached, class-level list of local vertices that can be re-used during rendering
53
+
51
54
  def render(matrix)
55
+ # TODO: store these vertices in a cached, class-level array (see above)
52
56
  local_vertices = (0...RENDER_VERTEX_COUNT).map do |i|
53
57
  get_point_at_angle(Math::PI * 2 * i / RENDER_VERTEX_COUNT)
54
58
  end
59
+ # TODO: retain an array of vertices in memory; write transformed vertices to this array
55
60
  global_vertices = local_vertices.map { |v| Transformable.transform_point(matrix, v) }
56
- i = 2
57
- while i < global_vertices.length
58
- v0 = global_vertices[0]
59
- v1 = global_vertices[i-1]
60
- v2 = global_vertices[i]
61
- @window.draw_triangle(
62
- v0[0].to_f, v0[1].to_f, @color,
63
- v1[0].to_f, v1[1].to_f, @color,
64
- v2[0].to_f, v2[1].to_f, @color,
65
- )
66
- i += 1
67
- end
61
+
62
+ fill_polygon(global_vertices)
68
63
  end
69
64
  end
70
65
  end
@@ -33,9 +33,11 @@ module Gosling
33
33
 
34
34
  return false if shapeA === shapeB
35
35
 
36
- separation_axes = get_separation_axes(shapeA, shapeB)
36
+ get_separation_axes(shapeA, shapeB)
37
37
 
38
+ reset_projection_axis_tracking
38
39
  separation_axes.each do |axis|
40
+ next if axis_already_projected?(axis)
39
41
  projectionA = project_onto_axis(shapeA, axis)
40
42
  projectionB = project_onto_axis(shapeB, axis)
41
43
  return false unless projections_overlap?(projectionA, projectionB)
@@ -58,18 +60,26 @@ module Gosling
58
60
  # - penetration: if colliding, a vector representing how far shape B must move to be separated from (or merely
59
61
  # touching) shape A; nil otherwise
60
62
  #
61
- def self.get_collision_info(shapeA, shapeB)
62
- info = { colliding: false, overlap: nil, penetration: nil }
63
+ def self.get_collision_info(shapeA, shapeB, info = nil)
64
+ if info
65
+ info.clear
66
+ else
67
+ info = {}
68
+ end
69
+ info.merge!(actors: [shapeA, shapeB], colliding: false, overlap: nil, penetration: nil)
63
70
 
64
71
  return info if shapeA.instance_of?(Actor) || shapeB.instance_of?(Actor)
65
72
 
66
73
  return info if shapeA === shapeB
67
74
 
68
- separation_axes = get_separation_axes(shapeA, shapeB)
75
+ get_separation_axes(shapeA, shapeB)
76
+ return info if separation_axes.empty?
69
77
 
70
78
  smallest_overlap = nil
71
79
  smallest_axis = nil
80
+ reset_projection_axis_tracking
72
81
  separation_axes.each do |axis|
82
+ next if axis_already_projected?(axis)
73
83
  projectionA = project_onto_axis(shapeA, axis)
74
84
  projectionB = project_onto_axis(shapeB, axis)
75
85
  overlap = get_overlap(projectionA, projectionB)
@@ -77,7 +87,8 @@ module Gosling
77
87
  if smallest_overlap.nil? || smallest_overlap > overlap
78
88
  smallest_overlap = overlap
79
89
  flip = (projectionA[0] + projectionA[1]) * 0.5 > (projectionB[0] + projectionB[1]) * 0.5
80
- smallest_axis = flip ? -axis : axis
90
+ smallest_axis = axis
91
+ smallest_axis.negate! if flip
81
92
  end
82
93
  end
83
94
 
@@ -85,7 +96,7 @@ module Gosling
85
96
  info[:overlap] = smallest_overlap
86
97
  info[:penetration] = smallest_axis.normalize * smallest_overlap
87
98
 
88
- return info
99
+ info
89
100
  end
90
101
 
91
102
  ##
@@ -104,47 +115,240 @@ module Gosling
104
115
 
105
116
  return false if shape.instance_of?(Actor)
106
117
 
107
- separation_axes = []
118
+ global_pos = nil
119
+ centers_axis = nil
120
+ global_vertices = nil
108
121
  if shape.instance_of?(Circle)
109
- centers_axis = point - shape.get_global_position
110
- separation_axes.push(centers_axis) if centers_axis && centers_axis.magnitude > 0
122
+ unless @@global_position_cache.key?(shape)
123
+ global_pos = VectorCache.instance.get
124
+ shape.get_global_position(global_pos)
125
+ end
126
+ centers_axis = VectorCache.instance.get
127
+ point.subtract(@@global_position_cache.fetch(shape, global_pos), centers_axis)
128
+ next_separation_axis.set(centers_axis) if centers_axis && (centers_axis[0] != 0 || centers_axis[1] != 0)
111
129
  else
112
- separation_axes.concat(get_polygon_separation_axes(shape.get_global_vertices))
130
+ unless @@global_vertices_cache.key?(shape)
131
+ global_vertices = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
132
+ shape.get_global_vertices(global_vertices)
133
+ end
134
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shape, global_vertices))
113
135
  end
114
136
 
137
+ reset_projection_axis_tracking
115
138
  separation_axes.each do |axis|
139
+ next if axis_already_projected?(axis)
116
140
  shape_projection = project_onto_axis(shape, axis)
117
141
  point_projection = point.dot_product(axis)
118
- return false unless shape_projection.min <= point_projection && point_projection <= shape_projection.max
142
+ return false unless shape_projection.first <= point_projection && point_projection <= shape_projection.last
119
143
  end
120
144
 
121
145
  return true
146
+ ensure
147
+ VectorCache.instance.recycle(global_pos) if global_pos
148
+ VectorCache.instance.recycle(centers_axis) if centers_axis
149
+ global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
150
+ end
151
+
152
+ @@collision_buffer = []
153
+ @@global_position_cache = {}
154
+ @@global_vertices_cache = {}
155
+ @@global_transform_cache = {}
156
+ @@buffer_iterator_a = nil
157
+ @@buffer_iterator_b = nil
158
+
159
+ ##
160
+ # Adds one or more descendents of Actor to the collision testing buffer. The buffer's iterators will be reset to the
161
+ # first potential collision in the buffer.
162
+ #
163
+ # When added to the buffer, important and expensive global-space collision values for each Actor - transform,
164
+ # position, and any vertices - are calculated and cached for re-use. This ensures that expensive transform
165
+ # calculations are only performed once per actor during each collision resolution step.
166
+ #
167
+ # If you modify a buffered actor's transforms in any way, you will need to update its cached values by calling
168
+ # buffer_shapes again. Otherwise, it will continue to use stale and inaccurate transform information.
169
+ #
170
+ def self.buffer_shapes(actors)
171
+ type_check(actors, Array)
172
+ actors.each { |a| type_check(a, Actor) }
173
+
174
+ reset_buffer_iterators
175
+
176
+ shapes = actors.reject { |a| a.instance_of?(Actor) }
177
+
178
+ @@collision_buffer = @@collision_buffer | shapes
179
+ shapes.each do |shape|
180
+ unless @@global_transform_cache.key?(shape)
181
+ @@global_transform_cache[shape] = MatrixCache.instance.get
182
+ end
183
+ shape.get_global_transform(@@global_transform_cache[shape])
184
+
185
+ unless @@global_position_cache.key?(shape)
186
+ @@global_position_cache[shape] = VectorCache.instance.get
187
+ end
188
+ # TODO: can we calculate this position using the global transform we already have?
189
+ @@global_position_cache[shape].set(shape.get_global_position)
190
+
191
+ if shape.is_a?(Polygon)
192
+ unless @@global_vertices_cache.key?(shape)
193
+ @@global_vertices_cache[shape] = Array.new(shape.get_vertices.length) { VectorCache.instance.get }
194
+ end
195
+ # TODO: can we calculate these vertices using the global transform we already have?
196
+ shape.get_global_vertices(@@global_vertices_cache[shape])
197
+ end
198
+ end
199
+ end
200
+
201
+ ##
202
+ # Removes one or more descendents of Actor from the collision testing buffer. Any cached values for the actors
203
+ # are discarded. The buffer's iterators will be reset to the first potential collision in the buffer.
204
+ #
205
+ def self.unbuffer_shapes(actors)
206
+ type_check(actors, Array)
207
+ actors.each { |a| type_check(a, Actor) }
208
+
209
+ reset_buffer_iterators
210
+
211
+ @@collision_buffer = @@collision_buffer - actors
212
+ actors.each do |actor|
213
+ if @@global_transform_cache.key?(actor)
214
+ MatrixCache.instance.recycle(@@global_transform_cache[actor])
215
+ @@global_transform_cache.delete(actor)
216
+ end
217
+
218
+ if @@global_position_cache.key?(actor)
219
+ VectorCache.instance.recycle(@@global_position_cache[actor])
220
+ @@global_position_cache.delete(actor)
221
+ end
222
+
223
+ if @@global_vertices_cache.key?(actor)
224
+ @@global_vertices_cache[actor].each do |vertex|
225
+ VectorCache.instance.recycle(vertex)
226
+ end
227
+ @@global_vertices_cache.delete(actor)
228
+ end
229
+ end
230
+ end
231
+
232
+ ##
233
+ # Removes all actors from the collision testing buffer. See Collision.unbuffer_shapes.
234
+ #
235
+ def self.clear_buffer
236
+ unbuffer_shapes(@@collision_buffer)
237
+ end
238
+
239
+ ##
240
+ # Returns collision information for the next pair of actors in the collision buffer, or returns nil if all pairs in the
241
+ # buffer have been tested. Advances the buffer's iterators to the next pair. See Collision.get_collision_info.
242
+ #
243
+ def self.next_collision_info
244
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
245
+ return if iteration_complete?
246
+
247
+ info = get_collision_info(@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b])
248
+ skip_next_collision
249
+ info
250
+ end
251
+
252
+ ##
253
+ # Returns the pair of actors in the collision buffer that would be tested during the next call to
254
+ # Collision.next_collision_info, or returns nil if all pairs in the buffer have been tested. Does not perform
255
+ # collision testing or advance the buffer's iterators.
256
+ #
257
+ # One use of this method is to look at the two actors about to be tested and, using some custom and likely more
258
+ # efficient logic, determine if it's worth bothering to collision test these actors at all. If not, the pair's collision test
259
+ # can be skipped by calling Collision.skip_next_collision.
260
+ #
261
+ def self.peek_at_next_collision
262
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
263
+ return if iteration_complete?
264
+
265
+ [@@collision_buffer[@@buffer_iterator_a], @@collision_buffer[@@buffer_iterator_b]]
266
+ end
267
+
268
+ ##
269
+ # Advances the collision buffer's iterators to the next pair of actors in the buffer without performing any collision
270
+ # testing. By using this method in conjunction with Collision.peek_at_next_collision, it is possible to selectively
271
+ # skip collision testing for pairs of actors that meet certain criteria.
272
+ #
273
+ def self.skip_next_collision
274
+ reset_buffer_iterators if @@buffer_iterator_a.nil? || @@buffer_iterator_b.nil?
275
+ return if iteration_complete?
276
+
277
+ @@buffer_iterator_b += 1
278
+ if @@buffer_iterator_b >= @@buffer_iterator_a
279
+ @@buffer_iterator_b = 0
280
+ @@buffer_iterator_a += 1
281
+ end
122
282
  end
123
283
 
124
284
  private
125
285
 
126
- def self.get_normal(vector)
127
- type_check(vector, Snow::Vec3)
128
- raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector.magnitude_squared == 0
129
- Snow::Vec3[-vector[1], vector[0], 0]
286
+ def self.iteration_complete?
287
+ @@buffer_iterator_a >= @@collision_buffer.length
130
288
  end
131
289
 
132
- def self.get_polygon_separation_axes(vertices)
133
- type_check(vertices, Array)
134
- vertices.each { |v| type_check(v, Snow::Vec3) }
290
+ def self.reset_buffer_iterators
291
+ @@buffer_iterator_a = 1
292
+ @@buffer_iterator_b = 0
293
+ end
294
+
295
+ def self.get_normal(vector, out = nil)
296
+ raise ArgumentError.new("Cannot determine normal of zero-length vector") if vector[0] == 0 && vector[1] == 0
297
+ out ||= Snow::Vec3.new
298
+ out.set(-vector[1], vector[0], 0)
299
+ end
300
+
301
+ @@separation_axes = []
302
+ @@separation_axis_count = 0
303
+
304
+ def self.reset_separation_axes
305
+ @@separation_axis_count = 0
306
+ end
307
+
308
+ def self.next_separation_axis
309
+ axis = @@separation_axes[@@separation_axis_count] ||= Snow::Vec3.new
310
+ @@separation_axis_count += 1
311
+ axis
312
+ end
313
+
314
+ def self.separation_axes
315
+ @@separation_axes[0...@@separation_axis_count]
316
+ end
135
317
 
136
- axes = (0...vertices.length).map do |i|
137
- axis = vertices[(i + 1) % vertices.length] - vertices[i]
138
- (axis.magnitude > 0) ? get_normal(axis).normalize : nil
318
+ @@gpsa_axis = Snow::Vec3.new
319
+ def self.get_polygon_separation_axes(vertices)
320
+ # TODO: special case for Rects - only return two axes to avoid duplicitous math
321
+ vertices.each_index do |i|
322
+ vertices[i].subtract(vertices[i - 1], @@gpsa_axis)
323
+ if @@gpsa_axis[0] != 0 || @@gpsa_axis[1] != 0
324
+ get_normal(@@gpsa_axis, @@gpsa_axis).normalize(next_separation_axis)
325
+ end
139
326
  end
140
- axes.compact
327
+ nil
141
328
  end
142
329
 
330
+ @@global_pos_a = nil
331
+ @@global_pos_b = nil
332
+ @@gcsa_axis = nil
143
333
  def self.get_circle_separation_axis(circleA, circleB)
144
- type_check(circleA, Actor)
145
- type_check(circleB, Actor)
146
- axis = circleB.get_global_position - circleA.get_global_position
147
- (axis.magnitude > 0) ? axis.normalize : nil
334
+ unless @@global_position_cache.key?(circleA)
335
+ @@global_pos_a ||= Snow::Vec3.new
336
+ circleA.get_global_position(@@global_pos_a)
337
+ end
338
+
339
+ unless @@global_position_cache.key?(circleB)
340
+ @@global_pos_b ||= Snow::Vec3.new
341
+ circleB.get_global_position(@@global_pos_b)
342
+ end
343
+
344
+ @@gcsa_axis ||= Snow::Vec3.new
345
+ @@global_pos_a = @@global_position_cache.fetch(circleA, @@global_pos_a)
346
+ @@global_pos_b = @@global_position_cache.fetch(circleB, @@global_pos_b)
347
+ @@global_pos_b.subtract(@@global_pos_a, @@gcsa_axis)
348
+ if @@gcsa_axis[0] != 0 || @@gcsa_axis[1] != 0
349
+ @@gcsa_axis.normalize(next_separation_axis)
350
+ end
351
+ nil
148
352
  end
149
353
 
150
354
  def self.get_separation_axes(shapeA, shapeB)
@@ -156,40 +360,123 @@ module Gosling
156
360
  raise ArgumentError.new("Expected a child of the Actor class, but received #{shapeB.inspect}!")
157
361
  end
158
362
 
159
- separation_axes = []
363
+ reset_separation_axes
364
+ global_vertices = nil
160
365
 
161
366
  unless shapeA.instance_of?(Circle)
162
- separation_axes.concat(get_polygon_separation_axes(shapeA.get_global_vertices))
367
+ unless @@global_vertices_cache.key?(shapeA)
368
+ global_vertices = Array.new(shapeA.get_vertices.length) { VectorCache.instance.get }
369
+ shapeA.get_global_vertices(global_vertices)
370
+ end
371
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeA, global_vertices))
163
372
  end
164
373
 
165
374
  unless shapeB.instance_of?(Circle)
166
- separation_axes.concat(get_polygon_separation_axes(shapeB.get_global_vertices))
375
+ unless @@global_vertices_cache.key?(shapeB)
376
+ global_vertices ||= []
377
+ (shapeB.get_vertices.length - global_vertices.length).times do
378
+ global_vertices.push(VectorCache.instance.get)
379
+ end
380
+ (global_vertices.length - shapeB.get_vertices.length).times do
381
+ VectorCache.instance.recycle(global_vertices.pop)
382
+ end
383
+ shapeB.get_global_vertices(global_vertices)
384
+ end
385
+ get_polygon_separation_axes(@@global_vertices_cache.fetch(shapeB, global_vertices))
167
386
  end
168
387
 
169
388
  if shapeA.instance_of?(Circle) || shapeB.instance_of?(Circle)
170
- axis = get_circle_separation_axis(shapeA, shapeB)
171
- separation_axes.push(axis) if axis
389
+ get_circle_separation_axis(shapeA, shapeB)
172
390
  end
173
391
 
174
- separation_axes.map! { |v| v[0] < 0 ? v * -1 : v }
175
- separation_axes.uniq
392
+ nil
393
+ ensure
394
+ global_vertices.each { |v| VectorCache.instance.recycle(v) } if global_vertices
176
395
  end
177
396
 
178
- def self.project_onto_axis(shape, axis)
179
- type_check(shape, Actor)
180
- type_check(axis, Snow::Vec3)
397
+ @@poa_zero_z_axis = nil
398
+ @@poa_local_axis = nil
399
+ @@poa_intersection = nil
400
+ @@poa_global_tf = nil
401
+ @@poa_global_tf_inverse = nil
402
+ def self.get_circle_vertices_by_axis(shape, axis)
403
+ unless @@global_transform_cache.key?(shape)
404
+ @@poa_global_tf ||= Snow::Mat3.new
405
+ shape.get_global_transform(@@poa_global_tf)
406
+ end
181
407
 
182
- global_vertices = if shape.instance_of?(Circle)
183
- global_tf = shape.get_global_transform
184
- local_axis = global_tf.inverse * Snow::Vec3[axis[0], axis[1], 0]
185
- v = shape.get_point_at_angle(Math.atan2(local_axis[1], local_axis[0]))
186
- [v, v * -1].map { |vertex| Transformable.transform_point(global_tf, vertex) }
187
- else
188
- shape.get_global_vertices
408
+ @@poa_zero_z_axis ||= Snow::Vec3.new
409
+ @@poa_zero_z_axis.set(axis[0], axis[1], 0)
410
+
411
+ @@poa_global_tf_inverse ||= Snow::Mat3.new
412
+ @@global_transform_cache.fetch(shape, @@poa_global_tf).inverse(@@poa_global_tf_inverse)
413
+
414
+ @@poa_local_axis ||= Snow::Vec3.new
415
+ @@poa_global_tf_inverse.multiply(@@poa_zero_z_axis, @@poa_local_axis)
416
+
417
+ @@poa_intersection ||= Snow::Vec3.new
418
+ shape.get_point_at_angle(Math.atan2(@@poa_local_axis[1], @@poa_local_axis[0]), @@poa_intersection)
419
+
420
+ Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
421
+
422
+ @@poa_intersection.negate!
423
+ Transformable.transform_point(@@global_transform_cache.fetch(shape, @@poa_global_tf), @@poa_intersection, next_global_vertex)
424
+ end
425
+
426
+ @@global_vertices = nil
427
+ @@global_vertices_count = 0
428
+
429
+ def self.reset_global_vertices
430
+ @@global_vertices ||= []
431
+ @@global_vertices_count = 0
432
+ end
433
+
434
+ def self.next_global_vertex
435
+ vertex = @@global_vertices[@@global_vertices_count] ||= Snow::Vec3.new
436
+ @@global_vertices_count += 1
437
+ vertex
438
+ end
439
+
440
+ @@projected_axes = nil
441
+
442
+ def self.reset_projection_axis_tracking
443
+ @@projected_axes ||= {}
444
+ @@projected_axes.clear
445
+ end
446
+
447
+ def self.axis_already_projected?(axis)
448
+ key = axis.to_s
449
+ return true if @@projected_axes.key?(key)
450
+ @@projected_axes[key] = nil
451
+ end
452
+
453
+ def self.project_onto_axis(shape, axis, out = nil)
454
+ unless @@global_vertices_cache.key?(shape)
455
+ reset_global_vertices
456
+ if shape.instance_of?(Circle)
457
+ get_circle_vertices_by_axis(shape, axis)
458
+ else
459
+ shape.get_global_vertices(@@global_vertices)
460
+ @@global_vertices_count = shape.get_vertices.length
461
+ end
189
462
  end
190
463
 
191
- projections = global_vertices.map { |vertex| vertex.dot_product(axis) }.sort
192
- [projections.first, projections.last]
464
+ min = nil
465
+ max = nil
466
+ @@global_vertices_cache.fetch(shape, @@global_vertices[0...@@global_vertices_count]).each do |vertex|
467
+ projection = vertex.dot_product(axis)
468
+ if min.nil?
469
+ min = projection
470
+ max = projection
471
+ else
472
+ min = projection if projection < min
473
+ max = projection if projection > max
474
+ end
475
+ end
476
+ out ||= []
477
+ out[1] = max
478
+ out[0] = min
479
+ out
193
480
  end
194
481
 
195
482
  def self.projections_overlap?(a, b)
@@ -198,12 +485,8 @@ module Gosling
198
485
  end
199
486
 
200
487
  def self.get_overlap(a, b)
201
- type_check(a, Array)
202
- type_check(b, Array)
203
- a.each { |x| type_check(x, Numeric) }
204
- b.each { |x| type_check(x, Numeric) }
205
- raise ArgumentError.new("Expected two arrays of length 2, but received #{a.inspect} and #{b.inspect}!") unless a.length == 2 && b.length == 2
206
-
488
+ raise ArgumentError.new("Projection array must be length 2, not #{a.inspect}!") unless a.length == 2
489
+ raise ArgumentError.new("Projection array must be length 2, not #{b.inspect}!") unless b.length == 2
207
490
  a.sort! if a[0] > a[1]
208
491
  b.sort! if b[0] > b[1]
209
492
  return b[1] - b[0] if a[0] <= b[0] && b[1] <= a[1]