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
data/lib/box2d/body.rb ADDED
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class Body < Handle
5
+ include BodyShapes
6
+
7
+ ID_CLASS = Native::BodyId
8
+ TYPES = {
9
+ Native::BodyType::STATIC_BODY => :static,
10
+ Native::BodyType::KINEMATIC_BODY => :kinematic,
11
+ Native::BodyType::DYNAMIC_BODY => :dynamic
12
+ }.freeze
13
+ TYPE_VALUES = TYPES.invert.freeze
14
+
15
+ attr_reader :shapes, :chains, :joints
16
+
17
+ def initialize(world, id)
18
+ super
19
+ @shapes = []
20
+ @chains = []
21
+ @joints = []
22
+ end
23
+
24
+ def destroy
25
+ ensure_valid!
26
+ Native.b2DestroyBody(@id)
27
+ @joints.dup.each(&:invalidate_from_body!)
28
+ @shapes.each { |shape| shape.send(:invalidate!) }
29
+ @chains.each { |chain| chain.send(:invalidate!) }
30
+ invalidate!
31
+ @world.unregister_body(self)
32
+ true
33
+ end
34
+
35
+ def type
36
+ ensure_valid!
37
+ TYPES.fetch(Native.b2Body_GetType(@id))
38
+ end
39
+
40
+ def type=(value)
41
+ ensure_valid!
42
+ Native.b2Body_SetType(@id, TYPE_VALUES.fetch(value) { raise ArgumentError, "invalid body type: #{value.inspect}" })
43
+ end
44
+
45
+ def position
46
+ ensure_valid!
47
+ ValueConversion.vec2(Native.b2Body_GetPosition(@id))
48
+ end
49
+
50
+ def position=(value)
51
+ ensure_valid!
52
+ Native.b2Body_SetTransform(@id, ValueConversion.native_vec2(value, label: "position"), Native.b2Body_GetRotation(@id))
53
+ end
54
+
55
+ def angle
56
+ ensure_valid!
57
+ rotation = Native.b2Body_GetRotation(@id)
58
+ Math.atan2(rotation[:s], rotation[:c])
59
+ end
60
+
61
+ def angle=(value)
62
+ ensure_valid!
63
+ Native.b2Body_SetTransform(@id, Native.b2Body_GetPosition(@id), ValueConversion.native_rot(value))
64
+ end
65
+
66
+ def linear_velocity
67
+ ensure_valid!
68
+ ValueConversion.vec2(Native.b2Body_GetLinearVelocity(@id))
69
+ end
70
+
71
+ def linear_velocity=(value)
72
+ ensure_valid!
73
+ Native.b2Body_SetLinearVelocity(@id, ValueConversion.native_vec2(value, label: "linear_velocity"))
74
+ end
75
+
76
+ def angular_velocity
77
+ ensure_valid!
78
+ Native.b2Body_GetAngularVelocity(@id)
79
+ end
80
+
81
+ def angular_velocity=(value)
82
+ ensure_valid!
83
+ Native.b2Body_SetAngularVelocity(@id, ValueConversion.finite_float(value, label: "angular_velocity"))
84
+ end
85
+
86
+ def apply_impulse(impulse, point: nil, wake: true)
87
+ ensure_valid!
88
+ native_impulse = ValueConversion.native_vec2(impulse, label: "impulse")
89
+ if point
90
+ Native.b2Body_ApplyLinearImpulse(@id, native_impulse, ValueConversion.native_vec2(point, label: "point"), !!wake)
91
+ else
92
+ Native.b2Body_ApplyLinearImpulseToCenter(@id, native_impulse, !!wake)
93
+ end
94
+ self
95
+ end
96
+
97
+ def apply_force(force, point: nil, wake: true)
98
+ ensure_valid!
99
+ native_force = ValueConversion.native_vec2(force, label: "force")
100
+ if point
101
+ Native.b2Body_ApplyForce(@id, native_force, ValueConversion.native_vec2(point, label: "point"), !!wake)
102
+ else
103
+ Native.b2Body_ApplyForceToCenter(@id, native_force, !!wake)
104
+ end
105
+ self
106
+ end
107
+
108
+ def apply_torque(torque, wake: true)
109
+ ensure_valid!
110
+ Native.b2Body_ApplyTorque(@id, ValueConversion.finite_float(torque, label: "torque"), !!wake)
111
+ self
112
+ end
113
+
114
+ def mass
115
+ ensure_valid!
116
+ Native.b2Body_GetMass(@id)
117
+ end
118
+
119
+ def awake?
120
+ ensure_valid!
121
+ Native.b2Body_IsAwake(@id)
122
+ end
123
+
124
+ def awake=(value)
125
+ ensure_valid!
126
+ Native.b2Body_SetAwake(@id, !!value)
127
+ end
128
+
129
+ def bullet?
130
+ ensure_valid!
131
+ Native.b2Body_IsBullet(@id)
132
+ end
133
+
134
+ def bullet=(value)
135
+ ensure_valid!
136
+ Native.b2Body_SetBullet(@id, !!value)
137
+ end
138
+
139
+ def user_data
140
+ ensure_valid!
141
+ @world.body_user_data(self)
142
+ end
143
+
144
+ def user_data=(value)
145
+ ensure_valid!
146
+ @world.set_body_user_data(self, value)
147
+ end
148
+
149
+ def unregister_shape(shape)
150
+ @shapes.delete(shape)
151
+ end
152
+
153
+ def unregister_chain(chain)
154
+ @chains.delete(chain)
155
+ end
156
+
157
+ def register_joint(joint)
158
+ @joints << joint
159
+ joint
160
+ end
161
+
162
+ def unregister_joint(joint)
163
+ @joints.delete(joint)
164
+ end
165
+
166
+ protected
167
+
168
+ def native_valid?
169
+ Native.b2Body_IsValid(@id)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ module BodyDefinition
5
+ private
6
+
7
+ def build_body_definition(type:, position:, angle:, options:)
8
+ definition = Native.b2DefaultBodyDef
9
+ definition[:type] = self.class::BODY_TYPES.fetch(type) do
10
+ raise ArgumentError, "invalid body type: #{type.inspect}"
11
+ end
12
+ definition[:position] = ValueConversion.native_vec2(position, label: "position")
13
+ definition[:rotation] = ValueConversion.native_rot(angle)
14
+ definition[:linearVelocity] = ValueConversion.native_vec2(
15
+ options.fetch(:linear_velocity, [0, 0]),
16
+ label: "linear_velocity"
17
+ )
18
+ definition[:angularVelocity] = ValueConversion.finite_float(
19
+ options.fetch(:angular_velocity, 0),
20
+ label: "angular_velocity"
21
+ )
22
+ definition[:linearDamping] = ValueConversion.non_negative_float(
23
+ options.fetch(:linear_damping, 0),
24
+ label: "linear_damping"
25
+ )
26
+ definition[:angularDamping] = ValueConversion.non_negative_float(
27
+ options.fetch(:angular_damping, 0),
28
+ label: "angular_damping"
29
+ )
30
+ definition[:gravityScale] = ValueConversion.finite_float(options.fetch(:gravity_scale, 1), label: "gravity_scale")
31
+ definition[:isBullet] = !!options.fetch(:bullet, false)
32
+ definition[:fixedRotation] = !!options.fetch(:fixed_rotation, false)
33
+ definition[:isAwake] = !!options.fetch(:awake, true)
34
+ definition[:enableSleep] = !!options.fetch(:sleep, true)
35
+ definition
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ module BodyShapes
5
+ def box(half_width, half_height, center: [0, 0], angle: 0, **options)
6
+ ensure_valid!
7
+ width = ValueConversion.positive_float(half_width, label: "half_width")
8
+ height = ValueConversion.positive_float(half_height, label: "half_height")
9
+ polygon = Native.b2MakeOffsetBox(
10
+ width,
11
+ height,
12
+ ValueConversion.native_vec2(center, label: "center"),
13
+ ValueConversion.native_rot(angle)
14
+ )
15
+ create_shape(:b2CreatePolygonShape, polygon, options)
16
+ end
17
+
18
+ def circle(radius:, center: [0, 0], **options)
19
+ ensure_valid!
20
+ geometry = Native::Circle.new
21
+ geometry[:center] = ValueConversion.native_vec2(center, label: "center")
22
+ geometry[:radius] = ValueConversion.positive_float(radius, label: "radius")
23
+ create_shape(:b2CreateCircleShape, geometry, options)
24
+ end
25
+
26
+ def capsule(point1, point2, radius:, **options)
27
+ ensure_valid!
28
+ geometry = Native::Capsule.new
29
+ geometry[:center1] = ValueConversion.native_vec2(point1, label: "point1")
30
+ geometry[:center2] = ValueConversion.native_vec2(point2, label: "point2")
31
+ geometry[:radius] = ValueConversion.positive_float(radius, label: "radius")
32
+ create_shape(:b2CreateCapsuleShape, geometry, options)
33
+ end
34
+
35
+ def polygon(points, radius: 0.0, **options)
36
+ ensure_valid!
37
+ point_buffer, count = native_points(points, minimum: 3, maximum: 8)
38
+ hull = Native.b2ComputeHull(point_buffer, count)
39
+ raise ArgumentError, "points do not form a valid convex hull" if hull[:count] < 3
40
+
41
+ polygon = Native.b2MakePolygon(
42
+ hull.pointer,
43
+ ValueConversion.non_negative_float(radius, label: "radius")
44
+ )
45
+ create_shape(:b2CreatePolygonShape, polygon, options)
46
+ end
47
+
48
+ def segment(point1, point2, **options)
49
+ ensure_valid!
50
+ geometry = Native::Segment.new
51
+ geometry[:point1] = ValueConversion.native_vec2(point1, label: "point1")
52
+ geometry[:point2] = ValueConversion.native_vec2(point2, label: "point2")
53
+ validate_separation!(geometry[:point1], geometry[:point2], label: "segment endpoints")
54
+ create_shape(:b2CreateSegmentShape, geometry, options)
55
+ end
56
+
57
+ def chain(points, loop: false, **options)
58
+ ensure_valid!
59
+ point_buffer, count = native_points(points, minimum: 4)
60
+ if loop
61
+ first = Native::Vec2.new(point_buffer)
62
+ last = Native::Vec2.new(point_buffer + (count - 1) * Native::Vec2.size)
63
+ validate_separation!(last, first, label: "loop endpoints")
64
+ end
65
+ definition = Native.b2DefaultChainDef
66
+ definition[:points] = point_buffer
67
+ definition[:count] = count
68
+ definition[:isLoop] = !!loop
69
+ definition[:enableSensorEvents] = !!options.fetch(:sensor_events, true)
70
+ ShapeDefinition.apply_filter(definition[:filter], options.fetch(:filter, {}))
71
+
72
+ material_buffer = FFI::MemoryPointer.new(Native::SurfaceMaterial)
73
+ material = Native.b2DefaultSurfaceMaterial
74
+ configure_material(material, options)
75
+ material_buffer.put_bytes(0, material.pointer.read_bytes(Native::SurfaceMaterial.size))
76
+ definition[:materials] = material_buffer
77
+ definition[:materialCount] = 1
78
+
79
+ native_id = Native.b2CreateChain(@id, definition.pointer)
80
+ chain = Chain.new(@world, native_id, body: self)
81
+ @chains << chain
82
+ @world.register_chain(chain)
83
+ end
84
+
85
+ private
86
+
87
+ def create_shape(function, geometry, options)
88
+ definition = ShapeDefinition.build(options)
89
+ native_id = Native.public_send(function, @id, definition.pointer, geometry.pointer)
90
+ shape = Shape.new(@world, native_id, body: self)
91
+ shape.user_data = options[:user_data] if options.key?(:user_data)
92
+ @shapes << shape
93
+ @world.register_shape(shape)
94
+ end
95
+
96
+ def native_points(points, minimum:, maximum: nil)
97
+ values = points.to_a
98
+ valid = values.length >= minimum && (!maximum || values.length <= maximum)
99
+ range = maximum ? "#{minimum}..#{maximum}" : "at least #{minimum}"
100
+ raise ArgumentError, "points must contain #{range} vertices" unless valid
101
+
102
+ pointer = FFI::MemoryPointer.new(Native::Vec2, values.length)
103
+ values.each_with_index do |point, index|
104
+ vector = ValueConversion.native_vec2(point, label: "points[#{index}]")
105
+ pointer.put_bytes(index * Native::Vec2.size, vector.pointer.read_bytes(Native::Vec2.size))
106
+ end
107
+ validate_chain_points!(pointer, values.length) if minimum >= 4
108
+ [pointer, values.length]
109
+ end
110
+
111
+ def validate_chain_points!(pointer, count)
112
+ points = Array.new(count) { |index| Native::Vec2.new(pointer + index * Native::Vec2.size) }
113
+ points.each_cons(2) { |point1, point2| validate_separation!(point1, point2, label: "adjacent chain points") }
114
+ end
115
+
116
+ def validate_separation!(point1, point2, label:)
117
+ minimum = 0.005 * Native.b2GetLengthUnitsPerMeter
118
+ distance_squared = (point2[:x] - point1[:x])**2 + (point2[:y] - point1[:y])**2
119
+ return if distance_squared > minimum**2
120
+
121
+ raise ArgumentError, "#{label} must be more than #{minimum} units apart"
122
+ end
123
+
124
+ def configure_material(material, options)
125
+ material[:friction] = ValueConversion.non_negative_float(options.fetch(:friction, 0.6), label: "friction")
126
+ material[:restitution] = ShapeDefinition.coefficient(options.fetch(:restitution, 0.0), "restitution")
127
+ material[:rollingResistance] = ShapeDefinition.coefficient(
128
+ options.fetch(:rolling_resistance, 0.0),
129
+ "rolling_resistance"
130
+ )
131
+ material[:tangentSpeed] = ValueConversion.finite_float(options.fetch(:tangent_speed, 0.0), label: "tangent_speed")
132
+ material[:userMaterialId] = Integer(options.fetch(:material, 0))
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class Chain < Handle
5
+ ID_CLASS = Native::ChainId
6
+
7
+ attr_reader :body
8
+
9
+ def initialize(world, id, body: nil)
10
+ super(world, id)
11
+ @body = body
12
+ end
13
+
14
+ def destroy
15
+ ensure_valid!
16
+ Native.b2DestroyChain(@id)
17
+ invalidate!
18
+ @world.unregister_chain(self)
19
+ @body&.unregister_chain(self)
20
+ true
21
+ end
22
+
23
+ def segments
24
+ ensure_valid!
25
+ count = Native.b2Chain_GetSegmentCount(@id)
26
+ pointer = FFI::MemoryPointer.new(Native::ShapeId, count)
27
+ actual = Native.b2Chain_GetSegments(@id, pointer, count)
28
+
29
+ Array.new(actual) do |index|
30
+ id = Native::ShapeId.new(pointer + index * Native::ShapeId.size)
31
+ @world.shape_for_id(id)
32
+ end
33
+ end
34
+
35
+ def friction
36
+ ensure_valid!
37
+ Native.b2Chain_GetFriction(@id)
38
+ end
39
+
40
+ def friction=(value)
41
+ ensure_valid!
42
+ Native.b2Chain_SetFriction(@id, ValueConversion.non_negative_float(value, label: "friction"))
43
+ end
44
+
45
+ def restitution
46
+ ensure_valid!
47
+ Native.b2Chain_GetRestitution(@id)
48
+ end
49
+
50
+ def restitution=(value)
51
+ ensure_valid!
52
+ Native.b2Chain_SetRestitution(@id, ShapeDefinition.coefficient(value, "restitution"))
53
+ end
54
+
55
+ protected
56
+
57
+ def native_valid?
58
+ Native.b2Chain_IsValid(@id)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class DebugDraw
5
+ FLAG_FIELDS = {
6
+ shapes: :drawShapes,
7
+ joints: :drawJoints,
8
+ joint_extras: :drawJointExtras,
9
+ aabbs: :drawBounds,
10
+ bounds: :drawBounds,
11
+ mass: :drawMass,
12
+ body_names: :drawBodyNames,
13
+ contacts: :drawContacts,
14
+ graph_colors: :drawGraphColors,
15
+ contact_normals: :drawContactNormals,
16
+ contact_impulses: :drawContactImpulses,
17
+ contact_features: :drawContactFeatures,
18
+ friction_impulses: :drawFrictionImpulses,
19
+ islands: :drawIslands
20
+ }.freeze
21
+
22
+ attr_reader :definition, :callbacks
23
+
24
+ def initialize(flags, &block)
25
+ @block = block
26
+ @error = nil
27
+ @definition = Native.b2DefaultDebugDraw
28
+ @callbacks = []
29
+ configure_flags(flags)
30
+ configure_callbacks
31
+ end
32
+
33
+ def raise_callback_error
34
+ raise @error if @error
35
+ end
36
+
37
+ private
38
+
39
+ def configure_flags(flags)
40
+ flags.each do |flag|
41
+ field = FLAG_FIELDS.fetch(flag) { raise ArgumentError, "unknown debug draw flag: #{flag.inspect}" }
42
+ @definition[field] = true
43
+ end
44
+ end
45
+
46
+ def configure_callbacks
47
+ callback(:DrawPolygonFcn, :void, [:pointer, :int, :int, :pointer]) do |vertices, count, color, _context|
48
+ emit([:polygon, points(vertices, count), color])
49
+ end
50
+ callback(
51
+ :DrawSolidPolygonFcn,
52
+ :void,
53
+ [Native::Transform.by_value, :pointer, :int, :float, :int, :pointer]
54
+ ) do |transform, vertices, count, radius, color, _context|
55
+ emit([:solid_polygon, transform_value(transform), points(vertices, count), radius, color])
56
+ end
57
+ callback(:DrawCircleFcn, :void, [Native::Vec2.by_value, :float, :int, :pointer]) do |center, radius, color, _context|
58
+ emit([:circle, ValueConversion.vec2(center), radius, color])
59
+ end
60
+ callback(
61
+ :DrawSolidCircleFcn,
62
+ :void,
63
+ [Native::Transform.by_value, :float, :int, :pointer]
64
+ ) do |transform, radius, color, _context|
65
+ emit([:solid_circle, transform_value(transform), radius, color])
66
+ end
67
+ callback(
68
+ :DrawSolidCapsuleFcn,
69
+ :void,
70
+ [Native::Vec2.by_value, Native::Vec2.by_value, :float, :int, :pointer]
71
+ ) do |point1, point2, radius, color, _context|
72
+ emit([:solid_capsule, ValueConversion.vec2(point1), ValueConversion.vec2(point2), radius, color])
73
+ end
74
+ callback(
75
+ :DrawSegmentFcn,
76
+ :void,
77
+ [Native::Vec2.by_value, Native::Vec2.by_value, :int, :pointer]
78
+ ) do |point1, point2, color, _context|
79
+ emit([:segment, ValueConversion.vec2(point1), ValueConversion.vec2(point2), color])
80
+ end
81
+ callback(:DrawTransformFcn, :void, [Native::Transform.by_value, :pointer]) do |transform, _context|
82
+ emit([:transform, transform_value(transform)])
83
+ end
84
+ callback(:DrawPointFcn, :void, [Native::Vec2.by_value, :float, :int, :pointer]) do |point, size, color, _context|
85
+ emit([:point, ValueConversion.vec2(point), size, color])
86
+ end
87
+ callback(:DrawStringFcn, :void, [Native::Vec2.by_value, :pointer, :int, :pointer]) do |point, string, color, _context|
88
+ emit([:string, ValueConversion.vec2(point), string.read_string, color])
89
+ end
90
+ end
91
+
92
+ def callback(field, result, arguments, &block)
93
+ function = FFI::Function.new(result, arguments, &block)
94
+ @callbacks << function
95
+ @definition[field] = function
96
+ end
97
+
98
+ def points(pointer, count)
99
+ Array.new(count) do |index|
100
+ ValueConversion.vec2(Native::Vec2.new(pointer + index * Native::Vec2.size))
101
+ end
102
+ end
103
+
104
+ def transform_value(transform)
105
+ rotation = transform[:q]
106
+ {
107
+ position: ValueConversion.vec2(transform[:p]),
108
+ angle: Math.atan2(rotation[:s], rotation[:c])
109
+ }
110
+ end
111
+
112
+ def emit(command)
113
+ @block.call(command)
114
+ rescue Exception => error
115
+ @error ||= error
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ ContactBegin = Data.define(:shape_a, :shape_b, :manifold)
5
+ ContactEnd = Data.define(:shape_a, :shape_b)
6
+ ContactHit = Data.define(:shape_a, :shape_b, :point, :normal, :approach_speed)
7
+ SensorBegin = Data.define(:sensor, :visitor)
8
+ SensorEnd = Data.define(:sensor, :visitor)
9
+ Manifold = Data.define(:normal, :points)
10
+ ManifoldPoint = Data.define(:point, :separation, :normal_impulse, :tangent_impulse, :persisted)
11
+
12
+ class Events
13
+ EMPTY = [].freeze
14
+
15
+ attr_reader :begin_contacts, :end_contacts, :hits, :sensor_begins, :sensor_ends
16
+
17
+ def self.empty
18
+ new(
19
+ begin_contacts: EMPTY,
20
+ end_contacts: EMPTY,
21
+ hits: EMPTY,
22
+ sensor_begins: EMPTY,
23
+ sensor_ends: EMPTY
24
+ )
25
+ end
26
+
27
+ def self.capture(world)
28
+ contact_events = Native.b2World_GetContactEvents(world.id)
29
+ sensor_events = Native.b2World_GetSensorEvents(world.id)
30
+ new(
31
+ begin_contacts: read_array(contact_events[:beginEvents], contact_events[:beginCount], Native::ContactBeginTouchEvent) do |event|
32
+ ContactBegin.new(
33
+ shape_a: world.shape_for_id(event[:shapeIdA]),
34
+ shape_b: world.shape_for_id(event[:shapeIdB]),
35
+ manifold: capture_manifold(event[:manifold])
36
+ )
37
+ end,
38
+ end_contacts: read_array(contact_events[:endEvents], contact_events[:endCount], Native::ContactEndTouchEvent) do |event|
39
+ ContactEnd.new(
40
+ shape_a: world.shape_for_id(event[:shapeIdA]),
41
+ shape_b: world.shape_for_id(event[:shapeIdB])
42
+ )
43
+ end,
44
+ hits: read_array(contact_events[:hitEvents], contact_events[:hitCount], Native::ContactHitEvent) do |event|
45
+ ContactHit.new(
46
+ shape_a: world.shape_for_id(event[:shapeIdA]),
47
+ shape_b: world.shape_for_id(event[:shapeIdB]),
48
+ point: ValueConversion.vec2(event[:point]),
49
+ normal: ValueConversion.vec2(event[:normal]),
50
+ approach_speed: event[:approachSpeed]
51
+ )
52
+ end,
53
+ sensor_begins: read_array(sensor_events[:beginEvents], sensor_events[:beginCount], Native::SensorBeginTouchEvent) do |event|
54
+ SensorBegin.new(
55
+ sensor: world.shape_for_id(event[:sensorShapeId]),
56
+ visitor: world.shape_for_id(event[:visitorShapeId])
57
+ )
58
+ end,
59
+ sensor_ends: read_array(sensor_events[:endEvents], sensor_events[:endCount], Native::SensorEndTouchEvent) do |event|
60
+ SensorEnd.new(
61
+ sensor: world.shape_for_id(event[:sensorShapeId]),
62
+ visitor: world.shape_for_id(event[:visitorShapeId])
63
+ )
64
+ end
65
+ )
66
+ end
67
+
68
+ def initialize(begin_contacts:, end_contacts:, hits:, sensor_begins:, sensor_ends:)
69
+ @begin_contacts = begin_contacts.freeze
70
+ @end_contacts = end_contacts.freeze
71
+ @hits = hits.freeze
72
+ @sensor_begins = sensor_begins.freeze
73
+ @sensor_ends = sensor_ends.freeze
74
+ freeze
75
+ end
76
+
77
+ class << self
78
+ private
79
+
80
+ def read_array(pointer, count, struct_class)
81
+ return [] if count.zero?
82
+
83
+ Array.new(count) do |index|
84
+ yield struct_class.new(pointer + index * struct_class.size)
85
+ end
86
+ end
87
+
88
+ def capture_manifold(manifold)
89
+ points = Array.new(manifold[:pointCount]) do |index|
90
+ point = Native::ManifoldPoint.new(manifold[:points].to_ptr + index * Native::ManifoldPoint.size)
91
+ ManifoldPoint.new(
92
+ point: ValueConversion.vec2(point[:point]),
93
+ separation: point[:separation],
94
+ normal_impulse: point[:normalImpulse],
95
+ tangent_impulse: point[:tangentImpulse],
96
+ persisted: point[:persisted]
97
+ )
98
+ end
99
+ Manifold.new(normal: ValueConversion.vec2(manifold[:normal]), points: points.freeze)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class FixedStepper
5
+ attr_reader :step, :max_substeps
6
+
7
+ def initialize(hz: 60, max_substeps: 5)
8
+ frequency = ValueConversion.positive_float(hz, label: "hz")
9
+ @step = 1.0 / frequency
10
+ @max_substeps = Integer(max_substeps)
11
+ raise ArgumentError, "max_substeps must be greater than zero" unless @max_substeps.positive?
12
+
13
+ @accumulator = 0.0
14
+ end
15
+
16
+ def advance(delta_time)
17
+ raise ArgumentError, "a step block is required" unless block_given?
18
+
19
+ delta = ValueConversion.non_negative_float(delta_time, label: "delta_time")
20
+ @accumulator = [@accumulator + delta, @step * @max_substeps].min
21
+
22
+ steps = 0
23
+ epsilon = @step * 1e-12
24
+ while @accumulator + epsilon >= @step && steps < @max_substeps
25
+ yield @step
26
+ @accumulator -= @step
27
+ @accumulator = 0.0 if @accumulator.abs < epsilon
28
+ steps += 1
29
+ end
30
+
31
+ [@accumulator / @step, 1.0 - Float::EPSILON].min
32
+ end
33
+
34
+ def reset
35
+ @accumulator = 0.0
36
+ self
37
+ end
38
+ end
39
+ end