@woosh/meep-engine 2.163.0 → 2.163.2

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.163.0",
9
+ "version": "2.163.2",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -1 +1 @@
1
- {"version":3,"file":"build_geometry_catmullrom.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.js"],"names":[],"mappings":"AAoFA;;;;;;;;;;GAUG;AACH,mFAPW,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,mBACrB,MAAM,EAAE,iBACR,MAAM,eACN,MAAM,GACL,MAAM,cAAc,CAoI/B"}
1
+ {"version":3,"file":"build_geometry_catmullrom.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.js"],"names":[],"mappings":"AAsJA;;;;;;;;;;GAUG;AACH,mFAPW,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,mBACrB,MAAM,EAAE,iBACR,MAAM,eACN,MAAM,GACL,MAAM,cAAc,CAkJ/B"}
@@ -1,226 +1,306 @@
1
- import { v3_dot } from "../../../../../../core/geom/vec3/v3_dot.js";
2
- import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
3
- import { clamp } from "../../../../../../core/math/clamp.js";
4
- import { max2 } from "../../../../../../core/math/max2.js";
5
- import {
6
- computeNonuniformCaltmullRomSplineDerivative
7
- } from "../../../../../../core/math/spline/computeNonuniformCaltmullRomSplineDerivative.js";
8
- import { PathNormalType } from "../PathNormalType.js";
9
- import { computeFrenetFrames } from "./computeFrenetFrames.js";
10
- import { makeTubeGeometry } from "./makeTubeGeometry.js";
11
-
12
-
13
- const scratch_array_0 = [];
14
- const scratch_array_1 = [];
15
- const scratch_array_2 = [];
16
- const scratch_array_3 = [];
17
-
18
- /**
19
- *
20
- * @param {number[]} positions
21
- * @param {number[]} derivatives
22
- * @param {number} result_offset
23
- * @param {Path} path
24
- * @param {number} offset
25
- */
26
- function sample_path(positions, derivatives, result_offset, path, offset) {
27
-
28
-
29
- if (!path.find_index_and_normalized_distance(scratch_array_0, offset)) {
30
- return 0;
31
- }
32
-
33
-
34
- /**
35
- *
36
- * @type {number}
37
- */
38
- const i1 = scratch_array_0[0];
39
-
40
- /**
41
- *
42
- * @type {number}
43
- */
44
- const t = scratch_array_0[1];
45
-
46
-
47
- const input_length = path.getPointCount();
48
-
49
- const max_index = input_length - 1;
50
-
51
- const i0 = clamp(i1 - 1, 0, max_index);
52
- const i2 = clamp(i1 + 1, 0, max_index);
53
- const i3 = clamp(i1 + 2, 0, max_index);
54
-
55
- path.readPositionToArray(i0, scratch_array_0, 0);
56
- path.readPositionToArray(i1, scratch_array_1, 0);
57
- path.readPositionToArray(i2, scratch_array_2, 0);
58
- path.readPositionToArray(i3, scratch_array_3, 0);
59
-
60
- computeNonuniformCaltmullRomSplineDerivative(
61
- positions, result_offset,
62
- derivatives, result_offset,
63
- scratch_array_0, scratch_array_1, scratch_array_2, scratch_array_3,
64
- 3, t, 0.5
65
- );
66
-
67
- // normalize derivative
68
- const dx = derivatives[result_offset];
69
- const dy = derivatives[result_offset + 1];
70
- const dz = derivatives[result_offset + 2];
71
-
72
- const mag = v3_length(dx, dy, dz);
73
-
74
- if (mag !== 0) {
75
- const inv_mag = 1 / mag;
76
-
77
- derivatives[result_offset] = dx * inv_mag;
78
- derivatives[result_offset + 1] = dy * inv_mag;
79
- derivatives[result_offset + 2] = dz * inv_mag;
80
- }
81
-
82
- return i1;
83
- }
84
-
85
- /**
86
- *
87
- * @param {Path} path
88
- * @param {TubePathStyle} style
89
- * @param {number[]} shape
90
- * @param {number[]|Float32Array} shape_normal
91
- * @param {number[]} shape_transform
92
- * @param {number} segment_start
93
- * @param {number} segment_end
94
- * @return {THREE.BufferGeometry}
95
- */
96
- export function build_geometry_catmullrom(
97
- path, style,
98
- shape, shape_normal, shape_transform,
99
- segment_start, segment_end
100
- ) {
101
-
102
- const point_count = path.getPointCount();
103
-
104
- // resample curve
105
- const total_points = Math.ceil(point_count * style.resolution);
106
-
107
- const reference_step_size = 1 / max2(0.00001, style.resolution);
108
- const reference_min_step_size = reference_step_size * 0.25;
109
-
110
- let step_size = reference_step_size;
111
-
112
- const path_length = path.length;
113
-
114
- const sample_positions_f32 = [];
115
- const sample_derivatives_f32 = [];
116
-
117
- let added_points = 0;
118
-
119
- // initial segment
120
- let last_knot_index = sample_path(
121
- sample_positions_f32,
122
- sample_derivatives_f32,
123
- added_points * 3,
124
- path,
125
- segment_start * path_length
126
- );
127
-
128
- added_points++;
129
-
130
- let current_offset = segment_start * path_length;
131
-
132
- let step_resized_direction = 0;
133
-
134
- const absolute_end_offset = segment_end * path_length;
135
-
136
- for (; current_offset < absolute_end_offset; current_offset += step_size) {
137
-
138
-
139
- const point_address = added_points * 3;
140
-
141
- const knot_index = sample_path(
142
- sample_positions_f32,
143
- sample_derivatives_f32,
144
- point_address,
145
- path,
146
- current_offset
147
- );
148
-
149
- // check difference with previous derivative
150
- const previous_point_index = added_points - 1;
151
- const previous_point_address = previous_point_index * 3;
152
-
153
- // derivatives are basically normals, and dot product is has Cosine value of angle between two vectors
154
- const dot = v3_dot(
155
- sample_derivatives_f32[previous_point_address], sample_derivatives_f32[previous_point_address + 1], sample_derivatives_f32[previous_point_address + 2],
156
- sample_derivatives_f32[point_address], sample_derivatives_f32[point_address + 1], sample_derivatives_f32[point_address + 2]
157
- );
158
-
159
- // angular difference results in larger visual error for longer runs
160
- const error_size = (1 - dot) * step_size;
161
-
162
- if (
163
- (step_size > reference_min_step_size)
164
- && (
165
- (
166
- // check if we jumped over a knot, so that we can sample down to get closer to it
167
- knot_index > last_knot_index
168
- && step_size > reference_min_step_size
169
- )
170
- || (
171
- error_size * 10 > reference_min_step_size
172
- && step_resized_direction <= 0
173
- )
174
- || (
175
- // check if we're getting too close to the end of the segment, this lets us create a nice end
176
- current_offset + step_size > absolute_end_offset
177
- )
178
- )
179
- ) {
180
-
181
- // step looks to be too large
182
- current_offset -= step_size;
183
- step_size *= 0.5;
184
- step_resized_direction = -1;
185
- continue; // retry
186
-
187
- } else if (
188
- error_size < 0.02
189
- && step_resized_direction >= 0
190
- ) {
191
-
192
- // step looks to be too small
193
- current_offset -= step_size;
194
- step_size *= 2;
195
- step_resized_direction = 1;
196
- continue; // retry
197
-
198
- }
199
-
200
- // reset adaptive step direction
201
- step_resized_direction = 0;
202
- // remember the last knot, so we know when we get to the end of the segment
203
- last_knot_index = knot_index;
204
-
205
- added_points++
206
- }
207
-
208
- if ((absolute_end_offset - current_offset + step_size) > 0.0001) {
209
- // end
210
-
211
- sample_path(sample_positions_f32, sample_derivatives_f32, added_points * 3, path, absolute_end_offset);
212
-
213
- added_points++;
214
- }
215
-
216
- // console.log(`Total Points ${added_points}`) // DEBUG info
217
-
218
- const normal_hint = style.path_normal_type === PathNormalType.FixedStart ? style.path_normal : undefined;
219
-
220
- const frames = computeFrenetFrames(sample_positions_f32, false, normal_hint);
221
-
222
- return makeTubeGeometry(
223
- sample_positions_f32, frames.normals, frames.binormals, frames.tangents,
224
- shape, shape_normal, shape.length / 2, shape_transform, false, style.cap_type
225
- );
226
- }
1
+ import { v3_dot } from "../../../../../../core/geom/vec3/v3_dot.js";
2
+ import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
3
+ import { clamp } from "../../../../../../core/math/clamp.js";
4
+ import { max2 } from "../../../../../../core/math/max2.js";
5
+ import {
6
+ computeNonuniformCaltmullRomSplineDerivative
7
+ } from "../../../../../../core/math/spline/computeNonuniformCaltmullRomSplineDerivative.js";
8
+ import { PathNormalType } from "../PathNormalType.js";
9
+ import { computeFrenetFrames } from "./computeFrenetFrames.js";
10
+ import { makeTubeGeometry } from "./makeTubeGeometry.js";
11
+
12
+
13
+ const scratch_array_0 = [];
14
+ const scratch_array_1 = [];
15
+ const scratch_array_2 = [];
16
+ const scratch_array_3 = [];
17
+
18
+ /**
19
+ *
20
+ * @param {number[]} positions
21
+ * @param {number[]} derivatives
22
+ * @param {number} result_offset
23
+ * @param {Path} path
24
+ * @param {number} offset
25
+ */
26
+ function sample_path(positions, derivatives, result_offset, path, offset) {
27
+
28
+
29
+ if (!path.find_index_and_normalized_distance(scratch_array_0, offset)) {
30
+ return 0;
31
+ }
32
+
33
+
34
+ /**
35
+ *
36
+ * @type {number}
37
+ */
38
+ const i1 = scratch_array_0[0];
39
+
40
+ /**
41
+ *
42
+ * @type {number}
43
+ */
44
+ const t = scratch_array_0[1];
45
+
46
+
47
+ const input_length = path.getPointCount();
48
+
49
+ const max_index = input_length - 1;
50
+
51
+ const i0 = clamp(i1 - 1, 0, max_index);
52
+ const i2 = clamp(i1 + 1, 0, max_index);
53
+ const i3 = clamp(i1 + 2, 0, max_index);
54
+
55
+ path.readPositionToArray(i0, scratch_array_0, 0);
56
+ path.readPositionToArray(i1, scratch_array_1, 0);
57
+ path.readPositionToArray(i2, scratch_array_2, 0);
58
+ path.readPositionToArray(i3, scratch_array_3, 0);
59
+
60
+ computeNonuniformCaltmullRomSplineDerivative(
61
+ positions, result_offset,
62
+ derivatives, result_offset,
63
+ scratch_array_0, scratch_array_1, scratch_array_2, scratch_array_3,
64
+ 3, t, 0.5
65
+ );
66
+
67
+ // normalize derivative
68
+ const dx = derivatives[result_offset];
69
+ const dy = derivatives[result_offset + 1];
70
+ const dz = derivatives[result_offset + 2];
71
+
72
+ const mag = v3_length(dx, dy, dz);
73
+
74
+ if (mag !== 0) {
75
+ const inv_mag = 1 / mag;
76
+
77
+ derivatives[result_offset] = dx * inv_mag;
78
+ derivatives[result_offset + 1] = dy * inv_mag;
79
+ derivatives[result_offset + 2] = dz * inv_mag;
80
+ }
81
+
82
+ return i1;
83
+ }
84
+
85
+ /**
86
+ * Stabilise one open-end frame. computeFrenetFrames derives the tangent at a
87
+ * path end from the end chord (P1-P0 / P[n]-P[n-1]); the adaptive resampler sets
88
+ * that neighbour's spacing, so on a curve the chord direction jumps frame to
89
+ * frame and swings the end cap (a small wobble becomes a >100-degree flip as an
90
+ * animated dash slides across a path knot). The analytic spline derivative is
91
+ * smooth in the path parameter and resampling-independent, so adopt it for the
92
+ * end tangent and re-orthogonalise the end normal/binormal around it.
93
+ *
94
+ * @param {{normals:Vector3[],binormals:Vector3[],tangents:Vector3[]}} frames
95
+ * @param {number[]|Float32Array} derivatives normalised per-sample spline derivatives
96
+ * @param {number} i sample index whose frame is stabilised
97
+ * @param {number} derivative_index sample to read the spline derivative from
98
+ * (use an interior neighbour: the analytic derivative is bogus at the
99
+ * path's exact parametric ends, param 0/1, where the spline clamps its
100
+ * control points)
101
+ */
102
+ function stabilize_end_frame(frames, derivatives, i, derivative_index) {
103
+ const d3 = derivative_index * 3;
104
+
105
+ let tx = derivatives[d3];
106
+ let ty = derivatives[d3 + 1];
107
+ let tz = derivatives[d3 + 2];
108
+
109
+ const tl = Math.sqrt(tx * tx + ty * ty + tz * tz);
110
+ if (!(tl > 1e-8)) {
111
+ // derivative unavailable/degenerate: keep the Frenet frame
112
+ return;
113
+ }
114
+ tx /= tl; ty /= tl; tz /= tl;
115
+
116
+ const N = frames.normals[i];
117
+
118
+ // The spline derivative already points 'forward' (towards increasing
119
+ // samples), the same orientation computeFrenetFrames uses, so adopt it as-is.
120
+ // (Aligning it to the OLD end tangent would re-import that tangent's jitter -
121
+ // when the jittery chord swings past 90 degrees the sign would flip and the
122
+ // cap axis would reverse.)
123
+ frames.tangents[i].set(tx, ty, tz);
124
+
125
+ // re-orthogonalise the existing normal against the new tangent (Gram-Schmidt)
126
+ const dot = N.x * tx + N.y * ty + N.z * tz;
127
+ let nx = N.x - dot * tx;
128
+ let ny = N.y - dot * ty;
129
+ let nz = N.z - dot * tz;
130
+ let nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
131
+
132
+ if (!(nl > 1e-6)) {
133
+ // old normal was ~parallel to the new tangent: pick any perpendicular axis
134
+ const ax = Math.abs(tx), ay = Math.abs(ty), az = Math.abs(tz);
135
+ if (ax <= ay && ax <= az) { nx = 0; ny = -tz; nz = ty; }
136
+ else if (ay <= az) { nx = -tz; ny = 0; nz = tx; }
137
+ else { nx = -ty; ny = tx; nz = 0; }
138
+ nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
139
+ }
140
+ nx /= nl; ny /= nl; nz /= nl;
141
+ N.set(nx, ny, nz);
142
+
143
+ // binormal = tangent x normal (matches computeFrenetFrames' convention)
144
+ frames.binormals[i].set(
145
+ ty * nz - tz * ny,
146
+ tz * nx - tx * nz,
147
+ tx * ny - ty * nx
148
+ );
149
+ }
150
+
151
+ /**
152
+ *
153
+ * @param {Path} path
154
+ * @param {TubePathStyle} style
155
+ * @param {number[]} shape
156
+ * @param {number[]|Float32Array} shape_normal
157
+ * @param {number[]} shape_transform
158
+ * @param {number} segment_start
159
+ * @param {number} segment_end
160
+ * @return {THREE.BufferGeometry}
161
+ */
162
+ export function build_geometry_catmullrom(
163
+ path, style,
164
+ shape, shape_normal, shape_transform,
165
+ segment_start, segment_end
166
+ ) {
167
+
168
+ const point_count = path.getPointCount();
169
+
170
+ // resample curve
171
+ const total_points = Math.ceil(point_count * style.resolution);
172
+
173
+ const reference_step_size = 1 / max2(0.00001, style.resolution);
174
+ const reference_min_step_size = reference_step_size * 0.25;
175
+
176
+ let step_size = reference_step_size;
177
+
178
+ const path_length = path.length;
179
+
180
+ const sample_positions_f32 = [];
181
+ const sample_derivatives_f32 = [];
182
+
183
+ let added_points = 0;
184
+
185
+ // initial segment
186
+ let last_knot_index = sample_path(
187
+ sample_positions_f32,
188
+ sample_derivatives_f32,
189
+ added_points * 3,
190
+ path,
191
+ segment_start * path_length
192
+ );
193
+
194
+ added_points++;
195
+
196
+ let current_offset = segment_start * path_length;
197
+
198
+ let step_resized_direction = 0;
199
+
200
+ const absolute_end_offset = segment_end * path_length;
201
+
202
+ for (; current_offset < absolute_end_offset; current_offset += step_size) {
203
+
204
+
205
+ const point_address = added_points * 3;
206
+
207
+ const knot_index = sample_path(
208
+ sample_positions_f32,
209
+ sample_derivatives_f32,
210
+ point_address,
211
+ path,
212
+ current_offset
213
+ );
214
+
215
+ // check difference with previous derivative
216
+ const previous_point_index = added_points - 1;
217
+ const previous_point_address = previous_point_index * 3;
218
+
219
+ // derivatives are basically normals, and dot product is has Cosine value of angle between two vectors
220
+ const dot = v3_dot(
221
+ sample_derivatives_f32[previous_point_address], sample_derivatives_f32[previous_point_address + 1], sample_derivatives_f32[previous_point_address + 2],
222
+ sample_derivatives_f32[point_address], sample_derivatives_f32[point_address + 1], sample_derivatives_f32[point_address + 2]
223
+ );
224
+
225
+ // angular difference results in larger visual error for longer runs
226
+ const error_size = (1 - dot) * step_size;
227
+
228
+ if (
229
+ (step_size > reference_min_step_size)
230
+ && (
231
+ (
232
+ // check if we jumped over a knot, so that we can sample down to get closer to it
233
+ knot_index > last_knot_index
234
+ && step_size > reference_min_step_size
235
+ )
236
+ || (
237
+ error_size * 10 > reference_min_step_size
238
+ && step_resized_direction <= 0
239
+ )
240
+ || (
241
+ // check if we're getting too close to the end of the segment, this lets us create a nice end
242
+ current_offset + step_size > absolute_end_offset
243
+ )
244
+ )
245
+ ) {
246
+
247
+ // step looks to be too large
248
+ current_offset -= step_size;
249
+ step_size *= 0.5;
250
+ step_resized_direction = -1;
251
+ continue; // retry
252
+
253
+ } else if (
254
+ error_size < 0.02
255
+ && step_resized_direction >= 0
256
+ ) {
257
+
258
+ // step looks to be too small
259
+ current_offset -= step_size;
260
+ step_size *= 2;
261
+ step_resized_direction = 1;
262
+ continue; // retry
263
+
264
+ }
265
+
266
+ // reset adaptive step direction
267
+ step_resized_direction = 0;
268
+ // remember the last knot, so we know when we get to the end of the segment
269
+ last_knot_index = knot_index;
270
+
271
+ added_points++
272
+ }
273
+
274
+ if ((absolute_end_offset - current_offset + step_size) > 0.0001) {
275
+ // end
276
+
277
+ sample_path(sample_positions_f32, sample_derivatives_f32, added_points * 3, path, absolute_end_offset);
278
+
279
+ added_points++;
280
+ }
281
+
282
+ // console.log(`Total Points ${added_points}`) // DEBUG info
283
+
284
+ const normal_hint = style.path_normal_type === PathNormalType.FixedStart ? style.path_normal : undefined;
285
+
286
+ const frames = computeFrenetFrames(sample_positions_f32, false, normal_hint);
287
+
288
+ // Stabilise the START frame so its round cap doesn't twitch as the adaptive
289
+ // resampling shifts. Only the start needs it: computeFrenetFrames' start
290
+ // tangent is the first chord (P1-P0), and the resampler's first-segment
291
+ // length/direction jitters frame to frame; the analytic derivative at the
292
+ // start sample is smooth in the path parameter. The END frame's tangent is
293
+ // already stable (the sampler approaches the end with a small, consistent
294
+ // final segment) and the analytic derivative is unreliable at the path's
295
+ // exact parametric end (param 1, where the spline clamps control points), so
296
+ // the end is deliberately left on its chord-based frame.
297
+ const last_sample_index = (sample_positions_f32.length / 3) - 1;
298
+ if (last_sample_index >= 2) {
299
+ stabilize_end_frame(frames, sample_derivatives_f32, 0, 0);
300
+ }
301
+
302
+ return makeTubeGeometry(
303
+ sample_positions_f32, frames.normals, frames.binormals, frames.tangents,
304
+ shape, shape_normal, shape.length / 2, shape_transform, false, style.cap_type
305
+ );
306
+ }
@@ -1,202 +1,218 @@
1
- import { BufferGeometry, Vector3 } from "three";
2
- import { assert } from "../../../../../../core/assert.js";
3
- import { v3_angle_cos_between } from "../../../../../../core/geom/vec3/v3_angle_cos_between.js";
4
- import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
5
- import { CapType } from "../CapType.js";
6
- import { append_compute_cap_geometry_size, make_cap } from "./make_cap.js";
7
- import { make_ring_faces } from "./make_ring_faces.js";
8
- import { make_ring_vertices } from "./make_ring_vertices.js";
9
- import { StreamGeometryBuilder } from "./StreamGeometryBuilder.js";
10
-
11
-
12
- const v4_array = new Float32Array(4);
13
-
14
- /**
15
- * @see https://github.com/mrdoob/three.js/blob/master/src/geometries/TubeGeometry.js
16
- * @see https://github.com/hofk/THREEg.js/blob/488f1128a25321a76888aa1fa19db64750318444/THREEg.js#L3483
17
- * @param {Float32Array|number[]} in_positions
18
- * @param {Vector3[]} in_normals
19
- * @param {Vector3[]} in_binormals
20
- * @param {Vector3[]} in_tangents
21
- * @param {number[]} shape
22
- * @param {number[]|Float32Array} shape_normal
23
- * @param {number} shape_length
24
- * @param {number[]|Float32Array} shape_transform
25
- * @param {boolean} [closed]
26
- * @param {CapType} [cap_type]
27
- * @returns {BufferGeometry}
28
- */
29
- export function makeTubeGeometry(
30
- in_positions, in_normals, in_binormals, in_tangents,
31
- shape, shape_normal, shape_length, shape_transform, closed = false, cap_type = CapType.Round
32
- ) {
33
- assert.enum(cap_type, CapType, 'cap_type');
34
- assert.isBoolean(closed, 'closed');
35
-
36
- assert.isNumber(shape_length, 'shape_length');
37
- assert.isArrayLike(shape, 'shape');
38
-
39
- const out = new StreamGeometryBuilder();
40
-
41
- // helper variables
42
-
43
- const point_count = in_positions.length / 3;
44
- const tubular_segments = point_count - 1;
45
-
46
-
47
- const geometry_size = {
48
- vertex_count: (tubular_segments + 1) * (shape_length + 1),
49
- polygon_count: tubular_segments * shape_length * 2
50
- };
51
-
52
- if (!closed) {
53
- append_compute_cap_geometry_size(2, geometry_size, shape_length, cap_type);
54
- }
55
-
56
- out.allocate(
57
- geometry_size.vertex_count,
58
- geometry_size.polygon_count
59
- );
60
-
61
- // create buffer data
62
-
63
- if (!closed) {
64
- // start cap
65
- make_cap(
66
- out, 0,
67
- in_positions, in_normals, in_binormals, in_tangents,
68
- shape, shape_normal, shape_length, shape_transform, 1, cap_type
69
- );
70
- }
71
-
72
- const index_offset = out.cursor_vertices;
73
-
74
- for (let i = 0; i < tubular_segments; i++) {
75
-
76
- generateSegment(i);
77
-
78
- }
79
-
80
- // if the geometry is not closed, generate the last row of vertices and normals
81
- // at the regular position on the given path
82
- //
83
- // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)
84
-
85
- generateSegment((closed === false) ? tubular_segments : 0);
86
-
87
- // finally create faces
88
- make_ring_faces(out, index_offset, tubular_segments, shape_length);
89
-
90
- if (!closed) {
91
- // end cap
92
- make_cap(
93
- out, point_count - 1,
94
- in_positions, in_normals, in_binormals, in_tangents,
95
- shape, shape_normal, shape_length, shape_transform, -1, cap_type
96
- );
97
- }
98
-
99
- /**
100
- *
101
- * @param {number} i
102
- */
103
- function generateSegment(i) {
104
-
105
- // we use getPointAt to sample evenly distributed points from the given path
106
-
107
- const i3 = i * 3;
108
-
109
- const Px = in_positions[i3];
110
- const Py = in_positions[i3 + 1];
111
- const Pz = in_positions[i3 + 2];
112
-
113
- // retrieve corresponding normal and binormal
114
-
115
- const N = in_normals[i];
116
- const B = in_binormals[i];
117
-
118
- // generate normals and vertices for the current segment
119
- compute_bend_normal(v4_array, i, tubular_segments, in_positions);
120
-
121
- make_ring_vertices(
122
- out,
123
- Px, Py, Pz,
124
- N, B, in_tangents[i],
125
- i / tubular_segments, v4_array,
126
- shape, shape_normal, shape_length, shape_transform
127
- );
128
-
129
- }
130
-
131
-
132
- return out.build();
133
- }
134
-
135
-
136
- /**
137
- *
138
- * @param {number[]|Float32Array} out
139
- * @param {number} index
140
- * @param {number} index_count
141
- * @param {number[]|Float32Array} positions
142
- */
143
- function compute_bend_normal(
144
- out,
145
- index,
146
- index_count,
147
- positions
148
- ) {
149
- if (index <= 0 || index >= index_count - 1) {
150
- // end points, no bending
151
-
152
- out[0] = 0;
153
- out[1] = 1;
154
- out[2] = 0;
155
- out[3] = 0;
156
-
157
- return;
158
- }
159
-
160
- const index_next = index + 1;
161
- const index_prev = index - 1;
162
-
163
- const address_current = index * 3;
164
- const address_next = index_next * 3;
165
- const address_prev = index_prev * 3;
166
-
167
- const i0_x = positions[address_prev];
168
- const i0_y = positions[address_prev + 1];
169
- const i0_z = positions[address_prev + 2];
170
-
171
- const i1_x = positions[address_current];
172
- const i1_y = positions[address_current + 1];
173
- const i1_z = positions[address_current + 2];
174
-
175
- const i2_x = positions[address_next];
176
- const i2_y = positions[address_next + 1];
177
- const i2_z = positions[address_next + 2];
178
-
179
- const d0_x = i0_x - i1_x;
180
- const d0_y = i0_y - i1_y;
181
- const d0_z = i0_z - i1_z;
182
-
183
- const d1_x = i1_x - i2_x;
184
- const d1_y = i1_y - i2_y;
185
- const d1_z = i1_z - i2_z;
186
-
187
- // compute rotation axis
188
- const cross_x = d0_y * d1_z - d0_z * d1_y;
189
- const cross_y = d0_z * d1_x - d0_x * d1_z;
190
- const cross_z = d0_x * d1_y - d0_y * d1_x;
191
-
192
- const angle = v3_angle_cos_between(d0_x, d0_y, d0_z, d1_x, d1_y, d1_z);
193
-
194
- const length_inv = 1 / v3_length(cross_x, cross_y, cross_z);
195
-
196
- out[0] = cross_x * length_inv;
197
- out[1] = cross_y * length_inv;
198
- out[2] = cross_z * length_inv;
199
-
200
- // bend amount
201
- out[3] = (1 - Math.abs(angle)) * 0.5;
202
- }
1
+ import { BufferGeometry, Vector3 } from "three";
2
+ import { assert } from "../../../../../../core/assert.js";
3
+ import { v3_angle_cos_between } from "../../../../../../core/geom/vec3/v3_angle_cos_between.js";
4
+ import { v3_length } from "../../../../../../core/geom/vec3/v3_length.js";
5
+ import { CapType } from "../CapType.js";
6
+ import { append_compute_cap_geometry_size, make_cap } from "./make_cap.js";
7
+ import { make_ring_faces } from "./make_ring_faces.js";
8
+ import { make_ring_vertices } from "./make_ring_vertices.js";
9
+ import { StreamGeometryBuilder } from "./StreamGeometryBuilder.js";
10
+
11
+
12
+ const v4_array = new Float32Array(4);
13
+
14
+ /**
15
+ * @see https://github.com/mrdoob/three.js/blob/master/src/geometries/TubeGeometry.js
16
+ * @see https://github.com/hofk/THREEg.js/blob/488f1128a25321a76888aa1fa19db64750318444/THREEg.js#L3483
17
+ * @param {Float32Array|number[]} in_positions
18
+ * @param {Vector3[]} in_normals
19
+ * @param {Vector3[]} in_binormals
20
+ * @param {Vector3[]} in_tangents
21
+ * @param {number[]} shape
22
+ * @param {number[]|Float32Array} shape_normal
23
+ * @param {number} shape_length
24
+ * @param {number[]|Float32Array} shape_transform
25
+ * @param {boolean} [closed]
26
+ * @param {CapType} [cap_type]
27
+ * @returns {BufferGeometry}
28
+ */
29
+ export function makeTubeGeometry(
30
+ in_positions, in_normals, in_binormals, in_tangents,
31
+ shape, shape_normal, shape_length, shape_transform, closed = false, cap_type = CapType.Round
32
+ ) {
33
+ assert.enum(cap_type, CapType, 'cap_type');
34
+ assert.isBoolean(closed, 'closed');
35
+
36
+ assert.isNumber(shape_length, 'shape_length');
37
+ assert.isArrayLike(shape, 'shape');
38
+
39
+ const out = new StreamGeometryBuilder();
40
+
41
+ // helper variables
42
+
43
+ const point_count = in_positions.length / 3;
44
+ const tubular_segments = point_count - 1;
45
+
46
+
47
+ const geometry_size = {
48
+ vertex_count: (tubular_segments + 1) * (shape_length + 1),
49
+ polygon_count: tubular_segments * shape_length * 2
50
+ };
51
+
52
+ if (!closed) {
53
+ append_compute_cap_geometry_size(2, geometry_size, shape_length, cap_type);
54
+ }
55
+
56
+ out.allocate(
57
+ geometry_size.vertex_count,
58
+ geometry_size.polygon_count
59
+ );
60
+
61
+ // create buffer data
62
+
63
+ if (!closed) {
64
+ // start cap
65
+ make_cap(
66
+ out, 0,
67
+ in_positions, in_normals, in_binormals, in_tangents,
68
+ shape, shape_normal, shape_length, shape_transform, 1, cap_type
69
+ );
70
+ }
71
+
72
+ const index_offset = out.cursor_vertices;
73
+
74
+ for (let i = 0; i < tubular_segments; i++) {
75
+
76
+ generateSegment(i);
77
+
78
+ }
79
+
80
+ // if the geometry is not closed, generate the last row of vertices and normals
81
+ // at the regular position on the given path
82
+ //
83
+ // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)
84
+
85
+ generateSegment((closed === false) ? tubular_segments : 0);
86
+
87
+ // finally create faces
88
+ make_ring_faces(out, index_offset, tubular_segments, shape_length);
89
+
90
+ if (!closed) {
91
+ // end cap
92
+ make_cap(
93
+ out, point_count - 1,
94
+ in_positions, in_normals, in_binormals, in_tangents,
95
+ shape, shape_normal, shape_length, shape_transform, -1, cap_type
96
+ );
97
+ }
98
+
99
+ /**
100
+ *
101
+ * @param {number} i
102
+ */
103
+ function generateSegment(i) {
104
+
105
+ // we use getPointAt to sample evenly distributed points from the given path
106
+
107
+ const i3 = i * 3;
108
+
109
+ const Px = in_positions[i3];
110
+ const Py = in_positions[i3 + 1];
111
+ const Pz = in_positions[i3 + 2];
112
+
113
+ // retrieve corresponding normal and binormal
114
+
115
+ const N = in_normals[i];
116
+ const B = in_binormals[i];
117
+
118
+ // generate normals and vertices for the current segment
119
+ compute_bend_normal(v4_array, i, tubular_segments, in_positions);
120
+
121
+ make_ring_vertices(
122
+ out,
123
+ Px, Py, Pz,
124
+ N, B, in_tangents[i],
125
+ i / tubular_segments, v4_array,
126
+ shape, shape_normal, shape_length, shape_transform
127
+ );
128
+
129
+ }
130
+
131
+
132
+ return out.build();
133
+ }
134
+
135
+
136
+ /**
137
+ *
138
+ * @param {number[]|Float32Array} out
139
+ * @param {number} index
140
+ * @param {number} index_count
141
+ * @param {number[]|Float32Array} positions
142
+ */
143
+ function compute_bend_normal(
144
+ out,
145
+ index,
146
+ index_count,
147
+ positions
148
+ ) {
149
+ if (index <= 0 || index >= index_count - 1) {
150
+ // end points, no bending
151
+
152
+ out[0] = 0;
153
+ out[1] = 1;
154
+ out[2] = 0;
155
+ out[3] = 0;
156
+
157
+ return;
158
+ }
159
+
160
+ const index_next = index + 1;
161
+ const index_prev = index - 1;
162
+
163
+ const address_current = index * 3;
164
+ const address_next = index_next * 3;
165
+ const address_prev = index_prev * 3;
166
+
167
+ const i0_x = positions[address_prev];
168
+ const i0_y = positions[address_prev + 1];
169
+ const i0_z = positions[address_prev + 2];
170
+
171
+ const i1_x = positions[address_current];
172
+ const i1_y = positions[address_current + 1];
173
+ const i1_z = positions[address_current + 2];
174
+
175
+ const i2_x = positions[address_next];
176
+ const i2_y = positions[address_next + 1];
177
+ const i2_z = positions[address_next + 2];
178
+
179
+ const d0_x = i0_x - i1_x;
180
+ const d0_y = i0_y - i1_y;
181
+ const d0_z = i0_z - i1_z;
182
+
183
+ const d1_x = i1_x - i2_x;
184
+ const d1_y = i1_y - i2_y;
185
+ const d1_z = i1_z - i2_z;
186
+
187
+ // compute rotation axis
188
+ const cross_x = d0_y * d1_z - d0_z * d1_y;
189
+ const cross_y = d0_z * d1_x - d0_x * d1_z;
190
+ const cross_z = d0_x * d1_y - d0_y * d1_x;
191
+
192
+ const angle = v3_angle_cos_between(d0_x, d0_y, d0_z, d1_x, d1_y, d1_z);
193
+
194
+ const cross_length = v3_length(cross_x, cross_y, cross_z);
195
+
196
+ if (cross_length < 1e-10) {
197
+ // Collinear (or duplicate) neighbours: zero curvature, so no bend.
198
+ // The rotation axis is undefined here and computing it would be
199
+ // 0 / 0 = NaN, poisoning this ring's vertices (visible as shattered
200
+ // geometry on short / straight resampled segments, e.g. the animated
201
+ // marching dashes). Emit the same no-bend value the path ends use.
202
+ out[0] = 0;
203
+ out[1] = 1;
204
+ out[2] = 0;
205
+ out[3] = 0;
206
+
207
+ return;
208
+ }
209
+
210
+ const length_inv = 1 / cross_length;
211
+
212
+ out[0] = cross_x * length_inv;
213
+ out[1] = cross_y * length_inv;
214
+ out[2] = cross_z * length_inv;
215
+
216
+ // bend amount
217
+ out[3] = (1 - Math.abs(angle)) * 0.5;
218
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"make_cap.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/make_cap.js"],"names":[],"mappings":"AAwQA;;;;;;;;;;;;;;GAcG;AACH,4DAbW,MAAM,gBACN,YAAY,GAAC,MAAM,EAAE,cACrB,OAAO,EAAE,gBACT,OAAO,EAAE,eAET,OAAO,EAAE,SACT,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,gBACrB,MAAM,mBACN,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,QACN,OAAO,QA6BjB;AAED;;;;;;GAMG;AACH,wDALW,MAAM,OACN;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,YAAY,EAAC,MAAM,CAAA;CAAC,mBAC3C,MAAM,QACN,OAAO,QAiBjB;wBA1UuB,OAAO;wBAMP,eAAe"}
1
+ {"version":3,"file":"make_cap.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/make_cap.js"],"names":[],"mappings":"AAiRA;;;;;;;;;;;;;;GAcG;AACH,4DAbW,MAAM,gBACN,YAAY,GAAC,MAAM,EAAE,cACrB,OAAO,EAAE,gBACT,OAAO,EAAE,eAET,OAAO,EAAE,SACT,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,gBACrB,MAAM,mBACN,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,QACN,OAAO,QA6BjB;AAED;;;;;;GAMG;AACH,wDALW,MAAM,OACN;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,YAAY,EAAC,MAAM,CAAA;CAAC,mBAC3C,MAAM,QACN,OAAO,QAiBjB;wBAnVuB,OAAO;wBAMP,eAAe"}
@@ -79,31 +79,40 @@ function make_cap_round(
79
79
  const B = in_binormals[index];
80
80
  const T = in_tangents[index];
81
81
 
82
- // Outward cap axis. Derive it from the PATH itself (the segment between the
83
- // cap and its neighbour sample) rather than from N x B: the Frenet frame's
84
- // tangent can flip sign at curvature features, and an N x B-derived axis
85
- // would then make the cap bulge inward for a frame ("cap twists off-axis").
86
- // The path direction is always well-defined and outward. Fall back to the
87
- // frame only if the neighbour sample is coincident (degenerate segment).
82
+ // Outward cap axis. The dome bulges along the tube tangent at the cap; the
83
+ // axis must point outward (away from the body) and be stable frame to frame.
84
+ // T is the per-sample curve tangent, oriented towards increasing samples
85
+ // ("forward"). Outward is therefore backward at the start cap and forward at
86
+ // the end cap - i.e. -direction * T - a sign that depends only on which end
87
+ // this is, never on the neighbour sample. The old code derived the axis from
88
+ // the neighbour chord (cap point minus the adjacent sample); that points
89
+ // outward but its DIRECTION jitters, because the adaptive resampler sets the
90
+ // neighbour's spacing and on a curve a longer/shorter first segment points a
91
+ // different way - so the chord, and the dome with it, swung as animated
92
+ // dashes slid across path knots (the "twitch"). The neighbour chord is now
93
+ // only a fallback for when T is unavailable.
88
94
  const point_count = in_positions.length / 3;
89
95
  const neighbour = direction > 0
90
96
  ? Math.min(index + 1, point_count - 1)
91
97
  : Math.max(index - 1, 0);
92
98
  const n3 = neighbour * 3;
93
99
 
94
- const tangent = new Vector3(
95
- Px - in_positions[n3],
96
- Py - in_positions[n3 + 1],
97
- Pz - in_positions[n3 + 2]
98
- );
100
+ const tangent = new Vector3();
99
101
 
100
- if (tangent.lengthSq() > 1e-12) {
101
- tangent.normalize();
102
+ if (T !== undefined && T.lengthSq() > 1e-12) {
103
+ tangent.copy(T).normalize().multiplyScalar(-direction);
102
104
  } else {
103
- // degenerate: neighbour coincides with the cap point, use the frame
104
- tangent.crossVectors(N, B);
105
- tangent.normalize();
106
- tangent.multiplyScalar(-direction);
105
+ const out_x = Px - in_positions[n3];
106
+ const out_y = Py - in_positions[n3 + 1];
107
+ const out_z = Pz - in_positions[n3 + 2];
108
+
109
+ if ((out_x * out_x + out_y * out_y + out_z * out_z) > 1e-12) {
110
+ // no usable tangent: the neighbour chord already points outward
111
+ tangent.set(out_x, out_y, out_z).normalize();
112
+ } else {
113
+ // fully degenerate: derive the axis from the frame
114
+ tangent.crossVectors(N, B).normalize().multiplyScalar(-direction);
115
+ }
107
116
  }
108
117
 
109
118
  // Use the path frame's N/B directly for BOTH ends. The profile orientation