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,54 @@
1
+ # Browser Examples
2
+
3
+ These examples are the browser-facing coverage map for three-rb's browser-first alpha. They run Ruby through ruby.wasm, load pnpm-managed three.js packages, and render through `Three::Renderers::ThreeJSRenderer`.
4
+
5
+ ## Run Examples
6
+
7
+ Install browser dependencies and serve the repository root:
8
+
9
+ ```sh
10
+ pnpm install
11
+ ruby -run -e httpd . -p 8000
12
+ ```
13
+
14
+ Open an example URL:
15
+
16
+ ```text
17
+ http://localhost:8000/examples/browser/cube/
18
+ http://localhost:8000/examples/browser/composition/
19
+ http://localhost:8000/examples/browser/textures/
20
+ http://localhost:8000/examples/browser/cubemap/
21
+ http://localhost:8000/examples/browser/gltf/
22
+ http://localhost:8000/examples/browser/serialization/
23
+ http://localhost:8000/examples/browser/picking/
24
+ http://localhost:8000/examples/browser/primitives/
25
+ http://localhost:8000/examples/browser/postprocessing/
26
+ ```
27
+
28
+ ## Smoke Tests
29
+
30
+ Run all browser smoke tests:
31
+
32
+ ```sh
33
+ pnpm install
34
+ pnpm exec playwright install chromium
35
+ pnpm test:browser
36
+ ```
37
+
38
+ Run one smoke test by using the command listed in the table below.
39
+
40
+ | Example | Primary APIs covered | Smoke command | Why it exists |
41
+ | --- | --- | --- | --- |
42
+ | `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
+ | `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
+ | `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. |
45
+ | `examples/browser/cubemap/` | `CubeTextureLoader`, `CubeTexture`, scene `background`, scene `environment`, reflective `MeshStandardMaterial` | `pnpm test:browser:cubemap` | Keeps cubemap background/environment behavior covered separately from ordinary 2D texture loading. |
46
+ | `examples/browser/gltf/` | `GLTFLoader`, `DRACOLoader` decoder path, loaded external scenes, `AnimationMixer`, `Clock`, loaded subtree disposal | `pnpm test:browser:gltf` | Verifies that external assets can be loaded, animated, attached to Ruby scenes, and disposed through the renderer API. |
47
+ | `examples/browser/serialization/` | `ThreeJSONExporter`, `ThreeJSONLoader`, deterministic ids, shared geometry/material/texture resources, loaded scene rendering | `pnpm test:browser:serialization` | Confirms exported Ruby scenes round-trip through JSON and render after loading. |
48
+ | `examples/browser/picking/` | `Raycaster`, pointer-to-camera coordinates, intersection mapping back to Ruby `Object3D`, selected material updates | `pnpm test:browser:picking` | Verifies browser event coordinates can drive Ruby-side picking and mutate rendered objects. |
49
+ | `examples/browser/primitives/` | `BufferGeometry`, `Float32BufferAttribute`, `Line`, `Points`, `Sprite`, `LineBasicMaterial`, `PointsMaterial`, `SpriteMaterial` | `pnpm test:browser:primitives` | Covers non-`Mesh` primitive rendering, generic buffer attribute synchronization, and textured billboard markers. |
50
+ | `examples/browser/postprocessing/` | `EffectComposer`, `RenderPass`, `UnrealBloomPass`, `DotScreenPass`, `OutputPass`, composer sizing, pass property/uniform updates, `composer.render` | `pnpm test:browser:postprocessing` | Verifies the explicit postprocessing render path stays separate from direct renderer rendering and remains smoke-tested. |
51
+
52
+ ## Adding Browser Coverage
53
+
54
+ 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.
@@ -0,0 +1,123 @@
1
+ {
2
+ "asset": {
3
+ "version": "2.0",
4
+ "generator": "three-rb animated smoke asset"
5
+ },
6
+ "scene": 0,
7
+ "scenes": [
8
+ {
9
+ "nodes": [0]
10
+ }
11
+ ],
12
+ "nodes": [
13
+ {
14
+ "name": "AnimatedRubyTriangle",
15
+ "mesh": 0,
16
+ "rotation": [0, 0, 0, 1]
17
+ }
18
+ ],
19
+ "meshes": [
20
+ {
21
+ "name": "AnimatedRubyTriangleMesh",
22
+ "primitives": [
23
+ {
24
+ "attributes": {
25
+ "POSITION": 0
26
+ },
27
+ "material": 0
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "materials": [
33
+ {
34
+ "name": "AnimatedRubyTriangleMaterial",
35
+ "pbrMetallicRoughness": {
36
+ "baseColorFactor": [0.35, 0.78, 1.0, 1.0],
37
+ "metallicFactor": 0.0,
38
+ "roughnessFactor": 0.45
39
+ },
40
+ "doubleSided": true
41
+ }
42
+ ],
43
+ "animations": [
44
+ {
45
+ "name": "TriangleSpin",
46
+ "channels": [
47
+ {
48
+ "sampler": 0,
49
+ "target": {
50
+ "node": 0,
51
+ "path": "rotation"
52
+ }
53
+ }
54
+ ],
55
+ "samplers": [
56
+ {
57
+ "input": 1,
58
+ "output": 2,
59
+ "interpolation": "LINEAR"
60
+ }
61
+ ]
62
+ }
63
+ ],
64
+ "buffers": [
65
+ {
66
+ "uri": "data:application/octet-stream;base64,AAAAAAAAgD8AAAAAAACAvwAAgL8AAAAAAACAPwAAgL8AAAAA",
67
+ "byteLength": 36
68
+ },
69
+ {
70
+ "uri": "data:application/octet-stream;base64,AAAAAAAAAEA=",
71
+ "byteLength": 8
72
+ },
73
+ {
74
+ "uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAIA/AAAAAAAAAAA=",
75
+ "byteLength": 32
76
+ }
77
+ ],
78
+ "bufferViews": [
79
+ {
80
+ "buffer": 0,
81
+ "byteOffset": 0,
82
+ "byteLength": 36,
83
+ "target": 34962
84
+ },
85
+ {
86
+ "buffer": 1,
87
+ "byteOffset": 0,
88
+ "byteLength": 8
89
+ },
90
+ {
91
+ "buffer": 2,
92
+ "byteOffset": 0,
93
+ "byteLength": 32
94
+ }
95
+ ],
96
+ "accessors": [
97
+ {
98
+ "bufferView": 0,
99
+ "byteOffset": 0,
100
+ "componentType": 5126,
101
+ "count": 3,
102
+ "type": "VEC3",
103
+ "min": [-1.0, -1.0, 0.0],
104
+ "max": [1.0, 1.0, 0.0]
105
+ },
106
+ {
107
+ "bufferView": 1,
108
+ "byteOffset": 0,
109
+ "componentType": 5126,
110
+ "count": 2,
111
+ "type": "SCALAR",
112
+ "min": [0.0],
113
+ "max": [2.0]
114
+ },
115
+ {
116
+ "bufferView": 2,
117
+ "byteOffset": 0,
118
+ "componentType": 5126,
119
+ "count": 2,
120
+ "type": "VEC4"
121
+ }
122
+ ]
123
+ }
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
2
+ <rect width="128" height="128" fill="#eef3f7"/>
3
+ <rect width="32" height="32" fill="#22313f"/>
4
+ <rect x="64" width="32" height="32" fill="#22313f"/>
5
+ <rect x="32" y="32" width="32" height="32" fill="#6ed69a"/>
6
+ <rect x="96" y="32" width="32" height="32" fill="#6ed69a"/>
7
+ <rect y="64" width="32" height="32" fill="#22313f"/>
8
+ <rect x="64" y="64" width="32" height="32" fill="#22313f"/>
9
+ <rect x="32" y="96" width="32" height="32" fill="#6ed69a"/>
10
+ <rect x="96" y="96" width="32" height="32" fill="#6ed69a"/>
11
+ </svg>
@@ -0,0 +1,74 @@
1
+ {
2
+ "asset": {
3
+ "version": "2.0",
4
+ "generator": "three-rb draco smoke asset"
5
+ },
6
+ "extensionsUsed": ["KHR_draco_mesh_compression"],
7
+ "extensionsRequired": ["KHR_draco_mesh_compression"],
8
+ "scene": 0,
9
+ "scenes": [
10
+ {
11
+ "nodes": [0]
12
+ }
13
+ ],
14
+ "nodes": [
15
+ {
16
+ "name": "CompressedRubyTriangle",
17
+ "mesh": 0
18
+ }
19
+ ],
20
+ "meshes": [
21
+ {
22
+ "name": "CompressedRubyTriangleMesh",
23
+ "primitives": [
24
+ {
25
+ "attributes": {
26
+ "POSITION": 0
27
+ },
28
+ "extensions": {
29
+ "KHR_draco_mesh_compression": {
30
+ "bufferView": 0,
31
+ "attributes": {
32
+ "POSITION": 0
33
+ }
34
+ }
35
+ },
36
+ "material": 0
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "materials": [
42
+ {
43
+ "name": "CompressedRubyTriangleMaterial",
44
+ "pbrMetallicRoughness": {
45
+ "baseColorFactor": [1.0, 0.62, 0.28, 1.0],
46
+ "metallicFactor": 0.0,
47
+ "roughnessFactor": 0.35
48
+ },
49
+ "doubleSided": true
50
+ }
51
+ ],
52
+ "buffers": [
53
+ {
54
+ "uri": "data:application/octet-stream;base64,RFJBQ08CAgEBAAAAAwEAAQAAAQf/AREB/wAAAQAJAwAAAgEBAQAPA60qL1UVA6B6gUj/HwAAAAAAAAD/PwAAAACAvwAAgL8AAAAAAAAAQA4=",
55
+ "byteLength": 80
56
+ }
57
+ ],
58
+ "bufferViews": [
59
+ {
60
+ "buffer": 0,
61
+ "byteOffset": 0,
62
+ "byteLength": 80
63
+ }
64
+ ],
65
+ "accessors": [
66
+ {
67
+ "componentType": 5126,
68
+ "count": 3,
69
+ "type": "VEC3",
70
+ "min": [-1.0, -1.0, 0.0],
71
+ "max": [1.0, 1.0, 0.0]
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,5 @@
1
+ #?RADIANCE
2
+ FORMAT=32-bit_rle_rgbe
3
+
4
+ -Y 2 +X 2
5
+ ~~~~~~~~~~~~~~~~
@@ -0,0 +1,67 @@
1
+ {
2
+ "asset": {
3
+ "version": "2.0",
4
+ "generator": "three-rb smoke asset"
5
+ },
6
+ "scene": 0,
7
+ "scenes": [
8
+ {
9
+ "nodes": [0]
10
+ }
11
+ ],
12
+ "nodes": [
13
+ {
14
+ "name": "RubyTriangle",
15
+ "mesh": 0
16
+ }
17
+ ],
18
+ "meshes": [
19
+ {
20
+ "name": "RubyTriangleMesh",
21
+ "primitives": [
22
+ {
23
+ "attributes": {
24
+ "POSITION": 0
25
+ },
26
+ "material": 0
27
+ }
28
+ ]
29
+ }
30
+ ],
31
+ "materials": [
32
+ {
33
+ "name": "RubyTriangleMaterial",
34
+ "pbrMetallicRoughness": {
35
+ "baseColorFactor": [0.35, 0.78, 1.0, 1.0],
36
+ "metallicFactor": 0.0,
37
+ "roughnessFactor": 0.45
38
+ },
39
+ "doubleSided": true
40
+ }
41
+ ],
42
+ "buffers": [
43
+ {
44
+ "uri": "data:application/octet-stream;base64,AAAAAAAAgD8AAAAAAACAvwAAgL8AAAAAAACAPwAAgL8AAAAA",
45
+ "byteLength": 36
46
+ }
47
+ ],
48
+ "bufferViews": [
49
+ {
50
+ "buffer": 0,
51
+ "byteOffset": 0,
52
+ "byteLength": 36,
53
+ "target": 34962
54
+ }
55
+ ],
56
+ "accessors": [
57
+ {
58
+ "bufferView": 0,
59
+ "byteOffset": 0,
60
+ "componentType": 5126,
61
+ "count": 3,
62
+ "type": "VEC3",
63
+ "min": [-1.0, -1.0, 0.0],
64
+ "max": [1.0, 1.0, 0.0]
65
+ }
66
+ ]
67
+ }
@@ -0,0 +1,35 @@
1
+ # Browser Composition Example
2
+
3
+ This example renders a Ruby-authored scene through ruby.wasm and the JavaScript three.js renderer. It uses ambient, directional, point, and hemisphere lights, directional shadow mapping, a `PlaneGeometry` backdrop and shadow catcher, `SphereGeometry`, grouped child meshes, `TextureLoader` repeat/wrap/filter settings, `MeshLambertMaterial`, `MeshPhongMaterial`, `MeshStandardMaterial`, `MeshNormalMaterial`, `ShadowMaterial`, `OrbitControls`, backend material/texture disposal, and a material color update in the animation loop.
4
+
5
+ Install browser dependencies and serve the repository root over HTTP:
6
+
7
+ ```sh
8
+ pnpm install
9
+ ruby -run -e httpd . -p 8000
10
+ ```
11
+
12
+ Then open:
13
+
14
+ ```text
15
+ http://localhost:8000/examples/browser/composition/
16
+ ```
17
+
18
+ The repository root must be served, not only this directory, because `boot.mjs` loads browser packages from `node_modules/` and `main.rb` loads the library source from `lib/`.
19
+
20
+ ## Browser Smoke Test
21
+
22
+ Install the optional Node dependency and browser binary:
23
+
24
+ ```sh
25
+ pnpm install
26
+ pnpm exec playwright install chromium
27
+ ```
28
+
29
+ Run the browser smoke test:
30
+
31
+ ```sh
32
+ pnpm test:browser:composition
33
+ ```
34
+
35
+ The smoke test serves the repository root, waits for the Ruby scene to reach `Running`, samples the WebGL canvas for nonblank pixels, verifies ambient/directional/point/hemisphere lights, directional shadow mapping, grouped meshes, `PlaneGeometry`, `SphereGeometry`, `TextureLoader` repeat/wrap/filter settings, `MeshLambertMaterial`, `MeshPhongMaterial`, `MeshStandardMaterial`, `MeshNormalMaterial`, `ShadowMaterial`, backend material/texture disposal, and `OrbitControls`, then confirms that material changes and camera drag controls reach the renderer.
@@ -0,0 +1,6 @@
1
+ import { bootRubyExample } from "../shared/boot.mjs";
2
+
3
+ await bootRubyExample({
4
+ main: "examples/browser/composition/main",
5
+ clearColor: 0x0f1419
6
+ });
@@ -0,0 +1,136 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>three-rb composition</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ font-family:
11
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
12
+ "Segoe UI", sans-serif;
13
+ background: #0f1419;
14
+ color: #eef3f7;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html,
22
+ body {
23
+ width: 100%;
24
+ height: 100%;
25
+ margin: 0;
26
+ }
27
+
28
+ body {
29
+ display: grid;
30
+ place-items: stretch;
31
+ overflow: hidden;
32
+ }
33
+
34
+ .viewport {
35
+ position: relative;
36
+ width: 100%;
37
+ height: 100%;
38
+ min-width: 320px;
39
+ min-height: 320px;
40
+ background:
41
+ linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px),
42
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
43
+ linear-gradient(135deg, #0f1419 0%, #17221e 48%, #151820 100%);
44
+ background-size: 36px 36px, 36px 36px, auto;
45
+ }
46
+
47
+ canvas {
48
+ display: block;
49
+ width: 100%;
50
+ height: 100%;
51
+ }
52
+
53
+ .hud {
54
+ position: absolute;
55
+ top: 16px;
56
+ left: 16px;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 8px;
60
+ max-width: calc(100% - 32px);
61
+ padding: 8px 10px;
62
+ border: 1px solid rgba(255, 255, 255, 0.14);
63
+ border-radius: 6px;
64
+ background: rgba(9, 13, 17, 0.72);
65
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24);
66
+ color: #dce7ef;
67
+ font-size: 13px;
68
+ line-height: 1.3;
69
+ backdrop-filter: blur(10px);
70
+ }
71
+
72
+ .status-dot {
73
+ width: 8px;
74
+ height: 8px;
75
+ flex: 0 0 auto;
76
+ border-radius: 999px;
77
+ background: #f2b84b;
78
+ box-shadow: 0 0 14px rgba(242, 184, 75, 0.72);
79
+ }
80
+
81
+ .status-dot[data-state="running"] {
82
+ background: #60d394;
83
+ box-shadow: 0 0 14px rgba(96, 211, 148, 0.72);
84
+ }
85
+
86
+ .status-dot[data-state="error"] {
87
+ background: #f15b5b;
88
+ box-shadow: 0 0 14px rgba(241, 91, 91, 0.72);
89
+ }
90
+ </style>
91
+ <script type="importmap">
92
+ {
93
+ "imports": {
94
+ "@bjorn3/browser_wasi_shim": "/node_modules/@bjorn3/browser_wasi_shim/dist/index.js",
95
+ "@ruby/wasm-wasi/browser": "/node_modules/@ruby/wasm-wasi/dist/esm/browser.js",
96
+ "three": "/node_modules/three/build/three.module.js",
97
+ "three/addons/": "/node_modules/three/examples/jsm/"
98
+ }
99
+ }
100
+ </script>
101
+ <script type="module">
102
+ const status = document.querySelector("#status");
103
+ const statusDot = document.querySelector("#status-dot");
104
+
105
+ globalThis.__threeRbSetStatus = (message, state) => {
106
+ if (status) status.textContent = message;
107
+ if (statusDot) statusDot.dataset.state = state;
108
+ };
109
+
110
+ globalThis.__threeRbBootFailed = (message) => {
111
+ globalThis.__threeRbSetStatus(message, "error");
112
+ };
113
+
114
+ globalThis.addEventListener("error", (event) => {
115
+ globalThis.__threeRbBootFailed(event.message || "Browser error");
116
+ });
117
+
118
+ globalThis.addEventListener("unhandledrejection", (event) => {
119
+ const reason = event.reason;
120
+ globalThis.__threeRbBootFailed(reason && reason.message ? reason.message : "Ruby boot failed");
121
+ });
122
+
123
+ globalThis.__threeRbSetStatus("Loading ruby.wasm", "loading");
124
+ </script>
125
+ <script type="module" src="./boot.mjs"></script>
126
+ </head>
127
+ <body>
128
+ <main class="viewport" id="viewport">
129
+ <canvas id="scene" data-testid="scene-canvas"></canvas>
130
+ <div class="hud" aria-live="polite">
131
+ <span class="status-dot" id="status-dot"></span>
132
+ <span id="status" data-testid="status">Loading ruby.wasm</span>
133
+ </div>
134
+ </main>
135
+ </body>
136
+ </html>