From d4987658e0428aece3e603b34d465266aaf7d404 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 17 Jan 2025 15:22:22 -0600 Subject: [PATCH] Move categorizer scoring logic (#2102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * move categorizer scoring logic * Move NumericInput and InputNumber scoring logic to `perseus-score` (#2105) * move numeric-input and input-number * respond to Jeremy's feedback * Move Radio scorer/validator (#2106) * move radio scorer/validator * lint errorToString better * comment my unique type * respond to Jeremy's feedback * Move scoring logic: CSProgram, Iframe, and Dropdown (#2111) * cs-program * move dropdown * move iframe * Move scoring logic: Table, NumberLine, Matcher (#2112) * move table * number-line * move matcher * Move scorers: Plotter and Sorter (#2113) * plotter * sorter * Move scorer: Orderer (#2114) * orderer * Move scorerer: LabelImage (#2115) * move label-image * rename labelImageScoreMarker * Move scoring logic: Matrix (#2116) * move matrix * Move scoring logic: Expression (#2118) * move expression scorer * respond to Ben's feedback * merge Grapher move * fix conflict again * Move scorer: Grapher (#2119) * move matrix * move expression scorer * move grapher scorer * respond to Ben's feedback * Move scorer: Interactive Graph (#2120) * 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 --- config/test/custom-matchers.ts | 2 +- dev/flipbook.tsx | 4 +- .../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 + .../components/key-handlers/key-translator.ts | 3 +- .../src/components/keypad/button-assets.tsx | 7 +- packages/math-input/src/utils.ts | 37 - packages/perseus-core/src/index.ts | 7 + .../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 ++ .../src/utils}/get-decimal-separator.ts | 0 .../perseus-core/src/utils/get-matrix-size.ts | 27 + .../src/utils}/grapher-types.ts | 2 +- .../perseus-core/src/utils/grapher-util.ts | 646 +++++++++++++++++ packages/perseus-editor/package.json | 1 + .../src/components/graph-settings.tsx | 3 +- .../src/widgets/grapher-editor.tsx | 4 +- .../src/widgets/input-number-editor.tsx | 38 +- .../interaction-editor/interaction-editor.tsx | 3 +- .../start-coords/util.ts | 5 +- .../src/widgets/matrix-editor.tsx | 25 +- packages/perseus-score/src/error-codes.ts | 8 + packages/perseus-score/src/index.ts | 24 + .../src/util}/tex-wrangler.test.ts | 4 +- .../src/util}/tex-wrangler.ts | 9 +- .../perseus-score/src/validation.types.ts | 15 + .../categorizer/score-categorizer.test.ts | 8 +- .../widgets/categorizer/score-categorizer.ts | 12 +- .../categorizer/validate-categorizer.test.ts | 18 +- .../categorizer/validate-categorizer.ts | 8 +- .../cs-program/score-cs-program.test.ts | 2 +- .../widgets/cs-program/score-cs-program.ts | 11 +- .../widgets/dropdown/score-dropdown.test.ts | 25 +- .../src/widgets/dropdown/score-dropdown.ts | 4 +- .../dropdown/validate-dropdown.test.ts | 2 +- .../src/widgets/dropdown/validate-dropdown.ts | 6 +- .../expression/score-expression.test.ts | 7 +- .../expression/score-expression.testdata.ts | 36 + .../widgets/expression/score-expression.ts | 26 +- .../expression/validate-expression.test.ts | 0 .../widgets/expression/validate-expression.ts | 6 +- .../src/widgets/grapher/score-grapher.test.ts | 6 +- .../src/widgets/grapher/score-grapher.ts | 18 +- .../src/widgets/iframe/score-iframe.test.ts | 4 +- .../src/widgets/iframe/score-iframe.ts | 13 +- .../input-number/score-input-number.test.ts | 53 +- .../input-number/score-input-number.ts | 19 +- .../score-interactive-graph.test.ts | 9 +- .../score-interactive-graph.ts | 59 +- .../label-image/score-label-image.test.ts | 14 +- .../widgets/label-image/score-label-image.ts | 12 +- .../src/widgets/matcher/score-matcher.test.ts | 2 +- .../src/widgets/matcher/score-matcher.ts | 4 +- .../src/widgets/matrix/score-matrix.test.ts | 40 +- .../src/widgets/matrix/score-matrix.ts | 15 +- .../widgets/matrix/validate-matrix.test.ts | 10 +- .../src/widgets/matrix/validate-matrix.ts | 12 +- .../number-line/score-number-line.test.ts | 2 +- .../widgets/number-line/score-number-line.ts | 4 +- .../number-line/validate-number-line.test.ts | 2 +- .../number-line/validate-number-line.ts | 6 +- .../numeric-input/score-numeric-input.test.ts | 76 +- .../numeric-input/score-numeric-input.ts | 23 +- .../src/widgets/orderer/score-orderer.test.ts | 50 +- .../src/widgets/orderer/score-orderer.ts | 8 +- .../widgets/orderer/validate-orderer.test.ts | 2 +- .../src/widgets/orderer/validate-orderer.ts | 6 +- .../src/widgets/plotter/score-plotter.test.ts | 2 +- .../src/widgets/plotter/score-plotter.ts | 13 +- .../widgets/plotter/validate-plotter.test.ts | 2 +- .../src/widgets/plotter/validate-plotter.ts | 11 +- .../src/widgets/radio/score-radio.test.ts | 20 +- .../src/widgets/radio/score-radio.ts | 12 +- .../src/widgets/radio/validate-radio.test.ts | 2 +- .../src/widgets/radio/validate-radio.ts | 6 +- .../src/widgets/sorter/score-sorter.test.ts | 4 +- .../src/widgets/sorter/score-sorter.ts | 11 +- .../widgets/sorter/validate-sorter.test.ts | 2 +- .../src/widgets/sorter/validate-sorter.ts | 6 +- .../src/widgets/table/score-table.test.ts | 18 +- .../src/widgets/table/score-table.ts | 9 +- .../src/widgets/table/utils.ts | 0 .../src/widgets/table/validate-table.test.ts | 2 +- .../src/widgets/table/validate-table.ts | 6 +- 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 | 7 - packages/perseus/src/renderer-util.ts | 7 +- packages/perseus/src/renderer.tsx | 7 +- packages/perseus/src/strings.ts | 41 +- packages/perseus/src/types.ts | 16 +- packages/perseus/src/util.ts | 74 -- packages/perseus/src/util/interactive.ts | 4 +- .../perseus/src/util/is-real-json-parse.ts | 6 +- packages/perseus/src/util/scoring.ts | 3 +- packages/perseus/src/util/test-utils.ts | 3 +- .../src/widgets/__shared__/score-noop.ts | 2 +- .../widgets/categorizer/categorizer.test.ts | 4 +- .../src/widgets/categorizer/categorizer.tsx | 3 +- .../src/widgets/cs-program/cs-program.tsx | 11 +- .../perseus/src/widgets/dropdown/dropdown.tsx | 3 +- .../src/widgets/expression/expression.tsx | 17 +- .../src/widgets/graded-group/graded-group.tsx | 9 +- .../perseus/src/widgets/grapher/grapher.tsx | 18 +- packages/perseus/src/widgets/grapher/util.tsx | 661 +----------------- .../perseus/src/widgets/group/score-group.ts | 2 +- .../perseus/src/widgets/iframe/iframe.tsx | 13 +- .../widgets/input-number/input-number.test.ts | 33 +- .../src/widgets/input-number/input-number.tsx | 14 +- .../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 +- .../src/widgets/label-image/label-image.tsx | 15 +- .../perseus/src/widgets/matcher/matcher.tsx | 11 +- .../perseus/src/widgets/matrix/matrix.tsx | 44 +- .../widgets/mock-widgets/score-mock-widget.ts | 2 +- .../src/widgets/number-line/number-line.tsx | 7 +- .../widgets/numeric-input/numeric-input.tsx | 11 +- .../perseus/src/widgets/orderer/orderer.tsx | 11 +- .../perseus/src/widgets/plotter/plotter.tsx | 11 +- .../src/widgets/radio/__tests__/radio.test.ts | 19 +- .../src/widgets/radio/radio-component.tsx | 16 +- packages/perseus/src/widgets/radio/radio.ts | 2 +- .../perseus/src/widgets/sorter/sorter.tsx | 11 +- packages/perseus/src/widgets/table/table.tsx | 11 +- 144 files changed, 1618 insertions(+), 1606 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/expression => perseus-core/src/utils}/get-decimal-separator.ts (100%) create mode 100644 packages/perseus-core/src/utils/get-matrix-size.ts rename packages/{perseus/src/widgets/grapher => perseus-core/src/utils}/grapher-types.ts (97%) create mode 100644 packages/perseus-core/src/utils/grapher-util.ts rename packages/{perseus/src/__tests__ => perseus-score/src/util}/tex-wrangler.test.ts (96%) rename packages/{perseus/src => perseus-score/src/util}/tex-wrangler.ts (97%) rename packages/{perseus => perseus-score}/src/widgets/categorizer/score-categorizer.test.ts (73%) rename packages/{perseus => perseus-score}/src/widgets/categorizer/score-categorizer.ts (70%) rename packages/{perseus => perseus-score}/src/widgets/categorizer/validate-categorizer.test.ts (64%) rename packages/{perseus => perseus-score}/src/widgets/categorizer/validate-categorizer.ts (83%) rename packages/{perseus => perseus-score}/src/widgets/cs-program/score-cs-program.test.ts (94%) rename packages/{perseus => perseus-score}/src/widgets/cs-program/score-cs-program.ts (69%) rename packages/{perseus => perseus-score}/src/widgets/dropdown/score-dropdown.test.ts (59%) rename packages/{perseus => perseus-score}/src/widgets/dropdown/score-dropdown.ts (87%) rename packages/{perseus => perseus-score}/src/widgets/dropdown/validate-dropdown.test.ts (91%) rename packages/{perseus => perseus-score}/src/widgets/dropdown/validate-dropdown.ts (76%) rename packages/{perseus => perseus-score}/src/widgets/expression/score-expression.test.ts (96%) create mode 100644 packages/perseus-score/src/widgets/expression/score-expression.testdata.ts rename packages/{perseus => perseus-score}/src/widgets/expression/score-expression.ts (92%) rename packages/{perseus => perseus-score}/src/widgets/expression/validate-expression.test.ts (100%) rename packages/{perseus => perseus-score}/src/widgets/expression/validate-expression.ts (80%) rename packages/{perseus => perseus-score}/src/widgets/grapher/score-grapher.test.ts (98%) rename packages/{perseus => perseus-score}/src/widgets/grapher/score-grapher.ts (84%) rename packages/{perseus => perseus-score}/src/widgets/iframe/score-iframe.test.ts (91%) rename packages/{perseus => perseus-score}/src/widgets/iframe/score-iframe.ts (66%) rename packages/{perseus => perseus-score}/src/widgets/input-number/score-input-number.test.ts (68%) rename packages/{perseus => perseus-score}/src/widgets/input-number/score-input-number.ts (80%) 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%) rename packages/{perseus => perseus-score}/src/widgets/label-image/score-label-image.test.ts (94%) rename packages/{perseus => perseus-score}/src/widgets/label-image/score-label-image.ts (93%) rename packages/{perseus => perseus-score}/src/widgets/matcher/score-matcher.test.ts (97%) rename packages/{perseus => perseus-score}/src/widgets/matcher/score-matcher.ts (84%) rename packages/{perseus => perseus-score}/src/widgets/matrix/score-matrix.test.ts (82%) rename packages/{perseus => perseus-score}/src/widgets/matrix/score-matrix.ts (84%) rename packages/{perseus => perseus-score}/src/widgets/matrix/validate-matrix.test.ts (75%) rename packages/{perseus => perseus-score}/src/widgets/matrix/validate-matrix.ts (77%) rename packages/{perseus => perseus-score}/src/widgets/number-line/score-number-line.test.ts (98%) rename packages/{perseus => perseus-score}/src/widgets/number-line/score-number-line.ts (94%) rename packages/{perseus => perseus-score}/src/widgets/number-line/validate-number-line.test.ts (93%) rename packages/{perseus => perseus-score}/src/widgets/number-line/validate-number-line.ts (86%) rename packages/{perseus => perseus-score}/src/widgets/numeric-input/score-numeric-input.test.ts (85%) rename packages/{perseus => perseus-score}/src/widgets/numeric-input/score-numeric-input.ts (93%) rename packages/{perseus => perseus-score}/src/widgets/orderer/score-orderer.test.ts (65%) rename packages/{perseus => perseus-score}/src/widgets/orderer/score-orderer.ts (84%) rename packages/{perseus => perseus-score}/src/widgets/orderer/validate-orderer.test.ts (91%) rename packages/{perseus => perseus-score}/src/widgets/orderer/validate-orderer.ts (78%) rename packages/{perseus => perseus-score}/src/widgets/plotter/score-plotter.test.ts (96%) rename packages/{perseus => perseus-score}/src/widgets/plotter/score-plotter.ts (69%) rename packages/{perseus => perseus-score}/src/widgets/plotter/validate-plotter.test.ts (96%) rename packages/{perseus => perseus-score}/src/widgets/plotter/validate-plotter.ts (73%) rename packages/{perseus => perseus-score}/src/widgets/radio/score-radio.test.ts (89%) rename packages/{perseus => perseus-score}/src/widgets/radio/score-radio.ts (86%) rename packages/{perseus => perseus-score}/src/widgets/radio/validate-radio.test.ts (90%) rename packages/{perseus => perseus-score}/src/widgets/radio/validate-radio.ts (85%) rename packages/{perseus => perseus-score}/src/widgets/sorter/score-sorter.test.ts (98%) rename packages/{perseus => perseus-score}/src/widgets/sorter/score-sorter.ts (69%) rename packages/{perseus => perseus-score}/src/widgets/sorter/validate-sorter.test.ts (92%) rename packages/{perseus => perseus-score}/src/widgets/sorter/validate-sorter.ts (88%) rename packages/{perseus => perseus-score}/src/widgets/table/score-table.test.ts (87%) rename packages/{perseus => perseus-score}/src/widgets/table/score-table.ts (87%) rename packages/{perseus => perseus-score}/src/widgets/table/utils.ts (100%) rename packages/{perseus => perseus-score}/src/widgets/table/validate-table.test.ts (90%) rename packages/{perseus => perseus-score}/src/widgets/table/validate-table.ts (80%) diff --git a/config/test/custom-matchers.ts b/config/test/custom-matchers.ts index afddd66c24..4f2eab260b 100644 --- a/config/test/custom-matchers.ts +++ b/config/test/custom-matchers.ts @@ -1,6 +1,6 @@ // Ok here we are -import type {PerseusScore} from "../../packages/perseus/src/types"; +import type {PerseusScore} from "../../packages/perseus-score/src/validation.types"; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/dev/flipbook.tsx b/dev/flipbook.tsx index b6cf09fb3c..1944dffebf 100644 --- a/dev/flipbook.tsx +++ b/dev/flipbook.tsx @@ -39,13 +39,15 @@ import { } from "./flipbook-model"; import {Header} from "./header"; -import type {APIOptions, PerseusScore} from "../packages/perseus/src"; +import type {APIOptions} from "../packages/perseus/src"; import type { InteractiveGraphWidget, PerseusRenderer, PerseusWidget, } from "../packages/perseus-core/src/data-schema"; +import type {PerseusScore} from "../packages/perseus-score/src/validation.types"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; + import "../packages/perseus/src/styles/perseus-renderer.less"; const exampleCommands = ` 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/math-input/src/components/key-handlers/key-translator.ts b/packages/math-input/src/components/key-handlers/key-translator.ts index ced26e7254..6d5b8347bf 100644 --- a/packages/math-input/src/components/key-handlers/key-translator.ts +++ b/packages/math-input/src/components/key-handlers/key-translator.ts @@ -1,5 +1,6 @@ +import {getDecimalSeparator} from "@khanacademy/perseus-core"; + import {MathFieldActionType} from "../../types"; -import {getDecimalSeparator} from "../../utils"; import {mathQuillInstance} from "../input/mathquill-instance"; import handleArrow from "./handle-arrow"; diff --git a/packages/math-input/src/components/keypad/button-assets.tsx b/packages/math-input/src/components/keypad/button-assets.tsx index 732efcd596..5803f6ceca 100644 --- a/packages/math-input/src/components/keypad/button-assets.tsx +++ b/packages/math-input/src/components/keypad/button-assets.tsx @@ -10,9 +10,9 @@ asset. In the future it would be great if these were included from files so that no copying and pasting is necessary. */ +import {getDecimalSeparator} from "@khanacademy/perseus-core"; import * as React from "react"; -import {DecimalSeparator, getDecimalSeparator} from "../../utils"; import {useMathInputI18n} from "../i18n-context"; import type Key from "../../data/keys"; @@ -176,10 +176,7 @@ export default function ButtonAsset({id}: Props): React.ReactNode { case "PERIOD": // Different locales use different symbols for the decimal separator // (, vs .) - if ( - id === "DECIMAL" && - getDecimalSeparator(locale) === DecimalSeparator.COMMA - ) { + if (id === "DECIMAL" && getDecimalSeparator(locale) !== ".") { // comma decimal separator return ( { - let separator: string = DecimalSeparator.PERIOD; - - switch (locale) { - // TODO(somewhatabstract): Remove this when Chrome supports the `ka` - // locale properly. - // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201 - // - // Supported locales in Chrome: - // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list - case "ka": - separator = ","; - break; - - default: - const numberWithDecimalSeparator = 1.1; - // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to - // support Safari 12. - const match = new Intl.NumberFormat(locale) - .format(numberWithDecimalSeparator) - // 0x661 is ARABIC-INDIC DIGIT ONE - // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE - .match(/[^\d\u0661\u06F1]/); - separator = match?.[0] ?? "."; - } - - return separator === "," ? DecimalSeparator.COMMA : DecimalSeparator.PERIOD; -}; - const CDOT_ONLY = [ "az", "cs", diff --git a/packages/perseus-core/src/index.ts b/packages/perseus-core/src/index.ts index a284735b04..df95a6a770 100644 --- a/packages/perseus-core/src/index.ts +++ b/packages/perseus-core/src/index.ts @@ -8,9 +8,16 @@ export type { Relationship, } from "./types"; export type {ErrorKind} from "./error/errors"; +export type {FunctionTypeMappingKeys} from "./utils/grapher-util"; +export type {Coords} from "./utils/grapher-types"; // Careful, `version.ts` uses this function so it _must_ be imported above it 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/src/widgets/expression/get-decimal-separator.ts b/packages/perseus-core/src/utils/get-decimal-separator.ts similarity index 100% rename from packages/perseus/src/widgets/expression/get-decimal-separator.ts rename to packages/perseus-core/src/utils/get-decimal-separator.ts diff --git a/packages/perseus-core/src/utils/get-matrix-size.ts b/packages/perseus-core/src/utils/get-matrix-size.ts new file mode 100644 index 0000000000..1388852973 --- /dev/null +++ b/packages/perseus-core/src/utils/get-matrix-size.ts @@ -0,0 +1,27 @@ +import _ from "underscore"; + +function getMatrixSize(matrix: ReadonlyArray>) { + const matrixSize = [1, 1]; + + // We need to find the widest row and tallest column to get the correct + // matrix size. + _(matrix).each((matrixRow, row) => { + let rowWidth = 0; + _(matrixRow).each((matrixCol, col) => { + if (matrixCol != null && matrixCol.toString().length) { + rowWidth = col + 1; + } + }); + + // Matrix width: + matrixSize[1] = Math.max(matrixSize[1], rowWidth); + + // Matrix height: + if (rowWidth > 0) { + matrixSize[0] = Math.max(matrixSize[0], row + 1); + } + }); + return matrixSize; +} + +export default getMatrixSize; diff --git a/packages/perseus/src/widgets/grapher/grapher-types.ts b/packages/perseus-core/src/utils/grapher-types.ts similarity index 97% rename from packages/perseus/src/widgets/grapher/grapher-types.ts rename to packages/perseus-core/src/utils/grapher-types.ts index c004902cbb..232f91831d 100644 --- a/packages/perseus/src/widgets/grapher/grapher-types.ts +++ b/packages/perseus-core/src/utils/grapher-types.ts @@ -1,4 +1,4 @@ -import type {Coord} from "@khanacademy/perseus"; +import type {Coord} from "../data-schema"; export type Coords = [Coord, Coord]; diff --git a/packages/perseus-core/src/utils/grapher-util.ts b/packages/perseus-core/src/utils/grapher-util.ts new file mode 100644 index 0000000000..072b9d8f58 --- /dev/null +++ b/packages/perseus-core/src/utils/grapher-util.ts @@ -0,0 +1,646 @@ +import _ from "underscore"; + +import {approximateDeepEqual} from "./equality"; + +import type { + LinearType, + QuadraticType, + SinusoidType, + TangentType, + ExponentialType, + LogarithmType, + AbsoluteValueType, + Coords, +} from "./grapher-types"; +import type {Coord} from "../data-schema"; + +export const MOVABLES = { + PLOT: "PLOT", + PARABOLA: "PARABOLA", + SINUSOID: "SINUSOID", +}; + +// TODO(charlie): These really need to go into a utility file as they're being +// used by both interactive-graph and now grapher. +function canonicalSineCoefficients(coeffs: any) { + // For a curve of the form f(x) = a * Sin(b * x - c) + d, + // this function ensures that a, b > 0, and c is its + // smallest possible positive value. + let amplitude = coeffs[0]; + let angularFrequency = coeffs[1]; + let phase = coeffs[2]; + const verticalOffset = coeffs[3]; + + // Guarantee a > 0 + if (amplitude < 0) { + amplitude *= -1; + angularFrequency *= -1; + phase *= -1; + } + + const period = 2 * Math.PI; + // Guarantee b > 0 + if (angularFrequency < 0) { + angularFrequency *= -1; + phase *= -1; + phase += period / 2; + } + + // Guarantee c is smallest possible positive value + while (phase > 0) { + phase -= period; + } + while (phase < 0) { + phase += period; + } + + return [amplitude, angularFrequency, phase, verticalOffset]; +} + +function canonicalTangentCoefficients(coeffs: any) { + // For a curve of the form f(x) = a * Tan(b * x - c) + d, + // this function ensures that a, b > 0, and c is its + // smallest possible positive value. + let amplitude = coeffs[0]; + let angularFrequency = coeffs[1]; + let phase = coeffs[2]; + const verticalOffset = coeffs[3]; + + // Guarantee a > 0 + if (amplitude < 0) { + amplitude *= -1; + angularFrequency *= -1; + phase *= -1; + } + + const period = Math.PI; + // Guarantee b > 0 + if (angularFrequency < 0) { + angularFrequency *= -1; + phase *= -1; + phase += period / 2; + } + + // Guarantee c is smallest possible positive value + while (phase > 0) { + phase -= period; + } + while (phase < 0) { + phase += period; + } + + return [amplitude, angularFrequency, phase, verticalOffset]; +} + +const PlotDefaults = { + areEqual: function ( + coeffs1: ReadonlyArray, + coeffs2: ReadonlyArray, + ): boolean { + return approximateDeepEqual(coeffs1, coeffs2); + }, + movable: MOVABLES.PLOT, + getPropsForCoeffs: function (coeffs: ReadonlyArray): {fn: any} { + return { + // @ts-expect-error - TS2339 - Property 'getFunctionForCoeffs' does not exist on type '{ readonly areEqual: (coeffs1: any, coeffs2: any) => boolean; readonly Movable: any; readonly getPropsForCoeffs: (coeffs: any) => any; }'. + fn: _.partial(this.getFunctionForCoeffs, coeffs), + }; + }, +} as const; + +const Linear: LinearType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png", + + defaultCoords: [ + [0.25, 0.75], + [0.75, 0.75], + ], + + getCoefficients: function ( + coords: Coords, + ): ReadonlyArray | undefined { + const p1 = coords[0]; + const p2 = coords[1]; + + const denom = p2[0] - p1[0]; + const num = p2[1] - p1[1]; + + if (denom === 0) { + return; + } + + const m = num / denom; + const b = p2[1] - m * p2[0]; + return [m, b]; + }, + + getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { + const m = coeffs[0]; + const b = coeffs[1]; + return m * x + b; + }, + + getEquationString: function (coords: Coords) { + const coeffs: ReadonlyArray = this.getCoefficients(coords); + const m: number = coeffs[0]; + const b: number = coeffs[1]; + return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); + }, +}); + +const Quadratic: QuadraticType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png", + + defaultCoords: [ + [0.5, 0.5], + [0.75, 0.75], + ], + movable: MOVABLES.PARABOLA, + + getCoefficients: function (coords: Coords): ReadonlyArray { + const p1 = coords[0]; + const p2 = coords[1]; + + // Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k + const h = p1[0]; + const k = p1[1]; + + // Use these to calculate familiar a, b, c + const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h)); + const b = -2 * h * a; + const c = a * h * h + k; + + return [a, b, c]; + }, + + getFunctionForCoeffs: function ( + coeffs: ReadonlyArray, + x: number, + ): number { + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return (a * x + b) * x + c; + }, + + getPropsForCoeffs: function (coeffs: ReadonlyArray): { + a: number; + b: number; + c: number; + } { + return { + a: coeffs[0], + b: coeffs[1], + c: coeffs[2], + }; + }, + + getEquationString: function (coords: Coords) { + const coeffs = this.getCoefficients(coords); + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return ( + "y = " + + a.toFixed(3) + + "x^2 + " + + b.toFixed(3) + + "x + " + + c.toFixed(3) + ); + }, +}); + +const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png", + + defaultCoords: [ + [0.5, 0.5], + [0.6, 0.6], + ], + movable: MOVABLES.SINUSOID, + + getCoefficients: function (coords: Coords) { + const p1 = coords[0]; + const p2 = coords[1]; + + const a = p2[1] - p1[1]; + const b = Math.PI / (2 * (p2[0] - p1[0])); + const c = p1[0] * b; + const d = p1[1]; + + return [a, b, c, d]; + }, + + getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; + return a * Math.sin(b * x - c) + d; + }, + + getPropsForCoeffs: function (coeffs: ReadonlyArray) { + return { + a: coeffs[0], + b: coeffs[1], + c: coeffs[2], + d: coeffs[3], + }; + }, + + getEquationString: function (coords: Coords) { + const coeffs = this.getCoefficients(coords); + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; + return ( + "y = " + + a.toFixed(3) + + " sin(" + + b.toFixed(3) + + "x - " + + c.toFixed(3) + + ") + " + + d.toFixed(3) + ); + }, + + areEqual: function ( + coeffs1: ReadonlyArray, + coeffs2: ReadonlyArray, + ) { + return approximateDeepEqual( + canonicalSineCoefficients(coeffs1), + canonicalSineCoefficients(coeffs2), + ); + }, +}); + +const Tangent: TangentType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png", + + defaultCoords: [ + [0.5, 0.5], + [0.75, 0.75], + ], + + getCoefficients: function (coords: Coords) { + const p1 = coords[0]; + const p2 = coords[1]; + + const a = p2[1] - p1[1]; + const b = Math.PI / (4 * (p2[0] - p1[0])); + const c = p1[0] * b; + const d = p1[1]; + + return [a, b, c, d]; + }, + + getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; + return a * Math.tan(b * x - c) + d; + }, + + getEquationString: function (coords: Coords) { + const coeffs = this.getCoefficients(coords); + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; + return ( + "y = " + + a.toFixed(3) + + " sin(" + + b.toFixed(3) + + "x - " + + c.toFixed(3) + + ") + " + + d.toFixed(3) + ); + }, + + areEqual: function ( + coeffs1: ReadonlyArray, + coeffs2: ReadonlyArray, + ) { + return approximateDeepEqual( + canonicalTangentCoefficients(coeffs1), + canonicalTangentCoefficients(coeffs2), + ); + }, +}); + +const Exponential: ExponentialType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png", + + defaultCoords: [ + [0.5, 0.55], + [0.75, 0.75], + ], + + defaultAsymptote: [ + [0, 0.5], + [1.0, 0.5], + ], + + /** + * Add extra constraints for movement of the points or asymptote (below): + * newCoord: [x, y] + * The end position of the point or asymptote endpoint + * oldCoord: [x, y] + * The old position of the point or asymptote endpoint + * coords: + * An array of coordinates representing the proposed end configuration + * of the plot coordinates. + * asymptote: + * An array of coordinates representing the proposed end configuration + * of the asymptote. + * + * Return: either a coordinate (to be used as the resulting coordinate of + * the move) or a boolean, where `true` uses newCoord as the resulting + * coordinate, and `false` uses oldCoord as the resulting coordinate. + */ + extraCoordConstraint: function ( + newCoord: Coord, + oldCoord: Coord, + coords: Coords, + asymptote: Coords, + graph, + ) { + const y: number = asymptote[0][1]; + return _.all(coords, (coord) => coord[1] !== y); + }, + + extraAsymptoteConstraint: function ( + newCoord: Coord, + oldCoord: Coord, + coords: Coords, + asymptote: Coords, + graph, + ): Coord { + const y = newCoord[1]; + const isValid = + _.all(coords, (coord) => coord[1] > y) || + _.all(coords, (coord) => coord[1] < y); + + if (isValid) { + return [oldCoord[0], y]; + } + // Snap the asymptote as close as possible, i.e., if the user moves + // the mouse really quickly into an invalid region + const oldY = oldCoord[1]; + const wasBelow = _.all(coords, (coord) => coord[1] > oldY); + if (wasBelow) { + const bottomMost = _.min(_.map(coords, (coord) => coord[1])); + return [oldCoord[0], bottomMost - graph.snapStep[1]]; + } + const topMost = _.max(_.map(coords, (coord) => coord[1])); + return [oldCoord[0], topMost + graph.snapStep[1]]; + }, + + allowReflectOverAsymptote: true, + + getCoefficients: function ( + coords: Coords, + asymptote: Coords, + ): ReadonlyArray { + const p1 = coords[0]; + const p2 = coords[1]; + + const c = asymptote[0][1]; + const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]); + const a = (p1[1] - c) / Math.exp(b * p1[0]); + return [a, b, c]; + }, + + getFunctionForCoeffs: function ( + coeffs: ReadonlyArray, + x: number, + ): number { + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return a * Math.exp(b * x) + c; + }, + + getEquationString: function (coords: Coords, asymptote: Coords) { + if (!asymptote) { + return null; + } + const coeffs = this.getCoefficients(coords, asymptote); + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return ( + "y = " + + a.toFixed(3) + + "e^(" + + b.toFixed(3) + + "x) + " + + c.toFixed(3) + ); + }, +}); + +const Logarithm: LogarithmType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png", + + defaultCoords: [ + [0.55, 0.5], + [0.75, 0.75], + ], + + defaultAsymptote: [ + [0.5, 0], + [0.5, 1.0], + ], + + extraCoordConstraint: function ( + newCoord: Coord, + oldCoord: Coord, + coords: Coord, + asymptote: Coords, + graph, + ) { + const x = asymptote[0][0]; + return ( + _.all(coords, (coord) => coord[0] !== x) && + coords[0][1] !== coords[1][1] + ); + }, + + extraAsymptoteConstraint: function ( + newCoord: Coord, + oldCoord: Coord, + coords: Coords, + asymptote: Coords, + graph, + ): ReadonlyArray { + const x = newCoord[0]; + const isValid = + _.all(coords, (coord) => coord[0] > x) || + _.all(coords, (coord) => coord[0] < x); + + if (isValid) { + return [x, oldCoord[1]]; + } + // Snap the asymptote as close as possible, i.e., if the user moves + // the mouse really quickly into an invalid region + const oldX = oldCoord[0]; + const wasLeft = _.all(coords, (coord) => coord[0] > oldX); + if (wasLeft) { + const leftMost = _.min(_.map(coords, (coord) => coord[0])); + return [leftMost - graph.snapStep[0], oldCoord[1]]; + } + const rightMost = _.max(_.map(coords, (coord) => coord[0])); + return [rightMost + graph.snapStep[0], oldCoord[1]]; + }, + + allowReflectOverAsymptote: true, + + getCoefficients: function ( + coords: Coords, + asymptote: Coords, + ): ReadonlyArray | undefined { + // It's easiest to calculate the logarithm's coefficients by thinking + // about it as the inverse of the exponential, so we flip x and y and + // perform some algebra on the coefficients. This also unifies the + // logic between the two 'models'. + const flip = (coord: Coord): Coord => [coord[1], coord[0]]; + const inverseCoeffs = Exponential.getCoefficients( + _.map(coords, flip) as Coords, + _.map(asymptote, flip) as Coords, + ); + if (inverseCoeffs) { + const c = -inverseCoeffs[2] / inverseCoeffs[0]; + const b = 1 / inverseCoeffs[0]; + const a = 1 / inverseCoeffs[1]; + return [a, b, c]; + } + }, + + getFunctionForCoeffs: function ( + coeffs: ReadonlyArray, + x: number, + asymptote: Coords, + ) { + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return a * Math.log(b * x + c); + }, + + getEquationString: function (coords: Coords, asymptote: Coords) { + if (!asymptote) { + return null; + } + const coeffs: ReadonlyArray = this.getCoefficients( + coords, + asymptote, + ); + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + return ( + "y = ln(" + + a.toFixed(3) + + "x + " + + b.toFixed(3) + + ") + " + + c.toFixed(3) + ); + }, +}); + +const AbsoluteValue: AbsoluteValueType = _.extend({}, PlotDefaults, { + url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png", + + defaultCoords: [ + [0.5, 0.5], + [0.75, 0.75], + ], + + getCoefficients: function ( + coords: Coords, + ): ReadonlyArray | undefined { + const p1 = coords[0]; + const p2 = coords[1]; + + const denom = p2[0] - p1[0]; + const num = p2[1] - p1[1]; + + if (denom === 0) { + return; + } + + let m = Math.abs(num / denom); + if (p2[1] < p1[1]) { + m *= -1; + } + const horizontalOffset = p1[0]; + const verticalOffset = p1[1]; + + return [m, horizontalOffset, verticalOffset]; + }, + + getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; + return m * Math.abs(x - horizontalOffset) + verticalOffset; + }, + + getEquationString: function (coords: Coords) { + const coeffs: ReadonlyArray = this.getCoefficients(coords); + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; + return ( + "y = " + + m.toFixed(3) + + "| x - " + + horizontalOffset.toFixed(3) + + "| + " + + verticalOffset.toFixed(3) + ); + }, +}); + +/* Utility functions for dealing with graphing interfaces. */ +const functionTypeMapping = { + linear: Linear, + quadratic: Quadratic, + sinusoid: Sinusoid, + tangent: Tangent, + exponential: Exponential, + logarithm: Logarithm, + absolute_value: AbsoluteValue, +} as const; + +export const allTypes: any = _.keys(functionTypeMapping); + +export type FunctionTypeMappingKeys = keyof typeof functionTypeMapping; + +type ConditionalGraderType = + // prettier-ignore + T extends "linear" ? LinearType + : T extends "quadratic" ? QuadraticType + : T extends "sinusoid" ? SinusoidType + : T extends "tangent" ? TangentType + : T extends "exponential" ? ExponentialType + : T extends "logarithm" ? LogarithmType + : T extends "absolute_value" ? AbsoluteValueType + : never; + +export function functionForType( + type: T, +): ConditionalGraderType { + // @ts-expect-error: TypeScript doesn't know how to use deal with generics + // and conditional types in this way. + return functionTypeMapping[type]; +} diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 7a738f3cce..7bb81af91b 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -41,6 +41,7 @@ "@khanacademy/math-input": "^22.1.2", "@khanacademy/perseus": "^50.0.0", "@khanacademy/perseus-core": "3.1.0", + "@khanacademy/perseus-score": "^1.0.0", "@khanacademy/pure-markdown": "^0.3.21", "mafs": "^0.19.0" }, diff --git a/packages/perseus-editor/src/components/graph-settings.tsx b/packages/perseus-editor/src/components/graph-settings.tsx index 4ae48751f5..4f551f628e 100644 --- a/packages/perseus-editor/src/components/graph-settings.tsx +++ b/packages/perseus-editor/src/components/graph-settings.tsx @@ -15,8 +15,7 @@ import * as React from "react"; import ReactDOM from "react-dom"; import _ from "underscore"; -import type {Coords} from "@khanacademy/perseus"; -import type {MarkingsType} from "@khanacademy/perseus-core"; +import type {Coords, MarkingsType} from "@khanacademy/perseus-core"; const {ButtonGroup, InfoTip, RangeInput} = components; diff --git a/packages/perseus-editor/src/widgets/grapher-editor.tsx b/packages/perseus-editor/src/widgets/grapher-editor.tsx index f2056e96f5..9b2d1631bb 100644 --- a/packages/perseus-editor/src/widgets/grapher-editor.tsx +++ b/packages/perseus-editor/src/widgets/grapher-editor.tsx @@ -7,6 +7,7 @@ import { containerSizeClass, getInteractiveBoxFromSizeClass, } from "@khanacademy/perseus"; +import {GrapherUtil as CoreGrapherUtil} from "@khanacademy/perseus-core"; import * as React from "react"; import _ from "underscore"; @@ -16,7 +17,6 @@ const {InfoTip, MultiButtonGroup} = components; const Grapher = GrapherWidget.widget; const { DEFAULT_GRAPHER_PROPS, - allTypes, chooseType, defaultPlotProps, getEquationString, @@ -141,7 +141,7 @@ class GrapherEditor extends React.Component { diff --git a/packages/perseus-editor/src/widgets/input-number-editor.tsx b/packages/perseus-editor/src/widgets/input-number-editor.tsx index af93315324..9095e7f2e7 100644 --- a/packages/perseus-editor/src/widgets/input-number-editor.tsx +++ b/packages/perseus-editor/src/widgets/input-number-editor.tsx @@ -1,4 +1,5 @@ import {components, PerseusI18nContext, Util} from "@khanacademy/perseus"; +import {inputNumberAnswerTypes} from "@khanacademy/perseus-score"; import * as React from "react"; import _ from "underscore"; @@ -9,41 +10,6 @@ import type {PerseusInputNumberWidgetOptions} from "@khanacademy/perseus-core"; const {InfoTip} = components; -const answerTypes = { - number: { - name: "Numbers", - forms: "integer, decimal, proper, improper, mixed", - }, - decimal: { - name: "Decimals", - forms: "decimal", - }, - integer: { - name: "Integers", - forms: "integer", - }, - rational: { - name: "Fractions and mixed numbers", - forms: "integer, proper, improper, mixed", - }, - improper: { - name: "Improper numbers (no mixed)", - forms: "integer, proper, improper", - }, - mixed: { - name: "Mixed numbers (no improper)", - forms: "integer, proper, mixed", - }, - percent: { - name: "Numbers or percents", - forms: "integer, decimal, proper, improper, mixed, percent", - }, - pi: { - name: "Numbers with pi", - forms: "pi", - }, -} as const; - type Props = { value: number; simplify: PerseusInputNumberWidgetOptions["simplify"]; @@ -121,7 +87,7 @@ class InputNumberEditor extends React.Component { render(): React.ReactNode { const answerTypeOptions = _.map( - answerTypes, + inputNumberAnswerTypes, function (v, k) { return ( {capitalized} + {capitalized} ), }; }; diff --git a/packages/perseus/src/widgets/group/score-group.ts b/packages/perseus/src/widgets/group/score-group.ts index 8d4bbff6a0..afe02b1aa4 100644 --- a/packages/perseus/src/widgets/group/score-group.ts +++ b/packages/perseus/src/widgets/group/score-group.ts @@ -2,8 +2,8 @@ import {scoreWidgetsFunctional} from "../../renderer-util"; import {flattenScores} from "../../util/scoring"; import type {PerseusStrings} from "../../strings"; -import type {PerseusScore} from "../../types"; import type { + PerseusScore, PerseusGroupRubric, PerseusGroupUserInput, } from "@khanacademy/perseus-score"; diff --git a/packages/perseus/src/widgets/iframe/iframe.tsx b/packages/perseus/src/widgets/iframe/iframe.tsx index 4ab074b351..3104c2cefe 100644 --- a/packages/perseus/src/widgets/iframe/iframe.tsx +++ b/packages/perseus/src/widgets/iframe/iframe.tsx @@ -7,6 +7,12 @@ * but could also be used for embedding viz's hosted elsewhere. */ +import { + scoreIframe, + type PerseusIFrameRubric, + type PerseusIFrameUserInput, + type UserInputStatus, +} from "@khanacademy/perseus-score"; import $ from "jquery"; import * as React from "react"; import _ from "underscore"; @@ -16,16 +22,9 @@ import * as Changeable from "../../mixins/changeable"; import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/iframe/iframe-ai-utils"; -import {scoreIframe} from "./score-iframe"; - import type {WidgetExports, WidgetProps, Widget} from "../../types"; import type {UnsupportedWidgetPromptJSON} from "../../widget-ai-utils/unsupported-widget"; import type {PerseusIFrameWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusIFrameRubric, - PerseusIFrameUserInput, - UserInputStatus, -} from "@khanacademy/perseus-score"; const {updateQueryString} = Util; diff --git a/packages/perseus/src/widgets/input-number/input-number.test.ts b/packages/perseus/src/widgets/input-number/input-number.test.ts index 420b35a71a..5dc4b9500e 100644 --- a/packages/perseus/src/widgets/input-number/input-number.test.ts +++ b/packages/perseus/src/widgets/input-number/input-number.test.ts @@ -8,28 +8,17 @@ import _ from "underscore"; import {testDependencies} from "../../../../../testing/test-dependencies"; import * as Dependencies from "../../dependencies"; -import {mockStrings} from "../../strings"; import {scorePerseusItemTesting} from "../../util/test-utils"; import {renderQuestion} from "../__testutils__/renderQuestion"; import InputNumber from "./input-number"; import {question3 as question} from "./input-number.testdata"; -import scoreInputNumber from "./score-input-number"; -import type { - PerseusInputNumberWidgetOptions, - PerseusRenderer, -} from "@khanacademy/perseus-core"; +import type {PerseusRenderer} from "@khanacademy/perseus-core"; import type {UserEvent} from "@testing-library/user-event"; const {transform} = InputNumber; -const options: PerseusInputNumberWidgetOptions = { - value: "2^{-2}-3", - size: "normal", - simplify: "optional", -}; - describe("input-number", function () { let userEvent: UserEvent; beforeEach(() => { @@ -263,26 +252,6 @@ describe("input-number", function () { }); }); -describe("invalid", function () { - beforeEach(() => { - jest.spyOn(Dependencies, "getDependencies").mockReturnValue( - testDependencies, - ); - }); - - it("should handle invalid answers with no error callback", function () { - const err = scoreInputNumber( - {currentValue: "x+1"}, - options, - mockStrings, - ); - expect(err).toEqual({ - message: "EXTRA_SYMBOLS_ERROR", - type: "invalid", - }); - }); -}); - describe("getOneCorrectAnswerFromRubric", () => { beforeEach(() => { jest.spyOn(Dependencies, "getDependencies").mockReturnValue( diff --git a/packages/perseus/src/widgets/input-number/input-number.tsx b/packages/perseus/src/widgets/input-number/input-number.tsx index e166de6b1c..bcb0af99fd 100644 --- a/packages/perseus/src/widgets/input-number/input-number.tsx +++ b/packages/perseus/src/widgets/input-number/input-number.tsx @@ -1,4 +1,10 @@ import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + inputNumberAnswerTypes, + scoreInputNumber, + type PerseusInputNumberRubric, + type PerseusInputNumberUserInput, +} from "@khanacademy/perseus-score"; import {spacing} from "@khanacademy/wonder-blocks-tokens"; import {StyleSheet} from "aphrodite"; import * as React from "react"; @@ -10,16 +16,10 @@ import SimpleKeypadInput from "../../components/simple-keypad-input"; import {ApiOptions} from "../../perseus-api"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/input-number/input-number-ai-utils"; -import scoreInputNumber, {answerTypes} from "./score-input-number"; - import type {PerseusStrings} from "../../strings"; import type {Path, Widget, WidgetExports, WidgetProps} from "../../types"; import type {InputNumberPromptJSON} from "../../widget-ai-utils/input-number/input-number-ai-utils"; import type {PerseusInputNumberWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusInputNumberRubric, - PerseusInputNumberUserInput, -} from "@khanacademy/perseus-score"; const formExamples = { integer: function (options, strings: PerseusStrings) { @@ -178,7 +178,7 @@ class InputNumber extends React.Component implements Widget { examples(): ReadonlyArray { const {strings} = this.context; const type = this.props.answerType; - const forms = answerTypes[type].forms.split(/\s*,\s*/); + const forms = inputNumberAnswerTypes[type].forms.split(/\s*,\s*/); const examples = _.map(forms, (form) => formExamples[form](this.props, strings), 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 { diff --git a/packages/perseus/src/widgets/label-image/label-image.tsx b/packages/perseus/src/widgets/label-image/label-image.tsx index 15580dae08..527a08c317 100644 --- a/packages/perseus/src/widgets/label-image/label-image.tsx +++ b/packages/perseus/src/widgets/label-image/label-image.tsx @@ -6,6 +6,12 @@ * knowledge by directly interacting with the image. */ +import { + scoreLabelImageMarker, + scoreLabelImage, + type PerseusLabelImageRubric, + type PerseusLabelImageUserInput, +} from "@khanacademy/perseus-score"; import Clickable from "@khanacademy/wonder-blocks-clickable"; import {View} from "@khanacademy/wonder-blocks-core"; import {StyleSheet, css} from "aphrodite"; @@ -25,7 +31,6 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/label-image import AnswerChoices from "./answer-choices"; import {HideAnswersToggle} from "./hide-answers-toggle"; import Marker from "./marker"; -import scoreLabelImage, {scoreMarker} from "./score-label-image"; import type {DependencyProps} from "../../dependencies"; import type {ChangeableProps} from "../../mixins/changeable"; @@ -35,10 +40,6 @@ import type { InteractiveMarkerType, PerseusLabelImageWidgetOptions, } from "@khanacademy/perseus-core"; -import type { - PerseusLabelImageRubric, - PerseusLabelImageUserInput, -} from "@khanacademy/perseus-score"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; import type {CSSProperties} from "aphrodite"; @@ -326,7 +327,7 @@ export class LabelImage const {onChange} = this.props; const updatedMarkers = markers.map((marker) => { - const score = scoreMarker(marker); + const score = scoreLabelImageMarker(marker); return { ...marker, @@ -478,7 +479,7 @@ export class LabelImage }[markerPosition]; } - const score = scoreMarker(marker); + const score = scoreLabelImageMarker(marker); // Once the question is answered, show markers // with correct answers, otherwise passthrough // the correctness state. diff --git a/packages/perseus/src/widgets/matcher/matcher.tsx b/packages/perseus/src/widgets/matcher/matcher.tsx index cf28fd5cf6..740ca9c7ff 100644 --- a/packages/perseus/src/widgets/matcher/matcher.tsx +++ b/packages/perseus/src/widgets/matcher/matcher.tsx @@ -1,4 +1,9 @@ import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreMatcher, + type PerseusMatcherRubric, + type PerseusMatcherUserInput, +} from "@khanacademy/perseus-score"; import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner"; import {StyleSheet, css} from "aphrodite"; import * as React from "react"; @@ -11,16 +16,10 @@ import Renderer from "../../renderer"; import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/matcher/matcher-ai-utils"; -import scoreMatcher from "./score-matcher"; - import type {SortableOption} from "../../components/sortable"; import type {WidgetExports, WidgetProps, Widget} from "../../types"; import type {MatcherPromptJSON} from "../../widget-ai-utils/matcher/matcher-ai-utils"; import type {PerseusMatcherWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusMatcherRubric, - PerseusMatcherUserInput, -} from "@khanacademy/perseus-score"; const {shuffle, seededRNG} = Util; const HACKY_CSS_CLASSNAME = "perseus-widget-matcher"; diff --git a/packages/perseus/src/widgets/matrix/matrix.tsx b/packages/perseus/src/widgets/matrix/matrix.tsx index 036dd4140a..6a55a0ee88 100644 --- a/packages/perseus/src/widgets/matrix/matrix.tsx +++ b/packages/perseus/src/widgets/matrix/matrix.tsx @@ -1,4 +1,14 @@ +import { + getMatrixSize, + type PerseusMatrixWidgetAnswers, + type PerseusMatrixWidgetOptions, +} from "@khanacademy/perseus-core"; import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreMatrix, + type PerseusMatrixRubric, + type PerseusMatrixUserInput, +} from "@khanacademy/perseus-score"; import {StyleSheet} from "aphrodite"; import classNames from "classnames"; import * as React from "react"; @@ -15,18 +25,8 @@ import Renderer from "../../renderer"; import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/matrix/matrix-ai-utils"; -import scoreMatrix from "./score-matrix"; - import type {WidgetExports, WidgetProps, Widget, FocusPath} from "../../types"; import type {MatrixPromptJSON} from "../../widget-ai-utils/matrix/matrix-ai-utils"; -import type { - PerseusMatrixWidgetAnswers, - PerseusMatrixWidgetOptions, -} from "@khanacademy/perseus-core"; -import type { - PerseusMatrixRubric, - PerseusMatrixUserInput, -} from "@khanacademy/perseus-score"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const {assert} = InteractiveUtil; @@ -79,30 +79,6 @@ const getRefForPath = function (path: FocusPath) { return "answer" + row + "," + column; }; -export function getMatrixSize(matrix: ReadonlyArray>) { - const matrixSize = [1, 1]; - - // We need to find the widest row and tallest column to get the correct - // matrix size. - _(matrix).each((matrixRow, row) => { - let rowWidth = 0; - _(matrixRow).each((matrixCol, col) => { - if (matrixCol != null && matrixCol.toString().length) { - rowWidth = col + 1; - } - }); - - // Matrix width: - matrixSize[1] = Math.max(matrixSize[1], rowWidth); - - // Matrix height: - if (rowWidth > 0) { - matrixSize[0] = Math.max(matrixSize[0], row + 1); - } - }); - return matrixSize; -} - type ExternalProps = WidgetProps< { // Translatable Text; Shown before the matrix diff --git a/packages/perseus/src/widgets/mock-widgets/score-mock-widget.ts b/packages/perseus/src/widgets/mock-widgets/score-mock-widget.ts index 63d4c5b66a..defb522db6 100644 --- a/packages/perseus/src/widgets/mock-widgets/score-mock-widget.ts +++ b/packages/perseus/src/widgets/mock-widgets/score-mock-widget.ts @@ -1,10 +1,10 @@ import {KhanAnswerTypes} from "@khanacademy/perseus-score"; import type {PerseusStrings} from "../../strings"; -import type {PerseusScore} from "@khanacademy/perseus"; import type { PerseusMockWidgetUserInput, PerseusMockWidgetRubric, + PerseusScore, } from "@khanacademy/perseus-score"; function scoreMockWidget( diff --git a/packages/perseus/src/widgets/number-line/number-line.tsx b/packages/perseus/src/widgets/number-line/number-line.tsx index 5e707c1adb..bb7ef6807d 100644 --- a/packages/perseus/src/widgets/number-line/number-line.tsx +++ b/packages/perseus/src/widgets/number-line/number-line.tsx @@ -1,4 +1,8 @@ import {number as knumber, KhanMath} from "@khanacademy/kmath"; +import { + scoreNumberLine, + type PerseusNumberLineUserInput, +} from "@khanacademy/perseus-score"; import * as React from "react"; import ReactDOM from "react-dom"; import _ from "underscore"; @@ -13,13 +17,10 @@ import {ApiOptions} from "../../perseus-api"; import KhanColors from "../../util/colors"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/number-line/number-line-ai-utils"; -import scoreNumberLine from "./score-number-line"; - import type {ChangeableProps} from "../../mixins/changeable"; import type {APIOptions, WidgetExports, FocusPath, Widget} from "../../types"; import type {NumberLinePromptJSON} from "../../widget-ai-utils/number-line/number-line-ai-utils"; import type {Relationship} from "@khanacademy/perseus-core"; -import type {PerseusNumberLineUserInput} from "@khanacademy/perseus-score"; // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'. const MovablePoint = Graphie.MovablePoint; diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx index 4935691594..c30d110efa 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx @@ -1,5 +1,10 @@ import {KhanMath} from "@khanacademy/kmath"; import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreNumericInput, + type PerseusNumericInputRubric, + type PerseusNumericInputUserInput, +} from "@khanacademy/perseus-score"; import {StyleSheet} from "aphrodite"; import * as React from "react"; import _ from "underscore"; @@ -10,8 +15,6 @@ import SimpleKeypadInput from "../../components/simple-keypad-input"; import {ApiOptions} from "../../perseus-api"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/numeric-input/prompt-utils"; -import scoreNumericInput from "./score-numeric-input"; - import type {PerseusStrings} from "../../strings"; import type {FocusPath, Widget, WidgetExports, WidgetProps} from "../../types"; import type {NumericInputPromptJSON} from "../../widget-ai-utils/numeric-input/prompt-utils"; @@ -19,10 +22,6 @@ import type { PerseusNumericInputWidgetOptions, PerseusNumericInputAnswerForm, } from "@khanacademy/perseus-core"; -import type { - PerseusNumericInputRubric, - PerseusNumericInputUserInput, -} from "@khanacademy/perseus-score"; import type {PropsFor} from "@khanacademy/wonder-blocks-core"; const formExamples: { diff --git a/packages/perseus/src/widgets/orderer/orderer.tsx b/packages/perseus/src/widgets/orderer/orderer.tsx index d3bb28bdc3..faa41334e6 100644 --- a/packages/perseus/src/widgets/orderer/orderer.tsx +++ b/packages/perseus/src/widgets/orderer/orderer.tsx @@ -2,6 +2,11 @@ /* eslint-disable @babel/no-invalid-this, react/no-unsafe */ import {Errors} from "@khanacademy/perseus-core"; import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreOrderer, + type PerseusOrdererRubric, + type PerseusOrdererUserInput, +} from "@khanacademy/perseus-score"; import $ from "jquery"; import * as React from "react"; import ReactDOM from "react-dom"; @@ -14,16 +19,10 @@ import Renderer from "../../renderer"; import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/orderer/orderer-ai-utils"; -import {scoreOrderer} from "./score-orderer"; - import type {WidgetExports, WidgetProps, Widget} from "../../types"; import type {OrdererPromptJSON} from "../../widget-ai-utils/orderer/orderer-ai-utils"; import type {PerseusOrdererWidgetOptions} from "@khanacademy/perseus-core"; import type {LinterContextProps} from "@khanacademy/perseus-linter"; -import type { - PerseusOrdererRubric, - PerseusOrdererUserInput, -} from "@khanacademy/perseus-score"; type PlaceholderCardProps = { width: number | null | undefined; diff --git a/packages/perseus/src/widgets/plotter/plotter.tsx b/packages/perseus/src/widgets/plotter/plotter.tsx index 21ab30a1cb..3eb871496b 100644 --- a/packages/perseus/src/widgets/plotter/plotter.tsx +++ b/packages/perseus/src/widgets/plotter/plotter.tsx @@ -1,5 +1,10 @@ /* eslint-disable react/no-unsafe */ import {KhanMath} from "@khanacademy/kmath"; +import { + scorePlotter, + type PerseusPlotterScoringData, + type PerseusPlotterUserInput, +} from "@khanacademy/perseus-score"; import $ from "jquery"; import * as React from "react"; import ReactDOM from "react-dom"; @@ -13,15 +18,9 @@ import KhanColors from "../../util/colors"; import GraphUtils from "../../util/graph-utils"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/plotter/plotter-ai-utils"; -import scorePlotter from "./score-plotter"; - import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type {UnsupportedWidgetPromptJSON} from "../../widget-ai-utils/unsupported-widget"; import type {PerseusPlotterWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusPlotterScoringData, - PerseusPlotterUserInput, -} from "@khanacademy/perseus-score"; type RenderProps = PerseusPlotterWidgetOptions; diff --git a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts index a661f15d03..a3410225b9 100644 --- a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts +++ b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts @@ -1,16 +1,18 @@ import {describe, beforeEach, it} from "@jest/globals"; +import { + scoreRadio, + type PerseusRadioUserInput, +} from "@khanacademy/perseus-score"; import {act, screen, fireEvent, waitFor} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; import {clone} from "../../../../../../testing/object-utils"; import {testDependencies} from "../../../../../../testing/test-dependencies"; import * as Dependencies from "../../../dependencies"; -import {mockStrings} from "../../../strings"; import {scorePerseusItemTesting} from "../../../util/test-utils"; import {renderQuestion} from "../../__testutils__/renderQuestion"; import PassageWidget from "../../passage"; import RadioWidgetExport from "../radio"; -import scoreRadio from "../score-radio"; import { questionAndAnswer, @@ -24,7 +26,6 @@ import type { PerseusRadioWidgetOptions, PerseusRenderer, } from "@khanacademy/perseus-core"; -import type {PerseusRadioUserInput} from "@khanacademy/perseus-score"; import type {UserEvent} from "@testing-library/user-event"; const selectOption = async ( @@ -901,9 +902,7 @@ describe("Radio Widget", () => { ); // Assert - expect(score).toHaveInvalidInput( - "Please choose the correct number of answers", - ); + expect(score).toHaveInvalidInput("CHOOSE_CORRECT_NUM_ERROR"); }); it.each(incorrect)( @@ -967,7 +966,7 @@ describe("Radio Widget", () => { const userInput = renderer.getUserInput()[0] as PerseusRadioUserInput; const rubric = shuffledQuestion.widgets["radio 1"].options; - const widgetScore = scoreRadio(userInput, rubric, mockStrings); + const widgetScore = scoreRadio(userInput, rubric); const rendererScore = scorePerseusItemTesting( shuffledQuestion, renderer.getUserInputMap(), @@ -995,7 +994,7 @@ describe("Radio Widget", () => { const userInput = renderer.getUserInput()[0] as PerseusRadioUserInput; const rubric = shuffledQuestion.widgets["radio 1"].options; - const widgetScore = scoreRadio(userInput, rubric, mockStrings); + const widgetScore = scoreRadio(userInput, rubric); const rendererScore = scorePerseusItemTesting( shuffledQuestion, renderer.getUserInputMap(), @@ -1023,7 +1022,7 @@ describe("Radio Widget", () => { const userInput = renderer.getUserInput()[0] as PerseusRadioUserInput; const rubric = shuffledNoneQuestion.widgets["radio 1"].options; - const widgetScore = scoreRadio(userInput, rubric, mockStrings); + const widgetScore = scoreRadio(userInput, rubric); const rendererScore = scorePerseusItemTesting( shuffledNoneQuestion, renderer.getUserInputMap(), @@ -1051,7 +1050,7 @@ describe("Radio Widget", () => { const userInput = renderer.getUserInput()[0] as PerseusRadioUserInput; const rubric = shuffledNoneQuestion.widgets["radio 1"].options; - const widgetScore = scoreRadio(userInput, rubric, mockStrings); + const widgetScore = scoreRadio(userInput, rubric); const rendererScore = scorePerseusItemTesting( shuffledQuestion, renderer.getUserInputMap(), diff --git a/packages/perseus/src/widgets/radio/radio-component.tsx b/packages/perseus/src/widgets/radio/radio-component.tsx index 3d081bc7ec..501b08f993 100644 --- a/packages/perseus/src/widgets/radio/radio-component.tsx +++ b/packages/perseus/src/widgets/radio/radio-component.tsx @@ -1,4 +1,9 @@ import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreRadio, + type PerseusRadioRubric, + type PerseusRadioUserInput, +} from "@khanacademy/perseus-score"; import * as React from "react"; import {PerseusI18nContext} from "../../components/i18n-context"; @@ -8,7 +13,6 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/radio/radio import PassageRef from "../passage-ref/passage-ref"; import BaseRadio from "./base-radio"; -import scoreRadio from "./score-radio"; import type {FocusFunction, ChoiceType} from "./base-radio"; import type {WidgetProps, ChoiceState, Widget} from "../../types"; @@ -18,10 +22,6 @@ import type { PerseusRadioWidgetOptions, ShowSolutions, } from "@khanacademy/perseus-core"; -import type { - PerseusRadioRubric, - PerseusRadioUserInput, -} from "@khanacademy/perseus-score"; // RenderProps is the return type for radio.jsx#transform export type RenderProps = { @@ -271,11 +271,7 @@ class Radio extends React.Component implements Widget { ) => void = (rubric) => { const {choiceStates} = this.props; if (choiceStates) { - const score = scoreRadio( - this.getUserInput(), - rubric, - this.context.strings, - ); + const score = scoreRadio(this.getUserInput(), rubric); const widgetCorrect = score.type === "points" && score.total === score.earned; diff --git a/packages/perseus/src/widgets/radio/radio.ts b/packages/perseus/src/widgets/radio/radio.ts index 25778f91a5..c832796a7b 100644 --- a/packages/perseus/src/widgets/radio/radio.ts +++ b/packages/perseus/src/widgets/radio/radio.ts @@ -1,9 +1,9 @@ +import {scoreRadio} from "@khanacademy/perseus-score"; import _ from "underscore"; import Util from "../../util"; import Radio from "./radio-component"; -import scoreRadio from "./score-radio"; import type {RenderProps, RadioChoiceWithMetadata} from "./radio-component"; import type {PerseusStrings} from "../../strings"; diff --git a/packages/perseus/src/widgets/sorter/sorter.tsx b/packages/perseus/src/widgets/sorter/sorter.tsx index 9c9931902f..baacc2201b 100644 --- a/packages/perseus/src/widgets/sorter/sorter.tsx +++ b/packages/perseus/src/widgets/sorter/sorter.tsx @@ -1,20 +1,19 @@ import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreSorter, + type PerseusSorterRubric, + type PerseusSorterUserInput, +} from "@khanacademy/perseus-score"; import * as React from "react"; import Sortable from "../../components/sortable"; import Util from "../../util"; import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/sorter/sorter-ai-utils"; -import scoreSorter from "./score-sorter"; - import type {SortableOption} from "../../components/sortable"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type {SorterPromptJSON} from "../../widget-ai-utils/sorter/sorter-ai-utils"; import type {PerseusSorterWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusSorterRubric, - PerseusSorterUserInput, -} from "@khanacademy/perseus-score"; const {shuffle} = Util; diff --git a/packages/perseus/src/widgets/table/table.tsx b/packages/perseus/src/widgets/table/table.tsx index 2ad7f90786..44c25afef3 100644 --- a/packages/perseus/src/widgets/table/table.tsx +++ b/packages/perseus/src/widgets/table/table.tsx @@ -1,4 +1,9 @@ import {linterContextDefault} from "@khanacademy/perseus-linter"; +import { + scoreTable, + type PerseusTableRubric, + type PerseusTableUserInput, +} from "@khanacademy/perseus-score"; import * as React from "react"; import ReactDOM from "react-dom"; import _ from "underscore"; @@ -10,15 +15,9 @@ import {ApiOptions} from "../../perseus-api"; import Renderer from "../../renderer"; import Util from "../../util"; -import scoreTable from "./score-table"; - import type {ChangeableProps} from "../../mixins/changeable"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type {PerseusTableWidgetOptions} from "@khanacademy/perseus-core"; -import type { - PerseusTableRubric, - PerseusTableUserInput, -} from "@khanacademy/perseus-score"; const {assert} = InteractiveUtil;