From 10dd11208ede8c10336527cd45adfb96ad1a732d Mon Sep 17 00:00:00 2001 From: Jonathan Olson Date: Thu, 13 Feb 2025 16:39:46 -0700 Subject: [PATCH] progress on dot Util separation, see https://github.com/phetsims/dot/issues/4 --- js/LinearFunction.ts | 7 +++-- js/Utils.js | 59 ++++++++++++++++-------------------- js/UtilsTests.js | 7 +++-- js/Vector2.ts | 6 ++-- js/Vector3.ts | 4 +-- js/Vector4.ts | 6 ++-- js/util/clamp.ts | 8 +++-- js/util/distanceXY.ts | 6 +++- js/util/factorial.ts | 35 ++++++++++++--------- js/util/moduloBetweenDown.ts | 28 +++++++++++++++++ js/util/moduloBetweenUp.ts | 18 +++++++++++ js/util/rangeExclusive.ts | 15 +++++++++ js/util/rangeInclusive.ts | 21 +++++++++++++ js/util/roundSymmetric.ts | 8 +++-- js/util/toDegrees.ts | 14 +++++++++ js/util/toRadians.ts | 14 +++++++++ 16 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 js/util/moduloBetweenDown.ts create mode 100644 js/util/moduloBetweenUp.ts create mode 100644 js/util/rangeExclusive.ts create mode 100644 js/util/rangeInclusive.ts create mode 100644 js/util/toDegrees.ts create mode 100644 js/util/toRadians.ts diff --git a/js/LinearFunction.ts b/js/LinearFunction.ts index 31bbd7c..e0f8ce5 100644 --- a/js/LinearFunction.ts +++ b/js/LinearFunction.ts @@ -15,6 +15,7 @@ import dot from './dot.js'; import Utils from './Utils.js'; +import { clamp } from './util/clamp.js'; export default class LinearFunction { private a1: number; @@ -58,12 +59,12 @@ export default class LinearFunction { * f( a1 ) = b1, f( a2 ) = b2, f( a3 ) = * Optionally clamp the result to the range [b1,b2]. */ -const map = ( a1: number, a2: number, b1: number, b2: number, a3: number, clamp: boolean ): number => { +const map = ( a1: number, a2: number, b1: number, b2: number, a3: number, clampValue: boolean ): number => { let b3 = Utils.linear( a1, a2, b1, b2, a3 ); - if ( clamp ) { + if ( clampValue ) { const max = Math.max( b1, b2 ); const min = Math.min( b1, b2 ); - b3 = Utils.clamp( b3, min, max ); + b3 = clamp( b3, min, max ); } return b3; }; diff --git a/js/Utils.js b/js/Utils.js index 67b26c1..60e7485 100644 --- a/js/Utils.js +++ b/js/Utils.js @@ -8,8 +8,14 @@ import Big from '../../sherpa/lib/big-6.2.1.js'; // eslint-disable-line phet/default-import-match-filename import dot from './dot.js'; -import clamp from './util/clamp.js'; -import roundSymmetric from './util/roundSymmetric.js'; +import { clamp } from './util/clamp.js'; +import { moduloBetweenDown } from './util/moduloBetweenDown.js'; +import { moduloBetweenUp } from './util/moduloBetweenUp.js'; +import { rangeExclusive } from './util/rangeExclusive.js'; +import { rangeInclusive } from './util/rangeInclusive.js'; +import { roundSymmetric } from './util/roundSymmetric.js'; +import { toDegrees } from './util/toDegrees.js'; +import { toRadians } from './util/toRadians.js'; import Vector2 from './Vector2.js'; import Vector3 from './Vector3.js'; @@ -50,20 +56,11 @@ const Utils = { * @param {number} min * @param {number} max * @returns {number} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/moduloBetweenDown.ts */ moduloBetweenDown( value, min, max ) { - assert && assert( max > min, 'max > min required for moduloBetween' ); - - const divisor = max - min; - - // get a partial result of value-min between [0,divisor) - let partial = ( value - min ) % divisor; - if ( partial < 0 ) { - // since if value-min < 0, the remainder will give us a negative number - partial += divisor; - } - - return partial + min; // add back in the minimum value + return moduloBetweenDown( value, min, max ); }, /** @@ -77,9 +74,11 @@ const Utils = { * @param {number} min * @param {number} max * @returns {number} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/moduloBetweenUp.ts */ moduloBetweenUp( value, min, max ) { - return -Utils.moduloBetweenDown( -value, -max, -min ); + return moduloBetweenUp( value, min, max ); }, /** @@ -89,16 +88,11 @@ const Utils = { * @param {number} a * @param {number} b * @returns {Array.} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/rangeInclusive.ts */ rangeInclusive( a, b ) { - if ( b < a ) { - return []; - } - const result = new Array( b - a + 1 ); - for ( let i = a; i <= b; i++ ) { - result[ i - a ] = i; - } - return result; + return rangeInclusive( a, b ); }, /** @@ -108,9 +102,11 @@ const Utils = { * @param {number} a * @param {number} b * @returns {Array.} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/rangeExclusive.ts */ rangeExclusive( a, b ) { - return Utils.rangeInclusive( a + 1, b - 1 ); + return rangeExclusive( a, b ); }, /** @@ -119,9 +115,11 @@ const Utils = { * * @param {number} degrees * @returns {number} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/toRadians.ts */ toRadians( degrees ) { - return Math.PI * degrees / 180; + return toRadians( degrees ); }, /** @@ -130,9 +128,11 @@ const Utils = { * * @param {number} radians * @returns {number} + * + * NOTE: this function is deprecated - please use the separate file function directly, js/util/toDegrees.ts */ toDegrees( radians ) { - return 180 * radians / Math.PI; + return toDegrees( radians ); }, /** @@ -895,13 +895,6 @@ const Utils = { dot.register( 'Utils', Utils ); // make these available in the main namespace directly (for now) -dot.clamp = Utils.clamp; -dot.moduloBetweenDown = Utils.moduloBetweenDown; -dot.moduloBetweenUp = Utils.moduloBetweenUp; -dot.rangeInclusive = Utils.rangeInclusive; -dot.rangeExclusive = Utils.rangeExclusive; -dot.toRadians = Utils.toRadians; -dot.toDegrees = Utils.toDegrees; dot.lineLineIntersection = Utils.lineLineIntersection; dot.lineSegmentIntersection = Utils.lineSegmentIntersection; dot.sphereRayIntersection = Utils.sphereRayIntersection; diff --git a/js/UtilsTests.js b/js/UtilsTests.js index 760f314..54e08b3 100644 --- a/js/UtilsTests.js +++ b/js/UtilsTests.js @@ -8,6 +8,7 @@ */ import dot from './dot.js'; +import { clamp } from './util/clamp.js'; import Utils from './Utils.js'; import Vector2 from './Vector2.js'; @@ -126,9 +127,9 @@ QUnit.test( 'linear map', assert => { } ); QUnit.test( 'clamp', assert => { - assert.equal( Utils.clamp( 5, 1, 4 ), 4 ); - assert.equal( Utils.clamp( 3, 1, 4 ), 3 ); - assert.equal( Utils.clamp( 0, 1, 4 ), 1 ); + assert.equal( clamp( 5, 1, 4 ), 4 ); + assert.equal( clamp( 3, 1, 4 ), 3 ); + assert.equal( clamp( 0, 1, 4 ), 1 ); } ); QUnit.test( 'rangeInclusive', assert => { diff --git a/js/Vector2.ts b/js/Vector2.ts index 74d7d48..63d858b 100644 --- a/js/Vector2.ts +++ b/js/Vector2.ts @@ -11,7 +11,8 @@ import IOType from '../../tandem/js/types/IOType.js'; import NumberIO from '../../tandem/js/types/NumberIO.js'; import { StateObject } from '../../tandem/js/types/StateSchema.js'; import dot from './dot.js'; -import roundSymmetric from './util/roundSymmetric.js'; +import { roundSymmetric } from './util/roundSymmetric.js'; +import { clamp } from './util/clamp.js'; const ADDING_ACCUMULATOR = ( vector: Vector2, nextVector: Vector2 ) => { return vector.add( nextVector ); @@ -126,8 +127,7 @@ export default class Vector2 implements TPoolable { public angleBetween( v: Vector2 ): number { const thisMagnitude = this.magnitude; const vMagnitude = v.magnitude; - // @ts-expect-error TODO: import with circular protection https://github.com/phetsims/dot/issues/96 - return Math.acos( dot.clamp( ( this.x * v.x + this.y * v.y ) / ( thisMagnitude * vMagnitude ), -1, 1 ) ); + return Math.acos( clamp( ( this.x * v.x + this.y * v.y ) / ( thisMagnitude * vMagnitude ), -1, 1 ) ); } /** diff --git a/js/Vector3.ts b/js/Vector3.ts index 4516a42..a429a4c 100644 --- a/js/Vector3.ts +++ b/js/Vector3.ts @@ -10,8 +10,8 @@ import Pool, { TPoolable } from '../../phet-core/js/Pool.js'; import IOType from '../../tandem/js/types/IOType.js'; import NumberIO from '../../tandem/js/types/NumberIO.js'; import dot from './dot.js'; -import clamp from './util/clamp.js'; -import roundSymmetric from './util/roundSymmetric.js'; +import { clamp } from './util/clamp.js'; +import { roundSymmetric } from './util/roundSymmetric.js'; const ADDING_ACCUMULATOR = ( vector: Vector3, nextVector: Vector3 ) => { return vector.add( nextVector ); diff --git a/js/Vector4.ts b/js/Vector4.ts index cd313ff..d90e138 100644 --- a/js/Vector4.ts +++ b/js/Vector4.ts @@ -8,7 +8,8 @@ import Pool, { TPoolable } from '../../phet-core/js/Pool.js'; import dot from './dot.js'; -import roundSymmetric from './util/roundSymmetric.js'; +import { clamp } from './util/clamp.js'; +import { roundSymmetric } from './util/roundSymmetric.js'; export type Vector4StateObject = { x: number; @@ -129,8 +130,7 @@ export default class Vector4 implements TPoolable { * is the input vector (normalized). */ public angleBetween( v: Vector4 ): number { - // @ts-expect-error TODO: import with circular protection https://github.com/phetsims/dot/issues/96 - return Math.acos( dot.clamp( this.normalized().dot( v.normalized() ), -1, 1 ) ); + return Math.acos( clamp( this.normalized().dot( v.normalized() ), -1, 1 ) ); } /** diff --git a/js/util/clamp.ts b/js/util/clamp.ts index e9cd0eb..01a566d 100644 --- a/js/util/clamp.ts +++ b/js/util/clamp.ts @@ -6,7 +6,10 @@ * * @author Jonathan Olson */ -export default function clamp( value: number, min: number, max: number ): number { + +import dot from '../dot.js'; + +export function clamp( value: number, min: number, max: number ): number { if ( value < min ) { return min; } @@ -16,4 +19,5 @@ export default function clamp( value: number, min: number, max: number ): number else { return value; } -} \ No newline at end of file +} +dot.register( 'clamp', clamp ); \ No newline at end of file diff --git a/js/util/distanceXY.ts b/js/util/distanceXY.ts index 431357c..8a2a9b5 100644 --- a/js/util/distanceXY.ts +++ b/js/util/distanceXY.ts @@ -5,8 +5,12 @@ * * @author Chris Malley (cmalley@pixelzoom.com) */ + +import dot from '../dot.js'; + export default function distanceXY( x1: number, y1: number, x2: number, y2: number ): number { const dx = x1 - x2; const dy = y1 - y2; return Math.sqrt( dx * dx + dy * dy ); -} \ No newline at end of file +} +dot.register( 'distanceXY', distanceXY ); \ No newline at end of file diff --git a/js/util/factorial.ts b/js/util/factorial.ts index d2e4764..62d3d61 100644 --- a/js/util/factorial.ts +++ b/js/util/factorial.ts @@ -1,16 +1,21 @@ - // Copyright 2025, University of Colorado Boulder +// Copyright 2025, University of Colorado Boulder - /** - * Computes the factorial of a non-negative integer n without using recursion. - * n! = 1 * 2 * ... * ( n - 1 ) * n - * - * @author Chris Malley (cmalley@pixelzoom.com) - */ - export default function factorial( n: number ): number { - assert && assert( Number.isInteger( n ) && n >= 0, `n must be a non-negative integer: ${n}` ); - let f = 1; - for ( let i = 2; i <= n; i++ ) { - f *= i; - } - return f; - } \ No newline at end of file +/** + * Computes the factorial of a non-negative integer n without using recursion. + * n! = 1 * 2 * ... * ( n - 1 ) * n + * + * @author Chris Malley (cmalley@pixelzoom.com) + */ + +import dot from '../dot.js'; + +export default function factorial( n: number ): number { + assert && assert( Number.isInteger( n ) && n >= 0, `n must be a non-negative integer: ${n}` ); + let f = 1; + for ( let i = 2; i <= n; i++ ) { + f *= i; + } + return f; +} + +dot.register( 'factorial', factorial ); \ No newline at end of file diff --git a/js/util/moduloBetweenDown.ts b/js/util/moduloBetweenDown.ts new file mode 100644 index 0000000..3c17fd0 --- /dev/null +++ b/js/util/moduloBetweenDown.ts @@ -0,0 +1,28 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Returns a number in the range $n\in[\mathrm{min},\mathrm{max})$ with the same equivalence class as the input + * value mod (max-min), i.e. for a value $m$, $m\equiv n\ (\mathrm{mod}\ \mathrm{max}-\mathrm{min})$. + * + * The 'down' indicates that if the value is equal to min or max, the max is returned. + * + * @author Jonathan Olson + */ + +import dot from '../dot.js'; + +export function moduloBetweenDown( value: number, min: number, max: number ): number { + assert && assert( max > min, 'max > min required for moduloBetween' ); + + const divisor = max - min; + + // get a partial result of value-min between [0,divisor) + let partial = ( value - min ) % divisor; + if ( partial < 0 ) { + // since if value-min < 0, the remainder will give us a negative number + partial += divisor; + } + + return partial + min; // add back in the minimum value +} +dot.register( 'moduloBetweenDown', moduloBetweenDown ); \ No newline at end of file diff --git a/js/util/moduloBetweenUp.ts b/js/util/moduloBetweenUp.ts new file mode 100644 index 0000000..ec107d5 --- /dev/null +++ b/js/util/moduloBetweenUp.ts @@ -0,0 +1,18 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Returns a number in the range $n\in(\mathrm{min},\mathrm{max}]$ with the same equivalence class as the input + * value mod (max-min), i.e. for a value $m$, $m\equiv n\ (\mathrm{mod}\ \mathrm{max}-\mathrm{min})$. + * + * The 'up' indicates that if the value is equal to min or max, the min is returned. + * + * @author Jonathan Olson + */ + +import { moduloBetweenDown } from './moduloBetweenDown.js'; +import dot from '../dot.js'; + +export function moduloBetweenUp( value: number, min: number, max: number ): number { + return -moduloBetweenDown( -value, -max, -min ); +} +dot.register( 'moduloBetweenUp', moduloBetweenUp ); \ No newline at end of file diff --git a/js/util/rangeExclusive.ts b/js/util/rangeExclusive.ts new file mode 100644 index 0000000..9ebc352 --- /dev/null +++ b/js/util/rangeExclusive.ts @@ -0,0 +1,15 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Returns an array of integers from A to B (exclusive), e.g. rangeExclusive( 4, 7 ) maps to [ 5, 6 ]. + * + * @author Jonathan Olson + */ + +import { rangeInclusive } from './rangeInclusive.js'; +import dot from '../dot.js'; + +export function rangeExclusive( a: number, b: number ): number[] { + return rangeInclusive( a + 1, b - 1 ); +} +dot.register( 'rangeExclusive', rangeExclusive ); \ No newline at end of file diff --git a/js/util/rangeInclusive.ts b/js/util/rangeInclusive.ts new file mode 100644 index 0000000..7251d65 --- /dev/null +++ b/js/util/rangeInclusive.ts @@ -0,0 +1,21 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Returns an array of integers from A to B (inclusive), e.g. rangeInclusive( 4, 7 ) maps to [ 4, 5, 6, 7 ]. + * + * @author Jonathan Olson + */ + +import dot from '../dot.js'; + +export function rangeInclusive( a: number, b: number ): number[] { + if ( b < a ) { + return []; + } + const result = new Array( b - a + 1 ); + for ( let i = a; i <= b; i++ ) { + result[ i - a ] = i; + } + return result; +} +dot.register( 'rangeInclusive', rangeInclusive ); \ No newline at end of file diff --git a/js/util/roundSymmetric.ts b/js/util/roundSymmetric.ts index aaa5396..fe5f102 100644 --- a/js/util/roundSymmetric.ts +++ b/js/util/roundSymmetric.ts @@ -12,6 +12,10 @@ * * @author Jonathan Olson */ -export default function roundSymmetric( value: number ): number { + +import dot from '../dot.js'; + +export function roundSymmetric( value: number ): number { return ( ( value < 0 ) ? -1 : 1 ) * Math.round( Math.abs( value ) ); // eslint-disable-line phet/bad-sim-text -} \ No newline at end of file +} +dot.register( 'roundSymmetric', roundSymmetric ); \ No newline at end of file diff --git a/js/util/toDegrees.ts b/js/util/toDegrees.ts new file mode 100644 index 0000000..90a7a82 --- /dev/null +++ b/js/util/toDegrees.ts @@ -0,0 +1,14 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Converts radians to degrees. + * + * @author Jonathan Olson + */ + +import dot from '../dot.js'; + +export function toDegrees( radians: number ): number { + return 180 * radians / Math.PI; +} +dot.register( 'toDegrees', toDegrees ); \ No newline at end of file diff --git a/js/util/toRadians.ts b/js/util/toRadians.ts new file mode 100644 index 0000000..c5be550 --- /dev/null +++ b/js/util/toRadians.ts @@ -0,0 +1,14 @@ +// Copyright 2025, University of Colorado Boulder + +/** + * Converts degrees to radians. + * + * @author Jonathan Olson + */ + +import dot from '../dot.js'; + +export function toRadians( degrees: number ): number { + return Math.PI * degrees / 180; +} +dot.register( 'toRadians', toRadians ); \ No newline at end of file