three-rb 0.1.0 → 0.2.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -1
  3. data/README.md +64 -3
  4. data/docs/browser-runtime.md +90 -24
  5. data/docs/next-work.md +9 -5
  6. data/docs/publishing.md +116 -23
  7. data/docs/release-readiness.md +5 -3
  8. data/docs/standalone-browser-app.md +102 -0
  9. data/examples/browser/README.md +6 -0
  10. data/examples/browser/composition/main.rb +41 -61
  11. data/examples/browser/cube/main.rb +4 -34
  12. data/examples/browser/cubemap/main.rb +16 -39
  13. data/examples/browser/gltf/main.rb +28 -48
  14. data/examples/browser/picking/main.rb +27 -53
  15. data/examples/browser/postprocessing/main.rb +23 -42
  16. data/examples/browser/primitives/main.rb +18 -41
  17. data/examples/browser/ruby/README.md +22 -0
  18. data/examples/browser/ruby/boot.mjs +6 -0
  19. data/examples/browser/ruby/index.html +142 -0
  20. data/examples/browser/ruby/main.rb +313 -0
  21. data/examples/browser/ruby/smoke_test.mjs +126 -0
  22. data/examples/browser/serialization/main.rb +19 -41
  23. data/examples/browser/shared/boot.mjs +37 -5
  24. data/examples/browser/textures/main.rb +21 -39
  25. data/exe/three-rb +55 -0
  26. data/lib/three/backends/threejs/materialization.rb +6 -0
  27. data/lib/three/backends/threejs/parameters.rb +17 -0
  28. data/lib/three/backends/threejs/ruby_wasm_adapter.rb +166 -59
  29. data/lib/three/backends/threejs/synchronization.rb +38 -4
  30. data/lib/three/backends/threejs.rb +24 -0
  31. data/lib/three/browser.rb +389 -0
  32. data/lib/three/constants.rb +6 -0
  33. data/lib/three/core/buffer_attribute.rb +5 -1
  34. data/lib/three/core/buffer_geometry.rb +29 -1
  35. data/lib/three/core/object3d.rb +39 -1
  36. data/lib/three/exporters/three_json_exporter.rb +3 -0
  37. data/lib/three/generators/browser_example.rb +315 -0
  38. data/lib/three/geometries/text_geometry.rb +41 -0
  39. data/lib/three/loaders/font_loader.rb +29 -0
  40. data/lib/three/loaders/three_json_loader.rb +92 -46
  41. data/lib/three/materials/material.rb +2 -1
  42. data/lib/three/math/matrix4.rb +27 -0
  43. data/lib/three/renderers/threejs_renderer.rb +19 -0
  44. data/lib/three/scenes/fog.rb +86 -0
  45. data/lib/three/scenes/scene.rb +19 -1
  46. data/lib/three/textures/texture.rb +2 -1
  47. data/lib/three/version.rb +1 -1
  48. data/lib/three.rb +4 -0
  49. data/package.json +2 -1
  50. metadata +16 -3
@@ -0,0 +1,102 @@
1
+ # Standalone Browser App
2
+
3
+ This guide shows how to make a small browser app outside the three-rb repository with your own Ruby entrypoint.
4
+
5
+ The current browser runtime is still alpha. A standalone app needs a small JavaScript boot file because the browser must start ruby.wasm, expose three.js constructors, and then load your Ruby file over HTTP.
6
+
7
+ ## Generate The App
8
+
9
+ Install the gem, create a project directory, and generate a Ruby-only browser example:
10
+
11
+ ```sh
12
+ mkdir hello-three-rb
13
+ cd hello-three-rb
14
+
15
+ gem install three-rb
16
+ three-rb browser examples/browser/quickstart
17
+ ```
18
+
19
+ If you installed through Bundler, run:
20
+
21
+ ```sh
22
+ bundle exec three-rb browser examples/browser/quickstart
23
+ ```
24
+
25
+ The generator creates:
26
+
27
+ - `package.json` and `pnpm-lock.yaml`
28
+ - `lib/`, copied from the installed gem
29
+ - `examples/browser/shared/boot.mjs`
30
+ - `examples/browser/quickstart/index.html`
31
+ - `examples/browser/quickstart/boot.mjs`
32
+ - `examples/browser/quickstart/main.rb`
33
+ - `examples/browser/quickstart/README.md`
34
+
35
+ Copying `lib/` puts the installed gem's Ruby source in the served app directory. The browser Ruby VM loads Ruby files over HTTP, so this is the current standalone workflow.
36
+
37
+ Pass `--force` only when you want to overwrite generated example files:
38
+
39
+ ```sh
40
+ three-rb browser examples/browser/quickstart --force
41
+ ```
42
+
43
+ ## Ruby Entrypoint
44
+
45
+ The generated `examples/browser/quickstart/main.rb` is plain Ruby scene code. It does not require `js` or call `JS.global`:
46
+
47
+ ```ruby
48
+ # frozen_string_literal: true
49
+
50
+ require_relative "../../../lib/three"
51
+
52
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
53
+ scene = Three::Scene.new
54
+ camera = Three::PerspectiveCamera.new(70, aspect: 1.0, near: 0.1, far: 100)
55
+ camera.position.z = 3
56
+
57
+ cube = Three::Mesh.new(
58
+ Three::BoxGeometry.new(1, 1, 1),
59
+ Three::MeshBasicMaterial.new(color: 0x4ed08f)
60
+ )
61
+ scene.add(cube)
62
+
63
+ renderer = Three::Renderers::ThreeJSRenderer.new(
64
+ canvas: "#scene",
65
+ antialias: true,
66
+ alpha: false
67
+ )
68
+ renderer.set_clear_color(0x101418, 1)
69
+
70
+ app.resize_renderer(renderer, camera)
71
+ renderer.render(scene, camera)
72
+
73
+ app.animation_loop(renderer) do
74
+ cube.rotation.x += 0.01
75
+ cube.rotation.y += 0.015
76
+ renderer.render(scene, camera)
77
+ end
78
+ end
79
+ ```
80
+
81
+ Keep ordinary scene code inside `Three::Browser.run`. Use `app.resize_renderer(renderer, camera)` for responsive canvas sizing, `app.animation_loop(renderer)` for animation, and `app.on_key`, `app.on_pointer`, `app.pointer_ndc`, or `app.storage` before reaching for direct JavaScript access.
82
+
83
+ ## Run It
84
+
85
+ Install browser packages, serve the app directory, and open the page:
86
+
87
+ ```sh
88
+ pnpm install
89
+ ruby -run -e httpd . -p 8000
90
+ ```
91
+
92
+ If Ruby reports that `webrick` is not found, install it once with `gem install webrick`. Ruby 3.0 and later no longer include WEBrick as a standard library.
93
+
94
+ ```text
95
+ http://localhost:8000/examples/browser/quickstart/
96
+ ```
97
+
98
+ Use `http://localhost:8000/...`; do not open the files with `file://`, because the runtime loads ES modules, wasm, Ruby files, and assets over HTTP.
99
+
100
+ ## When JavaScript Is Still Involved
101
+
102
+ The generated app still includes a JavaScript boot file. That file imports three.js, registers addon constructors, starts ruby.wasm, and loads the Ruby entrypoint. Application scene code should not need `require "js"` unless it reaches outside the current three-rb browser API into custom browser APIs, unwrapped three.js addons, or application-specific JavaScript integrations. Use `Three::Browser.js` for that explicit escape hatch and keep it isolated.
@@ -11,9 +11,12 @@ pnpm install
11
11
  ruby -run -e httpd . -p 8000
12
12
  ```
13
13
 
14
+ If Ruby reports that `webrick` is not found, install it once with `gem install webrick`. Ruby 3.0 and later no longer include WEBrick as a standard library.
15
+
14
16
  Open an example URL:
15
17
 
16
18
  ```text
19
+ http://localhost:8000/examples/browser/ruby/
17
20
  http://localhost:8000/examples/browser/cube/
18
21
  http://localhost:8000/examples/browser/composition/
19
22
  http://localhost:8000/examples/browser/textures/
@@ -39,6 +42,7 @@ Run one smoke test by using the command listed in the table below.
39
42
 
40
43
  | Example | Primary APIs covered | Smoke command | Why it exists |
41
44
  | --- | --- | --- | --- |
45
+ | `examples/browser/ruby/` | `BufferGeometry`, `Float32BufferAttribute`, `MeshPhysicalMaterial`, `RGBELoader`, `FontLoader`, `TextGeometry`, `OrbitControls`, shadows | `pnpm test:browser:ruby` | Provides the first visual sample: a Ruby-authored faceted red gemstone with a three-dimensional `three-rb` title. |
42
46
  | `examples/browser/cube/` | `Scene`, `PerspectiveCamera`, `BoxGeometry`, `Mesh`, `MeshBasicMaterial`, `ThreeJSRenderer`, animation loop | `pnpm test:browser:cube` | Verifies the smallest Ruby-authored scene can boot through ruby.wasm and draw nonblank WebGL pixels through the three.js renderer path. |
43
47
  | `examples/browser/composition/` | `OrthographicCamera`, ambient/directional/point/hemisphere lights, shadows, `ShadowMaterial`, `Group`, `InstancedMesh`, `TextureLoader`, `OrbitControls`, material/texture disposal | `pnpm test:browser:composition` | Exercises the broad scene-composition path used by richer browser scenes, including dynamic material updates and camera controls. |
44
48
  | `examples/browser/textures/` | `TextureLoader`, `RGBELoader`, repeat/wrap/filter/UV-transform settings, `MeshPhysicalMaterial`, `MeshMatcapMaterial`, `MeshToonMaterial`, physical, matcap, and toon texture slots, scene environment | `pnpm test:browser:textures` | Verifies browser texture loading, HDR environment synchronization, and the current material texture bridge. |
@@ -52,3 +56,5 @@ Run one smoke test by using the command listed in the table below.
52
56
  ## Adding Browser Coverage
53
57
 
54
58
  New browser-facing features should add or extend one of these examples and include deterministic smoke coverage. Prefer extending an existing example when the feature strengthens the same workflow; add a new example when it introduces a distinct API surface such as a new loader family, render target workflow, or postprocessing pipeline.
59
+
60
+ Keep new Ruby entrypoints Ruby-only: use `Three::Browser.run`, `app.resize_renderer`, `app.on_resize`, `app.animation_loop`, `app.element`, `app.on_key`, `app.on_pointer`, `app.pointer_ndc`, `app.storage`, and `app.expose` instead of `require "js"` or direct `JS.global` calls. If a feature needs browser or three.js APIs that are not wrapped yet, add a small Ruby helper or backend method first. Use `Three::Browser.js` only as an explicit escape hatch and keep it isolated from scene construction code.
@@ -1,19 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting Ruby scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::OrthographicCamera.new(-3, 3, 1.8, -1.8, near: 0.1, far: 100)
19
8
  camera.position.z = 5
@@ -131,9 +120,7 @@ begin
131
120
  )
132
121
  controls.target.set(0, 0, 0)
133
122
 
134
- resize = proc do
135
- width = [viewport[:clientWidth].to_i, 1].max
136
- height = [viewport[:clientHeight].to_i, 1].max
123
+ app.resize_renderer(renderer, camera) do |width, height, _aspect|
137
124
  view_height = 3.8
138
125
  view_width = view_height * width.to_f / height
139
126
 
@@ -142,51 +129,50 @@ begin
142
129
  camera.top = view_height / 2
143
130
  camera.bottom = -view_height / 2
144
131
  camera.update_projection_matrix
145
- renderer.set_size(width, height)
146
132
  end
147
-
148
- resize.call
149
- window.call(:addEventListener, "resize", resize)
150
133
  renderer.render(scene, camera)
151
134
 
152
- JS.global[:__threeRbRenderer] = renderer.handle
153
- JS.global[:__threeRbControls] = controls.handle
154
- JS.global[:__threeRbScene] = renderer.backend.materialize(scene)
155
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
156
- JS.global[:__threeRbAmbientLight] = renderer.backend.materialize(ambient_light)
157
- JS.global[:__threeRbDirectionalLight] = renderer.backend.materialize(key_light)
158
- JS.global[:__threeRbPointLight] = renderer.backend.materialize(point_light)
159
- JS.global[:__threeRbHemisphereLight] = renderer.backend.materialize(hemisphere_light)
160
- JS.global[:__threeRbPlane] = renderer.backend.materialize(backdrop)
161
- JS.global[:__threeRbShadowCatcher] = renderer.backend.materialize(shadow_catcher)
162
- JS.global[:__threeRbShadowMaterial] = renderer.backend.materialize(shadow_material)
163
- JS.global[:__threeRbRig] = renderer.backend.materialize(rig)
164
- JS.global[:__threeRbPrimaryMesh] = renderer.backend.materialize(primary)
165
- JS.global[:__threeRbSatelliteMesh] = renderer.backend.materialize(satellite)
166
- JS.global[:__threeRbSphereMesh] = renderer.backend.materialize(orb)
167
- JS.global[:__threeRbPhongMesh] = renderer.backend.materialize(highlight)
168
- JS.global[:__threeRbInstancedMesh] = renderer.backend.materialize(instanced_field)
169
- JS.global[:__threeRbInstancedMaterial] = renderer.backend.materialize(instanced_material)
170
- JS.global[:__threeRbTexture] = renderer.backend.materialize(primary_texture)
171
- JS.global[:__threeRbChangingMaterial] = renderer.backend.materialize(primary_material)
172
- JS.global[:__threeRbLambertMaterial] = renderer.backend.materialize(primary_material)
173
- JS.global[:__threeRbNormalMaterial] = renderer.backend.materialize(satellite_material)
174
- JS.global[:__threeRbStandardMaterial] = renderer.backend.materialize(orb_material)
175
- JS.global[:__threeRbPhongMaterial] = renderer.backend.materialize(highlight_material)
176
- JS.global[:__threeRbMaterialDisposeEvent] = false
177
- JS.global[:__threeRbTextureDisposeEvent] = false
135
+ app.expose(
136
+ {
137
+ renderer: renderer,
138
+ controls: controls,
139
+ scene: scene,
140
+ camera: camera,
141
+ ambient_light: ambient_light,
142
+ directional_light: key_light,
143
+ point_light: point_light,
144
+ hemisphere_light: hemisphere_light,
145
+ plane: backdrop,
146
+ shadow_catcher: shadow_catcher,
147
+ shadow_material: shadow_material,
148
+ rig: rig,
149
+ primary_mesh: primary,
150
+ satellite_mesh: satellite,
151
+ sphere_mesh: orb,
152
+ phong_mesh: highlight,
153
+ instanced_mesh: instanced_field,
154
+ instanced_material: instanced_material,
155
+ texture: primary_texture,
156
+ changing_material: primary_material,
157
+ lambert_material: primary_material,
158
+ normal_material: satellite_material,
159
+ standard_material: orb_material,
160
+ phong_material: highlight_material,
161
+ material_dispose_event: false,
162
+ texture_dispose_event: false
163
+ },
164
+ renderer: renderer
165
+ )
178
166
 
179
167
  disposable_texture = Three::Loaders::TextureLoader.new.load("/examples/browser/assets/checker.svg")
180
168
  disposable_material = Three::MeshBasicMaterial.new(map: disposable_texture)
181
- disposable_texture_handle = renderer.backend.materialize(disposable_texture)
182
- disposable_material_handle = renderer.backend.materialize(disposable_material)
183
- disposable_texture_handle.call(:addEventListener, "dispose", proc { JS.global[:__threeRbTextureDisposeEvent] = true })
184
- disposable_material_handle.call(:addEventListener, "dispose", proc { JS.global[:__threeRbMaterialDisposeEvent] = true })
169
+ renderer.on_dispose(disposable_texture) { app.set(:texture_dispose_event, true) }
170
+ renderer.on_dispose(disposable_material) { app.set(:material_dispose_event, true) }
185
171
  renderer.dispose(disposable_material, dispose_textures: true)
186
- JS.global[:__threeRbMaterialHandleCachedAfterDispose] = renderer.backend.handles.key?(disposable_material.uuid)
187
- JS.global[:__threeRbTextureHandleCachedAfterDispose] = renderer.backend.handles.key?(disposable_texture.uuid)
188
- JS.global[:__threeRbInitialMaterialColor] = primary_material.color.hex
189
- JS.global[:__threeRbCompositionFrame] = 0
172
+ app.set(:material_handle_cached_after_dispose, renderer.cached?(disposable_material))
173
+ app.set(:texture_handle_cached_after_dispose, renderer.cached?(disposable_texture))
174
+ app.set(:initial_material_color, primary_material.color.hex)
175
+ app.set(:composition_frame, 0)
190
176
 
191
177
  frame = 0
192
178
  renderer.animation_loop do
@@ -203,14 +189,8 @@ begin
203
189
  pulse = (Math.sin(frame * 0.045) + 1) / 2.0
204
190
  primary_material.color.set_rgb(0.25 + (0.35 * pulse), 0.55 + (0.25 * pulse), 0.42)
205
191
 
206
- JS.global[:__threeRbCompositionFrame] = frame
192
+ app.set(:composition_frame, frame)
207
193
  controls.update
208
194
  renderer.render(scene, camera)
209
195
  end
210
-
211
- status[:textContent] = "Running"
212
- status_dot[:dataset][:state] = "running"
213
- rescue StandardError => error
214
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
215
- raise
216
196
  end
@@ -1,19 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting Ruby scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::PerspectiveCamera.new(70, aspect: 1.0, near: 0.1, far: 100)
19
8
  camera.position.z = 3
@@ -31,32 +20,13 @@ begin
31
20
  )
32
21
  renderer.set_clear_color(0x101418, 1)
33
22
 
34
- resize = proc do
35
- width = [viewport[:clientWidth].to_i, 1].max
36
- height = [viewport[:clientHeight].to_i, 1].max
37
-
38
- camera.aspect = width.to_f / height
39
- camera.update_projection_matrix
40
- renderer.set_size(width, height)
41
- end
42
-
43
- resize.call
44
- window.call(:addEventListener, "resize", resize)
23
+ app.resize_renderer(renderer, camera)
45
24
  renderer.render(scene, camera)
46
- JS.global[:__threeRbRenderer] = renderer.handle
47
- JS.global[:__threeRbScene] = renderer.backend.materialize(scene)
48
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
49
- JS.global[:__threeRbCube] = renderer.backend.materialize(cube)
25
+ app.expose({ renderer: renderer, scene: scene, camera: camera, cube: cube }, renderer: renderer)
50
26
 
51
27
  renderer.animation_loop do
52
28
  cube.rotation.x += 0.01
53
29
  cube.rotation.y += 0.015
54
30
  renderer.render(scene, camera)
55
31
  end
56
-
57
- status[:textContent] = "Running"
58
- status_dot[:dataset][:state] = "running"
59
- rescue StandardError => error
60
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
61
- raise
62
32
  end
@@ -1,19 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting Ruby scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::PerspectiveCamera.new(45, aspect: 1.0, near: 0.1, far: 100)
19
8
  camera.position.set(0, 0.7, 4.0)
@@ -46,39 +35,27 @@ begin
46
35
  )
47
36
  renderer.set_clear_color(0x10151b, 1)
48
37
 
49
- resize = proc do
50
- width = [viewport[:clientWidth].to_i, 1].max
51
- height = [viewport[:clientHeight].to_i, 1].max
52
-
53
- camera.aspect = width.to_f / height
54
- camera.update_projection_matrix
55
- renderer.set_size(width, height)
56
- end
57
-
58
- resize.call
59
- window.call(:addEventListener, "resize", resize)
38
+ app.resize_renderer(renderer, camera)
60
39
  renderer.render(scene, camera)
61
-
62
- JS.global[:__threeRbRenderer] = renderer.handle
63
- JS.global[:__threeRbCubemapScene] = renderer.backend.materialize(scene)
64
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
65
- JS.global[:__threeRbCubemapMesh] = renderer.backend.materialize(mesh)
66
- JS.global[:__threeRbCubemapMaterial] = renderer.backend.materialize(material)
67
- JS.global[:__threeRbCubeTexture] = renderer.backend.materialize(cube_texture)
68
- JS.global[:__threeRbCubemapFrame] = 0
40
+ app.expose(
41
+ {
42
+ renderer: renderer,
43
+ cubemap_scene: scene,
44
+ camera: camera,
45
+ cubemap_mesh: mesh,
46
+ cubemap_material: material,
47
+ cube_texture: cube_texture,
48
+ cubemap_frame: 0
49
+ },
50
+ renderer: renderer
51
+ )
69
52
 
70
53
  frame = 0
71
54
  renderer.animation_loop do
72
55
  frame += 1
73
56
  mesh.rotation.y += 0.01
74
57
  mesh.rotation.x = Math.sin(frame * 0.02) * 0.08
75
- JS.global[:__threeRbCubemapFrame] = frame
58
+ app.set(:cubemap_frame, frame)
76
59
  renderer.render(scene, camera)
77
60
  end
78
-
79
- status[:textContent] = "Running"
80
- status_dot[:dataset][:state] = "running"
81
- rescue StandardError => error
82
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
83
- raise
84
61
  end
@@ -1,19 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting Ruby scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting Ruby scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::PerspectiveCamera.new(45, aspect: 1.0, near: 0.1, far: 100)
19
8
  camera.position.set(0, 0.15, 4.0)
@@ -53,53 +42,44 @@ begin
53
42
  action = mixer.clip_action(gltf.animations.first)
54
43
  action.play
55
44
 
56
- resize = proc do
57
- width = [viewport[:clientWidth].to_i, 1].max
58
- height = [viewport[:clientHeight].to_i, 1].max
59
-
60
- camera.aspect = width.to_f / height
61
- camera.update_projection_matrix
62
- renderer.set_size(width, height)
63
- end
64
-
65
- resize.call
66
- window.call(:addEventListener, "resize", resize)
45
+ app.resize_renderer(renderer, camera)
67
46
  renderer.render(scene, camera)
68
47
 
69
- JS.global[:__threeRbRenderer] = renderer.handle
70
- JS.global[:__threeRbGltfRootScene] = renderer.backend.materialize(scene)
71
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
72
- JS.global[:__threeRbGltfScene] = renderer.backend.materialize(model)
73
- JS.global[:__threeRbCompressedGltfScene] = renderer.backend.materialize(compressed_model)
74
- JS.global[:__threeRbCompressedGltfDecoderPath] = draco_decoder_path
75
- JS.global[:__threeRbGltfAnimations] = gltf.animations.length
76
- JS.global[:__threeRbGltfAnimationName] = gltf.animations.first&.name
77
- JS.global[:__threeRbGltfAnimationDuration] = gltf.animations.first&.duration
78
- JS.global[:__threeRbGltfMixer] = mixer.handle
79
- JS.global[:__threeRbGltfAction] = action.handle
80
- JS.global[:__threeRbGltfFrame] = 0
81
- JS.global[:__threeRbGltfAnimationTime] = 0
82
- JS.global[:__threeRbDisposeGltf] = proc do
48
+ app.expose(
49
+ {
50
+ renderer: renderer,
51
+ gltf_root_scene: scene,
52
+ camera: camera,
53
+ gltf_scene: model,
54
+ compressed_gltf_scene: compressed_model,
55
+ compressed_gltf_decoder_path: draco_decoder_path,
56
+ gltf_animations: gltf.animations.length,
57
+ gltf_animation_name: gltf.animations.first&.name,
58
+ gltf_animation_duration: gltf.animations.first&.duration,
59
+ gltf_mixer: mixer,
60
+ gltf_action: action,
61
+ gltf_frame: 0,
62
+ gltf_animation_time: 0
63
+ },
64
+ renderer: renderer
65
+ )
66
+ app.set(:dispose_gltf, proc do
83
67
  mixer.stop_all_action
84
68
  mixer.uncache_root
85
69
  renderer.dispose_subtree(model, remove: true, dispose_textures: true)
86
70
  renderer.dispose_subtree(compressed_model, remove: true, dispose_textures: true)
87
- JS.global[:__threeRbGltfDisposed] = true
88
- end
71
+ app.set(:gltf_disposed, true)
72
+ end)
89
73
 
90
74
  frame = 0
75
+ animation_time = 0
91
76
  renderer.animation_loop do
92
77
  frame += 1
93
78
  delta = clock.get_delta
94
79
  mixer.update(delta)
95
- JS.global[:__threeRbGltfAnimationTime] = JS.global[:__threeRbGltfAnimationTime].to_f + delta
96
- JS.global[:__threeRbGltfFrame] = frame
80
+ animation_time += delta
81
+ app.set(:gltf_animation_time, animation_time)
82
+ app.set(:gltf_frame, frame)
97
83
  renderer.render(scene, camera)
98
84
  end
99
-
100
- status[:textContent] = "Running"
101
- status_dot[:dataset][:state] = "running"
102
- rescue StandardError => error
103
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
104
- raise
105
85
  end
@@ -1,19 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "js"
4
-
5
- begin
6
- JS.global[:__threeReady].await
7
-
8
- require_relative "../../../lib/three"
9
-
10
- document = JS.global[:document]
11
- window = JS.global[:window]
12
- viewport = document.call(:querySelector, "#viewport")
13
- status = document.call(:querySelector, "#status")
14
- status_dot = document.call(:querySelector, "#status-dot")
15
- status[:textContent] = "Starting picking scene"
3
+ require_relative "../../../lib/three"
16
4
 
5
+ Three::Browser.run(starting: "Starting picking scene") do |app|
17
6
  scene = Three::Scene.new
18
7
  camera = Three::PerspectiveCamera.new(60, aspect: 1.0, near: 0.1, far: 100)
19
8
  camera.position.z = 4
@@ -40,25 +29,15 @@ begin
40
29
  )
41
30
  renderer.set_clear_color(0x101418, 1)
42
31
 
43
- resize = proc do
44
- width = [viewport[:clientWidth].to_i, 1].max
45
- height = [viewport[:clientHeight].to_i, 1].max
46
-
47
- camera.aspect = width.to_f / height
48
- camera.update_projection_matrix
49
- renderer.set_size(width, height)
50
- end
51
-
52
32
  pointer = Three::Vector2.new
53
33
  raycaster = Three::Raycaster.new(backend: renderer.backend)
54
34
  pickables = [left_cube, right_cube]
55
35
  selected = nil
56
36
 
57
- pick = proc do |event|
58
- rect = renderer.dom_element.call(:getBoundingClientRect)
59
- x = ((event[:clientX].to_f - rect[:left].to_f) / rect[:width].to_f) * 2 - 1
60
- y = -(((event[:clientY].to_f - rect[:top].to_f) / rect[:height].to_f) * 2 - 1)
61
- pointer.set(x, y)
37
+ app.resize_renderer(renderer, camera)
38
+ canvas = app.element(renderer.dom_element)
39
+ canvas.on("click") do |event|
40
+ pointer.set(*canvas.pointer_ndc(event))
62
41
  raycaster.set_from_camera(pointer, camera)
63
42
  hits = raycaster.intersect_objects(pickables, recursive: false)
64
43
  hit = hits.find(&:object)
@@ -71,43 +50,38 @@ begin
71
50
  if hit
72
51
  selected = hit.object
73
52
  selected.material.color.set_hex(picked_color)
74
- JS.global[:__threeRbPickedName] = selected.name
75
- JS.global[:__threeRbPickedDistance] = hit.distance
76
- JS.global[:__threeRbPickedPoint] = hit.point.to_a
53
+ app.set(:picked_name, selected.name)
54
+ app.set(:picked_distance, hit.distance)
55
+ app.set(:picked_point, hit.point.to_a)
77
56
  else
78
- JS.global[:__threeRbPickedName] = nil
79
- JS.global[:__threeRbPickedDistance] = nil
80
- JS.global[:__threeRbPickedPoint] = nil
57
+ app.set(:picked_name, nil)
58
+ app.set(:picked_distance, nil)
59
+ app.set(:picked_point, nil)
81
60
  end
82
61
 
83
- JS.global[:__threeRbPickCount] = JS.global[:__threeRbPickCount].to_i + 1
62
+ app.increment(:pick_count)
84
63
  renderer.render(scene, camera)
85
64
  end
86
-
87
- resize.call
88
- window.call(:addEventListener, "resize", resize)
89
- renderer.dom_element.call(:addEventListener, "click", pick)
90
65
  renderer.render(scene, camera)
91
66
 
92
- JS.global[:__threeRbRenderer] = renderer.handle
93
- JS.global[:__threeRbScene] = renderer.backend.materialize(scene)
94
- JS.global[:__threeRbCamera] = renderer.backend.materialize(camera)
95
- JS.global[:__threeRbLeftCube] = renderer.backend.materialize(left_cube)
96
- JS.global[:__threeRbRightCube] = renderer.backend.materialize(right_cube)
97
- JS.global[:__threeRbRaycaster] = raycaster.handle
98
- JS.global[:__threeRbPickCount] = 0
99
- JS.global[:__threeRbPickingFrame] = 0
67
+ app.expose(
68
+ {
69
+ renderer: renderer,
70
+ scene: scene,
71
+ camera: camera,
72
+ left_cube: left_cube,
73
+ right_cube: right_cube,
74
+ raycaster: raycaster,
75
+ pick_count: 0,
76
+ picking_frame: 0
77
+ },
78
+ renderer: renderer
79
+ )
100
80
 
101
81
  renderer.animation_loop do
102
- JS.global[:__threeRbPickingFrame] = JS.global[:__threeRbPickingFrame].to_i + 1
82
+ app.increment(:picking_frame)
103
83
  left_cube.rotation.y += 0.01
104
84
  right_cube.rotation.y -= 0.01
105
85
  renderer.render(scene, camera)
106
86
  end
107
-
108
- status[:textContent] = "Running"
109
- status_dot[:dataset][:state] = "running"
110
- rescue StandardError => error
111
- JS.global.call(:__threeRbBootFailed, error.message) if JS.global[:__threeRbBootFailed]
112
- raise
113
87
  end