three-rb 0.1.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 (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE +21 -0
  4. data/README.md +153 -0
  5. data/docs/browser-runtime.md +153 -0
  6. data/docs/implementation-plan.md +874 -0
  7. data/docs/loaded-assets-design.md +400 -0
  8. data/docs/next-work.md +107 -0
  9. data/docs/publishing.md +64 -0
  10. data/docs/release-readiness.md +83 -0
  11. data/examples/browser/README.md +54 -0
  12. data/examples/browser/assets/animated_triangle.gltf +123 -0
  13. data/examples/browser/assets/checker.svg +11 -0
  14. data/examples/browser/assets/compressed_triangle.gltf +74 -0
  15. data/examples/browser/assets/studio.hdr +5 -0
  16. data/examples/browser/assets/triangle.gltf +67 -0
  17. data/examples/browser/composition/README.md +35 -0
  18. data/examples/browser/composition/boot.mjs +6 -0
  19. data/examples/browser/composition/index.html +136 -0
  20. data/examples/browser/composition/main.rb +216 -0
  21. data/examples/browser/composition/smoke_test.mjs +266 -0
  22. data/examples/browser/cube/README.md +41 -0
  23. data/examples/browser/cube/boot.mjs +6 -0
  24. data/examples/browser/cube/index.html +142 -0
  25. data/examples/browser/cube/main.rb +62 -0
  26. data/examples/browser/cube/smoke_test.mjs +99 -0
  27. data/examples/browser/cubemap/README.md +23 -0
  28. data/examples/browser/cubemap/boot.mjs +6 -0
  29. data/examples/browser/cubemap/index.html +142 -0
  30. data/examples/browser/cubemap/main.rb +84 -0
  31. data/examples/browser/cubemap/smoke_test.mjs +91 -0
  32. data/examples/browser/gltf/README.md +23 -0
  33. data/examples/browser/gltf/boot.mjs +6 -0
  34. data/examples/browser/gltf/index.html +142 -0
  35. data/examples/browser/gltf/main.rb +105 -0
  36. data/examples/browser/gltf/smoke_test.mjs +162 -0
  37. data/examples/browser/picking/README.md +33 -0
  38. data/examples/browser/picking/boot.mjs +6 -0
  39. data/examples/browser/picking/index.html +142 -0
  40. data/examples/browser/picking/main.rb +113 -0
  41. data/examples/browser/picking/smoke_test.mjs +78 -0
  42. data/examples/browser/postprocessing/README.md +26 -0
  43. data/examples/browser/postprocessing/boot.mjs +6 -0
  44. data/examples/browser/postprocessing/index.html +142 -0
  45. data/examples/browser/postprocessing/main.rb +117 -0
  46. data/examples/browser/postprocessing/smoke_test.mjs +121 -0
  47. data/examples/browser/primitives/README.md +33 -0
  48. data/examples/browser/primitives/boot.mjs +6 -0
  49. data/examples/browser/primitives/index.html +142 -0
  50. data/examples/browser/primitives/main.rb +116 -0
  51. data/examples/browser/primitives/smoke_test.mjs +113 -0
  52. data/examples/browser/serialization/README.md +33 -0
  53. data/examples/browser/serialization/boot.mjs +6 -0
  54. data/examples/browser/serialization/index.html +142 -0
  55. data/examples/browser/serialization/main.rb +97 -0
  56. data/examples/browser/serialization/smoke_test.mjs +67 -0
  57. data/examples/browser/shared/boot.mjs +79 -0
  58. data/examples/browser/shared/smoke_test_helpers.mjs +151 -0
  59. data/examples/browser/textures/README.md +35 -0
  60. data/examples/browser/textures/boot.mjs +6 -0
  61. data/examples/browser/textures/index.html +142 -0
  62. data/examples/browser/textures/main.rb +142 -0
  63. data/examples/browser/textures/smoke_test.mjs +189 -0
  64. data/lib/three/animation/animation_action.rb +57 -0
  65. data/lib/three/animation/animation_clip.rb +22 -0
  66. data/lib/three/animation/animation_mixer.rb +43 -0
  67. data/lib/three/backends/base.rb +87 -0
  68. data/lib/three/backends/threejs/materialization.rb +143 -0
  69. data/lib/three/backends/threejs/parameters.rb +97 -0
  70. data/lib/three/backends/threejs/resource_management.rb +69 -0
  71. data/lib/three/backends/threejs/ruby_wasm_adapter.rb +873 -0
  72. data/lib/three/backends/threejs/synchronization.rb +224 -0
  73. data/lib/three/backends/threejs.rb +365 -0
  74. data/lib/three/cameras/camera.rb +39 -0
  75. data/lib/three/cameras/orthographic_camera.rb +107 -0
  76. data/lib/three/cameras/perspective_camera.rb +137 -0
  77. data/lib/three/constants.rb +40 -0
  78. data/lib/three/controls/orbit_controls.rb +118 -0
  79. data/lib/three/core/buffer_attribute.rb +151 -0
  80. data/lib/three/core/buffer_geometry.rb +181 -0
  81. data/lib/three/core/clock.rb +58 -0
  82. data/lib/three/core/event_dispatcher.rb +57 -0
  83. data/lib/three/core/layers.rb +75 -0
  84. data/lib/three/core/object3d.rb +331 -0
  85. data/lib/three/core/raycaster.rb +73 -0
  86. data/lib/three/dirty.rb +58 -0
  87. data/lib/three/exporters/three_json_exporter.rb +187 -0
  88. data/lib/three/geometries/box_geometry.rb +97 -0
  89. data/lib/three/geometries/plane_geometry.rb +70 -0
  90. data/lib/three/geometries/sphere_geometry.rb +107 -0
  91. data/lib/three/lights/ambient_light.rb +12 -0
  92. data/lib/three/lights/directional_light.rb +38 -0
  93. data/lib/three/lights/hemisphere_light.rb +34 -0
  94. data/lib/three/lights/light.rb +85 -0
  95. data/lib/three/lights/point_light.rb +33 -0
  96. data/lib/three/loaders/cube_texture_loader.rb +13 -0
  97. data/lib/three/loaders/gltf_loader.rb +48 -0
  98. data/lib/three/loaders/rgbe_loader.rb +15 -0
  99. data/lib/three/loaders/texture_loader.rb +13 -0
  100. data/lib/three/loaders/three_json_loader.rb +409 -0
  101. data/lib/three/materials/line_basic_material.rb +65 -0
  102. data/lib/three/materials/material.rb +158 -0
  103. data/lib/three/materials/mesh_basic_material.rb +64 -0
  104. data/lib/three/materials/mesh_lambert_material.rb +71 -0
  105. data/lib/three/materials/mesh_matcap_material.rb +86 -0
  106. data/lib/three/materials/mesh_normal_material.rb +42 -0
  107. data/lib/three/materials/mesh_phong_material.rb +119 -0
  108. data/lib/three/materials/mesh_physical_material.rb +155 -0
  109. data/lib/three/materials/mesh_standard_material.rb +149 -0
  110. data/lib/three/materials/mesh_toon_material.rb +98 -0
  111. data/lib/three/materials/points_material.rb +74 -0
  112. data/lib/three/materials/shadow_material.rb +45 -0
  113. data/lib/three/materials/sprite_material.rb +75 -0
  114. data/lib/three/math/color.rb +133 -0
  115. data/lib/three/math/euler.rb +197 -0
  116. data/lib/three/math/math_utils.rb +36 -0
  117. data/lib/three/math/matrix3.rb +255 -0
  118. data/lib/three/math/matrix4.rb +448 -0
  119. data/lib/three/math/quaternion.rb +277 -0
  120. data/lib/three/math/vector2.rb +95 -0
  121. data/lib/three/math/vector3.rb +396 -0
  122. data/lib/three/objects/external_object3d.rb +28 -0
  123. data/lib/three/objects/group.rb +12 -0
  124. data/lib/three/objects/instanced_mesh.rb +110 -0
  125. data/lib/three/objects/line.rb +41 -0
  126. data/lib/three/objects/mesh.rb +45 -0
  127. data/lib/three/objects/points.rb +41 -0
  128. data/lib/three/objects/sprite.rb +57 -0
  129. data/lib/three/postprocessing/dot_screen_pass.rb +83 -0
  130. data/lib/three/postprocessing/effect_composer.rb +56 -0
  131. data/lib/three/postprocessing/output_pass.rb +40 -0
  132. data/lib/three/postprocessing/render_pass.rb +42 -0
  133. data/lib/three/postprocessing/unreal_bloom_pass.rb +56 -0
  134. data/lib/three/renderers/renderer.rb +11 -0
  135. data/lib/three/renderers/threejs_renderer.rb +85 -0
  136. data/lib/three/scenes/scene.rb +29 -0
  137. data/lib/three/textures/cube_texture.rb +72 -0
  138. data/lib/three/textures/rgbe_texture.rb +45 -0
  139. data/lib/three/textures/texture.rb +200 -0
  140. data/lib/three/version.rb +5 -0
  141. data/lib/three-rb.rb +3 -0
  142. data/lib/three.rb +77 -0
  143. data/package.json +30 -0
  144. data/pnpm-lock.yaml +86 -0
  145. metadata +216 -0
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "math_utils"
4
+ require_relative "matrix4"
5
+
6
+ module Three
7
+ class Euler
8
+ include Enumerable
9
+
10
+ DEFAULT_ORDER = "XYZ"
11
+ ORDERS = %w[XYZ YXZ ZXY ZYX YZX XZY].freeze
12
+
13
+ attr_reader :x, :y, :z, :order
14
+
15
+ def initialize(x = 0, y = 0, z = 0, order = DEFAULT_ORDER)
16
+ validate_order!(order)
17
+
18
+ @x = x
19
+ @y = y
20
+ @z = z
21
+ @order = order
22
+ @on_change_callback = proc {}
23
+ end
24
+
25
+ def x=(value)
26
+ @x = value
27
+ changed!
28
+ end
29
+
30
+ def y=(value)
31
+ @y = value
32
+ changed!
33
+ end
34
+
35
+ def z=(value)
36
+ @z = value
37
+ changed!
38
+ end
39
+
40
+ def order=(value)
41
+ validate_order!(value)
42
+ @order = value
43
+ changed!
44
+ end
45
+
46
+ def set(x, y, z, order = @order, update: true)
47
+ validate_order!(order)
48
+
49
+ @x = x
50
+ @y = y
51
+ @z = z
52
+ @order = order
53
+ changed! if update
54
+ self
55
+ end
56
+
57
+ def clone
58
+ self.class.new(@x, @y, @z, @order)
59
+ end
60
+
61
+ def copy(euler, update: true)
62
+ set(euler.x, euler.y, euler.z, euler.order, update: update)
63
+ end
64
+
65
+ def set_from_rotation_matrix(matrix, order = @order, update: true)
66
+ validate_order!(order)
67
+
68
+ elements = matrix.elements
69
+ m11 = elements[0]
70
+ m12 = elements[4]
71
+ m13 = elements[8]
72
+ m21 = elements[1]
73
+ m22 = elements[5]
74
+ m23 = elements[9]
75
+ m31 = elements[2]
76
+ m32 = elements[6]
77
+ m33 = elements[10]
78
+
79
+ case order
80
+ when "XYZ"
81
+ @y = Math.asin(MathUtils.clamp(m13, -1, 1))
82
+ if m13.abs < 0.9999999
83
+ @x = Math.atan2(-m23, m33)
84
+ @z = Math.atan2(-m12, m11)
85
+ else
86
+ @x = Math.atan2(m32, m22)
87
+ @z = 0
88
+ end
89
+ when "YXZ"
90
+ @x = Math.asin(-MathUtils.clamp(m23, -1, 1))
91
+ if m23.abs < 0.9999999
92
+ @y = Math.atan2(m13, m33)
93
+ @z = Math.atan2(m21, m22)
94
+ else
95
+ @y = Math.atan2(-m31, m11)
96
+ @z = 0
97
+ end
98
+ when "ZXY"
99
+ @x = Math.asin(MathUtils.clamp(m32, -1, 1))
100
+ if m32.abs < 0.9999999
101
+ @y = Math.atan2(-m31, m33)
102
+ @z = Math.atan2(-m12, m22)
103
+ else
104
+ @y = 0
105
+ @z = Math.atan2(m21, m11)
106
+ end
107
+ when "ZYX"
108
+ @y = Math.asin(-MathUtils.clamp(m31, -1, 1))
109
+ if m31.abs < 0.9999999
110
+ @x = Math.atan2(m32, m33)
111
+ @z = Math.atan2(m21, m11)
112
+ else
113
+ @x = 0
114
+ @z = Math.atan2(-m12, m22)
115
+ end
116
+ when "YZX"
117
+ @z = Math.asin(MathUtils.clamp(m21, -1, 1))
118
+ if m21.abs < 0.9999999
119
+ @x = Math.atan2(-m23, m22)
120
+ @y = Math.atan2(-m31, m11)
121
+ else
122
+ @x = 0
123
+ @y = Math.atan2(m13, m33)
124
+ end
125
+ when "XZY"
126
+ @z = Math.asin(-MathUtils.clamp(m12, -1, 1))
127
+ if m12.abs < 0.9999999
128
+ @x = Math.atan2(m32, m22)
129
+ @y = Math.atan2(m13, m11)
130
+ else
131
+ @x = Math.atan2(-m23, m33)
132
+ @y = 0
133
+ end
134
+ end
135
+
136
+ @order = order
137
+ changed! if update
138
+ self
139
+ end
140
+
141
+ def set_from_quaternion(quaternion, order = @order, update: true)
142
+ matrix = Matrix4.new.make_rotation_from_quaternion(quaternion)
143
+ set_from_rotation_matrix(matrix, order, update: update)
144
+ end
145
+
146
+ def equals?(euler, epsilon: 0.0)
147
+ @order == euler.order &&
148
+ (@x - euler.x).abs <= epsilon &&
149
+ (@y - euler.y).abs <= epsilon &&
150
+ (@z - euler.z).abs <= epsilon
151
+ end
152
+
153
+ def ==(other)
154
+ other.is_a?(self.class) && equals?(other)
155
+ end
156
+
157
+ def on_change(&callback)
158
+ @on_change_callback = callback || proc {}
159
+ self
160
+ end
161
+
162
+ alias _on_change on_change
163
+
164
+ def each
165
+ return enum_for(:each) unless block_given?
166
+
167
+ yield @x
168
+ yield @y
169
+ yield @z
170
+ yield @order
171
+ end
172
+
173
+ def to_a
174
+ [@x, @y, @z, @order]
175
+ end
176
+
177
+ def deconstruct
178
+ to_a
179
+ end
180
+
181
+ def inspect
182
+ "#<#{self.class} x=#{@x.inspect} y=#{@y.inspect} z=#{@z.inspect} order=#{@order.inspect}>"
183
+ end
184
+
185
+ private
186
+
187
+ def validate_order!(order)
188
+ return if ORDERS.include?(order)
189
+
190
+ raise ArgumentError, "unknown Euler order: #{order}"
191
+ end
192
+
193
+ def changed!
194
+ @on_change_callback.call
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Three
6
+ module MathUtils
7
+ DEG2RAD = Math::PI / 180.0
8
+ RAD2DEG = 180.0 / Math::PI
9
+
10
+ module_function
11
+
12
+ def clamp(value, min, max)
13
+ [[value, min].max, max].min
14
+ end
15
+
16
+ def euclidean_modulo(n, m)
17
+ ((n % m) + m) % m
18
+ end
19
+
20
+ def lerp(x, y, t)
21
+ (1 - t) * x + t * y
22
+ end
23
+
24
+ def deg_to_rad(degrees)
25
+ degrees * DEG2RAD
26
+ end
27
+
28
+ def rad_to_deg(radians)
29
+ radians * RAD2DEG
30
+ end
31
+
32
+ def generate_uuid
33
+ SecureRandom.uuid
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Three
4
+ class Matrix3
5
+ attr_reader :elements
6
+
7
+ def initialize(*values)
8
+ @elements = [
9
+ 1, 0, 0,
10
+ 0, 1, 0,
11
+ 0, 0, 1
12
+ ]
13
+
14
+ set(*values) unless values.empty?
15
+ end
16
+
17
+ def set(n11, n12, n13, n21, n22, n23, n31, n32, n33)
18
+ @elements[0] = n11
19
+ @elements[3] = n12
20
+ @elements[6] = n13
21
+ @elements[1] = n21
22
+ @elements[4] = n22
23
+ @elements[7] = n23
24
+ @elements[2] = n31
25
+ @elements[5] = n32
26
+ @elements[8] = n33
27
+ self
28
+ end
29
+
30
+ def identity
31
+ set(
32
+ 1, 0, 0,
33
+ 0, 1, 0,
34
+ 0, 0, 1
35
+ )
36
+ end
37
+
38
+ def clone
39
+ self.class.new.from_array(@elements)
40
+ end
41
+
42
+ def copy(matrix)
43
+ @elements = matrix.elements.dup
44
+ self
45
+ end
46
+
47
+ def set_from_matrix4(matrix)
48
+ me = matrix.elements
49
+ set(
50
+ me[0], me[4], me[8],
51
+ me[1], me[5], me[9],
52
+ me[2], me[6], me[10]
53
+ )
54
+ end
55
+
56
+ def multiply(matrix)
57
+ multiply_matrices(self, matrix)
58
+ end
59
+
60
+ def premultiply(matrix)
61
+ multiply_matrices(matrix, self)
62
+ end
63
+
64
+ def multiply_matrices(a, b)
65
+ ae = a.elements
66
+ be = b.elements
67
+
68
+ a11 = ae[0]
69
+ a12 = ae[3]
70
+ a13 = ae[6]
71
+ a21 = ae[1]
72
+ a22 = ae[4]
73
+ a23 = ae[7]
74
+ a31 = ae[2]
75
+ a32 = ae[5]
76
+ a33 = ae[8]
77
+
78
+ b11 = be[0]
79
+ b12 = be[3]
80
+ b13 = be[6]
81
+ b21 = be[1]
82
+ b22 = be[4]
83
+ b23 = be[7]
84
+ b31 = be[2]
85
+ b32 = be[5]
86
+ b33 = be[8]
87
+
88
+ @elements[0] = a11 * b11 + a12 * b21 + a13 * b31
89
+ @elements[3] = a11 * b12 + a12 * b22 + a13 * b32
90
+ @elements[6] = a11 * b13 + a12 * b23 + a13 * b33
91
+
92
+ @elements[1] = a21 * b11 + a22 * b21 + a23 * b31
93
+ @elements[4] = a21 * b12 + a22 * b22 + a23 * b32
94
+ @elements[7] = a21 * b13 + a22 * b23 + a23 * b33
95
+
96
+ @elements[2] = a31 * b11 + a32 * b21 + a33 * b31
97
+ @elements[5] = a31 * b12 + a32 * b22 + a33 * b32
98
+ @elements[8] = a31 * b13 + a32 * b23 + a33 * b33
99
+ self
100
+ end
101
+
102
+ def multiply_scalar(scalar)
103
+ @elements.map! { |value| value * scalar }
104
+ self
105
+ end
106
+
107
+ def determinant
108
+ a = @elements[0]
109
+ b = @elements[1]
110
+ c = @elements[2]
111
+ d = @elements[3]
112
+ e = @elements[4]
113
+ f = @elements[5]
114
+ g = @elements[6]
115
+ h = @elements[7]
116
+ i = @elements[8]
117
+
118
+ a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g
119
+ end
120
+
121
+ def invert
122
+ n11 = @elements[0]
123
+ n21 = @elements[1]
124
+ n31 = @elements[2]
125
+ n12 = @elements[3]
126
+ n22 = @elements[4]
127
+ n32 = @elements[5]
128
+ n13 = @elements[6]
129
+ n23 = @elements[7]
130
+ n33 = @elements[8]
131
+
132
+ t11 = n33 * n22 - n32 * n23
133
+ t12 = n32 * n13 - n33 * n12
134
+ t13 = n23 * n12 - n22 * n13
135
+ det = n11 * t11 + n21 * t12 + n31 * t13
136
+ return set(0, 0, 0, 0, 0, 0, 0, 0, 0) if det.zero?
137
+
138
+ det_inv = 1.0 / det
139
+
140
+ @elements[0] = t11 * det_inv
141
+ @elements[1] = (n31 * n23 - n33 * n21) * det_inv
142
+ @elements[2] = (n32 * n21 - n31 * n22) * det_inv
143
+ @elements[3] = t12 * det_inv
144
+ @elements[4] = (n33 * n11 - n31 * n13) * det_inv
145
+ @elements[5] = (n31 * n12 - n32 * n11) * det_inv
146
+ @elements[6] = t13 * det_inv
147
+ @elements[7] = (n21 * n13 - n23 * n11) * det_inv
148
+ @elements[8] = (n22 * n11 - n21 * n12) * det_inv
149
+ self
150
+ end
151
+
152
+ def transpose
153
+ @elements[1], @elements[3] = @elements[3], @elements[1]
154
+ @elements[2], @elements[6] = @elements[6], @elements[2]
155
+ @elements[5], @elements[7] = @elements[7], @elements[5]
156
+ self
157
+ end
158
+
159
+ def get_normal_matrix(matrix4)
160
+ set_from_matrix4(matrix4).invert.transpose
161
+ end
162
+
163
+ def set_uv_transform(tx, ty, sx, sy, rotation, cx, cy)
164
+ c = Math.cos(rotation)
165
+ s = Math.sin(rotation)
166
+
167
+ set(
168
+ sx * c, sx * s, -sx * (c * cx + s * cy) + cx + tx,
169
+ -sy * s, sy * c, -sy * (-s * cx + c * cy) + cy + ty,
170
+ 0, 0, 1
171
+ )
172
+ end
173
+
174
+ def scale(sx, sy)
175
+ @elements[0] *= sx
176
+ @elements[3] *= sx
177
+ @elements[6] *= sx
178
+ @elements[1] *= sy
179
+ @elements[4] *= sy
180
+ @elements[7] *= sy
181
+ self
182
+ end
183
+
184
+ def rotate(theta)
185
+ c = Math.cos(theta)
186
+ s = Math.sin(theta)
187
+
188
+ a11 = @elements[0]
189
+ a12 = @elements[3]
190
+ a13 = @elements[6]
191
+ a21 = @elements[1]
192
+ a22 = @elements[4]
193
+ a23 = @elements[7]
194
+
195
+ @elements[0] = c * a11 + s * a21
196
+ @elements[3] = c * a12 + s * a22
197
+ @elements[6] = c * a13 + s * a23
198
+ @elements[1] = -s * a11 + c * a21
199
+ @elements[4] = -s * a12 + c * a22
200
+ @elements[7] = -s * a13 + c * a23
201
+ self
202
+ end
203
+
204
+ def translate(tx, ty)
205
+ @elements[0] += tx * @elements[2]
206
+ @elements[3] += tx * @elements[5]
207
+ @elements[6] += tx * @elements[8]
208
+ @elements[1] += ty * @elements[2]
209
+ @elements[4] += ty * @elements[5]
210
+ @elements[7] += ty * @elements[8]
211
+ self
212
+ end
213
+
214
+ def transpose_into_array(array = [])
215
+ array[0] = @elements[0]
216
+ array[1] = @elements[3]
217
+ array[2] = @elements[6]
218
+ array[3] = @elements[1]
219
+ array[4] = @elements[4]
220
+ array[5] = @elements[7]
221
+ array[6] = @elements[2]
222
+ array[7] = @elements[5]
223
+ array[8] = @elements[8]
224
+ array
225
+ end
226
+
227
+ def from_array(array, offset = 0)
228
+ 9.times { |index| @elements[index] = array[index + offset] }
229
+ self
230
+ end
231
+
232
+ def to_a
233
+ @elements.dup
234
+ end
235
+
236
+ def to_array(array = [], offset = 0)
237
+ 9.times { |index| array[index + offset] = @elements[index] }
238
+ array
239
+ end
240
+
241
+ def equals?(matrix, epsilon: 0.0)
242
+ @elements.each_with_index.all? do |value, index|
243
+ (value - matrix.elements[index]).abs <= epsilon
244
+ end
245
+ end
246
+
247
+ def ==(other)
248
+ other.is_a?(self.class) && equals?(other)
249
+ end
250
+
251
+ def inspect
252
+ "#<#{self.class} elements=#{@elements.inspect}>"
253
+ end
254
+ end
255
+ end