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
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Box2D
6
+ module NativeLoader
7
+ module_function
8
+
9
+ ENVIRONMENT_KEY = "BOX2D_LIBRARY_PATH"
10
+ EXTENSION_NAME = "native.#{RbConfig::CONFIG.fetch("DLEXT")}"
11
+
12
+ def library_path
13
+ explicit_path = ENV[ENVIRONMENT_KEY]
14
+ return validate_explicit_path(explicit_path) if explicit_path
15
+
16
+ path = candidates.find { |candidate| File.file?(candidate) }
17
+ return path if path
18
+
19
+ raise LoadError, missing_library_message
20
+ end
21
+
22
+ def candidates
23
+ relative_path = File.join("box2d", EXTENSION_NAME)
24
+ load_path_candidates = $LOAD_PATH.map { |path| File.expand_path(relative_path, path) }
25
+ source_candidate = File.expand_path("../../ext/box2d/#{EXTENSION_NAME}", __dir__)
26
+
27
+ (load_path_candidates << source_candidate).uniq
28
+ end
29
+
30
+ def validate_explicit_path(path)
31
+ expanded_path = File.expand_path(path)
32
+ return expanded_path if File.file?(expanded_path)
33
+
34
+ raise LoadError, "#{ENVIRONMENT_KEY} does not point to a file: #{expanded_path}"
35
+ end
36
+
37
+ def missing_library_message
38
+ <<~MESSAGE.chomp
39
+ Box2D native library was not found. Reinstall the gem to build it, run
40
+ `bundle exec rake native:compile` from a source checkout, or set
41
+ #{ENVIRONMENT_KEY} to a compatible Box2D 3.1 shared library.
42
+ MESSAGE
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class PixelScale
5
+ attr_reader :pixels_per_meter
6
+
7
+ def initialize(pixels_per_meter = 32)
8
+ @pixels_per_meter = ValueConversion.positive_float(pixels_per_meter, label: "pixels_per_meter")
9
+ end
10
+
11
+ def to_pixels(meters)
12
+ ValueConversion.finite_float(meters, label: "meters") * @pixels_per_meter
13
+ end
14
+
15
+ def to_meters(pixels)
16
+ ValueConversion.finite_float(pixels, label: "pixels") / @pixels_per_meter
17
+ end
18
+
19
+ def vector_to_pixels(vector)
20
+ x, y = vector
21
+ [to_pixels(x), to_pixels(y)]
22
+ end
23
+
24
+ def vector_to_meters(vector)
25
+ x, y = vector
26
+ [to_meters(x), to_meters(y)]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class Shape < Handle
5
+ ID_CLASS = Native::ShapeId
6
+ TYPES = {
7
+ Native::ShapeType::CIRCLE_SHAPE => :circle,
8
+ Native::ShapeType::CAPSULE_SHAPE => :capsule,
9
+ Native::ShapeType::SEGMENT_SHAPE => :segment,
10
+ Native::ShapeType::POLYGON_SHAPE => :polygon,
11
+ Native::ShapeType::CHAIN_SEGMENT_SHAPE => :chain_segment
12
+ }.freeze
13
+
14
+ attr_reader :body
15
+
16
+ def initialize(world, id, body: nil)
17
+ super(world, id)
18
+ @body = body
19
+ end
20
+
21
+ def destroy
22
+ ensure_valid!
23
+ Native.b2DestroyShape(@id, true)
24
+ invalidate!
25
+ @world.unregister_shape(self)
26
+ @body&.unregister_shape(self)
27
+ true
28
+ end
29
+
30
+ def type
31
+ ensure_valid!
32
+ TYPES.fetch(Native.b2Shape_GetType(@id))
33
+ end
34
+
35
+ def sensor?
36
+ ensure_valid!
37
+ Native.b2Shape_IsSensor(@id)
38
+ end
39
+
40
+ def density
41
+ ensure_valid!
42
+ Native.b2Shape_GetDensity(@id)
43
+ end
44
+
45
+ def density=(value)
46
+ ensure_valid!
47
+ Native.b2Shape_SetDensity(@id, ValueConversion.non_negative_float(value, label: "density"), true)
48
+ end
49
+
50
+ def friction
51
+ ensure_valid!
52
+ Native.b2Shape_GetFriction(@id)
53
+ end
54
+
55
+ def friction=(value)
56
+ ensure_valid!
57
+ Native.b2Shape_SetFriction(@id, ValueConversion.non_negative_float(value, label: "friction"))
58
+ end
59
+
60
+ def restitution
61
+ ensure_valid!
62
+ Native.b2Shape_GetRestitution(@id)
63
+ end
64
+
65
+ def restitution=(value)
66
+ ensure_valid!
67
+ Native.b2Shape_SetRestitution(@id, ShapeDefinition.coefficient(value, "restitution"))
68
+ end
69
+
70
+ def filter
71
+ ensure_valid!
72
+ native_filter = Native.b2Shape_GetFilter(@id)
73
+ {
74
+ category: native_filter[:categoryBits],
75
+ mask: native_filter[:maskBits],
76
+ group: native_filter[:groupIndex]
77
+ }
78
+ end
79
+
80
+ def filter=(options)
81
+ ensure_valid!
82
+ native_filter = Native.b2Shape_GetFilter(@id)
83
+ ShapeDefinition.apply_filter(native_filter, options)
84
+ Native.b2Shape_SetFilter(@id, native_filter)
85
+ end
86
+
87
+ def user_data
88
+ ensure_valid!
89
+ @world.shape_user_data(self)
90
+ end
91
+
92
+ def user_data=(value)
93
+ ensure_valid!
94
+ @world.set_shape_user_data(self, value)
95
+ end
96
+
97
+ def aabb
98
+ ensure_valid!
99
+ bounds = Native.b2Shape_GetAABB(@id)
100
+ [ValueConversion.vec2(bounds[:lowerBound]), ValueConversion.vec2(bounds[:upperBound])]
101
+ end
102
+
103
+ protected
104
+
105
+ def native_valid?
106
+ Native.b2Shape_IsValid(@id)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ module ShapeDefinition
5
+ module_function
6
+
7
+ def build(options)
8
+ definition = Native.b2DefaultShapeDef
9
+ material = definition[:material]
10
+ material[:friction] = ValueConversion.non_negative_float(options.fetch(:friction, 0.6), label: "friction")
11
+ material[:restitution] = coefficient(options.fetch(:restitution, 0.0), "restitution")
12
+ material[:rollingResistance] = coefficient(options.fetch(:rolling_resistance, 0.0), "rolling_resistance")
13
+ material[:tangentSpeed] = ValueConversion.finite_float(options.fetch(:tangent_speed, 0.0), label: "tangent_speed")
14
+ material[:userMaterialId] = Integer(options.fetch(:material, 0))
15
+ definition[:density] = ValueConversion.non_negative_float(options.fetch(:density, 1.0), label: "density")
16
+ definition[:isSensor] = !!options.fetch(:sensor, false)
17
+ definition[:enableSensorEvents] = !!options.fetch(:sensor_events, true)
18
+ definition[:enableContactEvents] = !!options.fetch(:contact_events, true)
19
+ definition[:enableHitEvents] = !!options.fetch(:hit_events, true)
20
+ apply_filter(definition[:filter], options.fetch(:filter, {}))
21
+ definition
22
+ end
23
+
24
+ def query_filter(options)
25
+ filter = Native.b2DefaultQueryFilter
26
+ filter[:categoryBits] = Integer(options.fetch(:category, filter[:categoryBits]))
27
+ filter[:maskBits] = Integer(options.fetch(:mask, filter[:maskBits]))
28
+ filter
29
+ end
30
+
31
+ def apply_filter(filter, options)
32
+ filter[:categoryBits] = Integer(options.fetch(:category, filter[:categoryBits]))
33
+ filter[:maskBits] = Integer(options.fetch(:mask, filter[:maskBits]))
34
+ filter[:groupIndex] = Integer(options.fetch(:group, filter[:groupIndex]))
35
+ end
36
+
37
+ def coefficient(value, label)
38
+ number = ValueConversion.non_negative_float(value, label:)
39
+ return number if number <= 1.0
40
+
41
+ raise ArgumentError, "#{label} must be between 0 and 1"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ module ValueConversion
5
+ module_function
6
+
7
+ def native_vec2(value, label: "vector")
8
+ x, y = pair(value, label:)
9
+ Native::Vec2.new.tap do |vector|
10
+ vector[:x] = finite_float(x, label: "#{label}.x")
11
+ vector[:y] = finite_float(y, label: "#{label}.y")
12
+ end
13
+ end
14
+
15
+ def native_rot(angle)
16
+ radians = finite_float(angle, label: "angle")
17
+ Native::Rot.new.tap do |rotation|
18
+ rotation[:c] = Math.cos(radians)
19
+ rotation[:s] = Math.sin(radians)
20
+ end
21
+ end
22
+
23
+ def vec2(value)
24
+ x = value[:x]
25
+ y = value[:y]
26
+ return Larb::Vec2.new(x, y) if defined?(Larb::Vec2)
27
+
28
+ [x, y]
29
+ end
30
+
31
+ def finite_float(value, label:)
32
+ number = Float(value)
33
+ return number if number.finite?
34
+
35
+ raise ArgumentError, "#{label} must be finite"
36
+ rescue TypeError, ArgumentError
37
+ raise ArgumentError, "#{label} must be a finite number"
38
+ end
39
+
40
+ def positive_float(value, label:)
41
+ number = finite_float(value, label:)
42
+ return number if number.positive?
43
+
44
+ raise ArgumentError, "#{label} must be greater than zero"
45
+ end
46
+
47
+ def non_negative_float(value, label:)
48
+ number = finite_float(value, label:)
49
+ return number if number >= 0.0
50
+
51
+ raise ArgumentError, "#{label} must be non-negative"
52
+ end
53
+
54
+ def copy_struct(struct_class, value)
55
+ struct_class.new.tap do |copy|
56
+ copy.pointer.put_bytes(0, value.pointer.read_bytes(struct_class.size))
57
+ end
58
+ end
59
+
60
+ def id_key(id)
61
+ members = id.class.members
62
+ key = Integer(id[:index1]) | (Integer(id[:generation]) << 32)
63
+ return key unless members.include?(:world0)
64
+
65
+ key | (Integer(id[:world0]) << 48)
66
+ end
67
+
68
+ def pair(value, label:)
69
+ pair = if value.respond_to?(:to_ary)
70
+ value.to_ary
71
+ elsif value.respond_to?(:x) && value.respond_to?(:y)
72
+ [value.x, value.y]
73
+ end
74
+ return pair if pair&.length == 2
75
+
76
+ raise ArgumentError, "#{label} must contain exactly two components"
77
+ end
78
+ private_class_method :pair
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ VERSION = "1.0.0"
5
+ end
6
+
7
+ Box2d = Box2D unless Object.const_defined?(:Box2d)
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ class World
5
+ include WorldRegistry
6
+ include BodyDefinition
7
+ include WorldJoints
8
+ include WorldQueries
9
+
10
+ BODY_TYPES = Body::TYPE_VALUES
11
+
12
+ attr_reader :id
13
+
14
+ def initialize(gravity: [0, -9.8], substeps: 4, sleep: true, continuous: true)
15
+ definition = Native.b2DefaultWorldDef
16
+ definition[:gravity] = ValueConversion.native_vec2(gravity, label: "gravity")
17
+ definition[:enableSleep] = !!sleep
18
+ definition[:enableContinuous] = !!continuous
19
+
20
+ @id = Native.b2CreateWorld(definition.pointer)
21
+ @destroyed = false
22
+ @stepping = false
23
+ @drawing = false
24
+ initialize_registries
25
+ @events = Events.empty
26
+ self.substeps = substeps
27
+
28
+ yield self if block_given?
29
+ rescue Exception
30
+ destroy if @id && !@destroyed
31
+ raise
32
+ end
33
+
34
+ def valid?
35
+ raise ReentrantStepError, "the world cannot be accessed during a native operation" if @stepping || @drawing
36
+
37
+ !@destroyed && Native.b2World_IsValid(@id)
38
+ end
39
+
40
+ def destroyed?
41
+ !valid?
42
+ end
43
+
44
+ def destroy
45
+ return false if @destroyed
46
+
47
+ ensure_access!
48
+ Native.b2DestroyWorld(@id)
49
+ @destroyed = true
50
+ clear_registries
51
+ @events = Events.empty
52
+ true
53
+ end
54
+
55
+ def substeps
56
+ @substeps
57
+ end
58
+
59
+ def substeps=(value)
60
+ count = Integer(value)
61
+ raise ArgumentError, "substeps must be greater than zero" unless count.positive?
62
+
63
+ @substeps = count
64
+ end
65
+
66
+ def gravity
67
+ ensure_access!
68
+ ValueConversion.vec2(Native.b2World_GetGravity(@id))
69
+ end
70
+
71
+ def gravity=(value)
72
+ ensure_access!
73
+ Native.b2World_SetGravity(@id, ValueConversion.native_vec2(value, label: "gravity"))
74
+ end
75
+
76
+ def step(delta_time)
77
+ ensure_access!
78
+ time_step = ValueConversion.non_negative_float(delta_time, label: "delta_time")
79
+ @stepping = true
80
+ begin
81
+ Native.b2World_Step(@id, time_step, @substeps)
82
+ @events = Events.capture(self)
83
+ self
84
+ ensure
85
+ @stepping = false
86
+ end
87
+ end
88
+
89
+ def events
90
+ ensure_access!
91
+ @events
92
+ end
93
+
94
+ def create_body(type: :static, position: [0, 0], angle: 0, **options)
95
+ ensure_access!
96
+ definition = build_body_definition(type:, position:, angle:, options:)
97
+ body = Body.new(self, Native.b2CreateBody(@id, definition.pointer))
98
+ register_body(body)
99
+ body.user_data = options[:user_data] if options.key?(:user_data)
100
+
101
+ begin
102
+ yield body if block_given?
103
+ rescue Exception
104
+ body.destroy if body.valid?
105
+ raise
106
+ end
107
+ body
108
+ end
109
+
110
+ def debug_draw(flags: [:shapes])
111
+ raise ArgumentError, "a debug draw block is required" unless block_given?
112
+
113
+ ensure_access!
114
+ @debug_draw = DebugDraw.new(flags) { |command| yield command }
115
+ @drawing = true
116
+ begin
117
+ Native.b2World_Draw(@id, @debug_draw.definition.pointer)
118
+ ensure
119
+ @drawing = false
120
+ end
121
+ @debug_draw.raise_callback_error
122
+ self
123
+ end
124
+
125
+ def ensure_access!
126
+ raise UseAfterDestroyError, "Box2D::World has been destroyed" if @destroyed
127
+ if @stepping || @drawing
128
+ raise ReentrantStepError, "the world cannot be accessed during a native operation"
129
+ end
130
+
131
+ true
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Box2D
4
+ module WorldJoints
5
+ def create_revolute_joint(body_a:, body_b:, anchor:, limits: nil, motor: nil, spring: nil, **options)
6
+ create_typed_joint(:revolute, body_a:, body_b:, options:) do |definition|
7
+ definition[:localAnchorA] = local_point(body_a, anchor)
8
+ definition[:localAnchorB] = local_point(body_b, anchor)
9
+ definition[:referenceAngle] = body_b.angle - body_a.angle
10
+ configure_limits(definition, limits, :lowerAngle, :upperAngle)
11
+ configure_motor(definition, motor, maximum: :maxMotorTorque)
12
+ configure_spring(definition, spring)
13
+ end
14
+ end
15
+
16
+ def create_prismatic_joint(body_a:, body_b:, anchor:, axis: [1, 0], limits: nil, motor: nil, spring: nil, **options)
17
+ create_typed_joint(:prismatic, body_a:, body_b:, options:) do |definition|
18
+ definition[:localAnchorA] = local_point(body_a, anchor)
19
+ definition[:localAnchorB] = local_point(body_b, anchor)
20
+ definition[:localAxisA] = Native.b2Body_GetLocalVector(
21
+ body_a.id,
22
+ ValueConversion.native_vec2(axis, label: "axis")
23
+ )
24
+ definition[:referenceAngle] = body_b.angle - body_a.angle
25
+ configure_limits(definition, limits, :lowerTranslation, :upperTranslation)
26
+ configure_motor(definition, motor, maximum: :maxMotorForce)
27
+ configure_spring(definition, spring)
28
+ end
29
+ end
30
+
31
+ def create_distance_joint(body_a:, body_b:, anchor_a: nil, anchor_b: nil, length: nil, limits: nil,
32
+ motor: nil, spring: nil, **options)
33
+ point_a = anchor_a || body_a.position
34
+ point_b = anchor_b || body_b.position
35
+ create_typed_joint(:distance, body_a:, body_b:, options:) do |definition|
36
+ definition[:localAnchorA] = local_point(body_a, point_a)
37
+ definition[:localAnchorB] = local_point(body_b, point_b)
38
+ definition[:length] = length ? ValueConversion.positive_float(length, label: "length") : distance(point_a, point_b)
39
+ configure_limits(definition, limits, :minLength, :maxLength)
40
+ configure_motor(definition, motor, maximum: :maxMotorForce)
41
+ configure_spring(definition, spring)
42
+ end
43
+ end
44
+
45
+ def create_mouse_joint(body_a:, body_b:, target:, max_force:, hertz: 5.0, damping_ratio: 0.7, **options)
46
+ create_typed_joint(:mouse, body_a:, body_b:, options:) do |definition|
47
+ definition[:target] = ValueConversion.native_vec2(target, label: "target")
48
+ definition[:maxForce] = ValueConversion.positive_float(max_force, label: "max_force")
49
+ definition[:hertz] = ValueConversion.non_negative_float(hertz, label: "hertz")
50
+ definition[:dampingRatio] = ValueConversion.non_negative_float(damping_ratio, label: "damping_ratio")
51
+ end
52
+ end
53
+
54
+ def create_weld_joint(body_a:, body_b:, anchor:, linear: nil, angular: nil, **options)
55
+ create_typed_joint(:weld, body_a:, body_b:, options:) do |definition|
56
+ definition[:localAnchorA] = local_point(body_a, anchor)
57
+ definition[:localAnchorB] = local_point(body_b, anchor)
58
+ definition[:referenceAngle] = body_b.angle - body_a.angle
59
+ configure_weld_spring(definition, linear, :linear)
60
+ configure_weld_spring(definition, angular, :angular)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ JOINT_TYPES = {
67
+ revolute: [Native.method(:b2DefaultRevoluteJointDef), Native.method(:b2CreateRevoluteJoint), RevoluteJoint],
68
+ prismatic: [Native.method(:b2DefaultPrismaticJointDef), Native.method(:b2CreatePrismaticJoint), PrismaticJoint],
69
+ distance: [Native.method(:b2DefaultDistanceJointDef), Native.method(:b2CreateDistanceJoint), DistanceJoint],
70
+ mouse: [Native.method(:b2DefaultMouseJointDef), Native.method(:b2CreateMouseJoint), MouseJoint],
71
+ weld: [Native.method(:b2DefaultWeldJointDef), Native.method(:b2CreateWeldJoint), WeldJoint]
72
+ }.freeze
73
+
74
+ def create_typed_joint(type, body_a:, body_b:, options:)
75
+ ensure_access!
76
+ validate_joint_bodies(body_a, body_b)
77
+ default_function, create_function, wrapper_class = JOINT_TYPES.fetch(type)
78
+ definition = default_function.call
79
+ definition[:bodyIdA] = body_a.id
80
+ definition[:bodyIdB] = body_b.id
81
+ definition[:collideConnected] = !!options.fetch(:collide_connected, false)
82
+ yield definition
83
+
84
+ joint = wrapper_class.new(
85
+ self,
86
+ create_function.call(@id, definition.pointer),
87
+ body_a:,
88
+ body_b:,
89
+ user_data: options[:user_data]
90
+ )
91
+ body_a.register_joint(joint)
92
+ body_b.register_joint(joint)
93
+ register_joint(joint)
94
+ end
95
+
96
+ def validate_joint_bodies(body_a, body_b)
97
+ raise ArgumentError, "joint bodies must be distinct" if body_a.equal?(body_b)
98
+ raise ArgumentError, "joint bodies must belong to this world" unless body_a.world.equal?(self) && body_b.world.equal?(self)
99
+ raise UseAfterDestroyError, "joint body has been destroyed" unless body_a.valid? && body_b.valid?
100
+ end
101
+
102
+ def local_point(body, point)
103
+ Native.b2Body_GetLocalPoint(body.id, ValueConversion.native_vec2(point, label: "anchor"))
104
+ end
105
+
106
+ def configure_limits(definition, limits, lower_field, upper_field)
107
+ return unless limits
108
+
109
+ lower = ValueConversion.finite_float(limits.begin, label: "lower_limit")
110
+ upper = ValueConversion.finite_float(limits.end, label: "upper_limit")
111
+ raise ArgumentError, "lower limit must not exceed upper limit" if lower > upper
112
+
113
+ definition[:enableLimit] = true
114
+ definition[lower_field] = lower
115
+ definition[upper_field] = upper
116
+ end
117
+
118
+ def configure_motor(definition, motor, maximum:)
119
+ return unless motor
120
+
121
+ definition[:enableMotor] = true
122
+ definition[:motorSpeed] = ValueConversion.finite_float(motor.fetch(:speed, 0), label: "motor.speed")
123
+ definition[maximum] = ValueConversion.non_negative_float(
124
+ motor.fetch(maximum == :maxMotorTorque ? :max_torque : :max_force),
125
+ label: "motor maximum"
126
+ )
127
+ end
128
+
129
+ def configure_spring(definition, spring)
130
+ return unless spring
131
+
132
+ definition[:enableSpring] = true
133
+ definition[:hertz] = ValueConversion.non_negative_float(spring.fetch(:hertz), label: "spring.hertz")
134
+ definition[:dampingRatio] = ValueConversion.non_negative_float(
135
+ spring.fetch(:damping_ratio, 0.7),
136
+ label: "spring.damping_ratio"
137
+ )
138
+ end
139
+
140
+ def configure_weld_spring(definition, spring, prefix)
141
+ return unless spring
142
+
143
+ definition[:"#{prefix}Hertz"] = ValueConversion.non_negative_float(spring.fetch(:hertz), label: "#{prefix}.hertz")
144
+ definition[:"#{prefix}DampingRatio"] = ValueConversion.non_negative_float(
145
+ spring.fetch(:damping_ratio, 0.7),
146
+ label: "#{prefix}.damping_ratio"
147
+ )
148
+ end
149
+
150
+ def distance(point_a, point_b)
151
+ vector_a = ValueConversion.native_vec2(point_a, label: "anchor_a")
152
+ vector_b = ValueConversion.native_vec2(point_b, label: "anchor_b")
153
+ Math.hypot(vector_b[:x] - vector_a[:x], vector_b[:y] - vector_a[:y])
154
+ end
155
+ end
156
+ end