<commit_before>import { Vec2 } from "../../common/geometry";
import { LeastSquaresSolver } from "./lsq_solver";

export class Velocity {
    pixelsPerSecond: Vec2;

    constructor(pixelsPerSecond: Vec2) {
        this.pixelsPerSecond = pixelsPerSecond;
    }

    negate(): Velocity {
        return new Velocity(this.pixelsPerSecond.negate());
    }

    add(rhs: Velocity): Velocity {
        return new Velocity(this.pixelsPerSecond.add(rhs.pixelsPerSecond));
    }

    sub(rhs: Velocity): Velocity {
        return new Velocity(this.pixelsPerSecond.sub(rhs.pixelsPerSecond));
    }

    eq(rhs: Velocity): boolean {
        return this.pixelsPerSecond.eq(rhs.pixelsPerSecond);
    }

    clampMagnitude(minValue: number, maxValue: number): Velocity {
        const valueSquared = this.pixelsPerSecond.distanceSquared();
        if (valueSquared > maxValue * maxValue) {
            return new Velocity(this.pixelsPerSecond.div(this.pixelsPerSecond.distance()).mul(maxValue));
        }
        if (valueSquared < minValue * minValue) {
            return new Velocity(this.pixelsPerSecond.div(this.pixelsPerSecond.distance()).mul(minValue));
        }
        return this;
    }
}

export class VelocityEstimate {
    readonly pixelsPerSecond: Vec2;
    readonly confidence: number;
    readonly duration: number;
    readonly offset: Vec2;

    constructor(
        pixelsPerSecond: Vec2,
        confidence: number,
        duration: number,
        offset: Vec2,
    ) {
        this.pixelsPerSecond = pixelsPerSecond;
        this.confidence = confidence;
        this.duration = duration;
        this.offset = offset;
    }
}

class PointAtTime {
    time: number;
    point: Vec2;

    constructor(point: Vec2, time: number) {
        this.point = point;
        this.time = time;
    }
}

const ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS = 40;
const HISTORY_SIZE = 20;
const HORIZON_MILLISECONDS = 100;
const MIN_SAMPLE_SIZE = 3;

export class VelocityTracker {
    private _samples = new Array<PointAtTime>(HISTORY_SIZE);
    private _index = 0;

    addPosition(time: number, position: Vec2) {
        this._index++;
        if (this._index === HISTORY_SIZE) {
            this._index = 0;
        }
        this._samples[this._index] = new PointAtTime(position, time);
    }

    getVelocityEstimate(): VelocityEstimate | null {
        let index = this._index;

        const newestSample = this._samples[index];
        if (newestSample === null) {
            return null;
        }

        const x = [] as number[];
        const y = [] as number[];
        const w = [] as number[];
        const time = [] as number[];

        let sampleCount = 0;
        let previousSample = newestSample;
        let oldestSample = newestSample;

        do {
            const sample = this._samples[index];
            if (sample === null) {
                break;
            }

            const age = newestSample.time - sample.time;
            const delta = Math.abs(sample.time - previousSample.time);
            previousSample = sample;

            if (age > HORIZON_MILLISECONDS || delta > ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS) {
                break;
            }

            oldestSample = sample;
            const position = sample.point;
            x.push(position.x);
            y.push(position.y);
            w.push(1);
            time.push(-age);
            index = ((index === 0) ? HISTORY_SIZE : index) - 1;
            sampleCount++;
        } while (sampleCount < HISTORY_SIZE);

        if (sampleCount > MIN_SAMPLE_SIZE) {
            const xSolver = new LeastSquaresSolver(time, x, w);
            const xFit = xSolver.solve(2);
            if (xFit !== null) {
                const ySolver = new LeastSquaresSolver(time, y, w);
                const yFit = ySolver.solve(2);
                if (yFit !== null) {
                    return new VelocityEstimate(
                        new Vec2(xFit.coefficients[1] * 1000, yFit.coefficients[1] * 1000),
                        xFit.confidence * yFit.confidence,
                        newestSample.time - oldestSample.time,
                        newestSample.point.sub(oldestSample.point),
                    );
                }
            }
        }
        return new VelocityEstimate(
            new Vec2(0, 0),
            1,
            newestSample.time - oldestSample.time,
            newestSample.point.sub(oldestSample.point),
        );
    }

    getVelocity(): Velocity | null {
        const estimate = this.getVelocityEstimate();
        if (estimate === null || estimate.pixelsPerSecond.eq(Vec2.Zero)) {
            return null;
        }
        return new Velocity(estimate.pixelsPerSecond);
    }
}<commit_msg>Replace Velocity class with Vec2<commit_after>import { Vec2 } from "../../common/geometry";
import { LeastSquaresSolver } from "./lsq_solver";

export function velocityClampMagnitude(velocity: Vec2, minValue: number, maxValue: number): Vec2 {
    const valueSquared = velocity.distanceSquared();
    if (valueSquared > maxValue * maxValue) {
        return velocity.div(velocity.distance()).mul(maxValue);
    }
    if (valueSquared < minValue * minValue) {
        return velocity.div(velocity.distance()).mul(minValue);
    }
    return velocity;
}

export class VelocityEstimate {
    readonly pixelsPerSecond: Vec2;
    readonly confidence: number;
    readonly duration: number;
    readonly offset: Vec2;

    constructor(
        pixelsPerSecond: Vec2,
        confidence: number,
        duration: number,
        offset: Vec2,
    ) {
        this.pixelsPerSecond = pixelsPerSecond;
        this.confidence = confidence;
        this.duration = duration;
        this.offset = offset;
    }
}

class PointAtTime {
    time: number;
    point: Vec2;

    constructor(point: Vec2, time: number) {
        this.point = point;
        this.time = time;
    }
}

const ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS = 40;
const HISTORY_SIZE = 20;
const HORIZON_MILLISECONDS = 100;
const MIN_SAMPLE_SIZE = 3;

export class VelocityTracker {
    private _samples = new Array<PointAtTime>(HISTORY_SIZE);
    private _index = 0;

    addPosition(time: number, position: Vec2) {
        this._index++;
        if (this._index === HISTORY_SIZE) {
            this._index = 0;
        }
        this._samples[this._index] = new PointAtTime(position, time);
    }

    getVelocityEstimate(): VelocityEstimate | null {
        let index = this._index;

        const newestSample = this._samples[index];
        if (newestSample === null) {
            return null;
        }

        const x = [] as number[];
        const y = [] as number[];
        const w = [] as number[];
        const time = [] as number[];

        let sampleCount = 0;
        let previousSample = newestSample;
        let oldestSample = newestSample;

        do {
            const sample = this._samples[index];
            if (sample === null) {
                break;
            }

            const age = newestSample.time - sample.time;
            const delta = Math.abs(sample.time - previousSample.time);
            previousSample = sample;

            if (age > HORIZON_MILLISECONDS || delta > ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS) {
                break;
            }

            oldestSample = sample;
            const position = sample.point;
            x.push(position.x);
            y.push(position.y);
            w.push(1);
            time.push(-age);
            index = ((index === 0) ? HISTORY_SIZE : index) - 1;
            sampleCount++;
        } while (sampleCount < HISTORY_SIZE);

        if (sampleCount > MIN_SAMPLE_SIZE) {
            const xSolver = new LeastSquaresSolver(time, x, w);
            const xFit = xSolver.solve(2);
            if (xFit !== null) {
                const ySolver = new LeastSquaresSolver(time, y, w);
                const yFit = ySolver.solve(2);
                if (yFit !== null) {
                    return new VelocityEstimate(
                        new Vec2(xFit.coefficients[1] * 1000, yFit.coefficients[1] * 1000),
                        xFit.confidence * yFit.confidence,
                        newestSample.time - oldestSample.time,
                        newestSample.point.sub(oldestSample.point),
                    );
                }
            }
        }
        return new VelocityEstimate(
            new Vec2(0, 0),
            1,
            newestSample.time - oldestSample.time,
            newestSample.point.sub(oldestSample.point),
        );
    }

    getVelocity(): Vec2 | null {
        const estimate = this.getVelocityEstimate();
        if (estimate === null || estimate.pixelsPerSecond.eq(Vec2.Zero)) {
            return null;
        }
        return estimate.pixelsPerSecond;
    }
}
{'fraction_non_alphanumeric': 0.10410990009082652, 'fraction_numerical': 0.009763851044504996, 'mean_word_length': 1.9101420548397754, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5914628', 'n_tokens_mistral': 2649, 'n_tokens_neox': 2535, 'n_words': 558}