From 5c838695f53605d8f90ce0747f539e2d9f9fcd66 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 17 Jan 2025 15:02:01 -0600 Subject: [PATCH] Move scorer: Interactive Graph (#2120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * STOPSHIP some type errors still * add back duplicate declarations * add back Line duplicate * all tests passing * Revert changes to underscore's isEqual (#2125) ## Summary: [Original comment](https://github.com/Khan/perseus/pull/2113#discussion_r1919335906) I made a separate PR because I made this mistake in a couple of PRs so I thought I'd knock them out all at once. Issue: LEMS-2737 ## Test plan: Author: handeyeco Reviewers: jeremywiebe, benchristel Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ❌ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ⏹️ [cancelled] Publish npm snapshot (ubuntu-latest, 20.x), ⏹️ [cancelled] Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ⏹️ [cancelled] Check builds for changes in size (ubuntu-latest, 20.x), ⏹️ [cancelled] Cypress (ubuntu-latest, 20.x), ⏹️ [cancelled] Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⏹️ [cancelled] Publish npm snapshot (ubuntu-latest, 20.x), ⏹️ [cancelled] Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⏹️ [cancelled] Check builds for changes in size (ubuntu-latest, 20.x), ⏹️ [cancelled] Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ⏹️ [cancelled] Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x) Pull Request URL: https://github.com/Khan/perseus/pull/2125 * respond to Jeremy's feedback --- .../math => kmath/src}/angles.test.ts | 0 .../math => kmath/src}/angles.ts | 16 ++-- packages/kmath/src/coefficients.ts | 62 ++++++++++++++++ .../src/util => kmath/src}/geometry.test.ts | 0 .../src/util => kmath/src}/geometry.ts | 25 +++---- packages/kmath/src/index.ts | 8 ++ packages/kmath/src/types.ts | 3 + packages/perseus-core/src/index.ts | 2 + .../perseus-core/src/utils/deep-clone.test.ts | 25 +++++++ packages/perseus-core/src/utils/deep-clone.ts | 18 +++++ packages/perseus-core/src/utils/equality.ts | 55 ++++++++++++++ .../perseus-core/src/utils/grapher-util.ts | 8 +- .../start-coords/util.ts | 5 +- packages/perseus-score/src/index.ts | 1 + .../score-interactive-graph.test.ts | 9 +-- .../score-interactive-graph.ts | 57 +++++++------- .../src/widgets/plotter/score-plotter.ts | 3 +- .../src/widgets/plotter/validate-plotter.ts | 3 +- .../src/widgets/sorter/score-sorter.ts | 3 +- packages/perseus/src/__tests__/util.test.ts | 24 ------ .../perseus/src/components/graphie-classes.ts | 9 ++- packages/perseus/src/components/graphie.tsx | 10 +-- packages/perseus/src/index.ts | 3 - packages/perseus/src/util.ts | 74 ------------------- packages/perseus/src/util/interactive.ts | 4 +- .../perseus/src/util/is-real-json-parse.ts | 6 +- .../perseus/src/widgets/interactive-graph.tsx | 69 +++++++++-------- .../interactive-graphs/graphs/angle.tsx | 5 +- .../components/angle-indicators.test.ts | 4 +- .../graphs/components/angle-indicators.tsx | 7 +- .../graphs/components/vector.tsx | 4 +- .../interactive-graphs/graphs/quadratic.tsx | 3 +- .../interactive-graphs/graphs/sinusoid.tsx | 18 ++--- .../locked-figures/locked-line.tsx | 5 +- .../widgets/interactive-graphs/math/index.ts | 7 -- .../widgets/interactive-graphs/protractor.tsx | 11 +-- .../reducer/initialize-graph-state.ts | 4 +- .../reducer/interactive-graph-reducer.test.ts | 5 +- .../reducer/interactive-graph-reducer.ts | 51 ++++++------- .../reducer/interactive-graph-state.ts | 4 +- .../src/widgets/interactive-graphs/types.ts | 3 +- 41 files changed, 359 insertions(+), 274 deletions(-) rename packages/{perseus/src/widgets/interactive-graphs/math => kmath/src}/angles.test.ts (100%) rename packages/{perseus/src/widgets/interactive-graphs/math => kmath/src}/angles.ts (86%) create mode 100644 packages/kmath/src/coefficients.ts rename packages/{perseus/src/util => kmath/src}/geometry.test.ts (100%) rename packages/{perseus/src/util => kmath/src}/geometry.ts (96%) create mode 100644 packages/kmath/src/types.ts create mode 100644 packages/perseus-core/src/utils/deep-clone.test.ts create mode 100644 packages/perseus-core/src/utils/deep-clone.ts create mode 100644 packages/perseus-core/src/utils/equality.ts rename packages/{perseus/src/widgets/interactive-graphs => perseus-score/src/widgets/interactive-graph}/score-interactive-graph.test.ts (97%) rename packages/{perseus/src/widgets/interactive-graphs => perseus-score/src/widgets/interactive-graph}/score-interactive-graph.ts (88%) diff --git a/packages/perseus/src/widgets/interactive-graphs/math/angles.test.ts b/packages/kmath/src/angles.test.ts similarity index 100% rename from packages/perseus/src/widgets/interactive-graphs/math/angles.test.ts rename to packages/kmath/src/angles.test.ts diff --git a/packages/perseus/src/widgets/interactive-graphs/math/angles.ts b/packages/kmath/src/angles.ts similarity index 86% rename from packages/perseus/src/widgets/interactive-graphs/math/angles.ts rename to packages/kmath/src/angles.ts index c2d20bd4e8..ec4b55b294 100644 --- a/packages/perseus/src/widgets/interactive-graphs/math/angles.ts +++ b/packages/kmath/src/angles.ts @@ -1,9 +1,8 @@ -import {clockwise} from "../../../util/geometry"; +// This file contains helper functions for working with angles. -import type {Coord} from "@khanacademy/perseus"; -import type {vec} from "mafs"; +import {clockwise} from "./geometry"; -// This file contains helper functions for working with angles. +import type {Coord} from "@khanacademy/perseus-core"; export function convertDegreesToRadians(degrees: number): number { return (degrees / 180) * Math.PI; @@ -11,12 +10,12 @@ export function convertDegreesToRadians(degrees: number): number { // Returns a value between -180 and 180, inclusive. The angle is measured // between the positive x-axis and the given vector. -export function calculateAngleInDegrees([x, y]: vec.Vector2): number { +export function calculateAngleInDegrees([x, y]: Coord): number { return (Math.atan2(y, x) * 180) / Math.PI; } // Converts polar coordinates to cartesian. The th(eta) parameter is in degrees. -export function polar(r: number | vec.Vector2, th: number): vec.Vector2 { +export function polar(r: number | Coord, th: number): Coord { if (typeof r === "number") { r = [r, r]; } @@ -26,10 +25,7 @@ export function polar(r: number | vec.Vector2, th: number): vec.Vector2 { // This function calculates the angle between two points and an optional vertex. // If the vertex is not provided, the angle is measured between the two points. // This does not account for reflex angles or clockwise position. -export const getAngleFromVertex = ( - point: vec.Vector2, - vertex: vec.Vector2, -): number => { +export const getAngleFromVertex = (point: Coord, vertex: Coord): number => { const x = point[0] - vertex[0]; const y = point[1] - vertex[1]; if (!x && !y) { diff --git a/packages/kmath/src/coefficients.ts b/packages/kmath/src/coefficients.ts new file mode 100644 index 0000000000..c33c9ba9b2 --- /dev/null +++ b/packages/kmath/src/coefficients.ts @@ -0,0 +1,62 @@ +import type {SineCoefficient} from "./geometry"; +import type {Coord} from "@khanacademy/perseus-core"; + +export type NamedSineCoefficient = { + amplitude: number; + angularFrequency: number; + phase: number; + verticalOffset: number; +}; + +// TODO: there's another, very similar getSinusoidCoefficients function +// they should probably be merged +export function getSinusoidCoefficients( + coords: ReadonlyArray, +): SineCoefficient { + // It's assumed that p1 is the root and p2 is the first peak + const p1 = coords[0]; + const p2 = coords[1]; + + // Resulting coefficients are canonical for this sine curve + const amplitude = p2[1] - p1[1]; + const angularFrequency = Math.PI / (2 * (p2[0] - p1[0])); + const phase = p1[0] * angularFrequency; + const verticalOffset = p1[1]; + + return [amplitude, angularFrequency, phase, verticalOffset]; +} + +export type QuadraticCoefficient = [number, number, number]; + +// TODO: there's another, very similar getQuadraticCoefficients function +// they should probably be merged +export function getQuadraticCoefficients( + coords: ReadonlyArray, +): QuadraticCoefficient { + const p1 = coords[0]; + const p2 = coords[1]; + const p3 = coords[2]; + + const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]); + if (denom === 0) { + // Many of the callers assume that the return value is always defined. + // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'. + return; + } + const a = + (p3[0] * (p2[1] - p1[1]) + + p2[0] * (p1[1] - p3[1]) + + p1[0] * (p3[1] - p2[1])) / + denom; + const b = + (p3[0] * p3[0] * (p1[1] - p2[1]) + + p2[0] * p2[0] * (p3[1] - p1[1]) + + p1[0] * p1[0] * (p2[1] - p3[1])) / + denom; + const c = + (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / + denom; + return [a, b, c]; +} diff --git a/packages/perseus/src/util/geometry.test.ts b/packages/kmath/src/geometry.test.ts similarity index 100% rename from packages/perseus/src/util/geometry.test.ts rename to packages/kmath/src/geometry.test.ts diff --git a/packages/perseus/src/util/geometry.ts b/packages/kmath/src/geometry.ts similarity index 96% rename from packages/perseus/src/util/geometry.ts rename to packages/kmath/src/geometry.ts index ab7eacc0fe..3ded900a65 100644 --- a/packages/perseus/src/util/geometry.ts +++ b/packages/kmath/src/geometry.ts @@ -2,14 +2,16 @@ * A collection of geomtry-related utility functions */ -import {number as knumber, point as kpoint, sum} from "@khanacademy/kmath"; +import { + approximateDeepEqual, + approximateEqual, + type Coord, +} from "@khanacademy/perseus-core"; import _ from "underscore"; -import Util from "../util"; - -import type {Coord, Line} from "../interactive2/types"; +import {number as knumber, point as kpoint, sum} from "@khanacademy/kmath"; -const {eq, deepEq} = Util; +type Line = [Coord, Coord]; // This should really be a readonly tuple of [number, number] export type Range = [number, number]; @@ -21,12 +23,9 @@ export type SineCoefficient = [ number, // verticalOffset ]; -// a, b, c -export type QuadraticCoefficient = [number, number, number]; - // Given a number, return whether it is positive (1), negative (-1), or zero (0) export function sign(val: number): 0 | 1 | -1 { - if (eq(val, 0)) { + if (approximateEqual(val, 0)) { return 0; } return val > 0 ? 1 : -1; @@ -39,7 +38,7 @@ export function ccw(a: Coord, b: Coord, c: Coord): number { } export function collinear(a: Coord, b: Coord, c: Coord): boolean { - return eq(ccw(a, b, c), 0); + return approximateEqual(ccw(a, b, c), 0); } // Given rect bounding points A and B, whether point C is inside the rect @@ -229,7 +228,7 @@ export function similar( // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'. sides = rotate(sides, i); - if (deepEq(angles1, angles)) { + if (approximateDeepEqual(angles1, angles)) { const sidePairs = _.zip(sides1, sides); const factors = _.map(sidePairs, function (pair) { @@ -237,7 +236,7 @@ export function similar( }); const same = _.all(factors, function (factor) { - return eq(factors[0], factor); + return approximateEqual(factors[0], factor); }); const congruentEnough = _.all(sidePairs, function (pair) { @@ -304,7 +303,7 @@ export function rotate( } export function getLineEquation(first: Coord, second: Coord): string { - if (eq(first[0], second[0])) { + if (approximateEqual(first[0], second[0])) { return "x = " + first[0].toFixed(3); } const m = (second[1] - first[1]) / (second[0] - first[0]); diff --git a/packages/kmath/src/index.ts b/packages/kmath/src/index.ts index cbeb7d8403..284f4eb02e 100644 --- a/packages/kmath/src/index.ts +++ b/packages/kmath/src/index.ts @@ -5,5 +5,13 @@ export * as vector from "./vector"; export * as point from "./point"; export * as line from "./line"; export * as ray from "./ray"; +export * as angles from "./angles"; +export * as geometry from "./geometry"; +export * as coefficients from "./coefficients"; export {default as KhanMath, sum} from "./math"; + +export type {Range, SineCoefficient} from "./geometry"; +export type {NamedSineCoefficient, QuadraticCoefficient} from "./coefficients"; + +export type * from "./types"; diff --git a/packages/kmath/src/types.ts b/packages/kmath/src/types.ts new file mode 100644 index 0000000000..7c24a6dd7b --- /dev/null +++ b/packages/kmath/src/types.ts @@ -0,0 +1,3 @@ +import type {Coord} from "@khanacademy/perseus-core"; + +export type QuadraticCoords = [Coord, Coord, Coord]; diff --git a/packages/perseus-core/src/index.ts b/packages/perseus-core/src/index.ts index 947f093bc9..df95a6a770 100644 --- a/packages/perseus-core/src/index.ts +++ b/packages/perseus-core/src/index.ts @@ -15,6 +15,8 @@ export type {Coords} from "./utils/grapher-types"; export {addLibraryVersionToPerseusDebug} from "./utils/add-library-version-to-perseus-debug"; export {default as getMatrixSize} from "./utils/get-matrix-size"; export {default as getDecimalSeparator} from "./utils/get-decimal-separator"; +export {approximateEqual, approximateDeepEqual} from "./utils/equality"; +export {default as deepClone} from "./utils/deep-clone"; export * as GrapherUtil from "./utils/grapher-util"; export {libVersion} from "./version"; diff --git a/packages/perseus-core/src/utils/deep-clone.test.ts b/packages/perseus-core/src/utils/deep-clone.test.ts new file mode 100644 index 0000000000..682349af3f --- /dev/null +++ b/packages/perseus-core/src/utils/deep-clone.test.ts @@ -0,0 +1,25 @@ +import deepClone from "./deep-clone"; + +describe("deepClone", () => { + it("does nothing to a primitive", () => { + expect(deepClone(3)).toBe(3); + }); + + it("copies an array", () => { + const input = [1, 2, 3]; + + const result = deepClone(input); + + expect(result).toEqual(input); + expect(result).not.toBe(input); + }); + + it("recursively clones array elements", () => { + const input = [[1]]; + + const result = deepClone(input); + + expect(result).toEqual(input); + expect(result[0]).not.toBe(input[0]); + }); +}); diff --git a/packages/perseus-core/src/utils/deep-clone.ts b/packages/perseus-core/src/utils/deep-clone.ts new file mode 100644 index 0000000000..941a89e95a --- /dev/null +++ b/packages/perseus-core/src/utils/deep-clone.ts @@ -0,0 +1,18 @@ +// TODO(benchristel): in the future, we may want to make deepClone work for +// Record as well. Currently, it only does arrays. +type Cloneable = + | null + | undefined + | boolean + | string + | number + | Cloneable[] + | readonly Cloneable[]; +function deepClone(obj: T): T { + if (Array.isArray(obj)) { + return obj.map(deepClone) as T; + } + return obj; +} + +export default deepClone; diff --git a/packages/perseus-core/src/utils/equality.ts b/packages/perseus-core/src/utils/equality.ts new file mode 100644 index 0000000000..9d2bf59e46 --- /dev/null +++ b/packages/perseus-core/src/utils/equality.ts @@ -0,0 +1,55 @@ +import _ from "underscore"; + +/** + * APPROXIMATE equality on numbers and primitives. + */ +export function approximateEqual(x: T, y: T): boolean { + if (typeof x === "number" && typeof y === "number") { + return Math.abs(x - y) < 1e-9; + } + return x === y; +} + +/** + * Deep APPROXIMATE equality on primitives, numbers, arrays, and objects. + * Recursive. + */ +export function approximateDeepEqual(x: T, y: T): boolean { + if (Array.isArray(x) && Array.isArray(y)) { + if (x.length !== y.length) { + return false; + } + for (let i = 0; i < x.length; i++) { + if (!approximateDeepEqual(x[i], y[i])) { + return false; + } + } + return true; + } + if (Array.isArray(x) || Array.isArray(y)) { + return false; + } + if (typeof x === "function" && typeof y === "function") { + return approximateEqual(x, y); + } + if (typeof x === "function" || typeof y === "function") { + return false; + } + if (typeof x === "object" && typeof y === "object" && !!x && !!y) { + return ( + x === y || + (_.all(x, function (v, k) { + // @ts-expect-error - TS2536 - Type 'CollectionKey' cannot be used to index type 'T'. + return approximateDeepEqual(y[k], v); + }) && + _.all(y, function (v, k) { + // @ts-expect-error - TS2536 - Type 'CollectionKey' cannot be used to index type 'T'. + return approximateDeepEqual(x[k], v); + })) + ); + } + if ((typeof x === "object" && !!x) || (typeof y === "object" && !!y)) { + return false; + } + return approximateEqual(x, y); +} diff --git a/packages/perseus-core/src/utils/grapher-util.ts b/packages/perseus-core/src/utils/grapher-util.ts index bec1ffc1bc..072b9d8f58 100644 --- a/packages/perseus-core/src/utils/grapher-util.ts +++ b/packages/perseus-core/src/utils/grapher-util.ts @@ -1,5 +1,7 @@ import _ from "underscore"; +import {approximateDeepEqual} from "./equality"; + import type { LinearType, QuadraticType, @@ -95,7 +97,7 @@ const PlotDefaults = { coeffs1: ReadonlyArray, coeffs2: ReadonlyArray, ): boolean { - return _.isEqual(coeffs1, coeffs2); + return approximateDeepEqual(coeffs1, coeffs2); }, movable: MOVABLES.PLOT, getPropsForCoeffs: function (coeffs: ReadonlyArray): {fn: any} { @@ -269,7 +271,7 @@ const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { coeffs1: ReadonlyArray, coeffs2: ReadonlyArray, ) { - return _.isEqual( + return approximateDeepEqual( canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2), ); @@ -326,7 +328,7 @@ const Tangent: TangentType = _.extend({}, PlotDefaults, { coeffs1: ReadonlyArray, coeffs2: ReadonlyArray, ) { - return _.isEqual( + return approximateDeepEqual( canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2), ); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/start-coords/util.ts b/packages/perseus-editor/src/widgets/interactive-graph-editor/start-coords/util.ts index 8ce80a5df8..99dc26d410 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/start-coords/util.ts +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/start-coords/util.ts @@ -1,4 +1,4 @@ -import {vector as kvector} from "@khanacademy/kmath"; +import {angles, vector as kvector} from "@khanacademy/kmath"; import { getAngleCoords, getCircleCoords, @@ -9,13 +9,14 @@ import { getQuadraticCoords, getSegmentCoords, getSinusoidCoords, - getClockwiseAngle, } from "@khanacademy/perseus"; import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core"; import type {StartCoords} from "./types"; import type {Range, PerseusGraphType, Coord} from "@khanacademy/perseus-core"; +const {getClockwiseAngle} = angles; + export function getStartCoords(graph: PerseusGraphType): StartCoords { if ("startCoords" in graph) { return graph.startCoords; diff --git a/packages/perseus-score/src/index.ts b/packages/perseus-score/src/index.ts index c592a1739a..28a82ff3c2 100644 --- a/packages/perseus-score/src/index.ts +++ b/packages/perseus-score/src/index.ts @@ -8,6 +8,7 @@ export {default as scoreDropdown} from "./widgets/dropdown/score-dropdown"; export {default as scoreExpression} from "./widgets/expression/score-expression"; export {default as scoreGrapher} from "./widgets/grapher/score-grapher"; export {default as scoreIframe} from "./widgets/iframe/score-iframe"; +export {default as scoreInteractiveGraph} from "./widgets/interactive-graph/score-interactive-graph"; export { default as scoreLabelImage, labelImageScoreMarker, diff --git a/packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.test.ts b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts similarity index 97% rename from packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.test.ts rename to packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts index e6e09e8e78..a1c530ff08 100644 --- a/packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.test.ts +++ b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.test.ts @@ -1,11 +1,10 @@ import invariant from "tiny-invariant"; - -import {clone} from "../../../../../testing/object-utils"; +import _ from "underscore"; import scoreInteractiveGraph from "./score-interactive-graph"; +import type {PerseusInteractiveGraphRubric} from "../../validation.types"; import type {PerseusGraphType} from "@khanacademy/perseus-core"; -import type {PerseusInteractiveGraphRubric} from "@khanacademy/perseus-score"; describe("InteractiveGraph scoring on a segment question", () => { it("marks the answer invalid if guess.coords is missing", () => { @@ -326,7 +325,7 @@ describe("InteractiveGraph scoring on a point question", () => { }, }; - const guessClone = clone(guess); + const guessClone = _.clone(guess); scoreInteractiveGraph(guess, rubric); @@ -352,7 +351,7 @@ describe("InteractiveGraph scoring on a point question", () => { }, }; - const rubricClone = clone(rubric); + const rubricClone = _.clone(rubric); scoreInteractiveGraph(guess, rubric); diff --git a/packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.ts b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts similarity index 88% rename from packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.ts rename to packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts index 8e542b0249..d78804c196 100644 --- a/packages/perseus/src/widgets/interactive-graphs/score-interactive-graph.ts +++ b/packages/perseus-score/src/widgets/interactive-graph/score-interactive-graph.ts @@ -1,18 +1,15 @@ -import {number as knumber} from "@khanacademy/kmath"; -import _ from "underscore"; - -import Util from "../../util"; import { - canonicalSineCoefficients, - collinear, - similar, -} from "../../util/geometry"; + number as knumber, + geometry, + angles, + coefficients, +} from "@khanacademy/kmath"; import { - getQuadraticCoefficients, - getSinusoidCoefficients, -} from "../interactive-graph"; - -import {getClockwiseAngle} from "./math/angles"; + approximateDeepEqual, + approximateEqual, + deepClone, +} from "@khanacademy/perseus-core"; +import _ from "underscore"; import type { PerseusScore, @@ -20,8 +17,9 @@ import type { PerseusInteractiveGraphUserInput, } from "@khanacademy/perseus-score"; -const eq = Util.eq; -const deepEq = Util.deepEq; +const {collinear, canonicalSineCoefficients, similar} = geometry; +const {getClockwiseAngle} = angles; +const {getSinusoidCoefficients, getQuadraticCoefficients} = coefficients; function scoreInteractiveGraph( userInput: PerseusInteractiveGraphUserInput, @@ -104,7 +102,7 @@ function scoreInteractiveGraph( const correctCoeffs = getQuadraticCoefficients( rubric.correct.coords, ); - if (deepEq(guessCoeffs, correctCoeffs)) { + if (approximateDeepEqual(guessCoeffs, correctCoeffs)) { return { type: "points", earned: 1, @@ -126,7 +124,12 @@ function scoreInteractiveGraph( const canonicalCorrectCoeffs = canonicalSineCoefficients(correctCoeffs); // If the canonical coefficients match, it's correct. - if (deepEq(canonicalGuessCoeffs, canonicalCorrectCoeffs)) { + if ( + approximateDeepEqual( + canonicalGuessCoeffs, + canonicalCorrectCoeffs, + ) + ) { return { type: "points", earned: 1, @@ -139,8 +142,8 @@ function scoreInteractiveGraph( rubric.correct.type === "circle" ) { if ( - deepEq(userInput.center, rubric.correct.center) && - eq(userInput.radius, rubric.correct.radius) + approximateDeepEqual(userInput.center, rubric.correct.center) && + approximateEqual(userInput.radius, rubric.correct.radius) ) { return { type: "points", @@ -167,7 +170,7 @@ function scoreInteractiveGraph( guess?.sort(); // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'. correct.sort(); - if (deepEq(guess, correct)) { + if (approximateDeepEqual(guess, correct)) { return { type: "points", earned: 1, @@ -194,7 +197,7 @@ function scoreInteractiveGraph( /* exact */ guess.sort(); correct.sort(); - match = deepEq(guess, correct); + match = approximateDeepEqual(guess, correct); } if (match) { @@ -210,11 +213,11 @@ function scoreInteractiveGraph( rubric.correct.type === "segment" && userInput.coords != null ) { - let guess = Util.deepClone(userInput.coords); - let correct = Util.deepClone(rubric.correct.coords); + let guess = deepClone(userInput.coords); + let correct = deepClone(rubric.correct.coords); guess = _.invoke(guess, "sort").sort(); correct = _.invoke(correct, "sort").sort(); - if (deepEq(guess, correct)) { + if (approximateDeepEqual(guess, correct)) { return { type: "points", earned: 1, @@ -230,7 +233,7 @@ function scoreInteractiveGraph( const guess = userInput.coords; const correct = rubric.correct.coords; if ( - deepEq(guess[0], correct[0]) && + approximateDeepEqual(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1]) ) { return { @@ -258,11 +261,11 @@ function scoreInteractiveGraph( return angle; }); // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter. - match = eq(...angles); + match = approximateEqual(...angles); } else { /* exact */ match = // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. - deepEq(guess[1], correct[1]) && + approximateDeepEqual(guess[1], correct[1]) && // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. collinear(correct[1], correct[0], guess[0]) && // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. diff --git a/packages/perseus-score/src/widgets/plotter/score-plotter.ts b/packages/perseus-score/src/widgets/plotter/score-plotter.ts index 102667e988..fb311bf1d3 100644 --- a/packages/perseus-score/src/widgets/plotter/score-plotter.ts +++ b/packages/perseus-score/src/widgets/plotter/score-plotter.ts @@ -1,3 +1,4 @@ +import {approximateDeepEqual} from "@khanacademy/perseus-core"; import _ from "underscore"; import validatePlotter from "./validate-plotter"; @@ -18,7 +19,7 @@ function scorePlotter( } return { type: "points", - earned: _.isEqual(userInput, scoringData.correct) ? 1 : 0, + earned: approximateDeepEqual(userInput, scoringData.correct) ? 1 : 0, total: 1, message: null, }; diff --git a/packages/perseus-score/src/widgets/plotter/validate-plotter.ts b/packages/perseus-score/src/widgets/plotter/validate-plotter.ts index 0f3ac00a22..3c0c89f0c1 100644 --- a/packages/perseus-score/src/widgets/plotter/validate-plotter.ts +++ b/packages/perseus-score/src/widgets/plotter/validate-plotter.ts @@ -1,3 +1,4 @@ +import {approximateDeepEqual} from "@khanacademy/perseus-core"; import _ from "underscore"; import type { @@ -16,7 +17,7 @@ function validatePlotter( userInput: PerseusPlotterUserInput, validationData: PerseusPlotterValidationData, ): ValidationResult { - if (_.isEqual(userInput, validationData.starting)) { + if (approximateDeepEqual(userInput, validationData.starting)) { return { type: "invalid", message: null, diff --git a/packages/perseus-score/src/widgets/sorter/score-sorter.ts b/packages/perseus-score/src/widgets/sorter/score-sorter.ts index f82cbba472..ce7570b8f9 100644 --- a/packages/perseus-score/src/widgets/sorter/score-sorter.ts +++ b/packages/perseus-score/src/widgets/sorter/score-sorter.ts @@ -1,3 +1,4 @@ +import {approximateDeepEqual} from "@khanacademy/perseus-core"; import _ from "underscore"; import validateSorter from "./validate-sorter"; @@ -17,7 +18,7 @@ function scoreSorter( return validationError; } - const correct = _.isEqual(userInput.options, rubric.correct); + const correct = approximateDeepEqual(userInput.options, rubric.correct); return { type: "points", earned: correct ? 1 : 0, diff --git a/packages/perseus/src/__tests__/util.test.ts b/packages/perseus/src/__tests__/util.test.ts index e3d31b49f0..88e83f0255 100644 --- a/packages/perseus/src/__tests__/util.test.ts +++ b/packages/perseus/src/__tests__/util.test.ts @@ -38,27 +38,3 @@ describe("#constrainedTickStepsFromTickSteps", () => { expect(result).toEqual([5, 5]); }); }); - -describe("deepClone", () => { - it("does nothing to a primitive", () => { - expect(Util.deepClone(3)).toBe(3); - }); - - it("copies an array", () => { - const input = [1, 2, 3]; - - const result = Util.deepClone(input); - - expect(result).toEqual(input); - expect(result).not.toBe(input); - }); - - it("recursively clones array elements", () => { - const input = [[1]]; - - const result = Util.deepClone(input); - - expect(result).toEqual(input); - expect(result[0]).not.toBe(input[0]); - }); -}); diff --git a/packages/perseus/src/components/graphie-classes.ts b/packages/perseus/src/components/graphie-classes.ts index 350237f25d..689189679c 100644 --- a/packages/perseus/src/components/graphie-classes.ts +++ b/packages/perseus/src/components/graphie-classes.ts @@ -1,12 +1,15 @@ /* eslint-disable @babel/no-invalid-this */ -import {Errors, PerseusError} from "@khanacademy/perseus-core"; +import { + approximateDeepEqual, + Errors, + PerseusError, +} from "@khanacademy/perseus-core"; import _ from "underscore"; import Util from "../util"; const nestedMap = Util.nestedMap; -const deepEq = Util.deepEq; /** * A base class for all Graphie Movables @@ -116,7 +119,7 @@ const createSimpleClass = function (addFunction: any): any { }, modify: function (graphie) { - if (!deepEq(this.props, this._prevProps)) { + if (!approximateDeepEqual(this.props, this._prevProps)) { this.remove(); this.add(graphie); this._prevProps = this.props; diff --git a/packages/perseus/src/components/graphie.tsx b/packages/perseus/src/components/graphie.tsx index cbc30b4bfc..dc207229d2 100644 --- a/packages/perseus/src/components/graphie.tsx +++ b/packages/perseus/src/components/graphie.tsx @@ -1,4 +1,4 @@ -import {Errors} from "@khanacademy/perseus-core"; +import {approximateDeepEqual, Errors} from "@khanacademy/perseus-core"; import $ from "jquery"; import * as React from "react"; import _ from "underscore"; @@ -18,7 +18,7 @@ import type {Range, Size} from "@khanacademy/perseus-core"; const GraphieMovable = GraphieClasses.GraphieMovable; const createGraphie = GraphUtils.createGraphie; -const {deepEq, nestedMap} = Util; +const {nestedMap} = Util; const {assert} = InteractiveUtil; type Props = { @@ -114,9 +114,9 @@ class Graphie extends React.Component { ); } if ( - !deepEq(this.props.options, prevProps.options) || - !deepEq(this.props.box, prevProps.box) || - !deepEq(this.props.range, prevProps.range) + !approximateDeepEqual(this.props.options, prevProps.options) || + !approximateDeepEqual(this.props.box, prevProps.box) || + !approximateDeepEqual(this.props.range, prevProps.range) ) { this._setupGraphie(); } diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 0c169706a1..972bc3609f 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -145,9 +145,6 @@ export { getQuadraticCoords, getAngleCoords, } from "./widgets/interactive-graphs/reducer/initialize-graph-state"; -// This export is to support necessary functionality in the perseus-editor package. -// It should be removed if widgets and editors become colocated. -export {getClockwiseAngle} from "./widgets/interactive-graphs/math"; export {makeSafeUrl} from "./widgets/phet-simulation"; diff --git a/packages/perseus/src/util.ts b/packages/perseus/src/util.ts index d1f823a169..9fcd365604 100644 --- a/packages/perseus/src/util.ts +++ b/packages/perseus/src/util.ts @@ -408,60 +408,6 @@ function constrainedTickStepsFromTickSteps( ]; } -/** - * Approximate equality on numbers and primitives. - */ -function eq(x: T, y: T): boolean { - if (typeof x === "number" && typeof y === "number") { - return Math.abs(x - y) < 1e-9; - } - return x === y; -} - -/** - * Deep approximate equality on primitives, numbers, arrays, and objects. - * Recursive. - */ -function deepEq(x: T, y: T): boolean { - if (Array.isArray(x) && Array.isArray(y)) { - if (x.length !== y.length) { - return false; - } - for (let i = 0; i < x.length; i++) { - if (!deepEq(x[i], y[i])) { - return false; - } - } - return true; - } - if (Array.isArray(x) || Array.isArray(y)) { - return false; - } - if (typeof x === "function" && typeof y === "function") { - return eq(x, y); - } - if (typeof x === "function" || typeof y === "function") { - return false; - } - if (typeof x === "object" && typeof y === "object" && !!x && !!y) { - return ( - x === y || - (_.all(x, function (v, k) { - // @ts-expect-error - TS2536 - Type 'CollectionKey' cannot be used to index type 'T'. - return deepEq(y[k], v); - }) && - _.all(y, function (v, k) { - // @ts-expect-error - TS2536 - Type 'CollectionKey' cannot be used to index type 'T'. - return deepEq(x[k], v); - })) - ); - } - if ((typeof x === "object" && !!x) || (typeof y === "object" && !!y)) { - return false; - } - return eq(x, y); -} - /** * Query String Parser * @@ -704,23 +650,6 @@ const unescapeMathMode: (label: string) => string = (label) => const random: RNG = seededRNG(new Date().getTime() & 0xffffffff); -// TODO(benchristel): in the future, we may want to make deepClone work for -// Record as well. Currently, it only does arrays. -type Cloneable = - | null - | undefined - | boolean - | string - | number - | Cloneable[] - | readonly Cloneable[]; -function deepClone(obj: T): T { - if (Array.isArray(obj)) { - return obj.map(deepClone) as T; - } - return obj; -} - const Util = { inputPathsEqual, nestedMap, @@ -741,8 +670,6 @@ const Util = { gridStepFromTickStep, tickStepFromNumTicks, constrainedTickStepsFromTickSteps, - eq, - deepEq, parseQueryString, updateQueryString, strongEncodeURIComponent, @@ -761,7 +688,6 @@ const Util = { textarea, unescapeMathMode, random, - deepClone, } as const; export default Util; diff --git a/packages/perseus/src/util/interactive.ts b/packages/perseus/src/util/interactive.ts index 9c35fbf3fc..7724c82720 100644 --- a/packages/perseus/src/util/interactive.ts +++ b/packages/perseus/src/util/interactive.ts @@ -13,6 +13,7 @@ import { point as kpoint, line as kline, KhanMath, + geometry, } from "@khanacademy/kmath"; import {Errors, PerseusError} from "@khanacademy/perseus-core"; import $ from "jquery"; @@ -29,13 +30,14 @@ import WrappedEllipse from "../interactive2/wrapped-ellipse"; import WrappedLine from "../interactive2/wrapped-line"; import KhanColors from "./colors"; -import {clockwise, reverseVector} from "./geometry"; import GraphUtils, {polar} from "./graphie"; import type {Coord} from "../interactive2/types"; export type MouseHandler = (position: Coord) => void; +const {clockwise, reverseVector} = geometry; + function scaledDistanceFromAngle(angle: number) { const a = 3.51470560176242 * 20; const b = 0.5687298702748785 * 20; diff --git a/packages/perseus/src/util/is-real-json-parse.ts b/packages/perseus/src/util/is-real-json-parse.ts index 548f17518a..073a425d66 100644 --- a/packages/perseus/src/util/is-real-json-parse.ts +++ b/packages/perseus/src/util/is-real-json-parse.ts @@ -1,6 +1,4 @@ -import Util from "../util"; - -const deepEq = Util.deepEq; +import {approximateDeepEqual} from "@khanacademy/perseus-core"; export function isRealJSONParse(jsonParse: typeof JSON.parse): boolean { const randomPhrase = buildRandomPhrase(); @@ -55,7 +53,7 @@ export function isRealJSONParse(jsonParse: typeof JSON.parse): boolean { const parsedTestJSON = jsonParse(testJSON); const parsedTestItemData: string = parsedTestJSON.data.assessmentItem.item.itemData; - return deepEq(parsedTestItemData, testingObject); + return approximateDeepEqual(parsedTestItemData, testingObject); } function buildRandomString(capitalize: boolean = false) { diff --git a/packages/perseus/src/widgets/interactive-graph.tsx b/packages/perseus/src/widgets/interactive-graph.tsx index 21d4240078..c3b3433ff1 100644 --- a/packages/perseus/src/widgets/interactive-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graph.tsx @@ -1,6 +1,20 @@ /* eslint-disable @babel/no-invalid-this, react/no-unsafe, react/sort-comp */ -import {number as knumber, point as kpoint} from "@khanacademy/kmath"; -import {Errors, PerseusError} from "@khanacademy/perseus-core"; +import { + angles, + geometry, + number as knumber, + point as kpoint, +} from "@khanacademy/kmath"; +import { + approximateEqual, + Errors, + PerseusError, +} from "@khanacademy/perseus-core"; +import { + scoreInteractiveGraph, + type PerseusInteractiveGraphRubric, + type PerseusInteractiveGraphUserInput, +} from "@khanacademy/perseus-score"; import $ from "jquery"; import debounce from "lodash.debounce"; import * as React from "react"; @@ -12,39 +26,24 @@ import Interactive2 from "../interactive2"; import WrappedLine from "../interactive2/wrapped-line"; import Util from "../util"; import KhanColors from "../util/colors"; -import { - angleMeasures, - ccw, - collinear, - getLineEquation, - getLineIntersection, - intersects, - lawOfCosines, - magnitude, - rotate, - sign, - vector, -} from "../util/geometry"; import GraphUtils from "../util/graph-utils"; import {polar} from "../util/graphie"; import {getInteractiveBoxFromSizeClass} from "../util/sizing-utils"; import {getPromptJSON} from "../widget-ai-utils/interactive-graph/interactive-graph-ai-utils"; import {StatefulMafsGraph} from "./interactive-graphs"; -import {getClockwiseAngle} from "./interactive-graphs/math"; -import scoreInteractiveGraph from "./interactive-graphs/score-interactive-graph"; import type {StatefulMafsGraphType} from "./interactive-graphs/stateful-mafs-graph"; import type {QuadraticGraphState} from "./interactive-graphs/types"; import type {Coord} from "../interactive2/types"; import type {ChangeHandler, WidgetExports, WidgetProps} from "../types"; +import type {InteractiveGraphPromptJSON} from "../widget-ai-utils/interactive-graph/interactive-graph-ai-utils"; +import type {UnsupportedWidgetPromptJSON} from "../widget-ai-utils/unsupported-widget"; import type { QuadraticCoefficient, - Range, SineCoefficient, -} from "../util/geometry"; -import type {InteractiveGraphPromptJSON} from "../widget-ai-utils/interactive-graph/interactive-graph-ai-utils"; -import type {UnsupportedWidgetPromptJSON} from "../widget-ai-utils/unsupported-widget"; + Range, +} from "@khanacademy/kmath"; import type { PerseusGraphType, PerseusGraphTypeAngle, @@ -57,12 +56,24 @@ import type { PerseusImageBackground, MarkingsType, } from "@khanacademy/perseus-core"; -import type { - PerseusInteractiveGraphRubric, - PerseusInteractiveGraphUserInput, -} from "@khanacademy/perseus-score"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; +const {getClockwiseAngle} = angles; + +const { + angleMeasures, + ccw, + collinear, + getLineEquation, + getLineIntersection, + intersects, + lawOfCosines, + magnitude, + rotate, + sign, + vector, +} = geometry; + const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png"; @@ -70,8 +81,6 @@ const defaultBackgroundImage = { url: null, }; -const eq = Util.eq; - const UNLIMITED = "unlimited" as const; // Sample background image: @@ -84,7 +93,7 @@ function defaultVal(actual: T | null | undefined, defaultValue: T): T { // Less than or approximately equal function leq(a: any, b) { - return a < b || eq(a, b); + return a < b || approximateEqual(a, b); } function capitalize(str) { @@ -2291,12 +2300,12 @@ class InteractiveGraph extends React.Component { static getLinearEquationString(props: Props): string { const coords = InteractiveGraph.getLineCoords(props.graph, props); - if (eq(coords[0][0], coords[1][0])) { + if (approximateEqual(coords[0][0], coords[1][0])) { return "x = " + coords[0][0].toFixed(3); } const m = (coords[1][1] - coords[0][1]) / (coords[1][0] - coords[0][0]); const b = coords[0][1] - m * coords[0][0]; - if (eq(m, 0)) { + if (approximateEqual(m, 0)) { return "y = " + b.toFixed(3); } return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx index 9960185e23..48d01c8026 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/angle.tsx @@ -1,8 +1,9 @@ +import {angles} from "@khanacademy/kmath"; import {vec} from "mafs"; import * as React from "react"; import {usePerseusI18n} from "../../../components/i18n-context"; -import {X, Y, calculateAngleInDegrees, getClockwiseAngle, polar} from "../math"; +import {X, Y} from "../math"; import {findIntersectionOfRays} from "../math/geometry"; import {actions} from "../reducer/interactive-graph-action"; import useGraphConfig from "../reducer/use-graph-config"; @@ -25,6 +26,8 @@ import type { } from "../types"; import type {CollinearTuple} from "@khanacademy/perseus-core"; +const {calculateAngleInDegrees, getClockwiseAngle, polar} = angles; + type AngleGraphProps = MafsGraphProps; export function renderAngleGraph( diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.test.ts b/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.test.ts index e64854cf5e..dc730e1444 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.test.ts @@ -1,4 +1,4 @@ -import {getClockwiseAngle} from "../../math"; +import {angles} from "@khanacademy/kmath"; import {shouldDrawArcOutside} from "./angle-indicators"; @@ -6,6 +6,8 @@ import type {Coord} from "@khanacademy/perseus"; import type {CollinearTuple} from "@khanacademy/perseus-core"; import type {vec, Interval} from "mafs"; +const {getClockwiseAngle} = angles; + describe("shouldDrawArcOutside", () => { const range = [ [-1, 6], diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.tsx index 1d958c4917..6df6d4f865 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle-indicators.tsx @@ -1,9 +1,9 @@ +import {angles, geometry} from "@khanacademy/kmath"; import {color} from "@khanacademy/wonder-blocks-tokens"; import {vec} from "mafs"; import * as React from "react"; -import {clockwise} from "../../../../util/geometry"; -import {getAngleFromVertex, segmentsIntersect} from "../../math"; +import {segmentsIntersect} from "../../math"; import {getIntersectionOfRayWithBox as getRangeIntersectionVertex} from "../utils"; import {MafsCssTransformWrapper} from "./css-transform-wrapper"; @@ -12,6 +12,9 @@ import {TextLabel} from "./text-label"; import type {CollinearTuple} from "@khanacademy/perseus-core"; import type {Interval} from "mafs"; +const {clockwise} = geometry; +const {getAngleFromVertex} = angles; + interface PolygonAngleProps { centerPoint: vec.Vector2; endPoints: [vec.Vector2, vec.Vector2]; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/components/vector.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/components/vector.tsx index 637b5651fc..7c223434fe 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/components/vector.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/components/vector.tsx @@ -1,12 +1,14 @@ +import {angles} from "@khanacademy/kmath"; import {vec} from "mafs"; import * as React from "react"; -import {calculateAngleInDegrees} from "../../math"; import {useTransformVectorsToPixels} from "../use-transform"; import {Arrowhead} from "./arrowhead"; import {SVGLine} from "./svg-line"; +const {calculateAngleInDegrees} = angles; + type Props = { tail: vec.Vector2; tip: vec.Vector2; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx index c06966d992..e29aeee8e3 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/quadratic.tsx @@ -12,6 +12,7 @@ import type { Dispatch, InteractiveGraphElementSuite, } from "../types"; +import type {QuadraticCoefficient, QuadraticCoords} from "@khanacademy/kmath"; export function renderQuadraticGraph( state: QuadraticGraphState, @@ -24,8 +25,6 @@ export function renderQuadraticGraph( } type QuadraticGraphProps = MafsGraphProps; -type QuadraticCoefficient = [number, number, number]; -export type QuadraticCoords = QuadraticGraphState["coords"]; function QuadraticGraph(props: QuadraticGraphProps) { const {dispatch, graphState} = props; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx index d8ec98d7ef..974eda7b24 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx @@ -2,18 +2,19 @@ import {color} from "@khanacademy/wonder-blocks-tokens"; import {Plot} from "mafs"; import * as React from "react"; -import {X, Y} from "../math"; +import {X, Y} from "../math/coordinates"; import {actions} from "../reducer/interactive-graph-action"; import {MovablePoint} from "./components/movable-point"; -import type {Coord} from "../../../interactive2/types"; import type { SinusoidGraphState, MafsGraphProps, Dispatch, InteractiveGraphElementSuite, } from "../types"; +import type {NamedSineCoefficient} from "@khanacademy/kmath"; +import type {Coord} from "@khanacademy/perseus-core"; export function renderSinusoidGraph( state: SinusoidGraphState, @@ -27,13 +28,6 @@ export function renderSinusoidGraph( type SinusoidGraphProps = MafsGraphProps; -export type SineCoefficient = { - amplitude: number; - angularFrequency: number; - phase: number; - verticalOffset: number; -}; - function SinusoidGraph(props: SinusoidGraphProps) { const {dispatch, graphState} = props; @@ -46,7 +40,7 @@ function SinusoidGraph(props: SinusoidGraphProps) { // to content creators the currently selected "correct answer" in the Content Editor. // While we should technically never have invalid coordinates, we want to ensure that // we have a fallback so that the graph can still be plotted without crashing. - const coeffRef = React.useRef({ + const coeffRef = React.useRef({ amplitude: 1, angularFrequency: 1, phase: 1, @@ -82,7 +76,7 @@ function SinusoidGraph(props: SinusoidGraphProps) { // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d export const computeSine = function ( x: number, // x-coordinate - sinusoidCoefficients: SineCoefficient, + sinusoidCoefficients: NamedSineCoefficient, ) { // Break down the coefficients for the sine function to improve readability const { @@ -97,7 +91,7 @@ export const computeSine = function ( export const getSinusoidCoefficients = ( coords: ReadonlyArray, -): SineCoefficient | undefined => { +): NamedSineCoefficient | undefined => { // It's assumed that p1 is the root and p2 is the first peak const p1 = coords[0]; const p2 = coords[1]; diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-line.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-line.tsx index 36fcf12e48..65c5e2be3f 100644 --- a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-line.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-line.tsx @@ -1,3 +1,4 @@ +import {angles} from "@khanacademy/kmath"; import {lockedFigureColors} from "@khanacademy/perseus-core"; import {color as wbColor, spacing} from "@khanacademy/wonder-blocks-tokens"; import {Point, Line, vec} from "mafs"; @@ -7,11 +8,13 @@ import {Arrowhead} from "../graphs/components/arrowhead"; import {Vector} from "../graphs/components/vector"; import {useTransformVectorsToPixels} from "../graphs/use-transform"; import {getIntersectionOfRayWithBox} from "../graphs/utils"; -import {X, Y, calculateAngleInDegrees} from "../math"; +import {X, Y} from "../math"; import type {LockedLineType} from "@khanacademy/perseus-core"; import type {Interval} from "mafs"; +const {calculateAngleInDegrees} = angles; + type Props = LockedLineType & { range: [Interval, Interval]; }; diff --git a/packages/perseus/src/widgets/interactive-graphs/math/index.ts b/packages/perseus/src/widgets/interactive-graphs/math/index.ts index 86f02fcb3e..eb478ac6d9 100644 --- a/packages/perseus/src/widgets/interactive-graphs/math/index.ts +++ b/packages/perseus/src/widgets/interactive-graphs/math/index.ts @@ -5,10 +5,3 @@ export {X, Y} from "./coordinates"; export {MIN, MAX, size} from "./interval"; export {lerp} from "./interpolation"; export {segmentsIntersect} from "./geometry"; -export { - calculateAngleInDegrees, - convertDegreesToRadians, - getClockwiseAngle, - getAngleFromVertex, - polar, -} from "./angles"; diff --git a/packages/perseus/src/widgets/interactive-graphs/protractor.tsx b/packages/perseus/src/widgets/interactive-graphs/protractor.tsx index 6931c4a75f..ff9070468c 100644 --- a/packages/perseus/src/widgets/interactive-graphs/protractor.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/protractor.tsx @@ -1,3 +1,4 @@ +import {angles} from "@khanacademy/kmath"; import {useDrag} from "@use-gesture/react"; import {vec} from "mafs"; import * as React from "react"; @@ -7,13 +8,7 @@ import {pathBuilder} from "../../util/svg"; import {useDraggable} from "./graphs/use-draggable"; import {useTransformVectorsToPixels} from "./graphs/use-transform"; -import { - calculateAngleInDegrees, - convertDegreesToRadians, - lerp, - X, - Y, -} from "./math"; +import {lerp, X, Y} from "./math"; import useGraphConfig from "./reducer/use-graph-config"; import {bound, TARGET_SIZE} from "./utils"; @@ -21,6 +16,8 @@ import type {RefObject} from "react"; import "./protractor.css"; +const {calculateAngleInDegrees, convertDegreesToRadians} = angles; + const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png"; diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts index 6b95a560e0..63735b2926 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts @@ -1,7 +1,7 @@ +import {geometry} from "@khanacademy/kmath"; import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core"; import {vec} from "mafs"; -import {magnitude, vector} from "../../../util/geometry"; import {normalizeCoords, normalizePoints} from "../utils"; import type {Coord} from "../../../interactive2/types"; @@ -21,6 +21,8 @@ import type { } from "@khanacademy/perseus-core"; import type {Interval} from "mafs"; +const {magnitude, vector} = geometry; + export type InitializeGraphStateParams = { range: [x: Interval, y: Interval]; step: [x: number, y: number]; diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.test.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.test.ts index fc0ed20ee8..14044e27a5 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.test.ts @@ -1,7 +1,6 @@ +import {angles} from "@khanacademy/kmath"; import invariant from "tiny-invariant"; -import {getClockwiseAngle} from "../math/angles"; - import {changeSnapStep, changeRange, actions} from "./interactive-graph-action"; import {interactiveGraphReducer} from "./interactive-graph-reducer"; @@ -13,6 +12,8 @@ import type { } from "../types"; import type {GraphRange} from "@khanacademy/perseus-core"; +const {getClockwiseAngle} = angles; + const baseSegmentGraphState: InteractiveGraphState = { hasBeenInteractedWith: false, type: "segment", diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts index 5b5e6974c7..33d1611bca 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-reducer.ts @@ -1,32 +1,16 @@ -import {vector as kvector} from "@khanacademy/kmath"; +import { + angles, + coefficients, + geometry, + vector as kvector, +} from "@khanacademy/kmath"; +import {approximateEqual} from "@khanacademy/perseus-core"; import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core"; import {vec} from "mafs"; import _ from "underscore"; -import Util from "../../../util"; -import { - angleMeasures, - ccw, - lawOfCosines, - magnitude, - polygonSidesIntersect, - reverseVector, - sign, - vector, -} from "../../../util/geometry"; -import {getQuadraticCoefficients} from "../graphs/quadratic"; import {getArrayWithoutDuplicates} from "../graphs/utils"; -import { - clamp, - clampToBox, - getAngleFromVertex, - getClockwiseAngle, - inset, - polar, - snap, - X, - Y, -} from "../math"; +import {clamp, clampToBox, inset, snap, X, Y} from "../math"; import {bound, isUnlimitedGraphState} from "../utils"; import {initializeGraphState} from "./initialize-graph-state"; @@ -71,14 +55,27 @@ import { } from "./interactive-graph-action"; import type {Coord} from "../../../interactive2/types"; -import type {QuadraticCoords} from "../graphs/quadratic"; import type { AngleGraphState, InteractiveGraphState, PairOfPoints, } from "../types"; +import type {QuadraticCoords} from "@khanacademy/kmath"; import type {Interval} from "mafs"; +const {getAngleFromVertex, getClockwiseAngle, polar} = angles; +const { + angleMeasures, + ccw, + lawOfCosines, + magnitude, + polygonSidesIntersect, + reverseVector, + sign, + vector, +} = geometry; +const {getQuadraticCoefficients} = coefficients; + const minDistanceBetweenAngleVertexAndSidePoint = 2; export function interactiveGraphReducer( @@ -777,11 +774,9 @@ interface ConstraintArgs { point: vec.Vector2; } -const eq = Util.eq; - // Less than or approximately equal function leq(a: any, b) { - return a < b || eq(a, b); + return a < b || approximateEqual(a, b); } function boundAndSnapToGrid( diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-state.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-state.ts index 6e28b1aed4..28cb7ac64f 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-state.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/interactive-graph-state.ts @@ -1,9 +1,11 @@ -import {clockwise} from "../../../util/geometry"; +import {geometry} from "@khanacademy/kmath"; import type {Coord} from "../../../interactive2/types"; import type {CircleGraphState, InteractiveGraphState} from "../types"; import type {PerseusGraphType} from "@khanacademy/perseus-core"; +const {clockwise} = geometry; + export function getGradableGraph( state: InteractiveGraphState, initialGraph: PerseusGraphType, diff --git a/packages/perseus/src/widgets/interactive-graphs/types.ts b/packages/perseus/src/widgets/interactive-graphs/types.ts index 488d5c3b7a..7055d5a1d0 100644 --- a/packages/perseus/src/widgets/interactive-graphs/types.ts +++ b/packages/perseus/src/widgets/interactive-graphs/types.ts @@ -1,6 +1,7 @@ import type {InteractiveGraphAction} from "./reducer/interactive-graph-action"; import type {Coord} from "../../interactive2/types"; import type {WidgetProps} from "../../types"; +import type {QuadraticCoords} from "@khanacademy/kmath"; import type {PerseusInteractiveGraphWidgetOptions} from "@khanacademy/perseus-core"; import type {Interval, vec} from "mafs"; import type {ReactNode} from "react"; @@ -106,7 +107,7 @@ export interface CircleGraphState extends InteractiveGraphStateCommon { export interface QuadraticGraphState extends InteractiveGraphStateCommon { type: "quadratic"; - coords: [Coord, Coord, Coord]; + coords: QuadraticCoords; } export interface SinusoidGraphState extends InteractiveGraphStateCommon {