@khanacademy/perseus 50.1.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 (115) hide show
  1. package/dist/es/index.js +558 -2400
  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 +500 -2318
  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 +1 -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 +2 -2
  34. package/dist/widgets/cs-program/cs-program.d.ts +1 -2
  35. package/dist/widgets/definition/definition.d.ts +1 -1
  36. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
  37. package/dist/widgets/dropdown/dropdown.d.ts +2 -2
  38. package/dist/widgets/explanation/explanation.d.ts +1 -1
  39. package/dist/widgets/expression/expression.d.ts +5 -6
  40. package/dist/widgets/graded-group/graded-group.d.ts +1 -1
  41. package/dist/widgets/graded-group-set/graded-group-set.d.ts +1 -1
  42. package/dist/widgets/grapher/grapher.d.ts +1 -2
  43. package/dist/widgets/grapher/util.d.ts +5 -13
  44. package/dist/widgets/group/group.d.ts +2 -2
  45. package/dist/widgets/group/score-group.d.ts +1 -2
  46. package/dist/widgets/iframe/iframe.d.ts +1 -2
  47. package/dist/widgets/image/image.d.ts +1 -1
  48. package/dist/widgets/input-number/input-number.d.ts +1 -2
  49. package/dist/widgets/interaction/interaction.d.ts +1 -1
  50. package/dist/widgets/interactive-graph.d.ts +2 -3
  51. package/dist/widgets/interactive-graphs/graphs/linear-system.d.ts +2 -0
  52. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -3
  53. package/dist/widgets/interactive-graphs/graphs/ray.d.ts +2 -0
  54. package/dist/widgets/interactive-graphs/graphs/sinusoid.d.ts +4 -9
  55. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +4 -0
  56. package/dist/widgets/interactive-graphs/math/index.d.ts +0 -1
  57. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +1 -1
  58. package/dist/widgets/interactive-graphs/types.d.ts +2 -1
  59. package/dist/widgets/label-image/label-image.d.ts +2 -4
  60. package/dist/widgets/label-image/marker.d.ts +1 -2
  61. package/dist/widgets/matcher/matcher.d.ts +1 -2
  62. package/dist/widgets/matrix/matrix.d.ts +2 -4
  63. package/dist/widgets/measurer/measurer.d.ts +1 -1
  64. package/dist/widgets/mock-widgets/mock-widget.d.ts +1 -1
  65. package/dist/widgets/mock-widgets/score-mock-widget.d.ts +1 -2
  66. package/dist/widgets/molecule/molecule.d.ts +1 -1
  67. package/dist/widgets/number-line/number-line.d.ts +2 -3
  68. package/dist/widgets/numeric-input/numeric-input.d.ts +1 -2
  69. package/dist/widgets/orderer/orderer.d.ts +1 -2
  70. package/dist/widgets/passage/passage.d.ts +1 -1
  71. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  72. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +1 -1
  73. package/dist/widgets/plotter/plotter.d.ts +1 -2
  74. package/dist/widgets/radio/radio-component.d.ts +1 -1
  75. package/dist/widgets/radio/radio.d.ts +1 -1
  76. package/dist/widgets/sorter/sorter.d.ts +1 -2
  77. package/dist/widgets/table/table.d.ts +1 -2
  78. package/dist/widgets/video/video.d.ts +1 -1
  79. package/package.json +46 -46
  80. package/dist/tex-wrangler.d.ts +0 -7
  81. package/dist/util/geometry.d.ts +0 -28
  82. package/dist/validation.types.d.ts +0 -166
  83. package/dist/widgets/categorizer/score-categorizer.d.ts +0 -5
  84. package/dist/widgets/categorizer/validate-categorizer.d.ts +0 -13
  85. package/dist/widgets/cs-program/score-cs-program.d.ts +0 -4
  86. package/dist/widgets/dropdown/score-dropdown.d.ts +0 -4
  87. package/dist/widgets/dropdown/validate-dropdown.d.ts +0 -8
  88. package/dist/widgets/expression/get-decimal-separator.d.ts +0 -5
  89. package/dist/widgets/expression/score-expression.d.ts +0 -5
  90. package/dist/widgets/expression/validate-expression.d.ts +0 -12
  91. package/dist/widgets/grapher/grapher-types.d.ts +0 -58
  92. package/dist/widgets/grapher/score-grapher.d.ts +0 -4
  93. package/dist/widgets/iframe/score-iframe.d.ts +0 -3
  94. package/dist/widgets/input-number/score-input-number.d.ts +0 -39
  95. package/dist/widgets/interactive-graphs/math/angles.d.ts +0 -7
  96. package/dist/widgets/interactive-graphs/score-interactive-graph.d.ts +0 -4
  97. package/dist/widgets/label-image/score-label-image.d.ts +0 -10
  98. package/dist/widgets/label-image/types.d.ts +0 -11
  99. package/dist/widgets/matcher/score-matcher.d.ts +0 -4
  100. package/dist/widgets/matrix/score-matrix.d.ts +0 -5
  101. package/dist/widgets/matrix/validate-matrix.d.ts +0 -13
  102. package/dist/widgets/number-line/score-number-line.d.ts +0 -4
  103. package/dist/widgets/number-line/validate-number-line.d.ts +0 -12
  104. package/dist/widgets/numeric-input/score-numeric-input.d.ts +0 -6
  105. package/dist/widgets/orderer/score-orderer.d.ts +0 -3
  106. package/dist/widgets/orderer/validate-orderer.d.ts +0 -10
  107. package/dist/widgets/plotter/score-plotter.d.ts +0 -4
  108. package/dist/widgets/plotter/validate-plotter.d.ts +0 -10
  109. package/dist/widgets/radio/score-radio.d.ts +0 -5
  110. package/dist/widgets/radio/validate-radio.d.ts +0 -12
  111. package/dist/widgets/sorter/score-sorter.d.ts +0 -4
  112. package/dist/widgets/sorter/validate-sorter.d.ts +0 -11
  113. package/dist/widgets/table/score-table.d.ts +0 -5
  114. package/dist/widgets/table/utils.d.ts +0 -7
  115. 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,18 +679,77 @@ 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
 
739
+ // This type helps us make sure all error codes are mapped to strings
740
+
678
741
  const errorToString = {
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
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
686
753
  };
687
754
  function mapErrorToString(err) {
688
755
  if (!err) {
@@ -1616,204 +1683,6 @@ const getPromptJSON$t = (renderProps, userInput) => {
1616
1683
  };
1617
1684
  };
1618
1685
 
1619
- /**
1620
- * Get the character used for separating decimals.
1621
- */
1622
- const getDecimalSeparator = locale => {
1623
- switch (locale) {
1624
- // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
1625
- // locale properly.
1626
- // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
1627
- //
1628
- // Supported locales in Chrome:
1629
- // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
1630
- case "ka":
1631
- return ",";
1632
- default:
1633
- const numberWithDecimalSeparator = 1.1;
1634
- // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
1635
- // support Safari 12.
1636
- const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
1637
- // 0x661 is ARABIC-INDIC DIGIT ONE
1638
- // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
1639
- .match(/[^\d\u0661\u06F1]/);
1640
- return match?.[0] ?? ".";
1641
- }
1642
- };
1643
-
1644
- // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
1645
- // getDependencies() internally... simplify usage to just using a "singleton"
1646
- // which get's the injected logger.
1647
- /**
1648
- * Provides logging infrastructure for Perseus
1649
- */
1650
- const Log = {
1651
- // Logs a message
1652
- log: (message, extra) => {
1653
- getDependencies().Log.log(message, extra);
1654
- },
1655
- // Logs an error with a message
1656
- error: (message, kind, extra) => {
1657
- getDependencies().Log.error(message, kind, extra);
1658
- }
1659
- };
1660
-
1661
- /**
1662
- * Checks user input from the expression widget to see if it is scorable.
1663
- *
1664
- * Note: Most of the expression widget's validation requires the Rubric because
1665
- * of its use of KhanAnswerTypes as a core part of scoring.
1666
- *
1667
- * @see `scoreExpression()` for more details.
1668
- */
1669
- function validateExpression(userInput) {
1670
- if (userInput === "") {
1671
- return {
1672
- type: "invalid",
1673
- message: null
1674
- };
1675
- }
1676
- return null;
1677
- }
1678
-
1679
- /* Content creators input a list of answers which are matched from top to
1680
- * bottom. The intent is that they can include spcific solutions which should
1681
- * be graded as correct or incorrect (or ungraded!) first, then get more
1682
- * general.
1683
- *
1684
- * We iterate through each answer, trying to match it with the user's input
1685
- * using the following angorithm:
1686
- * - Try to parse the user's input. If it doesn't parse then return "not
1687
- * graded".
1688
- * - For each answer:
1689
- * ~ Try to validate the user's input against the answer. The answer is
1690
- * expected to parse.
1691
- * ~ If the user's input validates (the validator judges it "correct"), we've
1692
- * matched and can stop considering answers.
1693
- * - If there were no matches or the matching answer is considered "ungraded",
1694
- * show the user an error. TODO(joel) - what error?
1695
- * - Otherwise, pass through the resulting points and message.
1696
- */
1697
- function scoreExpression(userInput, rubric, strings, locale) {
1698
- const validationError = validateExpression(userInput);
1699
- if (validationError) {
1700
- return validationError;
1701
- }
1702
- const options = ___default["default"].clone(rubric);
1703
- ___default["default"].extend(options, {
1704
- decimal_separator: getDecimalSeparator(locale)
1705
- });
1706
- const createValidator = answer => {
1707
- // We give options to KAS.parse here because it is parsing the
1708
- // solution answer, not the student answer, and we don't want a
1709
- // solution to work if the student is using a different language
1710
- // (different from the content creation language, ie. English).
1711
- const expression = KAS__namespace.parse(answer.value, rubric);
1712
- // An answer may not be parsed if the expression was defined
1713
- // incorrectly. For example if the answer is using a symbol defined
1714
- // in the function variables list for the expression.
1715
- if (!expression.parsed) {
1716
- /* c8 ignore next */
1717
- Log.error("Unable to parse solution answer for expression", perseusCore.Errors.InvalidInput, {
1718
- loggedMetadata: {
1719
- rubric: JSON.stringify(rubric)
1720
- }
1721
- });
1722
- return null;
1723
- }
1724
- return perseusScore.KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, ___default["default"]({}).extend(options, {
1725
- simplify: answer.simplify,
1726
- form: answer.form
1727
- }));
1728
- };
1729
-
1730
- // Find the first answer form that matches the user's input and that
1731
- // is considered correct. Also, track whether the input is
1732
- // considered "empty" for all answer forms, and keep the validation
1733
- // result for the first answer form for which the user's input was
1734
- // considered "ungraded".
1735
- // (Terminology reminder: the answer forms are provided by the
1736
- // assessment items; they are not the user's input. Each one might
1737
- // represent a correct answer, an incorrect one (if the exercise
1738
- // creator has predicted certain common wrong answers and wants to
1739
- // provide guidance via a message), or an ungraded one (same idea,
1740
- // but without giving the user an incorrect mark for the question).
1741
- let matchingAnswerForm;
1742
- let matchMessage;
1743
- let allEmpty = true;
1744
- let firstUngradedResult;
1745
- for (const answerForm of rubric.answerForms || []) {
1746
- const validator = createValidator(answerForm);
1747
- if (!validator) {
1748
- continue;
1749
- }
1750
- const result = validator(userInput);
1751
-
1752
- // Short-circuit as soon as the user's input matches some answer
1753
- // (independently of whether the answer is correct)
1754
- if (result.correct) {
1755
- matchingAnswerForm = answerForm;
1756
- matchMessage = result.message || "";
1757
- break;
1758
- }
1759
- allEmpty = allEmpty && result.empty;
1760
- // If this answer form is correct and the user's input is considered
1761
- // "ungraded" for it, we'll want to keep the evaluation result for
1762
- // later. If the user's input doesn't match any answer forms, we'll
1763
- // show the message from this validation.
1764
- if (answerForm.considered === "correct" && result.ungraded && !firstUngradedResult) {
1765
- firstUngradedResult = result;
1766
- }
1767
- }
1768
-
1769
- // Now check to see if we matched any answer form at all, and if
1770
- // we did, whether it's considered correct, incorrect, or ungraded
1771
- if (!matchingAnswerForm) {
1772
- if (firstUngradedResult) {
1773
- // While we didn't directly match with any answer form, we
1774
- // did at some point get an "ungraded" validation result,
1775
- // which might indicate e.g. a mismatch in variable casing.
1776
- // We'll return "invalid", which will let the user try again
1777
- // with no penalty, and the hopefully helpful validation
1778
- // message.
1779
- return {
1780
- type: "invalid",
1781
- message: firstUngradedResult.message,
1782
- suppressAlmostThere: firstUngradedResult.suppressAlmostThere
1783
- };
1784
- }
1785
- if (allEmpty) {
1786
- // If everything graded as empty, it's invalid.
1787
- return {
1788
- type: "invalid",
1789
- message: null
1790
- };
1791
- }
1792
- // We fell through all the possibilities and we're not empty,
1793
- // so the answer is considered incorrect.
1794
- return {
1795
- type: "points",
1796
- earned: 0,
1797
- total: 1
1798
- };
1799
- }
1800
- if (matchingAnswerForm.considered === "ungraded") {
1801
- return {
1802
- type: "invalid",
1803
- message: matchMessage
1804
- };
1805
- }
1806
- // We matched a graded answer form, so we can now tell the user
1807
- // whether their input was correct or incorrect, and hand out
1808
- // points accordingly
1809
- return {
1810
- type: "points",
1811
- earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
1812
- total: 1,
1813
- message: matchMessage
1814
- };
1815
- }
1816
-
1817
1686
  // Map of international operator names to their English equivalents
1818
1687
  const englishOperators = {
1819
1688
  arctg: "arctan",
@@ -1924,7 +1793,7 @@ class Expression extends React__namespace.Component {
1924
1793
  // make it so that solution answers with ','s or '.'s work
1925
1794
  const options = ___default["default"].pick(props || this.props, "functions");
1926
1795
  ___default["default"].extend(options, {
1927
- decimal_separator: getDecimalSeparator(this.context.locale)
1796
+ decimal_separator: perseusCore.getDecimalSeparator(this.context.locale)
1928
1797
  });
1929
1798
  return KAS__namespace.parse(normalizeTex(value), options);
1930
1799
  };
@@ -2202,7 +2071,7 @@ var Expression$1 = {
2202
2071
  isLintable: true,
2203
2072
  // TODO(LEMS-2656): remove TS suppression
2204
2073
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusExpressionUserInput'.
2205
- scorer: scoreExpression,
2074
+ scorer: perseusScore.scoreExpression,
2206
2075
  // TODO(LEMS-2656): remove TS suppression
2207
2076
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
2208
2077
  getOneCorrectAnswerFromRubric(rubric) {
@@ -2572,56 +2441,6 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
2572
2441
  return [constrainTickStep(tickSteps[0], ranges[0]), constrainTickStep(tickSteps[1], ranges[1])];
2573
2442
  }
2574
2443
 
2575
- /**
2576
- * Approximate equality on numbers and primitives.
2577
- */
2578
- function eq$4(x, y) {
2579
- if (typeof x === "number" && typeof y === "number") {
2580
- return Math.abs(x - y) < 1e-9;
2581
- }
2582
- return x === y;
2583
- }
2584
-
2585
- /**
2586
- * Deep approximate equality on primitives, numbers, arrays, and objects.
2587
- * Recursive.
2588
- */
2589
- function deepEq$7(x, y) {
2590
- if (Array.isArray(x) && Array.isArray(y)) {
2591
- if (x.length !== y.length) {
2592
- return false;
2593
- }
2594
- for (let i = 0; i < x.length; i++) {
2595
- if (!deepEq$7(x[i], y[i])) {
2596
- return false;
2597
- }
2598
- }
2599
- return true;
2600
- }
2601
- if (Array.isArray(x) || Array.isArray(y)) {
2602
- return false;
2603
- }
2604
- if (typeof x === "function" && typeof y === "function") {
2605
- return eq$4(x, y);
2606
- }
2607
- if (typeof x === "function" || typeof y === "function") {
2608
- return false;
2609
- }
2610
- if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
2611
- return x === y || ___default["default"].all(x, function (v, k) {
2612
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2613
- return deepEq$7(y[k], v);
2614
- }) && ___default["default"].all(y, function (v, k) {
2615
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2616
- return deepEq$7(x[k], v);
2617
- });
2618
- }
2619
- if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
2620
- return false;
2621
- }
2622
- return eq$4(x, y);
2623
- }
2624
-
2625
2444
  /**
2626
2445
  * Query String Parser
2627
2446
  *
@@ -2836,15 +2655,6 @@ const textarea = {
2836
2655
  */
2837
2656
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
2838
2657
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
2839
-
2840
- // TODO(benchristel): in the future, we may want to make deepClone work for
2841
- // Record<string, Cloneable> as well. Currently, it only does arrays.
2842
- function deepClone(obj) {
2843
- if (Array.isArray(obj)) {
2844
- return obj.map(deepClone);
2845
- }
2846
- return obj;
2847
- }
2848
2658
  const Util = {
2849
2659
  inputPathsEqual,
2850
2660
  nestedMap: nestedMap$2,
@@ -2865,8 +2675,6 @@ const Util = {
2865
2675
  gridStepFromTickStep,
2866
2676
  tickStepFromNumTicks,
2867
2677
  constrainedTickStepsFromTickSteps,
2868
- eq: eq$4,
2869
- deepEq: deepEq$7,
2870
2678
  parseQueryString,
2871
2679
  updateQueryString: updateQueryString$2,
2872
2680
  strongEncodeURIComponent,
@@ -2884,8 +2692,24 @@ const Util = {
2884
2692
  getDataUrl: getDataUrl,
2885
2693
  textarea,
2886
2694
  unescapeMathMode: unescapeMathMode$1,
2887
- random: random$1,
2888
- 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
+ }
2889
2713
  };
2890
2714
 
2891
2715
  // For offline exercises in the mobile app, we download the graphie data
@@ -8434,7 +8258,7 @@ var Tex$1 = {
8434
8258
  const {
8435
8259
  processMath: processMath$1
8436
8260
  } = Tex$1;
8437
- function polar$1(r, th) {
8261
+ function polar$2(r, th) {
8438
8262
  if (typeof r === "number") {
8439
8263
  r = [r, r];
8440
8264
  }
@@ -8773,8 +8597,8 @@ class Graphie$1 {
8773
8597
  endAngle = (endAngle % 360 + 360) % 360;
8774
8598
  const cent = this.scalePoint(center);
8775
8599
  const radii = this.scaleVector(radius);
8776
- const startVector = polar$1(radius, startAngle);
8777
- const endVector = polar$1(radius, endAngle);
8600
+ const startVector = polar$2(radius, startAngle);
8601
+ const endVector = polar$2(radius, endAngle);
8778
8602
 
8779
8603
  // We round the coordinates to make testing easier, because trig
8780
8604
  // operations return unround numbers.
@@ -10318,261 +10142,11 @@ class WrappedLine extends WrappedDrawing {
10318
10142
  }
10319
10143
  }
10320
10144
 
10321
- /**
10322
- * A collection of geomtry-related utility functions
10323
- */
10324
- const {
10325
- eq: eq$3,
10326
- deepEq: deepEq$6
10327
- } = Util;
10328
-
10329
- // This should really be a readonly tuple of [number, number]
10330
-
10331
- // a, b, c
10332
-
10333
- // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10334
- function sign(val) {
10335
- if (eq$3(val, 0)) {
10336
- return 0;
10337
- }
10338
- return val > 0 ? 1 : -1;
10339
- }
10340
-
10341
- // Determine whether three points are collinear (0), for a clockwise turn (negative),
10342
- // or counterclockwise turn (positive)
10343
- function ccw(a, b, c) {
10344
- return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10345
- }
10346
- function collinear(a, b, c) {
10347
- return eq$3(ccw(a, b, c), 0);
10348
- }
10349
-
10350
- // Given rect bounding points A and B, whether point C is inside the rect
10351
- function pointInRect(a, b, c) {
10352
- 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]);
10353
- }
10354
-
10355
- // Whether line segment AB intersects line segment CD
10356
- // http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
10357
- function intersects(ab, cd) {
10358
- 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]]];
10359
- const orientations = ___default["default"].map(triplets, function (triplet) {
10360
- return sign(ccw(...triplet));
10361
- });
10362
- if (orientations[0] !== orientations[1] && orientations[2] !== orientations[3]) {
10363
- return true;
10364
- }
10365
- for (let i = 0; i < 4; i++) {
10366
- if (orientations[i] === 0 && pointInRect(...triplets[i])) {
10367
- return true;
10368
- }
10369
- }
10370
- return false;
10371
- }
10372
-
10373
- // Whether any two sides of a polygon intersect each other
10374
- function polygonSidesIntersect(vertices) {
10375
- for (let i = 0; i < vertices.length; i++) {
10376
- for (let k = i + 1; k < vertices.length; k++) {
10377
- // If any two vertices are the same point, sides overlap
10378
- if (kmath.point.equal(vertices[i], vertices[k])) {
10379
- return true;
10380
- }
10381
-
10382
- // Find the other end of the sides starting at vertices i and k
10383
- const iNext = (i + 1) % vertices.length;
10384
- const kNext = (k + 1) % vertices.length;
10385
-
10386
- // Adjacent sides always intersect (at the vertex); skip those
10387
- if (iNext === k || kNext === i) {
10388
- continue;
10389
- }
10390
- const side1 = [vertices[i], vertices[iNext]];
10391
- const side2 = [vertices[k], vertices[kNext]];
10392
- if (intersects(side1, side2)) {
10393
- return true;
10394
- }
10395
- }
10396
- }
10397
- return false;
10398
- }
10399
- function vector(a, b) {
10400
- return ___default["default"].map(___default["default"].zip(a, b), function (pair) {
10401
- return pair[0] - pair[1];
10402
- });
10403
- }
10404
- function reverseVector(vector) {
10405
- return [-vector[0], -vector[1]];
10406
- }
10407
-
10408
- // Returns whether connecting the given sequence of `points` forms a clockwise
10409
- // path (assuming a closed loop, where the last point connects back to the
10410
- // first).
10411
- function clockwise$1(points) {
10412
- const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
10413
- const areas = ___default["default"].map(segments, function (segment) {
10414
- const p1 = segment[0];
10415
- const p2 = segment[1];
10416
- return (p2[0] - p1[0]) * (p2[1] + p1[1]);
10417
- });
10418
- return kmath.sum(areas) > 0;
10419
- }
10420
- function magnitude(v) {
10421
- return Math.sqrt(___default["default"].reduce(v, function (memo, el) {
10422
- // @ts-expect-error - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
10423
- return memo + Math.pow(el, 2);
10424
- }, 0));
10425
- }
10426
- function dotProduct(a, b) {
10427
- return ___default["default"].reduce(___default["default"].zip(a, b), function (memo, pair) {
10428
- return memo + pair[0] * pair[1];
10429
- }, 0);
10430
- }
10431
- function sideLengths(coords) {
10432
- const segments = ___default["default"].zip(coords, rotate(coords));
10433
- return segments.map(function (segment) {
10434
- // @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.
10435
- return magnitude(vector(...segment));
10436
- });
10437
- }
10438
-
10439
- // Based on http://math.stackexchange.com/a/151149
10440
- function angleMeasures(coords) {
10441
- const triplets = ___default["default"].zip(rotate(coords, -1), coords, rotate(coords, 1));
10442
- const offsets = ___default["default"].map(triplets, function (triplet) {
10443
- const p = vector(triplet[1], triplet[0]);
10444
- const q = vector(triplet[2], triplet[1]);
10445
- // @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[]'.
10446
- const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
10447
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
10448
- return sign(ccw(...triplet)) > 0 ? raw : -raw;
10449
- });
10450
- const sum = ___default["default"].reduce(offsets, function (memo, arg) {
10451
- return memo + arg;
10452
- }, 0);
10453
- return ___default["default"].map(offsets, function (offset) {
10454
- return sum > 0 ? Math.PI - offset : Math.PI + offset;
10455
- });
10456
- }
10457
-
10458
- // Whether two polygons are similar (or if specified, congruent)
10459
- function similar(coords1, coords2, tolerance) {
10460
- if (coords1.length !== coords2.length) {
10461
- return false;
10462
- }
10463
- const n = coords1.length;
10464
- const angles1 = angleMeasures(coords1);
10465
- const angles2 = angleMeasures(coords2);
10466
- const sides1 = sideLengths(coords1);
10467
- const sides2 = sideLengths(coords2);
10468
- for (let i = 0; i < 2 * n; i++) {
10469
- let angles = angles2.slice();
10470
- let sides = sides2.slice();
10471
-
10472
- // Reverse angles and sides to allow matching reflected polygons
10473
- if (i >= n) {
10474
- angles.reverse();
10475
- sides.reverse();
10476
- // Since sides are calculated from two coordinates,
10477
- // simply reversing results in an off by one error
10478
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10479
- sides = rotate(sides, 1);
10480
- }
10481
-
10482
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10483
- angles = rotate(angles, i);
10484
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10485
- sides = rotate(sides, i);
10486
- if (deepEq$6(angles1, angles)) {
10487
- const sidePairs = ___default["default"].zip(sides1, sides);
10488
- const factors = ___default["default"].map(sidePairs, function (pair) {
10489
- return pair[0] / pair[1];
10490
- });
10491
- const same = ___default["default"].all(factors, function (factor) {
10492
- return eq$3(factors[0], factor);
10493
- });
10494
- const congruentEnough = ___default["default"].all(sidePairs, function (pair) {
10495
- return kmath.number.equal(pair[0], pair[1], tolerance);
10496
- });
10497
- if (same && congruentEnough) {
10498
- return true;
10499
- }
10500
- }
10501
- }
10502
- return false;
10503
- }
10504
-
10505
- // Given triangle with sides ABC return angle opposite side C in degrees
10506
- function lawOfCosines(a, b, c) {
10507
- return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180 / Math.PI;
10508
- }
10509
- function canonicalSineCoefficients$1(_ref) {
10510
- let [amplitude, angularFrequency, phase, verticalOffset] = _ref;
10511
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
10512
- // this function ensures that a, b > 0, and c is its
10513
- // smallest possible positive value.
10514
-
10515
- // Guarantee a > 0
10516
- if (amplitude < 0) {
10517
- amplitude *= -1;
10518
- angularFrequency *= -1;
10519
- phase *= -1;
10520
- }
10521
- const period = 2 * Math.PI;
10522
- // Guarantee b > 0
10523
- if (angularFrequency < 0) {
10524
- angularFrequency *= -1;
10525
- phase *= -1;
10526
- phase += period / 2;
10527
- }
10528
-
10529
- // Guarantee c is smallest possible positive value
10530
- while (phase > 0) {
10531
- phase -= period;
10532
- }
10533
- while (phase < 0) {
10534
- phase += period;
10535
- }
10536
- return [amplitude, angularFrequency, phase, verticalOffset];
10537
- }
10538
-
10539
- // e.g. rotate([1, 2, 3]) -> [2, 3, 1]
10540
- function rotate(array, n) {
10541
- n = typeof n === "undefined" ? 1 : n % array.length;
10542
- return array.slice(n).concat(array.slice(0, n));
10543
- }
10544
- function getLineEquation(first, second) {
10545
- if (eq$3(first[0], second[0])) {
10546
- return "x = " + first[0].toFixed(3);
10547
- }
10548
- const m = (second[1] - first[1]) / (second[0] - first[0]);
10549
- const b = first[1] - m * first[0];
10550
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
10551
- }
10552
-
10553
- // Stolen from the wikipedia article
10554
- // http://en.wikipedia.org/wiki/Line-line_intersection
10555
- function getLineIntersection(
10556
- // TODO(LP-10725): update these to be 2-tuples
10557
- firstPoints, secondPoints) {
10558
- const x1 = firstPoints[0][0];
10559
- const y1 = firstPoints[0][1];
10560
- const x2 = firstPoints[1][0];
10561
- const y2 = firstPoints[1][1];
10562
- const x3 = secondPoints[0][0];
10563
- const y3 = secondPoints[0][1];
10564
- const x4 = secondPoints[1][0];
10565
- const y4 = secondPoints[1][1];
10566
- const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
10567
- if (Math.abs(determinant) < 1e-9) {
10568
- return "Lines are parallel";
10569
- }
10570
- const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / determinant;
10571
- const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / determinant;
10572
- return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
10573
- }
10574
-
10575
10145
  /* eslint-disable @babel/no-invalid-this */
10146
+ const {
10147
+ clockwise: clockwise$3,
10148
+ reverseVector: reverseVector$1
10149
+ } = kmath.geometry;
10576
10150
  function scaledDistanceFromAngle(angle) {
10577
10151
  const a = 3.51470560176242 * 20;
10578
10152
  const b = 0.5687298702748785 * 20;
@@ -10716,7 +10290,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
10716
10290
  const sOffset = sSpacing * (i - (n - 1) / 2);
10717
10291
  const sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10718
10292
  const sHeightVector = scaledPolarRad(sHeight, perpendicularAngle);
10719
- 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))];
10720
10294
 
10721
10295
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
10722
10296
  temp.push(graphie.scaledPath(sPath, style));
@@ -10741,7 +10315,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
10741
10315
  const sOffset = sCumulativeOffset + sSpacing * i;
10742
10316
  let sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10743
10317
  if (start !== p1) {
10744
- sOffsetVector = reverseVector(sOffsetVector);
10318
+ sOffsetVector = reverseVector$1(sOffsetVector);
10745
10319
  }
10746
10320
  const sEnd = kmath.vector.add(sMidpoint, sOffsetVector);
10747
10321
 
@@ -11808,7 +11382,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11808
11382
  // Temporary objects
11809
11383
  ___default["default"].invoke(polygon.temp, "remove");
11810
11384
  polygon.temp = [];
11811
- const isClockwise = clockwise$1(polygon.coords);
11385
+ const isClockwise = clockwise$3(polygon.coords);
11812
11386
 
11813
11387
  // Update angle labels
11814
11388
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -13229,7 +12803,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13229
12803
  points[1].onMove = function (x, y) {
13230
12804
  const oldVertex = points[1].coord;
13231
12805
  const newVertex = [x, y];
13232
- const delta = kmath.vector.add(newVertex, reverseVector(oldVertex));
12806
+ const delta = kmath.vector.add(newVertex, reverseVector$1(oldVertex));
13233
12807
  let valid = true;
13234
12808
  const newPoints = {};
13235
12809
  ___default["default"].each([0, 2], function (i) {
@@ -13271,7 +12845,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13271
12845
  let angle = GraphUtils.findAngleDeprecated(newPoint, vertex);
13272
12846
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13273
12847
  const distance = GraphUtils.getDistance(newPoint, vertex);
13274
- return kmath.vector.add(vertex, polar$1(distance, angle));
12848
+ return kmath.vector.add(vertex, polar$2(distance, angle));
13275
12849
  }
13276
12850
  return true;
13277
12851
  };
@@ -13422,7 +12996,6 @@ ___default["default"].extend(MovableAngle.prototype, {
13422
12996
 
13423
12997
  /* eslint-disable @babel/no-invalid-this */
13424
12998
  const nestedMap$1 = Util.nestedMap;
13425
- const deepEq$5 = Util.deepEq;
13426
12999
 
13427
13000
  /**
13428
13001
  * A base class for all Graphie Movables
@@ -13519,7 +13092,7 @@ const createSimpleClass = function (addFunction) {
13519
13092
  this._prevProps = this.props;
13520
13093
  },
13521
13094
  modify: function (graphie) {
13522
- if (!deepEq$5(this.props, this._prevProps)) {
13095
+ if (!perseusCore.approximateDeepEqual(this.props, this._prevProps)) {
13523
13096
  this.remove();
13524
13097
  this.add(graphie);
13525
13098
  this._prevProps = this.props;
@@ -15242,7 +14815,7 @@ function sum(array) {
15242
14815
  return memo + arg;
15243
14816
  }, 0);
15244
14817
  }
15245
- function clockwise(points) {
14818
+ function clockwise$2(points) {
15246
14819
  const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
15247
14820
  const areas = ___default["default"].map(segments, function (segment) {
15248
14821
  const p1 = segment[0];
@@ -15308,7 +14881,7 @@ const draw$1 = {
15308
14881
  const graphie = this.graphie;
15309
14882
  const self = this;
15310
14883
  const coords = ___default["default"].invoke(state.points, "coord");
15311
- const isClockwise = clockwise(coords);
14884
+ const isClockwise = clockwise$2(coords);
15312
14885
  const n = coords.length;
15313
14886
 
15314
14887
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -16018,7 +15591,7 @@ const Parabola = GraphieClasses.createClass({
16018
15591
  this.parabola.toFront();
16019
15592
  }
16020
15593
  });
16021
- const Sinusoid$1 = GraphieClasses.createClass({
15594
+ const Sinusoid = GraphieClasses.createClass({
16022
15595
  displayName: "Sinusoid",
16023
15596
  movableProps: ["children"],
16024
15597
  add: function (graphie) {
@@ -16040,7 +15613,7 @@ const Sinusoid$1 = GraphieClasses.createClass({
16040
15613
  this.sinusoid.toFront();
16041
15614
  }
16042
15615
  });
16043
- const Plot$2 = GraphieClasses.createSimpleClass((graphie, props) => {
15616
+ const Plot$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16044
15617
  return graphie.plot(props.fn, props.range, props.style);
16045
15618
  });
16046
15619
  const PlotParametric$1 = GraphieClasses.createSimpleClass((graphie, props) => {
@@ -16098,17 +15671,16 @@ var Movables = {
16098
15671
  MovablePoint: MovablePoint$4,
16099
15672
  Parabola: Parabola,
16100
15673
  Path: Path,
16101
- Plot: Plot$2,
15674
+ Plot: Plot$1,
16102
15675
  PlotParametric: PlotParametric$1,
16103
15676
  Point: Point$1,
16104
- Sinusoid: Sinusoid$1,
15677
+ Sinusoid: Sinusoid,
16105
15678
  Rect: Rect$1
16106
15679
  };
16107
15680
 
16108
15681
  const GraphieMovable = GraphieClasses.GraphieMovable;
16109
15682
  const createGraphie = GraphUtils.createGraphie;
16110
15683
  const {
16111
- deepEq: deepEq$4,
16112
15684
  nestedMap
16113
15685
  } = Util;
16114
15686
  const {
@@ -16146,7 +15718,7 @@ class Graphie extends React__namespace.Component {
16146
15718
  if (this.props.setup !== prevProps.setup) {
16147
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);
16148
15720
  }
16149
- 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)) {
16150
15722
  this._setupGraphie();
16151
15723
  }
16152
15724
  this._updateMovables();
@@ -21157,230 +20729,6 @@ const getPromptJSON$s = (renderProps, userInput) => {
21157
20729
  };
21158
20730
  };
21159
20731
 
21160
- /*
21161
- * In this file, an `expression` is some portion of valid TeX enclosed in
21162
- * curly brackets.
21163
- */
21164
-
21165
- /*
21166
- * Find the index at which an expression ends, i.e., has an unmatched
21167
- * closing curly bracket. This method assumes that we start with a non-open
21168
- * bracket character and end when we've seen more left than right brackets
21169
- * (rather than assuming that we start with a bracket character and wait for
21170
- * bracket equality).
21171
- */
21172
- function findEndpoint(tex, currentIndex) {
21173
- let bracketDepth = 0;
21174
- for (let i = currentIndex, len = tex.length; i < len; i++) {
21175
- const c = tex[i];
21176
- if (c === "{") {
21177
- bracketDepth++;
21178
- } else if (c === "}") {
21179
- bracketDepth--;
21180
- }
21181
- if (bracketDepth < 0) {
21182
- return i;
21183
- }
21184
- }
21185
- // If we never see unbalanced curly brackets, default to the
21186
- // entire string
21187
- return tex.length;
21188
- }
21189
-
21190
- /*
21191
- * Parses an individual set of curly brackets into TeX.
21192
- */
21193
- function parseNextExpression(tex, currentIndex, handler) {
21194
- // Find the first '{' and grab subsequent TeX
21195
- // Ex) tex: '{3}{7}', and we want the '3'
21196
- const openBracketIndex = tex.indexOf("{", currentIndex);
21197
- const nextExpIndex = openBracketIndex + 1;
21198
-
21199
- // Truncate to only contain remaining TeX
21200
- const endpoint = findEndpoint(tex, nextExpIndex);
21201
- const expressionTeX = tex.substring(nextExpIndex, endpoint);
21202
- const parsedExp = walkTex(expressionTeX, handler);
21203
- return {
21204
- endpoint: endpoint,
21205
- expression: parsedExp
21206
- };
21207
- }
21208
- function getNextFracIndex(tex, currentIndex) {
21209
- const dfrac = "\\dfrac";
21210
- const frac = "\\frac";
21211
- const nextFrac = tex.indexOf(frac, currentIndex);
21212
- const nextDFrac = tex.indexOf(dfrac, currentIndex);
21213
- if (nextFrac > -1 && nextDFrac > -1) {
21214
- return Math.min(nextFrac, nextDFrac);
21215
- }
21216
- if (nextFrac > -1) {
21217
- return nextFrac;
21218
- }
21219
- if (nextDFrac > -1) {
21220
- return nextDFrac;
21221
- }
21222
- return -1;
21223
- }
21224
- function walkTex(tex, handler) {
21225
- if (!tex) {
21226
- return "";
21227
- }
21228
-
21229
- // Ex) tex: '2 \dfrac {3}{7}'
21230
- let parsedString = "";
21231
- let currentIndex = 0;
21232
- let nextFrac = getNextFracIndex(tex, currentIndex);
21233
-
21234
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
21235
- while (nextFrac > -1) {
21236
- // Gather first fragment, preceding \dfrac
21237
- // Ex) parsedString: '2 '
21238
- parsedString += tex.substring(currentIndex, nextFrac);
21239
-
21240
- // Remove everything preceding \dfrac, which has been parsed
21241
- currentIndex = nextFrac;
21242
-
21243
- // Parse first expression and move index past it
21244
- // Ex) firstParsedExpression.expression: '3'
21245
- const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21246
- currentIndex = firstParsedExpression.endpoint + 1;
21247
-
21248
- // Parse second expression
21249
- // Ex) secondParsedExpression.expression: '7'
21250
- const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21251
- currentIndex = secondParsedExpression.endpoint + 1;
21252
-
21253
- // Add expressions to running total of parsed expressions
21254
- if (parsedString.length) {
21255
- parsedString += " ";
21256
- }
21257
-
21258
- // Apply a custom handler based on the parsed subexpressions
21259
- parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21260
-
21261
- // Find next DFrac, relative to currentIndex
21262
- nextFrac = getNextFracIndex(tex, currentIndex);
21263
- }
21264
-
21265
- // Add remaining TeX, which is \dfrac-free
21266
- parsedString += tex.slice(currentIndex);
21267
- return parsedString;
21268
- }
21269
-
21270
- /*
21271
- * Modify a TeX expression, returning another TeX expression. The resulting
21272
- * expression will have its innermost fractions stubbed out with \fracs
21273
- * (as opposed to \dfracs). All other content will remain untouched.
21274
- */
21275
- function modifyTex(tex) {
21276
- function isNestedFraction(tex) {
21277
- return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21278
- }
21279
- const handler = function (exp1, exp2) {
21280
- let prefix;
21281
- if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21282
- prefix = "\\dfrac";
21283
- } else {
21284
- prefix = "\\frac";
21285
- }
21286
- return prefix + " {" + exp1 + "}{" + exp2 + "}";
21287
- };
21288
- return walkTex(tex, handler);
21289
- }
21290
-
21291
- /*
21292
- * Parse a TeX expression into something interpretable by input-number.
21293
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21294
- * (2) removing backslash-escaping from certain characters (right now, only
21295
- * percent signs).
21296
- *
21297
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21298
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21299
- * recursion is to allow for nested \dfrac elements.
21300
- *
21301
- * Backslash-escapes are removed with a simple search-and-replace.
21302
- */
21303
- function parseTex(tex) {
21304
- const handler = function (exp1, exp2) {
21305
- return exp1 + "/" + exp2;
21306
- };
21307
- const texWithoutFracs = walkTex(tex, handler);
21308
- return texWithoutFracs.replace("\\%", "%");
21309
- }
21310
- var TexWrangler = {
21311
- parseTex,
21312
- modifyTex
21313
- };
21314
-
21315
- const ParseTex$1 = TexWrangler.parseTex;
21316
- const answerTypes = {
21317
- number: {
21318
- name: "Numbers",
21319
- forms: "integer, decimal, proper, improper, mixed"
21320
- },
21321
- decimal: {
21322
- name: "Decimals",
21323
- forms: "decimal"
21324
- },
21325
- integer: {
21326
- name: "Integers",
21327
- forms: "integer"
21328
- },
21329
- rational: {
21330
- name: "Fractions and mixed numbers",
21331
- forms: "integer, proper, improper, mixed"
21332
- },
21333
- improper: {
21334
- name: "Improper numbers (no mixed)",
21335
- forms: "integer, proper, improper"
21336
- },
21337
- mixed: {
21338
- name: "Mixed numbers (no improper)",
21339
- forms: "integer, proper, mixed"
21340
- },
21341
- percent: {
21342
- name: "Numbers or percents",
21343
- forms: "integer, decimal, proper, improper, mixed, percent"
21344
- },
21345
- pi: {
21346
- name: "Numbers with pi",
21347
- forms: "pi"
21348
- }
21349
- };
21350
- function scoreInputNumber(userInput, rubric, strings) {
21351
- if (rubric.answerType == null) {
21352
- rubric.answerType = "number";
21353
- }
21354
-
21355
- // note(matthewc): this will get immediately parsed again by
21356
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21357
- // expected here
21358
- const stringValue = `${rubric.value}`;
21359
- const val = perseusScore.KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21360
- simplify: rubric.simplify,
21361
- inexact: rubric.inexact || undefined,
21362
- maxError: rubric.maxError,
21363
- forms: answerTypes[rubric.answerType].forms
21364
- });
21365
-
21366
- // We may have received TeX; try to parse it before grading.
21367
- // If `currentValue` is not TeX, this should be a no-op.
21368
- const currentValue = ParseTex$1(userInput.currentValue);
21369
- const result = val(currentValue);
21370
- if (result.empty) {
21371
- return {
21372
- type: "invalid",
21373
- message: result.message
21374
- };
21375
- }
21376
- return {
21377
- type: "points",
21378
- earned: result.correct ? 1 : 0,
21379
- total: 1,
21380
- message: result.message
21381
- };
21382
- }
21383
-
21384
20732
  const formExamples$1 = {
21385
20733
  integer: function (options, strings) {
21386
20734
  return strings.integerExample;
@@ -21492,7 +20840,7 @@ class InputNumber extends React__namespace.Component {
21492
20840
  strings
21493
20841
  } = this.context;
21494
20842
  const type = this.props.answerType;
21495
- const forms = answerTypes[type].forms.split(/\s*,\s*/);
20843
+ const forms = perseusScore.inputNumberAnswerTypes[type].forms.split(/\s*,\s*/);
21496
20844
  const examples = ___default["default"].map(forms, form => formExamples$1[form](this.props, strings));
21497
20845
  return [strings.yourAnswer].concat(examples);
21498
20846
  }
@@ -21588,7 +20936,7 @@ var InputNumber$1 = {
21588
20936
  isLintable: true,
21589
20937
  // TODO(LEMS-2656): remove TS suppression
21590
20938
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInputNumberUserInput'.
21591
- scorer: scoreInputNumber,
20939
+ scorer: perseusScore.scoreInputNumber,
21592
20940
  getOneCorrectAnswerFromRubric(rubric) {
21593
20941
  if (rubric.value == null) {
21594
20942
  return;
@@ -21611,139 +20959,6 @@ const getPromptJSON$r = (renderProps, userInput) => {
21611
20959
  };
21612
20960
  };
21613
20961
 
21614
- const ParseTex = TexWrangler.parseTex;
21615
- const answerFormButtons = [{
21616
- title: "Integers",
21617
- value: "integer",
21618
- content: "6"
21619
- }, {
21620
- title: "Decimals",
21621
- value: "decimal",
21622
- content: "0.75"
21623
- }, {
21624
- title: "Proper fractions",
21625
- value: "proper",
21626
- content: "\u2157"
21627
- }, {
21628
- title: "Improper fractions",
21629
- value: "improper",
21630
- content: "\u2077\u2044\u2084"
21631
- }, {
21632
- title: "Mixed numbers",
21633
- value: "mixed",
21634
- content: "1\u00BE"
21635
- }, {
21636
- title: "Numbers with \u03C0",
21637
- value: "pi",
21638
- content: "\u03C0"
21639
- }];
21640
-
21641
- // This function checks if the user inputted a percent value, parsing
21642
- // it as a number (and maybe scaling) so that it can be graded.
21643
- // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
21644
- // can accept several input forms with or without "%", the decision
21645
- // to parse based on the presence of "%" in the input, is so that we
21646
- // don't accidently scale the user typed value before grading, CP-930.
21647
- function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21648
- // If the input value is not a string ending with "%", then there's
21649
- // nothing more to do. The value will be graded as inputted by user.
21650
- if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
21651
- return inputValue;
21652
- }
21653
- const value = parseFloat(inputValue.slice(0, -1));
21654
- // If the input value stripped of the "%" cannot be parsed as a
21655
- // number (the slice is not really necessary for parseFloat to work
21656
- // if the string starts with a number) then return the original
21657
- // input for grading.
21658
- if (isNaN(value)) {
21659
- return inputValue;
21660
- }
21661
-
21662
- // Next, if all correct answers are in the range of |0,1| then we
21663
- // scale the user typed value. We assume this is the correct thing
21664
- // to do since the input value ends with "%".
21665
- if (normalizedAnswerExpected) {
21666
- return value / 100;
21667
- }
21668
-
21669
- // Otherwise, we return input value (number) stripped of the "%".
21670
- return value;
21671
- }
21672
- function scoreNumericInput(userInput, rubric, strings) {
21673
- const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21674
- // Don't default to validating the answer as a pi answer
21675
- // if answerForm isn't set on the answer
21676
- // https://khanacademy.atlassian.net/browse/LC-691
21677
- .filter(e => e !== "pi");
21678
- const createValidator = answer => {
21679
- const stringAnswer = `${answer.value}`;
21680
-
21681
- // Always validate against the provided answer forms (pi, decimal, etc.)
21682
- const validatorForms = [...(answer.answerForms ?? [])];
21683
-
21684
- // When an answer is set to strict, we validate using ONLY
21685
- // the provided answerForms. If strict is false, or if there
21686
- // were no provided answer forms, we will include all
21687
- // of the default answer forms in our validator.
21688
- if (!answer.strict || validatorForms.length === 0) {
21689
- validatorForms.push(...defaultAnswerForms);
21690
- }
21691
- return perseusScore.KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
21692
- message: answer.message,
21693
- simplify: answer.status === "correct" ? answer.simplify : "optional",
21694
- inexact: true,
21695
- // TODO(merlob) backfill / delete
21696
- maxError: answer.maxError,
21697
- forms: validatorForms
21698
- });
21699
- };
21700
-
21701
- // We may have received TeX; try to parse it before grading.
21702
- // If `currentValue` is not TeX, this should be a no-op.
21703
- const currentValue = ParseTex(userInput.currentValue);
21704
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value != null && Math.abs(answer.value) <= 1);
21705
-
21706
- // The coefficient is an attribute of the widget
21707
- let localValue = currentValue;
21708
- if (rubric.coefficient) {
21709
- if (!localValue) {
21710
- localValue = 1;
21711
- } else if (localValue === "-") {
21712
- localValue = -1;
21713
- }
21714
- }
21715
- const matchedAnswer = rubric.answers.map(answer => {
21716
- const validateFn = createValidator(answer);
21717
- const score = validateFn(maybeParsePercentInput(localValue, normalizedAnswerExpected));
21718
- return {
21719
- ...answer,
21720
- score
21721
- };
21722
- }).find(answer => {
21723
- // NOTE: "answer.score.correct" indicates a match via the validate function.
21724
- // It does NOT indicate that the answer itself is correct.
21725
- return answer.score.correct || answer.status === "correct" && answer.score.empty;
21726
- });
21727
- const result = matchedAnswer?.status === "correct" ? matchedAnswer.score : {
21728
- empty: matchedAnswer?.status === "ungraded",
21729
- correct: matchedAnswer?.status === "correct",
21730
- message: matchedAnswer?.message ?? null,
21731
- guess: localValue
21732
- };
21733
- if (result.empty) {
21734
- return {
21735
- type: "invalid",
21736
- message: result.message
21737
- };
21738
- }
21739
- return {
21740
- type: "points",
21741
- earned: result.correct ? 1 : 0,
21742
- total: 1,
21743
- message: result.message
21744
- };
21745
- }
21746
-
21747
20962
  const formExamples = {
21748
20963
  integer: (form, strings) => strings.integerExample,
21749
20964
  proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
@@ -21980,7 +21195,7 @@ var NumericInput$1 = {
21980
21195
  isLintable: true,
21981
21196
  // TODO(LEMS-2656): remove TS suppression
21982
21197
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumericInputUserInput'.
21983
- scorer: scoreNumericInput,
21198
+ scorer: perseusScore.scoreNumericInput,
21984
21199
  // TODO(LEMS-2656): remove TS suppression
21985
21200
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
21986
21201
  getOneCorrectAnswerFromRubric(rubric) {
@@ -25743,72 +24958,6 @@ const styles$o = aphrodite.StyleSheet.create({
25743
24958
  }
25744
24959
  });
25745
24960
 
25746
- /**
25747
- * Checks if the user has selected at least one option. Additional validation
25748
- * is done in scoreRadio to check if the number of selected options is correct
25749
- * and if the user has selected both a correct option and the "none of the above"
25750
- * option.
25751
- * @param userInput
25752
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
25753
- */
25754
- function validateRadio(userInput) {
25755
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25756
- return sum + (selected ? 1 : 0);
25757
- }, 0);
25758
- if (numSelected === 0) {
25759
- return {
25760
- type: "invalid",
25761
- message: null
25762
- };
25763
- }
25764
- return null;
25765
- }
25766
-
25767
- function scoreRadio(userInput, rubric, strings) {
25768
- const validationError = validateRadio(userInput);
25769
- if (validationError) {
25770
- return validationError;
25771
- }
25772
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25773
- return sum + (selected ? 1 : 0);
25774
- }, 0);
25775
- const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
25776
- return currentChoice.correct ? sum + 1 : sum;
25777
- }, 0);
25778
- if (numCorrect > 1 && numSelected !== numCorrect) {
25779
- return {
25780
- type: "invalid",
25781
- message: strings.chooseCorrectNum
25782
- };
25783
- // If NOTA and some other answer are checked, ...
25784
- }
25785
-
25786
- const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
25787
- if (noneOfTheAboveSelected && numSelected > 1) {
25788
- return {
25789
- type: "invalid",
25790
- message: strings.notNoneOfTheAbove
25791
- };
25792
- }
25793
- const correct = userInput.choicesSelected.every((selected, i) => {
25794
- let isCorrect;
25795
- if (rubric.choices[i].isNoneOfTheAbove) {
25796
- isCorrect = rubric.choices.every((choice, j) => {
25797
- return i === j || !choice.correct;
25798
- });
25799
- } else {
25800
- isCorrect = !!rubric.choices[i].correct;
25801
- }
25802
- return isCorrect === selected;
25803
- });
25804
- return {
25805
- type: "points",
25806
- earned: correct ? 1 : 0,
25807
- total: 1,
25808
- message: null
25809
- };
25810
- }
25811
-
25812
24961
  // RenderProps is the return type for radio.jsx#transform
25813
24962
 
25814
24963
  class Radio$1 extends React__namespace.Component {
@@ -25996,7 +25145,7 @@ class Radio$1 extends React__namespace.Component {
25996
25145
  choiceStates
25997
25146
  } = this.props;
25998
25147
  if (choiceStates) {
25999
- const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
25148
+ const score = perseusScore.scoreRadio(this.getUserInput(), rubric);
26000
25149
  const widgetCorrect = score.type === "points" && score.total === score.earned;
26001
25150
  const newStates = choiceStates.map(state => ({
26002
25151
  ...state,
@@ -26279,7 +25428,7 @@ var Radio = {
26279
25428
  isLintable: true,
26280
25429
  // TODO(LEMS-2656): remove TS suppression
26281
25430
  // @ts-expect-error: Type UserInput is not assignable to type PerseusRadioUserInput
26282
- scorer: scoreRadio
25431
+ scorer: perseusScore.scoreRadio
26283
25432
  };
26284
25433
 
26285
25434
  /* eslint-disable import/no-named-as-default */
@@ -26316,44 +25465,6 @@ function getCategorizerPublicWidgetOptions(options) {
26316
25465
  };
26317
25466
  }
26318
25467
 
26319
- /**
26320
- * Checks userInput from the categorizer widget to see if the user has selected
26321
- * a category for each item.
26322
- * @param userInput - The user's input corresponding to an array of indices that
26323
- * represent the selected category for each row/item.
26324
- * @param validationData - An array of strings corresponding to each row/item
26325
- * @param strings - Used to provide a validation message
26326
- */
26327
- function validateCategorizer(userInput, validationData, strings) {
26328
- const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26329
- if (incomplete) {
26330
- return {
26331
- type: "invalid",
26332
- message: strings.invalidSelection
26333
- };
26334
- }
26335
- return null;
26336
- }
26337
-
26338
- function scoreCategorizer(userInput, scoringData, strings) {
26339
- const validationError = validateCategorizer(userInput, scoringData, strings);
26340
- if (validationError) {
26341
- return validationError;
26342
- }
26343
- let allCorrect = true;
26344
- scoringData.values.forEach((value, i) => {
26345
- if (userInput.values[i] !== value) {
26346
- allCorrect = false;
26347
- }
26348
- });
26349
- return {
26350
- type: "points",
26351
- earned: allCorrect ? 1 : 0,
26352
- total: 1,
26353
- message: null
26354
- };
26355
- }
26356
-
26357
25468
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
26358
25469
  class Categorizer extends React__namespace.Component {
26359
25470
  static contextType = PerseusI18nContext;
@@ -26535,7 +25646,7 @@ var Categorizer$1 = {
26535
25646
  isLintable: true,
26536
25647
  // TODO(LEMS-2656): remove TS suppression
26537
25648
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
26538
- scorer: scoreCategorizer,
25649
+ scorer: perseusScore.scoreCategorizer,
26539
25650
  getPublicWidgetOptions: getCategorizerPublicWidgetOptions
26540
25651
  };
26541
25652
 
@@ -26599,31 +25710,6 @@ const getPromptJSON$m = () => {
26599
25710
  return getUnsupportedPromptJSON("cs-program");
26600
25711
  };
26601
25712
 
26602
- function scoreCSProgram(state) {
26603
- // The iframe can tell us whether it's correct or incorrect,
26604
- // and pass an optional message
26605
- if (state.status === "correct") {
26606
- return {
26607
- type: "points",
26608
- earned: 1,
26609
- total: 1,
26610
- message: state.message || null
26611
- };
26612
- }
26613
- if (state.status === "incorrect") {
26614
- return {
26615
- type: "points",
26616
- earned: 0,
26617
- total: 1,
26618
- message: state.message || null
26619
- };
26620
- }
26621
- return {
26622
- type: "invalid",
26623
- message: "Keep going, you're not there yet!"
26624
- };
26625
- }
26626
-
26627
25713
  /**
26628
25714
  * This widget is for embedding Khan Academy CS programs.
26629
25715
  */
@@ -26775,7 +25861,7 @@ var CSProgram$1 = {
26775
25861
  hidden: true,
26776
25862
  // TODO(LEMS-2656): remove TS suppression
26777
25863
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusCSProgramUserInput'.
26778
- scorer: scoreCSProgram
25864
+ scorer: perseusScore.scoreCSProgram
26779
25865
  };
26780
25866
 
26781
25867
  const getPromptJSON$l = renderProps => {
@@ -26900,34 +25986,6 @@ const getPromptJSON$k = (renderProps, userInput) => {
26900
25986
  };
26901
25987
  };
26902
25988
 
26903
- /**
26904
- * Checks if the user has selected an item from the dropdown before scoring.
26905
- * This is shown with a userInput value / index other than 0.
26906
- */
26907
- function validateDropdown(userInput) {
26908
- if (userInput.value === 0) {
26909
- return {
26910
- type: "invalid",
26911
- message: null
26912
- };
26913
- }
26914
- return null;
26915
- }
26916
-
26917
- function scoreDropdown(userInput, rubric) {
26918
- const validationError = validateDropdown(userInput);
26919
- if (validationError) {
26920
- return validationError;
26921
- }
26922
- const correct = rubric.choices[userInput.value - 1].correct;
26923
- return {
26924
- type: "points",
26925
- earned: correct ? 1 : 0,
26926
- total: 1,
26927
- message: null
26928
- };
26929
- }
26930
-
26931
25989
  class Dropdown extends React__namespace.Component {
26932
25990
  static contextType = PerseusI18nContext;
26933
25991
  static defaultProps = {
@@ -27028,7 +26086,7 @@ var Dropdown$1 = {
27028
26086
  transform: optionsTransform,
27029
26087
  // TODO(LEMS-2656): remove TS suppression
27030
26088
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusDropdownUserInput'.
27031
- scorer: scoreDropdown
26089
+ scorer: perseusScore.scoreDropdown
27032
26090
  };
27033
26091
 
27034
26092
  const getPromptJSON$j = renderProps => {
@@ -27999,408 +27057,21 @@ const getPromptJSON$g = (renderProps, userInput) => {
27999
27057
  };
28000
27058
  };
28001
27059
 
28002
- // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
28003
- 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
+ };
28004
27068
  const DEFAULT_BACKGROUND_IMAGE = {
28005
27069
  url: null
28006
27070
  };
28007
-
28008
- // TODO(charlie): These really need to go into a utility file as they're being
28009
- // used by both interactive-graph and now grapher.
28010
- function canonicalSineCoefficients(coeffs) {
28011
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
28012
- // this function ensures that a, b > 0, and c is its
28013
- // smallest possible positive value.
28014
- let amplitude = coeffs[0];
28015
- let angularFrequency = coeffs[1];
28016
- let phase = coeffs[2];
28017
- const verticalOffset = coeffs[3];
28018
-
28019
- // Guarantee a > 0
28020
- if (amplitude < 0) {
28021
- amplitude *= -1;
28022
- angularFrequency *= -1;
28023
- phase *= -1;
28024
- }
28025
- const period = 2 * Math.PI;
28026
- // Guarantee b > 0
28027
- if (angularFrequency < 0) {
28028
- angularFrequency *= -1;
28029
- phase *= -1;
28030
- phase += period / 2;
28031
- }
28032
-
28033
- // Guarantee c is smallest possible positive value
28034
- while (phase > 0) {
28035
- phase -= period;
28036
- }
28037
- while (phase < 0) {
28038
- phase += period;
28039
- }
28040
- return [amplitude, angularFrequency, phase, verticalOffset];
28041
- }
28042
- function canonicalTangentCoefficients(coeffs) {
28043
- // For a curve of the form f(x) = a * Tan(b * x - c) + d,
28044
- // this function ensures that a, b > 0, and c is its
28045
- // smallest possible positive value.
28046
- let amplitude = coeffs[0];
28047
- let angularFrequency = coeffs[1];
28048
- let phase = coeffs[2];
28049
- const verticalOffset = coeffs[3];
28050
-
28051
- // Guarantee a > 0
28052
- if (amplitude < 0) {
28053
- amplitude *= -1;
28054
- angularFrequency *= -1;
28055
- phase *= -1;
28056
- }
28057
- const period = Math.PI;
28058
- // Guarantee b > 0
28059
- if (angularFrequency < 0) {
28060
- angularFrequency *= -1;
28061
- phase *= -1;
28062
- phase += period / 2;
28063
- }
28064
-
28065
- // Guarantee c is smallest possible positive value
28066
- while (phase > 0) {
28067
- phase -= period;
28068
- }
28069
- while (phase < 0) {
28070
- phase += period;
28071
- }
28072
- return [amplitude, angularFrequency, phase, verticalOffset];
28073
- }
28074
- const PlotDefaults = {
28075
- areEqual: function (coeffs1, coeffs2) {
28076
- return Util.deepEq(coeffs1, coeffs2);
28077
- },
28078
- Movable: Plot$1,
28079
- getPropsForCoeffs: function (coeffs) {
28080
- return {
28081
- // @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; }'.
28082
- fn: ___default["default"].partial(this.getFunctionForCoeffs, coeffs)
28083
- };
28084
- }
28085
- };
28086
- const Linear = ___default["default"].extend({}, PlotDefaults, {
28087
- url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
28088
- defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
28089
- getCoefficients: function (coords) {
28090
- const p1 = coords[0];
28091
- const p2 = coords[1];
28092
- const denom = p2[0] - p1[0];
28093
- const num = p2[1] - p1[1];
28094
- if (denom === 0) {
28095
- return;
28096
- }
28097
- const m = num / denom;
28098
- const b = p2[1] - m * p2[0];
28099
- return [m, b];
28100
- },
28101
- getFunctionForCoeffs: function (coeffs, x) {
28102
- const m = coeffs[0];
28103
- const b = coeffs[1];
28104
- return m * x + b;
28105
- },
28106
- getEquationString: function (coords) {
28107
- const coeffs = this.getCoefficients(coords);
28108
- const m = coeffs[0];
28109
- const b = coeffs[1];
28110
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
28111
- }
28112
- });
28113
- const Quadratic = ___default["default"].extend({}, PlotDefaults, {
28114
- url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
28115
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28116
- // @ts-expect-error - TS2339 - Property 'Parabola' does not exist on type 'typeof Graphie'.
28117
- Movable: Graphie.Parabola,
28118
- getCoefficients: function (coords) {
28119
- const p1 = coords[0];
28120
- const p2 = coords[1];
28121
-
28122
- // Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
28123
- const h = p1[0];
28124
- const k = p1[1];
28125
-
28126
- // Use these to calculate familiar a, b, c
28127
- const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
28128
- const b = -2 * h * a;
28129
- const c = a * h * h + k;
28130
- return [a, b, c];
28131
- },
28132
- getFunctionForCoeffs: function (coeffs, x) {
28133
- const a = coeffs[0];
28134
- const b = coeffs[1];
28135
- const c = coeffs[2];
28136
- return (a * x + b) * x + c;
28137
- },
28138
- getPropsForCoeffs: function (coeffs) {
28139
- return {
28140
- a: coeffs[0],
28141
- b: coeffs[1],
28142
- c: coeffs[2]
28143
- };
28144
- },
28145
- getEquationString: function (coords) {
28146
- const coeffs = this.getCoefficients(coords);
28147
- const a = coeffs[0];
28148
- const b = coeffs[1];
28149
- const c = coeffs[2];
28150
- return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
28151
- }
28152
- });
28153
- const Sinusoid = ___default["default"].extend({}, PlotDefaults, {
28154
- url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
28155
- defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
28156
- // @ts-expect-error - TS2339 - Property 'Sinusoid' does not exist on type 'typeof Graphie'.
28157
- Movable: Graphie.Sinusoid,
28158
- getCoefficients: function (coords) {
28159
- const p1 = coords[0];
28160
- const p2 = coords[1];
28161
- const a = p2[1] - p1[1];
28162
- const b = Math.PI / (2 * (p2[0] - p1[0]));
28163
- const c = p1[0] * b;
28164
- const d = p1[1];
28165
- return [a, b, c, d];
28166
- },
28167
- getFunctionForCoeffs: function (coeffs, x) {
28168
- const a = coeffs[0];
28169
- const b = coeffs[1];
28170
- const c = coeffs[2];
28171
- const d = coeffs[3];
28172
- return a * Math.sin(b * x - c) + d;
28173
- },
28174
- getPropsForCoeffs: function (coeffs) {
28175
- return {
28176
- a: coeffs[0],
28177
- b: coeffs[1],
28178
- c: coeffs[2],
28179
- d: coeffs[3]
28180
- };
28181
- },
28182
- getEquationString: function (coords) {
28183
- const coeffs = this.getCoefficients(coords);
28184
- const a = coeffs[0];
28185
- const b = coeffs[1];
28186
- const c = coeffs[2];
28187
- const d = coeffs[3];
28188
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28189
- },
28190
- areEqual: function (coeffs1, coeffs2) {
28191
- return Util.deepEq(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
28192
- }
28193
- });
28194
- const Tangent = ___default["default"].extend({}, PlotDefaults, {
28195
- url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
28196
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28197
- getCoefficients: function (coords) {
28198
- const p1 = coords[0];
28199
- const p2 = coords[1];
28200
- const a = p2[1] - p1[1];
28201
- const b = Math.PI / (4 * (p2[0] - p1[0]));
28202
- const c = p1[0] * b;
28203
- const d = p1[1];
28204
- return [a, b, c, d];
28205
- },
28206
- getFunctionForCoeffs: function (coeffs, x) {
28207
- const a = coeffs[0];
28208
- const b = coeffs[1];
28209
- const c = coeffs[2];
28210
- const d = coeffs[3];
28211
- return a * Math.tan(b * x - c) + d;
28212
- },
28213
- getEquationString: function (coords) {
28214
- const coeffs = this.getCoefficients(coords);
28215
- const a = coeffs[0];
28216
- const b = coeffs[1];
28217
- const c = coeffs[2];
28218
- const d = coeffs[3];
28219
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
28220
- },
28221
- areEqual: function (coeffs1, coeffs2) {
28222
- return Util.deepEq(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
28223
- }
28224
- });
28225
- const Exponential = ___default["default"].extend({}, PlotDefaults, {
28226
- url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
28227
- defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
28228
- defaultAsymptote: [[0, 0.5], [1.0, 0.5]],
28229
- /**
28230
- * Add extra constraints for movement of the points or asymptote (below):
28231
- * newCoord: [x, y]
28232
- * The end position of the point or asymptote endpoint
28233
- * oldCoord: [x, y]
28234
- * The old position of the point or asymptote endpoint
28235
- * coords:
28236
- * An array of coordinates representing the proposed end configuration
28237
- * of the plot coordinates.
28238
- * asymptote:
28239
- * An array of coordinates representing the proposed end configuration
28240
- * of the asymptote.
28241
- *
28242
- * Return: either a coordinate (to be used as the resulting coordinate of
28243
- * the move) or a boolean, where `true` uses newCoord as the resulting
28244
- * coordinate, and `false` uses oldCoord as the resulting coordinate.
28245
- */
28246
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28247
- const y = asymptote[0][1];
28248
- return ___default["default"].all(coords, coord => coord[1] !== y);
28249
- },
28250
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28251
- const y = newCoord[1];
28252
- const isValid = ___default["default"].all(coords, coord => coord[1] > y) || ___default["default"].all(coords, coord => coord[1] < y);
28253
- if (isValid) {
28254
- return [oldCoord[0], y];
28255
- }
28256
- // Snap the asymptote as close as possible, i.e., if the user moves
28257
- // the mouse really quickly into an invalid region
28258
- const oldY = oldCoord[1];
28259
- const wasBelow = ___default["default"].all(coords, coord => coord[1] > oldY);
28260
- if (wasBelow) {
28261
- const bottomMost = ___default["default"].min(___default["default"].map(coords, coord => coord[1]));
28262
- return [oldCoord[0], bottomMost - graph.snapStep[1]];
28263
- }
28264
- const topMost = ___default["default"].max(___default["default"].map(coords, coord => coord[1]));
28265
- return [oldCoord[0], topMost + graph.snapStep[1]];
28266
- },
28267
- allowReflectOverAsymptote: true,
28268
- getCoefficients: function (coords, asymptote) {
28269
- const p1 = coords[0];
28270
- const p2 = coords[1];
28271
- const c = asymptote[0][1];
28272
- const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]);
28273
- const a = (p1[1] - c) / Math.exp(b * p1[0]);
28274
- return [a, b, c];
28275
- },
28276
- getFunctionForCoeffs: function (coeffs, x) {
28277
- const a = coeffs[0];
28278
- const b = coeffs[1];
28279
- const c = coeffs[2];
28280
- return a * Math.exp(b * x) + c;
28281
- },
28282
- getEquationString: function (coords, asymptote) {
28283
- if (!asymptote) {
28284
- return null;
28285
- }
28286
- const coeffs = this.getCoefficients(coords, asymptote);
28287
- const a = coeffs[0];
28288
- const b = coeffs[1];
28289
- const c = coeffs[2];
28290
- return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
28291
- }
28292
- });
28293
- const Logarithm = ___default["default"].extend({}, PlotDefaults, {
28294
- url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
28295
- defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
28296
- defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
28297
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28298
- const x = asymptote[0][0];
28299
- return ___default["default"].all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
28300
- },
28301
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28302
- const x = newCoord[0];
28303
- const isValid = ___default["default"].all(coords, coord => coord[0] > x) || ___default["default"].all(coords, coord => coord[0] < x);
28304
- if (isValid) {
28305
- return [x, oldCoord[1]];
28306
- }
28307
- // Snap the asymptote as close as possible, i.e., if the user moves
28308
- // the mouse really quickly into an invalid region
28309
- const oldX = oldCoord[0];
28310
- const wasLeft = ___default["default"].all(coords, coord => coord[0] > oldX);
28311
- if (wasLeft) {
28312
- const leftMost = ___default["default"].min(___default["default"].map(coords, coord => coord[0]));
28313
- return [leftMost - graph.snapStep[0], oldCoord[1]];
28314
- }
28315
- const rightMost = ___default["default"].max(___default["default"].map(coords, coord => coord[0]));
28316
- return [rightMost + graph.snapStep[0], oldCoord[1]];
28317
- },
28318
- allowReflectOverAsymptote: true,
28319
- getCoefficients: function (coords, asymptote) {
28320
- // It's easiest to calculate the logarithm's coefficients by thinking
28321
- // about it as the inverse of the exponential, so we flip x and y and
28322
- // perform some algebra on the coefficients. This also unifies the
28323
- // logic between the two 'models'.
28324
- const flip = coord => [coord[1], coord[0]];
28325
- const inverseCoeffs = Exponential.getCoefficients(___default["default"].map(coords, flip), ___default["default"].map(asymptote, flip));
28326
- if (inverseCoeffs) {
28327
- const c = -inverseCoeffs[2] / inverseCoeffs[0];
28328
- const b = 1 / inverseCoeffs[0];
28329
- const a = 1 / inverseCoeffs[1];
28330
- return [a, b, c];
28331
- }
28332
- },
28333
- getFunctionForCoeffs: function (coeffs, x, asymptote) {
28334
- const a = coeffs[0];
28335
- const b = coeffs[1];
28336
- const c = coeffs[2];
28337
- return a * Math.log(b * x + c);
28338
- },
28339
- getEquationString: function (coords, asymptote) {
28340
- if (!asymptote) {
28341
- return null;
28342
- }
28343
- const coeffs = this.getCoefficients(coords, asymptote);
28344
- const a = coeffs[0];
28345
- const b = coeffs[1];
28346
- const c = coeffs[2];
28347
- return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28348
- }
28349
- });
28350
- const AbsoluteValue = ___default["default"].extend({}, PlotDefaults, {
28351
- url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
28352
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28353
- getCoefficients: function (coords) {
28354
- const p1 = coords[0];
28355
- const p2 = coords[1];
28356
- const denom = p2[0] - p1[0];
28357
- const num = p2[1] - p1[1];
28358
- if (denom === 0) {
28359
- return;
28360
- }
28361
- let m = Math.abs(num / denom);
28362
- if (p2[1] < p1[1]) {
28363
- m *= -1;
28364
- }
28365
- const horizontalOffset = p1[0];
28366
- const verticalOffset = p1[1];
28367
- return [m, horizontalOffset, verticalOffset];
28368
- },
28369
- getFunctionForCoeffs: function (coeffs, x) {
28370
- const m = coeffs[0];
28371
- const horizontalOffset = coeffs[1];
28372
- const verticalOffset = coeffs[2];
28373
- return m * Math.abs(x - horizontalOffset) + verticalOffset;
28374
- },
28375
- getEquationString: function (coords) {
28376
- const coeffs = this.getCoefficients(coords);
28377
- const m = coeffs[0];
28378
- const horizontalOffset = coeffs[1];
28379
- const verticalOffset = coeffs[2];
28380
- return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28381
- }
28382
- });
28383
-
28384
- /* Utility functions for dealing with graphing interfaces. */
28385
- const functionTypeMapping = {
28386
- linear: Linear,
28387
- quadratic: Quadratic,
28388
- sinusoid: Sinusoid,
28389
- tangent: Tangent,
28390
- exponential: Exponential,
28391
- logarithm: Logarithm,
28392
- absolute_value: AbsoluteValue
28393
- };
28394
- const allTypes = ___default["default"].keys(functionTypeMapping);
28395
- function functionForType(type) {
28396
- // @ts-expect-error: TypeScript doesn't know how to use deal with generics
28397
- // and conditional types in this way.
28398
- return functionTypeMapping[type];
28399
- }
28400
27071
  const getEquationString = props => {
28401
27072
  const plot = props.plot;
28402
27073
  if (plot.type && plot.coords) {
28403
- const handler = functionForType(plot.type);
27074
+ const handler = perseusCore.GrapherUtil.functionForType(plot.type);
28404
27075
  const result = handler.getEquationString(plot.coords, plot.asymptote);
28405
27076
  return result || "";
28406
27077
  }
@@ -28458,7 +27129,7 @@ const defaultPlotProps = (type, graph) => {
28458
27129
  // extra information. It would be valid to always submit the default
28459
27130
  // widget before even reading the question; you can't lose, but you
28460
27131
  // might get a free win.
28461
- const model = functionForType(type);
27132
+ const model = perseusCore.GrapherUtil.functionForType(type);
28462
27133
  const defaultAsymptote = "defaultAsymptote" in model ? model.defaultAsymptote : null;
28463
27134
  const gridStep = [1, 1];
28464
27135
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
@@ -28504,70 +27175,12 @@ const typeToButton = type => {
28504
27175
  value: type,
28505
27176
  title: capitalized,
28506
27177
  content: /*#__PURE__*/React__namespace.createElement("img", {
28507
- src: staticUrl(functionForType(type).url),
27178
+ src: staticUrl(perseusCore.GrapherUtil.functionForType(type).url),
28508
27179
  alt: capitalized
28509
27180
  })
28510
27181
  };
28511
27182
  };
28512
27183
 
28513
- function getCoefficientsByType(data) {
28514
- if (data.coords == null) {
28515
- return undefined;
28516
- }
28517
- if (data.type === "exponential" || data.type === "logarithm") {
28518
- const grader = functionForType(data.type);
28519
- return grader.getCoefficients(data.coords, data.asymptote);
28520
- } else if (data.type === "linear" || data.type === "quadratic" || data.type === "absolute_value" || data.type === "sinusoid" || data.type === "tangent") {
28521
- const grader = functionForType(data.type);
28522
- return grader.getCoefficients(data.coords);
28523
- } else {
28524
- throw new perseusCore.PerseusError("Invalid grapher type", perseusCore.Errors.InvalidInput);
28525
- }
28526
- }
28527
- function scoreGrapher(userInput, rubric) {
28528
- if (userInput.type !== rubric.correct.type) {
28529
- return {
28530
- type: "points",
28531
- earned: 0,
28532
- total: 1,
28533
- message: null
28534
- };
28535
- }
28536
-
28537
- // We haven't moved the coords
28538
- if (userInput.coords == null) {
28539
- return {
28540
- type: "invalid",
28541
- message: null
28542
- };
28543
- }
28544
-
28545
- // Get new function handler for grading
28546
- const grader = functionForType(userInput.type);
28547
- const guessCoeffs = getCoefficientsByType(userInput);
28548
- const correctCoeffs = getCoefficientsByType(rubric.correct);
28549
- if (guessCoeffs == null || correctCoeffs == null) {
28550
- return {
28551
- type: "invalid",
28552
- message: null
28553
- };
28554
- }
28555
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
28556
- return {
28557
- type: "points",
28558
- earned: 1,
28559
- total: 1,
28560
- message: null
28561
- };
28562
- }
28563
- return {
28564
- type: "points",
28565
- earned: 0,
28566
- total: 1,
28567
- message: null
28568
- };
28569
- }
28570
-
28571
27184
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
28572
27185
  const MovablePoint$3 = Graphie.MovablePoint;
28573
27186
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -28629,7 +27242,8 @@ class FunctionGrapher extends React__namespace.Component {
28629
27242
  return;
28630
27243
  }
28631
27244
  const functionProps = model.getPropsForCoeffs(coeffs, xRange);
28632
- return /*#__PURE__*/React__namespace.createElement(model.Movable, _extends({}, functionProps, {
27245
+ const Movable = movableTypeToComponent[model.movable];
27246
+ return /*#__PURE__*/React__namespace.createElement(Movable, _extends({}, functionProps, {
28633
27247
  key: this.props.model.url,
28634
27248
  range: xRange,
28635
27249
  style: style
@@ -28922,7 +27536,7 @@ class Grapher extends React__namespace.Component {
28922
27536
  setup: this._setupGraphie
28923
27537
  },
28924
27538
  onChange: this.handlePlotChanges,
28925
- model: type && functionForType(type),
27539
+ model: type && perseusCore.GrapherUtil.functionForType(type),
28926
27540
  coords: coords,
28927
27541
  asymptote: asymptote,
28928
27542
  static: this.props.static,
@@ -28971,7 +27585,7 @@ var Grapher$1 = {
28971
27585
  staticTransform: staticTransform$4,
28972
27586
  // TODO(LEMS-2656): remove TS suppression
28973
27587
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusGrapherUserInput'.
28974
- scorer: scoreGrapher
27588
+ scorer: perseusScore.scoreGrapher
28975
27589
  };
28976
27590
 
28977
27591
  const getPromptJSON$f = rendererJSON => {
@@ -29157,31 +27771,6 @@ const getPromptJSON$e = () => {
29157
27771
  return getUnsupportedPromptJSON("iframe");
29158
27772
  };
29159
27773
 
29160
- function scoreIframe(userInput) {
29161
- // The iframe can tell us whether it's correct or incorrect,
29162
- // and pass an optional message
29163
- if (userInput.status === "correct") {
29164
- return {
29165
- type: "points",
29166
- earned: 1,
29167
- total: 1,
29168
- message: userInput.message || null
29169
- };
29170
- }
29171
- if (userInput.status === "incorrect") {
29172
- return {
29173
- type: "points",
29174
- earned: 0,
29175
- total: 1,
29176
- message: userInput.message || null
29177
- };
29178
- }
29179
- return {
29180
- type: "invalid",
29181
- message: "Keep going, you're not there yet!"
29182
- };
29183
- }
29184
-
29185
27774
  /**
29186
27775
  * This is an iframe widget. It is used for rendering an iframe that
29187
27776
  * then communicates its state via window.postMessage
@@ -29316,7 +27905,7 @@ var Iframe$1 = {
29316
27905
  hidden: true,
29317
27906
  // TODO(LEMS-2656): remove TS suppression
29318
27907
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusIframeUserInput'.
29319
- scorer: scoreIframe
27908
+ scorer: perseusScore.scoreIframe
29320
27909
  };
29321
27910
 
29322
27911
  const getPromptJSON$d = renderProps => {
@@ -30763,63 +29352,6 @@ function findIntersectionOfRays(_ref3, _ref4) {
30763
29352
  }
30764
29353
  }
30765
29354
 
30766
- // This file contains helper functions for working with angles.
30767
-
30768
- function convertDegreesToRadians(degrees) {
30769
- return degrees / 180 * Math.PI;
30770
- }
30771
-
30772
- // Returns a value between -180 and 180, inclusive. The angle is measured
30773
- // between the positive x-axis and the given vector.
30774
- function calculateAngleInDegrees(_ref) {
30775
- let [x, y] = _ref;
30776
- return Math.atan2(y, x) * 180 / Math.PI;
30777
- }
30778
-
30779
- // Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
30780
- function polar(r, th) {
30781
- if (typeof r === "number") {
30782
- r = [r, r];
30783
- }
30784
- th = th * Math.PI / 180;
30785
- return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
30786
- }
30787
- // This function calculates the angle between two points and an optional vertex.
30788
- // If the vertex is not provided, the angle is measured between the two points.
30789
- // This does not account for reflex angles or clockwise position.
30790
- const getAngleFromVertex = (point, vertex) => {
30791
- const x = point[0] - vertex[0];
30792
- const y = point[1] - vertex[1];
30793
- if (!x && !y) {
30794
- return 0;
30795
- }
30796
- return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
30797
- };
30798
-
30799
- // This function calculates the clockwise angle between three points,
30800
- // and is used to generate the labels and equation strings of the
30801
- // current angle for the interactive graph.
30802
- const getClockwiseAngle = function (coords) {
30803
- let allowReflexAngles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
30804
- const coordsCopy = [...coords];
30805
- // The coords are saved as [point1, vertex, point2] in the interactive graph
30806
- const areClockwise = clockwise$1([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
30807
-
30808
- // We may need to reverse the coordinates if we allow
30809
- // reflex angles and the points are not in clockwise order.
30810
- const shouldReverseCoords = !areClockwise || allowReflexAngles;
30811
-
30812
- // Reverse the coordinates accordingly to ensure the angle is calculated correctly
30813
- const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
30814
-
30815
- // Calculate the angles between the two points and get the difference
30816
- // between the two angles to get the clockwise angle.
30817
- const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
30818
- const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
30819
- const angle = (startAngle + 360 - endAngle) % 360;
30820
- return angle;
30821
- };
30822
-
30823
29355
  const defaultGraphConfig = {
30824
29356
  range: [[0, 1], [0, 1]],
30825
29357
  tickStep: [1, 1],
@@ -32030,6 +30562,9 @@ function SVGLine(props) {
32030
30562
  });
32031
30563
  }
32032
30564
 
30565
+ const {
30566
+ calculateAngleInDegrees: calculateAngleInDegrees$3
30567
+ } = kmath.angles;
32033
30568
  function Vector(props) {
32034
30569
  const {
32035
30570
  tail,
@@ -32039,7 +30574,7 @@ function Vector(props) {
32039
30574
  } = props;
32040
30575
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
32041
30576
  const direction = mafs.vec.sub(tipPx, tailPx);
32042
- const angle = calculateAngleInDegrees(direction);
30577
+ const angle = calculateAngleInDegrees$3(direction);
32043
30578
  return /*#__PURE__*/React__namespace.createElement("g", {
32044
30579
  style: {
32045
30580
  stroke: color,
@@ -32056,6 +30591,14 @@ function Vector(props) {
32056
30591
  }));
32057
30592
  }
32058
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
+
32059
30602
  /**
32060
30603
  * Given a ray and a rectangular box, find the point where the ray intersects
32061
30604
  * the edge of the box. Assumes the `initialPoint` is inside the box.
@@ -32104,7 +30647,45 @@ function getArrayWithoutDuplicates(array) {
32104
30647
  }
32105
30648
  return returnArray;
32106
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
+ }
32107
30685
 
30686
+ const {
30687
+ calculateAngleInDegrees: calculateAngleInDegrees$2
30688
+ } = kmath.angles;
32108
30689
  const LockedLine = props => {
32109
30690
  const {
32110
30691
  color,
@@ -32136,7 +30717,7 @@ const LockedLine = props => {
32136
30717
  // eslint-disable-next-line react-hooks/rules-of-hooks
32137
30718
  const [tailPx, tipPx] = useTransformVectorsToPixels(point2.coord, point1.coord);
32138
30719
  const direction = mafs.vec.sub(tailPx, tipPx);
32139
- let angle = calculateAngleInDegrees(direction);
30720
+ let angle = calculateAngleInDegrees$2(direction);
32140
30721
  const startArrowHead = kind !== "segment" && /*#__PURE__*/React__namespace.createElement(Arrowhead, {
32141
30722
  angle: angle,
32142
30723
  tip: arrowTip,
@@ -32548,6 +31129,12 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
32548
31129
  in: "SourceGraphic"
32549
31130
  })));
32550
31131
 
31132
+ const {
31133
+ clockwise: clockwise$1
31134
+ } = kmath.geometry;
31135
+ const {
31136
+ getAngleFromVertex: getAngleFromVertex$1
31137
+ } = kmath.angles;
32551
31138
  const PolygonAngle = _ref => {
32552
31139
  let {
32553
31140
  centerPoint,
@@ -32638,8 +31225,8 @@ const Angle = _ref2 => {
32638
31225
  const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32639
31226
 
32640
31227
  // Calculate the angles between the two points
32641
- const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32642
- const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
31228
+ const startAngle = getAngleFromVertex$1(clockwiseCoords[0], vertex);
31229
+ const endAngle = getAngleFromVertex$1(clockwiseCoords[1], vertex);
32643
31230
  const angle = (startAngle + 360 - endAngle) % 360;
32644
31231
 
32645
31232
  // Check if the angle is reflexive
@@ -33000,14 +31587,6 @@ function range(min, max) {
33000
31587
  return result;
33001
31588
  }
33002
31589
 
33003
- function srFormatNumber(a, locale) {
33004
- // adding zero here converts negative zero to positive zero.
33005
- return (0 + a).toLocaleString(locale, {
33006
- maximumFractionDigits: 3,
33007
- useGrouping: false // no thousands separators
33008
- });
33009
- }
33010
-
33011
31590
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
33012
31591
  // graph.
33013
31592
  const hitboxSizePx = 48;
@@ -33400,6 +31979,11 @@ const MovablePoint$1 = /*#__PURE__*/React__namespace.forwardRef((props, pointRef
33400
31979
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle, visiblePoint);
33401
31980
  });
33402
31981
 
31982
+ const {
31983
+ calculateAngleInDegrees: calculateAngleInDegrees$1,
31984
+ getClockwiseAngle: getClockwiseAngle$2,
31985
+ polar: polar$1
31986
+ } = kmath.angles;
33403
31987
  function renderAngleGraph(state, dispatch) {
33404
31988
  return {
33405
31989
  graph: /*#__PURE__*/React__namespace.createElement(AngleGraph, {
@@ -33470,7 +32054,7 @@ function AngleGraph(props) {
33470
32054
  strings,
33471
32055
  locale
33472
32056
  } = usePerseusI18n();
33473
- const angleMeasure = srFormatNumber(getClockwiseAngle([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
32057
+ const angleMeasure = srFormatNumber(getClockwiseAngle$2([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
33474
32058
  const wholeAngleAriaLabel = strings.srAngleGraphAriaLabel;
33475
32059
  const wholeAngleDescription = strings.srAngleGraphAriaDescription({
33476
32060
  angleMeasure,
@@ -33538,9 +32122,7 @@ function AngleGraph(props) {
33538
32122
  ariaLabel: initialSideAriaLabel
33539
32123
  }), /*#__PURE__*/React__namespace.createElement("g", {
33540
32124
  id: "angle-description",
33541
- style: {
33542
- display: "hidden"
33543
- }
32125
+ style: a11y.srOnly
33544
32126
  }, wholeAngleDescription));
33545
32127
  }
33546
32128
  const positiveX = [1, 0];
@@ -33548,7 +32130,7 @@ const negativeX = [-1, 0];
33548
32130
  const positiveY = [0, 1];
33549
32131
  const negativeY = [0, -1];
33550
32132
  function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33551
- const currentAngle = calculateAngleInDegrees(mafs.vec.sub(sidePoint, vertex));
32133
+ const currentAngle = calculateAngleInDegrees$1(mafs.vec.sub(sidePoint, vertex));
33552
32134
 
33553
32135
  // Find the rays that start at the current point and point up, down, left
33554
32136
  // and right.
@@ -33565,8 +32147,8 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33565
32147
 
33566
32148
  // find the rays that start from the vertex and point in the direction of
33567
32149
  // the angles we just computed.
33568
- const counterClockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepCounterClockwise))];
33569
- 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))];
33570
32152
 
33571
32153
  // find the intersections of those rays with the horizontal and vertical
33572
32154
  // rays extending from the sidePoint. These intersections are the points to
@@ -33583,6 +32165,9 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33583
32165
  };
33584
32166
  }
33585
32167
 
32168
+ const {
32169
+ clockwise
32170
+ } = kmath.geometry;
33586
32171
  function getGradableGraph(state, initialGraph) {
33587
32172
  if (!state.hasBeenInteractedWith) {
33588
32173
  return {
@@ -33664,7 +32249,7 @@ function getGradableGraph(state, initialGraph) {
33664
32249
  // by the order of the points in the coords array, and we want to maintain
33665
32250
  // the same angle scoring with the legacy graph (which had a bug).
33666
32251
  // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33667
- 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]]);
33668
32253
  const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33669
32254
  const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33670
32255
  return {
@@ -33777,14 +32362,10 @@ function CircleGraph(props) {
33777
32362
  }
33778
32363
  }), /*#__PURE__*/React__namespace.createElement("g", {
33779
32364
  id: radiusId,
33780
- style: {
33781
- display: "hidden"
33782
- }
32365
+ style: a11y.srOnly
33783
32366
  }, srCircleRadius), /*#__PURE__*/React__namespace.createElement("g", {
33784
32367
  id: outerPointsId,
33785
- style: {
33786
- display: "hidden"
33787
- }
32368
+ style: a11y.srOnly
33788
32369
  }, srCircleOuterPoints));
33789
32370
  }
33790
32371
  function MovableCircle(props) {
@@ -33970,49 +32551,16 @@ const LinearGraph = (props, key) => {
33970
32551
  point2X: srFormatNumber(line[1][0], locale),
33971
32552
  point2Y: srFormatNumber(line[1][1], locale)
33972
32553
  });
33973
-
33974
- // Slope description
33975
- const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
33976
- let slopeString = "";
33977
- if (slope === Infinity || slope === -Infinity) {
33978
- slopeString = strings.srLinearGraphSlopeVertical;
33979
- } else if (slope === 0) {
33980
- slopeString = strings.srLinearGraphSlopeHorizontal;
33981
- } else {
33982
- slopeString = slope > 0 ? strings.srLinearGraphSlopeIncreasing : strings.srLinearGraphSlopeDecreasing;
33983
- }
33984
-
33985
- // Intersection description
33986
- const xIntercept = (0 - line[0][1]) / slope + line[0][0];
33987
- const yIntercept = line[0][1] - slope * line[0][0];
33988
- const hasXIntercept = xIntercept !== Infinity && xIntercept !== -Infinity;
33989
- const hasYIntercept = yIntercept !== Infinity && yIntercept !== -Infinity;
33990
- let interceptString;
33991
- if (hasXIntercept && hasYIntercept) {
33992
- // Describe both intercepts in the same sentence.
33993
- interceptString = xIntercept === 0 && yIntercept === 0 ? strings.srLinearGraphOriginIntercept : strings.srLinearGraphBothIntercepts({
33994
- xIntercept: srFormatNumber(xIntercept, locale),
33995
- yIntercept: srFormatNumber(yIntercept, locale)
33996
- });
33997
- } else {
33998
- // Describe only one intercept.
33999
- interceptString = hasXIntercept ? strings.srLinearGraphXOnlyIntercept({
34000
- xIntercept: srFormatNumber(xIntercept, locale)
34001
- }) : strings.srLinearGraphYOnlyIntercept({
34002
- yIntercept: srFormatNumber(yIntercept, locale)
34003
- });
34004
- }
32554
+ const slopeString = getSlopeStringForLine(line, strings);
32555
+ const interceptString = getInterceptStringForLine(line, strings, locale);
34005
32556
 
34006
32557
  // Linear graphs only have one line
34007
32558
  // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
34008
32559
  // and another action for moving multiple lines
34009
32560
  return /*#__PURE__*/React__namespace.createElement("g", {
34010
- className: "linear-graph-container"
34011
32561
  // Outer line minimal description
34012
- ,
34013
32562
  "aria-label": strings.srLinearGraph,
34014
- "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`,
34015
- role: "figure"
32563
+ "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`
34016
32564
  }, /*#__PURE__*/React__namespace.createElement(MovableLine, {
34017
32565
  key: 0,
34018
32566
  ariaLabels: {
@@ -34031,19 +32579,13 @@ const LinearGraph = (props, key) => {
34031
32579
  color: "var(--movable-line-stroke-color)"
34032
32580
  }), /*#__PURE__*/React__namespace.createElement("g", {
34033
32581
  id: pointsDescriptionId,
34034
- style: {
34035
- display: "hidden"
34036
- }
32582
+ style: a11y.srOnly
34037
32583
  }, linearGraphPointsDescription), /*#__PURE__*/React__namespace.createElement("g", {
34038
32584
  id: interceptDescriptionId,
34039
- style: {
34040
- display: "hidden"
34041
- }
32585
+ style: a11y.srOnly
34042
32586
  }, interceptString), /*#__PURE__*/React__namespace.createElement("g", {
34043
32587
  id: slopeDescriptionId,
34044
- style: {
34045
- display: "hidden"
34046
- }
32588
+ style: a11y.srOnly
34047
32589
  }, slopeString));
34048
32590
  };
34049
32591
 
@@ -34053,7 +32595,9 @@ function renderLinearSystemGraph(state, dispatch) {
34053
32595
  graphState: state,
34054
32596
  dispatch: dispatch
34055
32597
  }),
34056
- interactiveElementsDescription: null
32598
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(LinearSystemGraphDescription, {
32599
+ state: state
32600
+ })
34057
32601
  };
34058
32602
  }
34059
32603
  const LinearSystemGraph = props => {
@@ -34063,9 +32607,56 @@ const LinearSystemGraph = props => {
34063
32607
  const {
34064
32608
  coords: lines
34065
32609
  } = props.graphState;
34066
- 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, {
34067
32642
  key: i,
34068
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}`,
34069
32660
  onMoveLine: delta => {
34070
32661
  dispatch(actions.linearSystem.moveLine(i, delta));
34071
32662
  },
@@ -34075,8 +32666,67 @@ const LinearSystemGraph = props => {
34075
32666
  },
34076
32667
  onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
34077
32668
  color: "var(--movable-line-stroke-color)"
34078
- })), ";");
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
+ }));
34079
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
+ }
34080
32730
 
34081
32731
  function renderPointGraph(state, dispatch) {
34082
32732
  return {
@@ -34518,7 +33168,7 @@ function QuadraticGraph(props) {
34518
33168
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
34519
33169
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
34520
33170
  const coeffRef = React__namespace.useRef([0, 0, 0]);
34521
- const coeffs = getQuadraticCoefficients$1(coords);
33171
+ const coeffs = getQuadraticCoefficients$2(coords);
34522
33172
  if (coeffs !== undefined) {
34523
33173
  coeffRef.current = coeffs;
34524
33174
  }
@@ -34543,7 +33193,7 @@ function QuadraticGraph(props) {
34543
33193
  // These equations were originally set up in 2013 and may require some
34544
33194
  // additional comments to help clarify the quadratic formula manipulations
34545
33195
  // Origin: https://phabricator.khanacademy.org/D2413
34546
- const getQuadraticCoefficients$1 = coords => {
33196
+ const getQuadraticCoefficients$2 = coords => {
34547
33197
  const p1 = coords[0];
34548
33198
  const p2 = coords[1];
34549
33199
  const p3 = coords[2];
@@ -34566,7 +33216,9 @@ function renderRayGraph(state, dispatch) {
34566
33216
  graphState: state,
34567
33217
  dispatch: dispatch
34568
33218
  }),
34569
- interactiveElementsDescription: null
33219
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(RayGraphDescription, {
33220
+ state: state
33221
+ })
34570
33222
  };
34571
33223
  }
34572
33224
  const RayGraph = props => {
@@ -34578,18 +33230,104 @@ const RayGraph = props => {
34578
33230
  } = props.graphState;
34579
33231
  const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
34580
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
+ });
34581
33251
 
34582
33252
  // Ray graphs only have one line
34583
- 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, {
34584
33258
  points: line,
33259
+ ariaLabels: {
33260
+ point1AriaLabel: srRayEndpoint,
33261
+ point2AriaLabel: srRayTerminalPoint,
33262
+ grabHandleAriaLabel: srRayGrabHandle
33263
+ },
34585
33264
  onMoveLine: handleMoveLine,
34586
33265
  onMovePoint: handleMovePoint,
34587
33266
  extend: {
34588
33267
  start: false,
34589
33268
  end: true
34590
33269
  }
34591
- });
33270
+ }), /*#__PURE__*/React__namespace.createElement("g", {
33271
+ id: pointsDescriptionId,
33272
+ style: a11y.srOnly
33273
+ }, srRayPoints));
34592
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
+ }
34593
33331
 
34594
33332
  function renderSegmentGraph(state, dispatch) {
34595
33333
  return {
@@ -34704,6 +33442,10 @@ const getSinusoidCoefficients$1 = coords => {
34704
33442
  };
34705
33443
  };
34706
33444
 
33445
+ const {
33446
+ calculateAngleInDegrees,
33447
+ convertDegreesToRadians
33448
+ } = kmath.angles;
34707
33449
  const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
34708
33450
 
34709
33451
  // The vector from the center of the protractor to the top left corner of the
@@ -35419,6 +34161,10 @@ function mafsStateToInteractiveGraph(state, originalGraph) {
35419
34161
  }
35420
34162
  }
35421
34163
 
34164
+ const {
34165
+ magnitude: magnitude$2,
34166
+ vector: vector$2
34167
+ } = kmath.geometry;
35422
34168
  function initializeGraphState(params) {
35423
34169
  const {
35424
34170
  graph,
@@ -35706,7 +34452,7 @@ const getAngleCoords = params => {
35706
34452
  defaultCoords = normalizePoints(range, step, defaultCoords, true);
35707
34453
 
35708
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.
35709
- const radius = magnitude(vector(...defaultCoords));
34455
+ const radius = magnitude$2(vector$2(...defaultCoords));
35710
34456
 
35711
34457
  // We're adding a placeholder for the third point to appease ts and so that we
35712
34458
  // can calculate it after we've adjusted the first point by the angleOffsetDeg
@@ -35720,6 +34466,24 @@ const getAngleCoords = params => {
35720
34466
  return coords;
35721
34467
  };
35722
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;
35723
34487
  const minDistanceBetweenAngleVertexAndSidePoint = 2;
35724
34488
  function interactiveGraphReducer(state, action) {
35725
34489
  switch (action.type) {
@@ -36264,11 +35028,9 @@ const getChange = (coords, delta, constraintOpts) => {
36264
35028
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
36265
35029
  return [dx, dy];
36266
35030
  };
36267
- const eq$2 = Util.eq;
36268
-
36269
35031
  // Less than or approximately equal
36270
35032
  function leq$1(a, b) {
36271
- return a < b || eq$2(a, b);
35033
+ return a < b || perseusCore.approximateEqual(a, b);
36272
35034
  }
36273
35035
  function boundAndSnapToGrid(point, _ref) {
36274
35036
  let {
@@ -36418,7 +35180,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36418
35180
  });
36419
35181
 
36420
35182
  // Gets the radian angles between the coords and maps them to degrees
36421
- const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
35183
+ const angles = angleMeasures$1(coordsCopy).map(angle => angle * 180 / Math.PI);
36422
35184
 
36423
35185
  // Gets the relative index of a point
36424
35186
  const rel = j => {
@@ -36430,7 +35192,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36430
35192
  angles[rel(j)] = Math.round(angles[rel(j)]);
36431
35193
  });
36432
35194
  const getAngle = function (a, vertex, b) {
36433
- 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)]]);
36434
35196
  return angle;
36435
35197
  };
36436
35198
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -36442,9 +35204,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36442
35204
  })) {
36443
35205
  return startingPoint;
36444
35206
  }
36445
- const knownSide = magnitude(
35207
+ const knownSide = magnitude$1(
36446
35208
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
36447
- vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
35209
+ vector$1(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
36448
35210
 
36449
35211
  // Returns true if the points form a counter-clockwise turn;
36450
35212
  // a.k.a if the point is on the left or right of the polygon.
@@ -36452,7 +35214,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
36452
35214
  // we want to add the inner angle to the outer angle, and if it's on the right,
36453
35215
  // we want to subtract the inner angle from the outer angle. The angle solved
36454
35216
  // for is then used in the polar function to determine the new point.
36455
- 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;
36456
35218
 
36457
35219
  // Solve for side by using the law of sines
36458
35220
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
@@ -36491,7 +35253,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36491
35253
  };
36492
35254
  const sides = ___default["default"].map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
36493
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.
36494
- return magnitude(vector(...coordsCopy));
35256
+ return magnitude$1(vector$1(...coordsCopy));
36495
35257
  });
36496
35258
 
36497
35259
  // Round the sides to left and right of the current point
@@ -36506,7 +35268,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36506
35268
 
36507
35269
  // Solve for angle by using the law of cosines
36508
35270
  // Angle at the first vertex of the polygon
36509
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
35271
+ const innerAngle = lawOfCosines$1(sides[0], sides[2], sides[1]);
36510
35272
 
36511
35273
  // Angle at the second vertex of the polygon
36512
35274
  const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
@@ -36517,7 +35279,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
36517
35279
  // we want to add the inner angle to the outer angle, and if it's on the right,
36518
35280
  // we want to subtract the inner angle from the outer angle. The angle solved
36519
35281
  // for is then used in the polar function to determine the new point.
36520
- 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;
36521
35283
 
36522
35284
  // Uses the length of the first side of the polygon (radial coordinate)
36523
35285
  // and the angle between the first and second sides of the
@@ -36651,216 +35413,26 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
36651
35413
  }));
36652
35414
  });
36653
35415
 
36654
- const eq$1 = Util.eq;
36655
- const deepEq$3 = Util.deepEq;
36656
- function scoreInteractiveGraph(userInput, rubric) {
36657
- // None-type graphs are not graded
36658
- if (userInput.type === "none" && rubric.correct.type === "none") {
36659
- return {
36660
- type: "points",
36661
- earned: 0,
36662
- total: 0,
36663
- message: null
36664
- };
36665
- }
36666
-
36667
- // When nothing has moved, there will neither be coords nor the
36668
- // circle's center/radius fields. When those fields are absent, skip
36669
- // all these checks; just go mark the answer as empty.
36670
- const hasValue = Boolean(
36671
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
36672
- userInput.coords ||
36673
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
36674
- userInput.center && userInput.radius);
36675
- if (userInput.type === rubric.correct.type && hasValue) {
36676
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
36677
- const guess = userInput.coords;
36678
- const correct = rubric.correct.coords;
36679
-
36680
- // If both of the guess points are on the correct line, it's
36681
- // correct.
36682
- if (collinear(correct[0], correct[1], guess[0]) && collinear(correct[0], correct[1], guess[1])) {
36683
- return {
36684
- type: "points",
36685
- earned: 1,
36686
- total: 1,
36687
- message: null
36688
- };
36689
- }
36690
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
36691
- const guess = userInput.coords;
36692
- const correct = rubric.correct.coords;
36693
- 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])) {
36694
- return {
36695
- type: "points",
36696
- earned: 1,
36697
- total: 1,
36698
- message: null
36699
- };
36700
- }
36701
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
36702
- // If the parabola coefficients match, it's correct.
36703
- const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36704
- const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36705
- if (deepEq$3(guessCoeffs, correctCoeffs)) {
36706
- return {
36707
- type: "points",
36708
- earned: 1,
36709
- total: 1,
36710
- message: null
36711
- };
36712
- }
36713
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
36714
- const guessCoeffs = getSinusoidCoefficients(userInput.coords);
36715
- const correctCoeffs = getSinusoidCoefficients(rubric.correct.coords);
36716
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36717
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36718
- // If the canonical coefficients match, it's correct.
36719
- if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36720
- return {
36721
- type: "points",
36722
- earned: 1,
36723
- total: 1,
36724
- message: null
36725
- };
36726
- }
36727
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36728
- if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36729
- return {
36730
- type: "points",
36731
- earned: 1,
36732
- total: 1,
36733
- message: null
36734
- };
36735
- }
36736
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
36737
- let correct = rubric.correct.coords;
36738
- if (correct == null) {
36739
- throw new Error("Point graph rubric has null coords");
36740
- }
36741
- const guess = userInput.coords.slice();
36742
- correct = correct.slice();
36743
- // Everything's already rounded so we shouldn't need to do an
36744
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
36745
- // eq() anyway. The sort should be fine because it'll stringify
36746
- // it and -0 converted to a string is "0"
36747
- guess?.sort();
36748
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36749
- correct.sort();
36750
- if (deepEq$3(guess, correct)) {
36751
- return {
36752
- type: "points",
36753
- earned: 1,
36754
- total: 1,
36755
- message: null
36756
- };
36757
- }
36758
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
36759
- const guess = userInput.coords.slice();
36760
- const correct = rubric.correct.coords.slice();
36761
- let match;
36762
- if (rubric.correct.match === "similar") {
36763
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
36764
- } else if (rubric.correct.match === "congruent") {
36765
- match = similar(guess, correct, kmath.number.DEFAULT_TOLERANCE);
36766
- } else if (rubric.correct.match === "approx") {
36767
- match = similar(guess, correct, 0.1);
36768
- } else {
36769
- /* exact */
36770
- guess.sort();
36771
- correct.sort();
36772
- match = deepEq$3(guess, correct);
36773
- }
36774
- if (match) {
36775
- return {
36776
- type: "points",
36777
- earned: 1,
36778
- total: 1,
36779
- message: null
36780
- };
36781
- }
36782
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
36783
- let guess = Util.deepClone(userInput.coords);
36784
- let correct = Util.deepClone(rubric.correct.coords);
36785
- guess = ___default["default"].invoke(guess, "sort").sort();
36786
- correct = ___default["default"].invoke(correct, "sort").sort();
36787
- if (deepEq$3(guess, correct)) {
36788
- return {
36789
- type: "points",
36790
- earned: 1,
36791
- total: 1,
36792
- message: null
36793
- };
36794
- }
36795
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
36796
- const guess = userInput.coords;
36797
- const correct = rubric.correct.coords;
36798
- if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36799
- return {
36800
- type: "points",
36801
- earned: 1,
36802
- total: 1,
36803
- message: null
36804
- };
36805
- }
36806
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
36807
- const guess = userInput.coords;
36808
- const correct = rubric.correct.coords;
36809
- const allowReflexAngles = rubric.correct.allowReflexAngles;
36810
- let match;
36811
- if (rubric.correct.match === "congruent") {
36812
- const angles = ___default["default"].map([guess, correct], function (coords) {
36813
- if (!coords) {
36814
- return false;
36815
- }
36816
- const angle = getClockwiseAngle(coords, allowReflexAngles);
36817
- return angle;
36818
- });
36819
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36820
- match = eq$1(...angles);
36821
- } else {
36822
- /* exact */
36823
- match =
36824
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36825
- deepEq$3(guess[1], correct[1]) &&
36826
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36827
- collinear(correct[1], correct[0], guess[0]) &&
36828
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36829
- collinear(correct[1], correct[2], guess[2]);
36830
- }
36831
- if (match) {
36832
- return {
36833
- type: "points",
36834
- earned: 1,
36835
- total: 1,
36836
- message: null
36837
- };
36838
- }
36839
- }
36840
- }
36841
-
36842
- // The input wasn't correct, so check if it's a blank input or if it's
36843
- // actually just wrong
36844
- if (!hasValue || ___default["default"].isEqual(userInput, rubric.graph)) {
36845
- // We're where we started.
36846
- return {
36847
- type: "invalid",
36848
- message: null
36849
- };
36850
- }
36851
- return {
36852
- type: "points",
36853
- earned: 0,
36854
- total: 1,
36855
- message: null
36856
- };
36857
- }
36858
-
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;
36859
35432
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
36860
35433
  const defaultBackgroundImage = {
36861
35434
  url: null
36862
35435
  };
36863
- const eq = Util.eq;
36864
35436
  const UNLIMITED = "unlimited";
36865
35437
 
36866
35438
  // Sample background image:
@@ -36873,7 +35445,7 @@ function defaultVal(actual, defaultValue) {
36873
35445
 
36874
35446
  // Less than or approximately equal
36875
35447
  function leq(a, b) {
36876
- return a < b || eq(a, b);
35448
+ return a < b || perseusCore.approximateEqual(a, b);
36877
35449
  }
36878
35450
  function capitalize(str) {
36879
35451
  return str.replace(/(?:^|-)(.)/g, function (match, letter) {
@@ -37735,7 +36307,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37735
36307
  // Solve for side by using the law of sines
37736
36308
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
37737
36309
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37738
- const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
36310
+ const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
37739
36311
  return this.graphie?.addPoints(coords[rel(-1)], offset);
37740
36312
  }
37741
36313
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -37759,7 +36331,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
37759
36331
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
37760
36332
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37761
36333
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
37762
- const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36334
+ const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
37763
36335
  return this.graphie?.addPoints(coords[rel(-1)], offset);
37764
36336
  }
37765
36337
  // Snap to grid (already done)
@@ -38371,12 +36943,12 @@ class InteractiveGraph extends React__namespace.Component {
38371
36943
  }
38372
36944
  static getLinearEquationString(props) {
38373
36945
  const coords = InteractiveGraph.getLineCoords(props.graph, props);
38374
- if (eq(coords[0][0], coords[1][0])) {
36946
+ if (perseusCore.approximateEqual(coords[0][0], coords[1][0])) {
38375
36947
  return "x = " + coords[0][0].toFixed(3);
38376
36948
  }
38377
36949
  const m = (coords[1][1] - coords[0][1]) / (coords[1][0] - coords[0][0]);
38378
36950
  const b = coords[0][1] - m * coords[0][0];
38379
- if (eq(m, 0)) {
36951
+ if (perseusCore.approximateEqual(m, 0)) {
38380
36952
  return "y = " + b.toFixed(3);
38381
36953
  }
38382
36954
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
@@ -38518,7 +37090,7 @@ var InteractiveGraph$1 = {
38518
37090
  staticTransform: staticTransform$3,
38519
37091
  // TODO(LEMS-2656): remove TS suppression
38520
37092
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusInteractiveGraphUserInput'.
38521
- scorer: scoreInteractiveGraph
37093
+ scorer: perseusScore.scoreInteractiveGraph
38522
37094
  };
38523
37095
 
38524
37096
  // TODO: can we use WB typography?
@@ -38895,59 +37467,6 @@ const styles$a = aphrodite.StyleSheet.create({
38895
37467
  }
38896
37468
  });
38897
37469
 
38898
- // Question state for marker as result of user selected answers.
38899
-
38900
- function scoreMarker(marker) {
38901
- const score = {
38902
- hasAnswers: false,
38903
- isCorrect: false
38904
- };
38905
- if (marker.selected && marker.selected.length > 0) {
38906
- score.hasAnswers = true;
38907
- }
38908
- if (marker.answers.length > 0) {
38909
- if (marker.selected && marker.selected.length === marker.answers.length) {
38910
- // All correct answers are selected by the user.
38911
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38912
- }
38913
- } else if (!marker.selected || marker.selected.length === 0) {
38914
- // Correct as no answers should be selected by the user.
38915
- score.isCorrect = true;
38916
- }
38917
- return score;
38918
- }
38919
-
38920
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
38921
- function scoreLabelImage(userInput, rubric) {
38922
- let numAnswered = 0;
38923
- let numCorrect = 0;
38924
- for (const marker of userInput.markers) {
38925
- const score = scoreMarker(marker);
38926
- if (score.hasAnswers) {
38927
- numAnswered++;
38928
- }
38929
- if (score.isCorrect) {
38930
- numCorrect++;
38931
- }
38932
- }
38933
-
38934
- // We expect all question markers to be answered before grading.
38935
- if (numAnswered !== userInput.markers.length) {
38936
- return {
38937
- type: "invalid",
38938
- message: null
38939
- };
38940
- }
38941
- return {
38942
- type: "points",
38943
- // Markers with no expected answers are graded as correct if user
38944
- // makes no answer selection.
38945
- earned: numCorrect === userInput.markers.length ? 1 : 0,
38946
- total: 1,
38947
- message: null
38948
- };
38949
- }
38950
-
38951
37470
  /**
38952
37471
  * Represents a direction vector.
38953
37472
  * No diagonals allowed.
@@ -39171,7 +37690,7 @@ class LabelImage extends React__namespace.Component {
39171
37690
  onChange
39172
37691
  } = this.props;
39173
37692
  const updatedMarkers = markers.map(marker => {
39174
- const score = scoreMarker(marker);
37693
+ const score = perseusScore.scoreLabelImageMarker(marker);
39175
37694
  return {
39176
37695
  ...marker,
39177
37696
  // Reveal correctness state for markers with answers.
@@ -39320,7 +37839,7 @@ class LabelImage extends React__namespace.Component {
39320
37839
  bottom: "top"
39321
37840
  }[markerPosition];
39322
37841
  }
39323
- const score = scoreMarker(marker);
37842
+ const score = perseusScore.scoreLabelImageMarker(marker);
39324
37843
  // Once the question is answered, show markers
39325
37844
  // with correct answers, otherwise passthrough
39326
37845
  // the correctness state.
@@ -39528,7 +38047,7 @@ var LabelImage$1 = {
39528
38047
  isLintable: true,
39529
38048
  // TODO(LEMS-2656): remove TS suppression
39530
38049
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusLabelImageUserInput'.
39531
- scorer: scoreLabelImage
38050
+ scorer: perseusScore.scoreLabelImage
39532
38051
  };
39533
38052
 
39534
38053
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -40254,16 +38773,6 @@ const getPromptJSON$9 = (renderProps, userInput) => {
40254
38773
  };
40255
38774
  };
40256
38775
 
40257
- function scoreMatcher(state, rubric) {
40258
- const correct = ___default["default"].isEqual(state.left, rubric.left) && ___default["default"].isEqual(state.right, rubric.right);
40259
- return {
40260
- type: "points",
40261
- earned: correct ? 1 : 0,
40262
- total: 1,
40263
- message: null
40264
- };
40265
- }
40266
-
40267
38776
  const {
40268
38777
  shuffle: shuffle$1,
40269
38778
  seededRNG
@@ -40469,7 +38978,7 @@ var Matcher$1 = {
40469
38978
  isLintable: true,
40470
38979
  // TODO(LEMS-2656): remove TS suppression
40471
38980
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatcherUserInput'.
40472
- scorer: scoreMatcher
38981
+ scorer: perseusScore.scoreMatcher
40473
38982
  };
40474
38983
 
40475
38984
  const getPromptJSON$8 = (renderProps, userInput) => {
@@ -40485,78 +38994,6 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40485
38994
  };
40486
38995
  };
40487
38996
 
40488
- /**
40489
- * Checks user input from the matrix widget to see if it is scorable.
40490
- *
40491
- * Note: The matrix widget cannot do much validation without the Scoring
40492
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
40493
- *
40494
- * @see `scoreMatrix()` for more details.
40495
- */
40496
- function validateMatrix(userInput, validationData, strings) {
40497
- const supplied = userInput.answers;
40498
- const suppliedSize = getMatrixSize(supplied);
40499
- for (let row = 0; row < suppliedSize[0]; row++) {
40500
- for (let col = 0; col < suppliedSize[1]; col++) {
40501
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40502
- return {
40503
- type: "invalid",
40504
- message: strings.fillAllCells
40505
- };
40506
- }
40507
- }
40508
- }
40509
- return null;
40510
- }
40511
-
40512
- function scoreMatrix(userInput, rubric, strings) {
40513
- const validationResult = validateMatrix(userInput, rubric, strings);
40514
- if (validationResult != null) {
40515
- return validationResult;
40516
- }
40517
- const solution = rubric.answers;
40518
- const supplied = userInput.answers;
40519
- const solutionSize = getMatrixSize(solution);
40520
- const suppliedSize = getMatrixSize(supplied);
40521
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
40522
- const createValidator = perseusScore.KhanAnswerTypes.number.createValidatorFunctional;
40523
- let message = null;
40524
- let incorrect = false;
40525
- ___default["default"](suppliedSize[0]).times(row => {
40526
- ___default["default"](suppliedSize[1]).times(col => {
40527
- if (!incorrectSize) {
40528
- const validator = createValidator(
40529
- // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
40530
- solution[row][col], {
40531
- simplify: true
40532
- });
40533
- const result = validator(supplied[row][col]);
40534
- if (result.message) {
40535
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
40536
- message = result.message;
40537
- }
40538
- if (!result.correct) {
40539
- incorrect = true;
40540
- }
40541
- }
40542
- });
40543
- });
40544
- if (incorrectSize) {
40545
- return {
40546
- type: "points",
40547
- earned: 0,
40548
- total: 1,
40549
- message: null
40550
- };
40551
- }
40552
- return {
40553
- type: "points",
40554
- earned: incorrect ? 0 : 1,
40555
- total: 1,
40556
- message: message
40557
- };
40558
- }
40559
-
40560
38997
  const {
40561
38998
  assert: assert$2
40562
38999
  } = InteractiveUtil;
@@ -40605,29 +39042,6 @@ const getRefForPath$1 = function (path) {
40605
39042
  const column = getColumnFromPath$1(path);
40606
39043
  return "answer" + row + "," + column;
40607
39044
  };
40608
- function getMatrixSize(matrix) {
40609
- const matrixSize = [1, 1];
40610
-
40611
- // We need to find the widest row and tallest column to get the correct
40612
- // matrix size.
40613
- ___default["default"](matrix).each((matrixRow, row) => {
40614
- let rowWidth = 0;
40615
- ___default["default"](matrixRow).each((matrixCol, col) => {
40616
- if (matrixCol != null && matrixCol.toString().length) {
40617
- rowWidth = col + 1;
40618
- }
40619
- });
40620
-
40621
- // Matrix width:
40622
- matrixSize[1] = Math.max(matrixSize[1], rowWidth);
40623
-
40624
- // Matrix height:
40625
- if (rowWidth > 0) {
40626
- matrixSize[0] = Math.max(matrixSize[0], row + 1);
40627
- }
40628
- });
40629
- return matrixSize;
40630
- }
40631
39045
  class Matrix extends React__namespace.Component {
40632
39046
  static contextType = PerseusI18nContext;
40633
39047
  // @ts-expect-error - TS2564 - Property 'cursorPosition' has no initializer and is not definitely assigned in the constructor.
@@ -40798,7 +39212,7 @@ class Matrix extends React__namespace.Component {
40798
39212
  INPUT_HEIGHT,
40799
39213
  INPUT_WIDTH
40800
39214
  } = dimensions;
40801
- const matrixSize = getMatrixSize(this.props.answers);
39215
+ const matrixSize = perseusCore.getMatrixSize(this.props.answers);
40802
39216
  const maxRows = this.props.matrixBoardSize[0];
40803
39217
  const maxCols = this.props.matrixBoardSize[1];
40804
39218
  const cursorRow = this.props.cursorPosition[0];
@@ -40970,7 +39384,7 @@ var Matrix$1 = {
40970
39384
  isLintable: true,
40971
39385
  // TODO(LEMS-2656): remove TS suppression
40972
39386
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusMatrixUserInput'.
40973
- scorer: scoreMatrix
39387
+ scorer: perseusScore.scoreMatrix
40974
39388
  };
40975
39389
 
40976
39390
  const getPromptJSON$7 = () => {
@@ -42015,59 +40429,6 @@ const getPromptJSON$6 = (renderProps, userInput) => {
42015
40429
  };
42016
40430
  };
42017
40431
 
42018
- /**
42019
- * Checks user input is within the allowed range and not the same as the initial
42020
- * state.
42021
- * @param userInput
42022
- * @see 'scoreNumberLine' for the scoring logic.
42023
- */
42024
- function validateNumberLine(userInput) {
42025
- const divisionRange = userInput.divisionRange;
42026
- const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
42027
-
42028
- // TODO: I don't think isTickCrtl is a thing anymore
42029
- if (userInput.isTickCrtl && outsideAllowedRange) {
42030
- return {
42031
- type: "invalid",
42032
- message: "Number of divisions is outside the allowed range."
42033
- };
42034
- }
42035
- return null;
42036
- }
42037
-
42038
- function scoreNumberLine(userInput, scoringData) {
42039
- const validationError = validateNumberLine(userInput);
42040
- if (validationError) {
42041
- return validationError;
42042
- }
42043
- const range = scoringData.range;
42044
- const start = scoringData.initialX != null ? scoringData.initialX : range[0];
42045
- const startRel = scoringData.isInequality ? "ge" : "eq";
42046
- const correctRel = scoringData.correctRel || "eq";
42047
- const correctPos = kmath.number.equal(userInput.numLinePosition, scoringData.correctX || 0);
42048
- if (correctPos && correctRel === userInput.rel) {
42049
- return {
42050
- type: "points",
42051
- earned: 1,
42052
- total: 1,
42053
- message: null
42054
- };
42055
- }
42056
- if (userInput.numLinePosition === start && userInput.rel === startRel) {
42057
- // We're where we started.
42058
- return {
42059
- type: "invalid",
42060
- message: null
42061
- };
42062
- }
42063
- return {
42064
- type: "points",
42065
- earned: 0,
42066
- total: 1,
42067
- message: null
42068
- };
42069
- }
42070
-
42071
40432
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
42072
40433
  const MovablePoint = Graphie.MovablePoint;
42073
40434
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -42627,7 +40988,7 @@ var NumberLine$1 = {
42627
40988
  staticTransform: staticTransform$1,
42628
40989
  // TODO(LEMS-2656): remove TS suppression
42629
40990
  // @ts-expect-error: Type 'UserInput' is not assignable to type 'PerseusNumberLineUserInput'.
42630
- scorer: scoreNumberLine
40991
+ scorer: perseusScore.scoreNumberLine
42631
40992
  };
42632
40993
 
42633
40994
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -42642,36 +41003,6 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42642
41003
  };
42643
41004
  };
42644
41005
 
42645
- /**
42646
- * Checks user input from the orderer widget to see if the user has started
42647
- * ordering the options, making the widget scorable.
42648
- * @param userInput
42649
- * @see `scoreOrderer` for more details.
42650
- */
42651
- function validateOrderer(userInput) {
42652
- if (userInput.current.length === 0) {
42653
- return {
42654
- type: "invalid",
42655
- message: null
42656
- };
42657
- }
42658
- return null;
42659
- }
42660
-
42661
- function scoreOrderer(userInput, rubric) {
42662
- const validateError = validateOrderer(userInput);
42663
- if (validateError) {
42664
- return validateError;
42665
- }
42666
- const correct = ___default["default"].isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42667
- return {
42668
- type: "points",
42669
- earned: correct ? 1 : 0,
42670
- total: 1,
42671
- message: null
42672
- };
42673
- }
42674
-
42675
41006
  class PlaceholderCard extends React__namespace.Component {
42676
41007
  render() {
42677
41008
  return /*#__PURE__*/React__namespace.createElement("div", {
@@ -43211,7 +41542,7 @@ var Orderer$1 = {
43211
41542
  isLintable: true,
43212
41543
  // TODO(LEMS-2656): remove TS suppression
43213
41544
  // @ts-expect-error: Type UserInput is not assignable to type PerseusOrdererUserInput
43214
- scorer: scoreOrderer
41545
+ scorer: perseusScore.scoreOrderer
43215
41546
  };
43216
41547
 
43217
41548
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -43483,42 +41814,6 @@ const getPromptJSON$3 = () => {
43483
41814
  return getUnsupportedPromptJSON("plotter");
43484
41815
  };
43485
41816
 
43486
- const {
43487
- deepEq: deepEq$2
43488
- } = Util;
43489
-
43490
- /**
43491
- * Checks user input to confirm it is not the same as the starting values for the graph.
43492
- * This means the user has modified the graph, and the question can be scored.
43493
- *
43494
- * @see 'scorePlotter' for more details on scoring.
43495
- */
43496
- function validatePlotter(userInput, validationData) {
43497
- if (deepEq$2(userInput, validationData.starting)) {
43498
- return {
43499
- type: "invalid",
43500
- message: null
43501
- };
43502
- }
43503
- return null;
43504
- }
43505
-
43506
- const {
43507
- deepEq: deepEq$1
43508
- } = Util;
43509
- function scorePlotter(userInput, scoringData) {
43510
- const validationError = validatePlotter(userInput, scoringData);
43511
- if (validationError) {
43512
- return validationError;
43513
- }
43514
- return {
43515
- type: "points",
43516
- earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43517
- total: 1,
43518
- message: null
43519
- };
43520
- }
43521
-
43522
41817
  /* eslint-disable react/no-unsafe */
43523
41818
  class Plotter extends React__namespace.Component {
43524
41819
  static contextType = PerseusI18nContext;
@@ -44323,7 +42618,7 @@ var Plotter$1 = {
44323
42618
  staticTransform: staticTransform,
44324
42619
  // TODO(LEMS-2656): remove TS suppression
44325
42620
  // @ts-expect-error: Type UserInput is not assignable to type PerseusPlotterUserInput
44326
- scorer: scorePlotter
42621
+ scorer: perseusScore.scorePlotter
44327
42622
  };
44328
42623
 
44329
42624
  const getPromptJSON$2 = () => {
@@ -44400,43 +42695,6 @@ const getPromptJSON$1 = userInput => {
44400
42695
  };
44401
42696
  };
44402
42697
 
44403
- /**
44404
- * Checks user input for the sorter widget to ensure that the user has made
44405
- * changes before attempting to score the widget.
44406
- * @param userInput
44407
- * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
44408
- * for more details on how the sorter widget is scored.
44409
- */
44410
- function validateSorter(userInput) {
44411
- // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44412
- // prevents the "Check" button from becoming active. We want the user
44413
- // to make a change before trying to move forward. This makes an
44414
- // assumption that the initial order isn't the correct order! However,
44415
- // this should be rare if it happens, and interacting with the list
44416
- // will enable the button, so they won't be locked out of progressing.
44417
- if (!userInput.changed) {
44418
- return {
44419
- type: "invalid",
44420
- message: null
44421
- };
44422
- }
44423
- return null;
44424
- }
44425
-
44426
- function scoreSorter(userInput, rubric) {
44427
- const validationError = validateSorter(userInput);
44428
- if (validationError) {
44429
- return validationError;
44430
- }
44431
- const correct = Util.deepEq(userInput.options, rubric.correct);
44432
- return {
44433
- type: "points",
44434
- earned: correct ? 1 : 0,
44435
- total: 1,
44436
- message: null
44437
- };
44438
- }
44439
-
44440
42698
  const {
44441
42699
  shuffle
44442
42700
  } = Util;
@@ -44523,84 +42781,9 @@ var Sorter$1 = {
44523
42781
  isLintable: true,
44524
42782
  // TODO(LEMS-2656): remove TS suppression
44525
42783
  // @ts-expect-error: Type UserInput is not assignable to type PerseusSorterUserInput
44526
- scorer: scoreSorter
44527
- };
44528
-
44529
- /**
44530
- * Filters the given table (modelled as a 2D array) to remove any rows that are
44531
- * completely empty.
44532
- *
44533
- * @returns A new table with only non-empty rows.
44534
- */
44535
- const filterNonEmpty = function (table) {
44536
- return table.filter(function (row) {
44537
- // Return only rows that are non-empty.
44538
- return row.some(cell => cell);
44539
- });
42784
+ scorer: perseusScore.scoreSorter
44540
42785
  };
44541
42786
 
44542
- function validateTable(userInput) {
44543
- const supplied = filterNonEmpty(userInput);
44544
- const hasEmptyCell = supplied.some(function (row) {
44545
- return row.some(function (cell) {
44546
- return cell === "";
44547
- });
44548
- });
44549
- if (hasEmptyCell || !supplied.length) {
44550
- return {
44551
- type: "invalid",
44552
- message: null
44553
- };
44554
- }
44555
- return null;
44556
- }
44557
-
44558
- function scoreTable(userInput, rubric, strings) {
44559
- const validationResult = validateTable(userInput);
44560
- if (validationResult != null) {
44561
- return validationResult;
44562
- }
44563
- const supplied = filterNonEmpty(userInput);
44564
- const solution = filterNonEmpty(rubric.answers);
44565
- if (supplied.length !== solution.length) {
44566
- return {
44567
- type: "points",
44568
- earned: 0,
44569
- total: 1,
44570
- message: null
44571
- };
44572
- }
44573
- const createValidator = perseusScore.KhanAnswerTypes.number.createValidatorFunctional;
44574
- let message = null;
44575
- const allCorrect = solution.every(function (rowSolution) {
44576
- for (let i = 0; i < supplied.length; i++) {
44577
- const rowSupplied = supplied[i];
44578
- const correct = rowSupplied.every(function (cellSupplied, i) {
44579
- const cellSolution = rowSolution[i];
44580
- const validator = createValidator(cellSolution, {
44581
- simplify: true
44582
- });
44583
- const result = validator(cellSupplied);
44584
- if (result.message) {
44585
- message = result.message;
44586
- }
44587
- return result.correct;
44588
- });
44589
- if (correct) {
44590
- supplied.splice(i, 1);
44591
- return true;
44592
- }
44593
- }
44594
- return false;
44595
- });
44596
- return {
44597
- type: "points",
44598
- earned: allCorrect ? 1 : 0,
44599
- total: 1,
44600
- message
44601
- };
44602
- }
44603
-
44604
42787
  const {
44605
42788
  assert
44606
42789
  } = InteractiveUtil;
@@ -44817,7 +43000,7 @@ var Table$1 = {
44817
43000
  isLintable: true,
44818
43001
  // TODO(LEMS-2656): remove TS suppression
44819
43002
  // @ts-expect-error: Type UserInput is not assignable to type PerseusTableUserInput
44820
- scorer: scoreTable
43003
+ scorer: perseusScore.scoreTable
44821
43004
  };
44822
43005
 
44823
43006
  const getPromptJSON = () => {
@@ -45002,7 +43185,7 @@ const init = function () {
45002
43185
 
45003
43186
  // This file is processed by a Rollup plugin (replace) to inject the production
45004
43187
  const libName = "@khanacademy/perseus";
45005
- const libVersion = "50.1.0";
43188
+ const libVersion = "51.0.0";
45006
43189
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45007
43190
 
45008
43191
  const apiVersion = {
@@ -46971,7 +45154,6 @@ const getValidWidgetIds = perseusItem => {
46971
45154
  return wonderStuffCore.keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
46972
45155
  };
46973
45156
 
46974
- const deepEq = Util.deepEq;
46975
45157
  function isRealJSONParse(jsonParse) {
46976
45158
  const randomPhrase = buildRandomPhrase();
46977
45159
  const randomHintPhrase = buildRandomPhrase();
@@ -47028,7 +45210,7 @@ function isRealJSONParse(jsonParse) {
47028
45210
  const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
47029
45211
  const parsedTestJSON = jsonParse(testJSON);
47030
45212
  const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
47031
- return deepEq(parsedTestItemData, testingObject);
45213
+ return perseusCore.approximateDeepEqual(parsedTestItemData, testingObject);
47032
45214
  }
47033
45215
  function buildRandomString() {
47034
45216
  let capitalize = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
@@ -47235,7 +45417,9 @@ function object(schema) {
47235
45417
  for (const [prop, propParser] of Object.entries(schema)) {
47236
45418
  const result = propParser(rawValue[prop], ctx.forSubtree(prop));
47237
45419
  if (isSuccess(result)) {
47238
- ret[prop] = result.value;
45420
+ if (result.value !== undefined || prop in rawValue) {
45421
+ ret[prop] = result.value;
45422
+ }
47239
45423
  } else {
47240
45424
  mismatches.push(...result.detail);
47241
45425
  }
@@ -48854,7 +47038,6 @@ const EditorJsonify = {
48854
47038
  * Main entry point
48855
47039
  */
48856
47040
  const GrapherUtil = {
48857
- allTypes,
48858
47041
  DEFAULT_GRAPHER_PROPS,
48859
47042
  chooseType,
48860
47043
  defaultPlotProps,
@@ -48922,7 +47105,6 @@ exports.getAngleCoords = getAngleCoords;
48922
47105
  exports.getAnswerFromUserInput = getAnswerFromUserInput;
48923
47106
  exports.getAnswersFromWidgets = getAnswersFromWidgets;
48924
47107
  exports.getCircleCoords = getCircleCoords;
48925
- exports.getClockwiseAngle = getClockwiseAngle;
48926
47108
  exports.getCorrectAnswerForWidgetId = getCorrectAnswerForWidgetId;
48927
47109
  exports.getImagesWithoutAltData = getImagesWithoutAltData;
48928
47110
  exports.getInteractiveBoxFromSizeClass = getInteractiveBoxFromSizeClass;