@khanacademy/perseus 50.0.0 → 51.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/es/index.js +597 -2403
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +58 -11
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +3 -8
  6. package/dist/index.js +537 -2320
  7. package/dist/index.js.map +1 -1
  8. package/dist/renderer-util.d.ts +1 -2
  9. package/dist/renderer.d.ts +2 -2
  10. package/dist/server-item-renderer.d.ts +1 -1
  11. package/dist/strings.d.ts +204 -13
  12. package/dist/strings.js +75 -10
  13. package/dist/strings.js.map +1 -1
  14. package/dist/types.d.ts +11 -14
  15. package/dist/util/scoring.d.ts +1 -2
  16. package/dist/util/test-utils.d.ts +1 -2
  17. package/dist/util.d.ts +0 -14
  18. package/dist/widget-ai-utils/categorizer/categorizer-ai-utils.d.ts +1 -1
  19. package/dist/widget-ai-utils/dropdown/dropdown-ai-utils.d.ts +1 -1
  20. package/dist/widget-ai-utils/expression/expression-ai-utils.d.ts +1 -1
  21. package/dist/widget-ai-utils/grapher/grapher-ai-utils.d.ts +1 -1
  22. package/dist/widget-ai-utils/input-number/input-number-ai-utils.d.ts +1 -1
  23. package/dist/widget-ai-utils/label-image/label-image-ai-utils.d.ts +1 -1
  24. package/dist/widget-ai-utils/matcher/matcher-ai-utils.d.ts +1 -1
  25. package/dist/widget-ai-utils/matrix/matrix-ai-utils.d.ts +1 -1
  26. package/dist/widget-ai-utils/mock-widget/prompt-utils.d.ts +1 -1
  27. package/dist/widget-ai-utils/number-line/number-line-ai-utils.d.ts +1 -1
  28. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +1 -1
  29. package/dist/widget-ai-utils/orderer/orderer-ai-utils.d.ts +1 -1
  30. package/dist/widget-ai-utils/radio/radio-ai-utils.d.ts +1 -1
  31. package/dist/widget-ai-utils/sorter/sorter-ai-utils.d.ts +1 -1
  32. package/dist/widgets/__shared__/score-noop.d.ts +1 -1
  33. package/dist/widgets/categorizer/categorizer.d.ts +4 -2
  34. package/dist/widgets/categorizer/categorizer.util.d.ts +17 -0
  35. package/dist/widgets/cs-program/cs-program.d.ts +1 -2
  36. package/dist/widgets/definition/definition.d.ts +1 -1
  37. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
  38. package/dist/widgets/dropdown/dropdown.d.ts +2 -2
  39. package/dist/widgets/explanation/explanation.d.ts +1 -1
  40. package/dist/widgets/expression/expression.d.ts +5 -6
  41. package/dist/widgets/graded-group/graded-group.d.ts +1 -1
  42. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
  43. package/dist/widgets/grapher/grapher.d.ts +1 -2
  44. package/dist/widgets/grapher/util.d.ts +5 -13
  45. package/dist/widgets/group/group.d.ts +2 -2
  46. package/dist/widgets/group/score-group.d.ts +1 -2
  47. package/dist/widgets/iframe/iframe.d.ts +1 -2
  48. package/dist/widgets/image/image.d.ts +1 -1
  49. package/dist/widgets/input-number/input-number.d.ts +1 -2
  50. package/dist/widgets/interaction/interaction.d.ts +1 -1
  51. package/dist/widgets/interactive-graph.d.ts +2 -3
  52. package/dist/widgets/interactive-graphs/graphs/linear-system.d.ts +2 -0
  53. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -3
  54. package/dist/widgets/interactive-graphs/graphs/ray.d.ts +2 -0
  55. package/dist/widgets/interactive-graphs/graphs/sinusoid.d.ts +4 -9
  56. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +4 -0
  57. package/dist/widgets/interactive-graphs/math/index.d.ts +0 -1
  58. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +1 -1
  59. package/dist/widgets/interactive-graphs/types.d.ts +2 -1
  60. package/dist/widgets/label-image/label-image.d.ts +2 -4
  61. package/dist/widgets/label-image/marker.d.ts +1 -2
  62. package/dist/widgets/matcher/matcher.d.ts +1 -2
  63. package/dist/widgets/matrix/matrix.d.ts +2 -4
  64. package/dist/widgets/measurer/measurer.d.ts +1 -1
  65. package/dist/widgets/mock-widgets/mock-widget.d.ts +1 -1
  66. package/dist/widgets/mock-widgets/score-mock-widget.d.ts +1 -2
  67. package/dist/widgets/molecule/molecule.d.ts +1 -1
  68. package/dist/widgets/number-line/number-line.d.ts +2 -3
  69. package/dist/widgets/numeric-input/numeric-input.d.ts +1 -2
  70. package/dist/widgets/orderer/orderer.d.ts +1 -2
  71. package/dist/widgets/passage/passage.d.ts +1 -1
  72. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  73. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +1 -1
  74. package/dist/widgets/plotter/plotter.d.ts +1 -2
  75. package/dist/widgets/radio/radio-component.d.ts +1 -1
  76. package/dist/widgets/radio/radio.d.ts +1 -1
  77. package/dist/widgets/sorter/sorter.d.ts +1 -2
  78. package/dist/widgets/table/table.d.ts +1 -2
  79. package/dist/widgets/video/video.d.ts +1 -1
  80. package/dist/widgets.d.ts +2 -1
  81. package/package.json +11 -11
  82. package/dist/tex-wrangler.d.ts +0 -7
  83. package/dist/util/geometry.d.ts +0 -28
  84. package/dist/validation.types.d.ts +0 -166
  85. package/dist/widgets/categorizer/score-categorizer.d.ts +0 -5
  86. package/dist/widgets/categorizer/validate-categorizer.d.ts +0 -13
  87. package/dist/widgets/cs-program/score-cs-program.d.ts +0 -4
  88. package/dist/widgets/dropdown/score-dropdown.d.ts +0 -4
  89. package/dist/widgets/dropdown/validate-dropdown.d.ts +0 -8
  90. package/dist/widgets/expression/get-decimal-separator.d.ts +0 -5
  91. package/dist/widgets/expression/score-expression.d.ts +0 -5
  92. package/dist/widgets/expression/validate-expression.d.ts +0 -12
  93. package/dist/widgets/grapher/grapher-types.d.ts +0 -58
  94. package/dist/widgets/grapher/score-grapher.d.ts +0 -4
  95. package/dist/widgets/iframe/score-iframe.d.ts +0 -3
  96. package/dist/widgets/input-number/score-input-number.d.ts +0 -39
  97. package/dist/widgets/interactive-graphs/math/angles.d.ts +0 -7
  98. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +0 -4
  99. package/dist/widgets/label-image/score-label-image.d.ts +0 -10
  100. package/dist/widgets/label-image/types.d.ts +0 -11
  101. package/dist/widgets/matcher/score-matcher.d.ts +0 -4
  102. package/dist/widgets/matrix/score-matrix.d.ts +0 -5
  103. package/dist/widgets/matrix/validate-matrix.d.ts +0 -13
  104. package/dist/widgets/number-line/score-number-line.d.ts +0 -4
  105. package/dist/widgets/number-line/validate-number-line.d.ts +0 -12
  106. package/dist/widgets/numeric-input/score-numeric-input.d.ts +0 -6
  107. package/dist/widgets/orderer/score-orderer.d.ts +0 -3
  108. package/dist/widgets/orderer/validate-orderer.d.ts +0 -10
  109. package/dist/widgets/plotter/score-plotter.d.ts +0 -4
  110. package/dist/widgets/plotter/validate-plotter.d.ts +0 -10
  111. package/dist/widgets/radio/score-radio.d.ts +0 -5
  112. package/dist/widgets/radio/validate-radio.d.ts +0 -12
  113. package/dist/widgets/sorter/score-sorter.d.ts +0 -4
  114. package/dist/widgets/sorter/validate-sorter.d.ts +0 -11
  115. package/dist/widgets/table/score-table.d.ts +0 -5
  116. package/dist/widgets/table/utils.d.ts +0 -7
  117. package/dist/widgets/table/validate-table.d.ts +0 -4
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var KAS = require('@khanacademy/kas');
6
6
  var mathInput = require('@khanacademy/math-input');
7
+ var perseusCore = require('@khanacademy/perseus-core');
7
8
  var PerseusLinter = require('@khanacademy/perseus-linter');
9
+ var perseusScore = require('@khanacademy/perseus-score');
8
10
  var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
9
11
  var Tooltip$1 = require('@khanacademy/wonder-blocks-tooltip');
10
12
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
@@ -13,13 +15,11 @@ var classNames$1 = require('classnames');
13
15
  var React = require('react');
14
16
  var ReactDOM = require('react-dom');
15
17
  var _ = require('underscore');
16
- var perseusScore = require('@khanacademy/perseus-score');
17
18
  var Clickable = require('@khanacademy/wonder-blocks-clickable');
18
19
  var wonderBlocksPopover = require('@khanacademy/wonder-blocks-popover');
19
20
  var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
20
21
  var $ = require('jquery');
21
22
  var uuid = require('uuid');
22
- var perseusCore = require('@khanacademy/perseus-core');
23
23
  var PropTypes = require('prop-types');
24
24
  var wonderStuffCore = require('@khanacademy/wonder-stuff-core');
25
25
  var wonderBlocksProgressSpinner = require('@khanacademy/wonder-blocks-progress-spinner');
@@ -282,7 +282,15 @@ const strings = {
282
282
  srAngleSideAtCoordinates: "Point %(point)s, %(side)s at %(x)s comma %(y)s",
283
283
  srAngleVertexAtCoordinatesWithAngleMeasure: "Point 2, vertex at %(x)s comma %(y)s. Angle %(angleMeasure)s degrees",
284
284
  srAngleGraphAriaLabel: "An angle on a coordinate plane.",
285
- srAngleGraphAriaDescription: "The angle measure is %(angleMeasure)s degrees with a vertex at %(vertexX)s comma %(vertexY)s, a point on the starting side at %(startingSideX)s comma %(startingSideY)s and a point on the ending side at %(endingSideX)s comma %(endingSideY)s"
285
+ srAngleGraphAriaDescription: "The angle measure is %(angleMeasure)s degrees with a vertex at %(vertexX)s comma %(vertexY)s, a point on the starting side at %(startingSideX)s comma %(startingSideY)s and a point on the ending side at %(endingSideX)s comma %(endingSideY)s",
286
+ srLinearSystemGraph: "Two lines on a coordinate plane.",
287
+ srLinearSystemPoints: "Line %(lineNumber)s has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
288
+ srLinearSystemPoint: "Point %(pointSequence)s on line %(lineNumber)s at %(x)s comma %(y)s.",
289
+ srRayGraph: "A ray on a coordinate plane.",
290
+ srRayPoints: "The endpoint is at %(point1X)s comma %(point1Y)s and the ray goes through point %(point2X)s comma %(point2Y)s.",
291
+ srRayGrabHandle: "Ray with endpoint %(point1X)s comma %(point1Y)s going through point %(point2X)s comma %(point2Y)s.",
292
+ srRayEndpoint: "Endpoint at %(point1X)s comma %(point1Y)s.",
293
+ srRayTerminalPoint: "Through point at %(point2X)s comma %(point2Y)s."
286
294
  // The above strings are used for interactive graph SR descriptions.
287
295
  };
288
296
 
@@ -671,19 +679,84 @@ const mockStrings = {
671
679
  endingSideY
672
680
  } = _ref40;
673
681
  return `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the starting side at ${startingSideX} comma ${startingSideY} and a point on the ending side at ${endingSideX} comma ${endingSideY}.`;
682
+ },
683
+ srLinearSystemGraph: "Two lines on a coordinate plane.",
684
+ srLinearSystemPoints: _ref41 => {
685
+ let {
686
+ lineNumber,
687
+ point1X,
688
+ point1Y,
689
+ point2X,
690
+ point2Y
691
+ } = _ref41;
692
+ return `Line ${lineNumber} has two points, point 1 at ${point1X} comma ${point1Y} and point 2 at ${point2X} comma ${point2Y}.`;
693
+ },
694
+ srLinearSystemPoint: _ref42 => {
695
+ let {
696
+ lineNumber,
697
+ pointSequence,
698
+ x,
699
+ y
700
+ } = _ref42;
701
+ return `Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`;
702
+ },
703
+ srRayGraph: "A ray on a coordinate plane.",
704
+ srRayPoints: _ref43 => {
705
+ let {
706
+ point1X,
707
+ point1Y,
708
+ point2X,
709
+ point2Y
710
+ } = _ref43;
711
+ return `The endpoint is at ${point1X} comma ${point1Y} and the ray goes through point ${point2X} comma ${point2Y}.`;
712
+ },
713
+ srRayGrabHandle: _ref44 => {
714
+ let {
715
+ point1X,
716
+ point1Y,
717
+ point2X,
718
+ point2Y
719
+ } = _ref44;
720
+ return `Ray with endpoint ${point1X} comma ${point1Y} going through point ${point2X} comma ${point2Y}.`;
721
+ },
722
+ srRayEndpoint: _ref45 => {
723
+ let {
724
+ x,
725
+ y
726
+ } = _ref45;
727
+ return `Endpoint at ${x} comma ${y}.`;
728
+ },
729
+ srRayTerminalPoint: _ref46 => {
730
+ let {
731
+ x,
732
+ y
733
+ } = _ref46;
734
+ return `Through point at ${x} comma ${y}.`;
674
735
  }
675
736
  // The above strings are used for interactive graph SR descriptions.
676
737
  };
677
738
 
678
- ({
679
- [perseusScore.ErrorCodes.MISSING_PERCENT_ERROR]: strings.MISSING_PERCENT_ERROR,
680
- [perseusScore.ErrorCodes.NEEDS_TO_BE_SIMPLIFIED_ERROR]: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
681
- [perseusScore.ErrorCodes.APPROXIMATED_PI_ERROR]: strings.APPROXIMATED_PI_ERROR,
682
- [perseusScore.ErrorCodes.EXTRA_SYMBOLS_ERROR]: strings.EXTRA_SYMBOLS_ERROR,
683
- [perseusScore.ErrorCodes.WRONG_CASE_ERROR]: strings.WRONG_CASE_ERROR,
684
- [perseusScore.ErrorCodes.WRONG_LETTER_ERROR]: strings.WRONG_LETTER_ERROR,
685
- [perseusScore.ErrorCodes.MULTIPLICATION_SIGN_ERROR]: strings.MULTIPLICATION_SIGN_ERROR
686
- });
739
+ // This type helps us make sure all error codes are mapped to strings
740
+
741
+ const errorToString = {
742
+ MISSING_PERCENT_ERROR: strings.MISSING_PERCENT_ERROR,
743
+ NEEDS_TO_BE_SIMPLIFIED_ERROR: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
744
+ APPROXIMATED_PI_ERROR: strings.APPROXIMATED_PI_ERROR,
745
+ EXTRA_SYMBOLS_ERROR: strings.EXTRA_SYMBOLS_ERROR,
746
+ WRONG_CASE_ERROR: strings.WRONG_CASE_ERROR,
747
+ WRONG_LETTER_ERROR: strings.WRONG_LETTER_ERROR,
748
+ MULTIPLICATION_SIGN_ERROR: strings.MULTIPLICATION_SIGN_ERROR,
749
+ INVALID_SELECTION_ERROR: strings.invalidSelection,
750
+ CHOOSE_CORRECT_NUM_ERROR: strings.chooseCorrectNum,
751
+ NOT_NONE_ABOVE_ERROR: strings.notNoneOfTheAbove,
752
+ FILL_ALL_CELLS_ERROR: strings.fillAllCells
753
+ };
754
+ function mapErrorToString(err) {
755
+ if (!err) {
756
+ return err;
757
+ }
758
+ return errorToString[err] || err;
759
+ }
687
760
 
688
761
  /**
689
762
  * PerseusI18nContext provides a way to set the strings and locale that are
@@ -1610,204 +1683,6 @@ const getPromptJSON$t = (renderProps, userInput) => {
1610
1683
  };
1611
1684
  };
1612
1685
 
1613
- /**
1614
- * Get the character used for separating decimals.
1615
- */
1616
- const getDecimalSeparator = locale => {
1617
- switch (locale) {
1618
- // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
1619
- // locale properly.
1620
- // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
1621
- //
1622
- // Supported locales in Chrome:
1623
- // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
1624
- case "ka":
1625
- return ",";
1626
- default:
1627
- const numberWithDecimalSeparator = 1.1;
1628
- // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
1629
- // support Safari 12.
1630
- const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
1631
- // 0x661 is ARABIC-INDIC DIGIT ONE
1632
- // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
1633
- .match(/[^\d\u0661\u06F1]/);
1634
- return match?.[0] ?? ".";
1635
- }
1636
- };
1637
-
1638
- // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
1639
- // getDependencies() internally... simplify usage to just using a "singleton"
1640
- // which get's the injected logger.
1641
- /**
1642
- * Provides logging infrastructure for Perseus
1643
- */
1644
- const Log = {
1645
- // Logs a message
1646
- log: (message, extra) => {
1647
- getDependencies().Log.log(message, extra);
1648
- },
1649
- // Logs an error with a message
1650
- error: (message, kind, extra) => {
1651
- getDependencies().Log.error(message, kind, extra);
1652
- }
1653
- };
1654
-
1655
- /**
1656
- * Checks user input from the expression widget to see if it is scorable.
1657
- *
1658
- * Note: Most of the expression widget's validation requires the Rubric because
1659
- * of its use of KhanAnswerTypes as a core part of scoring.
1660
- *
1661
- * @see `scoreExpression()` for more details.
1662
- */
1663
- function validateExpression(userInput) {
1664
- if (userInput === "") {
1665
- return {
1666
- type: "invalid",
1667
- message: null
1668
- };
1669
- }
1670
- return null;
1671
- }
1672
-
1673
- /* Content creators input a list of answers which are matched from top to
1674
- * bottom. The intent is that they can include spcific solutions which should
1675
- * be graded as correct or incorrect (or ungraded!) first, then get more
1676
- * general.
1677
- *
1678
- * We iterate through each answer, trying to match it with the user's input
1679
- * using the following angorithm:
1680
- * - Try to parse the user's input. If it doesn't parse then return "not
1681
- * graded".
1682
- * - For each answer:
1683
- * ~ Try to validate the user's input against the answer. The answer is
1684
- * expected to parse.
1685
- * ~ If the user's input validates (the validator judges it "correct"), we've
1686
- * matched and can stop considering answers.
1687
- * - If there were no matches or the matching answer is considered "ungraded",
1688
- * show the user an error. TODO(joel) - what error?
1689
- * - Otherwise, pass through the resulting points and message.
1690
- */
1691
- function scoreExpression(userInput, rubric, strings, locale) {
1692
- const validationError = validateExpression(userInput);
1693
- if (validationError) {
1694
- return validationError;
1695
- }
1696
- const options = ___default["default"].clone(rubric);
1697
- ___default["default"].extend(options, {
1698
- decimal_separator: getDecimalSeparator(locale)
1699
- });
1700
- const createValidator = answer => {
1701
- // We give options to KAS.parse here because it is parsing the
1702
- // solution answer, not the student answer, and we don't want a
1703
- // solution to work if the student is using a different language
1704
- // (different from the content creation language, ie. English).
1705
- const expression = KAS__namespace.parse(answer.value, rubric);
1706
- // An answer may not be parsed if the expression was defined
1707
- // incorrectly. For example if the answer is using a symbol defined
1708
- // in the function variables list for the expression.
1709
- if (!expression.parsed) {
1710
- /* c8 ignore next */
1711
- Log.error("Unable to parse solution answer for expression", perseusCore.Errors.InvalidInput, {
1712
- loggedMetadata: {
1713
- rubric: JSON.stringify(rubric)
1714
- }
1715
- });
1716
- return null;
1717
- }
1718
- return perseusScore.KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, ___default["default"]({}).extend(options, {
1719
- simplify: answer.simplify,
1720
- form: answer.form
1721
- }));
1722
- };
1723
-
1724
- // Find the first answer form that matches the user's input and that
1725
- // is considered correct. Also, track whether the input is
1726
- // considered "empty" for all answer forms, and keep the validation
1727
- // result for the first answer form for which the user's input was
1728
- // considered "ungraded".
1729
- // (Terminology reminder: the answer forms are provided by the
1730
- // assessment items; they are not the user's input. Each one might
1731
- // represent a correct answer, an incorrect one (if the exercise
1732
- // creator has predicted certain common wrong answers and wants to
1733
- // provide guidance via a message), or an ungraded one (same idea,
1734
- // but without giving the user an incorrect mark for the question).
1735
- let matchingAnswerForm;
1736
- let matchMessage;
1737
- let allEmpty = true;
1738
- let firstUngradedResult;
1739
- for (const answerForm of rubric.answerForms || []) {
1740
- const validator = createValidator(answerForm);
1741
- if (!validator) {
1742
- continue;
1743
- }
1744
- const result = validator(userInput);
1745
-
1746
- // Short-circuit as soon as the user's input matches some answer
1747
- // (independently of whether the answer is correct)
1748
- if (result.correct) {
1749
- matchingAnswerForm = answerForm;
1750
- matchMessage = result.message || "";
1751
- break;
1752
- }
1753
- allEmpty = allEmpty && result.empty;
1754
- // If this answer form is correct and the user's input is considered
1755
- // "ungraded" for it, we'll want to keep the evaluation result for
1756
- // later. If the user's input doesn't match any answer forms, we'll
1757
- // show the message from this validation.
1758
- if (answerForm.considered === "correct" && result.ungraded && !firstUngradedResult) {
1759
- firstUngradedResult = result;
1760
- }
1761
- }
1762
-
1763
- // Now check to see if we matched any answer form at all, and if
1764
- // we did, whether it's considered correct, incorrect, or ungraded
1765
- if (!matchingAnswerForm) {
1766
- if (firstUngradedResult) {
1767
- // While we didn't directly match with any answer form, we
1768
- // did at some point get an "ungraded" validation result,
1769
- // which might indicate e.g. a mismatch in variable casing.
1770
- // We'll return "invalid", which will let the user try again
1771
- // with no penalty, and the hopefully helpful validation
1772
- // message.
1773
- return {
1774
- type: "invalid",
1775
- message: firstUngradedResult.message,
1776
- suppressAlmostThere: firstUngradedResult.suppressAlmostThere
1777
- };
1778
- }
1779
- if (allEmpty) {
1780
- // If everything graded as empty, it's invalid.
1781
- return {
1782
- type: "invalid",
1783
- message: null
1784
- };
1785
- }
1786
- // We fell through all the possibilities and we're not empty,
1787
- // so the answer is considered incorrect.
1788
- return {
1789
- type: "points",
1790
- earned: 0,
1791
- total: 1
1792
- };
1793
- }
1794
- if (matchingAnswerForm.considered === "ungraded") {
1795
- return {
1796
- type: "invalid",
1797
- message: matchMessage
1798
- };
1799
- }
1800
- // We matched a graded answer form, so we can now tell the user
1801
- // whether their input was correct or incorrect, and hand out
1802
- // points accordingly
1803
- return {
1804
- type: "points",
1805
- earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
1806
- total: 1,
1807
- message: matchMessage
1808
- };
1809
- }
1810
-
1811
1686
  // Map of international operator names to their English equivalents
1812
1687
  const englishOperators = {
1813
1688
  arctg: "arctan",
@@ -1918,7 +1793,7 @@ class Expression extends React__namespace.Component {
1918
1793
  // make it so that solution answers with ','s or '.'s work
1919
1794
  const options = ___default["default"].pick(props || this.props, "functions");
1920
1795
  ___default["default"].extend(options, {
1921
- decimal_separator: getDecimalSeparator(this.context.locale)
1796
+ decimal_separator: perseusCore.getDecimalSeparator(this.context.locale)
1922
1797
  });
1923
1798
  return KAS__namespace.parse(normalizeTex(value), options);
1924
1799
  };
@@ -2196,7 +2071,7 @@ var Expression$1 = {
2196
2071
  isLintable: true,
2197
2072
  // TODO(LEMS-2656): remove TS suppression
2198
2073
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2199
- scorer: scoreExpression,
2074
+ scorer: perseusScore.scoreExpression,
2200
2075
  // TODO(LEMS-2656): remove TS suppression
2201
2076
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
2202
2077
  getOneCorrectAnswerFromRubric(rubric) {
@@ -2566,56 +2441,6 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
2566
2441
  return [constrainTickStep(tickSteps[0], ranges[0]), constrainTickStep(tickSteps[1], ranges[1])];
2567
2442
  }
2568
2443
 
2569
- /**
2570
- * Approximate equality on numbers and primitives.
2571
- */
2572
- function eq$4(x, y) {
2573
- if (typeof x === "number" && typeof y === "number") {
2574
- return Math.abs(x - y) < 1e-9;
2575
- }
2576
- return x === y;
2577
- }
2578
-
2579
- /**
2580
- * Deep approximate equality on primitives, numbers, arrays, and objects.
2581
- * Recursive.
2582
- */
2583
- function deepEq$7(x, y) {
2584
- if (Array.isArray(x) && Array.isArray(y)) {
2585
- if (x.length !== y.length) {
2586
- return false;
2587
- }
2588
- for (let i = 0; i < x.length; i++) {
2589
- if (!deepEq$7(x[i], y[i])) {
2590
- return false;
2591
- }
2592
- }
2593
- return true;
2594
- }
2595
- if (Array.isArray(x) || Array.isArray(y)) {
2596
- return false;
2597
- }
2598
- if (typeof x === "function" && typeof y === "function") {
2599
- return eq$4(x, y);
2600
- }
2601
- if (typeof x === "function" || typeof y === "function") {
2602
- return false;
2603
- }
2604
- if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
2605
- return x === y || ___default["default"].all(x, function (v, k) {
2606
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2607
- return deepEq$7(y[k], v);
2608
- }) && ___default["default"].all(y, function (v, k) {
2609
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2610
- return deepEq$7(x[k], v);
2611
- });
2612
- }
2613
- if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
2614
- return false;
2615
- }
2616
- return eq$4(x, y);
2617
- }
2618
-
2619
2444
  /**
2620
2445
  * Query String Parser
2621
2446
  *
@@ -2830,15 +2655,6 @@ const textarea = {
2830
2655
  */
2831
2656
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
2832
2657
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
2833
-
2834
- // TODO(benchristel): in the future, we may want to make deepClone work for
2835
- // Record<string, Cloneable> as well. Currently, it only does arrays.
2836
- function deepClone(obj) {
2837
- if (Array.isArray(obj)) {
2838
- return obj.map(deepClone);
2839
- }
2840
- return obj;
2841
- }
2842
2658
  const Util = {
2843
2659
  inputPathsEqual,
2844
2660
  nestedMap: nestedMap$2,
@@ -2859,8 +2675,6 @@ const Util = {
2859
2675
  gridStepFromTickStep,
2860
2676
  tickStepFromNumTicks,
2861
2677
  constrainedTickStepsFromTickSteps,
2862
- eq: eq$4,
2863
- deepEq: deepEq$7,
2864
2678
  parseQueryString,
2865
2679
  updateQueryString: updateQueryString$2,
2866
2680
  strongEncodeURIComponent,
@@ -2878,8 +2692,24 @@ const Util = {
2878
2692
  getDataUrl: getDataUrl,
2879
2693
  textarea,
2880
2694
  unescapeMathMode: unescapeMathMode$1,
2881
- random: random$1,
2882
- deepClone
2695
+ random: random$1
2696
+ };
2697
+
2698
+ // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
2699
+ // getDependencies() internally... simplify usage to just using a "singleton"
2700
+ // which get's the injected logger.
2701
+ /**
2702
+ * Provides logging infrastructure for Perseus
2703
+ */
2704
+ const Log = {
2705
+ // Logs a message
2706
+ log: (message, extra) => {
2707
+ getDependencies().Log.log(message, extra);
2708
+ },
2709
+ // Logs an error with a message
2710
+ error: (message, kind, extra) => {
2711
+ getDependencies().Log.error(message, kind, extra);
2712
+ }
2883
2713
  };
2884
2714
 
2885
2715
  // For offline exercises in the mobile app, we download the graphie data
@@ -8428,7 +8258,7 @@ var Tex$1 = {
8428
8258
  const {
8429
8259
  processMath: processMath$1
8430
8260
  } = Tex$1;
8431
- function polar$1(r, th) {
8261
+ function polar$2(r, th) {
8432
8262
  if (typeof r === "number") {
8433
8263
  r = [r, r];
8434
8264
  }
@@ -8767,8 +8597,8 @@ class Graphie$1 {
8767
8597
  endAngle = (endAngle % 360 + 360) % 360;
8768
8598
  const cent = this.scalePoint(center);
8769
8599
  const radii = this.scaleVector(radius);
8770
- const startVector = polar$1(radius, startAngle);
8771
- const endVector = polar$1(radius, endAngle);
8600
+ const startVector = polar$2(radius, startAngle);
8601
+ const endVector = polar$2(radius, endAngle);
8772
8602
 
8773
8603
  // We round the coordinates to make testing easier, because trig
8774
8604
  // operations return unround numbers.
@@ -10312,261 +10142,11 @@ class WrappedLine extends WrappedDrawing {
10312
10142
  }
10313
10143
  }
10314
10144
 
10315
- /**
10316
- * A collection of geomtry-related utility functions
10317
- */
10318
- const {
10319
- eq: eq$3,
10320
- deepEq: deepEq$6
10321
- } = Util;
10322
-
10323
- // This should really be a readonly tuple of [number, number]
10324
-
10325
- // a, b, c
10326
-
10327
- // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10328
- function sign(val) {
10329
- if (eq$3(val, 0)) {
10330
- return 0;
10331
- }
10332
- return val > 0 ? 1 : -1;
10333
- }
10334
-
10335
- // Determine whether three points are collinear (0), for a clockwise turn (negative),
10336
- // or counterclockwise turn (positive)
10337
- function ccw(a, b, c) {
10338
- return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10339
- }
10340
- function collinear(a, b, c) {
10341
- return eq$3(ccw(a, b, c), 0);
10342
- }
10343
-
10344
- // Given rect bounding points A and B, whether point C is inside the rect
10345
- function pointInRect(a, b, c) {
10346
- return c[0] <= Math.max(a[0], b[0]) && c[0] >= Math.min(a[0], b[0]) && c[1] <= Math.max(a[1], b[1]) && c[1] >= Math.min(a[1], b[1]);
10347
- }
10348
-
10349
- // Whether line segment AB intersects line segment CD
10350
- // http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
10351
- function intersects(ab, cd) {
10352
- const triplets = [[ab[0], ab[1], cd[0]], [ab[0], ab[1], cd[1]], [cd[0], cd[1], ab[0]], [cd[0], cd[1], ab[1]]];
10353
- const orientations = ___default["default"].map(triplets, function (triplet) {
10354
- return sign(ccw(...triplet));
10355
- });
10356
- if (orientations[0] !== orientations[1] && orientations[2] !== orientations[3]) {
10357
- return true;
10358
- }
10359
- for (let i = 0; i < 4; i++) {
10360
- if (orientations[i] === 0 && pointInRect(...triplets[i])) {
10361
- return true;
10362
- }
10363
- }
10364
- return false;
10365
- }
10366
-
10367
- // Whether any two sides of a polygon intersect each other
10368
- function polygonSidesIntersect(vertices) {
10369
- for (let i = 0; i < vertices.length; i++) {
10370
- for (let k = i + 1; k < vertices.length; k++) {
10371
- // If any two vertices are the same point, sides overlap
10372
- if (kmath.point.equal(vertices[i], vertices[k])) {
10373
- return true;
10374
- }
10375
-
10376
- // Find the other end of the sides starting at vertices i and k
10377
- const iNext = (i + 1) % vertices.length;
10378
- const kNext = (k + 1) % vertices.length;
10379
-
10380
- // Adjacent sides always intersect (at the vertex); skip those
10381
- if (iNext === k || kNext === i) {
10382
- continue;
10383
- }
10384
- const side1 = [vertices[i], vertices[iNext]];
10385
- const side2 = [vertices[k], vertices[kNext]];
10386
- if (intersects(side1, side2)) {
10387
- return true;
10388
- }
10389
- }
10390
- }
10391
- return false;
10392
- }
10393
- function vector(a, b) {
10394
- return ___default["default"].map(___default["default"].zip(a, b), function (pair) {
10395
- return pair[0] - pair[1];
10396
- });
10397
- }
10398
- function reverseVector(vector) {
10399
- return [-vector[0], -vector[1]];
10400
- }
10401
-
10402
- // Returns whether connecting the given sequence of `points` forms a clockwise
10403
- // path (assuming a closed loop, where the last point connects back to the
10404
- // first).
10405
- function clockwise$1(points) {
10406
- const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
10407
- const areas = ___default["default"].map(segments, function (segment) {
10408
- const p1 = segment[0];
10409
- const p2 = segment[1];
10410
- return (p2[0] - p1[0]) * (p2[1] + p1[1]);
10411
- });
10412
- return kmath.sum(areas) > 0;
10413
- }
10414
- function magnitude(v) {
10415
- return Math.sqrt(___default["default"].reduce(v, function (memo, el) {
10416
- // @ts-expect-error - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
10417
- return memo + Math.pow(el, 2);
10418
- }, 0));
10419
- }
10420
- function dotProduct(a, b) {
10421
- return ___default["default"].reduce(___default["default"].zip(a, b), function (memo, pair) {
10422
- return memo + pair[0] * pair[1];
10423
- }, 0);
10424
- }
10425
- function sideLengths(coords) {
10426
- const segments = ___default["default"].zip(coords, rotate(coords));
10427
- return segments.map(function (segment) {
10428
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
10429
- return magnitude(vector(...segment));
10430
- });
10431
- }
10432
-
10433
- // Based on http://math.stackexchange.com/a/151149
10434
- function angleMeasures(coords) {
10435
- const triplets = ___default["default"].zip(rotate(coords, -1), coords, rotate(coords, 1));
10436
- const offsets = ___default["default"].map(triplets, function (triplet) {
10437
- const p = vector(triplet[1], triplet[0]);
10438
- const q = vector(triplet[2], triplet[1]);
10439
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'Coord'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
10440
- const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
10441
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
10442
- return sign(ccw(...triplet)) > 0 ? raw : -raw;
10443
- });
10444
- const sum = ___default["default"].reduce(offsets, function (memo, arg) {
10445
- return memo + arg;
10446
- }, 0);
10447
- return ___default["default"].map(offsets, function (offset) {
10448
- return sum > 0 ? Math.PI - offset : Math.PI + offset;
10449
- });
10450
- }
10451
-
10452
- // Whether two polygons are similar (or if specified, congruent)
10453
- function similar(coords1, coords2, tolerance) {
10454
- if (coords1.length !== coords2.length) {
10455
- return false;
10456
- }
10457
- const n = coords1.length;
10458
- const angles1 = angleMeasures(coords1);
10459
- const angles2 = angleMeasures(coords2);
10460
- const sides1 = sideLengths(coords1);
10461
- const sides2 = sideLengths(coords2);
10462
- for (let i = 0; i < 2 * n; i++) {
10463
- let angles = angles2.slice();
10464
- let sides = sides2.slice();
10465
-
10466
- // Reverse angles and sides to allow matching reflected polygons
10467
- if (i >= n) {
10468
- angles.reverse();
10469
- sides.reverse();
10470
- // Since sides are calculated from two coordinates,
10471
- // simply reversing results in an off by one error
10472
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10473
- sides = rotate(sides, 1);
10474
- }
10475
-
10476
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10477
- angles = rotate(angles, i);
10478
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10479
- sides = rotate(sides, i);
10480
- if (deepEq$6(angles1, angles)) {
10481
- const sidePairs = ___default["default"].zip(sides1, sides);
10482
- const factors = ___default["default"].map(sidePairs, function (pair) {
10483
- return pair[0] / pair[1];
10484
- });
10485
- const same = ___default["default"].all(factors, function (factor) {
10486
- return eq$3(factors[0], factor);
10487
- });
10488
- const congruentEnough = ___default["default"].all(sidePairs, function (pair) {
10489
- return kmath.number.equal(pair[0], pair[1], tolerance);
10490
- });
10491
- if (same && congruentEnough) {
10492
- return true;
10493
- }
10494
- }
10495
- }
10496
- return false;
10497
- }
10498
-
10499
- // Given triangle with sides ABC return angle opposite side C in degrees
10500
- function lawOfCosines(a, b, c) {
10501
- return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180 / Math.PI;
10502
- }
10503
- function canonicalSineCoefficients$1(_ref) {
10504
- let [amplitude, angularFrequency, phase, verticalOffset] = _ref;
10505
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
10506
- // this function ensures that a, b > 0, and c is its
10507
- // smallest possible positive value.
10508
-
10509
- // Guarantee a > 0
10510
- if (amplitude < 0) {
10511
- amplitude *= -1;
10512
- angularFrequency *= -1;
10513
- phase *= -1;
10514
- }
10515
- const period = 2 * Math.PI;
10516
- // Guarantee b > 0
10517
- if (angularFrequency < 0) {
10518
- angularFrequency *= -1;
10519
- phase *= -1;
10520
- phase += period / 2;
10521
- }
10522
-
10523
- // Guarantee c is smallest possible positive value
10524
- while (phase > 0) {
10525
- phase -= period;
10526
- }
10527
- while (phase < 0) {
10528
- phase += period;
10529
- }
10530
- return [amplitude, angularFrequency, phase, verticalOffset];
10531
- }
10532
-
10533
- // e.g. rotate([1, 2, 3]) -> [2, 3, 1]
10534
- function rotate(array, n) {
10535
- n = typeof n === "undefined" ? 1 : n % array.length;
10536
- return array.slice(n).concat(array.slice(0, n));
10537
- }
10538
- function getLineEquation(first, second) {
10539
- if (eq$3(first[0], second[0])) {
10540
- return "x = " + first[0].toFixed(3);
10541
- }
10542
- const m = (second[1] - first[1]) / (second[0] - first[0]);
10543
- const b = first[1] - m * first[0];
10544
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
10545
- }
10546
-
10547
- // Stolen from the wikipedia article
10548
- // http://en.wikipedia.org/wiki/Line-line_intersection
10549
- function getLineIntersection(
10550
- // TODO(LP-10725): update these to be 2-tuples
10551
- firstPoints, secondPoints) {
10552
- const x1 = firstPoints[0][0];
10553
- const y1 = firstPoints[0][1];
10554
- const x2 = firstPoints[1][0];
10555
- const y2 = firstPoints[1][1];
10556
- const x3 = secondPoints[0][0];
10557
- const y3 = secondPoints[0][1];
10558
- const x4 = secondPoints[1][0];
10559
- const y4 = secondPoints[1][1];
10560
- const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
10561
- if (Math.abs(determinant) < 1e-9) {
10562
- return "Lines are parallel";
10563
- }
10564
- const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / determinant;
10565
- const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / determinant;
10566
- return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
10567
- }
10568
-
10569
10145
  /* eslint-disable @babel/no-invalid-this */
10146
+ const {
10147
+ clockwise: clockwise$3,
10148
+ reverseVector: reverseVector$1
10149
+ } = kmath.geometry;
10570
10150
  function scaledDistanceFromAngle(angle) {
10571
10151
  const a = 3.51470560176242 * 20;
10572
10152
  const b = 0.5687298702748785 * 20;
@@ -10710,7 +10290,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
10710
10290
  const sOffset = sSpacing * (i - (n - 1) / 2);
10711
10291
  const sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10712
10292
  const sHeightVector = scaledPolarRad(sHeight, perpendicularAngle);
10713
- const sPath = [kmath.vector.add(sMidpoint, sOffsetVector, sHeightVector), kmath.vector.add(sMidpoint, sOffsetVector, reverseVector(sHeightVector))];
10293
+ const sPath = [kmath.vector.add(sMidpoint, sOffsetVector, sHeightVector), kmath.vector.add(sMidpoint, sOffsetVector, reverseVector$1(sHeightVector))];
10714
10294
 
10715
10295
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
10716
10296
  temp.push(graphie.scaledPath(sPath, style));
@@ -10735,7 +10315,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
10735
10315
  const sOffset = sCumulativeOffset + sSpacing * i;
10736
10316
  let sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10737
10317
  if (start !== p1) {
10738
- sOffsetVector = reverseVector(sOffsetVector);
10318
+ sOffsetVector = reverseVector$1(sOffsetVector);
10739
10319
  }
10740
10320
  const sEnd = kmath.vector.add(sMidpoint, sOffsetVector);
10741
10321
 
@@ -11802,7 +11382,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11802
11382
  // Temporary objects
11803
11383
  ___default["default"].invoke(polygon.temp, "remove");
11804
11384
  polygon.temp = [];
11805
- const isClockwise = clockwise$1(polygon.coords);
11385
+ const isClockwise = clockwise$3(polygon.coords);
11806
11386
 
11807
11387
  // Update angle labels
11808
11388
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -13223,7 +12803,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13223
12803
  points[1].onMove = function (x, y) {
13224
12804
  const oldVertex = points[1].coord;
13225
12805
  const newVertex = [x, y];
13226
- const delta = kmath.vector.add(newVertex, reverseVector(oldVertex));
12806
+ const delta = kmath.vector.add(newVertex, reverseVector$1(oldVertex));
13227
12807
  let valid = true;
13228
12808
  const newPoints = {};
13229
12809
  ___default["default"].each([0, 2], function (i) {
@@ -13265,7 +12845,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13265
12845
  let angle = GraphUtils.findAngleDeprecated(newPoint, vertex);
13266
12846
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13267
12847
  const distance = GraphUtils.getDistance(newPoint, vertex);
13268
- return kmath.vector.add(vertex, polar$1(distance, angle));
12848
+ return kmath.vector.add(vertex, polar$2(distance, angle));
13269
12849
  }
13270
12850
  return true;
13271
12851
  };
@@ -13416,7 +12996,6 @@ ___default["default"].extend(MovableAngle.prototype, {
13416
12996
 
13417
12997
  /* eslint-disable @babel/no-invalid-this */
13418
12998
  const nestedMap$1 = Util.nestedMap;
13419
- const deepEq$5 = Util.deepEq;
13420
12999
 
13421
13000
  /**
13422
13001
  * A base class for all Graphie Movables
@@ -13513,7 +13092,7 @@ const createSimpleClass = function (addFunction) {
13513
13092
  this._prevProps = this.props;
13514
13093
  },
13515
13094
  modify: function (graphie) {
13516
- if (!deepEq$5(this.props, this._prevProps)) {
13095
+ if (!perseusCore.approximateDeepEqual(this.props, this._prevProps)) {
13517
13096
  this.remove();
13518
13097
  this.add(graphie);
13519
13098
  this._prevProps = this.props;
@@ -15236,7 +14815,7 @@ function sum(array) {
15236
14815
  return memo + arg;
15237
14816
  }, 0);
15238
14817
  }
15239
- function clockwise(points) {
14818
+ function clockwise$2(points) {
15240
14819
  const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
15241
14820
  const areas = ___default["default"].map(segments, function (segment) {
15242
14821
  const p1 = segment[0];
@@ -15302,7 +14881,7 @@ const draw$1 = {
15302
14881
  const graphie = this.graphie;
15303
14882
  const self = this;
15304
14883
  const coords = ___default["default"].invoke(state.points, "coord");
15305
- const isClockwise = clockwise(coords);
14884
+ const isClockwise = clockwise$2(coords);
15306
14885
  const n = coords.length;
15307
14886
 
15308
14887
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -16012,7 +15591,7 @@ const Parabola = GraphieClasses.createClass({
16012
15591
  this.parabola.toFront();
16013
15592
  }
16014
15593
  });
16015
- const Sinusoid$1 = GraphieClasses.createClass({
15594
+ const Sinusoid = GraphieClasses.createClass({
16016
15595
  displayName: "Sinusoid",
16017
15596
  movableProps: ["children"],
16018
15597
  add: function (graphie) {
@@ -16034,7 +15613,7 @@ const Sinusoid$1 = GraphieClasses.createClass({
16034
15613
  this.sinusoid.toFront();
16035
15614
  }
16036
15615
  });
16037
- const Plot$2 = GraphieClasses.createSimpleClass((graphie, props) => {
15616
+ const Plot$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16038
15617
  return graphie.plot(props.fn, props.range, props.style);
16039
15618
  });
16040
15619
  const PlotParametric$1 = GraphieClasses.createSimpleClass((graphie, props) => {
@@ -16092,17 +15671,16 @@ var Movables = {
16092
15671
  MovablePoint: MovablePoint$4,
16093
15672
  Parabola: Parabola,
16094
15673
  Path: Path,
16095
- Plot: Plot$2,
15674
+ Plot: Plot$1,
16096
15675
  PlotParametric: PlotParametric$1,
16097
15676
  Point: Point$1,
16098
- Sinusoid: Sinusoid$1,
15677
+ Sinusoid: Sinusoid,
16099
15678
  Rect: Rect$1
16100
15679
  };
16101
15680
 
16102
15681
  const GraphieMovable = GraphieClasses.GraphieMovable;
16103
15682
  const createGraphie = GraphUtils.createGraphie;
16104
15683
  const {
16105
- deepEq: deepEq$4,
16106
15684
  nestedMap
16107
15685
  } = Util;
16108
15686
  const {
@@ -16140,7 +15718,7 @@ class Graphie extends React__namespace.Component {
16140
15718
  if (this.props.setup !== prevProps.setup) {
16141
15719
  Log.error("<Graphie> was given a new setup function. " + "This is a bad idea; please refactor your code to give " + "the same setup function reference to <Graphie> on " + "every render.", perseusCore.Errors.Internal);
16142
15720
  }
16143
- if (!deepEq$4(this.props.options, prevProps.options) || !deepEq$4(this.props.box, prevProps.box) || !deepEq$4(this.props.range, prevProps.range)) {
15721
+ if (!perseusCore.approximateDeepEqual(this.props.options, prevProps.options) || !perseusCore.approximateDeepEqual(this.props.box, prevProps.box) || !perseusCore.approximateDeepEqual(this.props.range, prevProps.range)) {
16144
15722
  this._setupGraphie();
16145
15723
  }
16146
15724
  this._updateMovables();
@@ -18469,6 +18047,9 @@ const getWidgetExport = name => {
18469
18047
  const getWidgetScorer = name => {
18470
18048
  return widgets[name]?.scorer ?? null;
18471
18049
  };
18050
+ const getPublicWidgetOptionsFunction = name => {
18051
+ return widgets[name]?.getPublicWidgetOptions ?? (i => i);
18052
+ };
18472
18053
  const getEditor = name => {
18473
18054
  return ___default["default"].has(editors, name) ? editors[name] : null;
18474
18055
  };
@@ -18776,6 +18357,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18776
18357
  getWidget: getWidget,
18777
18358
  getWidgetExport: getWidgetExport,
18778
18359
  getWidgetScorer: getWidgetScorer,
18360
+ getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
18779
18361
  getEditor: getEditor,
18780
18362
  getTransform: getTransform,
18781
18363
  getVersion: getVersion,
@@ -21147,230 +20729,6 @@ const getPromptJSON$s = (renderProps, userInput) => {
21147
20729
  };
21148
20730
  };
21149
20731
 
21150
- /*
21151
- * In this file, an `expression` is some portion of valid TeX enclosed in
21152
- * curly brackets.
21153
- */
21154
-
21155
- /*
21156
- * Find the index at which an expression ends, i.e., has an unmatched
21157
- * closing curly bracket. This method assumes that we start with a non-open
21158
- * bracket character and end when we've seen more left than right brackets
21159
- * (rather than assuming that we start with a bracket character and wait for
21160
- * bracket equality).
21161
- */
21162
- function findEndpoint(tex, currentIndex) {
21163
- let bracketDepth = 0;
21164
- for (let i = currentIndex, len = tex.length; i < len; i++) {
21165
- const c = tex[i];
21166
- if (c === "{") {
21167
- bracketDepth++;
21168
- } else if (c === "}") {
21169
- bracketDepth--;
21170
- }
21171
- if (bracketDepth < 0) {
21172
- return i;
21173
- }
21174
- }
21175
- // If we never see unbalanced curly brackets, default to the
21176
- // entire string
21177
- return tex.length;
21178
- }
21179
-
21180
- /*
21181
- * Parses an individual set of curly brackets into TeX.
21182
- */
21183
- function parseNextExpression(tex, currentIndex, handler) {
21184
- // Find the first '{' and grab subsequent TeX
21185
- // Ex) tex: '{3}{7}', and we want the '3'
21186
- const openBracketIndex = tex.indexOf("{", currentIndex);
21187
- const nextExpIndex = openBracketIndex + 1;
21188
-
21189
- // Truncate to only contain remaining TeX
21190
- const endpoint = findEndpoint(tex, nextExpIndex);
21191
- const expressionTeX = tex.substring(nextExpIndex, endpoint);
21192
- const parsedExp = walkTex(expressionTeX, handler);
21193
- return {
21194
- endpoint: endpoint,
21195
- expression: parsedExp
21196
- };
21197
- }
21198
- function getNextFracIndex(tex, currentIndex) {
21199
- const dfrac = "\\dfrac";
21200
- const frac = "\\frac";
21201
- const nextFrac = tex.indexOf(frac, currentIndex);
21202
- const nextDFrac = tex.indexOf(dfrac, currentIndex);
21203
- if (nextFrac > -1 && nextDFrac > -1) {
21204
- return Math.min(nextFrac, nextDFrac);
21205
- }
21206
- if (nextFrac > -1) {
21207
- return nextFrac;
21208
- }
21209
- if (nextDFrac > -1) {
21210
- return nextDFrac;
21211
- }
21212
- return -1;
21213
- }
21214
- function walkTex(tex, handler) {
21215
- if (!tex) {
21216
- return "";
21217
- }
21218
-
21219
- // Ex) tex: '2 \dfrac {3}{7}'
21220
- let parsedString = "";
21221
- let currentIndex = 0;
21222
- let nextFrac = getNextFracIndex(tex, currentIndex);
21223
-
21224
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
21225
- while (nextFrac > -1) {
21226
- // Gather first fragment, preceding \dfrac
21227
- // Ex) parsedString: '2 '
21228
- parsedString += tex.substring(currentIndex, nextFrac);
21229
-
21230
- // Remove everything preceding \dfrac, which has been parsed
21231
- currentIndex = nextFrac;
21232
-
21233
- // Parse first expression and move index past it
21234
- // Ex) firstParsedExpression.expression: '3'
21235
- const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21236
- currentIndex = firstParsedExpression.endpoint + 1;
21237
-
21238
- // Parse second expression
21239
- // Ex) secondParsedExpression.expression: '7'
21240
- const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21241
- currentIndex = secondParsedExpression.endpoint + 1;
21242
-
21243
- // Add expressions to running total of parsed expressions
21244
- if (parsedString.length) {
21245
- parsedString += " ";
21246
- }
21247
-
21248
- // Apply a custom handler based on the parsed subexpressions
21249
- parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21250
-
21251
- // Find next DFrac, relative to currentIndex
21252
- nextFrac = getNextFracIndex(tex, currentIndex);
21253
- }
21254
-
21255
- // Add remaining TeX, which is \dfrac-free
21256
- parsedString += tex.slice(currentIndex);
21257
- return parsedString;
21258
- }
21259
-
21260
- /*
21261
- * Modify a TeX expression, returning another TeX expression. The resulting
21262
- * expression will have its innermost fractions stubbed out with \fracs
21263
- * (as opposed to \dfracs). All other content will remain untouched.
21264
- */
21265
- function modifyTex(tex) {
21266
- function isNestedFraction(tex) {
21267
- return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21268
- }
21269
- const handler = function (exp1, exp2) {
21270
- let prefix;
21271
- if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21272
- prefix = "\\dfrac";
21273
- } else {
21274
- prefix = "\\frac";
21275
- }
21276
- return prefix + " {" + exp1 + "}{" + exp2 + "}";
21277
- };
21278
- return walkTex(tex, handler);
21279
- }
21280
-
21281
- /*
21282
- * Parse a TeX expression into something interpretable by input-number.
21283
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21284
- * (2) removing backslash-escaping from certain characters (right now, only
21285
- * percent signs).
21286
- *
21287
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21288
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21289
- * recursion is to allow for nested \dfrac elements.
21290
- *
21291
- * Backslash-escapes are removed with a simple search-and-replace.
21292
- */
21293
- function parseTex(tex) {
21294
- const handler = function (exp1, exp2) {
21295
- return exp1 + "/" + exp2;
21296
- };
21297
- const texWithoutFracs = walkTex(tex, handler);
21298
- return texWithoutFracs.replace("\\%", "%");
21299
- }
21300
- var TexWrangler = {
21301
- parseTex,
21302
- modifyTex
21303
- };
21304
-
21305
- const ParseTex$1 = TexWrangler.parseTex;
21306
- const answerTypes = {
21307
- number: {
21308
- name: "Numbers",
21309
- forms: "integer, decimal, proper, improper, mixed"
21310
- },
21311
- decimal: {
21312
- name: "Decimals",
21313
- forms: "decimal"
21314
- },
21315
- integer: {
21316
- name: "Integers",
21317
- forms: "integer"
21318
- },
21319
- rational: {
21320
- name: "Fractions and mixed numbers",
21321
- forms: "integer, proper, improper, mixed"
21322
- },
21323
- improper: {
21324
- name: "Improper numbers (no mixed)",
21325
- forms: "integer, proper, improper"
21326
- },
21327
- mixed: {
21328
- name: "Mixed numbers (no improper)",
21329
- forms: "integer, proper, mixed"
21330
- },
21331
- percent: {
21332
- name: "Numbers or percents",
21333
- forms: "integer, decimal, proper, improper, mixed, percent"
21334
- },
21335
- pi: {
21336
- name: "Numbers with pi",
21337
- forms: "pi"
21338
- }
21339
- };
21340
- function scoreInputNumber(userInput, rubric, strings) {
21341
- if (rubric.answerType == null) {
21342
- rubric.answerType = "number";
21343
- }
21344
-
21345
- // note(matthewc): this will get immediately parsed again by
21346
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21347
- // expected here
21348
- const stringValue = `${rubric.value}`;
21349
- const val = perseusScore.KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21350
- simplify: rubric.simplify,
21351
- inexact: rubric.inexact || undefined,
21352
- maxError: rubric.maxError,
21353
- forms: answerTypes[rubric.answerType].forms
21354
- });
21355
-
21356
- // We may have received TeX; try to parse it before grading.
21357
- // If `currentValue` is not TeX, this should be a no-op.
21358
- const currentValue = ParseTex$1(userInput.currentValue);
21359
- const result = val(currentValue);
21360
- if (result.empty) {
21361
- return {
21362
- type: "invalid",
21363
- message: result.message
21364
- };
21365
- }
21366
- return {
21367
- type: "points",
21368
- earned: result.correct ? 1 : 0,
21369
- total: 1,
21370
- message: result.message
21371
- };
21372
- }
21373
-
21374
20732
  const formExamples$1 = {
21375
20733
  integer: function (options, strings) {
21376
20734
  return strings.integerExample;
@@ -21482,7 +20840,7 @@ class InputNumber extends React__namespace.Component {
21482
20840
  strings
21483
20841
  } = this.context;
21484
20842
  const type = this.props.answerType;
21485
- const forms = answerTypes[type].forms.split(/\s*,\s*/);
20843
+ const forms = perseusScore.inputNumberAnswerTypes[type].forms.split(/\s*,\s*/);
21486
20844
  const examples = ___default["default"].map(forms, form => formExamples$1[form](this.props, strings));
21487
20845
  return [strings.yourAnswer].concat(examples);
21488
20846
  }
@@ -21578,7 +20936,7 @@ var InputNumber$1 = {
21578
20936
  isLintable: true,
21579
20937
  // TODO(LEMS-2656): remove TS suppression
21580
20938
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
21581
- scorer: scoreInputNumber,
20939
+ scorer: perseusScore.scoreInputNumber,
21582
20940
  getOneCorrectAnswerFromRubric(rubric) {
21583
20941
  if (rubric.value == null) {
21584
20942
  return;
@@ -21601,139 +20959,6 @@ const getPromptJSON$r = (renderProps, userInput) => {
21601
20959
  };
21602
20960
  };
21603
20961
 
21604
- const ParseTex = TexWrangler.parseTex;
21605
- const answerFormButtons = [{
21606
- title: "Integers",
21607
- value: "integer",
21608
- content: "6"
21609
- }, {
21610
- title: "Decimals",
21611
- value: "decimal",
21612
- content: "0.75"
21613
- }, {
21614
- title: "Proper fractions",
21615
- value: "proper",
21616
- content: "\u2157"
21617
- }, {
21618
- title: "Improper fractions",
21619
- value: "improper",
21620
- content: "\u2077\u2044\u2084"
21621
- }, {
21622
- title: "Mixed numbers",
21623
- value: "mixed",
21624
- content: "1\u00BE"
21625
- }, {
21626
- title: "Numbers with \u03C0",
21627
- value: "pi",
21628
- content: "\u03C0"
21629
- }];
21630
-
21631
- // This function checks if the user inputted a percent value, parsing
21632
- // it as a number (and maybe scaling) so that it can be graded.
21633
- // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
21634
- // can accept several input forms with or without "%", the decision
21635
- // to parse based on the presence of "%" in the input, is so that we
21636
- // don't accidently scale the user typed value before grading, CP-930.
21637
- function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21638
- // If the input value is not a string ending with "%", then there's
21639
- // nothing more to do. The value will be graded as inputted by user.
21640
- if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
21641
- return inputValue;
21642
- }
21643
- const value = parseFloat(inputValue.slice(0, -1));
21644
- // If the input value stripped of the "%" cannot be parsed as a
21645
- // number (the slice is not really necessary for parseFloat to work
21646
- // if the string starts with a number) then return the original
21647
- // input for grading.
21648
- if (isNaN(value)) {
21649
- return inputValue;
21650
- }
21651
-
21652
- // Next, if all correct answers are in the range of |0,1| then we
21653
- // scale the user typed value. We assume this is the correct thing
21654
- // to do since the input value ends with "%".
21655
- if (normalizedAnswerExpected) {
21656
- return value / 100;
21657
- }
21658
-
21659
- // Otherwise, we return input value (number) stripped of the "%".
21660
- return value;
21661
- }
21662
- function scoreNumericInput(userInput, rubric, strings) {
21663
- const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21664
- // Don't default to validating the answer as a pi answer
21665
- // if answerForm isn't set on the answer
21666
- // https://khanacademy.atlassian.net/browse/LC-691
21667
- .filter(e => e !== "pi");
21668
- const createValidator = answer => {
21669
- const stringAnswer = `${answer.value}`;
21670
-
21671
- // Always validate against the provided answer forms (pi, decimal, etc.)
21672
- const validatorForms = [...(answer.answerForms ?? [])];
21673
-
21674
- // When an answer is set to strict, we validate using ONLY
21675
- // the provided answerForms. If strict is false, or if there
21676
- // were no provided answer forms, we will include all
21677
- // of the default answer forms in our validator.
21678
- if (!answer.strict || validatorForms.length === 0) {
21679
- validatorForms.push(...defaultAnswerForms);
21680
- }
21681
- return perseusScore.KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
21682
- message: answer.message,
21683
- simplify: answer.status === "correct" ? answer.simplify : "optional",
21684
- inexact: true,
21685
- // TODO(merlob) backfill / delete
21686
- maxError: answer.maxError,
21687
- forms: validatorForms
21688
- });
21689
- };
21690
-
21691
- // We may have received TeX; try to parse it before grading.
21692
- // If `currentValue` is not TeX, this should be a no-op.
21693
- const currentValue = ParseTex(userInput.currentValue);
21694
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value != null && Math.abs(answer.value) <= 1);
21695
-
21696
- // The coefficient is an attribute of the widget
21697
- let localValue = currentValue;
21698
- if (rubric.coefficient) {
21699
- if (!localValue) {
21700
- localValue = 1;
21701
- } else if (localValue === "-") {
21702
- localValue = -1;
21703
- }
21704
- }
21705
- const matchedAnswer = rubric.answers.map(answer => {
21706
- const validateFn = createValidator(answer);
21707
- const score = validateFn(maybeParsePercentInput(localValue, normalizedAnswerExpected));
21708
- return {
21709
- ...answer,
21710
- score
21711
- };
21712
- }).find(answer => {
21713
- // NOTE: "answer.score.correct" indicates a match via the validate function.
21714
- // It does NOT indicate that the answer itself is correct.
21715
- return answer.score.correct || answer.status === "correct" && answer.score.empty;
21716
- });
21717
- const result = matchedAnswer?.status === "correct" ? matchedAnswer.score : {
21718
- empty: matchedAnswer?.status === "ungraded",
21719
- correct: matchedAnswer?.status === "correct",
21720
- message: matchedAnswer?.message ?? null,
21721
- guess: localValue
21722
- };
21723
- if (result.empty) {
21724
- return {
21725
- type: "invalid",
21726
- message: result.message
21727
- };
21728
- }
21729
- return {
21730
- type: "points",
21731
- earned: result.correct ? 1 : 0,
21732
- total: 1,
21733
- message: result.message
21734
- };
21735
- }
21736
-
21737
20962
  const formExamples = {
21738
20963
  integer: (form, strings) => strings.integerExample,
21739
20964
  proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
@@ -21970,7 +21195,7 @@ var NumericInput$1 = {
21970
21195
  isLintable: true,
21971
21196
  // TODO(LEMS-2656): remove TS suppression
21972
21197
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
21973
- scorer: scoreNumericInput,
21198
+ scorer: perseusScore.scoreNumericInput,
21974
21199
  // TODO(LEMS-2656): remove TS suppression
21975
21200
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
21976
21201
  getOneCorrectAnswerFromRubric(rubric) {
@@ -25733,72 +24958,6 @@ const styles$o = aphrodite.StyleSheet.create({
25733
24958
  }
25734
24959
  });
25735
24960
 
25736
- /**
25737
- * Checks if the user has selected at least one option. Additional validation
25738
- * is done in scoreRadio to check if the number of selected options is correct
25739
- * and if the user has selected both a correct option and the "none of the above"
25740
- * option.
25741
- * @param userInput
25742
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
25743
- */
25744
- function validateRadio(userInput) {
25745
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25746
- return sum + (selected ? 1 : 0);
25747
- }, 0);
25748
- if (numSelected === 0) {
25749
- return {
25750
- type: "invalid",
25751
- message: null
25752
- };
25753
- }
25754
- return null;
25755
- }
25756
-
25757
- function scoreRadio(userInput, rubric, strings) {
25758
- const validationError = validateRadio(userInput);
25759
- if (validationError) {
25760
- return validationError;
25761
- }
25762
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25763
- return sum + (selected ? 1 : 0);
25764
- }, 0);
25765
- const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
25766
- return currentChoice.correct ? sum + 1 : sum;
25767
- }, 0);
25768
- if (numCorrect > 1 && numSelected !== numCorrect) {
25769
- return {
25770
- type: "invalid",
25771
- message: strings.chooseCorrectNum
25772
- };
25773
- // If NOTA and some other answer are checked, ...
25774
- }
25775
-
25776
- const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
25777
- if (noneOfTheAboveSelected && numSelected > 1) {
25778
- return {
25779
- type: "invalid",
25780
- message: strings.notNoneOfTheAbove
25781
- };
25782
- }
25783
- const correct = userInput.choicesSelected.every((selected, i) => {
25784
- let isCorrect;
25785
- if (rubric.choices[i].isNoneOfTheAbove) {
25786
- isCorrect = rubric.choices.every((choice, j) => {
25787
- return i === j || !choice.correct;
25788
- });
25789
- } else {
25790
- isCorrect = !!rubric.choices[i].correct;
25791
- }
25792
- return isCorrect === selected;
25793
- });
25794
- return {
25795
- type: "points",
25796
- earned: correct ? 1 : 0,
25797
- total: 1,
25798
- message: null
25799
- };
25800
- }
25801
-
25802
24961
  // RenderProps is the return type for radio.jsx#transform
25803
24962
 
25804
24963
  class Radio$1 extends React__namespace.Component {
@@ -25986,7 +25145,7 @@ class Radio$1 extends React__namespace.Component {
25986
25145
  choiceStates
25987
25146
  } = this.props;
25988
25147
  if (choiceStates) {
25989
- const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
25148
+ const score = perseusScore.scoreRadio(this.getUserInput(), rubric);
25990
25149
  const widgetCorrect = score.type === "points" && score.total === score.earned;
25991
25150
  const newStates = choiceStates.map(state => ({
25992
25151
  ...state,
@@ -26269,7 +25428,7 @@ var Radio = {
26269
25428
  isLintable: true,
26270
25429
  // TODO(LEMS-2656): remove TS suppression
26271
25430
  // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
26272
- scorer: scoreRadio
25431
+ scorer: perseusScore.scoreRadio
26273
25432
  };
26274
25433
 
26275
25434
  /* eslint-disable import/no-named-as-default */
@@ -26289,40 +25448,20 @@ const getPromptJSON$n = (renderProps, userInput) => {
26289
25448
  };
26290
25449
 
26291
25450
  /**
26292
- * Checks userInput from the categorizer widget to see if the user has selected
26293
- * a category for each item.
26294
- * @param userInput - The user's input corresponding to an array of indices that
26295
- * represent the selected category for each row/item.
26296
- * @param validationData - An array of strings corresponding to each row/item
26297
- * @param strings - Used to provide a validation message
25451
+ * For details on the individual options, see the
25452
+ * PerseusCategorizerWidgetOptions type
26298
25453
  */
26299
- function validateCategorizer(userInput, validationData, strings) {
26300
- const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26301
- if (incomplete) {
26302
- return {
26303
- type: "invalid",
26304
- message: strings.invalidSelection
26305
- };
26306
- }
26307
- return null;
26308
- }
26309
25454
 
26310
- function scoreCategorizer(userInput, scoringData, strings) {
26311
- const validationError = validateCategorizer(userInput, scoringData, strings);
26312
- if (validationError) {
26313
- return validationError;
26314
- }
26315
- let allCorrect = true;
26316
- scoringData.values.forEach((value, i) => {
26317
- if (userInput.values[i] !== value) {
26318
- allCorrect = false;
26319
- }
26320
- });
25455
+ /**
25456
+ * Given a PerseusCategorizerWidgetOptions object, return a new object with only
25457
+ * the public options that should be exposed to the client.
25458
+ */
25459
+ function getCategorizerPublicWidgetOptions(options) {
26321
25460
  return {
26322
- type: "points",
26323
- earned: allCorrect ? 1 : 0,
26324
- total: 1,
26325
- message: null
25461
+ items: options.items,
25462
+ categories: options.categories,
25463
+ randomizeItems: options.randomizeItems,
25464
+ static: options.static
26326
25465
  };
26327
25466
  }
26328
25467
 
@@ -26507,7 +25646,8 @@ var Categorizer$1 = {
26507
25646
  isLintable: true,
26508
25647
  // TODO(LEMS-2656): remove TS suppression
26509
25648
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
26510
- scorer: scoreCategorizer
25649
+ scorer: perseusScore.scoreCategorizer,
25650
+ getPublicWidgetOptions: getCategorizerPublicWidgetOptions
26511
25651
  };
26512
25652
 
26513
25653
  /**
@@ -26570,31 +25710,6 @@ const getPromptJSON$m = () => {
26570
25710
  return getUnsupportedPromptJSON("cs-program");
26571
25711
  };
26572
25712
 
26573
- function scoreCSProgram(state) {
26574
- // The iframe can tell us whether it's correct or incorrect,
26575
- // and pass an optional message
26576
- if (state.status === "correct") {
26577
- return {
26578
- type: "points",
26579
- earned: 1,
26580
- total: 1,
26581
- message: state.message || null
26582
- };
26583
- }
26584
- if (state.status === "incorrect") {
26585
- return {
26586
- type: "points",
26587
- earned: 0,
26588
- total: 1,
26589
- message: state.message || null
26590
- };
26591
- }
26592
- return {
26593
- type: "invalid",
26594
- message: "Keep going, you're not there yet!"
26595
- };
26596
- }
26597
-
26598
25713
  /**
26599
25714
  * This widget is for embedding Khan Academy CS programs.
26600
25715
  */
@@ -26746,7 +25861,7 @@ var CSProgram$1 = {
26746
25861
  hidden: true,
26747
25862
  // TODO(LEMS-2656): remove TS suppression
26748
25863
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
26749
- scorer: scoreCSProgram
25864
+ scorer: perseusScore.scoreCSProgram
26750
25865
  };
26751
25866
 
26752
25867
  const getPromptJSON$l = renderProps => {
@@ -26871,34 +25986,6 @@ const getPromptJSON$k = (renderProps, userInput) => {
26871
25986
  };
26872
25987
  };
26873
25988
 
26874
- /**
26875
- * Checks if the user has selected an item from the dropdown before scoring.
26876
- * This is shown with a userInput value / index other than 0.
26877
- */
26878
- function validateDropdown(userInput) {
26879
- if (userInput.value === 0) {
26880
- return {
26881
- type: "invalid",
26882
- message: null
26883
- };
26884
- }
26885
- return null;
26886
- }
26887
-
26888
- function scoreDropdown(userInput, rubric) {
26889
- const validationError = validateDropdown(userInput);
26890
- if (validationError) {
26891
- return validationError;
26892
- }
26893
- const correct = rubric.choices[userInput.value - 1].correct;
26894
- return {
26895
- type: "points",
26896
- earned: correct ? 1 : 0,
26897
- total: 1,
26898
- message: null
26899
- };
26900
- }
26901
-
26902
25989
  class Dropdown extends React__namespace.Component {
26903
25990
  static contextType = PerseusI18nContext;
26904
25991
  static defaultProps = {
@@ -26999,7 +26086,7 @@ var Dropdown$1 = {
26999
26086
  transform: optionsTransform,
27000
26087
  // TODO(LEMS-2656): remove TS suppression
27001
26088
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
27002
- scorer: scoreDropdown
26089
+ scorer: perseusScore.scoreDropdown
27003
26090
  };
27004
26091
 
27005
26092
  const getPromptJSON$j = renderProps => {
@@ -27371,7 +26458,7 @@ class GradedGroup extends React__namespace.Component {
27371
26458
  DEFAULT_INVALID_MESSAGE_2
27372
26459
  } = this.context.strings;
27373
26460
  const status = score.type === "points" ? score.total === score.earned ? GRADING_STATUSES.correct : GRADING_STATUSES.incorrect : GRADING_STATUSES.invalid;
27374
- const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${score.message}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE_1}${DEFAULT_INVALID_MESSAGE_2}`;
26461
+ const message = score.type === "points" ? score.message || "" : score.message ? `${INVALID_MESSAGE_PREFIX} ${mapErrorToString(score.message)}` : `${INVALID_MESSAGE_PREFIX} ${DEFAULT_INVALID_MESSAGE_1}${DEFAULT_INVALID_MESSAGE_2}`;
27375
26462
  this.setState({
27376
26463
  status: status,
27377
26464
  message: message,
@@ -27970,408 +27057,21 @@ const getPromptJSON$g = (renderProps, userInput) => {
27970
27057
  };
27971
27058
  };
27972
27059
 
27973
- // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
27974
- const Plot$1 = Graphie.Plot;
27060
+ const movableTypeToComponent = {
27061
+ // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
27062
+ PLOT: Graphie.Plot,
27063
+ // @ts-expect-error - TS2339 - Property 'Parabola' does not exist on type 'typeof Graphie'.
27064
+ PARABOLA: Graphie.Parabola,
27065
+ // @ts-expect-error - TS2339 - Property 'Sinusoid' does not exist on type 'typeof Graphie'.
27066
+ SINUSOID: Graphie.Sinusoid
27067
+ };
27975
27068
  const DEFAULT_BACKGROUND_IMAGE = {
27976
27069
  url: null
27977
27070
  };
27978
-
27979
- // TODO(charlie): These really need to go into a utility file as they're being
27980
- // used by both interactive-graph and now grapher.
27981
- function canonicalSineCoefficients(coeffs) {
27982
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
27983
- // this function ensures that a, b > 0, and c is its
27984
- // smallest possible positive value.
27985
- let amplitude = coeffs[0];
27986
- let angularFrequency = coeffs[1];
27987
- let phase = coeffs[2];
27988
- const verticalOffset = coeffs[3];
27989
-
27990
- // Guarantee a > 0
27991
- if (amplitude < 0) {
27992
- amplitude *= -1;
27993
- angularFrequency *= -1;
27994
- phase *= -1;
27995
- }
27996
- const period = 2 * Math.PI;
27997
- // Guarantee b > 0
27998
- if (angularFrequency < 0) {
27999
- angularFrequency *= -1;
28000
- phase *= -1;
28001
- phase += period / 2;
28002
- }
28003
-
28004
- // Guarantee c is smallest possible positive value
28005
- while (phase > 0) {
28006
- phase -= period;
28007
- }
28008
- while (phase < 0) {
28009
- phase += period;
28010
- }
28011
- return [amplitude, angularFrequency, phase, verticalOffset];
28012
- }
28013
- function canonicalTangentCoefficients(coeffs) {
28014
- // For a curve of the form f(x) = a * Tan(b * x - c) + d,
28015
- // this function ensures that a, b > 0, and c is its
28016
- // smallest possible positive value.
28017
- let amplitude = coeffs[0];
28018
- let angularFrequency = coeffs[1];
28019
- let phase = coeffs[2];
28020
- const verticalOffset = coeffs[3];
28021
-
28022
- // Guarantee a > 0
28023
- if (amplitude < 0) {
28024
- amplitude *= -1;
28025
- angularFrequency *= -1;
28026
- phase *= -1;
28027
- }
28028
- const period = Math.PI;
28029
- // Guarantee b > 0
28030
- if (angularFrequency < 0) {
28031
- angularFrequency *= -1;
28032
- phase *= -1;
28033
- phase += period / 2;
28034
- }
28035
-
28036
- // Guarantee c is smallest possible positive value
28037
- while (phase > 0) {
28038
- phase -= period;
28039
- }
28040
- while (phase < 0) {
28041
- phase += period;
28042
- }
28043
- return [amplitude, angularFrequency, phase, verticalOffset];
28044
- }
28045
- const PlotDefaults = {
28046
- areEqual: function (coeffs1, coeffs2) {
28047
- return Util.deepEq(coeffs1, coeffs2);
28048
- },
28049
- Movable: Plot$1,
28050
- getPropsForCoeffs: function (coeffs) {
28051
- return {
28052
- // @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; }'.
28053
- fn: ___default["default"].partial(this.getFunctionForCoeffs, coeffs)
28054
- };
28055
- }
28056
- };
28057
- const Linear = ___default["default"].extend({}, PlotDefaults, {
28058
- url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
28059
- defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
28060
- getCoefficients: function (coords) {
28061
- const p1 = coords[0];
28062
- const p2 = coords[1];
28063
- const denom = p2[0] - p1[0];
28064
- const num = p2[1] - p1[1];
28065
- if (denom === 0) {
28066
- return;
28067
- }
28068
- const m = num / denom;
28069
- const b = p2[1] - m * p2[0];
28070
- return [m, b];
28071
- },
28072
- getFunctionForCoeffs: function (coeffs, x) {
28073
- const m = coeffs[0];
28074
- const b = coeffs[1];
28075
- return m * x + b;
28076
- },
28077
- getEquationString: function (coords) {
28078
- const coeffs = this.getCoefficients(coords);
28079
- const m = coeffs[0];
28080
- const b = coeffs[1];
28081
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
28082
- }
28083
- });
28084
- const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28085
- url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
28086
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28087
- // @ts-expect-error - TS2339 - Property 'Parabola' does not exist on type 'typeof Graphie'.
28088
- Movable: Graphie.Parabola,
28089
- getCoefficients: function (coords) {
28090
- const p1 = coords[0];
28091
- const p2 = coords[1];
28092
-
28093
- // Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
28094
- const h = p1[0];
28095
- const k = p1[1];
28096
-
28097
- // Use these to calculate familiar a, b, c
28098
- const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
28099
- const b = -2 * h * a;
28100
- const c = a * h * h + k;
28101
- return [a, b, c];
28102
- },
28103
- getFunctionForCoeffs: function (coeffs, x) {
28104
- const a = coeffs[0];
28105
- const b = coeffs[1];
28106
- const c = coeffs[2];
28107
- return (a * x + b) * x + c;
28108
- },
28109
- getPropsForCoeffs: function (coeffs) {
28110
- return {
28111
- a: coeffs[0],
28112
- b: coeffs[1],
28113
- c: coeffs[2]
28114
- };
28115
- },
28116
- getEquationString: function (coords) {
28117
- const coeffs = this.getCoefficients(coords);
28118
- const a = coeffs[0];
28119
- const b = coeffs[1];
28120
- const c = coeffs[2];
28121
- return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
28122
- }
28123
- });
28124
- const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28125
- url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
28126
- defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
28127
- // @ts-expect-error - TS2339 - Property 'Sinusoid' does not exist on type 'typeof Graphie'.
28128
- Movable: Graphie.Sinusoid,
28129
- getCoefficients: function (coords) {
28130
- const p1 = coords[0];
28131
- const p2 = coords[1];
28132
- const a = p2[1] - p1[1];
28133
- const b = Math.PI / (2 * (p2[0] - p1[0]));
28134
- const c = p1[0] * b;
28135
- const d = p1[1];
28136
- return [a, b, c, d];
28137
- },
28138
- getFunctionForCoeffs: function (coeffs, x) {
28139
- const a = coeffs[0];
28140
- const b = coeffs[1];
28141
- const c = coeffs[2];
28142
- const d = coeffs[3];
28143
- return a * Math.sin(b * x - c) + d;
28144
- },
28145
- getPropsForCoeffs: function (coeffs) {
28146
- return {
28147
- a: coeffs[0],
28148
- b: coeffs[1],
28149
- c: coeffs[2],
28150
- d: coeffs[3]
28151
- };
28152
- },
28153
- getEquationString: function (coords) {
28154
- const coeffs = this.getCoefficients(coords);
28155
- const a = coeffs[0];
28156
- const b = coeffs[1];
28157
- const c = coeffs[2];
28158
- const d = coeffs[3];
28159
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28160
- },
28161
- areEqual: function (coeffs1, coeffs2) {
28162
- return Util.deepEq(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
28163
- }
28164
- });
28165
- const Tangent = ___default["default"].extend({}, PlotDefaults, {
28166
- url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
28167
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28168
- getCoefficients: function (coords) {
28169
- const p1 = coords[0];
28170
- const p2 = coords[1];
28171
- const a = p2[1] - p1[1];
28172
- const b = Math.PI / (4 * (p2[0] - p1[0]));
28173
- const c = p1[0] * b;
28174
- const d = p1[1];
28175
- return [a, b, c, d];
28176
- },
28177
- getFunctionForCoeffs: function (coeffs, x) {
28178
- const a = coeffs[0];
28179
- const b = coeffs[1];
28180
- const c = coeffs[2];
28181
- const d = coeffs[3];
28182
- return a * Math.tan(b * x - c) + d;
28183
- },
28184
- getEquationString: function (coords) {
28185
- const coeffs = this.getCoefficients(coords);
28186
- const a = coeffs[0];
28187
- const b = coeffs[1];
28188
- const c = coeffs[2];
28189
- const d = coeffs[3];
28190
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28191
- },
28192
- areEqual: function (coeffs1, coeffs2) {
28193
- return Util.deepEq(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
28194
- }
28195
- });
28196
- const Exponential = ___default["default"].extend({}, PlotDefaults, {
28197
- url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
28198
- defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
28199
- defaultAsymptote: [[0, 0.5], [1.0, 0.5]],
28200
- /**
28201
- * Add extra constraints for movement of the points or asymptote (below):
28202
- * newCoord: [x, y]
28203
- * The end position of the point or asymptote endpoint
28204
- * oldCoord: [x, y]
28205
- * The old position of the point or asymptote endpoint
28206
- * coords:
28207
- * An array of coordinates representing the proposed end configuration
28208
- * of the plot coordinates.
28209
- * asymptote:
28210
- * An array of coordinates representing the proposed end configuration
28211
- * of the asymptote.
28212
- *
28213
- * Return: either a coordinate (to be used as the resulting coordinate of
28214
- * the move) or a boolean, where `true` uses newCoord as the resulting
28215
- * coordinate, and `false` uses oldCoord as the resulting coordinate.
28216
- */
28217
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28218
- const y = asymptote[0][1];
28219
- return ___default["default"].all(coords, coord => coord[1] !== y);
28220
- },
28221
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28222
- const y = newCoord[1];
28223
- const isValid = ___default["default"].all(coords, coord => coord[1] > y) || ___default["default"].all(coords, coord => coord[1] < y);
28224
- if (isValid) {
28225
- return [oldCoord[0], y];
28226
- }
28227
- // Snap the asymptote as close as possible, i.e., if the user moves
28228
- // the mouse really quickly into an invalid region
28229
- const oldY = oldCoord[1];
28230
- const wasBelow = ___default["default"].all(coords, coord => coord[1] > oldY);
28231
- if (wasBelow) {
28232
- const bottomMost = ___default["default"].min(___default["default"].map(coords, coord => coord[1]));
28233
- return [oldCoord[0], bottomMost - graph.snapStep[1]];
28234
- }
28235
- const topMost = ___default["default"].max(___default["default"].map(coords, coord => coord[1]));
28236
- return [oldCoord[0], topMost + graph.snapStep[1]];
28237
- },
28238
- allowReflectOverAsymptote: true,
28239
- getCoefficients: function (coords, asymptote) {
28240
- const p1 = coords[0];
28241
- const p2 = coords[1];
28242
- const c = asymptote[0][1];
28243
- const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]);
28244
- const a = (p1[1] - c) / Math.exp(b * p1[0]);
28245
- return [a, b, c];
28246
- },
28247
- getFunctionForCoeffs: function (coeffs, x) {
28248
- const a = coeffs[0];
28249
- const b = coeffs[1];
28250
- const c = coeffs[2];
28251
- return a * Math.exp(b * x) + c;
28252
- },
28253
- getEquationString: function (coords, asymptote) {
28254
- if (!asymptote) {
28255
- return null;
28256
- }
28257
- const coeffs = this.getCoefficients(coords, asymptote);
28258
- const a = coeffs[0];
28259
- const b = coeffs[1];
28260
- const c = coeffs[2];
28261
- return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
28262
- }
28263
- });
28264
- const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28265
- url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
28266
- defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
28267
- defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
28268
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28269
- const x = asymptote[0][0];
28270
- return ___default["default"].all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
28271
- },
28272
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28273
- const x = newCoord[0];
28274
- const isValid = ___default["default"].all(coords, coord => coord[0] > x) || ___default["default"].all(coords, coord => coord[0] < x);
28275
- if (isValid) {
28276
- return [x, oldCoord[1]];
28277
- }
28278
- // Snap the asymptote as close as possible, i.e., if the user moves
28279
- // the mouse really quickly into an invalid region
28280
- const oldX = oldCoord[0];
28281
- const wasLeft = ___default["default"].all(coords, coord => coord[0] > oldX);
28282
- if (wasLeft) {
28283
- const leftMost = ___default["default"].min(___default["default"].map(coords, coord => coord[0]));
28284
- return [leftMost - graph.snapStep[0], oldCoord[1]];
28285
- }
28286
- const rightMost = ___default["default"].max(___default["default"].map(coords, coord => coord[0]));
28287
- return [rightMost + graph.snapStep[0], oldCoord[1]];
28288
- },
28289
- allowReflectOverAsymptote: true,
28290
- getCoefficients: function (coords, asymptote) {
28291
- // It's easiest to calculate the logarithm's coefficients by thinking
28292
- // about it as the inverse of the exponential, so we flip x and y and
28293
- // perform some algebra on the coefficients. This also unifies the
28294
- // logic between the two 'models'.
28295
- const flip = coord => [coord[1], coord[0]];
28296
- const inverseCoeffs = Exponential.getCoefficients(___default["default"].map(coords, flip), ___default["default"].map(asymptote, flip));
28297
- if (inverseCoeffs) {
28298
- const c = -inverseCoeffs[2] / inverseCoeffs[0];
28299
- const b = 1 / inverseCoeffs[0];
28300
- const a = 1 / inverseCoeffs[1];
28301
- return [a, b, c];
28302
- }
28303
- },
28304
- getFunctionForCoeffs: function (coeffs, x, asymptote) {
28305
- const a = coeffs[0];
28306
- const b = coeffs[1];
28307
- const c = coeffs[2];
28308
- return a * Math.log(b * x + c);
28309
- },
28310
- getEquationString: function (coords, asymptote) {
28311
- if (!asymptote) {
28312
- return null;
28313
- }
28314
- const coeffs = this.getCoefficients(coords, asymptote);
28315
- const a = coeffs[0];
28316
- const b = coeffs[1];
28317
- const c = coeffs[2];
28318
- return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28319
- }
28320
- });
28321
- const AbsoluteValue = ___default["default"].extend({}, PlotDefaults, {
28322
- url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
28323
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28324
- getCoefficients: function (coords) {
28325
- const p1 = coords[0];
28326
- const p2 = coords[1];
28327
- const denom = p2[0] - p1[0];
28328
- const num = p2[1] - p1[1];
28329
- if (denom === 0) {
28330
- return;
28331
- }
28332
- let m = Math.abs(num / denom);
28333
- if (p2[1] < p1[1]) {
28334
- m *= -1;
28335
- }
28336
- const horizontalOffset = p1[0];
28337
- const verticalOffset = p1[1];
28338
- return [m, horizontalOffset, verticalOffset];
28339
- },
28340
- getFunctionForCoeffs: function (coeffs, x) {
28341
- const m = coeffs[0];
28342
- const horizontalOffset = coeffs[1];
28343
- const verticalOffset = coeffs[2];
28344
- return m * Math.abs(x - horizontalOffset) + verticalOffset;
28345
- },
28346
- getEquationString: function (coords) {
28347
- const coeffs = this.getCoefficients(coords);
28348
- const m = coeffs[0];
28349
- const horizontalOffset = coeffs[1];
28350
- const verticalOffset = coeffs[2];
28351
- return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28352
- }
28353
- });
28354
-
28355
- /* Utility functions for dealing with graphing interfaces. */
28356
- const functionTypeMapping = {
28357
- linear: Linear,
28358
- quadratic: Quadratic,
28359
- sinusoid: Sinusoid,
28360
- tangent: Tangent,
28361
- exponential: Exponential,
28362
- logarithm: Logarithm,
28363
- absolute_value: AbsoluteValue
28364
- };
28365
- const allTypes = ___default["default"].keys(functionTypeMapping);
28366
- function functionForType(type) {
28367
- // @ts-expect-error: TypeScript doesn't know how to use deal with generics
28368
- // and conditional types in this way.
28369
- return functionTypeMapping[type];
28370
- }
28371
27071
  const getEquationString = props => {
28372
27072
  const plot = props.plot;
28373
27073
  if (plot.type && plot.coords) {
28374
- const handler = functionForType(plot.type);
27074
+ const handler = perseusCore.GrapherUtil.functionForType(plot.type);
28375
27075
  const result = handler.getEquationString(plot.coords, plot.asymptote);
28376
27076
  return result || "";
28377
27077
  }
@@ -28429,7 +27129,7 @@ const defaultPlotProps = (type, graph) => {
28429
27129
  // extra information. It would be valid to always submit the default
28430
27130
  // widget before even reading the question; you can't lose, but you
28431
27131
  // might get a free win.
28432
- const model = functionForType(type);
27132
+ const model = perseusCore.GrapherUtil.functionForType(type);
28433
27133
  const defaultAsymptote = "defaultAsymptote" in model ? model.defaultAsymptote : null;
28434
27134
  const gridStep = [1, 1];
28435
27135
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
@@ -28475,70 +27175,12 @@ const typeToButton = type => {
28475
27175
  value: type,
28476
27176
  title: capitalized,
28477
27177
  content: /*#__PURE__*/React__namespace.createElement("img", {
28478
- src: staticUrl(functionForType(type).url),
27178
+ src: staticUrl(perseusCore.GrapherUtil.functionForType(type).url),
28479
27179
  alt: capitalized
28480
27180
  })
28481
27181
  };
28482
27182
  };
28483
27183
 
28484
- function getCoefficientsByType(data) {
28485
- if (data.coords == null) {
28486
- return undefined;
28487
- }
28488
- if (data.type === "exponential" || data.type === "logarithm") {
28489
- const grader = functionForType(data.type);
28490
- return grader.getCoefficients(data.coords, data.asymptote);
28491
- } else if (data.type === "linear" || data.type === "quadratic" || data.type === "absolute_value" || data.type === "sinusoid" || data.type === "tangent") {
28492
- const grader = functionForType(data.type);
28493
- return grader.getCoefficients(data.coords);
28494
- } else {
28495
- throw new perseusCore.PerseusError("Invalid grapher type", perseusCore.Errors.InvalidInput);
28496
- }
28497
- }
28498
- function scoreGrapher(userInput, rubric) {
28499
- if (userInput.type !== rubric.correct.type) {
28500
- return {
28501
- type: "points",
28502
- earned: 0,
28503
- total: 1,
28504
- message: null
28505
- };
28506
- }
28507
-
28508
- // We haven't moved the coords
28509
- if (userInput.coords == null) {
28510
- return {
28511
- type: "invalid",
28512
- message: null
28513
- };
28514
- }
28515
-
28516
- // Get new function handler for grading
28517
- const grader = functionForType(userInput.type);
28518
- const guessCoeffs = getCoefficientsByType(userInput);
28519
- const correctCoeffs = getCoefficientsByType(rubric.correct);
28520
- if (guessCoeffs == null || correctCoeffs == null) {
28521
- return {
28522
- type: "invalid",
28523
- message: null
28524
- };
28525
- }
28526
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
28527
- return {
28528
- type: "points",
28529
- earned: 1,
28530
- total: 1,
28531
- message: null
28532
- };
28533
- }
28534
- return {
28535
- type: "points",
28536
- earned: 0,
28537
- total: 1,
28538
- message: null
28539
- };
28540
- }
28541
-
28542
27184
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
28543
27185
  const MovablePoint$3 = Graphie.MovablePoint;
28544
27186
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -28600,7 +27242,8 @@ class FunctionGrapher extends React__namespace.Component {
28600
27242
  return;
28601
27243
  }
28602
27244
  const functionProps = model.getPropsForCoeffs(coeffs, xRange);
28603
- return /*#__PURE__*/React__namespace.createElement(model.Movable, _extends({}, functionProps, {
27245
+ const Movable = movableTypeToComponent[model.movable];
27246
+ return /*#__PURE__*/React__namespace.createElement(Movable, _extends({}, functionProps, {
28604
27247
  key: this.props.model.url,
28605
27248
  range: xRange,
28606
27249
  style: style
@@ -28893,7 +27536,7 @@ class Grapher extends React__namespace.Component {
28893
27536
  setup: this._setupGraphie
28894
27537
  },
28895
27538
  onChange: this.handlePlotChanges,
28896
- model: type && functionForType(type),
27539
+ model: type && perseusCore.GrapherUtil.functionForType(type),
28897
27540
  coords: coords,
28898
27541
  asymptote: asymptote,
28899
27542
  static: this.props.static,
@@ -28942,7 +27585,7 @@ var Grapher$1 = {
28942
27585
  staticTransform: staticTransform$4,
28943
27586
  // TODO(LEMS-2656): remove TS suppression
28944
27587
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
28945
- scorer: scoreGrapher
27588
+ scorer: perseusScore.scoreGrapher
28946
27589
  };
28947
27590
 
28948
27591
  const getPromptJSON$f = rendererJSON => {
@@ -29128,31 +27771,6 @@ const getPromptJSON$e = () => {
29128
27771
  return getUnsupportedPromptJSON("iframe");
29129
27772
  };
29130
27773
 
29131
- function scoreIframe(userInput) {
29132
- // The iframe can tell us whether it's correct or incorrect,
29133
- // and pass an optional message
29134
- if (userInput.status === "correct") {
29135
- return {
29136
- type: "points",
29137
- earned: 1,
29138
- total: 1,
29139
- message: userInput.message || null
29140
- };
29141
- }
29142
- if (userInput.status === "incorrect") {
29143
- return {
29144
- type: "points",
29145
- earned: 0,
29146
- total: 1,
29147
- message: userInput.message || null
29148
- };
29149
- }
29150
- return {
29151
- type: "invalid",
29152
- message: "Keep going, you're not there yet!"
29153
- };
29154
- }
29155
-
29156
27774
  /**
29157
27775
  * This is an iframe widget. It is used for rendering an iframe that
29158
27776
  * then communicates its state via window.postMessage
@@ -29287,7 +27905,7 @@ var Iframe$1 = {
29287
27905
  hidden: true,
29288
27906
  // TODO(LEMS-2656): remove TS suppression
29289
27907
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
29290
- scorer: scoreIframe
27908
+ scorer: perseusScore.scoreIframe
29291
27909
  };
29292
27910
 
29293
27911
  const getPromptJSON$d = renderProps => {
@@ -30734,63 +29352,6 @@ function findIntersectionOfRays(_ref3, _ref4) {
30734
29352
  }
30735
29353
  }
30736
29354
 
30737
- // This file contains helper functions for working with angles.
30738
-
30739
- function convertDegreesToRadians(degrees) {
30740
- return degrees / 180 * Math.PI;
30741
- }
30742
-
30743
- // Returns a value between -180 and 180, inclusive. The angle is measured
30744
- // between the positive x-axis and the given vector.
30745
- function calculateAngleInDegrees(_ref) {
30746
- let [x, y] = _ref;
30747
- return Math.atan2(y, x) * 180 / Math.PI;
30748
- }
30749
-
30750
- // Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
30751
- function polar(r, th) {
30752
- if (typeof r === "number") {
30753
- r = [r, r];
30754
- }
30755
- th = th * Math.PI / 180;
30756
- return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
30757
- }
30758
- // This function calculates the angle between two points and an optional vertex.
30759
- // If the vertex is not provided, the angle is measured between the two points.
30760
- // This does not account for reflex angles or clockwise position.
30761
- const getAngleFromVertex = (point, vertex) => {
30762
- const x = point[0] - vertex[0];
30763
- const y = point[1] - vertex[1];
30764
- if (!x && !y) {
30765
- return 0;
30766
- }
30767
- return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
30768
- };
30769
-
30770
- // This function calculates the clockwise angle between three points,
30771
- // and is used to generate the labels and equation strings of the
30772
- // current angle for the interactive graph.
30773
- const getClockwiseAngle = function (coords) {
30774
- let allowReflexAngles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
30775
- const coordsCopy = [...coords];
30776
- // The coords are saved as [point1, vertex, point2] in the interactive graph
30777
- const areClockwise = clockwise$1([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
30778
-
30779
- // We may need to reverse the coordinates if we allow
30780
- // reflex angles and the points are not in clockwise order.
30781
- const shouldReverseCoords = !areClockwise || allowReflexAngles;
30782
-
30783
- // Reverse the coordinates accordingly to ensure the angle is calculated correctly
30784
- const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
30785
-
30786
- // Calculate the angles between the two points and get the difference
30787
- // between the two angles to get the clockwise angle.
30788
- const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
30789
- const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
30790
- const angle = (startAngle + 360 - endAngle) % 360;
30791
- return angle;
30792
- };
30793
-
30794
29355
  const defaultGraphConfig = {
30795
29356
  range: [[0, 1], [0, 1]],
30796
29357
  tickStep: [1, 1],
@@ -31923,7 +30484,8 @@ const LockedEllipse = props => {
31923
30484
  return /*#__PURE__*/React__namespace.createElement("g", {
31924
30485
  className: "locked-ellipse",
31925
30486
  "aria-label": hasAria ? ariaLabel : undefined,
31926
- "aria-hidden": !hasAria
30487
+ "aria-hidden": !hasAria,
30488
+ role: "img"
31927
30489
  }, /*#__PURE__*/React__namespace.createElement(mafs.Ellipse, {
31928
30490
  center: center,
31929
30491
  radius: radius,
@@ -31968,7 +30530,8 @@ const LockedFunction = props => {
31968
30530
  return /*#__PURE__*/React__namespace.createElement("g", {
31969
30531
  className: "locked-function",
31970
30532
  "aria-label": hasAria ? props.ariaLabel : undefined,
31971
- "aria-hidden": !hasAria
30533
+ "aria-hidden": !hasAria,
30534
+ role: "img"
31972
30535
  }, directionalAxis === "x" && /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, _extends({
31973
30536
  y: x => equation.eval({
31974
30537
  x
@@ -31999,6 +30562,9 @@ function SVGLine(props) {
31999
30562
  });
32000
30563
  }
32001
30564
 
30565
+ const {
30566
+ calculateAngleInDegrees: calculateAngleInDegrees$3
30567
+ } = kmath.angles;
32002
30568
  function Vector(props) {
32003
30569
  const {
32004
30570
  tail,
@@ -32008,7 +30574,7 @@ function Vector(props) {
32008
30574
  } = props;
32009
30575
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
32010
30576
  const direction = mafs.vec.sub(tipPx, tailPx);
32011
- const angle = calculateAngleInDegrees(direction);
30577
+ const angle = calculateAngleInDegrees$3(direction);
32012
30578
  return /*#__PURE__*/React__namespace.createElement("g", {
32013
30579
  style: {
32014
30580
  stroke: color,
@@ -32025,6 +30591,14 @@ function Vector(props) {
32025
30591
  }));
32026
30592
  }
32027
30593
 
30594
+ function srFormatNumber(a, locale) {
30595
+ // adding zero here converts negative zero to positive zero.
30596
+ return (0 + a).toLocaleString(locale, {
30597
+ maximumFractionDigits: 3,
30598
+ useGrouping: false // no thousands separators
30599
+ });
30600
+ }
30601
+
32028
30602
  /**
32029
30603
  * Given a ray and a rectangular box, find the point where the ray intersects
32030
30604
  * the edge of the box. Assumes the `initialPoint` is inside the box.
@@ -32073,7 +30647,45 @@ function getArrayWithoutDuplicates(array) {
32073
30647
  }
32074
30648
  return returnArray;
32075
30649
  }
30650
+ function getSlopeStringForLine(line, strings) {
30651
+ const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
30652
+ if (!Number.isFinite(slope)) {
30653
+ return strings.srLinearGraphSlopeVertical;
30654
+ }
30655
+ if (slope === 0) {
30656
+ return strings.srLinearGraphSlopeHorizontal;
30657
+ }
30658
+ return slope > 0 ? strings.srLinearGraphSlopeIncreasing : strings.srLinearGraphSlopeDecreasing;
30659
+ }
30660
+ function getInterceptStringForLine(line, strings, locale) {
30661
+ const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
30662
+ const xIntercept = (0 - line[0][1]) / slope + line[0][0];
30663
+ const yIntercept = line[0][1] - slope * line[0][0];
30664
+
30665
+ // Check if the line fully overlaps with an axis.
30666
+ const overlapsXAxis = line[0][1] === 0 && line[1][1] === 0;
30667
+ const overlapsYAxis = line[0][0] === 0 && line[1][0] === 0;
30668
+ const hasXIntercept = Number.isFinite(xIntercept) && !overlapsXAxis;
30669
+ const hasYIntercept = Number.isFinite(yIntercept) && !overlapsYAxis;
30670
+ if (hasXIntercept && hasYIntercept) {
30671
+ // Describe both intercepts in the same sentence.
30672
+ return xIntercept === 0 && yIntercept === 0 ? strings.srLinearGraphOriginIntercept : strings.srLinearGraphBothIntercepts({
30673
+ xIntercept: srFormatNumber(xIntercept, locale),
30674
+ yIntercept: srFormatNumber(yIntercept, locale)
30675
+ });
30676
+ }
30677
+
30678
+ // Describe only one intercept.
30679
+ return hasXIntercept ? strings.srLinearGraphXOnlyIntercept({
30680
+ xIntercept: srFormatNumber(xIntercept, locale)
30681
+ }) : strings.srLinearGraphYOnlyIntercept({
30682
+ yIntercept: srFormatNumber(yIntercept, locale)
30683
+ });
30684
+ }
32076
30685
 
30686
+ const {
30687
+ calculateAngleInDegrees: calculateAngleInDegrees$2
30688
+ } = kmath.angles;
32077
30689
  const LockedLine = props => {
32078
30690
  const {
32079
30691
  color,
@@ -32105,7 +30717,7 @@ const LockedLine = props => {
32105
30717
  // eslint-disable-next-line react-hooks/rules-of-hooks
32106
30718
  const [tailPx, tipPx] = useTransformVectorsToPixels(point2.coord, point1.coord);
32107
30719
  const direction = mafs.vec.sub(tailPx, tipPx);
32108
- let angle = calculateAngleInDegrees(direction);
30720
+ let angle = calculateAngleInDegrees$2(direction);
32109
30721
  const startArrowHead = kind !== "segment" && /*#__PURE__*/React__namespace.createElement(Arrowhead, {
32110
30722
  angle: angle,
32111
30723
  tip: arrowTip,
@@ -32128,7 +30740,8 @@ const LockedLine = props => {
32128
30740
  return /*#__PURE__*/React__namespace.createElement("g", {
32129
30741
  className: kind === "ray" ? "locked-ray" : "locked-line",
32130
30742
  "aria-label": hasAria ? ariaLabel : undefined,
32131
- "aria-hidden": !hasAria
30743
+ "aria-hidden": !hasAria,
30744
+ role: "img"
32132
30745
  }, line, showPoint1 && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
32133
30746
  x: point1.coord[X],
32134
30747
  y: point1.coord[Y],
@@ -32164,7 +30777,8 @@ const LockedPoint = props => {
32164
30777
  return /*#__PURE__*/React__namespace.createElement("g", {
32165
30778
  className: "locked-point",
32166
30779
  "aria-label": hasAria ? ariaLabel : undefined,
32167
- "aria-hidden": !hasAria
30780
+ "aria-hidden": !hasAria,
30781
+ role: "img"
32168
30782
  }, /*#__PURE__*/React__namespace.createElement(mafs.Point, {
32169
30783
  x: x,
32170
30784
  y: y,
@@ -32190,7 +30804,8 @@ const LockedPolygon = props => {
32190
30804
  return /*#__PURE__*/React__namespace.createElement("g", {
32191
30805
  className: "locked-polygon",
32192
30806
  "aria-label": hasAria ? props.ariaLabel : undefined,
32193
- "aria-hidden": !hasAria
30807
+ "aria-hidden": !hasAria,
30808
+ role: "img"
32194
30809
  }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32195
30810
  points: [...points],
32196
30811
  fillOpacity: perseusCore.lockedFigureFillStyles[fillStyle],
@@ -32224,7 +30839,8 @@ const LockedVector = props => {
32224
30839
  return /*#__PURE__*/React__namespace.createElement("g", {
32225
30840
  className: "locked-vector",
32226
30841
  "aria-label": hasAria ? ariaLabel : undefined,
32227
- "aria-hidden": !hasAria
30842
+ "aria-hidden": !hasAria,
30843
+ role: "img"
32228
30844
  }, /*#__PURE__*/React__namespace.createElement(Vector, {
32229
30845
  tail: tail,
32230
30846
  tip: tip,
@@ -32513,6 +31129,12 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
32513
31129
  in: "SourceGraphic"
32514
31130
  })));
32515
31131
 
31132
+ const {
31133
+ clockwise: clockwise$1
31134
+ } = kmath.geometry;
31135
+ const {
31136
+ getAngleFromVertex: getAngleFromVertex$1
31137
+ } = kmath.angles;
32516
31138
  const PolygonAngle = _ref => {
32517
31139
  let {
32518
31140
  centerPoint,
@@ -32603,8 +31225,8 @@ const Angle = _ref2 => {
32603
31225
  const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32604
31226
 
32605
31227
  // Calculate the angles between the two points
32606
- const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32607
- const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
31228
+ const startAngle = getAngleFromVertex$1(clockwiseCoords[0], vertex);
31229
+ const endAngle = getAngleFromVertex$1(clockwiseCoords[1], vertex);
32608
31230
  const angle = (startAngle + 360 - endAngle) % 360;
32609
31231
 
32610
31232
  // Check if the angle is reflexive
@@ -32965,14 +31587,6 @@ function range(min, max) {
32965
31587
  return result;
32966
31588
  }
32967
31589
 
32968
- function srFormatNumber(a, locale) {
32969
- // adding zero here converts negative zero to positive zero.
32970
- return (0 + a).toLocaleString(locale, {
32971
- maximumFractionDigits: 3,
32972
- useGrouping: false // no thousands separators
32973
- });
32974
- }
32975
-
32976
31590
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
32977
31591
  // graph.
32978
31592
  const hitboxSizePx = 48;
@@ -33365,6 +31979,11 @@ const MovablePoint$1 = /*#__PURE__*/React__namespace.forwardRef((props, pointRef
33365
31979
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle, visiblePoint);
33366
31980
  });
33367
31981
 
31982
+ const {
31983
+ calculateAngleInDegrees: calculateAngleInDegrees$1,
31984
+ getClockwiseAngle: getClockwiseAngle$2,
31985
+ polar: polar$1
31986
+ } = kmath.angles;
33368
31987
  function renderAngleGraph(state, dispatch) {
33369
31988
  return {
33370
31989
  graph: /*#__PURE__*/React__namespace.createElement(AngleGraph, {
@@ -33435,7 +32054,7 @@ function AngleGraph(props) {
33435
32054
  strings,
33436
32055
  locale
33437
32056
  } = usePerseusI18n();
33438
- const angleMeasure = srFormatNumber(getClockwiseAngle([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
32057
+ const angleMeasure = srFormatNumber(getClockwiseAngle$2([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
33439
32058
  const wholeAngleAriaLabel = strings.srAngleGraphAriaLabel;
33440
32059
  const wholeAngleDescription = strings.srAngleGraphAriaDescription({
33441
32060
  angleMeasure,
@@ -33503,9 +32122,7 @@ function AngleGraph(props) {
33503
32122
  ariaLabel: initialSideAriaLabel
33504
32123
  }), /*#__PURE__*/React__namespace.createElement("g", {
33505
32124
  id: "angle-description",
33506
- style: {
33507
- display: "hidden"
33508
- }
32125
+ style: a11y.srOnly
33509
32126
  }, wholeAngleDescription));
33510
32127
  }
33511
32128
  const positiveX = [1, 0];
@@ -33513,7 +32130,7 @@ const negativeX = [-1, 0];
33513
32130
  const positiveY = [0, 1];
33514
32131
  const negativeY = [0, -1];
33515
32132
  function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33516
- const currentAngle = calculateAngleInDegrees(mafs.vec.sub(sidePoint, vertex));
32133
+ const currentAngle = calculateAngleInDegrees$1(mafs.vec.sub(sidePoint, vertex));
33517
32134
 
33518
32135
  // Find the rays that start at the current point and point up, down, left
33519
32136
  // and right.
@@ -33530,8 +32147,8 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33530
32147
 
33531
32148
  // find the rays that start from the vertex and point in the direction of
33532
32149
  // the angles we just computed.
33533
- const counterClockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepCounterClockwise))];
33534
- const clockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepClockwise))];
32150
+ const counterClockwiseRay = [vertex, mafs.vec.add(vertex, polar$1(1, oneStepCounterClockwise))];
32151
+ const clockwiseRay = [vertex, mafs.vec.add(vertex, polar$1(1, oneStepClockwise))];
33535
32152
 
33536
32153
  // find the intersections of those rays with the horizontal and vertical
33537
32154
  // rays extending from the sidePoint. These intersections are the points to
@@ -33548,6 +32165,9 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33548
32165
  };
33549
32166
  }
33550
32167
 
32168
+ const {
32169
+ clockwise
32170
+ } = kmath.geometry;
33551
32171
  function getGradableGraph(state, initialGraph) {
33552
32172
  if (!state.hasBeenInteractedWith) {
33553
32173
  return {
@@ -33629,7 +32249,7 @@ function getGradableGraph(state, initialGraph) {
33629
32249
  // by the order of the points in the coords array, and we want to maintain
33630
32250
  // the same angle scoring with the legacy graph (which had a bug).
33631
32251
  // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33632
- const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
32252
+ const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
33633
32253
  const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33634
32254
  const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33635
32255
  return {
@@ -33742,14 +32362,10 @@ function CircleGraph(props) {
33742
32362
  }
33743
32363
  }), /*#__PURE__*/React__namespace.createElement("g", {
33744
32364
  id: radiusId,
33745
- style: {
33746
- display: "hidden"
33747
- }
32365
+ style: a11y.srOnly
33748
32366
  }, srCircleRadius), /*#__PURE__*/React__namespace.createElement("g", {
33749
32367
  id: outerPointsId,
33750
- style: {
33751
- display: "hidden"
33752
- }
32368
+ style: a11y.srOnly
33753
32369
  }, srCircleOuterPoints));
33754
32370
  }
33755
32371
  function MovableCircle(props) {
@@ -33935,49 +32551,16 @@ const LinearGraph = (props, key) => {
33935
32551
  point2X: srFormatNumber(line[1][0], locale),
33936
32552
  point2Y: srFormatNumber(line[1][1], locale)
33937
32553
  });
33938
-
33939
- // Slope description
33940
- const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
33941
- let slopeString = "";
33942
- if (slope === Infinity || slope === -Infinity) {
33943
- slopeString = strings.srLinearGraphSlopeVertical;
33944
- } else if (slope === 0) {
33945
- slopeString = strings.srLinearGraphSlopeHorizontal;
33946
- } else {
33947
- slopeString = slope > 0 ? strings.srLinearGraphSlopeIncreasing : strings.srLinearGraphSlopeDecreasing;
33948
- }
33949
-
33950
- // Intersection description
33951
- const xIntercept = (0 - line[0][1]) / slope + line[0][0];
33952
- const yIntercept = line[0][1] - slope * line[0][0];
33953
- const hasXIntercept = xIntercept !== Infinity && xIntercept !== -Infinity;
33954
- const hasYIntercept = yIntercept !== Infinity && yIntercept !== -Infinity;
33955
- let interceptString;
33956
- if (hasXIntercept && hasYIntercept) {
33957
- // Describe both intercepts in the same sentence.
33958
- interceptString = xIntercept === 0 && yIntercept === 0 ? strings.srLinearGraphOriginIntercept : strings.srLinearGraphBothIntercepts({
33959
- xIntercept: srFormatNumber(xIntercept, locale),
33960
- yIntercept: srFormatNumber(yIntercept, locale)
33961
- });
33962
- } else {
33963
- // Describe only one intercept.
33964
- interceptString = hasXIntercept ? strings.srLinearGraphXOnlyIntercept({
33965
- xIntercept: srFormatNumber(xIntercept, locale)
33966
- }) : strings.srLinearGraphYOnlyIntercept({
33967
- yIntercept: srFormatNumber(yIntercept, locale)
33968
- });
33969
- }
32554
+ const slopeString = getSlopeStringForLine(line, strings);
32555
+ const interceptString = getInterceptStringForLine(line, strings, locale);
33970
32556
 
33971
32557
  // Linear graphs only have one line
33972
32558
  // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
33973
32559
  // and another action for moving multiple lines
33974
32560
  return /*#__PURE__*/React__namespace.createElement("g", {
33975
- className: "linear-graph-container"
33976
32561
  // Outer line minimal description
33977
- ,
33978
32562
  "aria-label": strings.srLinearGraph,
33979
- "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`,
33980
- role: "figure"
32563
+ "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`
33981
32564
  }, /*#__PURE__*/React__namespace.createElement(MovableLine, {
33982
32565
  key: 0,
33983
32566
  ariaLabels: {
@@ -33996,19 +32579,13 @@ const LinearGraph = (props, key) => {
33996
32579
  color: "var(--movable-line-stroke-color)"
33997
32580
  }), /*#__PURE__*/React__namespace.createElement("g", {
33998
32581
  id: pointsDescriptionId,
33999
- style: {
34000
- display: "hidden"
34001
- }
32582
+ style: a11y.srOnly
34002
32583
  }, linearGraphPointsDescription), /*#__PURE__*/React__namespace.createElement("g", {
34003
32584
  id: interceptDescriptionId,
34004
- style: {
34005
- display: "hidden"
34006
- }
32585
+ style: a11y.srOnly
34007
32586
  }, interceptString), /*#__PURE__*/React__namespace.createElement("g", {
34008
32587
  id: slopeDescriptionId,
34009
- style: {
34010
- display: "hidden"
34011
- }
32588
+ style: a11y.srOnly
34012
32589
  }, slopeString));
34013
32590
  };
34014
32591
 
@@ -34018,7 +32595,9 @@ function renderLinearSystemGraph(state, dispatch) {
34018
32595
  graphState: state,
34019
32596
  dispatch: dispatch
34020
32597
  }),
34021
- interactiveElementsDescription: null
32598
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(LinearSystemGraphDescription, {
32599
+ state: state
32600
+ })
34022
32601
  };
34023
32602
  }
34024
32603
  const LinearSystemGraph = props => {
@@ -34028,9 +32607,56 @@ const LinearSystemGraph = props => {
34028
32607
  const {
34029
32608
  coords: lines
34030
32609
  } = props.graphState;
34031
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
32610
+ const {
32611
+ strings,
32612
+ locale
32613
+ } = usePerseusI18n();
32614
+ const id = React__namespace.useId();
32615
+ const linesAriaInfo = lines.map((line, i) => {
32616
+ return {
32617
+ pointsDescriptionId: `${id}-line${i + 1}-points`,
32618
+ interceptDescriptionId: `${id}-line${i + 1}-intercept`,
32619
+ slopeDescriptionId: `${id}-line${i + 1}-slope`,
32620
+ pointsDescription: strings.srLinearSystemPoints({
32621
+ lineNumber: i + 1,
32622
+ point1X: srFormatNumber(line[0][0], locale),
32623
+ point1Y: srFormatNumber(line[0][1], locale),
32624
+ point2X: srFormatNumber(line[1][0], locale),
32625
+ point2Y: srFormatNumber(line[1][1], locale)
32626
+ }),
32627
+ interceptDescription: getInterceptStringForLine(line, strings, locale),
32628
+ slopeDescription: getSlopeStringForLine(line, strings)
32629
+ };
32630
+ });
32631
+ return /*#__PURE__*/React__namespace.createElement("g", {
32632
+ "aria-label": strings.srLinearSystemGraph,
32633
+ "aria-describedby": linesAriaInfo.map(_ref => {
32634
+ let {
32635
+ pointsDescriptionId,
32636
+ interceptDescriptionId,
32637
+ slopeDescriptionId
32638
+ } = _ref;
32639
+ return `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`;
32640
+ }).join(" ")
32641
+ }, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
34032
32642
  key: i,
34033
32643
  points: line,
32644
+ ariaLabels: {
32645
+ point1AriaLabel: strings.srLinearSystemPoint({
32646
+ lineNumber: i + 1,
32647
+ pointSequence: 1,
32648
+ x: srFormatNumber(line[0][0], locale),
32649
+ y: srFormatNumber(line[0][1], locale)
32650
+ }),
32651
+ point2AriaLabel: strings.srLinearSystemPoint({
32652
+ lineNumber: i + 1,
32653
+ pointSequence: 2,
32654
+ x: srFormatNumber(line[1][0], locale),
32655
+ y: srFormatNumber(line[1][1], locale)
32656
+ }),
32657
+ grabHandleAriaLabel: `${linesAriaInfo[i].interceptDescription} ${linesAriaInfo[i].slopeDescription}`
32658
+ },
32659
+ ariaDescribedBy: `${linesAriaInfo[i].interceptDescriptionId} ${linesAriaInfo[i].slopeDescriptionId}`,
34034
32660
  onMoveLine: delta => {
34035
32661
  dispatch(actions.linearSystem.moveLine(i, delta));
34036
32662
  },
@@ -34040,8 +32666,67 @@ const LinearSystemGraph = props => {
34040
32666
  },
34041
32667
  onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
34042
32668
  color: "var(--movable-line-stroke-color)"
34043
- })), ";");
32669
+ })), linesAriaInfo.map(_ref2 => {
32670
+ let {
32671
+ pointsDescriptionId,
32672
+ interceptDescriptionId,
32673
+ slopeDescriptionId,
32674
+ pointsDescription,
32675
+ interceptDescription,
32676
+ slopeDescription
32677
+ } = _ref2;
32678
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("g", {
32679
+ key: pointsDescriptionId,
32680
+ id: pointsDescriptionId,
32681
+ style: a11y.srOnly
32682
+ }, pointsDescription), /*#__PURE__*/React__namespace.createElement("g", {
32683
+ key: interceptDescriptionId,
32684
+ id: interceptDescriptionId,
32685
+ style: a11y.srOnly
32686
+ }, interceptDescription), /*#__PURE__*/React__namespace.createElement("g", {
32687
+ key: slopeDescriptionId,
32688
+ id: slopeDescriptionId,
32689
+ style: a11y.srOnly
32690
+ }, slopeDescription));
32691
+ }));
34044
32692
  };
32693
+ function LinearSystemGraphDescription(_ref3) {
32694
+ let {
32695
+ state
32696
+ } = _ref3;
32697
+ // The reason that LinearSystemGraphDescription is a component (rather
32698
+ // than a function that returns a string) is because it needs to use a
32699
+ // hook: `usePerseusI18n`.
32700
+ const i18n = usePerseusI18n();
32701
+ return describeLinearSystemGraph(state, i18n);
32702
+ }
32703
+
32704
+ // Exported for testing
32705
+ function describeLinearSystemGraph(state, i18n) {
32706
+ const {
32707
+ strings,
32708
+ locale
32709
+ } = i18n;
32710
+ const {
32711
+ coords: lines
32712
+ } = state;
32713
+ const graphDescription = strings.srLinearSystemGraph;
32714
+ const lineDescriptions = lines.map((line, i) => {
32715
+ const point1 = line[0];
32716
+ const point2 = line[1];
32717
+ return strings.srLinearSystemPoints({
32718
+ lineNumber: i + 1,
32719
+ point1X: srFormatNumber(point1[0], locale),
32720
+ point1Y: srFormatNumber(point1[1], locale),
32721
+ point2X: srFormatNumber(point2[0], locale),
32722
+ point2Y: srFormatNumber(point2[1], locale)
32723
+ });
32724
+ });
32725
+ const allDescriptions = [graphDescription, ...lineDescriptions];
32726
+ return strings.srInteractiveElements({
32727
+ elements: allDescriptions.join(" ")
32728
+ });
32729
+ }
34045
32730
 
34046
32731
  function renderPointGraph(state, dispatch) {
34047
32732
  return {
@@ -34483,7 +33168,7 @@ function QuadraticGraph(props) {
34483
33168
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
34484
33169
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
34485
33170
  const coeffRef = React__namespace.useRef([0, 0, 0]);
34486
- const coeffs = getQuadraticCoefficients$1(coords);
33171
+ const coeffs = getQuadraticCoefficients$2(coords);
34487
33172
  if (coeffs !== undefined) {
34488
33173
  coeffRef.current = coeffs;
34489
33174
  }
@@ -34508,7 +33193,7 @@ function QuadraticGraph(props) {
34508
33193
  // These equations were originally set up in 2013 and may require some
34509
33194
  // additional comments to help clarify the quadratic formula manipulations
34510
33195
  // Origin: https://phabricator.khanacademy.org/D2413
34511
- const getQuadraticCoefficients$1 = coords => {
33196
+ const getQuadraticCoefficients$2 = coords => {
34512
33197
  const p1 = coords[0];
34513
33198
  const p2 = coords[1];
34514
33199
  const p3 = coords[2];
@@ -34531,7 +33216,9 @@ function renderRayGraph(state, dispatch) {
34531
33216
  graphState: state,
34532
33217
  dispatch: dispatch
34533
33218
  }),
34534
- interactiveElementsDescription: null
33219
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(RayGraphDescription, {
33220
+ state: state
33221
+ })
34535
33222
  };
34536
33223
  }
34537
33224
  const RayGraph = props => {
@@ -34543,18 +33230,104 @@ const RayGraph = props => {
34543
33230
  } = props.graphState;
34544
33231
  const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
34545
33232
  const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
33233
+ const {
33234
+ strings,
33235
+ locale
33236
+ } = usePerseusI18n();
33237
+ const id = React__namespace.useId();
33238
+ const pointsDescriptionId = id + "-points";
33239
+
33240
+ // Aria label strings
33241
+ const {
33242
+ srRayGraph,
33243
+ srRayPoints,
33244
+ srRayEndpoint,
33245
+ srRayTerminalPoint,
33246
+ srRayGrabHandle
33247
+ } = describeRayGraph(props.graphState, {
33248
+ strings,
33249
+ locale
33250
+ });
34546
33251
 
34547
33252
  // Ray graphs only have one line
34548
- return /*#__PURE__*/React__namespace.createElement(MovableLine, {
33253
+ return /*#__PURE__*/React__namespace.createElement("g", {
33254
+ // Outer line minimal description
33255
+ "aria-label": srRayGraph,
33256
+ "aria-describedby": pointsDescriptionId
33257
+ }, /*#__PURE__*/React__namespace.createElement(MovableLine, {
34549
33258
  points: line,
33259
+ ariaLabels: {
33260
+ point1AriaLabel: srRayEndpoint,
33261
+ point2AriaLabel: srRayTerminalPoint,
33262
+ grabHandleAriaLabel: srRayGrabHandle
33263
+ },
34550
33264
  onMoveLine: handleMoveLine,
34551
33265
  onMovePoint: handleMovePoint,
34552
33266
  extend: {
34553
33267
  start: false,
34554
33268
  end: true
34555
33269
  }
34556
- });
33270
+ }), /*#__PURE__*/React__namespace.createElement("g", {
33271
+ id: pointsDescriptionId,
33272
+ style: a11y.srOnly
33273
+ }, srRayPoints));
34557
33274
  };
33275
+ function RayGraphDescription(_ref) {
33276
+ let {
33277
+ state
33278
+ } = _ref;
33279
+ // The reason that RayGraphDescription is a component (rather than a
33280
+ // function that returns a string) is because it needs to use a
33281
+ // hook: `usePerseusI18n`.
33282
+ const i18n = usePerseusI18n();
33283
+ const strings = describeRayGraph(state, i18n);
33284
+ return strings.srRayInteractiveElement;
33285
+ }
33286
+
33287
+ // Exported for testing
33288
+ function describeRayGraph(state, i18n) {
33289
+ const {
33290
+ coords: line
33291
+ } = state;
33292
+ const {
33293
+ strings,
33294
+ locale
33295
+ } = i18n;
33296
+
33297
+ // Aria label strings
33298
+ const srRayGraph = strings.srRayGraph;
33299
+ const srRayPoints = strings.srRayPoints({
33300
+ point1X: srFormatNumber(line[0][0], locale),
33301
+ point1Y: srFormatNumber(line[0][1], locale),
33302
+ point2X: srFormatNumber(line[1][0], locale),
33303
+ point2Y: srFormatNumber(line[1][1], locale)
33304
+ });
33305
+ const srRayEndpoint = strings.srRayEndpoint({
33306
+ x: srFormatNumber(line[0][0], locale),
33307
+ y: srFormatNumber(line[0][1], locale)
33308
+ });
33309
+ const srRayTerminalPoint = strings.srRayTerminalPoint({
33310
+ x: srFormatNumber(line[1][0], locale),
33311
+ y: srFormatNumber(line[1][1], locale)
33312
+ });
33313
+ const srRayGrabHandle = strings.srRayGrabHandle({
33314
+ point1X: srFormatNumber(line[0][0], locale),
33315
+ point1Y: srFormatNumber(line[0][1], locale),
33316
+ point2X: srFormatNumber(line[1][0], locale),
33317
+ point2Y: srFormatNumber(line[1][1], locale)
33318
+ });
33319
+ const srRayInteractiveElement = strings.srInteractiveElements({
33320
+ elements: [srRayGraph, srRayPoints].join(" ")
33321
+ });
33322
+ return {
33323
+ srRayGraph,
33324
+ srRayPoints,
33325
+ srRayEndpoint,
33326
+ srRayTerminalPoint,
33327
+ srRayGrabHandle,
33328
+ srRayInteractiveElement
33329
+ };
33330
+ }
34558
33331
 
34559
33332
  function renderSegmentGraph(state, dispatch) {
34560
33333
  return {
@@ -34669,6 +33442,10 @@ const getSinusoidCoefficients$1 = coords => {
34669
33442
  };
34670
33443
  };
34671
33444
 
33445
+ const {
33446
+ calculateAngleInDegrees,
33447
+ convertDegreesToRadians
33448
+ } = kmath.angles;
34672
33449
  const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
34673
33450
 
34674
33451
  // The vector from the center of the protractor to the top left corner of the
@@ -35384,6 +34161,10 @@ function mafsStateToInteractiveGraph(state, originalGraph) {
35384
34161
  }
35385
34162
  }
35386
34163
 
34164
+ const {
34165
+ magnitude: magnitude$2,
34166
+ vector: vector$2
34167
+ } = kmath.geometry;
35387
34168
  function initializeGraphState(params) {
35388
34169
  const {
35389
34170
  graph,
@@ -35671,7 +34452,7 @@ const getAngleCoords = params => {
35671
34452
  defaultCoords = normalizePoints(range, step, defaultCoords, true);
35672
34453
 
35673
34454
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35674
- const radius = magnitude(vector(...defaultCoords));
34455
+ const radius = magnitude$2(vector$2(...defaultCoords));
35675
34456
 
35676
34457
  // We're adding a placeholder for the third point to appease ts and so that we
35677
34458
  // can calculate it after we've adjusted the first point by the angleOffsetDeg
@@ -35685,6 +34466,24 @@ const getAngleCoords = params => {
35685
34466
  return coords;
35686
34467
  };
35687
34468
 
34469
+ const {
34470
+ getAngleFromVertex,
34471
+ getClockwiseAngle: getClockwiseAngle$1,
34472
+ polar
34473
+ } = kmath.angles;
34474
+ const {
34475
+ angleMeasures: angleMeasures$1,
34476
+ ccw: ccw$1,
34477
+ lawOfCosines: lawOfCosines$1,
34478
+ magnitude: magnitude$1,
34479
+ polygonSidesIntersect,
34480
+ reverseVector,
34481
+ sign: sign$1,
34482
+ vector: vector$1
34483
+ } = kmath.geometry;
34484
+ const {
34485
+ getQuadraticCoefficients: getQuadraticCoefficients$1
34486
+ } = kmath.coefficients;
35688
34487
  const minDistanceBetweenAngleVertexAndSidePoint = 2;
35689
34488
  function interactiveGraphReducer(state, action) {
35690
34489
  switch (action.type) {
@@ -36229,11 +35028,9 @@ const getChange = (coords, delta, constraintOpts) => {
36229
35028
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
36230
35029
  return [dx, dy];
36231
35030
  };
36232
- const eq$2 = Util.eq;
36233
-
36234
35031
  // Less than or approximately equal
36235
35032
  function leq$1(a, b) {
36236
- return a < b || eq$2(a, b);
35033
+ return a < b || perseusCore.approximateEqual(a, b);
36237
35034
  }
36238
35035
  function boundAndSnapToGrid(point, _ref) {
36239
35036
  let {
@@ -36383,7 +35180,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36383
35180
  });
36384
35181
 
36385
35182
  // Gets the radian angles between the coords and maps them to degrees
36386
- const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
35183
+ const angles = angleMeasures$1(coordsCopy).map(angle => angle * 180 / Math.PI);
36387
35184
 
36388
35185
  // Gets the relative index of a point
36389
35186
  const rel = j => {
@@ -36395,7 +35192,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36395
35192
  angles[rel(j)] = Math.round(angles[rel(j)]);
36396
35193
  });
36397
35194
  const getAngle = function (a, vertex, b) {
36398
- const angle = getClockwiseAngle([coordsCopy[rel(a)], coordsCopy[rel(vertex)], coordsCopy[rel(b)]]);
35195
+ const angle = getClockwiseAngle$1([coordsCopy[rel(a)], coordsCopy[rel(vertex)], coordsCopy[rel(b)]]);
36399
35196
  return angle;
36400
35197
  };
36401
35198
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -36407,9 +35204,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36407
35204
  })) {
36408
35205
  return startingPoint;
36409
35206
  }
36410
- const knownSide = magnitude(
35207
+ const knownSide = magnitude$1(
36411
35208
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
36412
- vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
35209
+ vector$1(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
36413
35210
 
36414
35211
  // Returns true if the points form a counter-clockwise turn;
36415
35212
  // a.k.a if the point is on the left or right of the polygon.
@@ -36417,7 +35214,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36417
35214
  // we want to add the inner angle to the outer angle, and if it's on the right,
36418
35215
  // we want to subtract the inner angle from the outer angle. The angle solved
36419
35216
  // for is then used in the polar function to determine the new point.
36420
- const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
35217
+ const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
36421
35218
 
36422
35219
  // Solve for side by using the law of sines
36423
35220
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
@@ -36456,7 +35253,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36456
35253
  };
36457
35254
  const sides = ___default["default"].map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
36458
35255
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36459
- return magnitude(vector(...coordsCopy));
35256
+ return magnitude$1(vector$1(...coordsCopy));
36460
35257
  });
36461
35258
 
36462
35259
  // Round the sides to left and right of the current point
@@ -36471,7 +35268,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36471
35268
 
36472
35269
  // Solve for angle by using the law of cosines
36473
35270
  // Angle at the first vertex of the polygon
36474
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
35271
+ const innerAngle = lawOfCosines$1(sides[0], sides[2], sides[1]);
36475
35272
 
36476
35273
  // Angle at the second vertex of the polygon
36477
35274
  const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
@@ -36482,7 +35279,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36482
35279
  // we want to add the inner angle to the outer angle, and if it's on the right,
36483
35280
  // we want to subtract the inner angle from the outer angle. The angle solved
36484
35281
  // for is then used in the polar function to determine the new point.
36485
- const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
35282
+ const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
36486
35283
 
36487
35284
  // Uses the length of the first side of the polygon (radial coordinate)
36488
35285
  // and the angle between the first and second sides of the
@@ -36616,216 +35413,26 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36616
35413
  }));
36617
35414
  });
36618
35415
 
36619
- const eq$1 = Util.eq;
36620
- const deepEq$3 = Util.deepEq;
36621
- function scoreInteractiveGraph(userInput, rubric) {
36622
- // None-type graphs are not graded
36623
- if (userInput.type === "none" && rubric.correct.type === "none") {
36624
- return {
36625
- type: "points",
36626
- earned: 0,
36627
- total: 0,
36628
- message: null
36629
- };
36630
- }
36631
-
36632
- // When nothing has moved, there will neither be coords nor the
36633
- // circle's center/radius fields. When those fields are absent, skip
36634
- // all these checks; just go mark the answer as empty.
36635
- const hasValue = Boolean(
36636
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
36637
- userInput.coords ||
36638
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
36639
- userInput.center && userInput.radius);
36640
- if (userInput.type === rubric.correct.type && hasValue) {
36641
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
36642
- const guess = userInput.coords;
36643
- const correct = rubric.correct.coords;
36644
-
36645
- // If both of the guess points are on the correct line, it's
36646
- // correct.
36647
- if (collinear(correct[0], correct[1], guess[0]) && collinear(correct[0], correct[1], guess[1])) {
36648
- return {
36649
- type: "points",
36650
- earned: 1,
36651
- total: 1,
36652
- message: null
36653
- };
36654
- }
36655
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
36656
- const guess = userInput.coords;
36657
- const correct = rubric.correct.coords;
36658
- if (collinear(correct[0][0], correct[0][1], guess[0][0]) && collinear(correct[0][0], correct[0][1], guess[0][1]) && collinear(correct[1][0], correct[1][1], guess[1][0]) && collinear(correct[1][0], correct[1][1], guess[1][1]) || collinear(correct[0][0], correct[0][1], guess[1][0]) && collinear(correct[0][0], correct[0][1], guess[1][1]) && collinear(correct[1][0], correct[1][1], guess[0][0]) && collinear(correct[1][0], correct[1][1], guess[0][1])) {
36659
- return {
36660
- type: "points",
36661
- earned: 1,
36662
- total: 1,
36663
- message: null
36664
- };
36665
- }
36666
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
36667
- // If the parabola coefficients match, it's correct.
36668
- const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36669
- const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36670
- if (deepEq$3(guessCoeffs, correctCoeffs)) {
36671
- return {
36672
- type: "points",
36673
- earned: 1,
36674
- total: 1,
36675
- message: null
36676
- };
36677
- }
36678
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
36679
- const guessCoeffs = getSinusoidCoefficients(userInput.coords);
36680
- const correctCoeffs = getSinusoidCoefficients(rubric.correct.coords);
36681
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36682
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36683
- // If the canonical coefficients match, it's correct.
36684
- if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36685
- return {
36686
- type: "points",
36687
- earned: 1,
36688
- total: 1,
36689
- message: null
36690
- };
36691
- }
36692
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36693
- if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36694
- return {
36695
- type: "points",
36696
- earned: 1,
36697
- total: 1,
36698
- message: null
36699
- };
36700
- }
36701
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
36702
- let correct = rubric.correct.coords;
36703
- if (correct == null) {
36704
- throw new Error("Point graph rubric has null coords");
36705
- }
36706
- const guess = userInput.coords.slice();
36707
- correct = correct.slice();
36708
- // Everything's already rounded so we shouldn't need to do an
36709
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
36710
- // eq() anyway. The sort should be fine because it'll stringify
36711
- // it and -0 converted to a string is "0"
36712
- guess?.sort();
36713
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36714
- correct.sort();
36715
- if (deepEq$3(guess, correct)) {
36716
- return {
36717
- type: "points",
36718
- earned: 1,
36719
- total: 1,
36720
- message: null
36721
- };
36722
- }
36723
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
36724
- const guess = userInput.coords.slice();
36725
- const correct = rubric.correct.coords.slice();
36726
- let match;
36727
- if (rubric.correct.match === "similar") {
36728
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
36729
- } else if (rubric.correct.match === "congruent") {
36730
- match = similar(guess, correct, kmath.number.DEFAULT_TOLERANCE);
36731
- } else if (rubric.correct.match === "approx") {
36732
- match = similar(guess, correct, 0.1);
36733
- } else {
36734
- /* exact */
36735
- guess.sort();
36736
- correct.sort();
36737
- match = deepEq$3(guess, correct);
36738
- }
36739
- if (match) {
36740
- return {
36741
- type: "points",
36742
- earned: 1,
36743
- total: 1,
36744
- message: null
36745
- };
36746
- }
36747
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
36748
- let guess = Util.deepClone(userInput.coords);
36749
- let correct = Util.deepClone(rubric.correct.coords);
36750
- guess = ___default["default"].invoke(guess, "sort").sort();
36751
- correct = ___default["default"].invoke(correct, "sort").sort();
36752
- if (deepEq$3(guess, correct)) {
36753
- return {
36754
- type: "points",
36755
- earned: 1,
36756
- total: 1,
36757
- message: null
36758
- };
36759
- }
36760
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
36761
- const guess = userInput.coords;
36762
- const correct = rubric.correct.coords;
36763
- if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36764
- return {
36765
- type: "points",
36766
- earned: 1,
36767
- total: 1,
36768
- message: null
36769
- };
36770
- }
36771
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
36772
- const guess = userInput.coords;
36773
- const correct = rubric.correct.coords;
36774
- const allowReflexAngles = rubric.correct.allowReflexAngles;
36775
- let match;
36776
- if (rubric.correct.match === "congruent") {
36777
- const angles = ___default["default"].map([guess, correct], function (coords) {
36778
- if (!coords) {
36779
- return false;
36780
- }
36781
- const angle = getClockwiseAngle(coords, allowReflexAngles);
36782
- return angle;
36783
- });
36784
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36785
- match = eq$1(...angles);
36786
- } else {
36787
- /* exact */
36788
- match =
36789
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36790
- deepEq$3(guess[1], correct[1]) &&
36791
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36792
- collinear(correct[1], correct[0], guess[0]) &&
36793
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36794
- collinear(correct[1], correct[2], guess[2]);
36795
- }
36796
- if (match) {
36797
- return {
36798
- type: "points",
36799
- earned: 1,
36800
- total: 1,
36801
- message: null
36802
- };
36803
- }
36804
- }
36805
- }
36806
-
36807
- // The input wasn't correct, so check if it's a blank input or if it's
36808
- // actually just wrong
36809
- if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
36810
- // We're where we started.
36811
- return {
36812
- type: "invalid",
36813
- message: null
36814
- };
36815
- }
36816
- return {
36817
- type: "points",
36818
- earned: 0,
36819
- total: 1,
36820
- message: null
36821
- };
36822
- }
36823
-
35416
+ const {
35417
+ getClockwiseAngle
35418
+ } = kmath.angles;
35419
+ const {
35420
+ angleMeasures,
35421
+ ccw,
35422
+ collinear,
35423
+ getLineEquation,
35424
+ getLineIntersection,
35425
+ intersects,
35426
+ lawOfCosines,
35427
+ magnitude,
35428
+ rotate,
35429
+ sign,
35430
+ vector
35431
+ } = kmath.geometry;
36824
35432
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
36825
35433
  const defaultBackgroundImage = {
36826
35434
  url: null
36827
35435
  };
36828
- const eq = Util.eq;
36829
35436
  const UNLIMITED = "unlimited";
36830
35437
 
36831
35438
  // Sample background image:
@@ -36838,7 +35445,7 @@ function defaultVal(actual, defaultValue) {
36838
35445
 
36839
35446
  // Less than or approximately equal
36840
35447
  function leq(a, b) {
36841
- return a < b || eq(a, b);
35448
+ return a < b || perseusCore.approximateEqual(a, b);
36842
35449
  }
36843
35450
  function capitalize(str) {
36844
35451
  return str.replace(/(?:^|-)(.)/g, function (match, letter) {
@@ -37700,7 +36307,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37700
36307
  // Solve for side by using the law of sines
37701
36308
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
37702
36309
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37703
- const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
36310
+ const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
37704
36311
  return this.graphie?.addPoints(coords[rel(-1)], offset);
37705
36312
  }
37706
36313
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -37724,7 +36331,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37724
36331
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
37725
36332
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37726
36333
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
37727
- const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36334
+ const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
37728
36335
  return this.graphie?.addPoints(coords[rel(-1)], offset);
37729
36336
  }
37730
36337
  // Snap to grid (already done)
@@ -38336,12 +36943,12 @@ class InteractiveGraph extends React__namespace.Component {
38336
36943
  }
38337
36944
  static getLinearEquationString(props) {
38338
36945
  const coords = InteractiveGraph.getLineCoords(props.graph, props);
38339
- if (eq(coords[0][0], coords[1][0])) {
36946
+ if (perseusCore.approximateEqual(coords[0][0], coords[1][0])) {
38340
36947
  return "x = " + coords[0][0].toFixed(3);
38341
36948
  }
38342
36949
  const m = (coords[1][1] - coords[0][1]) / (coords[1][0] - coords[0][0]);
38343
36950
  const b = coords[0][1] - m * coords[0][0];
38344
- if (eq(m, 0)) {
36951
+ if (perseusCore.approximateEqual(m, 0)) {
38345
36952
  return "y = " + b.toFixed(3);
38346
36953
  }
38347
36954
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
@@ -38483,7 +37090,7 @@ var InteractiveGraph$1 = {
38483
37090
  staticTransform: staticTransform$3,
38484
37091
  // TODO(LEMS-2656): remove TS suppression
38485
37092
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
38486
- scorer: scoreInteractiveGraph
37093
+ scorer: perseusScore.scoreInteractiveGraph
38487
37094
  };
38488
37095
 
38489
37096
  // TODO: can we use WB typography?
@@ -38860,59 +37467,6 @@ const styles$a = aphrodite.StyleSheet.create({
38860
37467
  }
38861
37468
  });
38862
37469
 
38863
- // Question state for marker as result of user selected answers.
38864
-
38865
- function scoreMarker(marker) {
38866
- const score = {
38867
- hasAnswers: false,
38868
- isCorrect: false
38869
- };
38870
- if (marker.selected && marker.selected.length > 0) {
38871
- score.hasAnswers = true;
38872
- }
38873
- if (marker.answers.length > 0) {
38874
- if (marker.selected && marker.selected.length === marker.answers.length) {
38875
- // All correct answers are selected by the user.
38876
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38877
- }
38878
- } else if (!marker.selected || marker.selected.length === 0) {
38879
- // Correct as no answers should be selected by the user.
38880
- score.isCorrect = true;
38881
- }
38882
- return score;
38883
- }
38884
-
38885
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
38886
- function scoreLabelImage(userInput, rubric) {
38887
- let numAnswered = 0;
38888
- let numCorrect = 0;
38889
- for (const marker of userInput.markers) {
38890
- const score = scoreMarker(marker);
38891
- if (score.hasAnswers) {
38892
- numAnswered++;
38893
- }
38894
- if (score.isCorrect) {
38895
- numCorrect++;
38896
- }
38897
- }
38898
-
38899
- // We expect all question markers to be answered before grading.
38900
- if (numAnswered !== userInput.markers.length) {
38901
- return {
38902
- type: "invalid",
38903
- message: null
38904
- };
38905
- }
38906
- return {
38907
- type: "points",
38908
- // Markers with no expected answers are graded as correct if user
38909
- // makes no answer selection.
38910
- earned: numCorrect === userInput.markers.length ? 1 : 0,
38911
- total: 1,
38912
- message: null
38913
- };
38914
- }
38915
-
38916
37470
  /**
38917
37471
  * Represents a direction vector.
38918
37472
  * No diagonals allowed.
@@ -39136,7 +37690,7 @@ class LabelImage extends React__namespace.Component {
39136
37690
  onChange
39137
37691
  } = this.props;
39138
37692
  const updatedMarkers = markers.map(marker => {
39139
- const score = scoreMarker(marker);
37693
+ const score = perseusScore.scoreLabelImageMarker(marker);
39140
37694
  return {
39141
37695
  ...marker,
39142
37696
  // Reveal correctness state for markers with answers.
@@ -39285,7 +37839,7 @@ class LabelImage extends React__namespace.Component {
39285
37839
  bottom: "top"
39286
37840
  }[markerPosition];
39287
37841
  }
39288
- const score = scoreMarker(marker);
37842
+ const score = perseusScore.scoreLabelImageMarker(marker);
39289
37843
  // Once the question is answered, show markers
39290
37844
  // with correct answers, otherwise passthrough
39291
37845
  // the correctness state.
@@ -39493,7 +38047,7 @@ var LabelImage$1 = {
39493
38047
  isLintable: true,
39494
38048
  // TODO(LEMS-2656): remove TS suppression
39495
38049
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
39496
- scorer: scoreLabelImage
38050
+ scorer: perseusScore.scoreLabelImage
39497
38051
  };
39498
38052
 
39499
38053
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -40219,16 +38773,6 @@ const getPromptJSON$9 = (renderProps, userInput) => {
40219
38773
  };
40220
38774
  };
40221
38775
 
40222
- function scoreMatcher(state, rubric) {
40223
- const correct = ___default["default"].isEqual(state.left, rubric.left) && ___default["default"].isEqual(state.right, rubric.right);
40224
- return {
40225
- type: "points",
40226
- earned: correct ? 1 : 0,
40227
- total: 1,
40228
- message: null
40229
- };
40230
- }
40231
-
40232
38776
  const {
40233
38777
  shuffle: shuffle$1,
40234
38778
  seededRNG
@@ -40434,7 +38978,7 @@ var Matcher$1 = {
40434
38978
  isLintable: true,
40435
38979
  // TODO(LEMS-2656): remove TS suppression
40436
38980
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
40437
- scorer: scoreMatcher
38981
+ scorer: perseusScore.scoreMatcher
40438
38982
  };
40439
38983
 
40440
38984
  const getPromptJSON$8 = (renderProps, userInput) => {
@@ -40450,78 +38994,6 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40450
38994
  };
40451
38995
  };
40452
38996
 
40453
- /**
40454
- * Checks user input from the matrix widget to see if it is scorable.
40455
- *
40456
- * Note: The matrix widget cannot do much validation without the Scoring
40457
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
40458
- *
40459
- * @see `scoreMatrix()` for more details.
40460
- */
40461
- function validateMatrix(userInput, validationData, strings) {
40462
- const supplied = userInput.answers;
40463
- const suppliedSize = getMatrixSize(supplied);
40464
- for (let row = 0; row < suppliedSize[0]; row++) {
40465
- for (let col = 0; col < suppliedSize[1]; col++) {
40466
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40467
- return {
40468
- type: "invalid",
40469
- message: strings.fillAllCells
40470
- };
40471
- }
40472
- }
40473
- }
40474
- return null;
40475
- }
40476
-
40477
- function scoreMatrix(userInput, rubric, strings) {
40478
- const validationResult = validateMatrix(userInput, rubric, strings);
40479
- if (validationResult != null) {
40480
- return validationResult;
40481
- }
40482
- const solution = rubric.answers;
40483
- const supplied = userInput.answers;
40484
- const solutionSize = getMatrixSize(solution);
40485
- const suppliedSize = getMatrixSize(supplied);
40486
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
40487
- const createValidator = perseusScore.KhanAnswerTypes.number.createValidatorFunctional;
40488
- let message = null;
40489
- let incorrect = false;
40490
- ___default["default"](suppliedSize[0]).times(row => {
40491
- ___default["default"](suppliedSize[1]).times(col => {
40492
- if (!incorrectSize) {
40493
- const validator = createValidator(
40494
- // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
40495
- solution[row][col], {
40496
- simplify: true
40497
- });
40498
- const result = validator(supplied[row][col]);
40499
- if (result.message) {
40500
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
40501
- message = result.message;
40502
- }
40503
- if (!result.correct) {
40504
- incorrect = true;
40505
- }
40506
- }
40507
- });
40508
- });
40509
- if (incorrectSize) {
40510
- return {
40511
- type: "points",
40512
- earned: 0,
40513
- total: 1,
40514
- message: null
40515
- };
40516
- }
40517
- return {
40518
- type: "points",
40519
- earned: incorrect ? 0 : 1,
40520
- total: 1,
40521
- message: message
40522
- };
40523
- }
40524
-
40525
38997
  const {
40526
38998
  assert: assert$2
40527
38999
  } = InteractiveUtil;
@@ -40570,29 +39042,6 @@ const getRefForPath$1 = function (path) {
40570
39042
  const column = getColumnFromPath$1(path);
40571
39043
  return "answer" + row + "," + column;
40572
39044
  };
40573
- function getMatrixSize(matrix) {
40574
- const matrixSize = [1, 1];
40575
-
40576
- // We need to find the widest row and tallest column to get the correct
40577
- // matrix size.
40578
- ___default["default"](matrix).each((matrixRow, row) => {
40579
- let rowWidth = 0;
40580
- ___default["default"](matrixRow).each((matrixCol, col) => {
40581
- if (matrixCol != null && matrixCol.toString().length) {
40582
- rowWidth = col + 1;
40583
- }
40584
- });
40585
-
40586
- // Matrix width:
40587
- matrixSize[1] = Math.max(matrixSize[1], rowWidth);
40588
-
40589
- // Matrix height:
40590
- if (rowWidth > 0) {
40591
- matrixSize[0] = Math.max(matrixSize[0], row + 1);
40592
- }
40593
- });
40594
- return matrixSize;
40595
- }
40596
39045
  class Matrix extends React__namespace.Component {
40597
39046
  static contextType = PerseusI18nContext;
40598
39047
  // @ts-expect-error - TS2564 - Property 'cursorPosition' has no initializer and is not definitely assigned in the constructor.
@@ -40763,7 +39212,7 @@ class Matrix extends React__namespace.Component {
40763
39212
  INPUT_HEIGHT,
40764
39213
  INPUT_WIDTH
40765
39214
  } = dimensions;
40766
- const matrixSize = getMatrixSize(this.props.answers);
39215
+ const matrixSize = perseusCore.getMatrixSize(this.props.answers);
40767
39216
  const maxRows = this.props.matrixBoardSize[0];
40768
39217
  const maxCols = this.props.matrixBoardSize[1];
40769
39218
  const cursorRow = this.props.cursorPosition[0];
@@ -40935,7 +39384,7 @@ var Matrix$1 = {
40935
39384
  isLintable: true,
40936
39385
  // TODO(LEMS-2656): remove TS suppression
40937
39386
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
40938
- scorer: scoreMatrix
39387
+ scorer: perseusScore.scoreMatrix
40939
39388
  };
40940
39389
 
40941
39390
  const getPromptJSON$7 = () => {
@@ -41980,59 +40429,6 @@ const getPromptJSON$6 = (renderProps, userInput) => {
41980
40429
  };
41981
40430
  };
41982
40431
 
41983
- /**
41984
- * Checks user input is within the allowed range and not the same as the initial
41985
- * state.
41986
- * @param userInput
41987
- * @see 'scoreNumberLine' for the scoring logic.
41988
- */
41989
- function validateNumberLine(userInput) {
41990
- const divisionRange = userInput.divisionRange;
41991
- const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
41992
-
41993
- // TODO: I don't think isTickCrtl is a thing anymore
41994
- if (userInput.isTickCrtl && outsideAllowedRange) {
41995
- return {
41996
- type: "invalid",
41997
- message: "Number of divisions is outside the allowed range."
41998
- };
41999
- }
42000
- return null;
42001
- }
42002
-
42003
- function scoreNumberLine(userInput, scoringData) {
42004
- const validationError = validateNumberLine(userInput);
42005
- if (validationError) {
42006
- return validationError;
42007
- }
42008
- const range = scoringData.range;
42009
- const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42010
- const startRel = scoringData.isInequality ? "ge" : "eq";
42011
- const correctRel = scoringData.correctRel || "eq";
42012
- const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
42013
- if (correctPos && correctRel === userInput.rel) {
42014
- return {
42015
- type: "points",
42016
- earned: 1,
42017
- total: 1,
42018
- message: null
42019
- };
42020
- }
42021
- if (userInput.numLinePosition === start && userInput.rel === startRel) {
42022
- // We're where we started.
42023
- return {
42024
- type: "invalid",
42025
- message: null
42026
- };
42027
- }
42028
- return {
42029
- type: "points",
42030
- earned: 0,
42031
- total: 1,
42032
- message: null
42033
- };
42034
- }
42035
-
42036
40432
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
42037
40433
  const MovablePoint = Graphie.MovablePoint;
42038
40434
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -42592,7 +40988,7 @@ var NumberLine$1 = {
42592
40988
  staticTransform: staticTransform$1,
42593
40989
  // TODO(LEMS-2656): remove TS suppression
42594
40990
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
42595
- scorer: scoreNumberLine
40991
+ scorer: perseusScore.scoreNumberLine
42596
40992
  };
42597
40993
 
42598
40994
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -42607,36 +41003,6 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42607
41003
  };
42608
41004
  };
42609
41005
 
42610
- /**
42611
- * Checks user input from the orderer widget to see if the user has started
42612
- * ordering the options, making the widget scorable.
42613
- * @param userInput
42614
- * @see `scoreOrderer` for more details.
42615
- */
42616
- function validateOrderer(userInput) {
42617
- if (userInput.current.length === 0) {
42618
- return {
42619
- type: "invalid",
42620
- message: null
42621
- };
42622
- }
42623
- return null;
42624
- }
42625
-
42626
- function scoreOrderer(userInput, rubric) {
42627
- const validateError = validateOrderer(userInput);
42628
- if (validateError) {
42629
- return validateError;
42630
- }
42631
- const correct = ___default["default"].isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42632
- return {
42633
- type: "points",
42634
- earned: correct ? 1 : 0,
42635
- total: 1,
42636
- message: null
42637
- };
42638
- }
42639
-
42640
41006
  class PlaceholderCard extends React__namespace.Component {
42641
41007
  render() {
42642
41008
  return /*#__PURE__*/React__namespace.createElement("div", {
@@ -43176,7 +41542,7 @@ var Orderer$1 = {
43176
41542
  isLintable: true,
43177
41543
  // TODO(LEMS-2656): remove TS suppression
43178
41544
  // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
43179
- scorer: scoreOrderer
41545
+ scorer: perseusScore.scoreOrderer
43180
41546
  };
43181
41547
 
43182
41548
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -43448,42 +41814,6 @@ const getPromptJSON$3 = () => {
43448
41814
  return getUnsupportedPromptJSON("plotter");
43449
41815
  };
43450
41816
 
43451
- const {
43452
- deepEq: deepEq$2
43453
- } = Util;
43454
-
43455
- /**
43456
- * Checks user input to confirm it is not the same as the starting values for the graph.
43457
- * This means the user has modified the graph, and the question can be scored.
43458
- *
43459
- * @see 'scorePlotter' for more details on scoring.
43460
- */
43461
- function validatePlotter(userInput, validationData) {
43462
- if (deepEq$2(userInput, validationData.starting)) {
43463
- return {
43464
- type: "invalid",
43465
- message: null
43466
- };
43467
- }
43468
- return null;
43469
- }
43470
-
43471
- const {
43472
- deepEq: deepEq$1
43473
- } = Util;
43474
- function scorePlotter(userInput, scoringData) {
43475
- const validationError = validatePlotter(userInput, scoringData);
43476
- if (validationError) {
43477
- return validationError;
43478
- }
43479
- return {
43480
- type: "points",
43481
- earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43482
- total: 1,
43483
- message: null
43484
- };
43485
- }
43486
-
43487
41817
  /* eslint-disable react/no-unsafe */
43488
41818
  class Plotter extends React__namespace.Component {
43489
41819
  static contextType = PerseusI18nContext;
@@ -44288,7 +42618,7 @@ var Plotter$1 = {
44288
42618
  staticTransform: staticTransform,
44289
42619
  // TODO(LEMS-2656): remove TS suppression
44290
42620
  // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
44291
- scorer: scorePlotter
42621
+ scorer: perseusScore.scorePlotter
44292
42622
  };
44293
42623
 
44294
42624
  const getPromptJSON$2 = () => {
@@ -44365,43 +42695,6 @@ const getPromptJSON$1 = userInput => {
44365
42695
  };
44366
42696
  };
44367
42697
 
44368
- /**
44369
- * Checks user input for the sorter widget to ensure that the user has made
44370
- * changes before attempting to score the widget.
44371
- * @param userInput
44372
- * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
44373
- * for more details on how the sorter widget is scored.
44374
- */
44375
- function validateSorter(userInput) {
44376
- // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44377
- // prevents the "Check" button from becoming active. We want the user
44378
- // to make a change before trying to move forward. This makes an
44379
- // assumption that the initial order isn't the correct order! However,
44380
- // this should be rare if it happens, and interacting with the list
44381
- // will enable the button, so they won't be locked out of progressing.
44382
- if (!userInput.changed) {
44383
- return {
44384
- type: "invalid",
44385
- message: null
44386
- };
44387
- }
44388
- return null;
44389
- }
44390
-
44391
- function scoreSorter(userInput, rubric) {
44392
- const validationError = validateSorter(userInput);
44393
- if (validationError) {
44394
- return validationError;
44395
- }
44396
- const correct = Util.deepEq(userInput.options, rubric.correct);
44397
- return {
44398
- type: "points",
44399
- earned: correct ? 1 : 0,
44400
- total: 1,
44401
- message: null
44402
- };
44403
- }
44404
-
44405
42698
  const {
44406
42699
  shuffle
44407
42700
  } = Util;
@@ -44488,84 +42781,9 @@ var Sorter$1 = {
44488
42781
  isLintable: true,
44489
42782
  // TODO(LEMS-2656): remove TS suppression
44490
42783
  // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
44491
- scorer: scoreSorter
44492
- };
44493
-
44494
- /**
44495
- * Filters the given table (modelled as a 2D array) to remove any rows that are
44496
- * completely empty.
44497
- *
44498
- * @returns A new table with only non-empty rows.
44499
- */
44500
- const filterNonEmpty = function (table) {
44501
- return table.filter(function (row) {
44502
- // Return only rows that are non-empty.
44503
- return row.some(cell => cell);
44504
- });
42784
+ scorer: perseusScore.scoreSorter
44505
42785
  };
44506
42786
 
44507
- function validateTable(userInput) {
44508
- const supplied = filterNonEmpty(userInput);
44509
- const hasEmptyCell = supplied.some(function (row) {
44510
- return row.some(function (cell) {
44511
- return cell === "";
44512
- });
44513
- });
44514
- if (hasEmptyCell || !supplied.length) {
44515
- return {
44516
- type: "invalid",
44517
- message: null
44518
- };
44519
- }
44520
- return null;
44521
- }
44522
-
44523
- function scoreTable(userInput, rubric, strings) {
44524
- const validationResult = validateTable(userInput);
44525
- if (validationResult != null) {
44526
- return validationResult;
44527
- }
44528
- const supplied = filterNonEmpty(userInput);
44529
- const solution = filterNonEmpty(rubric.answers);
44530
- if (supplied.length !== solution.length) {
44531
- return {
44532
- type: "points",
44533
- earned: 0,
44534
- total: 1,
44535
- message: null
44536
- };
44537
- }
44538
- const createValidator = perseusScore.KhanAnswerTypes.number.createValidatorFunctional;
44539
- let message = null;
44540
- const allCorrect = solution.every(function (rowSolution) {
44541
- for (let i = 0; i < supplied.length; i++) {
44542
- const rowSupplied = supplied[i];
44543
- const correct = rowSupplied.every(function (cellSupplied, i) {
44544
- const cellSolution = rowSolution[i];
44545
- const validator = createValidator(cellSolution, {
44546
- simplify: true
44547
- });
44548
- const result = validator(cellSupplied);
44549
- if (result.message) {
44550
- message = result.message;
44551
- }
44552
- return result.correct;
44553
- });
44554
- if (correct) {
44555
- supplied.splice(i, 1);
44556
- return true;
44557
- }
44558
- }
44559
- return false;
44560
- });
44561
- return {
44562
- type: "points",
44563
- earned: allCorrect ? 1 : 0,
44564
- total: 1,
44565
- message
44566
- };
44567
- }
44568
-
44569
42787
  const {
44570
42788
  assert
44571
42789
  } = InteractiveUtil;
@@ -44782,7 +43000,7 @@ var Table$1 = {
44782
43000
  isLintable: true,
44783
43001
  // TODO(LEMS-2656): remove TS suppression
44784
43002
  // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
44785
- scorer: scoreTable
43003
+ scorer: perseusScore.scoreTable
44786
43004
  };
44787
43005
 
44788
43006
  const getPromptJSON = () => {
@@ -44967,7 +43185,7 @@ const init = function () {
44967
43185
 
44968
43186
  // This file is processed by a Rollup plugin (replace) to inject the production
44969
43187
  const libName = "@khanacademy/perseus";
44970
- const libVersion = "50.0.0";
43188
+ const libVersion = "51.0.0";
44971
43189
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44972
43190
 
44973
43191
  const apiVersion = {
@@ -46936,7 +45154,6 @@ const getValidWidgetIds = perseusItem => {
46936
45154
  return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
46937
45155
  };
46938
45156
 
46939
- const deepEq = Util.deepEq;
46940
45157
  function isRealJSONParse(jsonParse) {
46941
45158
  const randomPhrase = buildRandomPhrase();
46942
45159
  const randomHintPhrase = buildRandomPhrase();
@@ -46993,7 +45210,7 @@ function isRealJSONParse(jsonParse) {
46993
45210
  const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
46994
45211
  const parsedTestJSON = jsonParse(testJSON);
46995
45212
  const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
46996
- return deepEq(parsedTestItemData, testingObject);
45213
+ return perseusCore.approximateDeepEqual(parsedTestItemData, testingObject);
46997
45214
  }
46998
45215
  function buildRandomString() {
46999
45216
  let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
@@ -47200,7 +45417,9 @@ function object(schema) {
47200
45417
  for (const [prop, propParser] of Object.entries(schema)) {
47201
45418
  const result = propParser(rawValue[prop], ctx.forSubtree(prop));
47202
45419
  if (isSuccess(result)) {
47203
- ret[prop] = result.value;
45420
+ if (result.value !== undefined || prop in rawValue) {
45421
+ ret[prop] = result.value;
45422
+ }
47204
45423
  } else {
47205
45424
  mismatches.push(...result.detail);
47206
45425
  }
@@ -48819,7 +47038,6 @@ const EditorJsonify = {
48819
47038
  * Main entry point
48820
47039
  */
48821
47040
  const GrapherUtil = {
48822
- allTypes,
48823
47041
  DEFAULT_GRAPHER_PROPS,
48824
47042
  chooseType,
48825
47043
  defaultPlotProps,
@@ -48887,7 +47105,6 @@ exports.getAngleCoords = getAngleCoords;
48887
47105
  exports.getAnswerFromUserInput = getAnswerFromUserInput;
48888
47106
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
48889
47107
  exports.getCircleCoords = getCircleCoords;
48890
- exports.getClockwiseAngle = getClockwiseAngle;
48891
47108
  exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
48892
47109
  exports.getImagesWithoutAltData = getImagesWithoutAltData;
48893
47110
  exports.getInteractiveBoxFromSizeClass = getInteractiveBoxFromSizeClass;