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,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../dirty"
4
+ require_relative "../math/math_utils"
5
+ require_relative "../math/vector3"
6
+ require_relative "buffer_attribute"
7
+ require_relative "event_dispatcher"
8
+
9
+ module Three
10
+ class BufferGeometry < EventDispatcher
11
+ include Dirty
12
+
13
+ @next_id = 0
14
+
15
+ class << self
16
+ attr_accessor :next_id
17
+ end
18
+
19
+ attr_reader :id, :uuid
20
+ attr_accessor :name, :type, :index, :attributes, :groups, :draw_range
21
+ attr_accessor :bounding_box, :bounding_sphere, :user_data
22
+
23
+ def initialize
24
+ super()
25
+ @id = self.class.allocate_id
26
+ @uuid = MathUtils.generate_uuid
27
+ @name = ""
28
+ @type = "BufferGeometry"
29
+ @index = nil
30
+ @attributes = {}
31
+ @groups = []
32
+ @bounding_box = nil
33
+ @bounding_sphere = nil
34
+ @draw_range = { start: 0, count: Float::INFINITY }
35
+ @user_data = {}
36
+ mark_dirty!
37
+ end
38
+
39
+ def self.allocate_id
40
+ id = BufferGeometry.next_id
41
+ BufferGeometry.next_id += 1
42
+ id
43
+ end
44
+
45
+ def get_index
46
+ @index
47
+ end
48
+
49
+ def set_index(index)
50
+ @index =
51
+ if index.is_a?(Array)
52
+ attribute_class = index.any? { |value| value > 65_535 } ? Uint32BufferAttribute : Uint16BufferAttribute
53
+ attribute_class.new(index, 1)
54
+ else
55
+ index
56
+ end
57
+ bind_attribute_change(@index)
58
+ mark_dirty!(:index)
59
+ self
60
+ end
61
+
62
+ def get_attribute(name)
63
+ @attributes[name.to_sym]
64
+ end
65
+
66
+ def set_attribute(name, attribute)
67
+ @attributes[name.to_sym] = attribute
68
+ bind_attribute_change(attribute)
69
+ mark_dirty!(:attributes)
70
+ self
71
+ end
72
+
73
+ def delete_attribute(name)
74
+ @attributes.delete(name.to_sym)
75
+ mark_dirty!(:attributes)
76
+ self
77
+ end
78
+
79
+ def has_attribute?(name)
80
+ @attributes.key?(name.to_sym)
81
+ end
82
+
83
+ def add_group(start, count, material_index = 0)
84
+ @groups << { start: start, count: count, material_index: material_index }
85
+ mark_dirty!(:groups)
86
+ self
87
+ end
88
+
89
+ def clear_groups
90
+ @groups.clear
91
+ mark_dirty!(:groups)
92
+ self
93
+ end
94
+
95
+ def set_draw_range(start, count)
96
+ @draw_range[:start] = start
97
+ @draw_range[:count] = count
98
+ mark_dirty!(:draw_range)
99
+ self
100
+ end
101
+
102
+ def compute_bounding_box
103
+ position = get_attribute(:position)
104
+ @bounding_box = nil
105
+ return self unless position && position.count.positive?
106
+
107
+ min = Vector3.new(Float::INFINITY, Float::INFINITY, Float::INFINITY)
108
+ max = Vector3.new(-Float::INFINITY, -Float::INFINITY, -Float::INFINITY)
109
+
110
+ position.count.times do |index|
111
+ x = position.get_x(index)
112
+ y = position.get_y(index)
113
+ z = position.get_z(index)
114
+ min.x = [min.x, x].min
115
+ min.y = [min.y, y].min
116
+ min.z = [min.z, z].min
117
+ max.x = [max.x, x].max
118
+ max.y = [max.y, y].max
119
+ max.z = [max.z, z].max
120
+ end
121
+
122
+ @bounding_box = { min: min, max: max }
123
+ self
124
+ end
125
+
126
+ def compute_bounding_sphere
127
+ compute_bounding_box unless @bounding_box
128
+ position = get_attribute(:position)
129
+ @bounding_sphere = nil
130
+ return self unless position && @bounding_box
131
+
132
+ center = @bounding_box[:min].clone.add(@bounding_box[:max]).multiply_scalar(0.5)
133
+ max_radius_sq = 0
134
+
135
+ position.count.times do |index|
136
+ point = Vector3.new(position.get_x(index), position.get_y(index), position.get_z(index))
137
+ max_radius_sq = [max_radius_sq, center.distance_to_squared(point)].max
138
+ end
139
+
140
+ @bounding_sphere = { center: center, radius: Math.sqrt(max_radius_sq) }
141
+ self
142
+ end
143
+
144
+ def dispose
145
+ dispatch_event(:dispose)
146
+ end
147
+
148
+ def to_h
149
+ {
150
+ uuid: @uuid,
151
+ type: @type,
152
+ name: @name,
153
+ index: @index&.to_h,
154
+ attributes: @attributes.transform_values(&:to_h),
155
+ groups: @groups.map(&:dup),
156
+ bounding_box: serialize_bounds(@bounding_box),
157
+ bounding_sphere: serialize_sphere(@bounding_sphere)
158
+ }
159
+ end
160
+
161
+ private
162
+
163
+ def serialize_bounds(bounds)
164
+ return nil unless bounds
165
+
166
+ { min: bounds[:min].to_a, max: bounds[:max].to_a }
167
+ end
168
+
169
+ def serialize_sphere(sphere)
170
+ return nil unless sphere
171
+
172
+ { center: sphere[:center].to_a, radius: sphere[:radius] }
173
+ end
174
+
175
+ def bind_attribute_change(attribute)
176
+ return unless attribute.respond_to?(:on)
177
+
178
+ attribute.on(:change) { mark_dirty!(:attributes) }
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Three
4
+ class Clock
5
+ attr_accessor :auto_start
6
+ attr_reader :start_time, :old_time, :elapsed_time, :running
7
+
8
+ def initialize(auto_start: true, time_source: nil)
9
+ @auto_start = auto_start
10
+ @time_source = time_source || proc { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
11
+ @start_time = 0
12
+ @old_time = 0
13
+ @elapsed_time = 0
14
+ @running = false
15
+ end
16
+
17
+ def start
18
+ @start_time = now
19
+ @old_time = @start_time
20
+ @elapsed_time = 0
21
+ @running = true
22
+ self
23
+ end
24
+
25
+ def stop
26
+ get_elapsed_time
27
+ @running = false
28
+ @auto_start = false
29
+ self
30
+ end
31
+
32
+ def get_elapsed_time
33
+ get_delta
34
+ @elapsed_time
35
+ end
36
+
37
+ def get_delta
38
+ if @auto_start && !@running
39
+ start
40
+ return 0
41
+ end
42
+
43
+ return 0 unless @running
44
+
45
+ current_time = now
46
+ diff = current_time - @old_time
47
+ @old_time = current_time
48
+ @elapsed_time += diff
49
+ diff
50
+ end
51
+
52
+ private
53
+
54
+ def now
55
+ @time_source.call.to_f
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Three
4
+ class EventDispatcher
5
+ Event = Struct.new(:type, :target, :data, keyword_init: true)
6
+
7
+ def initialize
8
+ @listeners = Hash.new { |hash, key| hash[key] = [] }
9
+ end
10
+
11
+ def add_event_listener(type, listener = nil, &block)
12
+ callback = listener || block
13
+ raise ArgumentError, "listener or block is required" unless callback
14
+
15
+ listeners = @listeners[type.to_sym]
16
+ listeners << callback unless listeners.include?(callback)
17
+ self
18
+ end
19
+
20
+ alias on add_event_listener
21
+
22
+ def has_event_listener?(type, listener)
23
+ @listeners[type.to_sym].include?(listener)
24
+ end
25
+
26
+ def remove_event_listener(type, listener)
27
+ @listeners[type.to_sym].delete(listener)
28
+ self
29
+ end
30
+
31
+ alias off remove_event_listener
32
+
33
+ def dispatch_event(event, data = nil)
34
+ event = normalize_event(event, data)
35
+ listeners = @listeners[event.type.to_sym]
36
+ return self if listeners.empty?
37
+
38
+ event.target = self
39
+ listeners.dup.each { |listener| listener.call(event) }
40
+ event.target = nil
41
+ self
42
+ end
43
+
44
+ private
45
+
46
+ def normalize_event(event, data)
47
+ case event
48
+ when Event
49
+ event
50
+ when Hash
51
+ Event.new(type: event.fetch(:type), data: event)
52
+ else
53
+ Event.new(type: event, data: data)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Three
4
+ class Layers
5
+ ALL_MASK = 0xffffffff
6
+
7
+ attr_reader :mask
8
+
9
+ def initialize
10
+ @mask = 1
11
+ @on_change_callback = proc {}
12
+ end
13
+
14
+ def mask=(value)
15
+ @mask = value.to_i
16
+ changed!
17
+ end
18
+
19
+ def set(channel)
20
+ self.mask = bit(channel)
21
+ self
22
+ end
23
+
24
+ def enable(channel)
25
+ self.mask = @mask | bit(channel)
26
+ self
27
+ end
28
+
29
+ def enable_all
30
+ self.mask = ALL_MASK
31
+ self
32
+ end
33
+
34
+ def toggle(channel)
35
+ self.mask = @mask ^ bit(channel)
36
+ self
37
+ end
38
+
39
+ def disable(channel)
40
+ self.mask = @mask & ~bit(channel)
41
+ self
42
+ end
43
+
44
+ def disable_all
45
+ self.mask = 0
46
+ self
47
+ end
48
+
49
+ def test(layers)
50
+ (@mask & layers.mask) != 0
51
+ end
52
+
53
+ def is_enabled(channel)
54
+ (@mask & bit(channel)) != 0
55
+ end
56
+
57
+ def on_change(&callback)
58
+ @on_change_callback = callback || proc {}
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def bit(channel)
65
+ channel = Integer(channel)
66
+ raise RangeError, "layer channel must be between 0 and 31" unless channel.between?(0, 31)
67
+
68
+ 1 << channel
69
+ end
70
+
71
+ def changed!
72
+ @on_change_callback.call
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../math/euler"
4
+ require_relative "../math/math_utils"
5
+ require_relative "../math/matrix4"
6
+ require_relative "../math/quaternion"
7
+ require_relative "../math/vector3"
8
+ require_relative "../dirty"
9
+ require_relative "event_dispatcher"
10
+ require_relative "layers"
11
+
12
+ module Three
13
+ class Object3D < EventDispatcher
14
+ include Dirty
15
+
16
+ DEFAULT_UP = Vector3.new(0, 1, 0)
17
+ DEFAULT_MATRIX_AUTO_UPDATE = true
18
+ DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true
19
+
20
+ @next_id = 0
21
+
22
+ class << self
23
+ attr_accessor :next_id
24
+ end
25
+
26
+ attr_reader :id, :uuid, :parent, :children
27
+ attr_reader :layers
28
+ attr_reader :position, :rotation, :quaternion, :scale
29
+ attr_reader :matrix, :matrix_world
30
+ attr_reader :name, :type, :up, :visible, :cast_shadow, :receive_shadow
31
+ attr_accessor :matrix_auto_update, :matrix_world_auto_update, :matrix_world_needs_update
32
+ attr_accessor :user_data
33
+
34
+ def initialize
35
+ super
36
+
37
+ @id = self.class.allocate_id
38
+ @uuid = MathUtils.generate_uuid
39
+ @name = ""
40
+ @type = "Object3D"
41
+ @parent = nil
42
+ @children = []
43
+ @layers = Layers.new
44
+ @up = DEFAULT_UP.clone
45
+
46
+ @position = Vector3.new
47
+ @rotation = Euler.new
48
+ @quaternion = Quaternion.new
49
+ @scale = Vector3.new(1, 1, 1)
50
+ @matrix = Matrix4.new
51
+ @matrix_world = Matrix4.new
52
+ @matrix_auto_update = DEFAULT_MATRIX_AUTO_UPDATE
53
+ @matrix_world_auto_update = DEFAULT_MATRIX_WORLD_AUTO_UPDATE
54
+ @matrix_world_needs_update = false
55
+ @visible = true
56
+ @cast_shadow = false
57
+ @receive_shadow = false
58
+ @user_data = {}
59
+
60
+ bind_rotation_and_quaternion
61
+ bind_transform_changes
62
+ bind_layer_changes
63
+ mark_dirty!
64
+ end
65
+
66
+ def name=(value)
67
+ @name = value
68
+ mark_dirty!(:properties)
69
+ end
70
+
71
+ def type=(value)
72
+ @type = value
73
+ mark_dirty!(:properties)
74
+ end
75
+
76
+ def up=(value)
77
+ @up = value
78
+ mark_dirty!(:transform)
79
+ end
80
+
81
+ def visible=(value)
82
+ @visible = value
83
+ mark_dirty!(:properties)
84
+ end
85
+
86
+ def cast_shadow=(value)
87
+ @cast_shadow = value
88
+ mark_dirty!(:properties)
89
+ end
90
+
91
+ def receive_shadow=(value)
92
+ @receive_shadow = value
93
+ mark_dirty!(:properties)
94
+ end
95
+
96
+ def mark_dirty!(field = :all)
97
+ super
98
+ @parent&.mark_descendant_dirty!
99
+ self
100
+ end
101
+
102
+ def mark_descendant_dirty!
103
+ return self if dirty_fields.key?(:descendants)
104
+
105
+ dirty_fields[:descendants] = true
106
+ @parent&.mark_descendant_dirty!
107
+ self
108
+ end
109
+
110
+ def dirty_dependency_changed(_resource, _field)
111
+ mark_dirty!(:resources)
112
+ end
113
+
114
+ def self.allocate_id
115
+ id = Object3D.next_id
116
+ Object3D.next_id += 1
117
+ id
118
+ end
119
+
120
+ def add(*objects)
121
+ objects.each { |object| add_one(object) }
122
+ self
123
+ end
124
+
125
+ def remove(*objects)
126
+ objects.each { |object| remove_one(object) }
127
+ self
128
+ end
129
+
130
+ def remove_from_parent
131
+ @parent&.remove(self)
132
+ self
133
+ end
134
+
135
+ def clear
136
+ remove(*@children.dup)
137
+ end
138
+
139
+ def traverse(&block)
140
+ return enum_for(:traverse) unless block
141
+
142
+ block.call(self)
143
+ @children.each { |child| child.traverse(&block) }
144
+ self
145
+ end
146
+
147
+ def traverse_visible(&block)
148
+ return enum_for(:traverse_visible) unless block
149
+ return self unless @visible
150
+
151
+ block.call(self)
152
+ @children.each { |child| child.traverse_visible(&block) }
153
+ self
154
+ end
155
+
156
+ def traverse_ancestors(&block)
157
+ return enum_for(:traverse_ancestors) unless block
158
+ return self unless @parent
159
+
160
+ block.call(@parent)
161
+ @parent.traverse_ancestors(&block)
162
+ self
163
+ end
164
+
165
+ def get_object_by_id(id)
166
+ get_object_by_property(:id, id)
167
+ end
168
+
169
+ def get_object_by_name(name)
170
+ get_object_by_property(:name, name)
171
+ end
172
+
173
+ def get_object_by_property(property, value)
174
+ return self if public_send(property) == value
175
+
176
+ @children.each do |child|
177
+ object = child.get_object_by_property(property, value)
178
+ return object if object
179
+ end
180
+
181
+ nil
182
+ end
183
+
184
+ def get_objects_by_property(property, value, result = [])
185
+ result << self if public_send(property) == value
186
+ @children.each { |child| child.get_objects_by_property(property, value, result) }
187
+ result
188
+ end
189
+
190
+ def update_matrix
191
+ @matrix.compose(@position, @quaternion, @scale)
192
+ @matrix_world_needs_update = true
193
+ self
194
+ end
195
+
196
+ def update_matrix_world(force = false)
197
+ update_matrix if @matrix_auto_update
198
+
199
+ if @matrix_world_needs_update || force
200
+ if @matrix_world_auto_update
201
+ if @parent
202
+ @matrix_world.multiply_matrices(@parent.matrix_world, @matrix)
203
+ else
204
+ @matrix_world.copy(@matrix)
205
+ end
206
+ end
207
+
208
+ @matrix_world_needs_update = false
209
+ force = true
210
+ end
211
+
212
+ @children.each { |child| child.update_matrix_world(force) }
213
+ self
214
+ end
215
+
216
+ def update_world_matrix(update_parents = false, update_children = false)
217
+ @parent&.update_world_matrix(true, false) if update_parents
218
+ update_matrix if @matrix_auto_update
219
+
220
+ if @matrix_world_auto_update
221
+ if @parent
222
+ @matrix_world.multiply_matrices(@parent.matrix_world, @matrix)
223
+ else
224
+ @matrix_world.copy(@matrix)
225
+ end
226
+ end
227
+
228
+ @children.each { |child| child.update_world_matrix(false, true) } if update_children
229
+ self
230
+ end
231
+
232
+ def get_world_position(target = Vector3.new)
233
+ update_world_matrix(true, false)
234
+ target.set_from_matrix_position(@matrix_world)
235
+ end
236
+
237
+ def get_world_quaternion(target = Quaternion.new)
238
+ update_world_matrix(true, false)
239
+ @matrix_world.decompose(Vector3.new, target, Vector3.new)
240
+ target
241
+ end
242
+
243
+ def get_world_scale(target = Vector3.new)
244
+ update_world_matrix(true, false)
245
+ @matrix_world.decompose(Vector3.new, Quaternion.new, target)
246
+ target
247
+ end
248
+
249
+ def get_world_direction(target = Vector3.new)
250
+ update_world_matrix(true, false)
251
+ elements = @matrix_world.elements
252
+ target.set(elements[8], elements[9], elements[10]).normalize
253
+ end
254
+
255
+ def to_h
256
+ {
257
+ uuid: @uuid,
258
+ type: @type,
259
+ name: @name,
260
+ matrix: @matrix.to_a,
261
+ layers: @layers.mask,
262
+ visible: @visible,
263
+ cast_shadow: @cast_shadow,
264
+ receive_shadow: @receive_shadow,
265
+ user_data: @user_data,
266
+ children: @children.map(&:to_h)
267
+ }
268
+ end
269
+
270
+ def to_json(*args)
271
+ Exporters::ThreeJSONExporter.new.to_json(self, *args)
272
+ end
273
+
274
+ protected
275
+
276
+ attr_writer :parent
277
+
278
+ private
279
+
280
+ def add_one(object)
281
+ raise ArgumentError, "object cannot be added as a child of itself" if object.equal?(self)
282
+ raise TypeError, "object must be a Three::Object3D" unless object.is_a?(Object3D)
283
+
284
+ object.remove_from_parent
285
+ object.parent = self
286
+ @children << object
287
+ mark_dirty!(:children)
288
+ object.mark_dirty!(:transform)
289
+ object.dispatch_event(:added)
290
+ dispatch_event(:childadded, object)
291
+ end
292
+
293
+ def remove_one(object)
294
+ return unless @children.include?(object)
295
+
296
+ @children.delete(object)
297
+ object.parent = nil
298
+ mark_dirty!(:children)
299
+ object.mark_dirty!(:transform)
300
+ object.dispatch_event(:removed)
301
+ dispatch_event(:childremoved, object)
302
+ end
303
+
304
+ def bind_rotation_and_quaternion
305
+ @rotation.on_change do
306
+ @quaternion.set_from_euler(@rotation, update: false)
307
+ @matrix_world_needs_update = true
308
+ mark_dirty!(:transform)
309
+ end
310
+
311
+ @quaternion.on_change do
312
+ @rotation.set_from_quaternion(@quaternion, @rotation.order, update: false)
313
+ @matrix_world_needs_update = true
314
+ mark_dirty!(:transform)
315
+ end
316
+ end
317
+
318
+ def bind_transform_changes
319
+ [@position, @scale].each do |vector|
320
+ vector.on_change do
321
+ @matrix_world_needs_update = true
322
+ mark_dirty!(:transform)
323
+ end
324
+ end
325
+ end
326
+
327
+ def bind_layer_changes
328
+ @layers.on_change { mark_dirty!(:properties) }
329
+ end
330
+ end
331
+ end