Keep a separate committed sampling anchor so a moving ribbon head does not prevent slow motion from adding trail points. Reset the anchor on warps, fades and explicit clears. Covered by a slow-motion regression.
- commit
- a8237c65e1c2260f12cdc98937f338c2f53a6756
- parent
- 7e7a85f65097b1cb7e73fc3b5b18448127938bd1
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 21 | +++++++++++++++++++-- |
| M | tests/ | 16 | ++++++++++++++++ |
@@ -61,6 +61,12 @@ export function createTrail(
6161 6262 const tint = new THREE.Color(color);6363 const history: THREE.Vector3[] = []; // newest first64+ // `history[0]` follows the live marble every frame so the ribbon stays65+ // attached. Sampling distance must therefore use a separate committed66+ // anchor; comparing against the moving head prevents slow/kinematic motion67+ // from ever accumulating MIN_STEP and stretches one old segment forever.68+ const sampleAnchor = new THREE.Vector3();69+ let hasSampleAnchor = false;6470 let headWidth = 0.2;6571 6672 const camPos = new THREE.Vector3();@@ -117,13 +123,22 @@ export function createTrail(
117123 },118124 update(pos: THREE.Vector3, camera: THREE.Camera): void {119125 const head = history[0];120- if (head && pos.distanceTo(head) > jumpReset) history.length = 0;126+ if (head && pos.distanceTo(head) > jumpReset) {127+ history.length = 0;128+ hasSampleAnchor = false;129+ }130+ if (!hasSampleAnchor) {131+ sampleAnchor.copy(pos);132+ hasSampleAnchor = true;133+ }121134 if (122- !history[0] || pos.distanceToSquared(history[0]) >= minStep * minStep135+ !history[0] || pos.distanceToSquared(sampleAnchor) >= minStep * minStep123136 ) {124137 history.unshift(pos.clone());138+ sampleAnchor.copy(pos);125139 if (history.length > maxPoints) history.length = maxPoints;126140 } else {141+ // Keep the visual head attached without moving the sampling anchor.127142 history[0].copy(pos);128143 }129144 rewrite(camera);@@ -131,6 +146,7 @@ export function createTrail(
131146 fade(camera: THREE.Camera, opacity: number): void {132147 if (opacity <= 0) {133148 history.length = 0;149+ hasSampleAnchor = false;134150 geometry.setDrawRange(0, 0);135151 return;136152 }@@ -138,6 +154,7 @@ export function createTrail(
138154 },139155 reset(): void {140156 history.length = 0;157+ hasSampleAnchor = false;141158 geometry.setDrawRange(0, 0);142159 },143160 dispose(): void {@@ -49,3 +49,19 @@ Deno.test("trail: warp reset never bridges distant points", () => {
4949 assertEquals(trail.obj.geometry.drawRange.count, 0);5050 trail.dispose();5151 });52+53+Deno.test("trail: slow rail motion accumulates into new samples", () => {54+ const trail = createTrail(0xffffff, { minStep: 0.06 });55+ const camera = new THREE.PerspectiveCamera();56+ camera.position.set(0, 3, 6);57+ // Kinematic/rider motion advances less than minStep on each frame. The58+ // sampler must measure from the last committed point, not its moving head.59+ for (let i = 0; i <= 12; i++) {60+ trail.update(new THREE.Vector3(i * 0.01, 0, 0), camera);61+ }62+ assert(63+ trail.obj.geometry.drawRange.count >= 12,64+ `slow movement did not produce trail samples: ${trail.obj.geometry.drawRange.count}`,65+ );66+ trail.dispose();67+});