@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.
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/CORRECTNESS_HUNT_2026_07_24.md +142 -0
- package/src/core/binary/BinaryBuffer.js +1 -1
- package/src/core/binary/EncodingBinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/EncodingBinaryBuffer.js +3 -1
- package/src/core/cache/Cache.d.ts.map +1 -1
- package/src/core/cache/Cache.js +0 -4
- package/src/core/cache/CacheElement.d.ts.map +1 -1
- package/src/core/cache/CacheElement.js +4 -0
- package/src/core/cache/FrequencySketch.d.ts +0 -21
- package/src/core/cache/FrequencySketch.d.ts.map +1 -1
- package/src/core/cache/FrequencySketch.js +66 -65
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts +17 -0
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheElementWTinylfu.js +18 -0
- package/src/core/cache/wtinylfu/CacheRegionType.d.ts +10 -0
- package/src/core/cache/wtinylfu/CacheRegionType.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheRegionType.js +10 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts +66 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheRegionWTinylfu.js +183 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts +279 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/CacheWTinylfu.js +807 -0
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +243 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts +80 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.d.ts.map +1 -0
- package/src/core/cache/wtinylfu/wtinylfuDecisions.js +102 -0
- package/src/core/collection/RingBuffer.d.ts.map +1 -1
- package/src/core/collection/RingBuffer.js +15 -3
- package/src/core/collection/map/HashMap.d.ts +1 -1
- package/src/core/collection/map/HashMap.d.ts.map +1 -1
- package/src/core/collection/map/HashMap.js +5 -17
- package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.d.ts.map +1 -1
- package/src/core/geom/2d/line/line2_compute_segment_point_distance_sqr.js +5 -0
- package/src/core/geom/ConicRay.d.ts.map +1 -1
- package/src/core/geom/ConicRay.js +9 -2
- package/src/core/geom/Quaternion.d.ts.map +1 -1
- package/src/core/geom/Quaternion.js +7 -3
- package/src/core/math/build_gaussian_kernel_1d.d.ts +2 -2
- package/src/core/math/build_gaussian_kernel_1d.d.ts.map +1 -1
- package/src/core/math/build_gaussian_kernel_1d.js +4 -2
- package/src/core/math/build_gaussian_kernel_2d.d.ts +2 -2
- package/src/core/math/build_gaussian_kernel_2d.d.ts.map +1 -1
- package/src/core/math/build_gaussian_kernel_2d.js +4 -2
- package/src/core/math/gaussian.d.ts +2 -2
- package/src/core/math/gaussian.js +2 -2
- package/src/core/math/spline/spline3_bezier_bounds.d.ts.map +1 -1
- package/src/core/math/spline/spline3_bezier_bounds.js +10 -3
- package/src/core/math/spline/spline3_bezier_to_hermite.d.ts +8 -7
- package/src/core/math/spline/spline3_bezier_to_hermite.d.ts.map +1 -1
- package/src/core/math/spline/spline3_bezier_to_hermite.js +13 -12
- package/src/core/math/spline/spline3_hermite_bounds.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_bounds.js +10 -3
- package/src/core/math/spline/spline3_hermite_bounds_t.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_bounds_t.js +115 -109
- package/src/core/math/spline/spline3_hermite_to_bezier.d.ts +2 -0
- package/src/core/math/spline/spline3_hermite_to_bezier.d.ts.map +1 -1
- package/src/core/math/spline/spline3_hermite_to_bezier.js +2 -0
- package/src/core/math/statistics/computeStatisticalMean.js +1 -1
- package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
- package/src/core/math/statistics/gaussian_amplitude.js +3 -1
- package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
- package/src/engine/animation/clip/ecd_bind_animation_curve.js +10 -1
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +20 -0
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -0
- package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +37 -0
- package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
- package/src/engine/animation/keyed2/AnimationTrackPlayback.js +5 -1
- package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
- package/src/engine/ecs/EntityComponentDataset.js +5 -21
- package/src/engine/ecs/fow/FogOfWar.d.ts +22 -0
- package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWar.js +202 -54
- package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts +16 -0
- package/src/engine/ecs/fow/FogOfWarEdgeMode.d.ts.map +1 -0
- package/src/engine/ecs/fow/FogOfWarEdgeMode.js +39 -0
- package/src/engine/ecs/fow/FogOfWarSystem.d.ts.map +1 -1
- package/src/engine/ecs/fow/FogOfWarSystem.js +1 -0
- package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.d.ts.map +1 -1
- package/src/engine/ecs/fow/serialization/FogOfWarSerializationAdapter.js +3 -0
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts +7 -0
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.d.ts.map +1 -1
- package/src/engine/ecs/fow/shader/FogOfWarRenderer.js +155 -145
- package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.d.ts.map +1 -1
- package/src/engine/ecs/fow/shader/screenSpaceFogOfWarShader.js +33 -8
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
- package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +3 -1
- package/src/engine/graphics/GraphicsEngine.d.ts +5 -1
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +14 -3
- package/src/engine/graphics/composit/CompositLayer.d.ts.map +1 -1
- package/src/engine/graphics/composit/CompositLayer.js +258 -255
- package/src/engine/graphics/composit/LayerCompositer.d.ts +12 -0
- package/src/engine/graphics/composit/LayerCompositer.d.ts.map +1 -1
- package/src/engine/graphics/composit/LayerCompositer.js +19 -10
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts +1 -1
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/plugin/OutlineRenderPlugin.js +10 -4
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts +6 -0
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js +21 -0
- package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/ribbon/RibbonPathBuilder.js +5 -23
- package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js +6 -4
- package/src/engine/graphics/ecs/water/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/WaterSystem.js +9 -3
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts +12 -0
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics/particles/ecs/ParticleEmitterSystem.js +46 -9
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +5 -0
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +8 -0
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts +7 -0
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/shader/MaterialRecord.js +8 -0
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +10 -2
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +42 -10
- package/src/engine/graphics/render/buffer/FrameBuffer.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/FrameBuffer.js +5 -2
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts +13 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +40 -7
- package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/taa/TemporalSupersamplingRenderPlugin.js +10 -4
- package/src/engine/graphics/render/forward_plus/LightManager.d.ts.map +1 -1
- package/src/engine/graphics/render/forward_plus/LightManager.js +1235 -1232
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts +5 -0
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.d.ts.map +1 -1
- package/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js +11 -10
- package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
- package/src/engine/graphics/trail/tube/TubeX.js +557 -555
- package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts +12 -1
- package/src/engine/graphics/trail/x/RibbonXPlugin.d.ts.map +1 -1
- package/src/engine/graphics/trail/x/RibbonXPlugin.js +15 -8
- package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
- package/src/engine/input/ecs/systems/InputControllerSystem.js +9 -6
- package/src/engine/navigation/ecs/components/Path.d.ts +5 -2
- package/src/engine/navigation/ecs/components/Path.d.ts.map +1 -1
- package/src/engine/navigation/ecs/components/Path.js +10 -4
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- 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
|
-
|
|
550
|
-
|
|
551
|
-
this.#
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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;
|