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,400 @@
1
+ # Loaded Asset Traversal and Disposal Design
2
+
3
+ This document records the recommended design for loaded assets such as glTF scenes. It is intended to be self-contained enough to resume implementation even if the conversation context is lost.
4
+
5
+ ## Status Snapshot
6
+
7
+ Repository state when this decision was written:
8
+
9
+ - Branch: `main`
10
+ - Relevant recent commits:
11
+ - `421a89c Add GLTF loader`
12
+ - `3bc389d Add CubeTexture loader`
13
+ - `9b6307c Add renderer disposal helper`
14
+ - Current implementation:
15
+ - `Three::Loaders::GLTFLoader` delegates to JavaScript `GLTFLoader#loadAsync` and can attach JavaScript `DRACOLoader` when `draco_decoder_path:` is configured.
16
+ - `Three::Loaders::GLTF#scene` is a `Three::ExternalObject3D`.
17
+ - `Three::ExternalObject3D` stores a loaded JavaScript `Object3D` handle.
18
+ - `Three::Backends::ThreeJS#materialize` returns that handle directly for `ExternalObject3D`.
19
+ - `examples/browser/gltf` verifies that a loaded glTF scene can be added to a Ruby-authored scene and rendered.
20
+
21
+ Important local files:
22
+
23
+ - `lib/three/loaders/gltf_loader.rb`
24
+ - `lib/three/objects/external_object3d.rb`
25
+ - `lib/three/backends/threejs.rb`
26
+ - `examples/browser/gltf/main.rb`
27
+ - `examples/browser/gltf/smoke_test.mjs`
28
+ - `test/three/loaders/gltf_loader_test.rb`
29
+ - `test/three/objects/external_object3d_test.rb`
30
+ - `test/three/backends/threejs_test.rb`
31
+
32
+ ## Implementation Status
33
+
34
+ The recommended design has been implemented for the current loaded-asset MVP:
35
+
36
+ - `ExternalObject3D#add`, `#remove`, and `#clear` now reject Ruby child mutation.
37
+ - `Three::Backends::ThreeJS#traverse_handles` traverses backend object handles without changing `Object3D#traverse`.
38
+ - `Three::Renderers::ThreeJSRenderer#traverse_handles` exposes the same traversal at renderer scope.
39
+ - `Three::Backends::ThreeJS#dispose_subtree` delegates subtree cleanup to the backend adapter.
40
+ - `Three::Renderers::ThreeJSRenderer#dispose_subtree` provides high-level loaded-asset cleanup and defaults `dispose_textures` to `true`.
41
+ - `RubyWasmAdapter` collects unique geometries, materials, common material texture slots, scene background/environment textures, and skeletons before disposing.
42
+ - `FakeThreeJSAdapter` mirrors this behavior for unit tests.
43
+ - `examples/browser/gltf/smoke_test.mjs` verifies that the Ruby renderer API decodes a Draco-compressed glTF fixture, detaches loaded glTF roots, and dispatches geometry/material/texture dispose events.
44
+
45
+ Remaining work:
46
+
47
+ - The Ruby material model exposes common `MeshStandardMaterial` PBR texture slots, `MeshPhysicalMaterial` physical texture slots, and `MeshMatcapMaterial` matcap texture slots. Broaden ownership helpers further if additional material classes introduce new texture slots.
48
+ - If future APIs need to inspect or edit loaded child objects, design explicit wrapper types instead of changing `Object3D#traverse`.
49
+
50
+ ## Decision
51
+
52
+ Keep loaded three.js assets opaque by default.
53
+
54
+ Do not fully convert a loaded glTF scene into Ruby `Mesh`, `Material`, `BufferGeometry`, `Texture`, `Camera`, or animation objects at this stage. Instead:
55
+
56
+ 1. Use `ExternalObject3D` as an opaque root wrapper around the loaded JavaScript `Object3D`.
57
+ 2. Let Ruby own only the attachment point and transform-level concerns for that external root.
58
+ 3. Add explicit backend/renderer helpers for traversing and disposing resources inside the external JavaScript subtree.
59
+ 4. Preserve the existing pure Ruby `Object3D#traverse` semantics for Ruby-authored objects.
60
+
61
+ The first target user-facing API should be:
62
+
63
+ ```ruby
64
+ gltf = Three::Loaders::GLTFLoader.new.load("/models/model.gltf")
65
+ scene.add(gltf.scene)
66
+
67
+ # Later, when the loaded asset is no longer needed:
68
+ renderer.dispose_subtree(gltf.scene, remove: true, dispose_textures: true)
69
+ ```
70
+
71
+ ## Rationale
72
+
73
+ three.js `GLTFLoader` already constructs a complete JavaScript scene graph. Its documented usage is to load a glTF asset and add `gltf.scene` directly to the scene. The returned object also carries `animations`, `scenes`, `cameras`, `asset`, `parser`, and `userData`.
74
+
75
+ `GLTFLoader` performs non-trivial work that should not be duplicated in Ruby early:
76
+
77
+ - It returns `Group` scenes, not `THREE.Scene` instances.
78
+ - It handles shared node references and clones reused nodes when necessary.
79
+ - It reuses non-scene resources such as materials, geometries, and textures by reference.
80
+ - It preserves parser associations and glTF extension data.
81
+ - It can involve skins, skeletons, morph targets, animations, compressed geometry, texture transforms, and custom extensions.
82
+
83
+ Ruby-side full conversion would either lose these semantics or force a large, fragile mirror of three.js loader internals. That is not justified for the current goal: write Ruby scene code and render through three.js in the browser.
84
+
85
+ ## Source Findings
86
+
87
+ These findings were verified against the local vendored/reference repositories.
88
+
89
+ three.js facts:
90
+
91
+ - `Object3D#add` removes an object from its previous parent and attaches it to the new parent.
92
+ - `Object3D#traverse` walks JavaScript children depth-first and discourages modifying the scene graph inside the callback.
93
+ - `BufferGeometry#dispose`, `Material#dispose`, and `Texture#dispose` dispatch disposal events for GPU-related resources.
94
+ - Removing a mesh from a scene does not dispose its geometry or material.
95
+ - Disposing a material does not dispose textures, because textures can be shared.
96
+ - The three.js manual recommends explicit resource tracking for cleanup.
97
+ - Scene-related resources such as `scene.background`, `scene.environment`, and `material.envMap` may leave renderer-internal resources visible in `renderer.info.memory`, even after reachable app resources are disposed.
98
+
99
+ Local source locations:
100
+
101
+ - `node_modules/three/src/core/Object3D.js`
102
+ - `node_modules/three/src/core/BufferGeometry.js`
103
+ - `node_modules/three/src/materials/Material.js`
104
+ - `node_modules/three/src/textures/Texture.js`
105
+ - `node_modules/three/examples/jsm/loaders/GLTFLoader.js`
106
+ - `node_modules/three/examples/jsm/utils/SkeletonUtils.js`
107
+ - `~/ghq/github.com/mrdoob/three.js/manual/en/how-to-dispose-of-objects.html`
108
+ - `~/ghq/github.com/mrdoob/three.js/manual/en/cleanup.html`
109
+
110
+ ruby.wasm facts:
111
+
112
+ - `JS::Object#await` works in `evalAsync` / `callAsync` contexts.
113
+ - `JS::Object` supports property access with `object[:name]`, method calls with `call`, and array conversion with `to_a`.
114
+ - Passing blocks/procs to JavaScript callbacks is supported enough for traversal-style APIs.
115
+
116
+ Local source locations:
117
+
118
+ - `~/ghq/github.com/ruby/ruby.wasm/packages/gems/js/lib/js.rb`
119
+ - `~/ghq/github.com/ruby/ruby.wasm/packages/npm-packages/ruby-wasm-wasi/test/eval_async.test.js`
120
+
121
+ ## Verified Behavior
122
+
123
+ The existing glTF browser smoke test verifies:
124
+
125
+ - `Three::Loaders::GLTFLoader` loads `examples/browser/assets/triangle.gltf`.
126
+ - The returned `gltf.scene` can be added to a Ruby `Scene`.
127
+ - The loaded JavaScript scene renders through `Three::Renderers::ThreeJSRenderer`.
128
+ - The animation loop can mutate the external root transform.
129
+
130
+ Additional manual verification was performed in a real headless browser:
131
+
132
+ ```text
133
+ objects=2
134
+ meshes=1
135
+ geometries=1
136
+ materials=1
137
+ textures=0
138
+ geometryDisposeEvents=1
139
+ materialDisposeEvents=1
140
+ textureDisposeEvents=0
141
+ ```
142
+
143
+ This confirmed that a loaded glTF JavaScript subtree can be traversed through its handle and that geometry/material disposal events fire as expected.
144
+
145
+ ## Recommended API Shape
146
+
147
+ ### External Object Traversal
148
+
149
+ Keep Ruby-authored traversal and JavaScript-loaded traversal separate.
150
+
151
+ Recommended:
152
+
153
+ ```ruby
154
+ gltf.scene.traverse_handles do |handle|
155
+ # handle is a JS object in ruby.wasm, or an adapter-provided object in tests.
156
+ end
157
+ ```
158
+
159
+ or renderer/backend scoped:
160
+
161
+ ```ruby
162
+ renderer.traverse_handles(gltf.scene) do |handle|
163
+ # inspect loaded JS Object3D handles
164
+ end
165
+ ```
166
+
167
+ Do not make `Object3D#traverse` silently enter the JavaScript subtree. That method currently traverses Ruby-owned `Object3D` instances and should remain predictable on MRI Ruby and ruby.wasm.
168
+
169
+ ### External Object Mutability
170
+
171
+ Guard `ExternalObject3D#add`, `#remove`, and `#clear` for now.
172
+
173
+ Reason: the backend currently syncs Ruby child-list changes by clearing and re-adding children on the JavaScript handle. If a user adds Ruby children under an `ExternalObject3D`, a later sync can wipe the loaded glTF children. Until mixed Ruby/loaded children are explicitly designed, this should fail loudly.
174
+
175
+ Recommended behavior:
176
+
177
+ ```ruby
178
+ class Three::ExternalObject3D
179
+ def add(*)
180
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
181
+ end
182
+
183
+ def remove(*)
184
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
185
+ end
186
+
187
+ def clear
188
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
189
+ end
190
+ end
191
+ ```
192
+
193
+ Transform properties should continue to work:
194
+
195
+ ```ruby
196
+ gltf.scene.position.y = 1
197
+ gltf.scene.scale.set(2, 2, 2)
198
+ ```
199
+
200
+ ### Resource Disposal
201
+
202
+ Add `dispose_subtree` to renderer/backend instead of putting disposal directly on `GLTF`.
203
+
204
+ Recommended renderer API:
205
+
206
+ ```ruby
207
+ renderer.dispose_subtree(
208
+ gltf.scene,
209
+ remove: true,
210
+ dispose_geometries: true,
211
+ dispose_materials: true,
212
+ dispose_textures: true,
213
+ dispose_skeletons: true
214
+ )
215
+ ```
216
+
217
+ Default recommendation:
218
+
219
+ - `remove: true`
220
+ - `dispose_geometries: true`
221
+ - `dispose_materials: true`
222
+ - `dispose_textures: false` at the lowest backend layer
223
+ - `dispose_textures: true` for high-level loaded-asset cleanup helpers
224
+ - `dispose_skeletons: true`, but de-duplicate skeletons
225
+
226
+ Texture disposal must be explicit because textures can be shared. High-level asset cleanup may opt into it because it is normally called when unloading the whole asset.
227
+
228
+ ### Resource Collection
229
+
230
+ Disposal should collect unique resources before disposing:
231
+
232
+ - Object roots:
233
+ - optionally remove root from parent
234
+ - Geometries:
235
+ - `object.geometry`
236
+ - Materials:
237
+ - `object.material`
238
+ - arrays of materials
239
+ - Textures:
240
+ - common material slots:
241
+ - `map`
242
+ - `normalMap`
243
+ - `roughnessMap`
244
+ - `metalnessMap`
245
+ - `aoMap`
246
+ - `emissiveMap`
247
+ - `alphaMap`
248
+ - `bumpMap`
249
+ - `displacementMap`
250
+ - `envMap`
251
+ - `lightMap`
252
+ - `specularMap`
253
+ - `anisotropyMap`
254
+ - `clearcoatMap`
255
+ - `clearcoatNormalMap`
256
+ - `clearcoatRoughnessMap`
257
+ - `transmissionMap`
258
+ - `thicknessMap`
259
+ - `iridescenceMap`
260
+ - `iridescenceThicknessMap`
261
+ - `sheenColorMap`
262
+ - `sheenRoughnessMap`
263
+ - `specularColorMap`
264
+ - `specularIntensityMap`
265
+ - Skeletons:
266
+ - `object.skeleton`
267
+ - Scene resources when disposing a `Scene` or when explicitly requested:
268
+ - `scene.background`
269
+ - `scene.environment`
270
+
271
+ Do not rely on `renderer.info.memory` reaching zero as a strict assertion. three.js may keep internal resources for backgrounds, environments, and other renderer internals.
272
+
273
+ ## Backend Boundary
274
+
275
+ Prefer implementing loaded-asset traversal/disposal behind adapter methods:
276
+
277
+ ```ruby
278
+ def dispose_subtree(object, **options)
279
+ handle = materialize(object)
280
+ @adapter.dispose_object3d_subtree(handle, **options)
281
+ @handles.delete(object.uuid) if object.respond_to?(:uuid)
282
+ handle
283
+ end
284
+ ```
285
+
286
+ Adapter responsibilities:
287
+
288
+ - RubyWasmAdapter:
289
+ - use JavaScript `Object3D#traverse`
290
+ - collect JS `Set`s of resources
291
+ - call `parent.remove(object)` when `remove: true`
292
+ - call `dispose()` on collected resources
293
+ - FakeThreeJSAdapter:
294
+ - provide equivalent behavior for hash handles
295
+ - record calls for unit tests
296
+
297
+ This keeps browser-specific JavaScript traversal out of the pure Ruby object model.
298
+
299
+ ## Rejected Alternatives
300
+
301
+ ### Full Ruby Conversion of glTF
302
+
303
+ Rejected for now.
304
+
305
+ Why:
306
+
307
+ - It would require mirroring too many three.js loader semantics.
308
+ - It risks breaking shared references, skins, animations, extension metadata, and parser associations.
309
+ - It increases maintenance burden without improving the current browser-first MVP.
310
+
311
+ ### Make `ExternalObject3D#traverse` Enter the JS Subtree
312
+
313
+ Rejected for now.
314
+
315
+ Why:
316
+
317
+ - It changes the meaning of existing Ruby traversal.
318
+ - It makes behavior runtime-dependent: MRI Ruby cannot traverse JS handles.
319
+ - It hides expensive JS bridge calls behind a familiar pure Ruby method.
320
+
321
+ ### Dispose Everything Automatically on Remove
322
+
323
+ Rejected.
324
+
325
+ Why:
326
+
327
+ - three.js explicitly separates removal from disposal.
328
+ - Geometry, material, texture, and skeleton resources can be shared.
329
+ - Automatic disposal would surprise users and could break reused assets.
330
+
331
+ ## Implementation Plan
332
+
333
+ 1. Guard `ExternalObject3D#add`, `#remove`, and `#clear`.
334
+ 2. Add backend adapter support for traversing an external JS `Object3D` handle.
335
+ 3. Add resource collection in `RubyWasmAdapter`.
336
+ 4. Add `Three::Backends::ThreeJS#dispose_subtree`.
337
+ 5. Add `Three::Renderers::ThreeJSRenderer#dispose_subtree`.
338
+ 6. Add unit tests with `FakeThreeJSAdapter`.
339
+ 7. Add browser smoke coverage to `examples/browser/gltf/smoke_test.mjs`:
340
+ - attach disposal listeners to loaded geometry/material
341
+ - call `renderer.dispose_subtree(gltf.scene, remove: true, dispose_textures: true)`
342
+ - assert geometry/material dispose events fired
343
+ - assert root was removed from parent
344
+ 8. Update `docs/implementation-plan.md` current status and next tasks.
345
+
346
+ ## Suggested First Patch
347
+
348
+ Implement only the guard first:
349
+
350
+ ```ruby
351
+ class Three::ExternalObject3D < Object3D
352
+ def add(*)
353
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
354
+ end
355
+
356
+ def remove(*)
357
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
358
+ end
359
+
360
+ def clear
361
+ raise NotImplementedError, "ExternalObject3D does not support Ruby child mutation yet"
362
+ end
363
+ end
364
+ ```
365
+
366
+ Tests:
367
+
368
+ ```sh
369
+ ruby -Itest test/three/objects/external_object3d_test.rb
370
+ bundle exec rake test
371
+ pnpm test:browser:gltf
372
+ ```
373
+
374
+ ## Validation Commands
375
+
376
+ After implementing traversal/disposal helpers, run:
377
+
378
+ ```sh
379
+ bundle exec rake test
380
+ pnpm test:browser
381
+ pnpm audit --audit-level moderate
382
+ pnpm audit signatures
383
+ gem build three-rb.gemspec --output /private/tmp/three-rb-check.gem
384
+ git diff --check
385
+ ```
386
+
387
+ If the browser smoke tests fail with a local server sandbox error, rerun the same pnpm command with the approved escalated prefix.
388
+
389
+ ## Resume Checklist
390
+
391
+ If work resumes from this document alone:
392
+
393
+ 1. Read the status snapshot and decision sections above before editing code.
394
+ 2. Confirm the repository state with `git status --short --branch`.
395
+ 3. Start with the implementation plan in order unless the current code already includes some steps.
396
+ 4. Keep `Object3D#traverse` Ruby-only.
397
+ 5. Keep loaded glTF internals opaque unless a later design explicitly changes that.
398
+ 6. Add disposal behavior behind backend/adapter APIs, not directly inside the pure Ruby object graph.
399
+ 7. Use pnpm-managed browser commands for browser verification.
400
+ 8. Update this document if implementation results force a design change.
data/docs/next-work.md ADDED
@@ -0,0 +1,107 @@
1
+ # Next Work
2
+
3
+ This document is the resume point for the next implementation session. It is intentionally narrower than `docs/implementation-plan.md`: use it to decide what to do next after conversation context is lost.
4
+
5
+ Last updated: 2026-05-14.
6
+
7
+ ## Current Position
8
+
9
+ The project is past the original MVP and is in Phase 8, renderer maturity. The `0.1.0` browser-first alpha target is implemented and release-gate ready; the remaining release work is manual metadata finalization and publishing after owner confirmation.
10
+
11
+ Recent completed work:
12
+
13
+ - `MeshPhysicalMaterial` support.
14
+ - `RGBELoader` / `RGBETexture` support.
15
+ - `DRACOLoader` integration through `GLTFLoader#draco_decoder_path`.
16
+ - Initial postprocessing support with `EffectComposer`, `RenderPass`, and `UnrealBloomPass`.
17
+ - Release readiness checks, gem install smoke, release preflight, and publishing documentation.
18
+ - Saved JSON export/load fixture regression coverage for `Three::Exporters::ThreeJSONExporter` and `Three::Loaders::ThreeJSONLoader`.
19
+ - Browser examples overview and smoke command map for cube, composition, textures, cubemap, glTF, serialization, picking, primitives, and postprocessing.
20
+ - Browser runtime guide documenting the current ruby.wasm, import-map, `globalThis.THREE`, and `Three::Renderers::ThreeJSRenderer` boot contract.
21
+ - `Three::Postprocessing::OutputPass` support in the postprocessing composer example and browser smoke test.
22
+ - `MeshMatcapMaterial` support with matcap texture-slot sync, JSON export/load, resource disposal, and texture browser smoke coverage.
23
+ - `ShadowMaterial` support with backend sync, JSON export/load, and composition browser smoke coverage.
24
+ - `MeshToonMaterial` support with gradient-map texture-slot sync, JSON export/load, resource disposal, and texture browser smoke coverage.
25
+ - `Three::Postprocessing::DotScreenPass` support with composer integration, uniform update coverage, browser runtime boot contract updates, and postprocessing browser smoke coverage.
26
+ - `Sprite` / `SpriteMaterial` support with textured billboard marker sync, JSON export/load, resource disposal, and primitives browser smoke coverage.
27
+ - Saved JSON fixture coverage now includes `Sprite` / `SpriteMaterial` so the representative exporter/loader regression fixture matches the current primitive surface.
28
+ - Completed public API and documentation consistency pass for the current browser-first alpha scope.
29
+ - Local release gate and latest `main` CI are green for the `0.1.0` release candidate.
30
+
31
+ Do not start Phase 9 native renderer work yet. The implementation plan still recommends keeping browser rendering delegated to three.js through ruby.wasm until the browser-first API is more stable.
32
+
33
+ ## Recommended Next Task
34
+
35
+ Keep the `0.1.0` release candidate current while the release is deferred. Publish only after the release owner confirms the date and RubyGems credentials.
36
+
37
+ This is the best next step because:
38
+
39
+ - The representative JSON fixture covers the current primitive/material surface, including `Sprite` / `SpriteMaterial`.
40
+ - The public docs now cover release readiness, publishing, browser example coverage, browser runtime boot contract, and the current implemented API scope.
41
+ - `lib/three/version.rb` already contains `0.1.0`.
42
+ - `CHANGELOG.md` intentionally remains `## 0.1.0 - Unreleased` until the owner confirms the release date, but its contents should continue to reflect the current release-candidate surface.
43
+ - Further feature scope should wait until after the `0.1.0` release unless an already-advertised example breaks.
44
+
45
+ ## Scope
46
+
47
+ For deferred-release maintenance, keep `CHANGELOG.md`, `docs/release-readiness.md`, and this resume point aligned with the implemented release-candidate surface. Do not date the changelog, tag, push, or publish until the release owner confirms the release window.
48
+
49
+ When the owner is ready to publish, follow `docs/publishing.md`: confirm the release date, update `CHANGELOG.md`, run `bundle exec rake release:preflight`, commit the metadata update, publish the gem, tag `v0.1.0`, push `main` and the tag, then verify installation from RubyGems.
50
+
51
+ After the release is published, pick one feature target and keep the change small enough to verify through one browser example.
52
+
53
+ Candidate targets, in recommended order when there is no stronger product signal:
54
+
55
+ 1. Another small primitive or material workflow only when it reuses existing object/material parameter, JSON, backend sync, and browser smoke patterns. After `Sprite` / `SpriteMaterial`, prefer this only for a concrete example gap rather than API breadth.
56
+ 2. Another small postprocessing pass only when it can extend `examples/browser/postprocessing` without adding render targets. After `DotScreenPass`, prefer this only for a specific visual workflow rather than pass count.
57
+ 3. Render target support, but only with a focused example that proves why it is needed.
58
+ 4. A new addon loader only when a committed fixture requires it.
59
+ 5. KTX2 loader after texture-compression fixture coverage and decoder-path handling are planned.
60
+
61
+ ## Suggested Implementation Plan
62
+
63
+ 1. While release is deferred, keep the unreleased changelog and release-readiness docs current without changing the version, date, tag, or published artifact.
64
+ 2. When release is confirmed, finalize `CHANGELOG.md` with the confirmed release date.
65
+ 3. Run `bundle exec rake release:preflight`.
66
+ 4. Commit the release metadata with a message that does not include co-author trailers.
67
+ 5. Publish and tag using `docs/publishing.md`.
68
+ 6. After publishing, start the next feature from a user-visible workflow and choose exactly one feature target.
69
+ 7. Add Ruby API coverage, fake adapter/backend tests, JSON export/load coverage when the object is serializable, and resource-disposal coverage when it owns GPU resources.
70
+ 8. Add or extend one browser example and keep `examples/browser/README.md` in sync.
71
+ 9. Add or update a deterministic Playwright smoke command in `package.json`.
72
+ 10. Run Ruby tests, the affected browser smoke test, `bundle exec rake release:gem_smoke`, and `bundle exec rake release:preflight` before release work.
73
+
74
+ ## Acceptance Criteria
75
+
76
+ - `bundle exec rake test` passes.
77
+ - The chosen feature has one dedicated or clearly extended browser example.
78
+ - `examples/browser/README.md` documents the new coverage.
79
+ - `package.json` has a matching `test:browser:*` command when a new example is added.
80
+ - The affected browser smoke test passes.
81
+ - `bundle exec rake release:gem_smoke` still passes.
82
+
83
+ Optional after the next feature task:
84
+
85
+ - Run `bundle exec rake release:preflight` before release work or when browser/runtime code changed.
86
+ - Run `pnpm benchmark:browser` only if synchronization or renderer internals changed.
87
+
88
+ ## What Not To Do Next
89
+
90
+ Do not prioritize these without a clear product need:
91
+
92
+ - KTX2 loader.
93
+ - Additional postprocessing passes.
94
+ - Render target API.
95
+ - WebGPU renderer.
96
+ - Native renderer.
97
+ - Broad public API documentation beyond the current README, release readiness, implementation plan, browser examples overview, and browser runtime guide.
98
+
99
+ Those are valid later tasks, but they expand feature scope. The immediate gap is keeping the deferred release candidate accurate, then getting final release-owner confirmation and publishing before choosing future feature work by visible workflow and smoke-testability instead of API breadth.
100
+
101
+ ## After This Task
102
+
103
+ After the next feature task, reassess in this order:
104
+
105
+ 1. Keep the release gate passing after each change.
106
+ 2. Decide whether the new example reveals a natural follow-up feature.
107
+ 3. Delay Phase 9 native renderer work until the browser-first API is stable enough to justify a second renderer target.
@@ -0,0 +1,64 @@
1
+ # Publishing
2
+
3
+ This document records the manual steps for publishing `three-rb`. Do not run the publish step until the release owner has confirmed the version, changelog date, and RubyGems credentials.
4
+
5
+ ## Preflight
6
+
7
+ Start from a clean `main` branch:
8
+
9
+ ```sh
10
+ git status --short
11
+ pnpm install --frozen-lockfile --ignore-scripts
12
+ pnpm audit --audit-level moderate
13
+ pnpm audit signatures
14
+ pnpm exec playwright install chromium
15
+ bundle exec rake release:preflight
16
+ ```
17
+
18
+ `release:preflight` runs Ruby tests, builds and installs the gem into a temporary `GEM_HOME`, runs the install smoke test, runs all browser smoke tests, and builds the gem. It does not publish anything.
19
+
20
+ ## Prepare Release Metadata
21
+
22
+ For version `0.1.0`:
23
+
24
+ 1. Confirm `lib/three/version.rb` contains `VERSION = "0.1.0"`.
25
+ 2. Change `CHANGELOG.md` from `## 0.1.0 - Unreleased` to the release date.
26
+ 3. Run `bundle exec rake release:preflight` again.
27
+ 4. Commit the metadata update:
28
+
29
+ ```sh
30
+ git add CHANGELOG.md lib/three/version.rb
31
+ git commit -m "Prepare 0.1.0 release"
32
+ ```
33
+
34
+ Skip `lib/three/version.rb` in the commit if the version was already correct.
35
+
36
+ ## Publish
37
+
38
+ Publishing requires RubyGems credentials with MFA enabled.
39
+
40
+ ```sh
41
+ gem build three-rb.gemspec
42
+ gem push three-rb-0.1.0.gem
43
+ ```
44
+
45
+ After RubyGems accepts the gem, create and push the git tag:
46
+
47
+ ```sh
48
+ git tag -a v0.1.0 -m "Release 0.1.0"
49
+ git push origin main
50
+ git push origin v0.1.0
51
+ ```
52
+
53
+ If `gem push` fails, do not create the tag until the published artifact is confirmed.
54
+
55
+ ## Post-publish
56
+
57
+ Verify the public install path from a clean temporary directory:
58
+
59
+ ```sh
60
+ gem install three-rb -v 0.1.0
61
+ ruby -e 'require "three"; puts Three::VERSION'
62
+ ```
63
+
64
+ Then update issue tracker or release notes with the published RubyGems URL.
@@ -0,0 +1,83 @@
1
+ # Release Readiness
2
+
3
+ This document defines the quality gate for publishing the first public `three-rb` release.
4
+
5
+ ## Current Position
6
+
7
+ The project is past the original MVP. It is now in Phase 8, renderer maturity: core Ruby scene authoring, browser rendering, JSON export/load, interaction, asset loading, instancing, picking, shadows, and initial postprocessing are implemented and covered by unit or browser smoke tests.
8
+
9
+ The first public release should be positioned as browser-first alpha. The stable promise is narrow: users can build Ruby-authored scenes and render them in a browser through ruby.wasm and the delegated three.js backend. Native rendering and full three.js API compatibility are not part of the first release.
10
+
11
+ ## 0.1.0 Target and Completion Definition
12
+
13
+ The current release plan targets `0.1.0` as the first public browser-first alpha. This is not a `1.0` stable API commitment. A future `1.0` should get a separate stability plan covering compatibility guarantees, deprecation policy, support windows, and any non-browser renderer commitment.
14
+
15
+ The `0.1.0` target is complete when all of these are true:
16
+
17
+ - Scope is frozen to the browser-first alpha surface in this document and `README.md`; no additional material, loader, render-target, postprocessing, WebGPU, XR, native-renderer, or broad compatibility work is required for the release unless an already-advertised example is broken.
18
+ - Public API documentation matches the implemented, tested Ruby API surface loaded by `require "three"`.
19
+ - Snake-case Ruby methods are the documented API style. Broad camelCase three.js compatibility aliases are deferred until after `0.1.0`.
20
+ - `Three::Exporters::ThreeJSONExporter` and `Three::Loaders::ThreeJSONLoader` continue to round-trip Ruby-authored scenes covered by the saved `test/fixtures/scene_export_v1.json` regression fixture.
21
+ - JavaScript-loaded assets remain opaque `ExternalObject3D` roots for `0.1.0`; transform-level use, renderer traversal helpers, animation mixer usage, and explicit subtree disposal are in scope, while Ruby child mutation inside loaded external roots is out of scope.
22
+ - Every browser example advertised in `README.md` and `examples/browser/README.md` has a matching deterministic Playwright smoke command, and `pnpm test:browser` runs them all.
23
+ - The required gate below passes locally before tagging, and the CI workflow passes on the release commit.
24
+ - Release metadata is final: `lib/three/version.rb` contains `0.1.0`, `CHANGELOG.md` has either the unreleased heading during development or the final release date before publishing, and `docs/publishing.md` has the manual publish steps.
25
+
26
+ ## Public Scope
27
+
28
+ Included in the first public scope:
29
+
30
+ - Ruby object model for scenes, groups, transforms, cameras, lights, geometries, non-mesh primitives including sprites, common materials including matcap, toon, sprite, and shadow materials, textures, and common math primitives.
31
+ - Browser rendering through `Three::Renderers::ThreeJSRenderer`, ruby.wasm, and `three@0.184.0`.
32
+ - Dirty-tracked synchronization from Ruby objects into three.js handles.
33
+ - JSON export/load for Ruby-authored scenes.
34
+ - JavaScript-delegated browser integrations for textures, cube maps, RGBE environment maps, glTF, DRACO, animation mixers, OrbitControls, raycasting, instancing, shadows, and initial postprocessing with composer/render/bloom/dot-screen/output passes.
35
+ - Browser examples and smoke tests that verify visible rendering paths.
36
+
37
+ Explicitly out of scope for the first public scope:
38
+
39
+ - A native Ruby renderer.
40
+ - Full three.js API coverage.
41
+ - Compatibility aliases for every camelCase three.js API.
42
+ - Broad addon coverage beyond examples that are already tested.
43
+ - WebGPU, WebXR, node materials, editor integration, physics, and production asset pipeline guarantees.
44
+
45
+ ## Required Gate
46
+
47
+ Run these before publishing or tagging:
48
+
49
+ ```sh
50
+ pnpm install --frozen-lockfile --ignore-scripts
51
+ pnpm audit --audit-level moderate
52
+ pnpm audit signatures
53
+ pnpm exec playwright install chromium
54
+ bundle exec rake release:preflight
55
+ ```
56
+
57
+ Optional but recommended when renderer internals change:
58
+
59
+ ```sh
60
+ pnpm benchmark:browser
61
+ ```
62
+
63
+ ## Release Criteria
64
+
65
+ The release is acceptable when:
66
+
67
+ - The required gate passes locally and in CI.
68
+ - `CHANGELOG.md` describes the release as unreleased or tagged with the final date.
69
+ - README documents the browser-first alpha scope and unsupported areas.
70
+ - `bundle exec rake release:gem_smoke` proves the built gem can be installed into a temporary `GEM_HOME` and used without the repository `lib/` path.
71
+ - `bundle exec rake release:preflight` proves the Ruby tests, install smoke, browser smoke tests, and gem build pass without publishing.
72
+ - Browser smoke tests cover every advertised browser example.
73
+
74
+ ## Recommended Next Work
75
+
76
+ Before expanding feature scope, prefer:
77
+
78
+ 1. Keep release checks fast and deterministic.
79
+ 2. Keep fixture-based JSON export/load regression tests current.
80
+ 3. Keep `CHANGELOG.md` accurate while it remains under the `0.1.0 - Unreleased` heading.
81
+ 4. Improve public docs around browser examples, browser boot, and unsupported APIs; see `docs/next-work.md`.
82
+ 5. Add new material classes, postprocessing passes, render targets, or loaders only when a dedicated example and smoke test need them.
83
+ 6. Treat KTX2, WebGPU, WebXR, and native rendering as post-0.1 planning items.