@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 +1 -1
- package/src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.js +306 -226
- package/src/engine/graphics/ecs/path/tube/build/makeTubeGeometry.js +218 -202
- package/src/engine/graphics/ecs/path/tube/build/make_cap.d.ts.map +1 -1
- package/src/engine/graphics/ecs/path/tube/build/make_cap.js +26 -17
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.
|
|
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":"
|
|
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
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* @
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
added_points
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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":"
|
|
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.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
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 (
|
|
101
|
-
tangent.normalize();
|
|
102
|
+
if (T !== undefined && T.lengthSq() > 1e-12) {
|
|
103
|
+
tangent.copy(T).normalize().multiplyScalar(-direction);
|
|
102
104
|
} else {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|