box2d-ruby 1.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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +336 -0
  4. data/examples/contact_events.rb +22 -0
  5. data/examples/debug_draw.rb +15 -0
  6. data/examples/falling_ball.rb +19 -0
  7. data/examples/rugl_debug_draw.rb +68 -0
  8. data/examples/stagecraft_debug_draw.rb +64 -0
  9. data/examples/support/box2d_debug_lines.rb +125 -0
  10. data/ext/box2d/CMakeLists.txt +35 -0
  11. data/ext/box2d/extconf.rb +42 -0
  12. data/ext/box2d/native.c +9 -0
  13. data/ext/box2d/vendor/box2d/LICENSE +21 -0
  14. data/ext/box2d/vendor/box2d/VERSION +1 -0
  15. data/ext/box2d/vendor/box2d/include/box2d/base.h +131 -0
  16. data/ext/box2d/vendor/box2d/include/box2d/box2d.h +1222 -0
  17. data/ext/box2d/vendor/box2d/include/box2d/collision.h +830 -0
  18. data/ext/box2d/vendor/box2d/include/box2d/id.h +144 -0
  19. data/ext/box2d/vendor/box2d/include/box2d/math_functions.h +761 -0
  20. data/ext/box2d/vendor/box2d/include/box2d/types.h +1457 -0
  21. data/ext/box2d/vendor/box2d/src/CMakeLists.txt +223 -0
  22. data/ext/box2d/vendor/box2d/src/aabb.c +132 -0
  23. data/ext/box2d/vendor/box2d/src/aabb.h +56 -0
  24. data/ext/box2d/vendor/box2d/src/arena_allocator.c +112 -0
  25. data/ext/box2d/vendor/box2d/src/arena_allocator.h +48 -0
  26. data/ext/box2d/vendor/box2d/src/array.c +8 -0
  27. data/ext/box2d/vendor/box2d/src/array.h +179 -0
  28. data/ext/box2d/vendor/box2d/src/atomic.h +79 -0
  29. data/ext/box2d/vendor/box2d/src/bitset.c +67 -0
  30. data/ext/box2d/vendor/box2d/src/bitset.h +65 -0
  31. data/ext/box2d/vendor/box2d/src/body.c +1884 -0
  32. data/ext/box2d/vendor/box2d/src/body.h +194 -0
  33. data/ext/box2d/vendor/box2d/src/box2d.natvis +41 -0
  34. data/ext/box2d/vendor/box2d/src/broad_phase.c +524 -0
  35. data/ext/box2d/vendor/box2d/src/broad_phase.h +83 -0
  36. data/ext/box2d/vendor/box2d/src/constants.h +54 -0
  37. data/ext/box2d/vendor/box2d/src/constraint_graph.c +322 -0
  38. data/ext/box2d/vendor/box2d/src/constraint_graph.h +58 -0
  39. data/ext/box2d/vendor/box2d/src/contact.c +650 -0
  40. data/ext/box2d/vendor/box2d/src/contact.h +148 -0
  41. data/ext/box2d/vendor/box2d/src/contact_solver.c +2120 -0
  42. data/ext/box2d/vendor/box2d/src/contact_solver.h +54 -0
  43. data/ext/box2d/vendor/box2d/src/core.c +178 -0
  44. data/ext/box2d/vendor/box2d/src/core.h +149 -0
  45. data/ext/box2d/vendor/box2d/src/ctz.h +112 -0
  46. data/ext/box2d/vendor/box2d/src/distance.c +1415 -0
  47. data/ext/box2d/vendor/box2d/src/distance_joint.c +556 -0
  48. data/ext/box2d/vendor/box2d/src/dynamic_tree.c +1989 -0
  49. data/ext/box2d/vendor/box2d/src/geometry.c +1028 -0
  50. data/ext/box2d/vendor/box2d/src/hull.c +328 -0
  51. data/ext/box2d/vendor/box2d/src/id_pool.c +79 -0
  52. data/ext/box2d/vendor/box2d/src/id_pool.h +35 -0
  53. data/ext/box2d/vendor/box2d/src/island.c +977 -0
  54. data/ext/box2d/vendor/box2d/src/island.h +89 -0
  55. data/ext/box2d/vendor/box2d/src/joint.c +1272 -0
  56. data/ext/box2d/vendor/box2d/src/joint.h +335 -0
  57. data/ext/box2d/vendor/box2d/src/manifold.c +1726 -0
  58. data/ext/box2d/vendor/box2d/src/math_functions.c +159 -0
  59. data/ext/box2d/vendor/box2d/src/motor_joint.c +283 -0
  60. data/ext/box2d/vendor/box2d/src/mouse_joint.c +214 -0
  61. data/ext/box2d/vendor/box2d/src/mover.c +73 -0
  62. data/ext/box2d/vendor/box2d/src/prismatic_joint.c +656 -0
  63. data/ext/box2d/vendor/box2d/src/revolute_joint.c +534 -0
  64. data/ext/box2d/vendor/box2d/src/sensor.c +389 -0
  65. data/ext/box2d/vendor/box2d/src/sensor.h +36 -0
  66. data/ext/box2d/vendor/box2d/src/shape.c +1714 -0
  67. data/ext/box2d/vendor/box2d/src/shape.h +123 -0
  68. data/ext/box2d/vendor/box2d/src/solver.c +2038 -0
  69. data/ext/box2d/vendor/box2d/src/solver.h +155 -0
  70. data/ext/box2d/vendor/box2d/src/solver_set.c +613 -0
  71. data/ext/box2d/vendor/box2d/src/solver_set.h +57 -0
  72. data/ext/box2d/vendor/box2d/src/table.c +238 -0
  73. data/ext/box2d/vendor/box2d/src/table.h +37 -0
  74. data/ext/box2d/vendor/box2d/src/timer.c +185 -0
  75. data/ext/box2d/vendor/box2d/src/types.c +151 -0
  76. data/ext/box2d/vendor/box2d/src/weld_joint.c +310 -0
  77. data/ext/box2d/vendor/box2d/src/wheel_joint.c +551 -0
  78. data/ext/box2d/vendor/box2d/src/world.c +3301 -0
  79. data/ext/box2d/vendor/box2d/src/world.h +192 -0
  80. data/generator/generate.rb +316 -0
  81. data/generator/layout_probe.c +507 -0
  82. data/generator/verify_layouts.rb +32 -0
  83. data/lib/box2d/body.rb +172 -0
  84. data/lib/box2d/body_definition.rb +38 -0
  85. data/lib/box2d/body_shapes.rb +135 -0
  86. data/lib/box2d/chain.rb +61 -0
  87. data/lib/box2d/debug_draw.rb +118 -0
  88. data/lib/box2d/events.rb +103 -0
  89. data/lib/box2d/fixed_stepper.rb +39 -0
  90. data/lib/box2d/handle.rb +46 -0
  91. data/lib/box2d/hit.rb +5 -0
  92. data/lib/box2d/joint.rb +197 -0
  93. data/lib/box2d/native.rb +1462 -0
  94. data/lib/box2d/native_loader.rb +45 -0
  95. data/lib/box2d/pixel_scale.rb +29 -0
  96. data/lib/box2d/shape.rb +109 -0
  97. data/lib/box2d/shape_definition.rb +44 -0
  98. data/lib/box2d/value_conversion.rb +80 -0
  99. data/lib/box2d/version.rb +7 -0
  100. data/lib/box2d/world.rb +135 -0
  101. data/lib/box2d/world_joints.rb +156 -0
  102. data/lib/box2d/world_queries.rb +122 -0
  103. data/lib/box2d/world_registry.rb +95 -0
  104. data/lib/box2d.rb +31 -0
  105. data/script/build_platform_gem.rb +24 -0
  106. data/script/deterministic_scene.rb +56 -0
  107. data/script/update_deterministic_snapshot.rb +11 -0
  108. data/script/verify_platform_gem.rb +24 -0
  109. metadata +164 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b27be6541a6fc5670786520d3679496edadc41237328ff6f3d5f7c561d19a4d8
4
+ data.tar.gz: 8f5162a1673fc3c797507b33ef51b48004e50bbd546c8e7b44377e6bf33ebbf5
5
+ SHA512:
6
+ metadata.gz: 1d29cb822f55e9ac3d222d3745c3e12ae323287f8098eb80752576d7cf090c0da8c4340d731491c66114d45bbfd8f0cff36223bf62b8dc2c84eb32e3c21f4439
7
+ data.tar.gz: d2972da3d6bd61af13f41619d3f6f9c2144718c53b1a04a1beac8233e76b8effe98fe49be0bf3514908a40d44b898d0361f5b561911655c36ae3a421d5c4f4f6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 ydah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,336 @@
1
+ # box2d-ruby
2
+
3
+ `box2d-ruby` is an idiomatic Ruby binding for the Box2D 3 C API. It ships a
4
+ complete generated FFI layer and a memory-safe Ruby API for worlds, bodies,
5
+ shapes, events, queries, joints, and debug drawing.
6
+
7
+ The gem currently vendors Box2D 3.1.0.
8
+
9
+ ## API stability
10
+
11
+ Version 1.0 freezes the documented Ruby API. Releases in the 1.x series follow
12
+ semantic versioning: existing public methods and their documented behavior will
13
+ remain compatible, and breaking changes require a new major version. The
14
+ generated `Box2D::Native` layer mirrors the vendored Box2D headers exactly and
15
+ is regenerated when that native dependency is intentionally upgraded.
16
+
17
+ ## Units and coordinates
18
+
19
+ Box2D uses SI units: meters, kilograms, seconds, and radians. It has no built-in
20
+ pixel scale and this binding uses a y-up coordinate system.
21
+
22
+ Do not pass screen pixels directly to physics APIs. A common game scale is
23
+ 32–64 pixels per meter:
24
+
25
+ ```ruby
26
+ scale = Box2D::PixelScale.new(64)
27
+ physics_position = scale.vector_to_meters([320, 128])
28
+ screen_position = scale.vector_to_pixels(physics_position)
29
+ ```
30
+
31
+ ## Installation
32
+
33
+ Add the gem to your bundle:
34
+
35
+ ```sh
36
+ bundle add box2d-ruby
37
+ ```
38
+
39
+ Or install it directly:
40
+
41
+ ```sh
42
+ gem install box2d-ruby
43
+ ```
44
+
45
+ Platform gems contain a precompiled Box2D library. The source gem falls back to
46
+ building the vendored C17 source with CMake and requires CMake 3.16 or newer, a
47
+ C compiler, `make` (or another CMake build backend), and Ruby development
48
+ headers.
49
+
50
+ The runtime dependency is `ffi`. `larb` is optional: if it is already loaded,
51
+ vector-returning APIs produce `Larb::Vec2`; otherwise they return two-element
52
+ arrays.
53
+
54
+ ## Quick start
55
+
56
+ ```ruby
57
+ require "box2d"
58
+
59
+ world = Box2D::World.new(gravity: [0, -9.8]) do |configuration|
60
+ configuration.substeps = 4
61
+ end
62
+
63
+ world.create_body(type: :static, position: [0, -10]) do |body|
64
+ body.box(50, 10)
65
+ end
66
+
67
+ ball = world.create_body(
68
+ type: :dynamic,
69
+ position: [0, 4],
70
+ user_data: {kind: :ball}
71
+ ) do |body|
72
+ body.circle(
73
+ radius: 0.5,
74
+ density: 1.0,
75
+ friction: 0.3,
76
+ restitution: 0.6
77
+ )
78
+ body.bullet = true
79
+ end
80
+
81
+ 60.times { world.step(1.0 / 60) }
82
+
83
+ p ball.position
84
+ ball.linear_velocity = [3, 0]
85
+ ball.apply_impulse([0, 5])
86
+
87
+ world.destroy
88
+ ```
89
+
90
+ `World#step` releases Ruby's GVL while Box2D is solving. Accessing the same
91
+ world from another Ruby thread during a step is rejected with
92
+ `Box2D::ReentrantStepError`.
93
+
94
+ ## Fixed timestep
95
+
96
+ `FixedStepper` implements an accumulator with a substep cap:
97
+
98
+ ```ruby
99
+ stepper = Box2D::FixedStepper.new(hz: 60, max_substeps: 5)
100
+
101
+ app.run do |delta_time|
102
+ alpha = stepper.advance(delta_time) { |step| world.step(step) }
103
+ render(alpha)
104
+ end
105
+ ```
106
+
107
+ `advance` returns interpolation alpha in `[0, 1)`. Excess accumulated time is
108
+ discarded at `max_substeps` to prevent a spiral of death.
109
+
110
+ ## Shapes
111
+
112
+ A body block is a shape builder:
113
+
114
+ ```ruby
115
+ body = world.create_body(type: :dynamic, position: [0, 3]) do |builder|
116
+ builder.box(half_width, half_height, center: [0, 0], angle: 0)
117
+ builder.circle(radius: 0.5, center: [0, 0])
118
+ builder.capsule([-1, 0], [1, 0], radius: 0.25)
119
+ builder.polygon([[0, 0], [1, 0], [0, 1]])
120
+ builder.segment([-1, 0], [1, 0])
121
+ end
122
+ ```
123
+
124
+ Every shape builder accepts:
125
+
126
+ - `density`, `friction`, `restitution`, and `rolling_resistance`
127
+ - `sensor: true`
128
+ - `filter: {category:, mask:, group:}`
129
+ - `sensor_events`, `contact_events`, and `hit_events`
130
+ - Ruby-owned `user_data`
131
+
132
+ Chains are separate native objects:
133
+
134
+ ```ruby
135
+ chain = ground.chain(
136
+ [[-4, 0], [-2, 1], [2, 1], [4, 0]],
137
+ loop: false,
138
+ friction: 0.8
139
+ )
140
+ ```
141
+
142
+ Box2D chains require at least four vertices.
143
+
144
+ ## Events
145
+
146
+ Contact and sensor event buffers are transient in Box2D. `World#step` copies
147
+ them into immutable Ruby values before returning:
148
+
149
+ ```ruby
150
+ world.step(1.0 / 60)
151
+
152
+ world.events.begin_contacts.each do |contact|
153
+ a = contact.shape_a.body.user_data
154
+ b = contact.shape_b.body.user_data
155
+ p [a, b, contact.manifold]
156
+ end
157
+
158
+ world.events.end_contacts.each { |contact| p contact }
159
+ world.events.hits.each { |hit| p hit.approach_speed }
160
+ world.events.sensor_begins.each { |event| p [event.sensor, event.visitor] }
161
+ world.events.sensor_ends.each { |event| p [event.sensor, event.visitor] }
162
+ ```
163
+
164
+ ## Queries
165
+
166
+ ```ruby
167
+ hit = world.raycast([0, 10], [0, -10])
168
+ if hit
169
+ p [hit.shape, hit.point, hit.normal, hit.fraction]
170
+ end
171
+
172
+ world.overlap_aabb([-2, -2], [2, 2]) do |shape|
173
+ p shape
174
+ end
175
+
176
+ world.overlap_circle([0, 1], radius: 0.5) { |shape| p shape }
177
+ world.overlap_capsule([-1, 0], [1, 0], radius: 0.25) { |shape| p shape }
178
+ world.overlap_polygon(
179
+ [[-1, -1], [1, -1], [1, 1], [-1, 1]],
180
+ position: [4, 0],
181
+ angle: Math::PI / 4
182
+ ) { |shape| p shape }
183
+ ```
184
+
185
+ All query methods accept `filter: {category:, mask:}`. Without a block, overlap
186
+ queries return an `Enumerator`. Returning `false` from the block stops the
187
+ query.
188
+
189
+ ## Joints
190
+
191
+ The high-level API supports revolute, prismatic, distance, mouse, and weld
192
+ joints:
193
+
194
+ ```ruby
195
+ joint = world.create_revolute_joint(
196
+ body_a: arm,
197
+ body_b: hand,
198
+ anchor: [1, 2],
199
+ limits: (-0.5..0.5),
200
+ motor: {speed: 2.0, max_torque: 10.0}
201
+ )
202
+
203
+ joint.motor_speed = -2.0
204
+ joint.destroy
205
+ ```
206
+
207
+ Equivalent constructors are:
208
+
209
+ - `create_prismatic_joint`
210
+ - `create_distance_joint`
211
+ - `create_mouse_joint`
212
+ - `create_weld_joint`
213
+
214
+ All anchors supplied to constructors are world coordinates and are converted to
215
+ body-local coordinates internally.
216
+
217
+ ## Debug drawing
218
+
219
+ Debug drawing converts native callbacks into arrays suitable for Ruby pattern
220
+ matching:
221
+
222
+ ```ruby
223
+ world.debug_draw(flags: [:shapes, :joints, :aabbs]) do |command|
224
+ case command
225
+ in [:polygon, points, color]
226
+ draw_outline(points, color)
227
+ in [:solid_circle, transform, radius, color]
228
+ draw_circle(transform[:position], radius, color)
229
+ in [:segment, point1, point2, color]
230
+ draw_line(point1, point2, color)
231
+ else
232
+ # Other commands include solid_polygon, solid_capsule, transform,
233
+ # point, circle, and string.
234
+ end
235
+ end
236
+ ```
237
+
238
+ The owning world retains every `FFI::Function` used by the draw session, so GC
239
+ cannot collect a callback while native code is using it.
240
+
241
+ Runnable line-renderer examples are included for both supported rendering
242
+ styles:
243
+
244
+ ```sh
245
+ gem install rugl glfw
246
+ ruby examples/rugl_debug_draw.rb
247
+
248
+ gem install stagecraft
249
+ ruby examples/stagecraft_debug_draw.rb
250
+ ```
251
+
252
+ Both adapters consume the same `Box2DDebugLines` converter in
253
+ `examples/support`, which turns debug callbacks into colored line-list
254
+ vertices.
255
+
256
+ ## Verification
257
+
258
+ `bundle exec rake` builds the CMake extension, checks all generated FFI struct
259
+ layouts, and runs the complete spec suite. Deterministic 100-body fixtures live
260
+ under `spec/snapshots/100_boxes` for every supported native-gem target. Update
261
+ the fixture for the current platform after an intentional solver change with:
262
+
263
+ ```sh
264
+ bundle exec rake snapshots:update
265
+ ```
266
+
267
+ `bundle exec rake native:verify_package` additionally builds a platform gem,
268
+ extracts its packaged library, and reruns the full suite against that exact
269
+ artifact. Release CI performs this check for all five supported targets before
270
+ uploading any gem.
271
+
272
+ ## Ownership and destruction
273
+
274
+ `World` owns every native body, shape, chain, and joint. Ruby wrappers are
275
+ non-owning ID values and do not use GC finalizers.
276
+
277
+ Destroy resources explicitly:
278
+
279
+ ```ruby
280
+ shape.destroy
281
+ body.destroy
282
+ joint.destroy
283
+ world.destroy # destroys all remaining children
284
+ ```
285
+
286
+ Access through a destroyed handle raises `Box2D::UseAfterDestroyError`. Ruby
287
+ objects assigned as `user_data` are held in world-side registries; Ruby object
288
+ pointers are never written to Box2D's native `void*` fields.
289
+
290
+ ## Native API
291
+
292
+ All 409 exported Box2D 3.1.0 functions are available under `Box2D::Native`.
293
+ Structs, enums, and function signatures are generated from the vendored headers:
294
+
295
+ ```ruby
296
+ version = Box2D::Native.b2GetVersion
297
+ p [version[:major], version[:minor], version[:revision]]
298
+ ```
299
+
300
+ Set `BOX2D_LIBRARY_PATH` to load a compatible external Box2D shared library
301
+ instead of the bundled extension.
302
+
303
+ ## Development
304
+
305
+ Install dependencies and run the complete quality gate:
306
+
307
+ ```sh
308
+ bundle install
309
+ bundle exec rake
310
+ ```
311
+
312
+ The default task:
313
+
314
+ 1. builds the vendored native library;
315
+ 2. checks that generated FFI declarations match the headers;
316
+ 3. compiles a C layout probe and checks all 71 struct sizes, alignments, and
317
+ field offsets;
318
+ 4. runs the RSpec suite, including 1000 steps under `GC.stress`.
319
+
320
+ Useful tasks:
321
+
322
+ ```sh
323
+ bundle exec rake bindings:generate
324
+ bundle exec rake bindings:check
325
+ bundle exec rake native:compile
326
+ bundle exec rake native:package
327
+ bundle exec rake build
328
+ ```
329
+
330
+ See `examples/` for complete runnable scripts.
331
+
332
+ ## License
333
+
334
+ The Ruby binding is available under the MIT License. Vendored Box2D source is
335
+ also MIT licensed; its license is included at
336
+ `ext/box2d/vendor/box2d/LICENSE`.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "box2d"
4
+
5
+ world = nil
6
+ begin
7
+ world = Box2D::World.new
8
+ world.create_body(position: [0, -1], user_data: :ground) { |body| body.box(10, 1) }
9
+ world.create_body(type: :dynamic, position: [0, 4], user_data: :ball) do |body|
10
+ body.circle(radius: 0.5)
11
+ end
12
+
13
+ 120.times do
14
+ world.step(1.0 / 60)
15
+ world.events.begin_contacts.each do |contact|
16
+ names = [contact.shape_a.body.user_data, contact.shape_b.body.user_data]
17
+ puts "contact began: #{names.join(" <-> ")}"
18
+ end
19
+ end
20
+ ensure
21
+ world&.destroy if world&.valid?
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "box2d"
4
+
5
+ world = nil
6
+ begin
7
+ world = Box2D::World.new
8
+ world.create_body { |body| body.box(2, 1) }
9
+
10
+ world.debug_draw(flags: [:shapes, :aabbs]) do |command|
11
+ p command
12
+ end
13
+ ensure
14
+ world&.destroy if world&.valid?
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "box2d"
4
+
5
+ world = nil
6
+ begin
7
+ world = Box2D::World.new
8
+ world.create_body(position: [0, -1]) { |body| body.box(10, 1) }
9
+ ball = world.create_body(type: :dynamic, position: [0, 4]) do |body|
10
+ body.circle(radius: 0.5, restitution: 0.6)
11
+ end
12
+
13
+ 120.times do |frame|
14
+ world.step(1.0 / 60)
15
+ puts format("%3d: x=% .3f y=% .3f", frame, *ball.position)
16
+ end
17
+ ensure
18
+ world&.destroy if world&.valid?
19
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "box2d"
4
+ require "rugl"
5
+ require_relative "support/box2d_debug_lines"
6
+
7
+ world = nil
8
+ rugl = nil
9
+ at_exit do
10
+ world&.destroy if world&.valid?
11
+ rugl&.destroy unless rugl&.destroyed?
12
+ end
13
+
14
+ world = Box2D::World.new
15
+ world.create_body(position: [0, -1]) { |body| body.box(8, 1) }
16
+ 10.times do |index|
17
+ world.create_body(type: :dynamic, position: [(index % 5) - 2, 1 + index / 5.0]) do |body|
18
+ body.box(0.4, 0.4)
19
+ end
20
+ end
21
+
22
+ rugl = Rugl.create(width: 960, height: 720, title: "Box2D debug draw")
23
+ positions = rugl.buffer(data: [], usage: :stream)
24
+ colors = rugl.buffer(data: [], usage: :stream)
25
+ draw_lines = rugl.command(
26
+ vert: <<~GLSL,
27
+ #version 410 core
28
+ layout(location = 0) in vec2 position;
29
+ layout(location = 1) in vec4 color;
30
+ out vec4 vertexColor;
31
+ void main() {
32
+ gl_Position = vec4(position * vec2(0.1, 0.12) + vec2(0.0, -0.65), 0.0, 1.0);
33
+ vertexColor = color;
34
+ }
35
+ GLSL
36
+ frag: <<~GLSL,
37
+ #version 410 core
38
+ in vec4 vertexColor;
39
+ out vec4 fragColor;
40
+ void main() {
41
+ fragColor = vertexColor;
42
+ }
43
+ GLSL
44
+ attributes: {
45
+ position: {buffer: positions, size: 2},
46
+ color: {buffer: colors, size: 4}
47
+ },
48
+ count: Rugl.prop(:count),
49
+ primitive: :lines,
50
+ line_width: 1.5
51
+ )
52
+
53
+ lines = Box2DDebugLines.new
54
+ stepper = Box2D::FixedStepper.new(hz: 60)
55
+ previous_time = 0.0
56
+ rugl.frame do |context|
57
+ delta_time = context[:time] - previous_time
58
+ previous_time = context[:time]
59
+ stepper.advance(delta_time) { |step| world.step(step) }
60
+
61
+ lines.clear
62
+ world.debug_draw(flags: [:shapes, :joints]) { |command| lines << command }
63
+ positions.update(data: lines.positions)
64
+ colors.update(data: lines.colors)
65
+
66
+ rugl.clear(color: [0.04, 0.05, 0.08, 1.0])
67
+ draw_lines.call(count: lines.positions.length)
68
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "box2d"
4
+ require "stagecraft"
5
+ require_relative "support/box2d_debug_lines"
6
+
7
+ world = nil
8
+ app = nil
9
+ at_exit do
10
+ world&.destroy if world&.valid?
11
+ if app && !app.renderer.disposed?
12
+ app.renderer.dispose
13
+ app.window.close
14
+ end
15
+ end
16
+
17
+ world = Box2D::World.new
18
+ world.create_body(position: [0, -1]) { |body| body.box(8, 1) }
19
+ 10.times do |index|
20
+ world.create_body(type: :dynamic, position: [(index % 5) - 2, 1 + index / 5.0]) do |body|
21
+ body.circle(radius: 0.4)
22
+ end
23
+ end
24
+
25
+ app = Stagecraft::App.new(title: "Box2D debug draw", width: 960, height: 720)
26
+ scene = Stagecraft::Scene.new
27
+ scene.background = "#0a0d14"
28
+ camera = Stagecraft::Cameras::Orthographic.new(
29
+ left: -10, right: 10, top: 12, bottom: -3, near: 0.1, far: 20
30
+ )
31
+ camera.position.z = 10
32
+
33
+ geometry = Stagecraft::Geometry.new(topology: :line_list)
34
+ geometry.set_attribute(:position, data: "".b, format: :float32x3, count: 0)
35
+ geometry.set_attribute(:color, data: "".b, format: :float32x4, count: 0)
36
+ mesh = Stagecraft::Mesh.new(geometry, Stagecraft::Materials::Unlit.new)
37
+ mesh.frustum_culled = false
38
+ scene.add(mesh)
39
+
40
+ lines = Box2DDebugLines.new
41
+ stepper = Box2D::FixedStepper.new(hz: 60)
42
+ app.run do |delta_time|
43
+ stepper.advance(delta_time) { |step| world.step(step) }
44
+ lines.clear
45
+ world.debug_draw(flags: [:shapes, :joints]) { |command| lines << command }
46
+
47
+ next_geometry = Stagecraft::Geometry.new(topology: :line_list)
48
+ next_geometry.set_attribute(
49
+ :position,
50
+ data: lines.positions.flat_map { |x, y| [x, y, 0.0] }.pack("e*"),
51
+ format: :float32x3,
52
+ count: lines.positions.length
53
+ )
54
+ next_geometry.set_attribute(
55
+ :color,
56
+ data: lines.colors.flatten.pack("e*"),
57
+ format: :float32x4,
58
+ count: lines.colors.length
59
+ )
60
+ mesh.geometry = next_geometry
61
+ geometry.dispose
62
+ geometry = next_geometry
63
+ app.renderer.render(scene, camera)
64
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Box2DDebugLines
4
+ CIRCLE_SEGMENTS = 24
5
+
6
+ attr_reader :positions, :colors
7
+
8
+ def initialize
9
+ clear
10
+ end
11
+
12
+ def clear
13
+ @positions = []
14
+ @colors = []
15
+ self
16
+ end
17
+
18
+ def <<(command)
19
+ type, *arguments = command
20
+ case type
21
+ when :polygon
22
+ points, color = arguments
23
+ add_loop(points, color)
24
+ when :solid_polygon
25
+ transform, points, _radius, color = arguments
26
+ add_loop(transform_points(points, transform), color)
27
+ when :circle
28
+ center, radius, color = arguments
29
+ add_circle(center, radius, color)
30
+ when :solid_circle
31
+ transform, radius, color = arguments
32
+ add_circle(transform.fetch(:position), radius, color)
33
+ when :solid_capsule
34
+ point1, point2, radius, color = arguments
35
+ add_capsule(point1, point2, radius, color)
36
+ when :segment
37
+ point1, point2, color = arguments
38
+ add_segment(point1, point2, color)
39
+ when :transform
40
+ add_transform(arguments.fetch(0))
41
+ when :point
42
+ point, _size, color = arguments
43
+ add_point(point, color)
44
+ end
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def add_loop(points, color)
51
+ return if points.empty?
52
+
53
+ points.each_with_index do |point, index|
54
+ add_segment(point, points[(index + 1) % points.length], color)
55
+ end
56
+ end
57
+
58
+ def add_circle(center, radius, color)
59
+ points = CIRCLE_SEGMENTS.times.map do |index|
60
+ angle = index * Math::PI * 2 / CIRCLE_SEGMENTS
61
+ [center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius]
62
+ end
63
+ add_loop(points, color)
64
+ end
65
+
66
+ def add_capsule(point1, point2, radius, color)
67
+ add_circle(point1, radius, color)
68
+ add_circle(point2, radius, color)
69
+ dx = point2[0] - point1[0]
70
+ dy = point2[1] - point1[1]
71
+ length = Math.hypot(dx, dy)
72
+ return if length.zero?
73
+
74
+ normal = [-dy * radius / length, dx * radius / length]
75
+ add_segment(offset(point1, normal), offset(point2, normal), color)
76
+ add_segment(offset(point1, normal, -1), offset(point2, normal, -1), color)
77
+ end
78
+
79
+ def add_transform(transform)
80
+ origin = transform.fetch(:position)
81
+ angle = transform.fetch(:angle)
82
+ x_axis = [origin[0] + Math.cos(angle) * 0.4, origin[1] + Math.sin(angle) * 0.4]
83
+ y_axis = [origin[0] - Math.sin(angle) * 0.4, origin[1] + Math.cos(angle) * 0.4]
84
+ add_segment(origin, x_axis, 0xFF0000)
85
+ add_segment(origin, y_axis, 0x00FF00)
86
+ end
87
+
88
+ def add_point(point, color)
89
+ add_segment([point[0] - 0.05, point[1]], [point[0] + 0.05, point[1]], color)
90
+ add_segment([point[0], point[1] - 0.05], [point[0], point[1] + 0.05], color)
91
+ end
92
+
93
+ def add_segment(point1, point2, color)
94
+ rgba = hex_color(color)
95
+ @positions.push(point1.to_a, point2.to_a)
96
+ @colors.push(rgba, rgba)
97
+ end
98
+
99
+ def transform_points(points, transform)
100
+ position = transform.fetch(:position)
101
+ angle = transform.fetch(:angle)
102
+ cosine = Math.cos(angle)
103
+ sine = Math.sin(angle)
104
+ points.map do |x, y|
105
+ [
106
+ position[0] + x * cosine - y * sine,
107
+ position[1] + x * sine + y * cosine
108
+ ]
109
+ end
110
+ end
111
+
112
+ def offset(point, normal, direction = 1)
113
+ [point[0] + normal[0] * direction, point[1] + normal[1] * direction]
114
+ end
115
+
116
+ def hex_color(value)
117
+ color = Integer(value) & 0xFFFFFF
118
+ [
119
+ ((color >> 16) & 0xFF) / 255.0,
120
+ ((color >> 8) & 0xFF) / 255.0,
121
+ (color & 0xFF) / 255.0,
122
+ 1.0
123
+ ]
124
+ end
125
+ end