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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +336 -0
- data/examples/contact_events.rb +22 -0
- data/examples/debug_draw.rb +15 -0
- data/examples/falling_ball.rb +19 -0
- data/examples/rugl_debug_draw.rb +68 -0
- data/examples/stagecraft_debug_draw.rb +64 -0
- data/examples/support/box2d_debug_lines.rb +125 -0
- data/ext/box2d/CMakeLists.txt +35 -0
- data/ext/box2d/extconf.rb +42 -0
- data/ext/box2d/native.c +9 -0
- data/ext/box2d/vendor/box2d/LICENSE +21 -0
- data/ext/box2d/vendor/box2d/VERSION +1 -0
- data/ext/box2d/vendor/box2d/include/box2d/base.h +131 -0
- data/ext/box2d/vendor/box2d/include/box2d/box2d.h +1222 -0
- data/ext/box2d/vendor/box2d/include/box2d/collision.h +830 -0
- data/ext/box2d/vendor/box2d/include/box2d/id.h +144 -0
- data/ext/box2d/vendor/box2d/include/box2d/math_functions.h +761 -0
- data/ext/box2d/vendor/box2d/include/box2d/types.h +1457 -0
- data/ext/box2d/vendor/box2d/src/CMakeLists.txt +223 -0
- data/ext/box2d/vendor/box2d/src/aabb.c +132 -0
- data/ext/box2d/vendor/box2d/src/aabb.h +56 -0
- data/ext/box2d/vendor/box2d/src/arena_allocator.c +112 -0
- data/ext/box2d/vendor/box2d/src/arena_allocator.h +48 -0
- data/ext/box2d/vendor/box2d/src/array.c +8 -0
- data/ext/box2d/vendor/box2d/src/array.h +179 -0
- data/ext/box2d/vendor/box2d/src/atomic.h +79 -0
- data/ext/box2d/vendor/box2d/src/bitset.c +67 -0
- data/ext/box2d/vendor/box2d/src/bitset.h +65 -0
- data/ext/box2d/vendor/box2d/src/body.c +1884 -0
- data/ext/box2d/vendor/box2d/src/body.h +194 -0
- data/ext/box2d/vendor/box2d/src/box2d.natvis +41 -0
- data/ext/box2d/vendor/box2d/src/broad_phase.c +524 -0
- data/ext/box2d/vendor/box2d/src/broad_phase.h +83 -0
- data/ext/box2d/vendor/box2d/src/constants.h +54 -0
- data/ext/box2d/vendor/box2d/src/constraint_graph.c +322 -0
- data/ext/box2d/vendor/box2d/src/constraint_graph.h +58 -0
- data/ext/box2d/vendor/box2d/src/contact.c +650 -0
- data/ext/box2d/vendor/box2d/src/contact.h +148 -0
- data/ext/box2d/vendor/box2d/src/contact_solver.c +2120 -0
- data/ext/box2d/vendor/box2d/src/contact_solver.h +54 -0
- data/ext/box2d/vendor/box2d/src/core.c +178 -0
- data/ext/box2d/vendor/box2d/src/core.h +149 -0
- data/ext/box2d/vendor/box2d/src/ctz.h +112 -0
- data/ext/box2d/vendor/box2d/src/distance.c +1415 -0
- data/ext/box2d/vendor/box2d/src/distance_joint.c +556 -0
- data/ext/box2d/vendor/box2d/src/dynamic_tree.c +1989 -0
- data/ext/box2d/vendor/box2d/src/geometry.c +1028 -0
- data/ext/box2d/vendor/box2d/src/hull.c +328 -0
- data/ext/box2d/vendor/box2d/src/id_pool.c +79 -0
- data/ext/box2d/vendor/box2d/src/id_pool.h +35 -0
- data/ext/box2d/vendor/box2d/src/island.c +977 -0
- data/ext/box2d/vendor/box2d/src/island.h +89 -0
- data/ext/box2d/vendor/box2d/src/joint.c +1272 -0
- data/ext/box2d/vendor/box2d/src/joint.h +335 -0
- data/ext/box2d/vendor/box2d/src/manifold.c +1726 -0
- data/ext/box2d/vendor/box2d/src/math_functions.c +159 -0
- data/ext/box2d/vendor/box2d/src/motor_joint.c +283 -0
- data/ext/box2d/vendor/box2d/src/mouse_joint.c +214 -0
- data/ext/box2d/vendor/box2d/src/mover.c +73 -0
- data/ext/box2d/vendor/box2d/src/prismatic_joint.c +656 -0
- data/ext/box2d/vendor/box2d/src/revolute_joint.c +534 -0
- data/ext/box2d/vendor/box2d/src/sensor.c +389 -0
- data/ext/box2d/vendor/box2d/src/sensor.h +36 -0
- data/ext/box2d/vendor/box2d/src/shape.c +1714 -0
- data/ext/box2d/vendor/box2d/src/shape.h +123 -0
- data/ext/box2d/vendor/box2d/src/solver.c +2038 -0
- data/ext/box2d/vendor/box2d/src/solver.h +155 -0
- data/ext/box2d/vendor/box2d/src/solver_set.c +613 -0
- data/ext/box2d/vendor/box2d/src/solver_set.h +57 -0
- data/ext/box2d/vendor/box2d/src/table.c +238 -0
- data/ext/box2d/vendor/box2d/src/table.h +37 -0
- data/ext/box2d/vendor/box2d/src/timer.c +185 -0
- data/ext/box2d/vendor/box2d/src/types.c +151 -0
- data/ext/box2d/vendor/box2d/src/weld_joint.c +310 -0
- data/ext/box2d/vendor/box2d/src/wheel_joint.c +551 -0
- data/ext/box2d/vendor/box2d/src/world.c +3301 -0
- data/ext/box2d/vendor/box2d/src/world.h +192 -0
- data/generator/generate.rb +316 -0
- data/generator/layout_probe.c +507 -0
- data/generator/verify_layouts.rb +32 -0
- data/lib/box2d/body.rb +172 -0
- data/lib/box2d/body_definition.rb +38 -0
- data/lib/box2d/body_shapes.rb +135 -0
- data/lib/box2d/chain.rb +61 -0
- data/lib/box2d/debug_draw.rb +118 -0
- data/lib/box2d/events.rb +103 -0
- data/lib/box2d/fixed_stepper.rb +39 -0
- data/lib/box2d/handle.rb +46 -0
- data/lib/box2d/hit.rb +5 -0
- data/lib/box2d/joint.rb +197 -0
- data/lib/box2d/native.rb +1462 -0
- data/lib/box2d/native_loader.rb +45 -0
- data/lib/box2d/pixel_scale.rb +29 -0
- data/lib/box2d/shape.rb +109 -0
- data/lib/box2d/shape_definition.rb +44 -0
- data/lib/box2d/value_conversion.rb +80 -0
- data/lib/box2d/version.rb +7 -0
- data/lib/box2d/world.rb +135 -0
- data/lib/box2d/world_joints.rb +156 -0
- data/lib/box2d/world_queries.rb +122 -0
- data/lib/box2d/world_registry.rb +95 -0
- data/lib/box2d.rb +31 -0
- data/script/build_platform_gem.rb +24 -0
- data/script/deterministic_scene.rb +56 -0
- data/script/update_deterministic_snapshot.rb +11 -0
- data/script/verify_platform_gem.rb +24 -0
- 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
|
data/lib/box2d/shape.rb
ADDED
|
@@ -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
|
data/lib/box2d/world.rb
ADDED
|
@@ -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
|