@needle-tools/engine 5.1.10 → 5.1.12
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/CHANGELOG.md +3 -0
- package/SKILL.md +2 -0
- package/components.needle.json +1 -1
- package/dist/{needle-engine.bundle-DP--RGAU.js → needle-engine.bundle-9m-VdGg2.js} +3798 -3755
- package/dist/{needle-engine.bundle-DZD-GG7l.min.js → needle-engine.bundle-BmdWLmLa.min.js} +127 -127
- package/dist/{needle-engine.bundle-Dv5TidOk.umd.cjs → needle-engine.bundle-DdH96IqH.umd.cjs} +113 -113
- package/dist/needle-engine.d.ts +52 -13
- package/dist/needle-engine.js +395 -395
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +71 -71
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_math.d.ts +13 -0
- package/lib/engine/engine_math.js +17 -0
- package/lib/engine/engine_math.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/DragControls.d.ts +0 -5
- package/lib/engine-components/DragControls.js +0 -8
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/RendererInstancing.d.ts +34 -3
- package/lib/engine-components/RendererInstancing.js +92 -11
- package/lib/engine-components/RendererInstancing.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/splines/SplineWalker.js +9 -3
- package/lib/engine-components/splines/SplineWalker.js.map +1 -1
- package/package.json +1 -1
- package/plugins/common/license.js +4 -4
- package/plugins/vite/license.js +4 -4
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_license.ts +68 -68
- package/src/engine/engine_math.ts +17 -0
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/DragControls.ts +0 -7
- package/src/engine-components/RendererInstancing.ts +92 -11
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/splines/SplineWalker.ts +9 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { calculateMeshLODLevel, getLODColor } from "@needle-tools/gltf-progressive";
|
|
2
|
-
import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial, Vector3 } from "three";
|
|
2
|
+
import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial, Sphere, Vector3 } from "three";
|
|
3
3
|
|
|
4
4
|
import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
|
|
5
5
|
import { Gizmos } from "../engine/engine_gizmos.js";
|
|
@@ -25,6 +25,11 @@ function trackProgressiveLOD<T>(context: Context, type: "mesh" | "texture", obje
|
|
|
25
25
|
return promise;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Mirrored = negative determinant, which reverses triangle winding. */
|
|
29
|
+
function isMirrored(matrix: Matrix4): boolean {
|
|
30
|
+
return matrix.determinant() < 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* Handles instancing for Needle Engine.
|
|
30
35
|
*/
|
|
@@ -108,8 +113,12 @@ export class InstancingHandler {
|
|
|
108
113
|
const mat = mesh.material as Material | Material[];
|
|
109
114
|
if (Array.isArray(mat)) return null;
|
|
110
115
|
|
|
116
|
+
// mirrored objects need their own batch, see InstancedMeshRenderer
|
|
117
|
+
mesh.updateWorldMatrix(true, false);
|
|
118
|
+
const mirrored = isMirrored(mesh.matrixWorld);
|
|
119
|
+
|
|
111
120
|
for (const i of this.objs) {
|
|
112
|
-
if (!i.canAdd(geometry, mat)) continue;
|
|
121
|
+
if (!i.canAdd(geometry, mat, mirrored)) continue;
|
|
113
122
|
const handle = i.addInstance(mesh, geometry);
|
|
114
123
|
return handle;
|
|
115
124
|
}
|
|
@@ -119,7 +128,7 @@ export class InstancingHandler {
|
|
|
119
128
|
}
|
|
120
129
|
let name = obj.name;
|
|
121
130
|
if (!name?.length) name = makeIdFromRandomWords();
|
|
122
|
-
const i = new InstancedMeshRenderer(name, geometry, mat, maxInstances, context);
|
|
131
|
+
const i = new InstancedMeshRenderer(name, geometry, mat, maxInstances, context, mirrored);
|
|
123
132
|
this.objs.push(i);
|
|
124
133
|
const handle = i.addInstance(mesh, geometry);
|
|
125
134
|
return handle;
|
|
@@ -240,8 +249,18 @@ export class InstanceHandle {
|
|
|
240
249
|
updateInstanceMatrix(updateChildren: boolean = false, updateMatrix: boolean = true) {
|
|
241
250
|
if (this.__instanceIndex < 0) return;
|
|
242
251
|
if (updateMatrix) this.object.updateWorldMatrix(true, updateChildren);
|
|
252
|
+
this.assertMirroringUnchanged();
|
|
243
253
|
this.renderer.updateInstance(this.object.matrixWorld, this.__instanceIndex);
|
|
244
254
|
}
|
|
255
|
+
|
|
256
|
+
private _didWarnAboutMirroringChange = false;
|
|
257
|
+
/** Batches are bucketed by mirroring at setup time and an instance can not move between them. */
|
|
258
|
+
private assertMirroringUnchanged() {
|
|
259
|
+
if (this._didWarnAboutMirroringChange) return;
|
|
260
|
+
if (isMirrored(this.object.matrixWorld) === this.renderer.mirrored) return;
|
|
261
|
+
this._didWarnAboutMirroringChange = true;
|
|
262
|
+
console.warn(`[Instancing] "${this.name}" changed its mirroring at runtime (world matrix determinant flipped sign). Instanced objects can not change mirroring - this object will render its backfaces. Disable instancing for it via Renderer.setInstanced(obj, false).`);
|
|
263
|
+
}
|
|
245
264
|
/** Updates the matrix of the instance */
|
|
246
265
|
setMatrix(matrix: Matrix4) {
|
|
247
266
|
if (this.__instanceIndex < 0) return;
|
|
@@ -320,6 +339,23 @@ export class InstanceHandle {
|
|
|
320
339
|
}
|
|
321
340
|
}
|
|
322
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Renders one geometry+material combination for many objects using a
|
|
344
|
+
* {@link https://threejs.org/docs/#api/en/objects/BatchedMesh | BatchedMesh}.
|
|
345
|
+
*
|
|
346
|
+
* The BatchedMesh belongs to this class: it is created here, parented directly to the untransformed
|
|
347
|
+
* `context.scene` and never moved, so its transform is whatever {@link applyBatchTransform} puts
|
|
348
|
+
* there and nothing else.
|
|
349
|
+
*
|
|
350
|
+
* **Mirroring:** three flips the winding for a mirrored transform per *draw call*, from
|
|
351
|
+
* `object.matrixWorld.determinant()` - and for a batch that object is the BatchedMesh, not the
|
|
352
|
+
* instance. So mirrored instances get their own batch, which carries the mirror on its own
|
|
353
|
+
* transform (`scale.z = -1`); otherwise their front faces would be culled.
|
|
354
|
+
*
|
|
355
|
+
* That mirror is the one transform the shader's `modelMatrix * batchingMatrix` would otherwise apply
|
|
356
|
+
* twice, so a mirrored batch stores its instance matrices with it undone. Every other batch is
|
|
357
|
+
* untransformed and stores world matrices as they are.
|
|
358
|
+
*/
|
|
323
359
|
class InstancedMeshRenderer {
|
|
324
360
|
/** The three instanced mesh
|
|
325
361
|
* @link https://threejs.org/docs/#api/en/objects/InstancedMesh
|
|
@@ -327,6 +363,11 @@ class InstancedMeshRenderer {
|
|
|
327
363
|
get batchedMesh() {
|
|
328
364
|
return this._batchedMesh;
|
|
329
365
|
}
|
|
366
|
+
|
|
367
|
+
/** True if this batch renders mirrored objects. A batch only holds one or the other. */
|
|
368
|
+
get mirrored() {
|
|
369
|
+
return this._mirrored;
|
|
370
|
+
}
|
|
330
371
|
get visible(): boolean {
|
|
331
372
|
return this._batchedMesh.visible;
|
|
332
373
|
}
|
|
@@ -369,15 +410,17 @@ class InstancedMeshRenderer {
|
|
|
369
410
|
if (sphere)
|
|
370
411
|
this._batchedMesh.computeBoundingSphere();
|
|
371
412
|
if (debugInstancing && this._batchedMesh.boundingSphere) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
// the bounding sphere is in the batch's local space, the gizmo expects world space
|
|
414
|
+
const sphere = InstancedMeshRenderer.debugBoundsSphere
|
|
415
|
+
.copy(this._batchedMesh.boundingSphere)
|
|
416
|
+
.applyMatrix4(this._batchedMesh.matrixWorld);
|
|
375
417
|
Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0x00ff00);
|
|
376
418
|
}
|
|
377
419
|
}
|
|
378
420
|
|
|
379
421
|
private _context: Context;
|
|
380
422
|
private _batchedMesh: BatchedMesh;
|
|
423
|
+
private readonly _mirrored: boolean;
|
|
381
424
|
private _handles: (InstanceHandle | null)[] = [];
|
|
382
425
|
private _geometryIds = new WeakMap<BufferGeometry, number>();
|
|
383
426
|
private _maxInstanceCount: number;
|
|
@@ -389,8 +432,9 @@ class InstancedMeshRenderer {
|
|
|
389
432
|
private _maxVertexCount: number;
|
|
390
433
|
private _maxIndexCount: number;
|
|
391
434
|
|
|
392
|
-
private static
|
|
435
|
+
private static instanceMatrix: Matrix4 = new Matrix4();
|
|
393
436
|
private static lodProjectionScreenMatrix: Matrix4 = new Matrix4();
|
|
437
|
+
private static debugBoundsSphere: Sphere = new Sphere();
|
|
394
438
|
private static debugLODColor = new Color();
|
|
395
439
|
private static lodSelectionResult = {
|
|
396
440
|
level: -1,
|
|
@@ -403,12 +447,15 @@ class InstancedMeshRenderer {
|
|
|
403
447
|
/** Check if the geometry can be added to this instancer
|
|
404
448
|
* @param geometry The geometry to check
|
|
405
449
|
* @param material The material of the geometry
|
|
450
|
+
* @param mirrored Whether the object's world matrix has a negative determinant
|
|
406
451
|
* @returns true if the geometry can be added
|
|
407
452
|
*/
|
|
408
|
-
canAdd(geometry: BufferGeometry, material: Material): boolean {
|
|
453
|
+
canAdd(geometry: BufferGeometry, material: Material, mirrored: boolean): boolean {
|
|
409
454
|
|
|
410
455
|
if (this._maxVertexCount > 10_000_000) return false;
|
|
411
456
|
|
|
457
|
+
if (this._mirrored !== mirrored) return false;
|
|
458
|
+
|
|
412
459
|
// The material instance must match
|
|
413
460
|
// perhaps at some point later we *could* check if it's the same shader and properties but this would be risky
|
|
414
461
|
if (material !== this.material) {
|
|
@@ -482,11 +529,12 @@ class InstancedMeshRenderer {
|
|
|
482
529
|
return this.name ? `${this.name} (BatchedMesh)` : "BatchedMesh";
|
|
483
530
|
}
|
|
484
531
|
|
|
485
|
-
constructor(name: string, geo: BufferGeometry, material: Material, initialMaxCount: number, context: Context) {
|
|
532
|
+
constructor(name: string, geo: BufferGeometry, material: Material, initialMaxCount: number, context: Context, mirrored: boolean = false) {
|
|
486
533
|
this.name = name;
|
|
487
534
|
this.geometry = geo;
|
|
488
535
|
this.material = material;
|
|
489
536
|
this._context = context;
|
|
537
|
+
this._mirrored = mirrored;
|
|
490
538
|
this._maxInstanceCount = Math.max(2, initialMaxCount);
|
|
491
539
|
if (debugInstancing) {
|
|
492
540
|
this._debugMaterial = createDebugMaterial(debugLODColors);
|
|
@@ -496,6 +544,7 @@ class InstancedMeshRenderer {
|
|
|
496
544
|
this._maxIndexCount = estimate.indexCount;
|
|
497
545
|
this._batchedMesh = new BatchedMesh(this._maxInstanceCount, this._maxVertexCount, this._maxIndexCount, this._debugMaterial ?? this.material);
|
|
498
546
|
this._batchedMesh.name = this.getBatchedMeshName();
|
|
547
|
+
this.applyBatchTransform(this._batchedMesh);
|
|
499
548
|
// this.inst = new InstancedMesh(geo, material, count);
|
|
500
549
|
this._batchedMesh[$instancingAutoUpdateBounds] = true;
|
|
501
550
|
// this.inst.count = 0;
|
|
@@ -614,10 +663,39 @@ class InstancedMeshRenderer {
|
|
|
614
663
|
}
|
|
615
664
|
|
|
616
665
|
updateInstance(mat: Matrix4, index: number) {
|
|
617
|
-
this.
|
|
666
|
+
this.setInstanceMatrix(index, mat);
|
|
618
667
|
this.markNeedsUpdate();
|
|
619
668
|
}
|
|
620
669
|
|
|
670
|
+
/** The batch's own transform. This class owns it: the mirror is the only thing that ever goes
|
|
671
|
+
* here, and the batch is never reparented away from the untransformed scene root. */
|
|
672
|
+
private applyBatchTransform(batch: BatchedMesh) {
|
|
673
|
+
// every other batch keeps the identity a fresh BatchedMesh is constructed with
|
|
674
|
+
if (!this._mirrored) return;
|
|
675
|
+
if (batch.scale.z === -1) return;
|
|
676
|
+
// negative determinant on the batch itself -> three flips the winding for the whole draw call
|
|
677
|
+
batch.scale.z = -1;
|
|
678
|
+
// three composes the TRS into matrix/matrixWorld no earlier than the render, which would leave
|
|
679
|
+
// those identity for a frame - and updateBounds() reads matrixWorld from a pre-render callback
|
|
680
|
+
batch.updateMatrixWorld();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Writes a *world* matrix to an instance slot, converted into the batch's local space.
|
|
684
|
+
* All instance matrix writes go through here. */
|
|
685
|
+
private setInstanceMatrix(index: number, worldMatrix: Matrix4) {
|
|
686
|
+
if (!this._mirrored) {
|
|
687
|
+
// an untransformed batch: local space *is* world space
|
|
688
|
+
this._batchedMesh.setMatrixAt(index, worldMatrix);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// undo the batch's mirror, so the shader's modelMatrix * batchingMatrix composes back to the world matrix.
|
|
692
|
+
// a pure z flip only negates row 2 (column-major indices 2/6/10/14), so it's cheaper than a full multiply
|
|
693
|
+
const local = InstancedMeshRenderer.instanceMatrix.copy(worldMatrix);
|
|
694
|
+
const e = local.elements;
|
|
695
|
+
e[2] = -e[2]; e[6] = -e[6]; e[10] = -e[10]; e[14] = -e[14];
|
|
696
|
+
this._batchedMesh.setMatrixAt(index, local);
|
|
697
|
+
}
|
|
698
|
+
|
|
621
699
|
updateGeometry(geo: BufferGeometry, geometryIndex: number): boolean {
|
|
622
700
|
if (!this.validateGeometry(geo)) {
|
|
623
701
|
return false;
|
|
@@ -815,11 +893,14 @@ class InstancedMeshRenderer {
|
|
|
815
893
|
newInst.receiveShadow = this._batchedMesh.receiveShadow;
|
|
816
894
|
newInst.visible = this._batchedMesh.visible;
|
|
817
895
|
newInst[$instancingAutoUpdateBounds] = this._batchedMesh[$instancingAutoUpdateBounds];
|
|
896
|
+
|
|
818
897
|
newInst.matrixAutoUpdate = this._batchedMesh.matrixAutoUpdate;
|
|
819
898
|
newInst.matrixWorldNeedsUpdate = this._batchedMesh.matrixWorldNeedsUpdate;
|
|
820
899
|
newInst.matrixAutoUpdate = this._batchedMesh.matrixAutoUpdate;
|
|
821
900
|
newInst.matrixWorld.copy(this._batchedMesh.matrixWorld);
|
|
822
901
|
newInst.matrix.copy(this._batchedMesh.matrix);
|
|
902
|
+
// the replacement must carry the mirror too, otherwise every instance in it ends up culled
|
|
903
|
+
this.applyBatchTransform(newInst);
|
|
823
904
|
|
|
824
905
|
// dispose the old batched mesh
|
|
825
906
|
this._batchedMesh.dispose();
|
|
@@ -942,7 +1023,7 @@ class InstancedMeshRenderer {
|
|
|
942
1023
|
handle.__instanceIndex = i;
|
|
943
1024
|
handle.__reservedVertexRange = handle.maxVertexCount;
|
|
944
1025
|
handle.__reservedIndexRange = handle.maxIndexCount;
|
|
945
|
-
this.
|
|
1026
|
+
this.setInstanceMatrix(i, handle.object.matrixWorld);
|
|
946
1027
|
this.updateDebugLODColor(handle);
|
|
947
1028
|
if (debugInstancing)
|
|
948
1029
|
console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}\nVertices: ${this._currentVertexCount.toLocaleString()}/${this._maxVertexCount.toLocaleString()},\nIndices: ${this._currentIndexCount.toLocaleString()}/${this._maxIndexCount.toLocaleString()}`);
|
|
@@ -3,7 +3,7 @@ import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "t
|
|
|
3
3
|
|
|
4
4
|
import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
|
|
5
5
|
import { findObjectOfType } from "../../../engine/engine_components.js";
|
|
6
|
-
import { $
|
|
6
|
+
import { _$VfAckLjB } from "../../../engine/engine_license.js";
|
|
7
7
|
import { serializable } from "../../../engine/engine_serialization.js";
|
|
8
8
|
import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
|
|
9
9
|
import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
|
|
@@ -277,7 +277,7 @@ export class USDZExporter extends Behaviour {
|
|
|
277
277
|
let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
|
|
278
278
|
name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
|
|
279
279
|
|
|
280
|
-
if (
|
|
280
|
+
if (!_$VfAckLjB()) {
|
|
281
281
|
if (name !== "") name += "-";
|
|
282
282
|
name += "MadeWithNeedle";
|
|
283
283
|
}
|
|
@@ -682,7 +682,7 @@ export class USDZExporter extends Behaviour {
|
|
|
682
682
|
if (debug)
|
|
683
683
|
showBalloonMessage("Quicklook url: " + callToActionURL);
|
|
684
684
|
if (callToActionURL) {
|
|
685
|
-
if (
|
|
685
|
+
if (!_$VfAckLjB()) {
|
|
686
686
|
console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
|
|
687
687
|
}
|
|
688
688
|
else {
|
|
@@ -697,7 +697,7 @@ export class USDZExporter extends Behaviour {
|
|
|
697
697
|
private buildQuicklookOverlay(): CustomBranding {
|
|
698
698
|
const obj: CustomBranding = {};
|
|
699
699
|
if (this.customBranding) Object.assign(obj, this.customBranding);
|
|
700
|
-
if (
|
|
700
|
+
if (!_$VfAckLjB()) {
|
|
701
701
|
console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
|
|
702
702
|
obj.callToAction = "Close";
|
|
703
703
|
obj.checkoutTitle = "🌵 Made with Needle";
|
|
@@ -74,7 +74,11 @@ export class SplineWalker extends Behaviour {
|
|
|
74
74
|
return this._position01;
|
|
75
75
|
}
|
|
76
76
|
set position01(v: number) {
|
|
77
|
-
|
|
77
|
+
// Normalize on assignment so reading the value back is predictable and never out of range.
|
|
78
|
+
// Doing this only in updateFromPosition() made it depend on a spline + curve + object being
|
|
79
|
+
// ready and a frame having run — until then this property reported e.g. 1.5.
|
|
80
|
+
// Note this is destructive (as clamping always is): setting 1.5 while clamped stores 1.
|
|
81
|
+
this._position01 = this.clamp ? Mathf.clamp01(v) : Mathf.repeat(v);
|
|
78
82
|
this._needsUpdate = true;
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -156,10 +160,12 @@ export class SplineWalker extends Behaviour {
|
|
|
156
160
|
if (!this.spline || !this.spline.curve) return;
|
|
157
161
|
if (!this.object) return;
|
|
158
162
|
|
|
163
|
+
// Still normalized here as well: autoRun accumulates into _position01 directly (bypassing the
|
|
164
|
+
// setter), and `clamp` may change after a value was assigned.
|
|
159
165
|
if (this.clamp) this._position01 = Mathf.clamp01(this._position01);
|
|
160
|
-
else this._position01 = this._position01
|
|
166
|
+
else this._position01 = Mathf.repeat(this._position01);
|
|
161
167
|
|
|
162
|
-
const t = this._position01 >= 1 ? 1 : this._position01
|
|
168
|
+
const t = this._position01 >= 1 ? 1 : Mathf.repeat(this._position01);
|
|
163
169
|
const pt = this.spline.getPointAt(t);
|
|
164
170
|
|
|
165
171
|
if (this.pullStrength >= 1) {
|