@woosh/meep-engine 2.165.7 → 2.167.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 (146) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/package.json +1 -1
  3. package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
  4. package/src/core/binary/BinaryBuffer.js +1 -1
  5. package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
  6. package/src/core/binary/EncodingBinaryBuffer.js +3 -1
  7. package/src/core/cache/Cache.d.ts.map +1 -1
  8. package/src/core/cache/Cache.js +0 -4
  9. package/src/core/cache/CacheElement.d.ts.map +1 -1
  10. package/src/core/cache/CacheElement.js +4 -0
  11. package/src/core/cache/FrequencySketch.d.ts +0 -21
  12. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  13. package/src/core/cache/FrequencySketch.js +66 -65
  14. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
  15. package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
  16. package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
  17. package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
  18. package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
  19. package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
  20. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
  21. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
  22. package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
  23. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
  24. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
  25. package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
  26. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
  27. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
  28. package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
  29. package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
  30. package/src/core/collection/RingBuffer.d.ts.map +1 -1
  31. package/src/core/collection/RingBuffer.js +15 -3
  32. package/src/core/collection/map/HashMap.d.ts +1 -1
  33. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  34. package/src/core/collection/map/HashMap.js +5 -17
  35. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
  36. package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
  37. package/src/core/geom/ConicRay.d.ts.map +1 -1
  38. package/src/core/geom/ConicRay.js +9 -2
  39. package/src/core/geom/Quaternion.d.ts.map +1 -1
  40. package/src/core/geom/Quaternion.js +7 -3
  41. package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
  42. package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
  43. package/src/core/math/build_gaussian_kernel_1d.js +4 -2
  44. package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
  45. package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
  46. package/src/core/math/build_gaussian_kernel_2d.js +4 -2
  47. package/src/core/math/gaussian.d.ts +2 -2
  48. package/src/core/math/gaussian.js +2 -2
  49. package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
  50. package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
  51. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
  52. package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
  53. package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
  54. package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
  55. package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
  56. package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
  57. package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
  58. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
  59. package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
  60. package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
  61. package/src/core/math/statistics/computeStatisticalMean.js +1 -1
  62. package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
  63. package/src/core/math/statistics/gaussian_amplitude.js +3 -1
  64. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  65. package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
  66. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
  67. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
  68. package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
  69. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
  70. package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
  71. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  72. package/src/engine/ecs/EntityComponentDataset.js +5 -21
  73. package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
  74. package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
  75. package/src/engine/ecs/fow/FogOfWar.js +202 -54
  76. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
  77. package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
  78. package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
  79. package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
  80. package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
  81. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
  82. package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
  83. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
  84. package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
  85. package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
  86. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
  87. package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
  88. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
  89. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
  90. package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
  91. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  92. package/src/engine/graphics/GraphicsEngine.js +14 -3
  93. package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
  94. package/src/engine/graphics/composit/CompositLayer.js +258 -255
  95. package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
  96. package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
  97. package/src/engine/graphics/composit/LayerCompositer.js +19 -10
  98. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
  99. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
  100. package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
  101. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
  102. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
  103. package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
  104. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
  105. package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
  106. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
  107. package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
  108. package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
  109. package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
  110. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
  111. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
  112. package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
  113. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
  114. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
  115. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
  116. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
  117. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
  118. package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
  119. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
  120. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
  121. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
  122. package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
  123. package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
  124. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
  125. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  126. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
  127. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
  128. package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
  129. package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
  130. package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
  131. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
  132. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
  133. package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
  134. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  135. package/src/engine/graphics/trail/tube/TubeX.js +557 -555
  136. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
  137. package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
  138. package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
  139. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  140. package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
  141. package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
  142. package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
  143. package/src/engine/navigation/ecs/components/Path.js +10 -4
  144. package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
  145. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  146. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
@@ -1,555 +1,557 @@
1
- import {
2
- BufferAttribute,
3
- BufferGeometry,
4
- DynamicDrawUsage,
5
- InterleavedBuffer,
6
- InterleavedBufferAttribute
7
- } from "three";
8
- import { assert } from "../../../../core/assert.js";
9
- import { RowFirstTable } from "../../../../core/collection/table/RowFirstTable.js";
10
- import { aabb3_array_set } from "../../../../core/geom/3d/aabb/aabb3_array_set.js";
11
- import { v3_orthonormal_matrix_from_normal } from "../../../../core/geom/vec3/v3_orthonormal_matrix_from_normal.js";
12
- import { max2 } from "../../../../core/math/max2.js";
13
- import { min2 } from "../../../../core/math/min2.js";
14
- import {
15
- TUBE_ATTRIBUTE_ADDRESS_ALPHA,
16
- TUBE_ATTRIBUTE_ADDRESS_BINORMAL,
17
- TUBE_ATTRIBUTE_ADDRESS_COLOR,
18
- TUBE_ATTRIBUTE_ADDRESS_NORMAL,
19
- TUBE_ATTRIBUTE_ADDRESS_OFF,
20
- TUBE_ATTRIBUTE_ADDRESS_POSITION,
21
- TUBE_ATTRIBUTE_ADDRESS_THICKNESS,
22
- tube_attributes_spec
23
- } from "./tube_attributes_spec.js";
24
-
25
- /**
26
- * Default number of radial subdivisions around the tube. Overridable per-tube
27
- * via {@link TubeX#setCount}; never hard-coded into the geometry.
28
- * @type {number}
29
- */
30
- const DEFAULT_RADIAL_SEGMENTS = 8;
31
-
32
- /**
33
- * Scratch for the arbitrary default frame written at build time; module-static so
34
- * setCount stays allocation-free on the data path.
35
- * @type {Float64Array}
36
- */
37
- const m3_default_frame = new Float64Array(9);
38
-
39
- /**
40
- * Dynamic, non-allocating 3D tube geometry — the volumetric analogue of
41
- * {@link RibbonX}.
42
- *
43
- * A tube is a ring buffer of `count` KNOTS along a centre-line. Each knot owns a
44
- * ring of `radial + 1` vertices: the extra vertex duplicates vertex 0 so the UV
45
- * seam (u = 0 meeting u = 1) closes cleanly when the texture wraps the tube.
46
- * Vertices are extruded in the vertex shader from the knot's stored tangent
47
- * frame, so the CPU only ever writes per-knot data (centre, frame, colour, …) —
48
- * never per-triangle.
49
- *
50
- * Like {@link RibbonX} the geometry is a fixed-capacity ring: {@link rotate}
51
- * recycles the oldest knot as the new head and rewrites a single segment's worth
52
- * of indices, so steady-state updates allocate nothing.
53
- */
54
- export class TubeX {
55
-
56
- /**
57
- * @type {BufferGeometry}
58
- */
59
- #geometry = new BufferGeometry();
60
-
61
- /**
62
- * @type {RowFirstTable}
63
- */
64
- #data = new RowFirstTable(tube_attributes_spec);
65
-
66
- /**
67
- * Knot index at the END (oldest) of the tube.
68
- * @type {number}
69
- */
70
- #tail_index = 0;
71
-
72
- /**
73
- * Knot index at the START (newest) of the tube.
74
- * @type {number}
75
- */
76
- #head_index = 0;
77
-
78
- /**
79
- * Index of the segment (knot-to-knot span of `radial` quads) currently at the
80
- * tail; recycled by {@link rotate}.
81
- * @type {number}
82
- */
83
- #tail_segment_index = 0;
84
-
85
- /**
86
- * Number of knots.
87
- * @type {number}
88
- */
89
- #count = 0;
90
-
91
- /**
92
- * Number of radial subdivisions. Ring vertex count per knot = radial + 1.
93
- * @type {number}
94
- */
95
- #radial = DEFAULT_RADIAL_SEGMENTS;
96
-
97
- // Interleaved buffers + attribute views over #data; created in buildGeometry().
98
- #ib_uint8 = null;
99
- #ib_float32 = null;
100
- #iba_position = null;
101
- #iba_normal = null;
102
- #iba_binormal = null;
103
- #iba_uv = null;
104
- #iba_alpha = null;
105
- #iba_thickness = null;
106
- #iba_color = null;
107
- #ba_index = null;
108
-
109
- dispose() {
110
- this.#geometry.dispose();
111
- }
112
-
113
- /**
114
- * @returns {BufferGeometry}
115
- */
116
- getGeometry() {
117
- return this.#geometry;
118
- }
119
-
120
- /**
121
- * @returns {number} number of knots
122
- */
123
- getCount() {
124
- return this.#count;
125
- }
126
-
127
- /**
128
- * @returns {number} radial subdivisions
129
- */
130
- getRadialSegments() {
131
- return this.#radial;
132
- }
133
-
134
- getHeadIndex() {
135
- return this.#head_index;
136
- }
137
-
138
- getTailIndex() {
139
- return this.#tail_index;
140
- }
141
-
142
- /**
143
- * Number of vertices in a single knot ring (includes the duplicated seam vertex).
144
- * @returns {number}
145
- */
146
- get #ring_vertex_count() {
147
- return this.#radial + 1;
148
- }
149
-
150
- buildGeometry() {
151
- const a_uint8 = new Uint8Array(this.#data.data);
152
- const a_float32 = new Float32Array(this.#data.data);
153
-
154
- const bpr = this.#data.spec.bytesPerRecord;
155
-
156
- this.#ib_uint8 = new InterleavedBuffer(a_uint8, bpr);
157
- this.#ib_float32 = new InterleavedBuffer(a_float32, bpr >> 2);
158
-
159
- this.#iba_position = new InterleavedBufferAttribute(this.#ib_float32, 3, 1, false);
160
- this.#iba_normal = new InterleavedBufferAttribute(this.#ib_float32, 3, 4, false);
161
- this.#iba_binormal = new InterleavedBufferAttribute(this.#ib_float32, 3, 7, false);
162
-
163
- // off (uv.x) and uv_v (uv.y) are contiguous, exposed as a single vec2.
164
- this.#iba_uv = new InterleavedBufferAttribute(this.#ib_float32, 2, 10, false);
165
- this.#iba_alpha = new InterleavedBufferAttribute(this.#ib_float32, 1, 12, false);
166
- this.#iba_thickness = new InterleavedBufferAttribute(this.#ib_float32, 1, 13, false);
167
-
168
- this.#iba_color = new InterleavedBufferAttribute(this.#ib_uint8, 3, 56, false);
169
-
170
- // Uint32 (not Uint16): vertex count = count * (radial + 1) easily exceeds
171
- // 65535 for a long tube with high radial resolution. WebGL2 supports it natively.
172
- this.#ba_index = new BufferAttribute(new Uint32Array(0), 1, false);
173
-
174
- this.#geometry.setAttribute('position', this.#iba_position);
175
- this.#geometry.setAttribute('tn', this.#iba_normal);
176
- this.#geometry.setAttribute('tb', this.#iba_binormal);
177
- this.#geometry.setAttribute('tubeUv', this.#iba_uv);
178
- this.#geometry.setAttribute('alpha', this.#iba_alpha);
179
- this.#geometry.setAttribute('thickness', this.#iba_thickness);
180
- this.#geometry.setAttribute('color', this.#iba_color);
181
-
182
- this.#geometry.setIndex(this.#ba_index);
183
-
184
- this.#ib_uint8.setUsage(DynamicDrawUsage);
185
- this.#ib_float32.setUsage(DynamicDrawUsage);
186
- this.#ba_index.setUsage(DynamicDrawUsage);
187
- }
188
-
189
- /**
190
- * Write the static angular `off` coordinate (uv.x) for every ring vertex. The
191
- * value goes 0 → 1 across the `radial + 1` vertices; vertex `radial` lands on
192
- * 1.0 so its shader angle is 2π — co-incident with vertex 0's angle 0, closing
193
- * the seam without a wrap-around index.
194
- */
195
- #initialize_off() {
196
- const data = this.#data;
197
- const count = this.#count;
198
- const radial = this.#radial;
199
- const v = this.#ring_vertex_count;
200
-
201
- for (let k = 0; k < count; k++) {
202
- const base = k * v;
203
- for (let j = 0; j <= radial; j++) {
204
- data.writeCellValue(base + j, TUBE_ATTRIBUTE_ADDRESS_OFF, j / radial);
205
- }
206
- }
207
- }
208
-
209
- /**
210
- * Seed every knot with a valid (arbitrary) orthonormal frame so the geometry is
211
- * well-formed from the moment it is built, before any head movement supplies a
212
- * real tangent. Without this the frames would be zero-initialised and the
213
- * transport's degeneracy fallback would have to cover the fresh-tube case; a
214
- * concrete default keeps the invariant "every knot frame is unit + orthonormal"
215
- * unconditionally true (read e.g. by getKnotNormal during transport).
216
- */
217
- #initialize_frame() {
218
- v3_orthonormal_matrix_from_normal(m3_default_frame, 0, 0, 1, 0);
219
-
220
- const nx = m3_default_frame[0], ny = m3_default_frame[1], nz = m3_default_frame[2];
221
- const bx = m3_default_frame[3], by = m3_default_frame[4], bz = m3_default_frame[5];
222
-
223
- const count = this.#count;
224
-
225
- for (let k = 0; k < count; k++) {
226
- this.setKnotFrame(k, nx, ny, nz, bx, by, bz);
227
- }
228
- }
229
-
230
- /**
231
- * Write the `radial` quads (2 triangles each) bridging the ring of knot `lo`
232
- * to the ring of knot `hi`, starting at `address` in the index array.
233
- * Winding matches the engine's static tube mesher (make_ring_faces).
234
- * @param {number} address index-array offset (in index entries)
235
- * @param {number} lo knot index whose ring is the "left" edge
236
- * @param {number} hi knot index whose ring is the "right" edge
237
- */
238
- #write_segment_indices(address, lo, hi) {
239
- const indices = this.#ba_index.array;
240
- const radial = this.#radial;
241
- const v = this.#ring_vertex_count;
242
-
243
- const lo_base = lo * v;
244
- const hi_base = hi * v;
245
-
246
- for (let r = 0; r < radial; r++) {
247
- const a = lo_base + r;
248
- const b = hi_base + r;
249
- const c = hi_base + r + 1;
250
- const d = lo_base + r + 1;
251
-
252
- const ci = address + r * 6;
253
-
254
- indices[ci] = a;
255
- indices[ci + 1] = b;
256
- indices[ci + 2] = d;
257
-
258
- indices[ci + 3] = b;
259
- indices[ci + 4] = c;
260
- indices[ci + 5] = d;
261
- }
262
- }
263
-
264
- /**
265
- * Build the index buffer as a linear strip of `count - 1` segments.
266
- */
267
- #initialize_index() {
268
- const segment_count = this.#count - 1;
269
- const indices_per_segment = this.#radial * 6;
270
-
271
- for (let s = 0; s < segment_count; s++) {
272
- this.#write_segment_indices(s * indices_per_segment, s, s + 1);
273
- }
274
- }
275
-
276
- /**
277
- * @param {number[]|Float32Array} result
278
- * @param {number} knot_index
279
- */
280
- getKnotPosition(result, knot_index) {
281
- const row = knot_index * this.#ring_vertex_count;
282
- const data = this.#data;
283
-
284
- result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION);
285
- result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 1);
286
- result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 2);
287
- }
288
-
289
- /**
290
- * Read the stored ring normal of a knot. Used by the head to parallel-transport
291
- * its frame from the previous knot.
292
- * @param {number[]|Float32Array} result
293
- * @param {number} knot_index
294
- */
295
- getKnotNormal(result, knot_index) {
296
- const row = knot_index * this.#ring_vertex_count;
297
- const data = this.#data;
298
-
299
- result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL);
300
- result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL + 1);
301
- result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL + 2);
302
- }
303
-
304
- /**
305
- * Read the stored ring binormal of a knot.
306
- * @param {number[]|Float32Array} result
307
- * @param {number} knot_index
308
- */
309
- getKnotBinormal(result, knot_index) {
310
- const row = knot_index * this.#ring_vertex_count;
311
- const data = this.#data;
312
-
313
- result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL);
314
- result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL + 1);
315
- result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL + 2);
316
- }
317
-
318
- /**
319
- * Write a vec3 attribute to all ring vertices of a knot.
320
- * @param {number} knot_index
321
- * @param {number} attribute_index
322
- * @param {number} x
323
- * @param {number} y
324
- * @param {number} z
325
- */
326
- setKnotAttribute_Vector3(knot_index, attribute_index, x, y, z) {
327
- const data = this.#data;
328
- const v = this.#ring_vertex_count;
329
- const base = knot_index * v;
330
-
331
- for (let j = 0; j < v; j++) {
332
- const row = base + j;
333
- data.writeCellValue(row, attribute_index, x);
334
- data.writeCellValue(row, attribute_index + 1, y);
335
- data.writeCellValue(row, attribute_index + 2, z);
336
- }
337
- }
338
-
339
- /**
340
- * Write a scalar attribute to all ring vertices of a knot.
341
- * @param {number} knot_index
342
- * @param {number} attribute_index
343
- * @param {number} value
344
- */
345
- setKnotAttribute_Scalar(knot_index, attribute_index, value) {
346
- const data = this.#data;
347
- const v = this.#ring_vertex_count;
348
- const base = knot_index * v;
349
-
350
- for (let j = 0; j < v; j++) {
351
- data.writeCellValue(base + j, attribute_index, value);
352
- }
353
- }
354
-
355
- /**
356
- * Add `delta` to a scalar attribute on every ring vertex of a knot.
357
- * @param {number} knot_index
358
- * @param {number} attribute_index
359
- * @param {number} delta
360
- * @returns {number} incremented value
361
- */
362
- incrementKnotAttribute_Scalar(knot_index, attribute_index, delta) {
363
- const data = this.#data;
364
- const v = this.#ring_vertex_count;
365
- const base = knot_index * v;
366
-
367
- const value = data.readCellValue(base, attribute_index) + delta;
368
-
369
- for (let j = 0; j < v; j++) {
370
- data.writeCellValue(base + j, attribute_index, value);
371
- }
372
-
373
- return value;
374
- }
375
-
376
- /**
377
- * @param {number} knot_index
378
- * @param {number} x
379
- * @param {number} y
380
- * @param {number} z
381
- */
382
- setKnotPosition(knot_index, x, y, z) {
383
- assert.isNonNegativeInteger(knot_index, 'knot_index');
384
-
385
- this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_POSITION, x, y, z);
386
-
387
- this.#ib_float32.needsUpdate = true;
388
- }
389
-
390
- /**
391
- * Set the orienting tangent frame of a knot. `nx,ny,nz` and `bx,by,bz` must be
392
- * unit vectors, mutually orthogonal and perpendicular to the local tangent.
393
- * @param {number} knot_index
394
- * @param {number} nx
395
- * @param {number} ny
396
- * @param {number} nz
397
- * @param {number} bx
398
- * @param {number} by
399
- * @param {number} bz
400
- */
401
- setKnotFrame(knot_index, nx, ny, nz, bx, by, bz) {
402
- this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_NORMAL, nx, ny, nz);
403
- this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_BINORMAL, bx, by, bz);
404
-
405
- this.#ib_float32.needsUpdate = true;
406
- }
407
-
408
- /**
409
- * @param {number} knot_index
410
- * @param {number} r 0..255
411
- * @param {number} g 0..255
412
- * @param {number} b 0..255
413
- */
414
- setKnotColor(knot_index, r, g, b) {
415
- assert.isNonNegativeInteger(knot_index, 'knot_index');
416
-
417
- this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_COLOR, r, g, b);
418
-
419
- this.#ib_uint8.needsUpdate = true;
420
- }
421
-
422
- /**
423
- * @param {number} knot_index
424
- * @param {number} value
425
- */
426
- setKnotAlpha(knot_index, value) {
427
- this.setKnotAttribute_Scalar(knot_index, TUBE_ATTRIBUTE_ADDRESS_ALPHA, value);
428
-
429
- this.#ib_float32.needsUpdate = true;
430
- }
431
-
432
- /**
433
- * @param {number} knot_index
434
- * @param {number} value tube diameter
435
- */
436
- setKnotThickness(knot_index, value) {
437
- this.setKnotAttribute_Scalar(knot_index, TUBE_ATTRIBUTE_ADDRESS_THICKNESS, value);
438
-
439
- this.#ib_float32.needsUpdate = true;
440
- }
441
-
442
- /**
443
- * @param {number} count number of knots (>= 2)
444
- * @param {number} [radial] radial subdivisions (>= 3)
445
- */
446
- setCount(count, radial = DEFAULT_RADIAL_SEGMENTS) {
447
- assert.greaterThanOrEqual(count, 2, 'a tube needs at least 2 knots (one segment)');
448
- assert.isNonNegativeInteger(count, 'count');
449
- assert.greaterThanOrEqual(radial, 3, 'a tube ring needs at least 3 radial segments');
450
- assert.isNonNegativeInteger(radial, 'radial');
451
-
452
- this.#geometry.dispose();
453
-
454
- this.#radial = radial;
455
-
456
- const v = radial + 1;
457
- const vertex_count = count * v;
458
-
459
- if (this.#data.length > vertex_count) {
460
- this.#data.removeRows(vertex_count, this.#data.length - vertex_count);
461
- }
462
-
463
- this.#data.setCapacity(vertex_count);
464
- this.#data.length = vertex_count;
465
-
466
- const a_uint8 = new Uint8Array(this.#data.data);
467
- const a_float32 = new Float32Array(this.#data.data);
468
-
469
- const segment_count = count - 1;
470
- const triangle_count = segment_count * radial * 2;
471
- const index_array_size = triangle_count * 3;
472
- const index_array = new Uint32Array(index_array_size);
473
-
474
- this.#ba_index.array = index_array;
475
- this.#ba_index.count = index_array_size;
476
-
477
- this.#ib_uint8.array = a_uint8;
478
- this.#ib_float32.array = a_float32;
479
-
480
- this.#ib_uint8.count = vertex_count;
481
- this.#ib_float32.count = vertex_count;
482
-
483
- this.#head_index = 0;
484
- this.#tail_index = count - 1;
485
- this.#tail_segment_index = segment_count - 1;
486
-
487
- this.#count = count;
488
-
489
- this.#initialize_off();
490
- this.#initialize_frame();
491
- this.#initialize_index();
492
- }
493
-
494
- /**
495
- * NOTE: takes tube thickness into account.
496
- * @param {AABB3|ArrayLike<number>} result
497
- */
498
- computeBoundingBox(result) {
499
- const count = this.#count;
500
- const v = this.#ring_vertex_count;
501
- const data = this.#data;
502
-
503
- let _x0 = Infinity, _y0 = Infinity, _z0 = Infinity;
504
- let _x1 = -Infinity, _y1 = -Infinity, _z1 = -Infinity;
505
-
506
- for (let k = 0; k < count; k++) {
507
- const row = k * v;
508
-
509
- const half_thickness = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_THICKNESS) * 0.5;
510
-
511
- const x = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION);
512
- const y = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 1);
513
- const z = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 2);
514
-
515
- _x0 = min2(_x0, x - half_thickness);
516
- _y0 = min2(_y0, y - half_thickness);
517
- _z0 = min2(_z0, z - half_thickness);
518
- _x1 = max2(_x1, x + half_thickness);
519
- _y1 = max2(_y1, y + half_thickness);
520
- _z1 = max2(_z1, z + half_thickness);
521
- }
522
-
523
- aabb3_array_set(result, 0, _x0, _y0, _z0, _x1, _y1, _z1);
524
- }
525
-
526
- /**
527
- * Recycle the oldest knot as the new head, in O(radial). Mirrors
528
- * {@link RibbonX#rotate}: the head/tail pointers decrement together (preserving
529
- * `head === (tail + 1) mod count`), and the single tail segment's indices are
530
- * rewritten to bridge the previous head's ring to the recycled (new head) ring.
531
- */
532
- rotate() {
533
- const old_tail = this.#tail_index;
534
- const old_head = this.#head_index;
535
-
536
- const max_index = this.#count - 1;
537
-
538
- // The recycled slot (old tail) becomes the new head: head and tail step
539
- // back in lockstep so head stays exactly one slot ahead of tail.
540
- this.#tail_index = old_tail <= 0 ? max_index : old_tail - 1;
541
- this.#head_index = old_head <= 0 ? max_index : old_head - 1;
542
-
543
- const old_segment = this.#tail_segment_index;
544
- const address = old_segment * this.#radial * 6;
545
-
546
- this.#tail_segment_index = old_segment > 0 ? old_segment - 1 : this.#count - 2;
547
-
548
- // Bridge old head -> new head (== old tail slot) with a fresh segment.
549
- this.#write_segment_indices(address, old_head, old_tail);
550
-
551
- this.#ba_index.needsUpdate = true;
552
- }
553
- }
554
-
555
- TubeX.DEFAULT_RADIAL_SEGMENTS = DEFAULT_RADIAL_SEGMENTS;
1
+ import {
2
+ BufferAttribute,
3
+ BufferGeometry,
4
+ DynamicDrawUsage,
5
+ InterleavedBuffer,
6
+ InterleavedBufferAttribute
7
+ } from "three";
8
+ import { assert } from "../../../../core/assert.js";
9
+ import { RowFirstTable } from "../../../../core/collection/table/RowFirstTable.js";
10
+ import { aabb3_array_set } from "../../../../core/geom/3d/aabb/aabb3_array_set.js";
11
+ import { v3_orthonormal_matrix_from_normal } from "../../../../core/geom/vec3/v3_orthonormal_matrix_from_normal.js";
12
+ import { max2 } from "../../../../core/math/max2.js";
13
+ import { min2 } from "../../../../core/math/min2.js";
14
+ import {
15
+ TUBE_ATTRIBUTE_ADDRESS_ALPHA,
16
+ TUBE_ATTRIBUTE_ADDRESS_BINORMAL,
17
+ TUBE_ATTRIBUTE_ADDRESS_COLOR,
18
+ TUBE_ATTRIBUTE_ADDRESS_NORMAL,
19
+ TUBE_ATTRIBUTE_ADDRESS_OFF,
20
+ TUBE_ATTRIBUTE_ADDRESS_POSITION,
21
+ TUBE_ATTRIBUTE_ADDRESS_THICKNESS,
22
+ tube_attributes_spec
23
+ } from "./tube_attributes_spec.js";
24
+
25
+ /**
26
+ * Default number of radial subdivisions around the tube. Overridable per-tube
27
+ * via {@link TubeX#setCount}; never hard-coded into the geometry.
28
+ * @type {number}
29
+ */
30
+ const DEFAULT_RADIAL_SEGMENTS = 8;
31
+
32
+ /**
33
+ * Scratch for the arbitrary default frame written at build time; module-static so
34
+ * setCount stays allocation-free on the data path.
35
+ * @type {Float64Array}
36
+ */
37
+ const m3_default_frame = new Float64Array(9);
38
+
39
+ /**
40
+ * Dynamic, non-allocating 3D tube geometry — the volumetric analogue of
41
+ * {@link RibbonX}.
42
+ *
43
+ * A tube is a ring buffer of `count` KNOTS along a centre-line. Each knot owns a
44
+ * ring of `radial + 1` vertices: the extra vertex duplicates vertex 0 so the UV
45
+ * seam (u = 0 meeting u = 1) closes cleanly when the texture wraps the tube.
46
+ * Vertices are extruded in the vertex shader from the knot's stored tangent
47
+ * frame, so the CPU only ever writes per-knot data (centre, frame, colour, …) —
48
+ * never per-triangle.
49
+ *
50
+ * Like {@link RibbonX} the geometry is a fixed-capacity ring: {@link rotate}
51
+ * recycles the oldest knot as the new head and rewrites a single segment's worth
52
+ * of indices, so steady-state updates allocate nothing.
53
+ */
54
+ export class TubeX {
55
+
56
+ /**
57
+ * @type {BufferGeometry}
58
+ */
59
+ #geometry = new BufferGeometry();
60
+
61
+ /**
62
+ * @type {RowFirstTable}
63
+ */
64
+ #data = new RowFirstTable(tube_attributes_spec);
65
+
66
+ /**
67
+ * Knot index at the END (oldest) of the tube.
68
+ * @type {number}
69
+ */
70
+ #tail_index = 0;
71
+
72
+ /**
73
+ * Knot index at the START (newest) of the tube.
74
+ * @type {number}
75
+ */
76
+ #head_index = 0;
77
+
78
+ /**
79
+ * Index of the segment (knot-to-knot span of `radial` quads) currently at the
80
+ * tail; recycled by {@link rotate}.
81
+ * @type {number}
82
+ */
83
+ #tail_segment_index = 0;
84
+
85
+ /**
86
+ * Number of knots.
87
+ * @type {number}
88
+ */
89
+ #count = 0;
90
+
91
+ /**
92
+ * Number of radial subdivisions. Ring vertex count per knot = radial + 1.
93
+ * @type {number}
94
+ */
95
+ #radial = DEFAULT_RADIAL_SEGMENTS;
96
+
97
+ // Interleaved buffers + attribute views over #data; created in buildGeometry().
98
+ #ib_uint8 = null;
99
+ #ib_float32 = null;
100
+ #iba_position = null;
101
+ #iba_normal = null;
102
+ #iba_binormal = null;
103
+ #iba_uv = null;
104
+ #iba_alpha = null;
105
+ #iba_thickness = null;
106
+ #iba_color = null;
107
+ #ba_index = null;
108
+
109
+ dispose() {
110
+ this.#geometry.dispose();
111
+ }
112
+
113
+ /**
114
+ * @returns {BufferGeometry}
115
+ */
116
+ getGeometry() {
117
+ return this.#geometry;
118
+ }
119
+
120
+ /**
121
+ * @returns {number} number of knots
122
+ */
123
+ getCount() {
124
+ return this.#count;
125
+ }
126
+
127
+ /**
128
+ * @returns {number} radial subdivisions
129
+ */
130
+ getRadialSegments() {
131
+ return this.#radial;
132
+ }
133
+
134
+ getHeadIndex() {
135
+ return this.#head_index;
136
+ }
137
+
138
+ getTailIndex() {
139
+ return this.#tail_index;
140
+ }
141
+
142
+ /**
143
+ * Number of vertices in a single knot ring (includes the duplicated seam vertex).
144
+ * @returns {number}
145
+ */
146
+ get #ring_vertex_count() {
147
+ return this.#radial + 1;
148
+ }
149
+
150
+ buildGeometry() {
151
+ const a_uint8 = new Uint8Array(this.#data.data);
152
+ const a_float32 = new Float32Array(this.#data.data);
153
+
154
+ const bpr = this.#data.spec.bytesPerRecord;
155
+
156
+ this.#ib_uint8 = new InterleavedBuffer(a_uint8, bpr);
157
+ this.#ib_float32 = new InterleavedBuffer(a_float32, bpr >> 2);
158
+
159
+ this.#iba_position = new InterleavedBufferAttribute(this.#ib_float32, 3, 1, false);
160
+ this.#iba_normal = new InterleavedBufferAttribute(this.#ib_float32, 3, 4, false);
161
+ this.#iba_binormal = new InterleavedBufferAttribute(this.#ib_float32, 3, 7, false);
162
+
163
+ // off (uv.x) and uv_v (uv.y) are contiguous, exposed as a single vec2.
164
+ this.#iba_uv = new InterleavedBufferAttribute(this.#ib_float32, 2, 10, false);
165
+ this.#iba_alpha = new InterleavedBufferAttribute(this.#ib_float32, 1, 12, false);
166
+ this.#iba_thickness = new InterleavedBufferAttribute(this.#ib_float32, 1, 13, false);
167
+
168
+ this.#iba_color = new InterleavedBufferAttribute(this.#ib_uint8, 3, 56, false);
169
+
170
+ // Uint32 (not Uint16): vertex count = count * (radial + 1) easily exceeds
171
+ // 65535 for a long tube with high radial resolution. WebGL2 supports it natively.
172
+ this.#ba_index = new BufferAttribute(new Uint32Array(0), 1, false);
173
+
174
+ this.#geometry.setAttribute('position', this.#iba_position);
175
+ this.#geometry.setAttribute('tn', this.#iba_normal);
176
+ this.#geometry.setAttribute('tb', this.#iba_binormal);
177
+ this.#geometry.setAttribute('tubeUv', this.#iba_uv);
178
+ this.#geometry.setAttribute('alpha', this.#iba_alpha);
179
+ this.#geometry.setAttribute('thickness', this.#iba_thickness);
180
+ this.#geometry.setAttribute('color', this.#iba_color);
181
+
182
+ this.#geometry.setIndex(this.#ba_index);
183
+
184
+ this.#ib_uint8.setUsage(DynamicDrawUsage);
185
+ this.#ib_float32.setUsage(DynamicDrawUsage);
186
+ this.#ba_index.setUsage(DynamicDrawUsage);
187
+ }
188
+
189
+ /**
190
+ * Write the static angular `off` coordinate (uv.x) for every ring vertex. The
191
+ * value goes 0 → 1 across the `radial + 1` vertices; vertex `radial` lands on
192
+ * 1.0 so its shader angle is 2π — co-incident with vertex 0's angle 0, closing
193
+ * the seam without a wrap-around index.
194
+ */
195
+ #initialize_off() {
196
+ const data = this.#data;
197
+ const count = this.#count;
198
+ const radial = this.#radial;
199
+ const v = this.#ring_vertex_count;
200
+
201
+ for (let k = 0; k < count; k++) {
202
+ const base = k * v;
203
+ for (let j = 0; j <= radial; j++) {
204
+ data.writeCellValue(base + j, TUBE_ATTRIBUTE_ADDRESS_OFF, j / radial);
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Seed every knot with a valid (arbitrary) orthonormal frame so the geometry is
211
+ * well-formed from the moment it is built, before any head movement supplies a
212
+ * real tangent. Without this the frames would be zero-initialised and the
213
+ * transport's degeneracy fallback would have to cover the fresh-tube case; a
214
+ * concrete default keeps the invariant "every knot frame is unit + orthonormal"
215
+ * unconditionally true (read e.g. by getKnotNormal during transport).
216
+ */
217
+ #initialize_frame() {
218
+ v3_orthonormal_matrix_from_normal(m3_default_frame, 0, 0, 1, 0);
219
+
220
+ const nx = m3_default_frame[0], ny = m3_default_frame[1], nz = m3_default_frame[2];
221
+ const bx = m3_default_frame[3], by = m3_default_frame[4], bz = m3_default_frame[5];
222
+
223
+ const count = this.#count;
224
+
225
+ for (let k = 0; k < count; k++) {
226
+ this.setKnotFrame(k, nx, ny, nz, bx, by, bz);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Write the `radial` quads (2 triangles each) bridging the ring of knot `lo`
232
+ * to the ring of knot `hi`, starting at `address` in the index array.
233
+ * Winding matches the engine's static tube mesher (make_ring_faces).
234
+ * @param {number} address index-array offset (in index entries)
235
+ * @param {number} lo knot index whose ring is the "left" edge
236
+ * @param {number} hi knot index whose ring is the "right" edge
237
+ */
238
+ #write_segment_indices(address, lo, hi) {
239
+ const indices = this.#ba_index.array;
240
+ const radial = this.#radial;
241
+ const v = this.#ring_vertex_count;
242
+
243
+ const lo_base = lo * v;
244
+ const hi_base = hi * v;
245
+
246
+ for (let r = 0; r < radial; r++) {
247
+ const a = lo_base + r;
248
+ const b = hi_base + r;
249
+ const c = hi_base + r + 1;
250
+ const d = lo_base + r + 1;
251
+
252
+ const ci = address + r * 6;
253
+
254
+ indices[ci] = a;
255
+ indices[ci + 1] = b;
256
+ indices[ci + 2] = d;
257
+
258
+ indices[ci + 3] = b;
259
+ indices[ci + 4] = c;
260
+ indices[ci + 5] = d;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Build the index buffer as a linear strip of `count - 1` segments.
266
+ */
267
+ #initialize_index() {
268
+ const segment_count = this.#count - 1;
269
+ const indices_per_segment = this.#radial * 6;
270
+
271
+ for (let s = 0; s < segment_count; s++) {
272
+ this.#write_segment_indices(s * indices_per_segment, s, s + 1);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * @param {number[]|Float32Array} result
278
+ * @param {number} knot_index
279
+ */
280
+ getKnotPosition(result, knot_index) {
281
+ const row = knot_index * this.#ring_vertex_count;
282
+ const data = this.#data;
283
+
284
+ result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION);
285
+ result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 1);
286
+ result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 2);
287
+ }
288
+
289
+ /**
290
+ * Read the stored ring normal of a knot. Used by the head to parallel-transport
291
+ * its frame from the previous knot.
292
+ * @param {number[]|Float32Array} result
293
+ * @param {number} knot_index
294
+ */
295
+ getKnotNormal(result, knot_index) {
296
+ const row = knot_index * this.#ring_vertex_count;
297
+ const data = this.#data;
298
+
299
+ result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL);
300
+ result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL + 1);
301
+ result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_NORMAL + 2);
302
+ }
303
+
304
+ /**
305
+ * Read the stored ring binormal of a knot.
306
+ * @param {number[]|Float32Array} result
307
+ * @param {number} knot_index
308
+ */
309
+ getKnotBinormal(result, knot_index) {
310
+ const row = knot_index * this.#ring_vertex_count;
311
+ const data = this.#data;
312
+
313
+ result[0] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL);
314
+ result[1] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL + 1);
315
+ result[2] = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_BINORMAL + 2);
316
+ }
317
+
318
+ /**
319
+ * Write a vec3 attribute to all ring vertices of a knot.
320
+ * @param {number} knot_index
321
+ * @param {number} attribute_index
322
+ * @param {number} x
323
+ * @param {number} y
324
+ * @param {number} z
325
+ */
326
+ setKnotAttribute_Vector3(knot_index, attribute_index, x, y, z) {
327
+ const data = this.#data;
328
+ const v = this.#ring_vertex_count;
329
+ const base = knot_index * v;
330
+
331
+ for (let j = 0; j < v; j++) {
332
+ const row = base + j;
333
+ data.writeCellValue(row, attribute_index, x);
334
+ data.writeCellValue(row, attribute_index + 1, y);
335
+ data.writeCellValue(row, attribute_index + 2, z);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Write a scalar attribute to all ring vertices of a knot.
341
+ * @param {number} knot_index
342
+ * @param {number} attribute_index
343
+ * @param {number} value
344
+ */
345
+ setKnotAttribute_Scalar(knot_index, attribute_index, value) {
346
+ const data = this.#data;
347
+ const v = this.#ring_vertex_count;
348
+ const base = knot_index * v;
349
+
350
+ for (let j = 0; j < v; j++) {
351
+ data.writeCellValue(base + j, attribute_index, value);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Add `delta` to a scalar attribute on every ring vertex of a knot.
357
+ * @param {number} knot_index
358
+ * @param {number} attribute_index
359
+ * @param {number} delta
360
+ * @returns {number} incremented value
361
+ */
362
+ incrementKnotAttribute_Scalar(knot_index, attribute_index, delta) {
363
+ const data = this.#data;
364
+ const v = this.#ring_vertex_count;
365
+ const base = knot_index * v;
366
+
367
+ const value = data.readCellValue(base, attribute_index) + delta;
368
+
369
+ for (let j = 0; j < v; j++) {
370
+ data.writeCellValue(base + j, attribute_index, value);
371
+ }
372
+
373
+ return value;
374
+ }
375
+
376
+ /**
377
+ * @param {number} knot_index
378
+ * @param {number} x
379
+ * @param {number} y
380
+ * @param {number} z
381
+ */
382
+ setKnotPosition(knot_index, x, y, z) {
383
+ assert.isNonNegativeInteger(knot_index, 'knot_index');
384
+
385
+ this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_POSITION, x, y, z);
386
+
387
+ this.#ib_float32.needsUpdate = true;
388
+ }
389
+
390
+ /**
391
+ * Set the orienting tangent frame of a knot. `nx,ny,nz` and `bx,by,bz` must be
392
+ * unit vectors, mutually orthogonal and perpendicular to the local tangent.
393
+ * @param {number} knot_index
394
+ * @param {number} nx
395
+ * @param {number} ny
396
+ * @param {number} nz
397
+ * @param {number} bx
398
+ * @param {number} by
399
+ * @param {number} bz
400
+ */
401
+ setKnotFrame(knot_index, nx, ny, nz, bx, by, bz) {
402
+ this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_NORMAL, nx, ny, nz);
403
+ this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_BINORMAL, bx, by, bz);
404
+
405
+ this.#ib_float32.needsUpdate = true;
406
+ }
407
+
408
+ /**
409
+ * @param {number} knot_index
410
+ * @param {number} r 0..255
411
+ * @param {number} g 0..255
412
+ * @param {number} b 0..255
413
+ */
414
+ setKnotColor(knot_index, r, g, b) {
415
+ assert.isNonNegativeInteger(knot_index, 'knot_index');
416
+
417
+ this.setKnotAttribute_Vector3(knot_index, TUBE_ATTRIBUTE_ADDRESS_COLOR, r, g, b);
418
+
419
+ this.#ib_uint8.needsUpdate = true;
420
+ }
421
+
422
+ /**
423
+ * @param {number} knot_index
424
+ * @param {number} value
425
+ */
426
+ setKnotAlpha(knot_index, value) {
427
+ this.setKnotAttribute_Scalar(knot_index, TUBE_ATTRIBUTE_ADDRESS_ALPHA, value);
428
+
429
+ this.#ib_float32.needsUpdate = true;
430
+ }
431
+
432
+ /**
433
+ * @param {number} knot_index
434
+ * @param {number} value tube diameter
435
+ */
436
+ setKnotThickness(knot_index, value) {
437
+ this.setKnotAttribute_Scalar(knot_index, TUBE_ATTRIBUTE_ADDRESS_THICKNESS, value);
438
+
439
+ this.#ib_float32.needsUpdate = true;
440
+ }
441
+
442
+ /**
443
+ * @param {number} count number of knots (>= 2)
444
+ * @param {number} [radial] radial subdivisions (>= 3)
445
+ */
446
+ setCount(count, radial = DEFAULT_RADIAL_SEGMENTS) {
447
+ assert.greaterThanOrEqual(count, 2, 'a tube needs at least 2 knots (one segment)');
448
+ assert.isNonNegativeInteger(count, 'count');
449
+ assert.greaterThanOrEqual(radial, 3, 'a tube ring needs at least 3 radial segments');
450
+ assert.isNonNegativeInteger(radial, 'radial');
451
+
452
+ this.#geometry.dispose();
453
+
454
+ this.#radial = radial;
455
+
456
+ const v = radial + 1;
457
+ const vertex_count = count * v;
458
+
459
+ if (this.#data.length > vertex_count) {
460
+ this.#data.removeRows(vertex_count, this.#data.length - vertex_count);
461
+ }
462
+
463
+ this.#data.setCapacity(vertex_count);
464
+ this.#data.length = vertex_count;
465
+
466
+ const a_uint8 = new Uint8Array(this.#data.data);
467
+ const a_float32 = new Float32Array(this.#data.data);
468
+
469
+ const segment_count = count - 1;
470
+ const triangle_count = segment_count * radial * 2;
471
+ const index_array_size = triangle_count * 3;
472
+ const index_array = new Uint32Array(index_array_size);
473
+
474
+ this.#ba_index.array = index_array;
475
+ this.#ba_index.count = index_array_size;
476
+
477
+ this.#ib_uint8.array = a_uint8;
478
+ this.#ib_float32.array = a_float32;
479
+
480
+ this.#ib_uint8.count = vertex_count;
481
+ this.#ib_float32.count = vertex_count;
482
+
483
+ this.#head_index = 0;
484
+ this.#tail_index = count - 1;
485
+ this.#tail_segment_index = segment_count - 1;
486
+
487
+ this.#count = count;
488
+
489
+ this.#initialize_off();
490
+ this.#initialize_frame();
491
+ this.#initialize_index();
492
+ }
493
+
494
+ /**
495
+ * NOTE: takes tube thickness into account.
496
+ * @param {AABB3|ArrayLike<number>} result
497
+ */
498
+ computeBoundingBox(result) {
499
+ const count = this.#count;
500
+ const v = this.#ring_vertex_count;
501
+ const data = this.#data;
502
+
503
+ let _x0 = Infinity, _y0 = Infinity, _z0 = Infinity;
504
+ let _x1 = -Infinity, _y1 = -Infinity, _z1 = -Infinity;
505
+
506
+ for (let k = 0; k < count; k++) {
507
+ const row = k * v;
508
+
509
+ const half_thickness = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_THICKNESS) * 0.5;
510
+
511
+ const x = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION);
512
+ const y = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 1);
513
+ const z = data.readCellValue(row, TUBE_ATTRIBUTE_ADDRESS_POSITION + 2);
514
+
515
+ _x0 = min2(_x0, x - half_thickness);
516
+ _y0 = min2(_y0, y - half_thickness);
517
+ _z0 = min2(_z0, z - half_thickness);
518
+ _x1 = max2(_x1, x + half_thickness);
519
+ _y1 = max2(_y1, y + half_thickness);
520
+ _z1 = max2(_z1, z + half_thickness);
521
+ }
522
+
523
+ aabb3_array_set(result, 0, _x0, _y0, _z0, _x1, _y1, _z1);
524
+ }
525
+
526
+ /**
527
+ * Recycle the oldest knot as the new head, in O(radial). Mirrors
528
+ * {@link RibbonX#rotate}: the head/tail pointers decrement together (preserving
529
+ * `head === (tail + 1) mod count`), and the single tail segment's indices are
530
+ * rewritten to bridge the previous head's ring to the recycled (new head) ring.
531
+ */
532
+ rotate() {
533
+ const old_tail = this.#tail_index;
534
+ const old_head = this.#head_index;
535
+
536
+ const max_index = this.#count - 1;
537
+
538
+ // The recycled slot (old tail) becomes the new head: head and tail step
539
+ // back in lockstep so head stays exactly one slot ahead of tail.
540
+ this.#tail_index = old_tail <= 0 ? max_index : old_tail - 1;
541
+ this.#head_index = old_head <= 0 ? max_index : old_head - 1;
542
+
543
+ const old_segment = this.#tail_segment_index;
544
+ const address = old_segment * this.#radial * 6;
545
+
546
+ this.#tail_segment_index = old_segment > 0 ? old_segment - 1 : this.#count - 2;
547
+
548
+ // Bridge old head -> new head (== old tail slot) with a fresh segment. The new head (old_tail slot)
549
+ // is the more head-ward knot, so it takes the `lo` position to match #initialize_index's winding
550
+ // convention (lo = more head-ward); swapping the two would invert this segment's triangle winding.
551
+ this.#write_segment_indices(address, old_tail, old_head);
552
+
553
+ this.#ba_index.needsUpdate = true;
554
+ }
555
+ }
556
+
557
+ TubeX.DEFAULT_RADIAL_SEGMENTS = DEFAULT_RADIAL_SEGMENTS;