@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/es/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
+ import { PerseusError, Errors, getDecimalSeparator, approximateDeepEqual, pluck, mapObject, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getMatrixSize, addLibraryVersionToPerseusDebug, lockedFigureColorNames, plotterPlotTypes, ItemExtras } from '@khanacademy/perseus-core';
3
4
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
5
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
6
+ import { scoreExpression, KhanAnswerTypes, scoreInputNumber, inputNumberAnswerTypes, scoreNumericInput, scoreRadio, scoreCategorizer, scoreCSProgram, scoreDropdown, scoreGrapher, scoreIframe, scoreInteractiveGraph, scoreLabelImage, scoreLabelImageMarker, scoreMatcher, scoreMatrix, scoreNumberLine, scoreOrderer, scorePlotter, scoreSorter, scoreTable } from '@khanacademy/perseus-score';
5
7
  import { View, RenderStateRoot, Id, useOnMountEffect, useLatestRef, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
6
8
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
7
9
  import { HeadingMedium, LabelSmall, LabelLarge, Body, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -12,17 +14,15 @@ import React__default, { useContext, useState, useEffect, useRef, createContext,
12
14
  import * as ReactDOM from 'react-dom';
13
15
  import ReactDOM__default from 'react-dom';
14
16
  import _, { debounce as debounce$1 } from 'underscore';
15
- import { ErrorCodes, KhanAnswerTypes } from '@khanacademy/perseus-score';
16
17
  import Clickable from '@khanacademy/wonder-blocks-clickable';
17
18
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
18
19
  import { color, spacing, font } from '@khanacademy/wonder-blocks-tokens';
19
20
  import $ from 'jquery';
20
21
  import { v4 } from 'uuid';
21
- import { PerseusError, Errors, pluck, mapObject, lockedFigureColors, lockedFigureFillStyles, addLibraryVersionToPerseusDebug, lockedFigureColorNames, plotterPlotTypes, ItemExtras } from '@khanacademy/perseus-core';
22
22
  import PropTypes from 'prop-types';
23
23
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
24
24
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
25
- import { point, KhanMath, number as number$1, vector as vector$1, sum as sum$1, line } from '@khanacademy/kmath';
25
+ import { point, KhanMath, number as number$1, vector as vector$3, line, geometry, angles, coefficients } from '@khanacademy/kmath';
26
26
  import SimpleMarkdown from '@khanacademy/simple-markdown';
27
27
  import { pureMarkdownRules } from '@khanacademy/pure-markdown';
28
28
  import { TextField } from '@khanacademy/wonder-blocks-form';
@@ -33,7 +33,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
33
33
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
34
34
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
35
35
  import debounce$2 from 'lodash.debounce';
36
- import { vec, Coordinates, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Polyline, Mafs } from 'mafs';
36
+ import { vec, Coordinates, Ellipse, Plot as Plot$2, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Polyline, Mafs } from 'mafs';
37
37
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
38
38
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
39
39
  import { useDrag } from '@use-gesture/react';
@@ -231,7 +231,15 @@ const strings = {
231
231
  srAngleSideAtCoordinates: "Point %(point)s, %(side)s at %(x)s comma %(y)s",
232
232
  srAngleVertexAtCoordinatesWithAngleMeasure: "Point 2, vertex at %(x)s comma %(y)s. Angle %(angleMeasure)s degrees",
233
233
  srAngleGraphAriaLabel: "An angle on a coordinate plane.",
234
- 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"
234
+ 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",
235
+ srLinearSystemGraph: "Two lines on a coordinate plane.",
236
+ srLinearSystemPoints: "Line %(lineNumber)s has two points, point 1 at %(point1X)s comma %(point1Y)s and point 2 at %(point2X)s comma %(point2Y)s.",
237
+ srLinearSystemPoint: "Point %(pointSequence)s on line %(lineNumber)s at %(x)s comma %(y)s.",
238
+ srRayGraph: "A ray on a coordinate plane.",
239
+ srRayPoints: "The endpoint is at %(point1X)s comma %(point1Y)s and the ray goes through point %(point2X)s comma %(point2Y)s.",
240
+ srRayGrabHandle: "Ray with endpoint %(point1X)s comma %(point1Y)s going through point %(point2X)s comma %(point2Y)s.",
241
+ srRayEndpoint: "Endpoint at %(point1X)s comma %(point1Y)s.",
242
+ srRayTerminalPoint: "Through point at %(point2X)s comma %(point2Y)s."
235
243
  // The above strings are used for interactive graph SR descriptions.
236
244
  };
237
245
 
@@ -500,18 +508,59 @@ const mockStrings = {
500
508
  startingSideY,
501
509
  endingSideX,
502
510
  endingSideY
503
- }) => `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}.`
511
+ }) => `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}.`,
512
+ srLinearSystemGraph: "Two lines on a coordinate plane.",
513
+ srLinearSystemPoints: ({
514
+ lineNumber,
515
+ point1X,
516
+ point1Y,
517
+ point2X,
518
+ point2Y
519
+ }) => `Line ${lineNumber} has two points, point 1 at ${point1X} comma ${point1Y} and point 2 at ${point2X} comma ${point2Y}.`,
520
+ srLinearSystemPoint: ({
521
+ lineNumber,
522
+ pointSequence,
523
+ x,
524
+ y
525
+ }) => `Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`,
526
+ srRayGraph: "A ray on a coordinate plane.",
527
+ srRayPoints: ({
528
+ point1X,
529
+ point1Y,
530
+ point2X,
531
+ point2Y
532
+ }) => `The endpoint is at ${point1X} comma ${point1Y} and the ray goes through point ${point2X} comma ${point2Y}.`,
533
+ srRayGrabHandle: ({
534
+ point1X,
535
+ point1Y,
536
+ point2X,
537
+ point2Y
538
+ }) => `Ray with endpoint ${point1X} comma ${point1Y} going through point ${point2X} comma ${point2Y}.`,
539
+ srRayEndpoint: ({
540
+ x,
541
+ y
542
+ }) => `Endpoint at ${x} comma ${y}.`,
543
+ srRayTerminalPoint: ({
544
+ x,
545
+ y
546
+ }) => `Through point at ${x} comma ${y}.`
504
547
  // The above strings are used for interactive graph SR descriptions.
505
548
  };
506
549
 
550
+ // This type helps us make sure all error codes are mapped to strings
551
+
507
552
  const errorToString = {
508
- [ErrorCodes.MISSING_PERCENT_ERROR]: strings.MISSING_PERCENT_ERROR,
509
- [ErrorCodes.NEEDS_TO_BE_SIMPLIFIED_ERROR]: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
510
- [ErrorCodes.APPROXIMATED_PI_ERROR]: strings.APPROXIMATED_PI_ERROR,
511
- [ErrorCodes.EXTRA_SYMBOLS_ERROR]: strings.EXTRA_SYMBOLS_ERROR,
512
- [ErrorCodes.WRONG_CASE_ERROR]: strings.WRONG_CASE_ERROR,
513
- [ErrorCodes.WRONG_LETTER_ERROR]: strings.WRONG_LETTER_ERROR,
514
- [ErrorCodes.MULTIPLICATION_SIGN_ERROR]: strings.MULTIPLICATION_SIGN_ERROR
553
+ MISSING_PERCENT_ERROR: strings.MISSING_PERCENT_ERROR,
554
+ NEEDS_TO_BE_SIMPLIFIED_ERROR: strings.NEEDS_TO_BE_SIMPLFIED_ERROR,
555
+ APPROXIMATED_PI_ERROR: strings.APPROXIMATED_PI_ERROR,
556
+ EXTRA_SYMBOLS_ERROR: strings.EXTRA_SYMBOLS_ERROR,
557
+ WRONG_CASE_ERROR: strings.WRONG_CASE_ERROR,
558
+ WRONG_LETTER_ERROR: strings.WRONG_LETTER_ERROR,
559
+ MULTIPLICATION_SIGN_ERROR: strings.MULTIPLICATION_SIGN_ERROR,
560
+ INVALID_SELECTION_ERROR: strings.invalidSelection,
561
+ CHOOSE_CORRECT_NUM_ERROR: strings.chooseCorrectNum,
562
+ NOT_NONE_ABOVE_ERROR: strings.notNoneOfTheAbove,
563
+ FILL_ALL_CELLS_ERROR: strings.fillAllCells
515
564
  };
516
565
  function mapErrorToString(err) {
517
566
  if (!err) {
@@ -1455,205 +1504,6 @@ const getPromptJSON$t = (renderProps, userInput) => {
1455
1504
  };
1456
1505
  };
1457
1506
 
1458
- /**
1459
- * Get the character used for separating decimals.
1460
- */
1461
- const getDecimalSeparator = locale => {
1462
- var _match$;
1463
- switch (locale) {
1464
- // TODO(somewhatabstract): Remove this when Chrome supports the `ka`
1465
- // locale properly.
1466
- // https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
1467
- //
1468
- // Supported locales in Chrome:
1469
- // https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
1470
- case "ka":
1471
- return ",";
1472
- default:
1473
- const numberWithDecimalSeparator = 1.1;
1474
- // TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
1475
- // support Safari 12.
1476
- const match = new Intl.NumberFormat(locale).format(numberWithDecimalSeparator)
1477
- // 0x661 is ARABIC-INDIC DIGIT ONE
1478
- // 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
1479
- .match(/[^\d\u0661\u06F1]/);
1480
- return (_match$ = match == null ? void 0 : match[0]) != null ? _match$ : ".";
1481
- }
1482
- };
1483
-
1484
- // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
1485
- // getDependencies() internally... simplify usage to just using a "singleton"
1486
- // which get's the injected logger.
1487
- /**
1488
- * Provides logging infrastructure for Perseus
1489
- */
1490
- const Log = {
1491
- // Logs a message
1492
- log: (message, extra) => {
1493
- getDependencies().Log.log(message, extra);
1494
- },
1495
- // Logs an error with a message
1496
- error: (message, kind, extra) => {
1497
- getDependencies().Log.error(message, kind, extra);
1498
- }
1499
- };
1500
-
1501
- /**
1502
- * Checks user input from the expression widget to see if it is scorable.
1503
- *
1504
- * Note: Most of the expression widget's validation requires the Rubric because
1505
- * of its use of KhanAnswerTypes as a core part of scoring.
1506
- *
1507
- * @see `scoreExpression()` for more details.
1508
- */
1509
- function validateExpression(userInput) {
1510
- if (userInput === "") {
1511
- return {
1512
- type: "invalid",
1513
- message: null
1514
- };
1515
- }
1516
- return null;
1517
- }
1518
-
1519
- /* Content creators input a list of answers which are matched from top to
1520
- * bottom. The intent is that they can include spcific solutions which should
1521
- * be graded as correct or incorrect (or ungraded!) first, then get more
1522
- * general.
1523
- *
1524
- * We iterate through each answer, trying to match it with the user's input
1525
- * using the following angorithm:
1526
- * - Try to parse the user's input. If it doesn't parse then return "not
1527
- * graded".
1528
- * - For each answer:
1529
- * ~ Try to validate the user's input against the answer. The answer is
1530
- * expected to parse.
1531
- * ~ If the user's input validates (the validator judges it "correct"), we've
1532
- * matched and can stop considering answers.
1533
- * - If there were no matches or the matching answer is considered "ungraded",
1534
- * show the user an error. TODO(joel) - what error?
1535
- * - Otherwise, pass through the resulting points and message.
1536
- */
1537
- function scoreExpression(userInput, rubric, strings, locale) {
1538
- const validationError = validateExpression(userInput);
1539
- if (validationError) {
1540
- return validationError;
1541
- }
1542
- const options = _.clone(rubric);
1543
- _.extend(options, {
1544
- decimal_separator: getDecimalSeparator(locale)
1545
- });
1546
- const createValidator = answer => {
1547
- // We give options to KAS.parse here because it is parsing the
1548
- // solution answer, not the student answer, and we don't want a
1549
- // solution to work if the student is using a different language
1550
- // (different from the content creation language, ie. English).
1551
- const expression = KAS.parse(answer.value, rubric);
1552
- // An answer may not be parsed if the expression was defined
1553
- // incorrectly. For example if the answer is using a symbol defined
1554
- // in the function variables list for the expression.
1555
- if (!expression.parsed) {
1556
- /* c8 ignore next */
1557
- Log.error("Unable to parse solution answer for expression", Errors.InvalidInput, {
1558
- loggedMetadata: {
1559
- rubric: JSON.stringify(rubric)
1560
- }
1561
- });
1562
- return null;
1563
- }
1564
- return KhanAnswerTypes.expression.createValidatorFunctional(expression.expr, _({}).extend(options, {
1565
- simplify: answer.simplify,
1566
- form: answer.form
1567
- }));
1568
- };
1569
-
1570
- // Find the first answer form that matches the user's input and that
1571
- // is considered correct. Also, track whether the input is
1572
- // considered "empty" for all answer forms, and keep the validation
1573
- // result for the first answer form for which the user's input was
1574
- // considered "ungraded".
1575
- // (Terminology reminder: the answer forms are provided by the
1576
- // assessment items; they are not the user's input. Each one might
1577
- // represent a correct answer, an incorrect one (if the exercise
1578
- // creator has predicted certain common wrong answers and wants to
1579
- // provide guidance via a message), or an ungraded one (same idea,
1580
- // but without giving the user an incorrect mark for the question).
1581
- let matchingAnswerForm;
1582
- let matchMessage;
1583
- let allEmpty = true;
1584
- let firstUngradedResult;
1585
- for (const answerForm of rubric.answerForms || []) {
1586
- const validator = createValidator(answerForm);
1587
- if (!validator) {
1588
- continue;
1589
- }
1590
- const result = validator(userInput);
1591
-
1592
- // Short-circuit as soon as the user's input matches some answer
1593
- // (independently of whether the answer is correct)
1594
- if (result.correct) {
1595
- matchingAnswerForm = answerForm;
1596
- matchMessage = result.message || "";
1597
- break;
1598
- }
1599
- allEmpty = allEmpty && result.empty;
1600
- // If this answer form is correct and the user's input is considered
1601
- // "ungraded" for it, we'll want to keep the evaluation result for
1602
- // later. If the user's input doesn't match any answer forms, we'll
1603
- // show the message from this validation.
1604
- if (answerForm.considered === "correct" && result.ungraded && !firstUngradedResult) {
1605
- firstUngradedResult = result;
1606
- }
1607
- }
1608
-
1609
- // Now check to see if we matched any answer form at all, and if
1610
- // we did, whether it's considered correct, incorrect, or ungraded
1611
- if (!matchingAnswerForm) {
1612
- if (firstUngradedResult) {
1613
- // While we didn't directly match with any answer form, we
1614
- // did at some point get an "ungraded" validation result,
1615
- // which might indicate e.g. a mismatch in variable casing.
1616
- // We'll return "invalid", which will let the user try again
1617
- // with no penalty, and the hopefully helpful validation
1618
- // message.
1619
- return {
1620
- type: "invalid",
1621
- message: firstUngradedResult.message,
1622
- suppressAlmostThere: firstUngradedResult.suppressAlmostThere
1623
- };
1624
- }
1625
- if (allEmpty) {
1626
- // If everything graded as empty, it's invalid.
1627
- return {
1628
- type: "invalid",
1629
- message: null
1630
- };
1631
- }
1632
- // We fell through all the possibilities and we're not empty,
1633
- // so the answer is considered incorrect.
1634
- return {
1635
- type: "points",
1636
- earned: 0,
1637
- total: 1
1638
- };
1639
- }
1640
- if (matchingAnswerForm.considered === "ungraded") {
1641
- return {
1642
- type: "invalid",
1643
- message: matchMessage
1644
- };
1645
- }
1646
- // We matched a graded answer form, so we can now tell the user
1647
- // whether their input was correct or incorrect, and hand out
1648
- // points accordingly
1649
- return {
1650
- type: "points",
1651
- earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
1652
- total: 1,
1653
- message: matchMessage
1654
- };
1655
- }
1656
-
1657
1507
  // Map of international operator names to their English equivalents
1658
1508
  const englishOperators = {
1659
1509
  arctg: "arctan",
@@ -2414,56 +2264,6 @@ function constrainedTickStepsFromTickSteps(tickSteps, ranges) {
2414
2264
  return [constrainTickStep(tickSteps[0], ranges[0]), constrainTickStep(tickSteps[1], ranges[1])];
2415
2265
  }
2416
2266
 
2417
- /**
2418
- * Approximate equality on numbers and primitives.
2419
- */
2420
- function eq$4(x, y) {
2421
- if (typeof x === "number" && typeof y === "number") {
2422
- return Math.abs(x - y) < 1e-9;
2423
- }
2424
- return x === y;
2425
- }
2426
-
2427
- /**
2428
- * Deep approximate equality on primitives, numbers, arrays, and objects.
2429
- * Recursive.
2430
- */
2431
- function deepEq$7(x, y) {
2432
- if (Array.isArray(x) && Array.isArray(y)) {
2433
- if (x.length !== y.length) {
2434
- return false;
2435
- }
2436
- for (let i = 0; i < x.length; i++) {
2437
- if (!deepEq$7(x[i], y[i])) {
2438
- return false;
2439
- }
2440
- }
2441
- return true;
2442
- }
2443
- if (Array.isArray(x) || Array.isArray(y)) {
2444
- return false;
2445
- }
2446
- if (typeof x === "function" && typeof y === "function") {
2447
- return eq$4(x, y);
2448
- }
2449
- if (typeof x === "function" || typeof y === "function") {
2450
- return false;
2451
- }
2452
- if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
2453
- return x === y || _.all(x, function (v, k) {
2454
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2455
- return deepEq$7(y[k], v);
2456
- }) && _.all(y, function (v, k) {
2457
- // @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
2458
- return deepEq$7(x[k], v);
2459
- });
2460
- }
2461
- if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
2462
- return false;
2463
- }
2464
- return eq$4(x, y);
2465
- }
2466
-
2467
2267
  /**
2468
2268
  * Query String Parser
2469
2269
  *
@@ -2680,15 +2480,6 @@ const textarea = {
2680
2480
  */
2681
2481
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
2682
2482
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
2683
-
2684
- // TODO(benchristel): in the future, we may want to make deepClone work for
2685
- // Record<string, Cloneable> as well. Currently, it only does arrays.
2686
- function deepClone(obj) {
2687
- if (Array.isArray(obj)) {
2688
- return obj.map(deepClone);
2689
- }
2690
- return obj;
2691
- }
2692
2483
  const Util = {
2693
2484
  inputPathsEqual,
2694
2485
  nestedMap: nestedMap$2,
@@ -2709,8 +2500,6 @@ const Util = {
2709
2500
  gridStepFromTickStep,
2710
2501
  tickStepFromNumTicks,
2711
2502
  constrainedTickStepsFromTickSteps,
2712
- eq: eq$4,
2713
- deepEq: deepEq$7,
2714
2503
  parseQueryString,
2715
2504
  updateQueryString: updateQueryString$2,
2716
2505
  strongEncodeURIComponent,
@@ -2728,8 +2517,24 @@ const Util = {
2728
2517
  getDataUrl: getDataUrl,
2729
2518
  textarea,
2730
2519
  unescapeMathMode: unescapeMathMode$1,
2731
- random: random$1,
2732
- deepClone
2520
+ random: random$1
2521
+ };
2522
+
2523
+ // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
2524
+ // getDependencies() internally... simplify usage to just using a "singleton"
2525
+ // which get's the injected logger.
2526
+ /**
2527
+ * Provides logging infrastructure for Perseus
2528
+ */
2529
+ const Log = {
2530
+ // Logs a message
2531
+ log: (message, extra) => {
2532
+ getDependencies().Log.log(message, extra);
2533
+ },
2534
+ // Logs an error with a message
2535
+ error: (message, kind, extra) => {
2536
+ getDependencies().Log.error(message, kind, extra);
2537
+ }
2733
2538
  };
2734
2539
 
2735
2540
  // For offline exercises in the mobile app, we download the graphie data
@@ -8277,7 +8082,7 @@ var Tex$1 = {
8277
8082
  const {
8278
8083
  processMath: processMath$1
8279
8084
  } = Tex$1;
8280
- function polar$1(r, th) {
8085
+ function polar$2(r, th) {
8281
8086
  if (typeof r === "number") {
8282
8087
  r = [r, r];
8283
8088
  }
@@ -8436,7 +8241,7 @@ class Graphie$1 {
8436
8241
  // See: http://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js
8437
8242
  const xCoords = [x0, x0 * 2 / 3 + x1 * 1 / 3, x0 * 1 / 3 + x1 * 2 / 3, x1];
8438
8243
  const yCoords = [computeSine(x0), computeSine(x0) + computeDerivative(x0) * (x1 - x0) / 3, computeSine(x1) - computeDerivative(x1) * (x1 - x0) / 3, computeSine(x1)];
8439
- const points = vector$1.zip(xCoords, yCoords);
8244
+ const points = vector$3.zip(xCoords, yCoords);
8440
8245
  return points.map(this.scalePoint);
8441
8246
  };
8442
8247
 
@@ -8769,8 +8574,8 @@ class Graphie$1 {
8769
8574
  endAngle = (endAngle % 360 + 360) % 360;
8770
8575
  const cent = this.scalePoint(center);
8771
8576
  const radii = this.scaleVector(radius);
8772
- const startVector = polar$1(radius, startAngle);
8773
- const endVector = polar$1(radius, endAngle);
8577
+ const startVector = polar$2(radius, startAngle);
8578
+ const endVector = polar$2(radius, endAngle);
8774
8579
 
8775
8580
  // We round the coordinates to make testing easier, because trig
8776
8581
  // operations return unround numbers.
@@ -8875,7 +8680,7 @@ class Graphie$1 {
8875
8680
  // Apply padding and offset to points to convert from
8876
8681
  // canvas coordinates to pixel coordinates relative to bounding box
8877
8682
  points = points.map(function (point) {
8878
- return vector$1.add(vector$1.subtract(point, topLeftOfBoundingBox), vector$1.scale(padding, 0.5));
8683
+ return vector$3.add(vector$3.subtract(point, topLeftOfBoundingBox), vector$3.scale(padding, 0.5));
8879
8684
  });
8880
8685
 
8881
8686
  // Calculate <div> dimensions
@@ -8939,8 +8744,8 @@ class Graphie$1 {
8939
8744
  const extraOffset = [Math.min(start[0], end[0]), Math.min(start[1], end[1])];
8940
8745
 
8941
8746
  // Apply padding and offset to start, end points
8942
- start = vector$1.add(vector$1.subtract(start, extraOffset), vector$1.scale(padding, 0.5));
8943
- end = vector$1.add(vector$1.subtract(end, extraOffset), vector$1.scale(padding, 0.5));
8747
+ start = vector$3.add(vector$3.subtract(start, extraOffset), vector$3.scale(padding, 0.5));
8748
+ end = vector$3.add(vector$3.subtract(end, extraOffset), vector$3.scale(padding, 0.5));
8944
8749
 
8945
8750
  // Calculate <div> dimensions
8946
8751
  const left = extraOffset[0] - padding[0] / 2;
@@ -9896,10 +9701,10 @@ const getClipPoint = function getClipPoint(graph, coord, angle) {
9896
9701
  // shoot a point off into the distance ...
9897
9702
  const distance = xExtent + yExtent;
9898
9703
  // we need to scale the point according to the scale of the axes
9899
- const angleVec = graph.unscaleVector(vector$1.cartFromPolarDeg(1, angle));
9900
- const distVec = vector$1.scale(vector$1.normalize(angleVec), distance);
9901
- const farCoord = vector$1.add(coord, distVec);
9902
- const scaledAngle = vector$1.polarDegFromCart(angleVec)[1];
9704
+ const angleVec = graph.unscaleVector(vector$3.cartFromPolarDeg(1, angle));
9705
+ const distVec = vector$3.scale(vector$3.normalize(angleVec), distance);
9706
+ const farCoord = vector$3.add(coord, distVec);
9707
+ const scaledAngle = vector$3.polarDegFromCart(angleVec)[1];
9903
9708
  // ... and then bring it back
9904
9709
  const clipPoint = graph.constrainToBoundsOnAngle(farCoord, 4, scaledAngle * Math.PI / 180);
9905
9710
  return clipPoint;
@@ -9950,7 +9755,7 @@ class WrappedDrawing {
9950
9755
  return this.visibleShape[0];
9951
9756
  }
9952
9757
  moveTo(point) {
9953
- const delta = vector$1.subtract(this.graphie.scalePoint(point), this.graphie.scalePoint(this.initialPoint));
9758
+ const delta = vector$3.subtract(this.graphie.scalePoint(point), this.graphie.scalePoint(this.initialPoint));
9954
9759
  const do3dTransform = InteractiveUtil.getCanUse3dTransform();
9955
9760
  const transformation = "translateX(" + delta[0] + "px) " + "translateY(" + delta[1] + "px)" + (do3dTransform ? " translateZ(0)" : "");
9956
9761
  this.transform(transformation);
@@ -10005,9 +9810,9 @@ class Arrowhead$1 extends WrappedPath {
10005
9810
  const center = [0.75, 0];
10006
9811
  const points = [[-3, 4], [-2.75, 2.5], [0, 0.25], center, [0, -0.25], [-2.75, -2.5], [-3, -4]].map(point => {
10007
9812
  // Scale points by `Arrowhead.scale` around `center`
10008
- const pv = vector$1.subtract(point, center);
10009
- const pvScaled = vector$1.scale(pv, Arrowhead$1.scale);
10010
- return vector$1.add(center, pvScaled);
9813
+ const pv = vector$3.subtract(point, center);
9814
+ const pvScaled = vector$3.scale(pv, Arrowhead$1.scale);
9815
+ return vector$3.add(center, pvScaled);
10011
9816
  });
10012
9817
  const unscaledPoints = points.map(graphie.unscalePoint);
10013
9818
  super(graphie, unscaledPoints, {
@@ -10078,7 +9883,7 @@ class WrappedEllipse extends WrappedDrawing {
10078
9883
  const wrapper = this.wrapper;
10079
9884
  wrapper.style.filter = filter;
10080
9885
  this.moveTo = function (point) {
10081
- const delta = vector$1.subtract(this.graphie.scalePoint(point), this.graphie.scalePoint(this.initialPoint));
9886
+ const delta = vector$3.subtract(this.graphie.scalePoint(point), this.graphie.scalePoint(this.initialPoint));
10082
9887
  const do3dTransform = InteractiveUtil.getCanUse3dTransform();
10083
9888
  const transform = "translateX(" + Math.round(delta[0]) + "px) " + "translateY(" + Math.round(delta[1]) + "px)" + (do3dTransform ? " translateZ(0)" : "");
10084
9889
  this.transform(transform);
@@ -10147,10 +9952,10 @@ class WrappedLine extends WrappedDrawing {
10147
9952
  const scaledEnd = this.graphie.scalePoint(end);
10148
9953
 
10149
9954
  // Compute transformation parameters
10150
- const polarDiff = vector$1.polarDegFromCart(vector$1.subtract(scaledEnd, scaledStart));
9955
+ const polarDiff = vector$3.polarDegFromCart(vector$3.subtract(scaledEnd, scaledStart));
10151
9956
  const lineLength = polarDiff[0];
10152
9957
  const angle = KhanMath.bound(polarDiff[1]);
10153
- const delta = vector$1.subtract(scaledStart, this.initialPoint);
9958
+ const delta = vector$3.subtract(scaledStart, this.initialPoint);
10154
9959
  const scale = KhanMath.bound(lineLength / this.initialLength);
10155
9960
 
10156
9961
  // Construct and apply transformation string
@@ -10160,260 +9965,11 @@ class WrappedLine extends WrappedDrawing {
10160
9965
  }
10161
9966
  }
10162
9967
 
10163
- /**
10164
- * A collection of geomtry-related utility functions
10165
- */
10166
- const {
10167
- eq: eq$3,
10168
- deepEq: deepEq$6
10169
- } = Util;
10170
-
10171
- // This should really be a readonly tuple of [number, number]
10172
-
10173
- // a, b, c
10174
-
10175
- // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10176
- function sign(val) {
10177
- if (eq$3(val, 0)) {
10178
- return 0;
10179
- }
10180
- return val > 0 ? 1 : -1;
10181
- }
10182
-
10183
- // Determine whether three points are collinear (0), for a clockwise turn (negative),
10184
- // or counterclockwise turn (positive)
10185
- function ccw(a, b, c) {
10186
- return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10187
- }
10188
- function collinear(a, b, c) {
10189
- return eq$3(ccw(a, b, c), 0);
10190
- }
10191
-
10192
- // Given rect bounding points A and B, whether point C is inside the rect
10193
- function pointInRect(a, b, c) {
10194
- 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]);
10195
- }
10196
-
10197
- // Whether line segment AB intersects line segment CD
10198
- // http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
10199
- function intersects(ab, cd) {
10200
- 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]]];
10201
- const orientations = _.map(triplets, function (triplet) {
10202
- return sign(ccw(...triplet));
10203
- });
10204
- if (orientations[0] !== orientations[1] && orientations[2] !== orientations[3]) {
10205
- return true;
10206
- }
10207
- for (let i = 0; i < 4; i++) {
10208
- if (orientations[i] === 0 && pointInRect(...triplets[i])) {
10209
- return true;
10210
- }
10211
- }
10212
- return false;
10213
- }
10214
-
10215
- // Whether any two sides of a polygon intersect each other
10216
- function polygonSidesIntersect(vertices) {
10217
- for (let i = 0; i < vertices.length; i++) {
10218
- for (let k = i + 1; k < vertices.length; k++) {
10219
- // If any two vertices are the same point, sides overlap
10220
- if (point.equal(vertices[i], vertices[k])) {
10221
- return true;
10222
- }
10223
-
10224
- // Find the other end of the sides starting at vertices i and k
10225
- const iNext = (i + 1) % vertices.length;
10226
- const kNext = (k + 1) % vertices.length;
10227
-
10228
- // Adjacent sides always intersect (at the vertex); skip those
10229
- if (iNext === k || kNext === i) {
10230
- continue;
10231
- }
10232
- const side1 = [vertices[i], vertices[iNext]];
10233
- const side2 = [vertices[k], vertices[kNext]];
10234
- if (intersects(side1, side2)) {
10235
- return true;
10236
- }
10237
- }
10238
- }
10239
- return false;
10240
- }
10241
- function vector(a, b) {
10242
- return _.map(_.zip(a, b), function (pair) {
10243
- return pair[0] - pair[1];
10244
- });
10245
- }
10246
- function reverseVector(vector) {
10247
- return [-vector[0], -vector[1]];
10248
- }
10249
-
10250
- // Returns whether connecting the given sequence of `points` forms a clockwise
10251
- // path (assuming a closed loop, where the last point connects back to the
10252
- // first).
10253
- function clockwise$1(points) {
10254
- const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
10255
- const areas = _.map(segments, function (segment) {
10256
- const p1 = segment[0];
10257
- const p2 = segment[1];
10258
- return (p2[0] - p1[0]) * (p2[1] + p1[1]);
10259
- });
10260
- return sum$1(areas) > 0;
10261
- }
10262
- function magnitude(v) {
10263
- return Math.sqrt(_.reduce(v, function (memo, el) {
10264
- // @ts-expect-error - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
10265
- return memo + Math.pow(el, 2);
10266
- }, 0));
10267
- }
10268
- function dotProduct(a, b) {
10269
- return _.reduce(_.zip(a, b), function (memo, pair) {
10270
- return memo + pair[0] * pair[1];
10271
- }, 0);
10272
- }
10273
- function sideLengths(coords) {
10274
- const segments = _.zip(coords, rotate(coords));
10275
- return segments.map(function (segment) {
10276
- // @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.
10277
- return magnitude(vector(...segment));
10278
- });
10279
- }
10280
-
10281
- // Based on http://math.stackexchange.com/a/151149
10282
- function angleMeasures(coords) {
10283
- const triplets = _.zip(rotate(coords, -1), coords, rotate(coords, 1));
10284
- const offsets = _.map(triplets, function (triplet) {
10285
- const p = vector(triplet[1], triplet[0]);
10286
- const q = vector(triplet[2], triplet[1]);
10287
- // @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[]'.
10288
- const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
10289
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
10290
- return sign(ccw(...triplet)) > 0 ? raw : -raw;
10291
- });
10292
- const sum = _.reduce(offsets, function (memo, arg) {
10293
- return memo + arg;
10294
- }, 0);
10295
- return _.map(offsets, function (offset) {
10296
- return sum > 0 ? Math.PI - offset : Math.PI + offset;
10297
- });
10298
- }
10299
-
10300
- // Whether two polygons are similar (or if specified, congruent)
10301
- function similar(coords1, coords2, tolerance) {
10302
- if (coords1.length !== coords2.length) {
10303
- return false;
10304
- }
10305
- const n = coords1.length;
10306
- const angles1 = angleMeasures(coords1);
10307
- const angles2 = angleMeasures(coords2);
10308
- const sides1 = sideLengths(coords1);
10309
- const sides2 = sideLengths(coords2);
10310
- for (let i = 0; i < 2 * n; i++) {
10311
- let angles = angles2.slice();
10312
- let sides = sides2.slice();
10313
-
10314
- // Reverse angles and sides to allow matching reflected polygons
10315
- if (i >= n) {
10316
- angles.reverse();
10317
- sides.reverse();
10318
- // Since sides are calculated from two coordinates,
10319
- // simply reversing results in an off by one error
10320
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10321
- sides = rotate(sides, 1);
10322
- }
10323
-
10324
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10325
- angles = rotate(angles, i);
10326
- // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
10327
- sides = rotate(sides, i);
10328
- if (deepEq$6(angles1, angles)) {
10329
- const sidePairs = _.zip(sides1, sides);
10330
- const factors = _.map(sidePairs, function (pair) {
10331
- return pair[0] / pair[1];
10332
- });
10333
- const same = _.all(factors, function (factor) {
10334
- return eq$3(factors[0], factor);
10335
- });
10336
- const congruentEnough = _.all(sidePairs, function (pair) {
10337
- return number$1.equal(pair[0], pair[1], tolerance);
10338
- });
10339
- if (same && congruentEnough) {
10340
- return true;
10341
- }
10342
- }
10343
- }
10344
- return false;
10345
- }
10346
-
10347
- // Given triangle with sides ABC return angle opposite side C in degrees
10348
- function lawOfCosines(a, b, c) {
10349
- return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180 / Math.PI;
10350
- }
10351
- function canonicalSineCoefficients$1([amplitude, angularFrequency, phase, verticalOffset]) {
10352
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
10353
- // this function ensures that a, b > 0, and c is its
10354
- // smallest possible positive value.
10355
-
10356
- // Guarantee a > 0
10357
- if (amplitude < 0) {
10358
- amplitude *= -1;
10359
- angularFrequency *= -1;
10360
- phase *= -1;
10361
- }
10362
- const period = 2 * Math.PI;
10363
- // Guarantee b > 0
10364
- if (angularFrequency < 0) {
10365
- angularFrequency *= -1;
10366
- phase *= -1;
10367
- phase += period / 2;
10368
- }
10369
-
10370
- // Guarantee c is smallest possible positive value
10371
- while (phase > 0) {
10372
- phase -= period;
10373
- }
10374
- while (phase < 0) {
10375
- phase += period;
10376
- }
10377
- return [amplitude, angularFrequency, phase, verticalOffset];
10378
- }
10379
-
10380
- // e.g. rotate([1, 2, 3]) -> [2, 3, 1]
10381
- function rotate(array, n) {
10382
- n = typeof n === "undefined" ? 1 : n % array.length;
10383
- return array.slice(n).concat(array.slice(0, n));
10384
- }
10385
- function getLineEquation(first, second) {
10386
- if (eq$3(first[0], second[0])) {
10387
- return "x = " + first[0].toFixed(3);
10388
- }
10389
- const m = (second[1] - first[1]) / (second[0] - first[0]);
10390
- const b = first[1] - m * first[0];
10391
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
10392
- }
10393
-
10394
- // Stolen from the wikipedia article
10395
- // http://en.wikipedia.org/wiki/Line-line_intersection
10396
- function getLineIntersection(
10397
- // TODO(LP-10725): update these to be 2-tuples
10398
- firstPoints, secondPoints) {
10399
- const x1 = firstPoints[0][0];
10400
- const y1 = firstPoints[0][1];
10401
- const x2 = firstPoints[1][0];
10402
- const y2 = firstPoints[1][1];
10403
- const x3 = secondPoints[0][0];
10404
- const y3 = secondPoints[0][1];
10405
- const x4 = secondPoints[1][0];
10406
- const y4 = secondPoints[1][1];
10407
- const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
10408
- if (Math.abs(determinant) < 1e-9) {
10409
- return "Lines are parallel";
10410
- }
10411
- const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / determinant;
10412
- const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / determinant;
10413
- return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
10414
- }
10415
-
10416
9968
  /* eslint-disable @babel/no-invalid-this */
9969
+ const {
9970
+ clockwise: clockwise$3,
9971
+ reverseVector: reverseVector$1
9972
+ } = geometry;
10417
9973
  function scaledDistanceFromAngle(angle) {
10418
9974
  const a = 3.51470560176242 * 20;
10419
9975
  const b = 0.5687298702748785 * 20;
@@ -10482,10 +10038,10 @@ _.extend(GraphUtils.Graphie.prototype, {
10482
10038
  let sRadius = sPadding + scaledDistanceFromAngle(angle);
10483
10039
  const temp = [];
10484
10040
  if (Math.abs(angle - 90) < 1e-9 && options.showRightAngleMarker) {
10485
- const v1 = vector$1.add(sVertex, scaledPolarDeg(sRadius, startAngle));
10486
- const v2 = vector$1.add(sVertex, scaledPolarDeg(sRadius, endAngle));
10041
+ const v1 = vector$3.add(sVertex, scaledPolarDeg(sRadius, startAngle));
10042
+ const v2 = vector$3.add(sVertex, scaledPolarDeg(sRadius, endAngle));
10487
10043
  sRadius *= Math.SQRT2;
10488
- const v3 = vector$1.add(sVertex, scaledPolarDeg(sRadius, halfAngle));
10044
+ const v3 = vector$3.add(sVertex, scaledPolarDeg(sRadius, halfAngle));
10489
10045
  _.each([v1, v2], function (v) {
10490
10046
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
10491
10047
  temp.push(graphie.scaledPath([v, v3], options.style));
@@ -10506,7 +10062,7 @@ _.extend(GraphUtils.Graphie.prototype, {
10506
10062
  text = text.replace(match[0], KhanMath.toFixedApprox(angle, precision) + "^{\\circ}");
10507
10063
  }
10508
10064
  const sOffset = scaledPolarDeg(sRadius + 15, halfAngle);
10509
- const sPosition = vector$1.add(sVertex, sOffset);
10065
+ const sPosition = vector$3.add(sVertex, sOffset);
10510
10066
  const position = graphie.unscalePoint(sPosition);
10511
10067
 
10512
10068
  // Reuse label if possible
@@ -10557,7 +10113,7 @@ _.extend(GraphUtils.Graphie.prototype, {
10557
10113
  const sOffset = sSpacing * (i - (n - 1) / 2);
10558
10114
  const sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10559
10115
  const sHeightVector = scaledPolarRad(sHeight, perpendicularAngle);
10560
- const sPath = [vector$1.add(sMidpoint, sOffsetVector, sHeightVector), vector$1.add(sMidpoint, sOffsetVector, reverseVector(sHeightVector))];
10116
+ const sPath = [vector$3.add(sMidpoint, sOffsetVector, sHeightVector), vector$3.add(sMidpoint, sOffsetVector, reverseVector$1(sHeightVector))];
10561
10117
 
10562
10118
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
10563
10119
  temp.push(graphie.scaledPath(sPath, style));
@@ -10582,9 +10138,9 @@ _.extend(GraphUtils.Graphie.prototype, {
10582
10138
  const sOffset = sCumulativeOffset + sSpacing * i;
10583
10139
  let sOffsetVector = scaledPolarRad(sOffset, parallelAngle);
10584
10140
  if (start !== p1) {
10585
- sOffsetVector = reverseVector(sOffsetVector);
10141
+ sOffsetVector = reverseVector$1(sOffsetVector);
10586
10142
  }
10587
- const sEnd = vector$1.add(sMidpoint, sOffsetVector);
10143
+ const sEnd = vector$3.add(sMidpoint, sOffsetVector);
10588
10144
 
10589
10145
  // @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
10590
10146
  temp.push(graphie.scaledPath([sStart, sEnd], style));
@@ -10600,7 +10156,7 @@ _.extend(GraphUtils.Graphie.prototype, {
10600
10156
  }
10601
10157
  const sOffset = 20;
10602
10158
  const sOffsetVector = scaledPolarRad(sOffset, perpendicularAngle);
10603
- const sPosition = vector$1.add(sMidpoint, sOffsetVector);
10159
+ const sPosition = vector$3.add(sMidpoint, sOffsetVector);
10604
10160
  const position = graphie.unscalePoint(sPosition);
10605
10161
 
10606
10162
  // Reuse label if possible
@@ -10658,7 +10214,7 @@ _.extend(GraphUtils.Graphie.prototype, {
10658
10214
  }
10659
10215
  const sRadius = 10 + scaledDistanceFromAngle(360 - angle);
10660
10216
  const sOffsetVector = scaledPolarDeg(sRadius, halfAngle);
10661
- const sPosition = vector$1.add(sVertex, sOffsetVector);
10217
+ const sPosition = vector$3.add(sVertex, sOffsetVector);
10662
10218
  const position = graphie.unscalePoint(sPosition);
10663
10219
 
10664
10220
  // Reuse label if possible
@@ -10934,7 +10490,7 @@ _.extend(GraphUtils.Graphie.prototype, {
10934
10490
 
10935
10491
  // Adjust the target coordinate by accounting for the gesture's
10936
10492
  // offset.
10937
- let coord = vector$1.add(graph.getMouseCoord(event), offset);
10493
+ let coord = vector$3.add(graph.getMouseCoord(event), offset);
10938
10494
  coord = applySnapAndConstraints(coord);
10939
10495
  let coordX = coord[0];
10940
10496
  let coordY = coord[1];
@@ -11045,7 +10601,7 @@ _.extend(GraphUtils.Graphie.prototype, {
11045
10601
  const startCoord = movablePoint.coord;
11046
10602
  const startMouseCoord = graph.getMouseCoord(event);
11047
10603
  const isMouse = !("ontouchstart" in window);
11048
- const touchOffset = isMouse ? [0, 0] : vector$1.subtract(startCoord, startMouseCoord);
10604
+ const touchOffset = isMouse ? [0, 0] : vector$3.subtract(startCoord, startMouseCoord);
11049
10605
  movablePoint.grab(touchOffset);
11050
10606
  }
11051
10607
  });
@@ -11265,17 +10821,17 @@ _.extend(GraphUtils.Graphie.prototype, {
11265
10821
  const getScaledAngle = function getScaledAngle(line) {
11266
10822
  const scaledA = line.graph.scalePoint(line.coordA);
11267
10823
  const scaledZ = line.graph.scalePoint(line.coordZ);
11268
- return vector$1.polarDegFromCart(vector$1.subtract(scaledZ, scaledA))[1];
10824
+ return vector$3.polarDegFromCart(vector$3.subtract(scaledZ, scaledA))[1];
11269
10825
  };
11270
10826
  const getClipPoint = function getClipPoint(graph, coord, angle) {
11271
10827
  graph = lineSegment.graph;
11272
10828
  const xExtent = graph.range[0][1] - graph.range[0][0];
11273
10829
  const yExtent = graph.range[1][1] - graph.range[1][0];
11274
10830
  const distance = xExtent + yExtent;
11275
- const angleVec = graph.unscaleVector(vector$1.cartFromPolarDeg(1, angle));
11276
- const distVec = vector$1.scale(vector$1.normalize(angleVec), distance);
11277
- const farCoord = vector$1.add(coord, distVec);
11278
- const scaledAngle = vector$1.polarDegFromCart(angleVec)[1];
10831
+ const angleVec = graph.unscaleVector(vector$3.cartFromPolarDeg(1, angle));
10832
+ const distVec = vector$3.scale(vector$3.normalize(angleVec), distance);
10833
+ const farCoord = vector$3.add(coord, distVec);
10834
+ const scaledAngle = vector$3.polarDegFromCart(angleVec)[1];
11279
10835
  const clipPoint = graph.constrainToBoundsOnAngle(farCoord, 4, scaledAngle * Math.PI / 180);
11280
10836
  return clipPoint;
11281
10837
  };
@@ -11649,7 +11205,7 @@ _.extend(GraphUtils.Graphie.prototype, {
11649
11205
  // Temporary objects
11650
11206
  _.invoke(polygon.temp, "remove");
11651
11207
  polygon.temp = [];
11652
- const isClockwise = clockwise$1(polygon.coords);
11208
+ const isClockwise = clockwise$3(polygon.coords);
11653
11209
 
11654
11210
  // Update angle labels
11655
11211
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -12177,8 +11733,8 @@ _.extend(GraphUtils.Graphie.prototype, {
12177
11733
  const drawRotateHandle = function drawRotateHandle(graphie, center, radius, halfWidth, lengthAngle, angle, interacting) {
12178
11734
  const getRotateHandlePoint = function getRotateHandlePoint(offset, distanceFromArrowMidline) {
12179
11735
  const distFromRotationCenter = radius + distanceFromArrowMidline;
12180
- const vec = vector$1.cartFromPolarDeg(distFromRotationCenter, angle + offset);
12181
- const absolute = vector$1.add(center, vec);
11736
+ const vec = vector$3.cartFromPolarDeg(distFromRotationCenter, angle + offset);
11737
+ const absolute = vector$3.add(center, vec);
12182
11738
  const pixels = graphie.scalePoint(absolute);
12183
11739
  return pixels[0] + "," + pixels[1];
12184
11740
  };
@@ -12218,7 +11774,7 @@ _.extend(GraphUtils.Graphie.prototype, {
12218
11774
  };
12219
11775
  }
12220
11776
  const rotateHandle = graph.addMovablePoint({
12221
- coord: point.addVector(rotatePoint.coord, vector$1.cartFromPolarDeg(radius, options.angleDeg || 0)),
11777
+ coord: point.addVector(rotatePoint.coord, vector$3.cartFromPolarDeg(radius, options.angleDeg || 0)),
12222
11778
  constraints: {
12223
11779
  fixedDistance: {
12224
11780
  dist: radius,
@@ -12243,8 +11799,8 @@ _.extend(GraphUtils.Graphie.prototype, {
12243
11799
  return; // Don't draw anything!
12244
11800
  }
12245
11801
 
12246
- const handleVec = vector$1.subtract(handleCoord, rotatePoint.coord);
12247
- const handlePolar = vector$1.polarDegFromCart(handleVec);
11802
+ const handleVec = vector$3.subtract(handleCoord, rotatePoint.coord);
11803
+ const handlePolar = vector$3.polarDegFromCart(handleVec);
12248
11804
  const angle = handlePolar[1];
12249
11805
  if (drawnRotateHandle) {
12250
11806
  drawnRotateHandle.remove();
@@ -12256,8 +11812,8 @@ _.extend(GraphUtils.Graphie.prototype, {
12256
11812
  // the rotationHandle as well, or it will end up out
12257
11813
  // of sync
12258
11814
  $(rotatePoint).on("move." + id, function () {
12259
- const delta = vector$1.subtract(rotatePoint.coord, rotatePointPrevCoord);
12260
- rotateHandle.setCoord(vector$1.add(rotateHandle.coord, delta));
11815
+ const delta = vector$3.subtract(rotatePoint.coord, rotatePointPrevCoord);
11816
+ rotateHandle.setCoord(vector$3.add(rotateHandle.coord, delta));
12261
11817
  redrawRotateHandle(rotateHandle.coord);
12262
11818
  rotatePointPrevCoord = rotatePoint.coord;
12263
11819
  rotateHandle.constraints.fixedDistance.point = rotatePoint;
@@ -12272,8 +11828,8 @@ _.extend(GraphUtils.Graphie.prototype, {
12272
11828
  }
12273
11829
  let coord = [x, y];
12274
11830
  if (options.onMove) {
12275
- const oldPolar = vector$1.polarDegFromCart(vector$1.subtract(rotateHandlePrevCoord, rotatePoint.coord));
12276
- const newPolar = vector$1.polarDegFromCart(vector$1.subtract(coord, rotatePoint.coord));
11831
+ const oldPolar = vector$3.polarDegFromCart(vector$3.subtract(rotateHandlePrevCoord, rotatePoint.coord));
11832
+ const newPolar = vector$3.polarDegFromCart(vector$3.subtract(coord, rotatePoint.coord));
12277
11833
  const oldAngle = oldPolar[1];
12278
11834
  const newAngle = newPolar[1];
12279
11835
  let result = options.onMove(newAngle, oldAngle);
@@ -12281,7 +11837,7 @@ _.extend(GraphUtils.Graphie.prototype, {
12281
11837
  if (result === false) {
12282
11838
  result = oldAngle;
12283
11839
  }
12284
- coord = vector$1.add(rotatePoint.coord, vector$1.cartFromPolarDeg(oldPolar[0], result));
11840
+ coord = vector$3.add(rotatePoint.coord, vector$3.cartFromPolarDeg(oldPolar[0], result));
12285
11841
  }
12286
11842
  }
12287
11843
  redrawRotateHandle(coord);
@@ -12292,8 +11848,8 @@ _.extend(GraphUtils.Graphie.prototype, {
12292
11848
  isRotating = false;
12293
11849
  redrawRotateHandle(rotateHandle.coord);
12294
11850
  if (options.onMoveEnd) {
12295
- const oldPolar = vector$1.polarDegFromCart(vector$1.subtract(rotateHandleStartCoord, rotatePoint.coord));
12296
- const newPolar = vector$1.polarDegFromCart(vector$1.subtract(rotateHandle.coord, rotatePoint.coord));
11851
+ const oldPolar = vector$3.polarDegFromCart(vector$3.subtract(rotateHandleStartCoord, rotatePoint.coord));
11852
+ const newPolar = vector$3.polarDegFromCart(vector$3.subtract(rotateHandle.coord, rotatePoint.coord));
12297
11853
  options.onMoveEnd(newPolar[1], oldPolar[1]);
12298
11854
  }
12299
11855
  };
@@ -12338,14 +11894,14 @@ _.extend(GraphUtils.Graphie.prototype, {
12338
11894
  if (point.equal(lineCoords[0], lineCoords[1])) {
12339
11895
  lineCoords = [lineCoords[0], point.addVector(lineCoords[0], [1, 1])];
12340
11896
  }
12341
- const lineDirection = vector$1.normalize(vector$1.subtract(lineCoords[1], lineCoords[0]));
12342
- const lineVec = vector$1.scale(lineDirection, size / 2);
12343
- const centerVec = vector$1.scale(lineDirection, distanceFromCenter);
12344
- const leftCenterVec = vector$1.rotateDeg(centerVec, 90);
12345
- const rightCenterVec = vector$1.rotateDeg(centerVec, -90);
12346
- const negLineVec = vector$1.negate(lineVec);
12347
- const leftVec = vector$1.rotateDeg(lineVec, 90);
12348
- const rightVec = vector$1.rotateDeg(lineVec, -90);
11897
+ const lineDirection = vector$3.normalize(vector$3.subtract(lineCoords[1], lineCoords[0]));
11898
+ const lineVec = vector$3.scale(lineDirection, size / 2);
11899
+ const centerVec = vector$3.scale(lineDirection, distanceFromCenter);
11900
+ const leftCenterVec = vector$3.rotateDeg(centerVec, 90);
11901
+ const rightCenterVec = vector$3.rotateDeg(centerVec, -90);
11902
+ const negLineVec = vector$3.negate(lineVec);
11903
+ const leftVec = vector$3.rotateDeg(lineVec, 90);
11904
+ const rightVec = vector$3.rotateDeg(lineVec, -90);
12349
11905
  const leftCenter = point.addVectors(buttonCoord, leftCenterVec);
12350
11906
  const rightCenter = point.addVectors(buttonCoord, rightCenterVec);
12351
11907
  const leftCoord1 = point.addVectors(buttonCoord, leftCenterVec, lineVec, leftVec);
@@ -12472,7 +12028,7 @@ _.extend(GraphUtils.Graphie.prototype, {
12472
12028
  // @ts-expect-error - TS2350 - Only a void function can be called with the 'new' keyword.
12473
12029
  return new Ruler(this, options || {});
12474
12030
  },
12475
- addPoints: vector$1.add
12031
+ addPoints: vector$3.add
12476
12032
  });
12477
12033
  function Protractor$1(graph, center) {
12478
12034
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
@@ -12746,10 +12302,10 @@ function Ruler(graphie, options) {
12746
12302
  });
12747
12303
  const width = options.units * options.pixelsPerUnit;
12748
12304
  const height = 50;
12749
- const leftBottom = graphie.unscalePoint(vector$1.subtract(graphie.scalePoint(options.center), vector$1.scale([width, -height], 0.5)));
12305
+ const leftBottom = graphie.unscalePoint(vector$3.subtract(graphie.scalePoint(options.center), vector$3.scale([width, -height], 0.5)));
12750
12306
  const graphieUnitsPerUnit = options.pixelsPerUnit / graphie.scale[0];
12751
12307
  const graphieUnitsHeight = height / graphie.scale[0];
12752
- const rightTop = vector$1.add(leftBottom, [options.units * graphieUnitsPerUnit, graphieUnitsHeight]);
12308
+ const rightTop = vector$3.add(leftBottom, [options.units * graphieUnitsPerUnit, graphieUnitsHeight]);
12753
12309
  const tickHeight = 1.0;
12754
12310
  let tickHeightMap;
12755
12311
  if (options.ticksPerUnit === 10) {
@@ -12896,11 +12452,11 @@ function Ruler(graphie, options) {
12896
12452
  const dy = rightBottomHandle.coord[1] - y;
12897
12453
  const dx = rightBottomHandle.coord[0] - x;
12898
12454
  const angle = Math.atan2(dy, dx) * 180 / Math.PI;
12899
- const center = vector$1.scale(vector$1.add([x, y], rightBottomHandle.coord), 0.5);
12455
+ const center = vector$3.scale(vector$3.add([x, y], rightBottomHandle.coord), 0.5);
12900
12456
  const scaledCenter = graphie.scalePoint(center);
12901
- const oldCenter = vector$1.scale(vector$1.add(leftBottomHandle.coord, rightBottomHandle.coord), 0.5);
12457
+ const oldCenter = vector$3.scale(vector$3.add(leftBottomHandle.coord, rightBottomHandle.coord), 0.5);
12902
12458
  const scaledOldCenter = graphie.scalePoint(oldCenter);
12903
- const diff = vector$1.subtract(scaledCenter, scaledOldCenter);
12459
+ const diff = vector$3.subtract(scaledCenter, scaledOldCenter);
12904
12460
  set.rotate(-angle, scaledOldCenter[0], scaledOldCenter[1]);
12905
12461
  set.translate(diff[0], diff[1]);
12906
12462
  }
@@ -12923,11 +12479,11 @@ function Ruler(graphie, options) {
12923
12479
  const dy = y - leftBottomHandle.coord[1];
12924
12480
  const dx = x - leftBottomHandle.coord[0];
12925
12481
  const angle = Math.atan2(dy, dx) * 180 / Math.PI;
12926
- const center = vector$1.scale(vector$1.add([x, y], leftBottomHandle.coord), 0.5);
12482
+ const center = vector$3.scale(vector$3.add([x, y], leftBottomHandle.coord), 0.5);
12927
12483
  const scaledCenter = graphie.scalePoint(center);
12928
- const oldCenter = vector$1.scale(vector$1.add(leftBottomHandle.coord, rightBottomHandle.coord), 0.5);
12484
+ const oldCenter = vector$3.scale(vector$3.add(leftBottomHandle.coord, rightBottomHandle.coord), 0.5);
12929
12485
  const scaledOldCenter = graphie.scalePoint(oldCenter);
12930
- const diff = vector$1.subtract(scaledCenter, scaledOldCenter);
12486
+ const diff = vector$3.subtract(scaledCenter, scaledOldCenter);
12931
12487
  set.rotate(-angle, scaledOldCenter[0], scaledOldCenter[1]);
12932
12488
  set.translate(diff[0], diff[1]);
12933
12489
  }
@@ -13070,12 +12626,12 @@ _.extend(MovableAngle.prototype, {
13070
12626
  points[1].onMove = function (x, y) {
13071
12627
  const oldVertex = points[1].coord;
13072
12628
  const newVertex = [x, y];
13073
- const delta = vector$1.add(newVertex, reverseVector(oldVertex));
12629
+ const delta = vector$3.add(newVertex, reverseVector$1(oldVertex));
13074
12630
  let valid = true;
13075
12631
  const newPoints = {};
13076
12632
  _.each([0, 2], function (i) {
13077
12633
  const oldPoint = points[i].coord;
13078
- let newPoint = vector$1.add(oldPoint, delta);
12634
+ let newPoint = vector$3.add(oldPoint, delta);
13079
12635
  let angle = GraphUtils.findAngleDeprecated(newVertex, newPoint);
13080
12636
  angle *= Math.PI / 180;
13081
12637
  newPoint = graphie.constrainToBoundsOnAngle(newPoint, 10, angle);
@@ -13112,7 +12668,7 @@ _.extend(MovableAngle.prototype, {
13112
12668
  let angle = GraphUtils.findAngleDeprecated(newPoint, vertex);
13113
12669
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13114
12670
  const distance = GraphUtils.getDistance(newPoint, vertex);
13115
- return vector$1.add(vertex, polar$1(distance, angle));
12671
+ return vector$3.add(vertex, polar$2(distance, angle));
13116
12672
  }
13117
12673
  return true;
13118
12674
  };
@@ -13263,7 +12819,6 @@ _.extend(MovableAngle.prototype, {
13263
12819
 
13264
12820
  /* eslint-disable @babel/no-invalid-this */
13265
12821
  const nestedMap$1 = Util.nestedMap;
13266
- const deepEq$5 = Util.deepEq;
13267
12822
 
13268
12823
  /**
13269
12824
  * A base class for all Graphie Movables
@@ -13360,7 +12915,7 @@ const createSimpleClass = function createSimpleClass(addFunction) {
13360
12915
  this._prevProps = this.props;
13361
12916
  },
13362
12917
  modify: function (graphie) {
13363
- if (!deepEq$5(this.props, this._prevProps)) {
12918
+ if (!approximateDeepEqual(this.props, this._prevProps)) {
13364
12919
  this.remove();
13365
12920
  this.add(graphie);
13366
12921
  this._prevProps = this.props;
@@ -13721,7 +13276,7 @@ function getEventName$1(key) {
13721
13276
  const getScaledAngle = function getScaledAngle(line) {
13722
13277
  const scaledA = line.graphie.scalePoint(line.coord(0));
13723
13278
  const scaledZ = line.graphie.scalePoint(line.coord(1));
13724
- return vector$1.polarDegFromCart(vector$1.subtract(scaledZ, scaledA))[1];
13279
+ return vector$3.polarDegFromCart(vector$3.subtract(scaledZ, scaledA))[1];
13725
13280
  };
13726
13281
 
13727
13282
  /**
@@ -13880,11 +13435,11 @@ const constraints$2 = {
13880
13435
  if (snap === null) {
13881
13436
  return true;
13882
13437
  }
13883
- let delta = vector$1.subtract(coord, prevCoord);
13438
+ let delta = vector$3.subtract(coord, prevCoord);
13884
13439
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
13885
13440
  snap = snap || this.graphie.snap;
13886
13441
  delta = point.roundTo(delta, snap);
13887
- return vector$1.add(prevCoord, delta);
13442
+ return vector$3.add(prevCoord, delta);
13888
13443
  };
13889
13444
  },
13890
13445
  bound: function (range, snap, paddingPx) {
@@ -13898,7 +13453,7 @@ const constraints$2 = {
13898
13453
  return function (coord, prevCoord) {
13899
13454
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
13900
13455
  const graphie = this.graphie;
13901
- const delta = vector$1.subtract(coord, prevCoord);
13456
+ const delta = vector$3.subtract(coord, prevCoord);
13902
13457
  range = range || graphie.range;
13903
13458
  // A null snap means no snap; an undefined snap means
13904
13459
  // default to graphie's
@@ -13918,8 +13473,8 @@ const constraints$2 = {
13918
13473
  const deltaBounds = _.map(
13919
13474
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
13920
13475
  this.coords(), function (coord, i) {
13921
- const max = vector$1.subtract(absoluteUpper, coord);
13922
- const min = vector$1.subtract(absoluteLower, coord);
13476
+ const max = vector$3.subtract(absoluteUpper, coord);
13477
+ const min = vector$3.subtract(absoluteLower, coord);
13923
13478
  return [min, max];
13924
13479
  });
13925
13480
 
@@ -13933,7 +13488,7 @@ const constraints$2 = {
13933
13488
  const deltaY = Math.max(lower[1], Math.min(upper[1], delta[1]));
13934
13489
  return [deltaX, deltaY];
13935
13490
  }, delta);
13936
- return vector$1.add(prevCoord, boundedDelta);
13491
+ return vector$3.add(prevCoord, boundedDelta);
13937
13492
  };
13938
13493
  }
13939
13494
  };
@@ -13942,10 +13497,10 @@ const constraints$2 = {
13942
13497
  constraints$2.standard = null;
13943
13498
  const onMove$1 = {
13944
13499
  updatePoints: function (coord, prevCoord) {
13945
- const actualDelta = vector$1.subtract(coord, prevCoord);
13500
+ const actualDelta = vector$3.subtract(coord, prevCoord);
13946
13501
  // @ts-expect-error - TS2339 - Property 'state' does not exist on type '{ readonly updatePoints: (coord: Coord, prevCoord: Coord) => void; }'.
13947
13502
  _.each(this.state.points, function (point) {
13948
- point.setCoord(vector$1.add(point.coord(), actualDelta));
13503
+ point.setCoord(vector$3.add(point.coord(), actualDelta));
13949
13504
  });
13950
13505
  }
13951
13506
  };
@@ -14107,9 +13662,9 @@ _.extend(MovableLine$4.prototype, {
14107
13662
  self._fireEvent(self.state.onMoveStart, self.coord(0), self.coord(0));
14108
13663
  },
14109
13664
  onMove: function (mouseCoord, prevMouseCoord) {
14110
- const delta = vector$1.subtract(mouseCoord, prevMouseCoord);
14111
- self._totalDelta = vector$1.add(self._totalDelta, delta);
14112
- let refCoord = vector$1.add(self._initialRefCoord, self._totalDelta);
13665
+ const delta = vector$3.subtract(mouseCoord, prevMouseCoord);
13666
+ self._totalDelta = vector$3.add(self._totalDelta, delta);
13667
+ let refCoord = vector$3.add(self._initialRefCoord, self._totalDelta);
14113
13668
  refCoord = self._applyConstraints(refCoord, self._prevRefCoord);
14114
13669
  if (refCoord === false) {
14115
13670
  return;
@@ -14621,7 +14176,7 @@ class MovablePoint$5 {
14621
14176
  // touch-based interaction.
14622
14177
  if (state.touchOffset == null) {
14623
14178
  const isMouse = !("ontouchstart" in window);
14624
- state.touchOffset = isMouse ? [0, 0] : vector$1.subtract(startCoord, startMouseCoord);
14179
+ state.touchOffset = isMouse ? [0, 0] : vector$3.subtract(startCoord, startMouseCoord);
14625
14180
  }
14626
14181
  const svgElem = state.visibleShape.wrapper;
14627
14182
  if (state.shadow) {
@@ -14648,7 +14203,7 @@ class MovablePoint$5 {
14648
14203
  self.draw();
14649
14204
  },
14650
14205
  onMove: (mouseCoord, prevMouseCoord) => {
14651
- const transformedCoord = vector$1.add(mouseCoord, state.touchOffset);
14206
+ const transformedCoord = vector$3.add(mouseCoord, state.touchOffset);
14652
14207
  self.moveTo(transformedCoord);
14653
14208
  if (state.showHairlines) {
14654
14209
  if (!this.state.outOfBounds) {
@@ -15041,7 +14596,7 @@ function sum(array) {
15041
14596
  return memo + arg;
15042
14597
  }, 0);
15043
14598
  }
15044
- function clockwise(points) {
14599
+ function clockwise$2(points) {
15045
14600
  const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
15046
14601
  const areas = _.map(segments, function (segment) {
15047
14602
  const p1 = segment[0];
@@ -15107,7 +14662,7 @@ const draw$1 = {
15107
14662
  const graphie = this.graphie;
15108
14663
  const self = this;
15109
14664
  const coords = _.invoke(state.points, "coord");
15110
- const isClockwise = clockwise(coords);
14665
+ const isClockwise = clockwise$2(coords);
15111
14666
  const n = coords.length;
15112
14667
 
15113
14668
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -15278,7 +14833,7 @@ const constraints = {
15278
14833
  return function (coord, prevCoord) {
15279
14834
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
15280
14835
  const graphie = this.graphie;
15281
- const delta = vector$1.subtract(coord, prevCoord);
14836
+ const delta = vector$3.subtract(coord, prevCoord);
15282
14837
  range = range || graphie.range;
15283
14838
  // A null snap means no snap; an undefined snap means
15284
14839
  // default to graphie's
@@ -15297,8 +14852,8 @@ const constraints = {
15297
14852
  // Calculate the bounds for the delta.
15298
14853
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
15299
14854
  const deltaBounds = _.map(this.coords(), function (coord, i) {
15300
- const max = vector$1.subtract(absoluteUpper, coord);
15301
- const min = vector$1.subtract(absoluteLower, coord);
14855
+ const max = vector$3.subtract(absoluteUpper, coord);
14856
+ const min = vector$3.subtract(absoluteLower, coord);
15302
14857
  return [min, max];
15303
14858
  });
15304
14859
 
@@ -15312,7 +14867,7 @@ const constraints = {
15312
14867
  const deltaY = Math.max(lower[1], Math.min(upper[1], delta[1]));
15313
14868
  return [deltaX, deltaY];
15314
14869
  }, delta);
15315
- return vector$1.add(prevCoord, boundedDelta);
14870
+ return vector$3.add(prevCoord, boundedDelta);
15316
14871
  };
15317
14872
  }
15318
14873
  };
@@ -15321,10 +14876,10 @@ const constraints = {
15321
14876
  constraints.standard = null;
15322
14877
  const onMove = {
15323
14878
  updatePoints: function (coord, prevCoord) {
15324
- const actualDelta = vector$1.subtract(coord, prevCoord);
14879
+ const actualDelta = vector$3.subtract(coord, prevCoord);
15325
14880
  // @ts-expect-error - TS2339 - Property 'state' does not exist on type '{ readonly updatePoints: (coord: Coord, prevCoord: Coord) => undefined; }'.
15326
14881
  _.each(this.state.points, function (point) {
15327
- point.setCoord(vector$1.add(point.coord(), actualDelta));
14882
+ point.setCoord(vector$3.add(point.coord(), actualDelta));
15328
14883
  });
15329
14884
  }
15330
14885
  };
@@ -15498,9 +15053,9 @@ _.extend(MovablePolygon.prototype, {
15498
15053
  self._fireEvent(self.state.onMoveStart, self.coord(0), self.coord(0));
15499
15054
  },
15500
15055
  onMove: function (mouseCoord, prevMouseCoord) {
15501
- const delta = vector$1.subtract(mouseCoord, prevMouseCoord);
15502
- self._totalDelta = vector$1.add(self._totalDelta, delta);
15503
- let refCoord = vector$1.add(self._initialRefCoord, self._totalDelta);
15056
+ const delta = vector$3.subtract(mouseCoord, prevMouseCoord);
15057
+ self._totalDelta = vector$3.add(self._totalDelta, delta);
15058
+ let refCoord = vector$3.add(self._initialRefCoord, self._totalDelta);
15504
15059
  refCoord = self._applyConstraints(refCoord, self._prevRefCoord);
15505
15060
  if (refCoord === false) {
15506
15061
  return;
@@ -15815,7 +15370,7 @@ const Parabola = GraphieClasses.createClass({
15815
15370
  this.parabola.toFront();
15816
15371
  }
15817
15372
  });
15818
- const Sinusoid$1 = GraphieClasses.createClass({
15373
+ const Sinusoid = GraphieClasses.createClass({
15819
15374
  displayName: "Sinusoid",
15820
15375
  movableProps: ["children"],
15821
15376
  add: function (graphie) {
@@ -15837,7 +15392,7 @@ const Sinusoid$1 = GraphieClasses.createClass({
15837
15392
  this.sinusoid.toFront();
15838
15393
  }
15839
15394
  });
15840
- const Plot$2 = GraphieClasses.createSimpleClass((graphie, props) => {
15395
+ const Plot$1 = GraphieClasses.createSimpleClass((graphie, props) => {
15841
15396
  return graphie.plot(props.fn, props.range, props.style);
15842
15397
  });
15843
15398
  const PlotParametric$1 = GraphieClasses.createSimpleClass((graphie, props) => {
@@ -15895,17 +15450,16 @@ var Movables = {
15895
15450
  MovablePoint: MovablePoint$4,
15896
15451
  Parabola: Parabola,
15897
15452
  Path: Path,
15898
- Plot: Plot$2,
15453
+ Plot: Plot$1,
15899
15454
  PlotParametric: PlotParametric$1,
15900
15455
  Point: Point$1,
15901
- Sinusoid: Sinusoid$1,
15456
+ Sinusoid: Sinusoid,
15902
15457
  Rect: Rect$1
15903
15458
  };
15904
15459
 
15905
15460
  const GraphieMovable = GraphieClasses.GraphieMovable;
15906
15461
  const createGraphie = GraphUtils.createGraphie;
15907
15462
  const {
15908
- deepEq: deepEq$4,
15909
15463
  nestedMap
15910
15464
  } = Util;
15911
15465
  const {
@@ -16147,7 +15701,7 @@ class Graphie extends React.Component {
16147
15701
  if (this.props.setup !== prevProps.setup) {
16148
15702
  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.", Errors.Internal);
16149
15703
  }
16150
- if (!deepEq$4(this.props.options, prevProps.options) || !deepEq$4(this.props.box, prevProps.box) || !deepEq$4(this.props.range, prevProps.range)) {
15704
+ if (!approximateDeepEqual(this.props.options, prevProps.options) || !approximateDeepEqual(this.props.box, prevProps.box) || !approximateDeepEqual(this.props.range, prevProps.range)) {
16151
15705
  this._setupGraphie();
16152
15706
  }
16153
15707
  this._updateMovables();
@@ -20902,230 +20456,6 @@ const getPromptJSON$s = (renderProps, userInput) => {
20902
20456
  };
20903
20457
  };
20904
20458
 
20905
- /*
20906
- * In this file, an `expression` is some portion of valid TeX enclosed in
20907
- * curly brackets.
20908
- */
20909
-
20910
- /*
20911
- * Find the index at which an expression ends, i.e., has an unmatched
20912
- * closing curly bracket. This method assumes that we start with a non-open
20913
- * bracket character and end when we've seen more left than right brackets
20914
- * (rather than assuming that we start with a bracket character and wait for
20915
- * bracket equality).
20916
- */
20917
- function findEndpoint(tex, currentIndex) {
20918
- let bracketDepth = 0;
20919
- for (let i = currentIndex, len = tex.length; i < len; i++) {
20920
- const c = tex[i];
20921
- if (c === "{") {
20922
- bracketDepth++;
20923
- } else if (c === "}") {
20924
- bracketDepth--;
20925
- }
20926
- if (bracketDepth < 0) {
20927
- return i;
20928
- }
20929
- }
20930
- // If we never see unbalanced curly brackets, default to the
20931
- // entire string
20932
- return tex.length;
20933
- }
20934
-
20935
- /*
20936
- * Parses an individual set of curly brackets into TeX.
20937
- */
20938
- function parseNextExpression(tex, currentIndex, handler) {
20939
- // Find the first '{' and grab subsequent TeX
20940
- // Ex) tex: '{3}{7}', and we want the '3'
20941
- const openBracketIndex = tex.indexOf("{", currentIndex);
20942
- const nextExpIndex = openBracketIndex + 1;
20943
-
20944
- // Truncate to only contain remaining TeX
20945
- const endpoint = findEndpoint(tex, nextExpIndex);
20946
- const expressionTeX = tex.substring(nextExpIndex, endpoint);
20947
- const parsedExp = walkTex(expressionTeX, handler);
20948
- return {
20949
- endpoint: endpoint,
20950
- expression: parsedExp
20951
- };
20952
- }
20953
- function getNextFracIndex(tex, currentIndex) {
20954
- const dfrac = "\\dfrac";
20955
- const frac = "\\frac";
20956
- const nextFrac = tex.indexOf(frac, currentIndex);
20957
- const nextDFrac = tex.indexOf(dfrac, currentIndex);
20958
- if (nextFrac > -1 && nextDFrac > -1) {
20959
- return Math.min(nextFrac, nextDFrac);
20960
- }
20961
- if (nextFrac > -1) {
20962
- return nextFrac;
20963
- }
20964
- if (nextDFrac > -1) {
20965
- return nextDFrac;
20966
- }
20967
- return -1;
20968
- }
20969
- function walkTex(tex, handler) {
20970
- if (!tex) {
20971
- return "";
20972
- }
20973
-
20974
- // Ex) tex: '2 \dfrac {3}{7}'
20975
- let parsedString = "";
20976
- let currentIndex = 0;
20977
- let nextFrac = getNextFracIndex(tex, currentIndex);
20978
-
20979
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
20980
- while (nextFrac > -1) {
20981
- // Gather first fragment, preceding \dfrac
20982
- // Ex) parsedString: '2 '
20983
- parsedString += tex.substring(currentIndex, nextFrac);
20984
-
20985
- // Remove everything preceding \dfrac, which has been parsed
20986
- currentIndex = nextFrac;
20987
-
20988
- // Parse first expression and move index past it
20989
- // Ex) firstParsedExpression.expression: '3'
20990
- const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
20991
- currentIndex = firstParsedExpression.endpoint + 1;
20992
-
20993
- // Parse second expression
20994
- // Ex) secondParsedExpression.expression: '7'
20995
- const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
20996
- currentIndex = secondParsedExpression.endpoint + 1;
20997
-
20998
- // Add expressions to running total of parsed expressions
20999
- if (parsedString.length) {
21000
- parsedString += " ";
21001
- }
21002
-
21003
- // Apply a custom handler based on the parsed subexpressions
21004
- parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21005
-
21006
- // Find next DFrac, relative to currentIndex
21007
- nextFrac = getNextFracIndex(tex, currentIndex);
21008
- }
21009
-
21010
- // Add remaining TeX, which is \dfrac-free
21011
- parsedString += tex.slice(currentIndex);
21012
- return parsedString;
21013
- }
21014
-
21015
- /*
21016
- * Modify a TeX expression, returning another TeX expression. The resulting
21017
- * expression will have its innermost fractions stubbed out with \fracs
21018
- * (as opposed to \dfracs). All other content will remain untouched.
21019
- */
21020
- function modifyTex(tex) {
21021
- function isNestedFraction(tex) {
21022
- return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21023
- }
21024
- const handler = function handler(exp1, exp2) {
21025
- let prefix;
21026
- if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21027
- prefix = "\\dfrac";
21028
- } else {
21029
- prefix = "\\frac";
21030
- }
21031
- return prefix + " {" + exp1 + "}{" + exp2 + "}";
21032
- };
21033
- return walkTex(tex, handler);
21034
- }
21035
-
21036
- /*
21037
- * Parse a TeX expression into something interpretable by input-number.
21038
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21039
- * (2) removing backslash-escaping from certain characters (right now, only
21040
- * percent signs).
21041
- *
21042
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21043
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21044
- * recursion is to allow for nested \dfrac elements.
21045
- *
21046
- * Backslash-escapes are removed with a simple search-and-replace.
21047
- */
21048
- function parseTex(tex) {
21049
- const handler = function handler(exp1, exp2) {
21050
- return exp1 + "/" + exp2;
21051
- };
21052
- const texWithoutFracs = walkTex(tex, handler);
21053
- return texWithoutFracs.replace("\\%", "%");
21054
- }
21055
- var TexWrangler = {
21056
- parseTex,
21057
- modifyTex
21058
- };
21059
-
21060
- const ParseTex$1 = TexWrangler.parseTex;
21061
- const answerTypes = {
21062
- number: {
21063
- name: "Numbers",
21064
- forms: "integer, decimal, proper, improper, mixed"
21065
- },
21066
- decimal: {
21067
- name: "Decimals",
21068
- forms: "decimal"
21069
- },
21070
- integer: {
21071
- name: "Integers",
21072
- forms: "integer"
21073
- },
21074
- rational: {
21075
- name: "Fractions and mixed numbers",
21076
- forms: "integer, proper, improper, mixed"
21077
- },
21078
- improper: {
21079
- name: "Improper numbers (no mixed)",
21080
- forms: "integer, proper, improper"
21081
- },
21082
- mixed: {
21083
- name: "Mixed numbers (no improper)",
21084
- forms: "integer, proper, mixed"
21085
- },
21086
- percent: {
21087
- name: "Numbers or percents",
21088
- forms: "integer, decimal, proper, improper, mixed, percent"
21089
- },
21090
- pi: {
21091
- name: "Numbers with pi",
21092
- forms: "pi"
21093
- }
21094
- };
21095
- function scoreInputNumber(userInput, rubric, strings) {
21096
- if (rubric.answerType == null) {
21097
- rubric.answerType = "number";
21098
- }
21099
-
21100
- // note(matthewc): this will get immediately parsed again by
21101
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21102
- // expected here
21103
- const stringValue = `${rubric.value}`;
21104
- const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21105
- simplify: rubric.simplify,
21106
- inexact: rubric.inexact || undefined,
21107
- maxError: rubric.maxError,
21108
- forms: answerTypes[rubric.answerType].forms
21109
- });
21110
-
21111
- // We may have received TeX; try to parse it before grading.
21112
- // If `currentValue` is not TeX, this should be a no-op.
21113
- const currentValue = ParseTex$1(userInput.currentValue);
21114
- const result = val(currentValue);
21115
- if (result.empty) {
21116
- return {
21117
- type: "invalid",
21118
- message: result.message
21119
- };
21120
- }
21121
- return {
21122
- type: "points",
21123
- earned: result.correct ? 1 : 0,
21124
- total: 1,
21125
- message: result.message
21126
- };
21127
- }
21128
-
21129
20459
  const formExamples$1 = {
21130
20460
  integer: function (options, strings) {
21131
20461
  return strings.integerExample;
@@ -21228,7 +20558,7 @@ class InputNumber extends React.Component {
21228
20558
  strings
21229
20559
  } = this.context;
21230
20560
  const type = this.props.answerType;
21231
- const forms = answerTypes[type].forms.split(/\s*,\s*/);
20561
+ const forms = inputNumberAnswerTypes[type].forms.split(/\s*,\s*/);
21232
20562
  const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
21233
20563
  return [strings.yourAnswer].concat(examples);
21234
20564
  }
@@ -21358,140 +20688,6 @@ const getPromptJSON$r = (renderProps, userInput) => {
21358
20688
  };
21359
20689
  };
21360
20690
 
21361
- const ParseTex = TexWrangler.parseTex;
21362
- const answerFormButtons = [{
21363
- title: "Integers",
21364
- value: "integer",
21365
- content: "6"
21366
- }, {
21367
- title: "Decimals",
21368
- value: "decimal",
21369
- content: "0.75"
21370
- }, {
21371
- title: "Proper fractions",
21372
- value: "proper",
21373
- content: "\u2157"
21374
- }, {
21375
- title: "Improper fractions",
21376
- value: "improper",
21377
- content: "\u2077\u2044\u2084"
21378
- }, {
21379
- title: "Mixed numbers",
21380
- value: "mixed",
21381
- content: "1\u00BE"
21382
- }, {
21383
- title: "Numbers with \u03C0",
21384
- value: "pi",
21385
- content: "\u03C0"
21386
- }];
21387
-
21388
- // This function checks if the user inputted a percent value, parsing
21389
- // it as a number (and maybe scaling) so that it can be graded.
21390
- // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
21391
- // can accept several input forms with or without "%", the decision
21392
- // to parse based on the presence of "%" in the input, is so that we
21393
- // don't accidently scale the user typed value before grading, CP-930.
21394
- function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
21395
- // If the input value is not a string ending with "%", then there's
21396
- // nothing more to do. The value will be graded as inputted by user.
21397
- if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
21398
- return inputValue;
21399
- }
21400
- const value = parseFloat(inputValue.slice(0, -1));
21401
- // If the input value stripped of the "%" cannot be parsed as a
21402
- // number (the slice is not really necessary for parseFloat to work
21403
- // if the string starts with a number) then return the original
21404
- // input for grading.
21405
- if (isNaN(value)) {
21406
- return inputValue;
21407
- }
21408
-
21409
- // Next, if all correct answers are in the range of |0,1| then we
21410
- // scale the user typed value. We assume this is the correct thing
21411
- // to do since the input value ends with "%".
21412
- if (normalizedAnswerExpected) {
21413
- return value / 100;
21414
- }
21415
-
21416
- // Otherwise, we return input value (number) stripped of the "%".
21417
- return value;
21418
- }
21419
- function scoreNumericInput(userInput, rubric, strings) {
21420
- var _matchedAnswer$messag;
21421
- const defaultAnswerForms = answerFormButtons.map(e => e["value"])
21422
- // Don't default to validating the answer as a pi answer
21423
- // if answerForm isn't set on the answer
21424
- // https://khanacademy.atlassian.net/browse/LC-691
21425
- .filter(e => e !== "pi");
21426
- const createValidator = answer => {
21427
- var _answer$answerForms;
21428
- const stringAnswer = `${answer.value}`;
21429
-
21430
- // Always validate against the provided answer forms (pi, decimal, etc.)
21431
- const validatorForms = [...((_answer$answerForms = answer.answerForms) != null ? _answer$answerForms : [])];
21432
-
21433
- // When an answer is set to strict, we validate using ONLY
21434
- // the provided answerForms. If strict is false, or if there
21435
- // were no provided answer forms, we will include all
21436
- // of the default answer forms in our validator.
21437
- if (!answer.strict || validatorForms.length === 0) {
21438
- validatorForms.push(...defaultAnswerForms);
21439
- }
21440
- return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
21441
- message: answer.message,
21442
- simplify: answer.status === "correct" ? answer.simplify : "optional",
21443
- inexact: true,
21444
- // TODO(merlob) backfill / delete
21445
- maxError: answer.maxError,
21446
- forms: validatorForms
21447
- });
21448
- };
21449
-
21450
- // We may have received TeX; try to parse it before grading.
21451
- // If `currentValue` is not TeX, this should be a no-op.
21452
- const currentValue = ParseTex(userInput.currentValue);
21453
- const normalizedAnswerExpected = rubric.answers.filter(answer => answer.status === "correct").every(answer => answer.value != null && Math.abs(answer.value) <= 1);
21454
-
21455
- // The coefficient is an attribute of the widget
21456
- let localValue = currentValue;
21457
- if (rubric.coefficient) {
21458
- if (!localValue) {
21459
- localValue = 1;
21460
- } else if (localValue === "-") {
21461
- localValue = -1;
21462
- }
21463
- }
21464
- const matchedAnswer = rubric.answers.map(answer => {
21465
- const validateFn = createValidator(answer);
21466
- const score = validateFn(maybeParsePercentInput(localValue, normalizedAnswerExpected));
21467
- return _extends({}, answer, {
21468
- score
21469
- });
21470
- }).find(answer => {
21471
- // NOTE: "answer.score.correct" indicates a match via the validate function.
21472
- // It does NOT indicate that the answer itself is correct.
21473
- return answer.score.correct || answer.status === "correct" && answer.score.empty;
21474
- });
21475
- const result = (matchedAnswer == null ? void 0 : matchedAnswer.status) === "correct" ? matchedAnswer.score : {
21476
- empty: (matchedAnswer == null ? void 0 : matchedAnswer.status) === "ungraded",
21477
- correct: (matchedAnswer == null ? void 0 : matchedAnswer.status) === "correct",
21478
- message: (_matchedAnswer$messag = matchedAnswer == null ? void 0 : matchedAnswer.message) != null ? _matchedAnswer$messag : null,
21479
- guess: localValue
21480
- };
21481
- if (result.empty) {
21482
- return {
21483
- type: "invalid",
21484
- message: result.message
21485
- };
21486
- }
21487
- return {
21488
- type: "points",
21489
- earned: result.correct ? 1 : 0,
21490
- total: 1,
21491
- message: result.message
21492
- };
21493
- }
21494
-
21495
20691
  const formExamples = {
21496
20692
  integer: (form, strings) => strings.integerExample,
21497
20693
  proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
@@ -25479,72 +24675,6 @@ const styles$o = StyleSheet.create({
25479
24675
  }
25480
24676
  });
25481
24677
 
25482
- /**
25483
- * Checks if the user has selected at least one option. Additional validation
25484
- * is done in scoreRadio to check if the number of selected options is correct
25485
- * and if the user has selected both a correct option and the "none of the above"
25486
- * option.
25487
- * @param userInput
25488
- * @see `scoreRadio` for the additional validation logic and the scoring logic.
25489
- */
25490
- function validateRadio(userInput) {
25491
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25492
- return sum + (selected ? 1 : 0);
25493
- }, 0);
25494
- if (numSelected === 0) {
25495
- return {
25496
- type: "invalid",
25497
- message: null
25498
- };
25499
- }
25500
- return null;
25501
- }
25502
-
25503
- function scoreRadio(userInput, rubric, strings) {
25504
- const validationError = validateRadio(userInput);
25505
- if (validationError) {
25506
- return validationError;
25507
- }
25508
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
25509
- return sum + (selected ? 1 : 0);
25510
- }, 0);
25511
- const numCorrect = rubric.choices.reduce((sum, currentChoice) => {
25512
- return currentChoice.correct ? sum + 1 : sum;
25513
- }, 0);
25514
- if (numCorrect > 1 && numSelected !== numCorrect) {
25515
- return {
25516
- type: "invalid",
25517
- message: strings.chooseCorrectNum
25518
- };
25519
- // If NOTA and some other answer are checked, ...
25520
- }
25521
-
25522
- const noneOfTheAboveSelected = rubric.choices.some((choice, index) => choice.isNoneOfTheAbove && userInput.choicesSelected[index]);
25523
- if (noneOfTheAboveSelected && numSelected > 1) {
25524
- return {
25525
- type: "invalid",
25526
- message: strings.notNoneOfTheAbove
25527
- };
25528
- }
25529
- const correct = userInput.choicesSelected.every((selected, i) => {
25530
- let isCorrect;
25531
- if (rubric.choices[i].isNoneOfTheAbove) {
25532
- isCorrect = rubric.choices.every((choice, j) => {
25533
- return i === j || !choice.correct;
25534
- });
25535
- } else {
25536
- isCorrect = !!rubric.choices[i].correct;
25537
- }
25538
- return isCorrect === selected;
25539
- });
25540
- return {
25541
- type: "points",
25542
- earned: correct ? 1 : 0,
25543
- total: 1,
25544
- message: null
25545
- };
25546
- }
25547
-
25548
24678
  // RenderProps is the return type for radio.jsx#transform
25549
24679
 
25550
24680
  class Radio$1 extends React.Component {
@@ -25650,7 +24780,7 @@ class Radio$1 extends React.Component {
25650
24780
  choiceStates
25651
24781
  } = this.props;
25652
24782
  if (choiceStates) {
25653
- const score = scoreRadio(this.getUserInput(), rubric, this.context.strings);
24783
+ const score = scoreRadio(this.getUserInput(), rubric);
25654
24784
  const widgetCorrect = score.type === "points" && score.total === score.earned;
25655
24785
  const newStates = choiceStates.map(state => _extends({}, state, {
25656
24786
  highlighted: state.selected,
@@ -26037,44 +25167,6 @@ function getCategorizerPublicWidgetOptions(options) {
26037
25167
  };
26038
25168
  }
26039
25169
 
26040
- /**
26041
- * Checks userInput from the categorizer widget to see if the user has selected
26042
- * a category for each item.
26043
- * @param userInput - The user's input corresponding to an array of indices that
26044
- * represent the selected category for each row/item.
26045
- * @param validationData - An array of strings corresponding to each row/item
26046
- * @param strings - Used to provide a validation message
26047
- */
26048
- function validateCategorizer(userInput, validationData, strings) {
26049
- const incomplete = validationData.items.some((_, i) => userInput.values[i] == null);
26050
- if (incomplete) {
26051
- return {
26052
- type: "invalid",
26053
- message: strings.invalidSelection
26054
- };
26055
- }
26056
- return null;
26057
- }
26058
-
26059
- function scoreCategorizer(userInput, scoringData, strings) {
26060
- const validationError = validateCategorizer(userInput, scoringData, strings);
26061
- if (validationError) {
26062
- return validationError;
26063
- }
26064
- let allCorrect = true;
26065
- scoringData.values.forEach((value, i) => {
26066
- if (userInput.values[i] !== value) {
26067
- allCorrect = false;
26068
- }
26069
- });
26070
- return {
26071
- type: "points",
26072
- earned: allCorrect ? 1 : 0,
26073
- total: 1,
26074
- message: null
26075
- };
26076
- }
26077
-
26078
25170
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
26079
25171
  class Categorizer extends React.Component {
26080
25172
  constructor(..._args) {
@@ -26316,31 +25408,6 @@ const getPromptJSON$m = () => {
26316
25408
  return getUnsupportedPromptJSON("cs-program");
26317
25409
  };
26318
25410
 
26319
- function scoreCSProgram(state) {
26320
- // The iframe can tell us whether it's correct or incorrect,
26321
- // and pass an optional message
26322
- if (state.status === "correct") {
26323
- return {
26324
- type: "points",
26325
- earned: 1,
26326
- total: 1,
26327
- message: state.message || null
26328
- };
26329
- }
26330
- if (state.status === "incorrect") {
26331
- return {
26332
- type: "points",
26333
- earned: 0,
26334
- total: 1,
26335
- message: state.message || null
26336
- };
26337
- }
26338
- return {
26339
- type: "invalid",
26340
- message: "Keep going, you're not there yet!"
26341
- };
26342
- }
26343
-
26344
25411
  /**
26345
25412
  * This widget is for embedding Khan Academy CS programs.
26346
25413
  */
@@ -26613,34 +25680,6 @@ const getPromptJSON$k = (renderProps, userInput) => {
26613
25680
  };
26614
25681
  };
26615
25682
 
26616
- /**
26617
- * Checks if the user has selected an item from the dropdown before scoring.
26618
- * This is shown with a userInput value / index other than 0.
26619
- */
26620
- function validateDropdown(userInput) {
26621
- if (userInput.value === 0) {
26622
- return {
26623
- type: "invalid",
26624
- message: null
26625
- };
26626
- }
26627
- return null;
26628
- }
26629
-
26630
- function scoreDropdown(userInput, rubric) {
26631
- const validationError = validateDropdown(userInput);
26632
- if (validationError) {
26633
- return validationError;
26634
- }
26635
- const correct = rubric.choices[userInput.value - 1].correct;
26636
- return {
26637
- type: "points",
26638
- earned: correct ? 1 : 0,
26639
- total: 1,
26640
- message: null
26641
- };
26642
- }
26643
-
26644
25683
  class Dropdown extends React.Component {
26645
25684
  constructor(...args) {
26646
25685
  super(...args);
@@ -27703,408 +26742,21 @@ const getPromptJSON$g = (renderProps, userInput) => {
27703
26742
  };
27704
26743
  };
27705
26744
 
27706
- // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
27707
- const Plot$1 = Graphie.Plot;
26745
+ const movableTypeToComponent = {
26746
+ // @ts-expect-error - TS2339 - Property 'Plot' does not exist on type 'typeof Graphie'.
26747
+ PLOT: Graphie.Plot,
26748
+ // @ts-expect-error - TS2339 - Property 'Parabola' does not exist on type 'typeof Graphie'.
26749
+ PARABOLA: Graphie.Parabola,
26750
+ // @ts-expect-error - TS2339 - Property 'Sinusoid' does not exist on type 'typeof Graphie'.
26751
+ SINUSOID: Graphie.Sinusoid
26752
+ };
27708
26753
  const DEFAULT_BACKGROUND_IMAGE = {
27709
26754
  url: null
27710
26755
  };
27711
-
27712
- // TODO(charlie): These really need to go into a utility file as they're being
27713
- // used by both interactive-graph and now grapher.
27714
- function canonicalSineCoefficients(coeffs) {
27715
- // For a curve of the form f(x) = a * Sin(b * x - c) + d,
27716
- // this function ensures that a, b > 0, and c is its
27717
- // smallest possible positive value.
27718
- let amplitude = coeffs[0];
27719
- let angularFrequency = coeffs[1];
27720
- let phase = coeffs[2];
27721
- const verticalOffset = coeffs[3];
27722
-
27723
- // Guarantee a > 0
27724
- if (amplitude < 0) {
27725
- amplitude *= -1;
27726
- angularFrequency *= -1;
27727
- phase *= -1;
27728
- }
27729
- const period = 2 * Math.PI;
27730
- // Guarantee b > 0
27731
- if (angularFrequency < 0) {
27732
- angularFrequency *= -1;
27733
- phase *= -1;
27734
- phase += period / 2;
27735
- }
27736
-
27737
- // Guarantee c is smallest possible positive value
27738
- while (phase > 0) {
27739
- phase -= period;
27740
- }
27741
- while (phase < 0) {
27742
- phase += period;
27743
- }
27744
- return [amplitude, angularFrequency, phase, verticalOffset];
27745
- }
27746
- function canonicalTangentCoefficients(coeffs) {
27747
- // For a curve of the form f(x) = a * Tan(b * x - c) + d,
27748
- // this function ensures that a, b > 0, and c is its
27749
- // smallest possible positive value.
27750
- let amplitude = coeffs[0];
27751
- let angularFrequency = coeffs[1];
27752
- let phase = coeffs[2];
27753
- const verticalOffset = coeffs[3];
27754
-
27755
- // Guarantee a > 0
27756
- if (amplitude < 0) {
27757
- amplitude *= -1;
27758
- angularFrequency *= -1;
27759
- phase *= -1;
27760
- }
27761
- const period = Math.PI;
27762
- // Guarantee b > 0
27763
- if (angularFrequency < 0) {
27764
- angularFrequency *= -1;
27765
- phase *= -1;
27766
- phase += period / 2;
27767
- }
27768
-
27769
- // Guarantee c is smallest possible positive value
27770
- while (phase > 0) {
27771
- phase -= period;
27772
- }
27773
- while (phase < 0) {
27774
- phase += period;
27775
- }
27776
- return [amplitude, angularFrequency, phase, verticalOffset];
27777
- }
27778
- const PlotDefaults = {
27779
- areEqual: function (coeffs1, coeffs2) {
27780
- return Util.deepEq(coeffs1, coeffs2);
27781
- },
27782
- Movable: Plot$1,
27783
- getPropsForCoeffs: function (coeffs) {
27784
- return {
27785
- // @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; }'.
27786
- fn: _.partial(this.getFunctionForCoeffs, coeffs)
27787
- };
27788
- }
27789
- };
27790
- const Linear = _.extend({}, PlotDefaults, {
27791
- url: "https://ka-perseus-graphie.s3.amazonaws.com/67aaf581e6d9ef9038c10558a1f70ac21c11c9f8.png",
27792
- defaultCoords: [[0.25, 0.75], [0.75, 0.75]],
27793
- getCoefficients: function (coords) {
27794
- const p1 = coords[0];
27795
- const p2 = coords[1];
27796
- const denom = p2[0] - p1[0];
27797
- const num = p2[1] - p1[1];
27798
- if (denom === 0) {
27799
- return;
27800
- }
27801
- const m = num / denom;
27802
- const b = p2[1] - m * p2[0];
27803
- return [m, b];
27804
- },
27805
- getFunctionForCoeffs: function (coeffs, x) {
27806
- const m = coeffs[0];
27807
- const b = coeffs[1];
27808
- return m * x + b;
27809
- },
27810
- getEquationString: function (coords) {
27811
- const coeffs = this.getCoefficients(coords);
27812
- const m = coeffs[0];
27813
- const b = coeffs[1];
27814
- return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
27815
- }
27816
- });
27817
- const Quadratic = _.extend({}, PlotDefaults, {
27818
- url: "https://ka-perseus-graphie.s3.amazonaws.com/e23d36e6fc29ee37174e92c9daba2a66677128ab.png",
27819
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
27820
- // @ts-expect-error - TS2339 - Property 'Parabola' does not exist on type 'typeof Graphie'.
27821
- Movable: Graphie.Parabola,
27822
- getCoefficients: function (coords) {
27823
- const p1 = coords[0];
27824
- const p2 = coords[1];
27825
-
27826
- // Parabola with vertex (h, k) has form: y = a * (h - k)^2 + k
27827
- const h = p1[0];
27828
- const k = p1[1];
27829
-
27830
- // Use these to calculate familiar a, b, c
27831
- const a = (p2[1] - k) / ((p2[0] - h) * (p2[0] - h));
27832
- const b = -2 * h * a;
27833
- const c = a * h * h + k;
27834
- return [a, b, c];
27835
- },
27836
- getFunctionForCoeffs: function (coeffs, x) {
27837
- const a = coeffs[0];
27838
- const b = coeffs[1];
27839
- const c = coeffs[2];
27840
- return (a * x + b) * x + c;
27841
- },
27842
- getPropsForCoeffs: function (coeffs) {
27843
- return {
27844
- a: coeffs[0],
27845
- b: coeffs[1],
27846
- c: coeffs[2]
27847
- };
27848
- },
27849
- getEquationString: function (coords) {
27850
- const coeffs = this.getCoefficients(coords);
27851
- const a = coeffs[0];
27852
- const b = coeffs[1];
27853
- const c = coeffs[2];
27854
- return "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3);
27855
- }
27856
- });
27857
- const Sinusoid = _.extend({}, PlotDefaults, {
27858
- url: "https://ka-perseus-graphie.s3.amazonaws.com/3d68e7718498475f53b206c2ab285626baf8857e.png",
27859
- defaultCoords: [[0.5, 0.5], [0.6, 0.6]],
27860
- // @ts-expect-error - TS2339 - Property 'Sinusoid' does not exist on type 'typeof Graphie'.
27861
- Movable: Graphie.Sinusoid,
27862
- getCoefficients: function (coords) {
27863
- const p1 = coords[0];
27864
- const p2 = coords[1];
27865
- const a = p2[1] - p1[1];
27866
- const b = Math.PI / (2 * (p2[0] - p1[0]));
27867
- const c = p1[0] * b;
27868
- const d = p1[1];
27869
- return [a, b, c, d];
27870
- },
27871
- getFunctionForCoeffs: function (coeffs, x) {
27872
- const a = coeffs[0];
27873
- const b = coeffs[1];
27874
- const c = coeffs[2];
27875
- const d = coeffs[3];
27876
- return a * Math.sin(b * x - c) + d;
27877
- },
27878
- getPropsForCoeffs: function (coeffs) {
27879
- return {
27880
- a: coeffs[0],
27881
- b: coeffs[1],
27882
- c: coeffs[2],
27883
- d: coeffs[3]
27884
- };
27885
- },
27886
- getEquationString: function (coords) {
27887
- const coeffs = this.getCoefficients(coords);
27888
- const a = coeffs[0];
27889
- const b = coeffs[1];
27890
- const c = coeffs[2];
27891
- const d = coeffs[3];
27892
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
27893
- },
27894
- areEqual: function (coeffs1, coeffs2) {
27895
- return Util.deepEq(canonicalSineCoefficients(coeffs1), canonicalSineCoefficients(coeffs2));
27896
- }
27897
- });
27898
- const Tangent = _.extend({}, PlotDefaults, {
27899
- url: "https://ka-perseus-graphie.s3.amazonaws.com/7db80d23c35214f98659fe1cf0765811c1bbfbba.png",
27900
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
27901
- getCoefficients: function (coords) {
27902
- const p1 = coords[0];
27903
- const p2 = coords[1];
27904
- const a = p2[1] - p1[1];
27905
- const b = Math.PI / (4 * (p2[0] - p1[0]));
27906
- const c = p1[0] * b;
27907
- const d = p1[1];
27908
- return [a, b, c, d];
27909
- },
27910
- getFunctionForCoeffs: function (coeffs, x) {
27911
- const a = coeffs[0];
27912
- const b = coeffs[1];
27913
- const c = coeffs[2];
27914
- const d = coeffs[3];
27915
- return a * Math.tan(b * x - c) + d;
27916
- },
27917
- getEquationString: function (coords) {
27918
- const coeffs = this.getCoefficients(coords);
27919
- const a = coeffs[0];
27920
- const b = coeffs[1];
27921
- const c = coeffs[2];
27922
- const d = coeffs[3];
27923
- return "y = " + a.toFixed(3) + " sin(" + b.toFixed(3) + "x - " + c.toFixed(3) + ") + " + d.toFixed(3);
27924
- },
27925
- areEqual: function (coeffs1, coeffs2) {
27926
- return Util.deepEq(canonicalTangentCoefficients(coeffs1), canonicalTangentCoefficients(coeffs2));
27927
- }
27928
- });
27929
- const Exponential = _.extend({}, PlotDefaults, {
27930
- url: "https://ka-perseus-graphie.s3.amazonaws.com/9cbfad55525e3ce755a31a631b074670a5dad611.png",
27931
- defaultCoords: [[0.5, 0.55], [0.75, 0.75]],
27932
- defaultAsymptote: [[0, 0.5], [1.0, 0.5]],
27933
- /**
27934
- * Add extra constraints for movement of the points or asymptote (below):
27935
- * newCoord: [x, y]
27936
- * The end position of the point or asymptote endpoint
27937
- * oldCoord: [x, y]
27938
- * The old position of the point or asymptote endpoint
27939
- * coords:
27940
- * An array of coordinates representing the proposed end configuration
27941
- * of the plot coordinates.
27942
- * asymptote:
27943
- * An array of coordinates representing the proposed end configuration
27944
- * of the asymptote.
27945
- *
27946
- * Return: either a coordinate (to be used as the resulting coordinate of
27947
- * the move) or a boolean, where `true` uses newCoord as the resulting
27948
- * coordinate, and `false` uses oldCoord as the resulting coordinate.
27949
- */
27950
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
27951
- const y = asymptote[0][1];
27952
- return _.all(coords, coord => coord[1] !== y);
27953
- },
27954
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
27955
- const y = newCoord[1];
27956
- const isValid = _.all(coords, coord => coord[1] > y) || _.all(coords, coord => coord[1] < y);
27957
- if (isValid) {
27958
- return [oldCoord[0], y];
27959
- }
27960
- // Snap the asymptote as close as possible, i.e., if the user moves
27961
- // the mouse really quickly into an invalid region
27962
- const oldY = oldCoord[1];
27963
- const wasBelow = _.all(coords, coord => coord[1] > oldY);
27964
- if (wasBelow) {
27965
- const bottomMost = _.min(_.map(coords, coord => coord[1]));
27966
- return [oldCoord[0], bottomMost - graph.snapStep[1]];
27967
- }
27968
- const topMost = _.max(_.map(coords, coord => coord[1]));
27969
- return [oldCoord[0], topMost + graph.snapStep[1]];
27970
- },
27971
- allowReflectOverAsymptote: true,
27972
- getCoefficients: function (coords, asymptote) {
27973
- const p1 = coords[0];
27974
- const p2 = coords[1];
27975
- const c = asymptote[0][1];
27976
- const b = Math.log((p1[1] - c) / (p2[1] - c)) / (p1[0] - p2[0]);
27977
- const a = (p1[1] - c) / Math.exp(b * p1[0]);
27978
- return [a, b, c];
27979
- },
27980
- getFunctionForCoeffs: function (coeffs, x) {
27981
- const a = coeffs[0];
27982
- const b = coeffs[1];
27983
- const c = coeffs[2];
27984
- return a * Math.exp(b * x) + c;
27985
- },
27986
- getEquationString: function (coords, asymptote) {
27987
- if (!asymptote) {
27988
- return null;
27989
- }
27990
- const coeffs = this.getCoefficients(coords, asymptote);
27991
- const a = coeffs[0];
27992
- const b = coeffs[1];
27993
- const c = coeffs[2];
27994
- return "y = " + a.toFixed(3) + "e^(" + b.toFixed(3) + "x) + " + c.toFixed(3);
27995
- }
27996
- });
27997
- const Logarithm = _.extend({}, PlotDefaults, {
27998
- url: "https://ka-perseus-graphie.s3.amazonaws.com/f6491e99d34af34d924bfe0231728ad912068dc3.png",
27999
- defaultCoords: [[0.55, 0.5], [0.75, 0.75]],
28000
- defaultAsymptote: [[0.5, 0], [0.5, 1.0]],
28001
- extraCoordConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28002
- const x = asymptote[0][0];
28003
- return _.all(coords, coord => coord[0] !== x) && coords[0][1] !== coords[1][1];
28004
- },
28005
- extraAsymptoteConstraint: function (newCoord, oldCoord, coords, asymptote, graph) {
28006
- const x = newCoord[0];
28007
- const isValid = _.all(coords, coord => coord[0] > x) || _.all(coords, coord => coord[0] < x);
28008
- if (isValid) {
28009
- return [x, oldCoord[1]];
28010
- }
28011
- // Snap the asymptote as close as possible, i.e., if the user moves
28012
- // the mouse really quickly into an invalid region
28013
- const oldX = oldCoord[0];
28014
- const wasLeft = _.all(coords, coord => coord[0] > oldX);
28015
- if (wasLeft) {
28016
- const leftMost = _.min(_.map(coords, coord => coord[0]));
28017
- return [leftMost - graph.snapStep[0], oldCoord[1]];
28018
- }
28019
- const rightMost = _.max(_.map(coords, coord => coord[0]));
28020
- return [rightMost + graph.snapStep[0], oldCoord[1]];
28021
- },
28022
- allowReflectOverAsymptote: true,
28023
- getCoefficients: function (coords, asymptote) {
28024
- // It's easiest to calculate the logarithm's coefficients by thinking
28025
- // about it as the inverse of the exponential, so we flip x and y and
28026
- // perform some algebra on the coefficients. This also unifies the
28027
- // logic between the two 'models'.
28028
- const flip = coord => [coord[1], coord[0]];
28029
- const inverseCoeffs = Exponential.getCoefficients(_.map(coords, flip), _.map(asymptote, flip));
28030
- if (inverseCoeffs) {
28031
- const c = -inverseCoeffs[2] / inverseCoeffs[0];
28032
- const b = 1 / inverseCoeffs[0];
28033
- const a = 1 / inverseCoeffs[1];
28034
- return [a, b, c];
28035
- }
28036
- },
28037
- getFunctionForCoeffs: function (coeffs, x, asymptote) {
28038
- const a = coeffs[0];
28039
- const b = coeffs[1];
28040
- const c = coeffs[2];
28041
- return a * Math.log(b * x + c);
28042
- },
28043
- getEquationString: function (coords, asymptote) {
28044
- if (!asymptote) {
28045
- return null;
28046
- }
28047
- const coeffs = this.getCoefficients(coords, asymptote);
28048
- const a = coeffs[0];
28049
- const b = coeffs[1];
28050
- const c = coeffs[2];
28051
- return "y = ln(" + a.toFixed(3) + "x + " + b.toFixed(3) + ") + " + c.toFixed(3);
28052
- }
28053
- });
28054
- const AbsoluteValue = _.extend({}, PlotDefaults, {
28055
- url: "https://ka-perseus-graphie.s3.amazonaws.com/8256a630175a0cb1d11de223d6de0266daf98721.png",
28056
- defaultCoords: [[0.5, 0.5], [0.75, 0.75]],
28057
- getCoefficients: function (coords) {
28058
- const p1 = coords[0];
28059
- const p2 = coords[1];
28060
- const denom = p2[0] - p1[0];
28061
- const num = p2[1] - p1[1];
28062
- if (denom === 0) {
28063
- return;
28064
- }
28065
- let m = Math.abs(num / denom);
28066
- if (p2[1] < p1[1]) {
28067
- m *= -1;
28068
- }
28069
- const horizontalOffset = p1[0];
28070
- const verticalOffset = p1[1];
28071
- return [m, horizontalOffset, verticalOffset];
28072
- },
28073
- getFunctionForCoeffs: function (coeffs, x) {
28074
- const m = coeffs[0];
28075
- const horizontalOffset = coeffs[1];
28076
- const verticalOffset = coeffs[2];
28077
- return m * Math.abs(x - horizontalOffset) + verticalOffset;
28078
- },
28079
- getEquationString: function (coords) {
28080
- const coeffs = this.getCoefficients(coords);
28081
- const m = coeffs[0];
28082
- const horizontalOffset = coeffs[1];
28083
- const verticalOffset = coeffs[2];
28084
- return "y = " + m.toFixed(3) + "| x - " + horizontalOffset.toFixed(3) + "| + " + verticalOffset.toFixed(3);
28085
- }
28086
- });
28087
-
28088
- /* Utility functions for dealing with graphing interfaces. */
28089
- const functionTypeMapping = {
28090
- linear: Linear,
28091
- quadratic: Quadratic,
28092
- sinusoid: Sinusoid,
28093
- tangent: Tangent,
28094
- exponential: Exponential,
28095
- logarithm: Logarithm,
28096
- absolute_value: AbsoluteValue
28097
- };
28098
- const allTypes = _.keys(functionTypeMapping);
28099
- function functionForType(type) {
28100
- // @ts-expect-error: TypeScript doesn't know how to use deal with generics
28101
- // and conditional types in this way.
28102
- return functionTypeMapping[type];
28103
- }
28104
26756
  const getEquationString = props => {
28105
26757
  const plot = props.plot;
28106
26758
  if (plot.type && plot.coords) {
28107
- const handler = functionForType(plot.type);
26759
+ const handler = GrapherUtil$1.functionForType(plot.type);
28108
26760
  const result = handler.getEquationString(plot.coords, plot.asymptote);
28109
26761
  return result || "";
28110
26762
  }
@@ -28162,7 +26814,7 @@ const defaultPlotProps = (type, graph) => {
28162
26814
  // extra information. It would be valid to always submit the default
28163
26815
  // widget before even reading the question; you can't lose, but you
28164
26816
  // might get a free win.
28165
- const model = functionForType(type);
26817
+ const model = GrapherUtil$1.functionForType(type);
28166
26818
  const defaultAsymptote = "defaultAsymptote" in model ? model.defaultAsymptote : null;
28167
26819
  const gridStep = [1, 1];
28168
26820
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
@@ -28208,70 +26860,12 @@ const typeToButton = type => {
28208
26860
  value: type,
28209
26861
  title: capitalized,
28210
26862
  content: /*#__PURE__*/React.createElement("img", {
28211
- src: staticUrl(functionForType(type).url),
26863
+ src: staticUrl(GrapherUtil$1.functionForType(type).url),
28212
26864
  alt: capitalized
28213
26865
  })
28214
26866
  };
28215
26867
  };
28216
26868
 
28217
- function getCoefficientsByType(data) {
28218
- if (data.coords == null) {
28219
- return undefined;
28220
- }
28221
- if (data.type === "exponential" || data.type === "logarithm") {
28222
- const grader = functionForType(data.type);
28223
- return grader.getCoefficients(data.coords, data.asymptote);
28224
- } else if (data.type === "linear" || data.type === "quadratic" || data.type === "absolute_value" || data.type === "sinusoid" || data.type === "tangent") {
28225
- const grader = functionForType(data.type);
28226
- return grader.getCoefficients(data.coords);
28227
- } else {
28228
- throw new PerseusError("Invalid grapher type", Errors.InvalidInput);
28229
- }
28230
- }
28231
- function scoreGrapher(userInput, rubric) {
28232
- if (userInput.type !== rubric.correct.type) {
28233
- return {
28234
- type: "points",
28235
- earned: 0,
28236
- total: 1,
28237
- message: null
28238
- };
28239
- }
28240
-
28241
- // We haven't moved the coords
28242
- if (userInput.coords == null) {
28243
- return {
28244
- type: "invalid",
28245
- message: null
28246
- };
28247
- }
28248
-
28249
- // Get new function handler for grading
28250
- const grader = functionForType(userInput.type);
28251
- const guessCoeffs = getCoefficientsByType(userInput);
28252
- const correctCoeffs = getCoefficientsByType(rubric.correct);
28253
- if (guessCoeffs == null || correctCoeffs == null) {
28254
- return {
28255
- type: "invalid",
28256
- message: null
28257
- };
28258
- }
28259
- if (grader.areEqual(guessCoeffs, correctCoeffs)) {
28260
- return {
28261
- type: "points",
28262
- earned: 1,
28263
- total: 1,
28264
- message: null
28265
- };
28266
- }
28267
- return {
28268
- type: "points",
28269
- earned: 0,
28270
- total: 1,
28271
- message: null
28272
- };
28273
- }
28274
-
28275
26869
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
28276
26870
  const MovablePoint$3 = Graphie.MovablePoint;
28277
26871
  // @ts-expect-error - TS2339 - Property 'MovableLine' does not exist on type 'typeof Graphie'.
@@ -28319,7 +26913,8 @@ class FunctionGrapher extends React.Component {
28319
26913
  return;
28320
26914
  }
28321
26915
  const functionProps = model.getPropsForCoeffs(coeffs, xRange);
28322
- return /*#__PURE__*/React.createElement(model.Movable, _extends({}, functionProps, {
26916
+ const Movable = movableTypeToComponent[model.movable];
26917
+ return /*#__PURE__*/React.createElement(Movable, _extends({}, functionProps, {
28323
26918
  key: this.props.model.url,
28324
26919
  range: xRange,
28325
26920
  style: style
@@ -28335,8 +26930,8 @@ class FunctionGrapher extends React.Component {
28335
26930
  return asymptote && /*#__PURE__*/React.createElement(MovableLine$2, {
28336
26931
  onMove: (newCoord, oldCoord) => {
28337
26932
  // Calculate and apply displacement
28338
- const delta = vector$1.subtract(newCoord, oldCoord);
28339
- const newAsymptote = _.map(this._asymptote(), coord => vector$1.add(coord, delta));
26933
+ const delta = vector$3.subtract(newCoord, oldCoord);
26934
+ const newAsymptote = _.map(this._asymptote(), coord => vector$3.add(coord, delta));
28340
26935
  this.props.onChange({
28341
26936
  asymptote: newAsymptote
28342
26937
  });
@@ -28347,8 +26942,8 @@ class FunctionGrapher extends React.Component {
28347
26942
  // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
28348
26943
  Interactive2.MovableLine.constraints.snap(), (newCoord, oldCoord) => {
28349
26944
  // Calculate and apply proposed displacement
28350
- const delta = vector$1.subtract(newCoord, oldCoord);
28351
- const proposedAsymptote = _.map(this._asymptote(), coord => vector$1.add(coord, delta));
26945
+ const delta = vector$3.subtract(newCoord, oldCoord);
26946
+ const proposedAsymptote = _.map(this._asymptote(), coord => vector$3.add(coord, delta));
28352
26947
  // Verify that resulting asymptote is valid for graph
28353
26948
  if (model.extraAsymptoteConstraint) {
28354
26949
  return model.extraAsymptoteConstraint(newCoord, oldCoord, this._coords(), proposedAsymptote, graph);
@@ -28620,7 +27215,7 @@ class Grapher extends React.Component {
28620
27215
  setup: this._setupGraphie
28621
27216
  },
28622
27217
  onChange: this.handlePlotChanges,
28623
- model: type && functionForType(type),
27218
+ model: type && GrapherUtil$1.functionForType(type),
28624
27219
  coords: coords,
28625
27220
  asymptote: asymptote,
28626
27221
  static: this.props.static,
@@ -28857,31 +27452,6 @@ const getPromptJSON$e = () => {
28857
27452
  return getUnsupportedPromptJSON("iframe");
28858
27453
  };
28859
27454
 
28860
- function scoreIframe(userInput) {
28861
- // The iframe can tell us whether it's correct or incorrect,
28862
- // and pass an optional message
28863
- if (userInput.status === "correct") {
28864
- return {
28865
- type: "points",
28866
- earned: 1,
28867
- total: 1,
28868
- message: userInput.message || null
28869
- };
28870
- }
28871
- if (userInput.status === "incorrect") {
28872
- return {
28873
- type: "points",
28874
- earned: 0,
28875
- total: 1,
28876
- message: userInput.message || null
28877
- };
28878
- }
28879
- return {
28880
- type: "invalid",
28881
- message: "Keep going, you're not there yet!"
28882
- };
28883
- }
28884
-
28885
27455
  /**
28886
27456
  * This is an iframe widget. It is used for rendering an iframe that
28887
27457
  * then communicates its state via window.postMessage
@@ -29315,7 +27885,7 @@ class Interaction extends React.Component {
29315
27885
  const xDiff = this._eval("(" + options.endX + ")-(" + options.startX + ")");
29316
27886
  // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
29317
27887
  const yDiff = this._eval("(" + options.endY + ")-(" + options.startY + ")");
29318
- const endCoord = vector$1.add(startCoord, [xDiff, yDiff]);
27888
+ const endCoord = vector$3.add(startCoord, [xDiff, yDiff]);
29319
27889
  const variables = _.clone(this.state.variables);
29320
27890
  variables["x_" + options.startSubscript] = startCoord[0];
29321
27891
  variables["y_" + options.startSubscript] = startCoord[1];
@@ -29903,15 +28473,15 @@ class Graph extends React.Component {
29903
28473
  if (this.props.markings === "graph") {
29904
28474
  graphie.graphInit({
29905
28475
  range: range,
29906
- scale: vector$1.map(gridConfig, g => g.scale),
28476
+ scale: vector$3.map(gridConfig, g => g.scale),
29907
28477
  axisArrows: "<->",
29908
28478
  labelFormat: function (s) {
29909
28479
  return "\\small{" + s + "}";
29910
28480
  },
29911
28481
  gridStep: this.props.gridStep,
29912
- tickStep: vector$1.map(gridConfig, g => g.tickStep),
28482
+ tickStep: vector$3.map(gridConfig, g => g.tickStep),
29913
28483
  labelStep: 1,
29914
- unityLabels: vector$1.map(gridConfig, g => g.unityLabel),
28484
+ unityLabels: vector$3.map(gridConfig, g => g.unityLabel),
29915
28485
  isMobile: this.props.isMobile
29916
28486
  });
29917
28487
  graphie.label([0, range[1][1]], labels[1], this.props.isMobile ? "below right" : "above");
@@ -29919,7 +28489,7 @@ class Graph extends React.Component {
29919
28489
  } else if (this.props.markings === "grid") {
29920
28490
  graphie.graphInit({
29921
28491
  range: range,
29922
- scale: vector$1.map(gridConfig, g => g.scale),
28492
+ scale: vector$3.map(gridConfig, g => g.scale),
29923
28493
  gridStep: this.props.gridStep,
29924
28494
  axes: false,
29925
28495
  ticks: false,
@@ -29929,7 +28499,7 @@ class Graph extends React.Component {
29929
28499
  } else if (this.props.markings === "none") {
29930
28500
  graphie.init({
29931
28501
  range: range,
29932
- scale: vector$1.map(gridConfig, g => g.scale),
28502
+ scale: vector$3.map(gridConfig, g => g.scale),
29933
28503
  isMobile: this.props.isMobile
29934
28504
  });
29935
28505
  }
@@ -30002,7 +28572,7 @@ class Graph extends React.Component {
30002
28572
  }
30003
28573
  };
30004
28574
  this._getGridConfig = () => {
30005
- return vector$1.map(this.props.step, (step, i) => Util.gridDimensionConfig(step, this.props.range[i], this.props.box[i], this.props.gridStep[i]));
28575
+ return vector$3.map(this.props.step, (step, i) => Util.gridDimensionConfig(step, this.props.range[i], this.props.box[i], this.props.gridStep[i]));
30006
28576
  };
30007
28577
  this._updateProtractor = () => {
30008
28578
  if (this.protractor) {
@@ -30444,61 +29014,6 @@ function findIntersectionOfRays([[a, b], [c, d]], [[p, q], [r, s]]) {
30444
29014
  }
30445
29015
  }
30446
29016
 
30447
- // This file contains helper functions for working with angles.
30448
-
30449
- function convertDegreesToRadians(degrees) {
30450
- return degrees / 180 * Math.PI;
30451
- }
30452
-
30453
- // Returns a value between -180 and 180, inclusive. The angle is measured
30454
- // between the positive x-axis and the given vector.
30455
- function calculateAngleInDegrees([x, y]) {
30456
- return Math.atan2(y, x) * 180 / Math.PI;
30457
- }
30458
-
30459
- // Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
30460
- function polar(r, th) {
30461
- if (typeof r === "number") {
30462
- r = [r, r];
30463
- }
30464
- th = th * Math.PI / 180;
30465
- return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
30466
- }
30467
- // This function calculates the angle between two points and an optional vertex.
30468
- // If the vertex is not provided, the angle is measured between the two points.
30469
- // This does not account for reflex angles or clockwise position.
30470
- const getAngleFromVertex = (point, vertex) => {
30471
- const x = point[0] - vertex[0];
30472
- const y = point[1] - vertex[1];
30473
- if (!x && !y) {
30474
- return 0;
30475
- }
30476
- return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
30477
- };
30478
-
30479
- // This function calculates the clockwise angle between three points,
30480
- // and is used to generate the labels and equation strings of the
30481
- // current angle for the interactive graph.
30482
- const getClockwiseAngle = (coords, allowReflexAngles = false) => {
30483
- const coordsCopy = [...coords];
30484
- // The coords are saved as [point1, vertex, point2] in the interactive graph
30485
- const areClockwise = clockwise$1([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
30486
-
30487
- // We may need to reverse the coordinates if we allow
30488
- // reflex angles and the points are not in clockwise order.
30489
- const shouldReverseCoords = !areClockwise || allowReflexAngles;
30490
-
30491
- // Reverse the coordinates accordingly to ensure the angle is calculated correctly
30492
- const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
30493
-
30494
- // Calculate the angles between the two points and get the difference
30495
- // between the two angles to get the clockwise angle.
30496
- const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
30497
- const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
30498
- const angle = (startAngle + 360 - endAngle) % 360;
30499
- return angle;
30500
- };
30501
-
30502
29017
  const defaultGraphConfig = {
30503
29018
  range: [[0, 1], [0, 1]],
30504
29019
  tickStep: [1, 1],
@@ -31669,11 +30184,11 @@ const LockedFunction = props => {
31669
30184
  "aria-label": hasAria ? props.ariaLabel : undefined,
31670
30185
  "aria-hidden": !hasAria,
31671
30186
  role: "img"
31672
- }, directionalAxis === "x" && /*#__PURE__*/React.createElement(Plot$3.OfX, _extends({
30187
+ }, directionalAxis === "x" && /*#__PURE__*/React.createElement(Plot$2.OfX, _extends({
31673
30188
  y: x => equation.eval({
31674
30189
  x
31675
30190
  })
31676
- }, plotProps)), directionalAxis === "y" && /*#__PURE__*/React.createElement(Plot$3.OfY, _extends({
30191
+ }, plotProps)), directionalAxis === "y" && /*#__PURE__*/React.createElement(Plot$2.OfY, _extends({
31677
30192
  x: y => equation.eval({
31678
30193
  y
31679
30194
  })
@@ -31699,6 +30214,9 @@ function SVGLine(props) {
31699
30214
  });
31700
30215
  }
31701
30216
 
30217
+ const {
30218
+ calculateAngleInDegrees: calculateAngleInDegrees$3
30219
+ } = angles;
31702
30220
  function Vector(props) {
31703
30221
  const {
31704
30222
  tail,
@@ -31708,7 +30226,7 @@ function Vector(props) {
31708
30226
  } = props;
31709
30227
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
31710
30228
  const direction = vec.sub(tipPx, tailPx);
31711
- const angle = calculateAngleInDegrees(direction);
30229
+ const angle = calculateAngleInDegrees$3(direction);
31712
30230
  return /*#__PURE__*/React.createElement("g", {
31713
30231
  style: {
31714
30232
  stroke: color,
@@ -31725,6 +30243,14 @@ function Vector(props) {
31725
30243
  }));
31726
30244
  }
31727
30245
 
30246
+ function srFormatNumber(a, locale) {
30247
+ // adding zero here converts negative zero to positive zero.
30248
+ return (0 + a).toLocaleString(locale, {
30249
+ maximumFractionDigits: 3,
30250
+ useGrouping: false // no thousands separators
30251
+ });
30252
+ }
30253
+
31728
30254
  /**
31729
30255
  * Given a ray and a rectangular box, find the point where the ray intersects
31730
30256
  * the edge of the box. Assumes the `initialPoint` is inside the box.
@@ -31770,7 +30296,45 @@ function getArrayWithoutDuplicates(array) {
31770
30296
  }
31771
30297
  return returnArray;
31772
30298
  }
30299
+ function getSlopeStringForLine(line, strings) {
30300
+ const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
30301
+ if (!Number.isFinite(slope)) {
30302
+ return strings.srLinearGraphSlopeVertical;
30303
+ }
30304
+ if (slope === 0) {
30305
+ return strings.srLinearGraphSlopeHorizontal;
30306
+ }
30307
+ return slope > 0 ? strings.srLinearGraphSlopeIncreasing : strings.srLinearGraphSlopeDecreasing;
30308
+ }
30309
+ function getInterceptStringForLine(line, strings, locale) {
30310
+ const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
30311
+ const xIntercept = (0 - line[0][1]) / slope + line[0][0];
30312
+ const yIntercept = line[0][1] - slope * line[0][0];
30313
+
30314
+ // Check if the line fully overlaps with an axis.
30315
+ const overlapsXAxis = line[0][1] === 0 && line[1][1] === 0;
30316
+ const overlapsYAxis = line[0][0] === 0 && line[1][0] === 0;
30317
+ const hasXIntercept = Number.isFinite(xIntercept) && !overlapsXAxis;
30318
+ const hasYIntercept = Number.isFinite(yIntercept) && !overlapsYAxis;
30319
+ if (hasXIntercept && hasYIntercept) {
30320
+ // Describe both intercepts in the same sentence.
30321
+ return xIntercept === 0 && yIntercept === 0 ? strings.srLinearGraphOriginIntercept : strings.srLinearGraphBothIntercepts({
30322
+ xIntercept: srFormatNumber(xIntercept, locale),
30323
+ yIntercept: srFormatNumber(yIntercept, locale)
30324
+ });
30325
+ }
30326
+
30327
+ // Describe only one intercept.
30328
+ return hasXIntercept ? strings.srLinearGraphXOnlyIntercept({
30329
+ xIntercept: srFormatNumber(xIntercept, locale)
30330
+ }) : strings.srLinearGraphYOnlyIntercept({
30331
+ yIntercept: srFormatNumber(yIntercept, locale)
30332
+ });
30333
+ }
31773
30334
 
30335
+ const {
30336
+ calculateAngleInDegrees: calculateAngleInDegrees$2
30337
+ } = angles;
31774
30338
  const LockedLine = props => {
31775
30339
  const {
31776
30340
  color: color$1,
@@ -31802,7 +30366,7 @@ const LockedLine = props => {
31802
30366
  // eslint-disable-next-line react-hooks/rules-of-hooks
31803
30367
  const [tailPx, tipPx] = useTransformVectorsToPixels(point2.coord, point1.coord);
31804
30368
  const direction = vec.sub(tailPx, tipPx);
31805
- let angle = calculateAngleInDegrees(direction);
30369
+ let angle = calculateAngleInDegrees$2(direction);
31806
30370
  const startArrowHead = kind !== "segment" && /*#__PURE__*/React.createElement(Arrowhead, {
31807
30371
  angle: angle,
31808
30372
  tip: arrowTip,
@@ -32212,6 +30776,12 @@ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*
32212
30776
  in: "SourceGraphic"
32213
30777
  })));
32214
30778
 
30779
+ const {
30780
+ clockwise: clockwise$1
30781
+ } = geometry;
30782
+ const {
30783
+ getAngleFromVertex: getAngleFromVertex$1
30784
+ } = angles;
32215
30785
  const PolygonAngle = ({
32216
30786
  centerPoint,
32217
30787
  endPoints,
@@ -32300,8 +30870,8 @@ const Angle = ({
32300
30870
  const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32301
30871
 
32302
30872
  // Calculate the angles between the two points
32303
- const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32304
- const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
30873
+ const startAngle = getAngleFromVertex$1(clockwiseCoords[0], vertex);
30874
+ const endAngle = getAngleFromVertex$1(clockwiseCoords[1], vertex);
32305
30875
  const angle = (startAngle + 360 - endAngle) % 360;
32306
30876
 
32307
30877
  // Check if the angle is reflexive
@@ -32657,14 +31227,6 @@ function range(min, max, step = 1) {
32657
31227
  return result;
32658
31228
  }
32659
31229
 
32660
- function srFormatNumber(a, locale) {
32661
- // adding zero here converts negative zero to positive zero.
32662
- return (0 + a).toLocaleString(locale, {
32663
- maximumFractionDigits: 3,
32664
- useGrouping: false // no thousands separators
32665
- });
32666
- }
32667
-
32668
31230
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
32669
31231
  // graph.
32670
31232
  const hitboxSizePx = 48;
@@ -33052,6 +31614,11 @@ const MovablePoint$1 = /*#__PURE__*/React.forwardRef((props, pointRef) => {
33052
31614
  return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle, visiblePoint);
33053
31615
  });
33054
31616
 
31617
+ const {
31618
+ calculateAngleInDegrees: calculateAngleInDegrees$1,
31619
+ getClockwiseAngle: getClockwiseAngle$2,
31620
+ polar: polar$1
31621
+ } = angles;
33055
31622
  function renderAngleGraph(state, dispatch) {
33056
31623
  return {
33057
31624
  graph: /*#__PURE__*/React.createElement(AngleGraph, {
@@ -33121,7 +31688,7 @@ function AngleGraph(props) {
33121
31688
  strings,
33122
31689
  locale
33123
31690
  } = usePerseusI18n();
33124
- const angleMeasure = srFormatNumber(getClockwiseAngle([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
31691
+ const angleMeasure = srFormatNumber(getClockwiseAngle$2([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
33125
31692
  const wholeAngleAriaLabel = strings.srAngleGraphAriaLabel;
33126
31693
  const wholeAngleDescription = strings.srAngleGraphAriaDescription({
33127
31694
  angleMeasure,
@@ -33185,9 +31752,7 @@ function AngleGraph(props) {
33185
31752
  ariaLabel: initialSideAriaLabel
33186
31753
  }), /*#__PURE__*/React.createElement("g", {
33187
31754
  id: "angle-description",
33188
- style: {
33189
- display: "hidden"
33190
- }
31755
+ style: a11y.srOnly
33191
31756
  }, wholeAngleDescription));
33192
31757
  }
33193
31758
  const positiveX = [1, 0];
@@ -33196,7 +31761,7 @@ const positiveY = [0, 1];
33196
31761
  const negativeY = [0, -1];
33197
31762
  function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33198
31763
  var _findIntersectionOfRa, _findIntersectionOfRa2, _findIntersectionOfRa3, _findIntersectionOfRa4;
33199
- const currentAngle = calculateAngleInDegrees(vec.sub(sidePoint, vertex));
31764
+ const currentAngle = calculateAngleInDegrees$1(vec.sub(sidePoint, vertex));
33200
31765
 
33201
31766
  // Find the rays that start at the current point and point up, down, left
33202
31767
  // and right.
@@ -33213,8 +31778,8 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33213
31778
 
33214
31779
  // find the rays that start from the vertex and point in the direction of
33215
31780
  // the angles we just computed.
33216
- const counterClockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepCounterClockwise))];
33217
- const clockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepClockwise))];
31781
+ const counterClockwiseRay = [vertex, vec.add(vertex, polar$1(1, oneStepCounterClockwise))];
31782
+ const clockwiseRay = [vertex, vec.add(vertex, polar$1(1, oneStepClockwise))];
33218
31783
 
33219
31784
  // find the intersections of those rays with the horizontal and vertical
33220
31785
  // rays extending from the sidePoint. These intersections are the points to
@@ -33231,6 +31796,9 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33231
31796
  };
33232
31797
  }
33233
31798
 
31799
+ const {
31800
+ clockwise
31801
+ } = geometry;
33234
31802
  function getGradableGraph(state, initialGraph) {
33235
31803
  if (!state.hasBeenInteractedWith) {
33236
31804
  return _extends({}, initialGraph);
@@ -33299,7 +31867,7 @@ function getGradableGraph(state, initialGraph) {
33299
31867
  // by the order of the points in the coords array, and we want to maintain
33300
31868
  // the same angle scoring with the legacy graph (which had a bug).
33301
31869
  // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33302
- const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
31870
+ const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
33303
31871
  const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33304
31872
  const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33305
31873
  return _extends({}, initialGraph, {
@@ -33411,14 +31979,10 @@ function CircleGraph(props) {
33411
31979
  }
33412
31980
  }), /*#__PURE__*/React.createElement("g", {
33413
31981
  id: radiusId,
33414
- style: {
33415
- display: "hidden"
33416
- }
31982
+ style: a11y.srOnly
33417
31983
  }, srCircleRadius), /*#__PURE__*/React.createElement("g", {
33418
31984
  id: outerPointsId,
33419
- style: {
33420
- display: "hidden"
33421
- }
31985
+ style: a11y.srOnly
33422
31986
  }, srCircleOuterPoints));
33423
31987
  }
33424
31988
  function MovableCircle(props) {
@@ -33603,49 +32167,16 @@ const LinearGraph = (props, key) => {
33603
32167
  point2X: srFormatNumber(line[1][0], locale),
33604
32168
  point2Y: srFormatNumber(line[1][1], locale)
33605
32169
  });
33606
-
33607
- // Slope description
33608
- const slope = (line[1][1] - line[0][1]) / (line[1][0] - line[0][0]);
33609
- let slopeString = "";
33610
- if (slope === Infinity || slope === -Infinity) {
33611
- slopeString = strings.srLinearGraphSlopeVertical;
33612
- } else if (slope === 0) {
33613
- slopeString = strings.srLinearGraphSlopeHorizontal;
33614
- } else {
33615
- slopeString = slope > 0 ? strings.srLinearGraphSlopeIncreasing : strings.srLinearGraphSlopeDecreasing;
33616
- }
33617
-
33618
- // Intersection description
33619
- const xIntercept = (0 - line[0][1]) / slope + line[0][0];
33620
- const yIntercept = line[0][1] - slope * line[0][0];
33621
- const hasXIntercept = xIntercept !== Infinity && xIntercept !== -Infinity;
33622
- const hasYIntercept = yIntercept !== Infinity && yIntercept !== -Infinity;
33623
- let interceptString;
33624
- if (hasXIntercept && hasYIntercept) {
33625
- // Describe both intercepts in the same sentence.
33626
- interceptString = xIntercept === 0 && yIntercept === 0 ? strings.srLinearGraphOriginIntercept : strings.srLinearGraphBothIntercepts({
33627
- xIntercept: srFormatNumber(xIntercept, locale),
33628
- yIntercept: srFormatNumber(yIntercept, locale)
33629
- });
33630
- } else {
33631
- // Describe only one intercept.
33632
- interceptString = hasXIntercept ? strings.srLinearGraphXOnlyIntercept({
33633
- xIntercept: srFormatNumber(xIntercept, locale)
33634
- }) : strings.srLinearGraphYOnlyIntercept({
33635
- yIntercept: srFormatNumber(yIntercept, locale)
33636
- });
33637
- }
32170
+ const slopeString = getSlopeStringForLine(line, strings);
32171
+ const interceptString = getInterceptStringForLine(line, strings, locale);
33638
32172
 
33639
32173
  // Linear graphs only have one line
33640
32174
  // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
33641
32175
  // and another action for moving multiple lines
33642
32176
  return /*#__PURE__*/React.createElement("g", {
33643
- className: "linear-graph-container"
33644
32177
  // Outer line minimal description
33645
- ,
33646
32178
  "aria-label": strings.srLinearGraph,
33647
- "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`,
33648
- role: "figure"
32179
+ "aria-describedby": `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`
33649
32180
  }, /*#__PURE__*/React.createElement(MovableLine, {
33650
32181
  key: 0,
33651
32182
  ariaLabels: {
@@ -33664,19 +32195,13 @@ const LinearGraph = (props, key) => {
33664
32195
  color: "var(--movable-line-stroke-color)"
33665
32196
  }), /*#__PURE__*/React.createElement("g", {
33666
32197
  id: pointsDescriptionId,
33667
- style: {
33668
- display: "hidden"
33669
- }
32198
+ style: a11y.srOnly
33670
32199
  }, linearGraphPointsDescription), /*#__PURE__*/React.createElement("g", {
33671
32200
  id: interceptDescriptionId,
33672
- style: {
33673
- display: "hidden"
33674
- }
32201
+ style: a11y.srOnly
33675
32202
  }, interceptString), /*#__PURE__*/React.createElement("g", {
33676
32203
  id: slopeDescriptionId,
33677
- style: {
33678
- display: "hidden"
33679
- }
32204
+ style: a11y.srOnly
33680
32205
  }, slopeString));
33681
32206
  };
33682
32207
 
@@ -33686,7 +32211,9 @@ function renderLinearSystemGraph(state, dispatch) {
33686
32211
  graphState: state,
33687
32212
  dispatch: dispatch
33688
32213
  }),
33689
- interactiveElementsDescription: null
32214
+ interactiveElementsDescription: /*#__PURE__*/React.createElement(LinearSystemGraphDescription, {
32215
+ state: state
32216
+ })
33690
32217
  };
33691
32218
  }
33692
32219
  const LinearSystemGraph = props => {
@@ -33696,9 +32223,53 @@ const LinearSystemGraph = props => {
33696
32223
  const {
33697
32224
  coords: lines
33698
32225
  } = props.graphState;
33699
- return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(MovableLine, {
32226
+ const {
32227
+ strings,
32228
+ locale
32229
+ } = usePerseusI18n();
32230
+ const id = React.useId();
32231
+ const linesAriaInfo = lines.map((line, i) => {
32232
+ return {
32233
+ pointsDescriptionId: `${id}-line${i + 1}-points`,
32234
+ interceptDescriptionId: `${id}-line${i + 1}-intercept`,
32235
+ slopeDescriptionId: `${id}-line${i + 1}-slope`,
32236
+ pointsDescription: strings.srLinearSystemPoints({
32237
+ lineNumber: i + 1,
32238
+ point1X: srFormatNumber(line[0][0], locale),
32239
+ point1Y: srFormatNumber(line[0][1], locale),
32240
+ point2X: srFormatNumber(line[1][0], locale),
32241
+ point2Y: srFormatNumber(line[1][1], locale)
32242
+ }),
32243
+ interceptDescription: getInterceptStringForLine(line, strings, locale),
32244
+ slopeDescription: getSlopeStringForLine(line, strings)
32245
+ };
32246
+ });
32247
+ return /*#__PURE__*/React.createElement("g", {
32248
+ "aria-label": strings.srLinearSystemGraph,
32249
+ "aria-describedby": linesAriaInfo.map(({
32250
+ pointsDescriptionId,
32251
+ interceptDescriptionId,
32252
+ slopeDescriptionId
32253
+ }) => `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`).join(" ")
32254
+ }, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(MovableLine, {
33700
32255
  key: i,
33701
32256
  points: line,
32257
+ ariaLabels: {
32258
+ point1AriaLabel: strings.srLinearSystemPoint({
32259
+ lineNumber: i + 1,
32260
+ pointSequence: 1,
32261
+ x: srFormatNumber(line[0][0], locale),
32262
+ y: srFormatNumber(line[0][1], locale)
32263
+ }),
32264
+ point2AriaLabel: strings.srLinearSystemPoint({
32265
+ lineNumber: i + 1,
32266
+ pointSequence: 2,
32267
+ x: srFormatNumber(line[1][0], locale),
32268
+ y: srFormatNumber(line[1][1], locale)
32269
+ }),
32270
+ grabHandleAriaLabel: `${linesAriaInfo[i].interceptDescription} ${linesAriaInfo[i].slopeDescription}`
32271
+ },
32272
+ ariaDescribedBy: `${linesAriaInfo[i].interceptDescriptionId} ${linesAriaInfo[i].slopeDescriptionId}`,
33702
32273
  onMoveLine: delta => {
33703
32274
  dispatch(actions.linearSystem.moveLine(i, delta));
33704
32275
  },
@@ -33708,8 +32279,63 @@ const LinearSystemGraph = props => {
33708
32279
  },
33709
32280
  onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
33710
32281
  color: "var(--movable-line-stroke-color)"
33711
- })), ";");
32282
+ })), linesAriaInfo.map(({
32283
+ pointsDescriptionId,
32284
+ interceptDescriptionId,
32285
+ slopeDescriptionId,
32286
+ pointsDescription,
32287
+ interceptDescription,
32288
+ slopeDescription
32289
+ }) => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("g", {
32290
+ key: pointsDescriptionId,
32291
+ id: pointsDescriptionId,
32292
+ style: a11y.srOnly
32293
+ }, pointsDescription), /*#__PURE__*/React.createElement("g", {
32294
+ key: interceptDescriptionId,
32295
+ id: interceptDescriptionId,
32296
+ style: a11y.srOnly
32297
+ }, interceptDescription), /*#__PURE__*/React.createElement("g", {
32298
+ key: slopeDescriptionId,
32299
+ id: slopeDescriptionId,
32300
+ style: a11y.srOnly
32301
+ }, slopeDescription))));
33712
32302
  };
32303
+ function LinearSystemGraphDescription({
32304
+ state
32305
+ }) {
32306
+ // The reason that LinearSystemGraphDescription is a component (rather
32307
+ // than a function that returns a string) is because it needs to use a
32308
+ // hook: `usePerseusI18n`.
32309
+ const i18n = usePerseusI18n();
32310
+ return describeLinearSystemGraph(state, i18n);
32311
+ }
32312
+
32313
+ // Exported for testing
32314
+ function describeLinearSystemGraph(state, i18n) {
32315
+ const {
32316
+ strings,
32317
+ locale
32318
+ } = i18n;
32319
+ const {
32320
+ coords: lines
32321
+ } = state;
32322
+ const graphDescription = strings.srLinearSystemGraph;
32323
+ const lineDescriptions = lines.map((line, i) => {
32324
+ const point1 = line[0];
32325
+ const point2 = line[1];
32326
+ return strings.srLinearSystemPoints({
32327
+ lineNumber: i + 1,
32328
+ point1X: srFormatNumber(point1[0], locale),
32329
+ point1Y: srFormatNumber(point1[1], locale),
32330
+ point2X: srFormatNumber(point2[0], locale),
32331
+ point2Y: srFormatNumber(point2[1], locale)
32332
+ });
32333
+ });
32334
+ const allDescriptions = [graphDescription, ...lineDescriptions];
32335
+ return strings.srInteractiveElements({
32336
+ elements: allDescriptions.join(" ")
32337
+ });
32338
+ }
33713
32339
 
33714
32340
  function renderPointGraph(state, dispatch) {
33715
32341
  return {
@@ -34148,7 +32774,7 @@ function QuadraticGraph(props) {
34148
32774
  // to content creators the currently selected "correct answer". ex: y = 0.200x^2 + 0.000x + 0.000
34149
32775
  // While we should technically never have invalid coordinates, we want to ensure that we have a fallback
34150
32776
  const coeffRef = React.useRef([0, 0, 0]);
34151
- const coeffs = getQuadraticCoefficients$1(coords);
32777
+ const coeffs = getQuadraticCoefficients$2(coords);
34152
32778
  if (coeffs !== undefined) {
34153
32779
  coeffRef.current = coeffs;
34154
32780
  }
@@ -34158,7 +32784,7 @@ function QuadraticGraph(props) {
34158
32784
 
34159
32785
  // Calculate the y value based on the current x value
34160
32786
  const y = x => (a * x + b) * x + c;
34161
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
32787
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$2.OfX, {
34162
32788
  y: y,
34163
32789
  color: color.blue
34164
32790
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
@@ -34173,7 +32799,7 @@ function QuadraticGraph(props) {
34173
32799
  // These equations were originally set up in 2013 and may require some
34174
32800
  // additional comments to help clarify the quadratic formula manipulations
34175
32801
  // Origin: https://phabricator.khanacademy.org/D2413
34176
- const getQuadraticCoefficients$1 = coords => {
32802
+ const getQuadraticCoefficients$2 = coords => {
34177
32803
  const p1 = coords[0];
34178
32804
  const p2 = coords[1];
34179
32805
  const p3 = coords[2];
@@ -34196,7 +32822,9 @@ function renderRayGraph(state, dispatch) {
34196
32822
  graphState: state,
34197
32823
  dispatch: dispatch
34198
32824
  }),
34199
- interactiveElementsDescription: null
32825
+ interactiveElementsDescription: /*#__PURE__*/React.createElement(RayGraphDescription, {
32826
+ state: state
32827
+ })
34200
32828
  };
34201
32829
  }
34202
32830
  const RayGraph = props => {
@@ -34208,18 +32836,103 @@ const RayGraph = props => {
34208
32836
  } = props.graphState;
34209
32837
  const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
34210
32838
  const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
32839
+ const {
32840
+ strings,
32841
+ locale
32842
+ } = usePerseusI18n();
32843
+ const id = React.useId();
32844
+ const pointsDescriptionId = id + "-points";
32845
+
32846
+ // Aria label strings
32847
+ const {
32848
+ srRayGraph,
32849
+ srRayPoints,
32850
+ srRayEndpoint,
32851
+ srRayTerminalPoint,
32852
+ srRayGrabHandle
32853
+ } = describeRayGraph(props.graphState, {
32854
+ strings,
32855
+ locale
32856
+ });
34211
32857
 
34212
32858
  // Ray graphs only have one line
34213
- return /*#__PURE__*/React.createElement(MovableLine, {
32859
+ return /*#__PURE__*/React.createElement("g", {
32860
+ // Outer line minimal description
32861
+ "aria-label": srRayGraph,
32862
+ "aria-describedby": pointsDescriptionId
32863
+ }, /*#__PURE__*/React.createElement(MovableLine, {
34214
32864
  points: line,
32865
+ ariaLabels: {
32866
+ point1AriaLabel: srRayEndpoint,
32867
+ point2AriaLabel: srRayTerminalPoint,
32868
+ grabHandleAriaLabel: srRayGrabHandle
32869
+ },
34215
32870
  onMoveLine: handleMoveLine,
34216
32871
  onMovePoint: handleMovePoint,
34217
32872
  extend: {
34218
32873
  start: false,
34219
32874
  end: true
34220
32875
  }
34221
- });
32876
+ }), /*#__PURE__*/React.createElement("g", {
32877
+ id: pointsDescriptionId,
32878
+ style: a11y.srOnly
32879
+ }, srRayPoints));
34222
32880
  };
32881
+ function RayGraphDescription({
32882
+ state
32883
+ }) {
32884
+ // The reason that RayGraphDescription is a component (rather than a
32885
+ // function that returns a string) is because it needs to use a
32886
+ // hook: `usePerseusI18n`.
32887
+ const i18n = usePerseusI18n();
32888
+ const strings = describeRayGraph(state, i18n);
32889
+ return strings.srRayInteractiveElement;
32890
+ }
32891
+
32892
+ // Exported for testing
32893
+ function describeRayGraph(state, i18n) {
32894
+ const {
32895
+ coords: line
32896
+ } = state;
32897
+ const {
32898
+ strings,
32899
+ locale
32900
+ } = i18n;
32901
+
32902
+ // Aria label strings
32903
+ const srRayGraph = strings.srRayGraph;
32904
+ const srRayPoints = strings.srRayPoints({
32905
+ point1X: srFormatNumber(line[0][0], locale),
32906
+ point1Y: srFormatNumber(line[0][1], locale),
32907
+ point2X: srFormatNumber(line[1][0], locale),
32908
+ point2Y: srFormatNumber(line[1][1], locale)
32909
+ });
32910
+ const srRayEndpoint = strings.srRayEndpoint({
32911
+ x: srFormatNumber(line[0][0], locale),
32912
+ y: srFormatNumber(line[0][1], locale)
32913
+ });
32914
+ const srRayTerminalPoint = strings.srRayTerminalPoint({
32915
+ x: srFormatNumber(line[1][0], locale),
32916
+ y: srFormatNumber(line[1][1], locale)
32917
+ });
32918
+ const srRayGrabHandle = strings.srRayGrabHandle({
32919
+ point1X: srFormatNumber(line[0][0], locale),
32920
+ point1Y: srFormatNumber(line[0][1], locale),
32921
+ point2X: srFormatNumber(line[1][0], locale),
32922
+ point2Y: srFormatNumber(line[1][1], locale)
32923
+ });
32924
+ const srRayInteractiveElement = strings.srInteractiveElements({
32925
+ elements: [srRayGraph, srRayPoints].join(" ")
32926
+ });
32927
+ return {
32928
+ srRayGraph,
32929
+ srRayPoints,
32930
+ srRayEndpoint,
32931
+ srRayTerminalPoint,
32932
+ srRayGrabHandle,
32933
+ srRayInteractiveElement
32934
+ };
32935
+ }
34223
32936
 
34224
32937
  function renderSegmentGraph(state, dispatch) {
34225
32938
  return {
@@ -34287,7 +33000,7 @@ function SinusoidGraph(props) {
34287
33000
  if (coeffs !== undefined) {
34288
33001
  coeffRef.current = coeffs;
34289
33002
  }
34290
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
33003
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$2.OfX, {
34291
33004
  y: x => computeSine(x, coeffRef.current),
34292
33005
  color: color.blue
34293
33006
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
@@ -34334,6 +33047,10 @@ const getSinusoidCoefficients$1 = coords => {
34334
33047
  };
34335
33048
  };
34336
33049
 
33050
+ const {
33051
+ calculateAngleInDegrees,
33052
+ convertDegreesToRadians
33053
+ } = angles;
34337
33054
  const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
34338
33055
 
34339
33056
  // The vector from the center of the protractor to the top left corner of the
@@ -35035,6 +33752,10 @@ function mafsStateToInteractiveGraph(state, originalGraph) {
35035
33752
  }
35036
33753
  }
35037
33754
 
33755
+ const {
33756
+ magnitude: magnitude$2,
33757
+ vector: vector$2
33758
+ } = geometry;
35038
33759
  function initializeGraphState(params) {
35039
33760
  var _graph$snapTo;
35040
33761
  const {
@@ -35314,7 +34035,7 @@ const getAngleCoords = params => {
35314
34035
  defaultCoords = normalizePoints(range, step, defaultCoords, true);
35315
34036
 
35316
34037
  // @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.
35317
- const radius = magnitude(vector(...defaultCoords));
34038
+ const radius = magnitude$2(vector$2(...defaultCoords));
35318
34039
 
35319
34040
  // We're adding a placeholder for the third point to appease ts and so that we
35320
34041
  // can calculate it after we've adjusted the first point by the angleOffsetDeg
@@ -35328,6 +34049,24 @@ const getAngleCoords = params => {
35328
34049
  return coords;
35329
34050
  };
35330
34051
 
34052
+ const {
34053
+ getAngleFromVertex,
34054
+ getClockwiseAngle: getClockwiseAngle$1,
34055
+ polar
34056
+ } = angles;
34057
+ const {
34058
+ angleMeasures: angleMeasures$1,
34059
+ ccw: ccw$1,
34060
+ lawOfCosines: lawOfCosines$1,
34061
+ magnitude: magnitude$1,
34062
+ polygonSidesIntersect,
34063
+ reverseVector,
34064
+ sign: sign$1,
34065
+ vector: vector$1
34066
+ } = geometry;
34067
+ const {
34068
+ getQuadraticCoefficients: getQuadraticCoefficients$1
34069
+ } = coefficients;
35331
34070
  const minDistanceBetweenAngleVertexAndSidePoint = 2;
35332
34071
  function interactiveGraphReducer(state, action) {
35333
34072
  switch (action.type) {
@@ -35846,11 +34585,9 @@ const getChange = (coords, delta, constraintOpts) => {
35846
34585
  const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
35847
34586
  return [dx, dy];
35848
34587
  };
35849
- const eq$2 = Util.eq;
35850
-
35851
34588
  // Less than or approximately equal
35852
34589
  function leq$1(a, b) {
35853
- return a < b || eq$2(a, b);
34590
+ return a < b || approximateEqual(a, b);
35854
34591
  }
35855
34592
  function boundAndSnapToGrid(point, {
35856
34593
  snapStep,
@@ -35994,7 +34731,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
35994
34731
  });
35995
34732
 
35996
34733
  // Gets the radian angles between the coords and maps them to degrees
35997
- const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
34734
+ const angles = angleMeasures$1(coordsCopy).map(angle => angle * 180 / Math.PI);
35998
34735
 
35999
34736
  // Gets the relative index of a point
36000
34737
  const rel = j => {
@@ -36006,7 +34743,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
36006
34743
  angles[rel(j)] = Math.round(angles[rel(j)]);
36007
34744
  });
36008
34745
  const getAngle = function getAngle(a, vertex, b) {
36009
- const angle = getClockwiseAngle([coordsCopy[rel(a)], coordsCopy[rel(vertex)], coordsCopy[rel(b)]]);
34746
+ const angle = getClockwiseAngle$1([coordsCopy[rel(a)], coordsCopy[rel(vertex)], coordsCopy[rel(b)]]);
36010
34747
  return angle;
36011
34748
  };
36012
34749
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -36018,9 +34755,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
36018
34755
  })) {
36019
34756
  return startingPoint;
36020
34757
  }
36021
- const knownSide = magnitude(
34758
+ const knownSide = magnitude$1(
36022
34759
  // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
36023
- vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
34760
+ vector$1(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
36024
34761
 
36025
34762
  // Returns true if the points form a counter-clockwise turn;
36026
34763
  // a.k.a if the point is on the left or right of the polygon.
@@ -36028,7 +34765,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
36028
34765
  // we want to add the inner angle to the outer angle, and if it's on the right,
36029
34766
  // we want to subtract the inner angle from the outer angle. The angle solved
36030
34767
  // for is then used in the polar function to determine the new point.
36031
- const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34768
+ const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
36032
34769
 
36033
34770
  // Solve for side by using the law of sines
36034
34771
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
@@ -36040,7 +34777,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
36040
34777
  // and the angle between the first and second sides of the
36041
34778
  // polygon (angular coordinate) to determine how to adjust the point
36042
34779
  const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
36043
- return vector$1.add(coordsCopy[rel(-1)], offset);
34780
+ return vector$3.add(coordsCopy[rel(-1)], offset);
36044
34781
  }
36045
34782
  function boundAndSnapToSides(destinationPoint, {
36046
34783
  range,
@@ -36066,7 +34803,7 @@ function boundAndSnapToSides(destinationPoint, {
36066
34803
  };
36067
34804
  const sides = _.map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
36068
34805
  // @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.
36069
- return magnitude(vector(...coordsCopy));
34806
+ return magnitude$1(vector$1(...coordsCopy));
36070
34807
  });
36071
34808
 
36072
34809
  // Round the sides to left and right of the current point
@@ -36081,7 +34818,7 @@ function boundAndSnapToSides(destinationPoint, {
36081
34818
 
36082
34819
  // Solve for angle by using the law of cosines
36083
34820
  // Angle at the first vertex of the polygon
36084
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
34821
+ const innerAngle = lawOfCosines$1(sides[0], sides[2], sides[1]);
36085
34822
 
36086
34823
  // Angle at the second vertex of the polygon
36087
34824
  const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
@@ -36092,13 +34829,13 @@ function boundAndSnapToSides(destinationPoint, {
36092
34829
  // we want to add the inner angle to the outer angle, and if it's on the right,
36093
34830
  // we want to subtract the inner angle from the outer angle. The angle solved
36094
34831
  // for is then used in the polar function to determine the new point.
36095
- const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34832
+ const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
36096
34833
 
36097
34834
  // Uses the length of the first side of the polygon (radial coordinate)
36098
34835
  // and the angle between the first and second sides of the
36099
34836
  // polygon (angular coordinate) to determine how to adjust the point
36100
34837
  const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36101
- return vector$1.add(coordsCopy[rel(-1)], offset);
34838
+ return vector$3.add(coordsCopy[rel(-1)], offset);
36102
34839
  }
36103
34840
 
36104
34841
  // Returns the vector from the given point to the top-right corner of the graph when snapped to the grid
@@ -36128,7 +34865,7 @@ function minMove({
36128
34865
  });
36129
34866
  return vec.sub(bottomLeft, point);
36130
34867
  }
36131
- const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && vector$1.equal(coord, c)));
34868
+ const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && vector$3.equal(coord, c)));
36132
34869
  function updateAtIndex(args) {
36133
34870
  const {
36134
34871
  array,
@@ -36223,216 +34960,26 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
36223
34960
  }));
36224
34961
  });
36225
34962
 
36226
- const eq$1 = Util.eq;
36227
- const deepEq$3 = Util.deepEq;
36228
- function scoreInteractiveGraph(userInput, rubric) {
36229
- // None-type graphs are not graded
36230
- if (userInput.type === "none" && rubric.correct.type === "none") {
36231
- return {
36232
- type: "points",
36233
- earned: 0,
36234
- total: 0,
36235
- message: null
36236
- };
36237
- }
36238
-
36239
- // When nothing has moved, there will neither be coords nor the
36240
- // circle's center/radius fields. When those fields are absent, skip
36241
- // all these checks; just go mark the answer as empty.
36242
- const hasValue = Boolean(
36243
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
36244
- userInput.coords ||
36245
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
36246
- userInput.center && userInput.radius);
36247
- if (userInput.type === rubric.correct.type && hasValue) {
36248
- if (userInput.type === "linear" && rubric.correct.type === "linear" && userInput.coords != null) {
36249
- const guess = userInput.coords;
36250
- const correct = rubric.correct.coords;
36251
-
36252
- // If both of the guess points are on the correct line, it's
36253
- // correct.
36254
- if (collinear(correct[0], correct[1], guess[0]) && collinear(correct[0], correct[1], guess[1])) {
36255
- return {
36256
- type: "points",
36257
- earned: 1,
36258
- total: 1,
36259
- message: null
36260
- };
36261
- }
36262
- } else if (userInput.type === "linear-system" && rubric.correct.type === "linear-system" && userInput.coords != null) {
36263
- const guess = userInput.coords;
36264
- const correct = rubric.correct.coords;
36265
- 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])) {
36266
- return {
36267
- type: "points",
36268
- earned: 1,
36269
- total: 1,
36270
- message: null
36271
- };
36272
- }
36273
- } else if (userInput.type === "quadratic" && rubric.correct.type === "quadratic" && userInput.coords != null) {
36274
- // If the parabola coefficients match, it's correct.
36275
- const guessCoeffs = getQuadraticCoefficients(userInput.coords);
36276
- const correctCoeffs = getQuadraticCoefficients(rubric.correct.coords);
36277
- if (deepEq$3(guessCoeffs, correctCoeffs)) {
36278
- return {
36279
- type: "points",
36280
- earned: 1,
36281
- total: 1,
36282
- message: null
36283
- };
36284
- }
36285
- } else if (userInput.type === "sinusoid" && rubric.correct.type === "sinusoid" && userInput.coords != null) {
36286
- const guessCoeffs = getSinusoidCoefficients(userInput.coords);
36287
- const correctCoeffs = getSinusoidCoefficients(rubric.correct.coords);
36288
- const canonicalGuessCoeffs = canonicalSineCoefficients$1(guessCoeffs);
36289
- const canonicalCorrectCoeffs = canonicalSineCoefficients$1(correctCoeffs);
36290
- // If the canonical coefficients match, it's correct.
36291
- if (deepEq$3(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
36292
- return {
36293
- type: "points",
36294
- earned: 1,
36295
- total: 1,
36296
- message: null
36297
- };
36298
- }
36299
- } else if (userInput.type === "circle" && rubric.correct.type === "circle") {
36300
- if (deepEq$3(userInput.center, rubric.correct.center) && eq$1(userInput.radius, rubric.correct.radius)) {
36301
- return {
36302
- type: "points",
36303
- earned: 1,
36304
- total: 1,
36305
- message: null
36306
- };
36307
- }
36308
- } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
36309
- let correct = rubric.correct.coords;
36310
- if (correct == null) {
36311
- throw new Error("Point graph rubric has null coords");
36312
- }
36313
- const guess = userInput.coords.slice();
36314
- correct = correct.slice();
36315
- // Everything's already rounded so we shouldn't need to do an
36316
- // eq() comparison but _.isEqual(0, -0) is false, so we'll use
36317
- // eq() anyway. The sort should be fine because it'll stringify
36318
- // it and -0 converted to a string is "0"
36319
- guess == null || guess.sort();
36320
- // @ts-expect-error - TS2339 - Property 'sort' does not exist on type 'readonly Coord[]'.
36321
- correct.sort();
36322
- if (deepEq$3(guess, correct)) {
36323
- return {
36324
- type: "points",
36325
- earned: 1,
36326
- total: 1,
36327
- message: null
36328
- };
36329
- }
36330
- } else if (userInput.type === "polygon" && rubric.correct.type === "polygon" && userInput.coords != null) {
36331
- const guess = userInput.coords.slice();
36332
- const correct = rubric.correct.coords.slice();
36333
- let match;
36334
- if (rubric.correct.match === "similar") {
36335
- match = similar(guess, correct, Number.POSITIVE_INFINITY);
36336
- } else if (rubric.correct.match === "congruent") {
36337
- match = similar(guess, correct, number$1.DEFAULT_TOLERANCE);
36338
- } else if (rubric.correct.match === "approx") {
36339
- match = similar(guess, correct, 0.1);
36340
- } else {
36341
- /* exact */
36342
- guess.sort();
36343
- correct.sort();
36344
- match = deepEq$3(guess, correct);
36345
- }
36346
- if (match) {
36347
- return {
36348
- type: "points",
36349
- earned: 1,
36350
- total: 1,
36351
- message: null
36352
- };
36353
- }
36354
- } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
36355
- let guess = Util.deepClone(userInput.coords);
36356
- let correct = Util.deepClone(rubric.correct.coords);
36357
- guess = _.invoke(guess, "sort").sort();
36358
- correct = _.invoke(correct, "sort").sort();
36359
- if (deepEq$3(guess, correct)) {
36360
- return {
36361
- type: "points",
36362
- earned: 1,
36363
- total: 1,
36364
- message: null
36365
- };
36366
- }
36367
- } else if (userInput.type === "ray" && rubric.correct.type === "ray" && userInput.coords != null) {
36368
- const guess = userInput.coords;
36369
- const correct = rubric.correct.coords;
36370
- if (deepEq$3(guess[0], correct[0]) && collinear(correct[0], correct[1], guess[1])) {
36371
- return {
36372
- type: "points",
36373
- earned: 1,
36374
- total: 1,
36375
- message: null
36376
- };
36377
- }
36378
- } else if (userInput.type === "angle" && rubric.correct.type === "angle") {
36379
- const guess = userInput.coords;
36380
- const correct = rubric.correct.coords;
36381
- const allowReflexAngles = rubric.correct.allowReflexAngles;
36382
- let match;
36383
- if (rubric.correct.match === "congruent") {
36384
- const angles = _.map([guess, correct], function (coords) {
36385
- if (!coords) {
36386
- return false;
36387
- }
36388
- const angle = getClockwiseAngle(coords, allowReflexAngles);
36389
- return angle;
36390
- });
36391
- // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
36392
- match = eq$1(...angles);
36393
- } else {
36394
- /* exact */
36395
- match =
36396
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36397
- deepEq$3(guess[1], correct[1]) &&
36398
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36399
- collinear(correct[1], correct[0], guess[0]) &&
36400
- // @ts-expect-error - TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'. | TS2532 - Object is possibly 'undefined'.
36401
- collinear(correct[1], correct[2], guess[2]);
36402
- }
36403
- if (match) {
36404
- return {
36405
- type: "points",
36406
- earned: 1,
36407
- total: 1,
36408
- message: null
36409
- };
36410
- }
36411
- }
36412
- }
36413
-
36414
- // The input wasn't correct, so check if it's a blank input or if it's
36415
- // actually just wrong
36416
- if (!hasValue || _.isEqual(userInput, rubric.graph)) {
36417
- // We're where we started.
36418
- return {
36419
- type: "invalid",
36420
- message: null
36421
- };
36422
- }
36423
- return {
36424
- type: "points",
36425
- earned: 0,
36426
- total: 1,
36427
- message: null
36428
- };
36429
- }
36430
-
34963
+ const {
34964
+ getClockwiseAngle
34965
+ } = angles;
34966
+ const {
34967
+ angleMeasures,
34968
+ ccw,
34969
+ collinear,
34970
+ getLineEquation,
34971
+ getLineIntersection,
34972
+ intersects,
34973
+ lawOfCosines,
34974
+ magnitude,
34975
+ rotate,
34976
+ sign,
34977
+ vector
34978
+ } = geometry;
36431
34979
  const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
36432
34980
  const defaultBackgroundImage = {
36433
34981
  url: null
36434
34982
  };
36435
- const eq = Util.eq;
36436
34983
  const UNLIMITED = "unlimited";
36437
34984
 
36438
34985
  // Sample background image:
@@ -36445,7 +34992,7 @@ function defaultVal(actual, defaultValue) {
36445
34992
 
36446
34993
  // Less than or approximately equal
36447
34994
  function leq(a, b) {
36448
- return a < b || eq(a, b);
34995
+ return a < b || approximateEqual(a, b);
36449
34996
  }
36450
34997
  function capitalize(str) {
36451
34998
  return str.replace(/(?:^|-)(.)/g, function (match, letter) {
@@ -37272,7 +35819,7 @@ class LegacyInteractiveGraph extends React.Component {
37272
35819
  // Solve for side by using the law of sines
37273
35820
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
37274
35821
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37275
- const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
35822
+ const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
37276
35823
  return (_this$graphie5 = this.graphie) == null ? void 0 : _this$graphie5.addPoints(coords[rel(-1)], offset);
37277
35824
  }
37278
35825
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -37297,7 +35844,7 @@ class LegacyInteractiveGraph extends React.Component {
37297
35844
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
37298
35845
  const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
37299
35846
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
37300
- const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
35847
+ const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
37301
35848
  return (_this$graphie6 = this.graphie) == null ? void 0 : _this$graphie6.addPoints(coords[rel(-1)], offset);
37302
35849
  }
37303
35850
  // Snap to grid (already done)
@@ -37959,12 +36506,12 @@ class InteractiveGraph extends React.Component {
37959
36506
  }
37960
36507
  static getLinearEquationString(props) {
37961
36508
  const coords = InteractiveGraph.getLineCoords(props.graph, props);
37962
- if (eq(coords[0][0], coords[1][0])) {
36509
+ if (approximateEqual(coords[0][0], coords[1][0])) {
37963
36510
  return "x = " + coords[0][0].toFixed(3);
37964
36511
  }
37965
36512
  const m = (coords[1][1] - coords[0][1]) / (coords[1][0] - coords[0][0]);
37966
36513
  const b = coords[0][1] - m * coords[0][0];
37967
- if (eq(m, 0)) {
36514
+ if (approximateEqual(m, 0)) {
37968
36515
  return "y = " + b.toFixed(3);
37969
36516
  }
37970
36517
  return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
@@ -38490,59 +37037,6 @@ const styles$a = StyleSheet.create({
38490
37037
  }
38491
37038
  });
38492
37039
 
38493
- // Question state for marker as result of user selected answers.
38494
-
38495
- function scoreMarker(marker) {
38496
- const score = {
38497
- hasAnswers: false,
38498
- isCorrect: false
38499
- };
38500
- if (marker.selected && marker.selected.length > 0) {
38501
- score.hasAnswers = true;
38502
- }
38503
- if (marker.answers.length > 0) {
38504
- if (marker.selected && marker.selected.length === marker.answers.length) {
38505
- // All correct answers are selected by the user.
38506
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
38507
- }
38508
- } else if (!marker.selected || marker.selected.length === 0) {
38509
- // Correct as no answers should be selected by the user.
38510
- score.isCorrect = true;
38511
- }
38512
- return score;
38513
- }
38514
-
38515
- // TODO(LEMS-2440): May need to pull answers out of PerseusLabelImageWidgetOptions[markers] for the rubric
38516
- function scoreLabelImage(userInput, rubric) {
38517
- let numAnswered = 0;
38518
- let numCorrect = 0;
38519
- for (const marker of userInput.markers) {
38520
- const score = scoreMarker(marker);
38521
- if (score.hasAnswers) {
38522
- numAnswered++;
38523
- }
38524
- if (score.isCorrect) {
38525
- numCorrect++;
38526
- }
38527
- }
38528
-
38529
- // We expect all question markers to be answered before grading.
38530
- if (numAnswered !== userInput.markers.length) {
38531
- return {
38532
- type: "invalid",
38533
- message: null
38534
- };
38535
- }
38536
- return {
38537
- type: "points",
38538
- // Markers with no expected answers are graded as correct if user
38539
- // makes no answer selection.
38540
- earned: numCorrect === userInput.markers.length ? 1 : 0,
38541
- total: 1,
38542
- message: null
38543
- };
38544
- }
38545
-
38546
37040
  /**
38547
37041
  * Represents a direction vector.
38548
37042
  * No diagonals allowed.
@@ -38764,7 +37258,7 @@ class LabelImage extends React.Component {
38764
37258
  onChange
38765
37259
  } = this.props;
38766
37260
  const updatedMarkers = markers.map(marker => {
38767
- const score = scoreMarker(marker);
37261
+ const score = scoreLabelImageMarker(marker);
38768
37262
  return _extends({}, marker, {
38769
37263
  // Reveal correctness state for markers with answers.
38770
37264
  showCorrectness: score.hasAnswers ? score.isCorrect ? "correct" : "incorrect" : undefined
@@ -38911,7 +37405,7 @@ class LabelImage extends React.Component {
38911
37405
  bottom: "top"
38912
37406
  }[markerPosition];
38913
37407
  }
38914
- const score = scoreMarker(marker);
37408
+ const score = scoreLabelImageMarker(marker);
38915
37409
  // Once the question is answered, show markers
38916
37410
  // with correct answers, otherwise passthrough
38917
37411
  // the correctness state.
@@ -39843,16 +38337,6 @@ const getPromptJSON$9 = (renderProps, userInput) => {
39843
38337
  };
39844
38338
  };
39845
38339
 
39846
- function scoreMatcher(state, rubric) {
39847
- const correct = _.isEqual(state.left, rubric.left) && _.isEqual(state.right, rubric.right);
39848
- return {
39849
- type: "points",
39850
- earned: correct ? 1 : 0,
39851
- total: 1,
39852
- message: null
39853
- };
39854
- }
39855
-
39856
38340
  const {
39857
38341
  shuffle: shuffle$1,
39858
38342
  seededRNG
@@ -40075,78 +38559,6 @@ const getPromptJSON$8 = (renderProps, userInput) => {
40075
38559
  };
40076
38560
  };
40077
38561
 
40078
- /**
40079
- * Checks user input from the matrix widget to see if it is scorable.
40080
- *
40081
- * Note: The matrix widget cannot do much validation without the Scoring
40082
- * Data because of its use of KhanAnswerTypes as a core part of scoring.
40083
- *
40084
- * @see `scoreMatrix()` for more details.
40085
- */
40086
- function validateMatrix(userInput, validationData, strings) {
40087
- const supplied = userInput.answers;
40088
- const suppliedSize = getMatrixSize(supplied);
40089
- for (let row = 0; row < suppliedSize[0]; row++) {
40090
- for (let col = 0; col < suppliedSize[1]; col++) {
40091
- if (supplied[row][col] == null || supplied[row][col].toString().length === 0) {
40092
- return {
40093
- type: "invalid",
40094
- message: strings.fillAllCells
40095
- };
40096
- }
40097
- }
40098
- }
40099
- return null;
40100
- }
40101
-
40102
- function scoreMatrix(userInput, rubric, strings) {
40103
- const validationResult = validateMatrix(userInput, rubric, strings);
40104
- if (validationResult != null) {
40105
- return validationResult;
40106
- }
40107
- const solution = rubric.answers;
40108
- const supplied = userInput.answers;
40109
- const solutionSize = getMatrixSize(solution);
40110
- const suppliedSize = getMatrixSize(supplied);
40111
- const incorrectSize = solutionSize[0] !== suppliedSize[0] || solutionSize[1] !== suppliedSize[1];
40112
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
40113
- let message = null;
40114
- let incorrect = false;
40115
- _(suppliedSize[0]).times(row => {
40116
- _(suppliedSize[1]).times(col => {
40117
- if (!incorrectSize) {
40118
- const validator = createValidator(
40119
- // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
40120
- solution[row][col], {
40121
- simplify: true
40122
- });
40123
- const result = validator(supplied[row][col]);
40124
- if (result.message) {
40125
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
40126
- message = result.message;
40127
- }
40128
- if (!result.correct) {
40129
- incorrect = true;
40130
- }
40131
- }
40132
- });
40133
- });
40134
- if (incorrectSize) {
40135
- return {
40136
- type: "points",
40137
- earned: 0,
40138
- total: 1,
40139
- message: null
40140
- };
40141
- }
40142
- return {
40143
- type: "points",
40144
- earned: incorrect ? 0 : 1,
40145
- total: 1,
40146
- message: message
40147
- };
40148
- }
40149
-
40150
38562
  const {
40151
38563
  assert: assert$2
40152
38564
  } = InteractiveUtil;
@@ -40195,29 +38607,6 @@ const getRefForPath$1 = function getRefForPath(path) {
40195
38607
  const column = getColumnFromPath$1(path);
40196
38608
  return "answer" + row + "," + column;
40197
38609
  };
40198
- function getMatrixSize(matrix) {
40199
- const matrixSize = [1, 1];
40200
-
40201
- // We need to find the widest row and tallest column to get the correct
40202
- // matrix size.
40203
- _(matrix).each((matrixRow, row) => {
40204
- let rowWidth = 0;
40205
- _(matrixRow).each((matrixCol, col) => {
40206
- if (matrixCol != null && matrixCol.toString().length) {
40207
- rowWidth = col + 1;
40208
- }
40209
- });
40210
-
40211
- // Matrix width:
40212
- matrixSize[1] = Math.max(matrixSize[1], rowWidth);
40213
-
40214
- // Matrix height:
40215
- if (rowWidth > 0) {
40216
- matrixSize[0] = Math.max(matrixSize[0], row + 1);
40217
- }
40218
- });
40219
- return matrixSize;
40220
- }
40221
38610
  class Matrix extends React.Component {
40222
38611
  constructor(...args) {
40223
38612
  super(...args);
@@ -41608,59 +39997,6 @@ const getPromptJSON$6 = (renderProps, userInput) => {
41608
39997
  };
41609
39998
  };
41610
39999
 
41611
- /**
41612
- * Checks user input is within the allowed range and not the same as the initial
41613
- * state.
41614
- * @param userInput
41615
- * @see 'scoreNumberLine' for the scoring logic.
41616
- */
41617
- function validateNumberLine(userInput) {
41618
- const divisionRange = userInput.divisionRange;
41619
- const outsideAllowedRange = userInput.numDivisions > divisionRange[1] || userInput.numDivisions < divisionRange[0];
41620
-
41621
- // TODO: I don't think isTickCrtl is a thing anymore
41622
- if (userInput.isTickCrtl && outsideAllowedRange) {
41623
- return {
41624
- type: "invalid",
41625
- message: "Number of divisions is outside the allowed range."
41626
- };
41627
- }
41628
- return null;
41629
- }
41630
-
41631
- function scoreNumberLine(userInput, scoringData) {
41632
- const validationError = validateNumberLine(userInput);
41633
- if (validationError) {
41634
- return validationError;
41635
- }
41636
- const range = scoringData.range;
41637
- const start = scoringData.initialX != null ? scoringData.initialX : range[0];
41638
- const startRel = scoringData.isInequality ? "ge" : "eq";
41639
- const correctRel = scoringData.correctRel || "eq";
41640
- const correctPos = number$1.equal(userInput.numLinePosition, scoringData.correctX || 0);
41641
- if (correctPos && correctRel === userInput.rel) {
41642
- return {
41643
- type: "points",
41644
- earned: 1,
41645
- total: 1,
41646
- message: null
41647
- };
41648
- }
41649
- if (userInput.numLinePosition === start && userInput.rel === startRel) {
41650
- // We're where we started.
41651
- return {
41652
- type: "invalid",
41653
- message: null
41654
- };
41655
- }
41656
- return {
41657
- type: "points",
41658
- earned: 0,
41659
- total: 1,
41660
- message: null
41661
- };
41662
- }
41663
-
41664
40000
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
41665
40001
  const MovablePoint = Graphie.MovablePoint;
41666
40002
  // @ts-expect-error - TS2339 - Property 'Line' does not exist on type 'typeof Graphie'.
@@ -42229,36 +40565,6 @@ const getPromptJSON$5 = (renderProps, userInput) => {
42229
40565
  };
42230
40566
  };
42231
40567
 
42232
- /**
42233
- * Checks user input from the orderer widget to see if the user has started
42234
- * ordering the options, making the widget scorable.
42235
- * @param userInput
42236
- * @see `scoreOrderer` for more details.
42237
- */
42238
- function validateOrderer(userInput) {
42239
- if (userInput.current.length === 0) {
42240
- return {
42241
- type: "invalid",
42242
- message: null
42243
- };
42244
- }
42245
- return null;
42246
- }
42247
-
42248
- function scoreOrderer(userInput, rubric) {
42249
- const validateError = validateOrderer(userInput);
42250
- if (validateError) {
42251
- return validateError;
42252
- }
42253
- const correct = _.isEqual(userInput.current, rubric.correctOptions.map(option => option.content));
42254
- return {
42255
- type: "points",
42256
- earned: correct ? 1 : 0,
42257
- total: 1,
42258
- message: null
42259
- };
42260
- }
42261
-
42262
40568
  class PlaceholderCard extends React.Component {
42263
40569
  render() {
42264
40570
  return /*#__PURE__*/React.createElement("div", {
@@ -43074,42 +41380,6 @@ const getPromptJSON$3 = () => {
43074
41380
  return getUnsupportedPromptJSON("plotter");
43075
41381
  };
43076
41382
 
43077
- const {
43078
- deepEq: deepEq$2
43079
- } = Util;
43080
-
43081
- /**
43082
- * Checks user input to confirm it is not the same as the starting values for the graph.
43083
- * This means the user has modified the graph, and the question can be scored.
43084
- *
43085
- * @see 'scorePlotter' for more details on scoring.
43086
- */
43087
- function validatePlotter(userInput, validationData) {
43088
- if (deepEq$2(userInput, validationData.starting)) {
43089
- return {
43090
- type: "invalid",
43091
- message: null
43092
- };
43093
- }
43094
- return null;
43095
- }
43096
-
43097
- const {
43098
- deepEq: deepEq$1
43099
- } = Util;
43100
- function scorePlotter(userInput, scoringData) {
43101
- const validationError = validatePlotter(userInput, scoringData);
43102
- if (validationError) {
43103
- return validationError;
43104
- }
43105
- return {
43106
- type: "points",
43107
- earned: deepEq$1(userInput, scoringData.correct) ? 1 : 0,
43108
- total: 1,
43109
- message: null
43110
- };
43111
- }
43112
-
43113
41383
  /* eslint-disable react/no-unsafe */
43114
41384
  class Plotter extends React.Component {
43115
41385
  constructor(..._args) {
@@ -43989,43 +42259,6 @@ const getPromptJSON$1 = userInput => {
43989
42259
  };
43990
42260
  };
43991
42261
 
43992
- /**
43993
- * Checks user input for the sorter widget to ensure that the user has made
43994
- * changes before attempting to score the widget.
43995
- * @param userInput
43996
- * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts'
43997
- * for more details on how the sorter widget is scored.
43998
- */
43999
- function validateSorter(userInput) {
44000
- // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44001
- // prevents the "Check" button from becoming active. We want the user
44002
- // to make a change before trying to move forward. This makes an
44003
- // assumption that the initial order isn't the correct order! However,
44004
- // this should be rare if it happens, and interacting with the list
44005
- // will enable the button, so they won't be locked out of progressing.
44006
- if (!userInput.changed) {
44007
- return {
44008
- type: "invalid",
44009
- message: null
44010
- };
44011
- }
44012
- return null;
44013
- }
44014
-
44015
- function scoreSorter(userInput, rubric) {
44016
- const validationError = validateSorter(userInput);
44017
- if (validationError) {
44018
- return validationError;
44019
- }
44020
- const correct = Util.deepEq(userInput.options, rubric.correct);
44021
- return {
44022
- type: "points",
44023
- earned: correct ? 1 : 0,
44024
- total: 1,
44025
- message: null
44026
- };
44027
- }
44028
-
44029
42262
  const {
44030
42263
  shuffle
44031
42264
  } = Util;
@@ -44118,81 +42351,6 @@ var Sorter$1 = {
44118
42351
  scorer: scoreSorter
44119
42352
  };
44120
42353
 
44121
- /**
44122
- * Filters the given table (modelled as a 2D array) to remove any rows that are
44123
- * completely empty.
44124
- *
44125
- * @returns A new table with only non-empty rows.
44126
- */
44127
- const filterNonEmpty = function filterNonEmpty(table) {
44128
- return table.filter(function (row) {
44129
- // Return only rows that are non-empty.
44130
- return row.some(cell => cell);
44131
- });
44132
- };
44133
-
44134
- function validateTable(userInput) {
44135
- const supplied = filterNonEmpty(userInput);
44136
- const hasEmptyCell = supplied.some(function (row) {
44137
- return row.some(function (cell) {
44138
- return cell === "";
44139
- });
44140
- });
44141
- if (hasEmptyCell || !supplied.length) {
44142
- return {
44143
- type: "invalid",
44144
- message: null
44145
- };
44146
- }
44147
- return null;
44148
- }
44149
-
44150
- function scoreTable(userInput, rubric, strings) {
44151
- const validationResult = validateTable(userInput);
44152
- if (validationResult != null) {
44153
- return validationResult;
44154
- }
44155
- const supplied = filterNonEmpty(userInput);
44156
- const solution = filterNonEmpty(rubric.answers);
44157
- if (supplied.length !== solution.length) {
44158
- return {
44159
- type: "points",
44160
- earned: 0,
44161
- total: 1,
44162
- message: null
44163
- };
44164
- }
44165
- const createValidator = KhanAnswerTypes.number.createValidatorFunctional;
44166
- let message = null;
44167
- const allCorrect = solution.every(function (rowSolution) {
44168
- for (let i = 0; i < supplied.length; i++) {
44169
- const rowSupplied = supplied[i];
44170
- const correct = rowSupplied.every(function (cellSupplied, i) {
44171
- const cellSolution = rowSolution[i];
44172
- const validator = createValidator(cellSolution, {
44173
- simplify: true
44174
- });
44175
- const result = validator(cellSupplied);
44176
- if (result.message) {
44177
- message = result.message;
44178
- }
44179
- return result.correct;
44180
- });
44181
- if (correct) {
44182
- supplied.splice(i, 1);
44183
- return true;
44184
- }
44185
- }
44186
- return false;
44187
- });
44188
- return {
44189
- type: "points",
44190
- earned: allCorrect ? 1 : 0,
44191
- total: 1,
44192
- message
44193
- };
44194
- }
44195
-
44196
42354
  const {
44197
42355
  assert
44198
42356
  } = InteractiveUtil;
@@ -44595,7 +42753,7 @@ const init = function init() {
44595
42753
 
44596
42754
  // This file is processed by a Rollup plugin (replace) to inject the production
44597
42755
  const libName = "@khanacademy/perseus";
44598
- const libVersion = "50.1.0";
42756
+ const libVersion = "51.0.0";
44599
42757
  addLibraryVersionToPerseusDebug(libName, libVersion);
44600
42758
 
44601
42759
  const apiVersion = {
@@ -46556,7 +44714,6 @@ const getValidWidgetIds = perseusItem => {
46556
44714
  return keys(widgets).filter(id => isWidgetIdInContent(perseusItem, id));
46557
44715
  };
46558
44716
 
46559
- const deepEq = Util.deepEq;
46560
44717
  function isRealJSONParse(jsonParse) {
46561
44718
  const randomPhrase = buildRandomPhrase();
46562
44719
  const randomHintPhrase = buildRandomPhrase();
@@ -46613,7 +44770,7 @@ function isRealJSONParse(jsonParse) {
46613
44770
  const testJSON = buildTestData(testingObject.replace(/"/g, '\\"'));
46614
44771
  const parsedTestJSON = jsonParse(testJSON);
46615
44772
  const parsedTestItemData = parsedTestJSON.data.assessmentItem.item.itemData;
46616
- return deepEq(parsedTestItemData, testingObject);
44773
+ return approximateDeepEqual(parsedTestItemData, testingObject);
46617
44774
  }
46618
44775
  function buildRandomString(capitalize = false) {
46619
44776
  let randomString = "";
@@ -46813,7 +44970,9 @@ function object(schema) {
46813
44970
  for (const [prop, propParser] of Object.entries(schema)) {
46814
44971
  const result = propParser(rawValue[prop], ctx.forSubtree(prop));
46815
44972
  if (isSuccess(result)) {
46816
- ret[prop] = result.value;
44973
+ if (result.value !== undefined || prop in rawValue) {
44974
+ ret[prop] = result.value;
44975
+ }
46817
44976
  } else {
46818
44977
  mismatches.push(...result.detail);
46819
44978
  }
@@ -48422,7 +46581,6 @@ const EditorJsonify = {
48422
46581
  * Main entry point
48423
46582
  */
48424
46583
  const GrapherUtil = {
48425
- allTypes,
48426
46584
  DEFAULT_GRAPHER_PROPS,
48427
46585
  chooseType,
48428
46586
  defaultPlotProps,
@@ -48437,5 +46595,5 @@ const ScoringUtil = {
48437
46595
  * Types
48438
46596
  */
48439
46597
 
48440
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getClockwiseAngle, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isFailure, isItemRenderableByVersion, isSuccess, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parseDataFromJSONP, parsePerseusItem, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
46598
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, addWidget, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isFailure, isItemRenderableByVersion, isSuccess, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parseDataFromJSONP, parsePerseusItem, preprocessTex, registerAllWidgetsForTesting, scorePerseusItem, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
48441
46599
  //# sourceMappingURL=index.js.map