@khanacademy/perseus 40.0.0 → 41.1.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 (44) hide show
  1. package/dist/components/hud.d.ts +4 -0
  2. package/dist/components/i18n-context.d.ts +4 -0
  3. package/dist/components/icon.d.ts +28 -27
  4. package/dist/components/image-loader.d.ts +9 -9
  5. package/dist/components/inline-icon.d.ts +6 -9
  6. package/dist/components/lint.d.ts +13 -0
  7. package/dist/components/math-input.d.ts +2 -2
  8. package/dist/components/multi-button-group.d.ts +26 -0
  9. package/dist/components/number-input.d.ts +19 -0
  10. package/dist/components/range-input.d.ts +3 -0
  11. package/dist/components/stub-tag-editor.d.ts +2 -2
  12. package/dist/components/svg-image.d.ts +30 -0
  13. package/dist/components/tooltip.d.ts +41 -30
  14. package/dist/es/index.css +6 -0
  15. package/dist/es/index.js +1034 -819
  16. package/dist/es/index.js.map +1 -1
  17. package/dist/es/strings.js +16 -2
  18. package/dist/es/strings.js.map +1 -1
  19. package/dist/icon-paths.d.ts +1 -5
  20. package/dist/index.css +6 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1062 -834
  23. package/dist/index.js.map +1 -1
  24. package/dist/strings.d.ts +8 -0
  25. package/dist/strings.js +22 -2
  26. package/dist/strings.js.map +1 -1
  27. package/dist/types.d.ts +5 -5
  28. package/dist/widgets/expression/expression.d.ts +4 -4
  29. package/dist/widgets/interactive-graphs/graphs/angle.d.ts +2 -5
  30. package/dist/widgets/interactive-graphs/graphs/circle.d.ts +2 -5
  31. package/dist/widgets/interactive-graphs/graphs/linear-system.d.ts +2 -5
  32. package/dist/widgets/interactive-graphs/graphs/linear.d.ts +2 -5
  33. package/dist/widgets/interactive-graphs/graphs/point.d.ts +7 -5
  34. package/dist/widgets/interactive-graphs/graphs/polygon.d.ts +2 -7
  35. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +2 -4
  36. package/dist/widgets/interactive-graphs/graphs/ray.d.ts +2 -5
  37. package/dist/widgets/interactive-graphs/graphs/screenreader-text.d.ts +1 -0
  38. package/dist/widgets/interactive-graphs/graphs/segment.d.ts +2 -5
  39. package/dist/widgets/interactive-graphs/graphs/sinusoid.d.ts +2 -5
  40. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +1 -1
  41. package/dist/widgets/interactive-graphs/types.d.ts +7 -2
  42. package/dist/widgets/label-image/label-image.d.ts +2 -2
  43. package/package.json +2 -2
  44. package/dist/widgets/interactive-graphs/graphs/index.d.ts +0 -9
package/dist/index.js CHANGED
@@ -367,7 +367,21 @@ const mockStrings = {
367
367
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
368
368
  addPoint: "Add Point",
369
369
  removePoint: "Remove Point",
370
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
370
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
371
+ srPointAtCoordinates: _ref27 => {
372
+ let {
373
+ x,
374
+ y
375
+ } = _ref27;
376
+ return `Point at ${x} comma ${y}`;
377
+ },
378
+ srInteractiveElements: _ref28 => {
379
+ let {
380
+ elements
381
+ } = _ref28;
382
+ return `Interactive elements: ${elements}`;
383
+ },
384
+ srNoInteractiveElements: "No interactive elements"
371
385
  };
372
386
 
373
387
  /**
@@ -375,11 +389,13 @@ const mockStrings = {
375
389
  * used inside the Perseus package.
376
390
  *
377
391
  */
378
- // @ts-expect-error - TS2322 - Type 'Context<{ strings: {}; locale: string; }>' is not assignable to type 'Context<I18nContextType>'.
379
- const PerseusI18nContext = /*#__PURE__*/React__namespace.createContext(process.env.NODE_ENV === "test" || process.env.STORYBOOK ? {
392
+ const mockPerseusI18nContext = {
380
393
  strings: mockStrings,
381
394
  locale: "en"
382
- } :
395
+ };
396
+
397
+ // @ts-expect-error - TS2322 - Type 'Context<{ strings: {}; locale: string; }>' is not assignable to type 'Context<I18nContextType>'.
398
+ const PerseusI18nContext = /*#__PURE__*/React__namespace.createContext(process.env.NODE_ENV === "test" || process.env.STORYBOOK ? mockPerseusI18nContext :
383
399
  // We want to return null here, not an empty object, so that we
384
400
  // are will throw an error when attempting to access the
385
401
  // undefined locale or strings, making it easier to debug.
@@ -664,7 +680,7 @@ class InnerMathInput extends React__namespace.Component {
664
680
  closeButtonVisible: true,
665
681
  style: styles$E.popoverContent
666
682
  }, /*#__PURE__*/React__namespace.createElement(mathInput.DesktopKeypad, _extends({
667
- onAnalyticsEvent: this.props.analytics.onAnalyticsEvent,
683
+ onAnalyticsEvent: this.props.onAnalyticsEvent,
668
684
  extraKeys: this.props.extraKeys,
669
685
  onClickKey: this.handleKeypadPress,
670
686
  cursorContext: this.state.cursorContext,
@@ -827,6 +843,9 @@ const getDependencies = () => {
827
843
  const DependenciesContext = /*#__PURE__*/React__namespace.createContext({
828
844
  analytics: {
829
845
  onAnalyticsEvent: async () => {}
846
+ },
847
+ useVideo: () => {
848
+ throw new Error("useVideo dependency not provided in Perseus dependencies");
830
849
  }
831
850
  });
832
851
 
@@ -967,7 +986,7 @@ function getTextWidth(text) {
967
986
  class TextListEditor extends React__namespace.Component {
968
987
  static propTypes = {
969
988
  options: PropTypes__default["default"].array,
970
- layout: PropTypes__default["default"].string,
989
+ layout: PropTypes__default["default"].oneOf(["horizontal", "vertical"]),
971
990
  onChange: PropTypes__default["default"].func.isRequired
972
991
  };
973
992
  static defaultProps = {
@@ -1085,6 +1104,8 @@ class TextListEditor extends React__namespace.Component {
1085
1104
  }
1086
1105
  }
1087
1106
 
1107
+ const EMPTY_ARRAY = [];
1108
+
1088
1109
  /**
1089
1110
  * Stub Tag Editor.
1090
1111
  *
@@ -1098,7 +1119,6 @@ class TextListEditor extends React__namespace.Component {
1098
1119
  * It also gives a nicer interface for the group metadata editor
1099
1120
  * in local demo mode.
1100
1121
  */
1101
- const EMPTY_ARRAY = [];
1102
1122
  class StubTagEditor extends React__namespace.Component {
1103
1123
  static propTypes = {
1104
1124
  value: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string),
@@ -2607,9 +2627,7 @@ class Expression extends React__namespace.Component {
2607
2627
  hasError: this.state.showErrorStyle,
2608
2628
  ariaLabel: this.props.ariaLabel || this.context.strings.mathInputBox,
2609
2629
  extraKeys: this.props.keypadConfiguration?.extraKeys,
2610
- analytics: this.props.analytics ?? {
2611
- onAnalyticsEvent: async () => {}
2612
- }
2630
+ onAnalyticsEvent: this.props.analytics?.onAnalyticsEvent ?? (async () => {})
2613
2631
  }))));
2614
2632
  }
2615
2633
  }
@@ -14942,11 +14960,7 @@ ___default["default"].extend(MovableLine$4.prototype, {
14942
14960
  * A stripped version of Icon.jsx from webapp. Takes an SVG icon and renders it
14943
14961
  * inline like Font Awesome did.
14944
14962
  *
14945
- * If you are looking for an icon that we've used before you should look in
14946
- * webapp's `icon-paths.js` which is a reference file for all the SVG paths
14947
- * that we've used. You'll need to copy the object from that file into
14948
- * whichever file you're using the icon and explicitly pass it in to the
14949
- * <InlineIcon/> React component.
14963
+ * You can refer to `icon-paths.ts` for all available SVG icons.
14950
14964
  *
14951
14965
  * We assume that the viewBox is cropped and aligned to (0, 0), but icons can
14952
14966
  * be defined differently. At some point we might want to add these attributes
@@ -14954,13 +14968,14 @@ ___default["default"].extend(MovableLine$4.prototype, {
14954
14968
  *
14955
14969
  * Sample usage:
14956
14970
  *
14957
- * const editIcon = {
14971
+ * ```
14972
+ * const editIcon = {
14958
14973
  * path: "M41.209 53.753l5.39 0l0 5.39l3.136 0l6.468-6.517-8.477-8.526-6.517 6.517l0 3.136zm33.173-34.937q-.882-.882-1.862.049l-19.6 19.6q-.931.98-.049 1.862t1.862-.049l19.6-19.6q.931-.98.049-1.862zm-38.563 45.668l0-16.121l37.632-37.632 16.17 16.121-37.632 37.632l-16.17 0zm43.022-12.397l0 10.633q-.049 6.713-4.753 11.417t-11.368 4.704l-46.599 0q-6.713 0-11.417-4.753t-4.704-11.368l0-46.599q0-6.664 4.753-11.417t11.368-4.704l46.599 0q3.528 0 6.566 1.372.833.392.98 1.323t-.49 1.617l-2.744 2.744q-.784.784-1.96.441t-2.352-.343l-46.599 0q-3.675 0-6.321 2.646t-2.646 6.321l0 46.599q0 3.675 2.646 6.321t6.321 2.646l46.599 0q3.675 0 6.321-2.646t2.646-6.321l0-7.056q0-.735.49-1.225l3.577-3.577q.833-.833 1.96-.392t1.127 1.617zm7.203-51.646q2.254 0 3.773 1.568l8.526 8.526q1.568 1.568 1.568 3.822t-1.568 3.773l-5.145 5.145-16.121-16.121 5.145-5.145q1.568-1.568 3.822-1.568z",
14959
14974
  * width: 100,
14960
14975
  * height: 78.912,
14961
- * };
14962
- * <InlineIcon {...editIcon} />
14963
- *
14976
+ * };
14977
+ * <InlineIcon {...editIcon} />
14978
+ * ```
14964
14979
  */
14965
14980
  const InlineIcon = _ref => {
14966
14981
  let {
@@ -14987,11 +15002,7 @@ const InlineIcon = _ref => {
14987
15002
  };
14988
15003
 
14989
15004
  /**
14990
- * Icon paths to be used with `inline-icon.jsx`.
14991
- *
14992
- * These paths are taken directly from webapp's `icon-paths.js`. Unlike the
14993
- * webapp equivalent, these can be directly required within Perseus files since
14994
- * this is all bundled together anyway.
15005
+ * Icon paths to be used with `inline-icon.tsx` and `icon.tsx`.
14995
15006
  */
14996
15007
 
14997
15008
  const iconCheck = {
@@ -16954,6 +16965,15 @@ const Status = {
16954
16965
  LOADED: "loaded",
16955
16966
  FAILED: "failed"
16956
16967
  };
16968
+ /**
16969
+ * Component to display an image (or other React components) while the desired
16970
+ * image is loading.
16971
+ *
16972
+ * Derived from
16973
+ * https://github.com/hzdg/react-imageloader/blob/master/src/index.js
16974
+ * to better suit our environment/build tools. Additionally, this one does
16975
+ * not introduce a wrapper element, which makes styling easier.
16976
+ */
16957
16977
  class ImageLoader extends React__namespace.Component {
16958
16978
  img;
16959
16979
  constructor(props) {
@@ -21158,7 +21178,7 @@ const Triangle = props => {
21158
21178
  width: 0,
21159
21179
  position: "absolute",
21160
21180
  left: props.left,
21161
- top: props["top"],
21181
+ top: props.top,
21162
21182
  borderLeft: borderLeft,
21163
21183
  borderRight: borderRight,
21164
21184
  borderTop: borderTop,
@@ -21239,6 +21259,47 @@ const HORIZONTAL_ALIGNMNENTS = {
21239
21259
  arrowLeft: arrowSize => -arrowSize - 2
21240
21260
  }
21241
21261
  };
21262
+ /**
21263
+ * DEPRECATED! Use Wonder Blocks tooltip instead.
21264
+ *
21265
+ * A generic tooltip library for React.js
21266
+ *
21267
+ * ```
21268
+ * import Tooltip from "./tooltip";
21269
+ * <Tooltip
21270
+ * className="class-for-tooltip-contents"
21271
+ * horizontalPosition={HoriziontalDirection.Left}
21272
+ * horizontalAlign={HoriziontalDirection.Left}
21273
+ * verticalPosition={VerticalDirection.Top}
21274
+ * arrowSize={10} // arrow size in pixels
21275
+ * borderColor="#ccc" // color of the border for the tooltip
21276
+ * show={true} // whether the tooltip should currently be visible
21277
+ * targetContainerStyle={targetContainerStyle}
21278
+ * >
21279
+ * <TargetElementOfTheTooltip />
21280
+ * <TooltipContents1 />
21281
+ * <TooltipContents2 />
21282
+ * </Tooltip>
21283
+ * ```
21284
+ *
21285
+ * To show/hide the tooltip, the parent component should call the
21286
+ * `.show()` and `.hide()` methods of the tooltip when appropriate.
21287
+ * (These are usually set up as handlers of events on the target element.)
21288
+ *
21289
+ * Notes:
21290
+ * `className` should not specify a border; that is handled by `borderColor`
21291
+ * so that the arrow and tooltip match
21292
+ *
21293
+ * ```
21294
+ * __,,--``\\
21295
+ * _,,-''`` \\ ,
21296
+ * '----------_.------'-.___|\__
21297
+ * _.--''`` `)__ )__ @\__
21298
+ * ( .. ''---/___,,E/__,E'------`
21299
+ * `-''`''
21300
+ * Here be dragons.
21301
+ * ```
21302
+ */
21242
21303
  class Tooltip extends React__namespace.Component {
21243
21304
  static defaultProps = {
21244
21305
  className: "",
@@ -21466,10 +21527,7 @@ class InputWithExamples extends React__namespace.Component {
21466
21527
  return "- " + example;
21467
21528
  }).join("\n");
21468
21529
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21469
- return /*#__PURE__*/React__namespace.createElement(Tooltip
21470
- // eslint-disable-next-line react/no-string-refs
21471
- , {
21472
- ref: "tooltip",
21530
+ return /*#__PURE__*/React__namespace.createElement(Tooltip, {
21473
21531
  className: "perseus-formats-tooltip preview-measure",
21474
21532
  horizontalPosition: HorizontalDirection.Left,
21475
21533
  horizontalAlign: HorizontalDirection.Left,
@@ -24966,6 +25024,37 @@ const BASE_ICON_SIZE = 10;
24966
25024
  // If you pass in an object you need to specify an SVG path to
24967
25025
  // render along with the width and height of the path (to ensure
24968
25026
  // that it gets scaled correctly).
25027
+ /**
25028
+ * An SVG Icon
25029
+ *
25030
+ * This component is designed to take in SVG paths and render icons based upon
25031
+ * them. If you are looking for an icon that we've used before you should look
25032
+ * in `icon-paths.js` which is a reference file for all the SVG paths that
25033
+ * we've used. You'll need to copy the object from that file into whichever
25034
+ * file you're using the icon and explicitly pass it in to the <Icon/> React
25035
+ * component.
25036
+ *
25037
+ * Sample usage:
25038
+ *
25039
+ * ```
25040
+ * const dropdownIcon = `M5,6L0,0L10,0`;
25041
+ * <Icon icon={dropdownIcon} />
25042
+ * ```
25043
+ *
25044
+ * Or:
25045
+ *
25046
+ * ```
25047
+ * const dropdownIcon = {
25048
+ * path: `M5,6L0,0L10,0`,
25049
+ * height: 10,
25050
+ * width: 10,
25051
+ * };
25052
+ * <Icon icon={dropdownIcon} size={20} />
25053
+ * ```
25054
+ *
25055
+ * If you want to add an entirely new icon please read the note inside
25056
+ * the `icon-paths.ts` file.
25057
+ */
24969
25058
  class Icon extends React__namespace.Component {
24970
25059
  static defaultProps = {
24971
25060
  color: "currentColor"
@@ -25189,7 +25278,7 @@ function ChoiceInner(props) {
25189
25278
  } = usePerseusI18n();
25190
25279
  const letter = getChoiceLetter(pos, strings);
25191
25280
  if (!showCorrectness) {
25192
- return letter;
25281
+ return /*#__PURE__*/React__namespace.createElement("span", null, letter);
25193
25282
  }
25194
25283
  if (correct) {
25195
25284
  return /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconCheck, {
@@ -25259,7 +25348,6 @@ const ChoiceIcon = function (props) {
25259
25348
  visible: focused || hovered,
25260
25349
  multipleSelect: multipleSelect
25261
25350
  }, /*#__PURE__*/React__namespace.createElement("div", {
25262
- // @ts-expect-error - TS2322 - Type '{ backgroundColor: string | null | undefined; borderColor: string; color: string; borderRadius: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
25263
25351
  style: dynamicStyles,
25264
25352
  "data-testid": "choice-icon__library-choice-icon",
25265
25353
  className: aphrodite.css(styles$s.circle, showCorrectness && correct && styles$s.circleCorrect, showCorrectness && !correct && styles$s.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$s.circleIncorrectAnswered)
@@ -31173,6 +31261,10 @@ const HiddenIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
31173
31261
  }, /*#__PURE__*/React__namespace.createElement("path", {
31174
31262
  d: "M0 0h24v24H0z"
31175
31263
  }))));
31264
+ /**
31265
+ * A "heads-up display" (HUD) indicator that includes a short message (usually
31266
+ * used for linting errors). The indicator can be disabled.
31267
+ */
31176
31268
  const HUD = _ref => {
31177
31269
  let {
31178
31270
  message,
@@ -31386,18 +31478,24 @@ const {
31386
31478
  const toNumericString = KhanMath.toNumericString;
31387
31479
  const getNumericFormat = KhanMath.getNumericFormat;
31388
31480
 
31389
- /* An input box that accepts only numeric strings
31481
+ /**
31482
+ * An input box that accepts only numeric strings
31390
31483
  *
31391
- * Calls onChange(value, format) for valid numbers.
31392
- * Reverts to the current value onBlur or on [ENTER],
31484
+ * Calls `onChange(value, format)` for valid numbers.
31485
+ *
31486
+ * Reverts to the current value `onBlur` or on [ENTER],
31393
31487
  * but maintains the format (i.e. 3/2, 1 1/2, 150%)
31394
- * Accepts empty input and sends it to onChange as null
31395
- * if no numeric placeholder is set.
31396
- * If given a checkValidity function, will turn
31397
- * the background/outline red when invalid
31398
- * If useArrowKeys is set to true, up/down arrows will
31399
- * increment/decrement integers
31400
- * Optionally takes a size ("mini", "small", "normal")
31488
+ *
31489
+ * Accepts empty input and sends it to `onChange` as `null` if no numeric
31490
+ * placeholder is set.
31491
+ *
31492
+ * If given a `checkValidity` function, will turn the background/outline red
31493
+ * when invalid.
31494
+ *
31495
+ * If `useArrowKeys` is set to `true`, up/down arrows will increment/decrement
31496
+ * integers.
31497
+ *
31498
+ * Optionally takes a `size` (`"mini"`, `"small"`,` `"normal"`)
31401
31499
  */
31402
31500
  class NumberInput extends React__namespace.Component {
31403
31501
  static contextType = PerseusI18nContext;
@@ -31408,7 +31506,7 @@ class NumberInput extends React__namespace.Component {
31408
31506
  onChange: PropTypes__default["default"].func.isRequired,
31409
31507
  onFormatChange: PropTypes__default["default"].func,
31410
31508
  checkValidity: PropTypes__default["default"].func,
31411
- size: PropTypes__default["default"].string,
31509
+ size: PropTypes__default["default"].oneOf(["mini", "small", "normal"]),
31412
31510
  label: PropTypes__default["default"].oneOf(["put your labels outside your inputs!"])
31413
31511
  };
31414
31512
  static defaultProps = {
@@ -31578,8 +31676,8 @@ class NumberInput extends React__namespace.Component {
31578
31676
 
31579
31677
  const truth = () => true;
31580
31678
 
31581
- /* A minor abstraction on top of NumberInput for ranges
31582
- *
31679
+ /**
31680
+ * A minor abstraction on top of `NumberInput` for ranges
31583
31681
  */
31584
31682
  class RangeInput extends React__namespace.Component {
31585
31683
  static propTypes = {
@@ -32423,6 +32521,335 @@ function reinitialize(params) {
32423
32521
  };
32424
32522
  }
32425
32523
 
32524
+ /**
32525
+ * Wrapper with Mafs transform styles for SVG elements.
32526
+ * Scales and translates the SVG element to match the Mafs view.
32527
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
32528
+ */
32529
+ const MafsCssTransformWrapper = _ref => {
32530
+ let {
32531
+ children
32532
+ } = _ref;
32533
+ return /*#__PURE__*/React__namespace.createElement("g", {
32534
+ style: {
32535
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
32536
+ }
32537
+ }, children);
32538
+ };
32539
+
32540
+ const TextLabel = _ref => {
32541
+ let {
32542
+ children,
32543
+ ...rest
32544
+ } = _ref;
32545
+ return /*#__PURE__*/React__default["default"].createElement(mafs.Text, _extends({
32546
+ size: 16,
32547
+ svgTextProps: {
32548
+ filter: "url(#background)",
32549
+ fontWeight: "bold"
32550
+ }
32551
+ }, rest), children);
32552
+ };
32553
+ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs", null, /*#__PURE__*/React__default["default"].createElement("filter", {
32554
+ id: "background",
32555
+ x: "-5%",
32556
+ width: "110%",
32557
+ y: "0%",
32558
+ height: "100%"
32559
+ }, /*#__PURE__*/React__default["default"].createElement("feFlood", {
32560
+ floodColor: "#FFF",
32561
+ floodOpacity: "0.64"
32562
+ }), /*#__PURE__*/React__default["default"].createElement("feComposite", {
32563
+ operator: "over",
32564
+ in: "SourceGraphic"
32565
+ })));
32566
+
32567
+ const PolygonAngle = _ref => {
32568
+ let {
32569
+ centerPoint,
32570
+ endPoints,
32571
+ range,
32572
+ polygonLines,
32573
+ showAngles,
32574
+ snapTo
32575
+ } = _ref;
32576
+ const [centerX, centerY] = centerPoint;
32577
+ const areClockwise = clockwise$1([centerPoint, ...endPoints]);
32578
+ const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
32579
+ const radius = 0.3;
32580
+ const a = mafs.vec.dist(centerPoint, endPoints[0]);
32581
+ const b = mafs.vec.dist(centerPoint, endPoints[1]);
32582
+ const c = mafs.vec.dist(endPoints[0], endPoints[1]);
32583
+
32584
+ // Law of cosines
32585
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
32586
+ const y1 = centerY + (startY - centerY) / a * radius;
32587
+ const x2 = centerX + (endX - centerX) / b * radius;
32588
+ const x1 = centerX + (startX - centerX) / a * radius;
32589
+ const y2 = centerY + (endY - centerY) / b * radius;
32590
+
32591
+ // Fourth point that would create a rhomboid
32592
+ // Used to calculate the inside of the angle and to create square
32593
+ const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
32594
+ if (!showAngles) {
32595
+ return isRightPolygonAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
32596
+ start: [x1, y1],
32597
+ vertex: [x2, y2],
32598
+ end: [x3, y3]
32599
+ }) : null;
32600
+ }
32601
+
32602
+ // Midpoint betwen ends of arc
32603
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
32604
+ const largeArcFlag = isOutside ? 1 : 0;
32605
+ const sweepFlag = isOutside ? 1 : 0;
32606
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32607
+ let angleInDegrees = angle * (180 / Math.PI);
32608
+ // If we have triggered "largArcFlag", the angle should be greater than 180
32609
+ if (isOutside) {
32610
+ angleInDegrees = 360 - angleInDegrees;
32611
+ }
32612
+
32613
+ // Only want to show whole numbers when in angles mode
32614
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
32615
+ const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
32616
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
32617
+ id: "background",
32618
+ x: "-5%",
32619
+ width: "110%",
32620
+ y: "0%",
32621
+ height: "100%"
32622
+ }, /*#__PURE__*/React__namespace.createElement("feFlood", {
32623
+ floodColor: "#FFF",
32624
+ floodOpacity: "0.5"
32625
+ }), /*#__PURE__*/React__namespace.createElement("feComposite", {
32626
+ operator: "over",
32627
+ in: "SourceGraphic"
32628
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
32629
+ start: [x1, y1],
32630
+ vertex: [x2, y2],
32631
+ end: [x3, y3]
32632
+ }) : /*#__PURE__*/React__namespace.createElement(Arc, {
32633
+ arc: arc
32634
+ }), /*#__PURE__*/React__namespace.createElement(TextLabel, {
32635
+ x: x3,
32636
+ y: y3,
32637
+ attach: y3 - centerY > 0 ? "s" : "n",
32638
+ attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
32639
+ }, angleLabel, "\xB0"));
32640
+ };
32641
+ const Angle = _ref2 => {
32642
+ let {
32643
+ vertex,
32644
+ coords,
32645
+ showAngles,
32646
+ allowReflexAngles,
32647
+ range
32648
+ } = _ref2;
32649
+ // Check if the points are clockwise or not, depending on whether we allow reflex angles
32650
+ const areClockwise = clockwise$1([...coords, vertex]);
32651
+ const shouldReverseCoords = areClockwise && !allowReflexAngles;
32652
+
32653
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
32654
+ const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32655
+
32656
+ // Calculate the angles between the two points
32657
+ const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32658
+ const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
32659
+ const angle = (startAngle + 360 - endAngle) % 360;
32660
+
32661
+ // Check if the angle is reflexive
32662
+ const isReflexive = angle > 180;
32663
+
32664
+ // Break out the necessary points for the arc calculation
32665
+ const [centerX, centerY] = vertex;
32666
+ const [point1, point2] = clockwiseCoords;
32667
+ const [startX, startY] = point1;
32668
+ const [endX, endY] = point2;
32669
+ const a = mafs.vec.dist(vertex, point1);
32670
+ const b = mafs.vec.dist(vertex, point2);
32671
+
32672
+ // Set the radius of the arc
32673
+ const radius = 2;
32674
+
32675
+ // Calculate the end points of the arc
32676
+ const y1 = centerY + (startY - centerY) / a * radius;
32677
+ const x2 = centerX + (endX - centerX) / b * radius;
32678
+ const x1 = centerX + (startX - centerX) / a * radius;
32679
+ const y2 = centerY + (endY - centerY) / b * radius;
32680
+
32681
+ // Used to calculate the inside of the angle and to create square
32682
+ const [x3, y3] = mafs.vec.add(vertex, mafs.vec.add(mafs.vec.sub([x1, y1], vertex), mafs.vec.sub([x2, y2], vertex)));
32683
+
32684
+ // Determine whether the arc should be drawn outside the angle
32685
+ const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
32686
+
32687
+ // Determine the flags for the arc
32688
+ // If the angle is outside OR reflexive, we want to draw a large arc
32689
+ // If the angle is outside AND reflexive, we want to sweep the arc
32690
+ const largeArcFlag = isOutside || isReflexive ? 1 : 0;
32691
+ const sweepFlag = isOutside && isReflexive ? 1 : 0;
32692
+
32693
+ // Create the SVG path for the arc
32694
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32695
+
32696
+ // We only ever want to show whole angles
32697
+ const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
32698
+
32699
+ // Calculate the text position based on the angle and whether we allow reflex angles
32700
+ // Let's try the angle bisector method to find the midpoint of the arc
32701
+ const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
32702
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
32703
+ id: "background",
32704
+ x: "-5%",
32705
+ width: "110%",
32706
+ y: "0%",
32707
+ height: "100%"
32708
+ }, /*#__PURE__*/React__namespace.createElement("feFlood", {
32709
+ floodColor: "#FFF",
32710
+ floodOpacity: "0.5"
32711
+ }), /*#__PURE__*/React__namespace.createElement("feComposite", {
32712
+ operator: "over",
32713
+ in: "SourceGraphic"
32714
+ }))), isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
32715
+ start: [x1, y1],
32716
+ vertex: [x2, y2],
32717
+ end: [x3, y3],
32718
+ className: "arc-right-angle"
32719
+ }) : /*#__PURE__*/React__namespace.createElement(Arc, {
32720
+ arc: arc,
32721
+ className: "angle-arc"
32722
+ }), showAngles && /*#__PURE__*/React__namespace.createElement(TextLabel, {
32723
+ x: textX,
32724
+ y: textY,
32725
+ color: wonderBlocksTokens.color.blue
32726
+ }, angleLabel, "\xB0"));
32727
+ };
32728
+
32729
+ /**
32730
+ * This is broken out into its own component so that it can be used for an early return
32731
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
32732
+ */
32733
+ const RightAngleSquare = _ref3 => {
32734
+ let {
32735
+ start: [x1, y1],
32736
+ vertex: [x2, y2],
32737
+ end: [x3, y3],
32738
+ className
32739
+ } = _ref3;
32740
+ return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
32741
+ d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
32742
+ strokeWidth: 0.02,
32743
+ fill: "none",
32744
+ className: className
32745
+ }));
32746
+ };
32747
+
32748
+ // We're conditionally adding the class name here so that we can style the arc differently
32749
+ // based on whether it's an angle or a polygon angle
32750
+ const Arc = _ref4 => {
32751
+ let {
32752
+ arc,
32753
+ className
32754
+ } = _ref4;
32755
+ return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
32756
+ d: arc,
32757
+ strokeWidth: 0.02,
32758
+ fill: "none",
32759
+ className: className
32760
+ }));
32761
+ };
32762
+ const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
32763
+ const isRightAngle = angle => Math.round(angle) === 90;
32764
+
32765
+ /**
32766
+ * Determines if an angle is an inside (false) or outside (true) angle.
32767
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
32768
+ * Uses the priciple that a ray from a point inside a polygon will intersect
32769
+ * with an odd number of lines, while a ray from a point outside the polygon
32770
+ * will intersect with an even number of lines.
32771
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
32772
+ */
32773
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
32774
+ // Create a ray from the midpoint (inside angle) to the edge of the range
32775
+ const rangeIntersectionPoint = getIntersectionOfRayWithBox(midpoint, vertex, range);
32776
+ let lineIntersectionCount = 0;
32777
+ polygonLines.forEach(line => segmentsIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
32778
+
32779
+ // If the number of intersections is even, the angle is inside the polygon
32780
+ return !isEven(lineIntersectionCount);
32781
+ };
32782
+ const isEven = n => n % 2 === 0;
32783
+
32784
+ // This function calculates the bisector point of an angle formed by two points
32785
+ // and a vertex. It is used to position the angle label so that it is always
32786
+ // outside the arc at the exact midpoint of the angle.
32787
+ // Reflex angles make use of polar coordinates to determine the correct direction
32788
+ function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
32789
+ const [originX, originY] = vertex;
32790
+ const [x1, y1] = point1;
32791
+ const [x2, y2] = point2;
32792
+
32793
+ // Convert the Cartesian Coordinates to vectors
32794
+ const vectorA = [x1 - originX, y1 - originY];
32795
+ const vectorB = [x2 - originX, y2 - originY];
32796
+
32797
+ // Calculate the average angle of the vectors
32798
+ // so that we can point the bisector in the correct direction using polar coordinates
32799
+ const angleA = Math.atan2(vectorA[1], vectorA[0]);
32800
+ const angleB = Math.atan2(vectorB[1], vectorB[0]);
32801
+
32802
+ // Calculate the average of these two angles
32803
+ let averageAngle = (angleA + angleB) / 2;
32804
+ const angleRadians = Math.abs(angleA - angleB);
32805
+
32806
+ // Adjust the average angle to point in the correct direction
32807
+ // and to account for reflex angles and a bug with 180 degrees
32808
+ // that occurs due to our angleOffsetDegrees option in the graph
32809
+ if (allowReflex) {
32810
+ // If the resulting angles are less than 180 degrees,
32811
+ // or if the user has flipped the points on the interactive element
32812
+ // ensure that we have the correct direction for the reflex angle
32813
+ if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
32814
+ averageAngle += Math.PI; // Add 180 degrees
32815
+ }
32816
+ } else {
32817
+ // If the resulting angles are greater than 180 degrees, adjust
32818
+ // to get the correct angle, as we already know that the angle is not reflexive
32819
+ // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
32820
+ if (angleRadians > Math.PI) {
32821
+ averageAngle -= Math.PI; // Subract 180 degrees
32822
+ }
32823
+ }
32824
+
32825
+ // Convert the average angle back to Cartesian coordinates
32826
+ const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
32827
+
32828
+ // Calculate the magnitude of the sum to normalize it
32829
+ const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
32830
+ const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
32831
+
32832
+ // Calculate the initial distance of the bisector direction from the origin
32833
+ const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
32834
+
32835
+ // Determine the minimum radius to ensure that the text is always outside the arc
32836
+ const requiredDistance = arcRadius * 1.75;
32837
+ let radius = requiredDistance / initialDistance;
32838
+
32839
+ // Apply radius only if the initial distance is less than required
32840
+ if (initialDistance >= requiredDistance) {
32841
+ radius = 1; // Use the unit vector as is
32842
+ }
32843
+
32844
+ // Scale the bisector direction by the radius
32845
+ const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
32846
+
32847
+ // Add the vertex to the bisector point to get the final position
32848
+ // to ensure that the angle label moves with the interactive element
32849
+ const scaledBisector = mafs.vec.add(bisectorPoint, vertex);
32850
+ return scaledBisector;
32851
+ }
32852
+
32426
32853
  /**
32427
32854
  * 44 is touch best practice and AAA compliant for WCAG
32428
32855
  * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
@@ -32630,6 +33057,14 @@ function range(min, max) {
32630
33057
  return result;
32631
33058
  }
32632
33059
 
33060
+ function srFormatNumber(a, locale) {
33061
+ // adding zero here converts negative zero to positive zero.
33062
+ return (0 + a).toLocaleString(locale, {
33063
+ maximumFractionDigits: 3,
33064
+ useGrouping: false // no thousands separators
33065
+ });
33066
+ }
33067
+
32633
33068
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
32634
33069
  // graph.
32635
33070
  const hitboxSizePx = 48;
@@ -32749,6 +33184,10 @@ function useControlPoint(params) {
32749
33184
  onBlur = noop$1,
32750
33185
  forwardedRef = noop$1
32751
33186
  } = params;
33187
+ const {
33188
+ strings,
33189
+ locale
33190
+ } = usePerseusI18n();
32752
33191
  const [focused, setFocused] = React.useState(false);
32753
33192
  const focusableHandleRef = React.useRef(null);
32754
33193
  useDraggable({
@@ -32774,6 +33213,15 @@ function useControlPoint(params) {
32774
33213
  className: "movable-point__focusable-handle",
32775
33214
  tabIndex: disableKeyboardInteraction ? -1 : 0,
32776
33215
  ref: focusableHandleRef,
33216
+ role: "button",
33217
+ "aria-label": strings.srPointAtCoordinates({
33218
+ x: srFormatNumber(point[X], locale),
33219
+ y: srFormatNumber(point[Y], locale)
33220
+ })
33221
+ // aria-live="assertive" causes the new location of the point to be
33222
+ // announced immediately on move.
33223
+ ,
33224
+ "aria-live": "assertive",
32777
33225
  onFocus: event => {
32778
33226
  onFocus(event);
32779
33227
  setFocused(true);
@@ -32953,25 +33401,351 @@ function trimRange(range, graphDimensionsInPixels) {
32953
33401
  return inset([graphUnitsToTrimX, graphUnitsToTrimY], range);
32954
33402
  }
32955
33403
 
32956
- const SegmentGraph = props => {
33404
+ const MovablePoint$1 = /*#__PURE__*/React__namespace.forwardRef((props, pointRef) => {
32957
33405
  const {
32958
- dispatch
33406
+ visiblePoint,
33407
+ focusableHandle
33408
+ } = useControlPoint({
33409
+ ...props,
33410
+ forwardedRef: pointRef
33411
+ });
33412
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle, visiblePoint);
33413
+ });
33414
+
33415
+ function renderAngleGraph(state, dispatch) {
33416
+ return {
33417
+ graph: /*#__PURE__*/React__namespace.createElement(AngleGraph, {
33418
+ graphState: state,
33419
+ dispatch: dispatch
33420
+ }),
33421
+ interactiveElementsDescription: null
33422
+ };
33423
+ }
33424
+ function AngleGraph(props) {
33425
+ const {
33426
+ dispatch,
33427
+ graphState
32959
33428
  } = props;
32960
33429
  const {
32961
- coords: segments
32962
- } = props.graphState;
32963
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
32964
- key: i,
32965
- points: segment,
32966
- onMoveLine: delta => {
32967
- dispatch(actions.segment.moveLine(i, delta));
32968
- },
32969
- onMovePoint: (endpointIndex, destination) => {
32970
- dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
33430
+ graphDimensionsInPixels
33431
+ } = useGraphConfig();
33432
+ const {
33433
+ coords,
33434
+ showAngles,
33435
+ range,
33436
+ allowReflexAngles,
33437
+ snapDegrees
33438
+ } = graphState;
33439
+
33440
+ // Break the coords into the two end points and the center point
33441
+ const endPoints = [coords[0], coords[2]];
33442
+ const centerPoint = coords[1];
33443
+
33444
+ // Convert the vectors to pixels for rendering the svg lines
33445
+ const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
33446
+ const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
33447
+
33448
+ // Create the SVG lines for the angle
33449
+ const svgLines = linePixelCoords.map((_ref, i) => {
33450
+ let [startPtPx, endPtPx] = _ref;
33451
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
33452
+ const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
33453
+ return /*#__PURE__*/React__namespace.createElement("g", {
33454
+ key: `line-${i}`
33455
+ }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
33456
+ start: startPtPx,
33457
+ end: endPtPx,
33458
+ style: {
33459
+ stroke: "var(--movable-line-stroke-color)",
33460
+ strokeWidth: "var(--movable-line-stroke-weight)"
33461
+ }
33462
+ }), /*#__PURE__*/React__namespace.createElement(Vector, {
33463
+ tail: angleLines[i][1],
33464
+ tip: endExtend,
33465
+ color: "var(--movable-line-stroke-color)"
33466
+ }));
33467
+ });
33468
+
33469
+ // Create the angle indicator parameters
33470
+ const angleParams = {
33471
+ vertex: centerPoint,
33472
+ coords: endPoints,
33473
+ allowReflexAngles: allowReflexAngles || false,
33474
+ // Whether to allow reflex angles or not
33475
+ snapDegrees: snapDegrees || 1,
33476
+ // The multiple of degrees to snap to
33477
+ range: range,
33478
+ showAngles: showAngles || false // Whether to show the angle or not
33479
+ };
33480
+
33481
+ // Render the lines, angle, and then movable points
33482
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33483
+ point: coords[1],
33484
+ constrain: p => p,
33485
+ onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33486
+ }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33487
+ point: coords[0],
33488
+ constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33489
+ onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33490
+ }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33491
+ point: coords[2],
33492
+ constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33493
+ onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33494
+ }));
33495
+ }
33496
+ const positiveX = [1, 0];
33497
+ const negativeX = [-1, 0];
33498
+ const positiveY = [0, 1];
33499
+ const negativeY = [0, -1];
33500
+ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33501
+ const currentAngle = calculateAngleInDegrees(mafs.vec.sub(sidePoint, vertex));
33502
+
33503
+ // Find the rays that start at the current point and point up, down, left
33504
+ // and right.
33505
+ const leftRay = [sidePoint, mafs.vec.add(sidePoint, negativeX)];
33506
+ const rightRay = [sidePoint, mafs.vec.add(sidePoint, positiveX)];
33507
+ const upRay = [sidePoint, mafs.vec.add(sidePoint, positiveY)];
33508
+ const downRay = [sidePoint, mafs.vec.add(sidePoint, negativeY)];
33509
+
33510
+ // find the angles that lie one snap step clockwise and counter-clockwise
33511
+ // from the current angle. These are the angles to which the side can be
33512
+ // moved.
33513
+ const oneStepCounterClockwise = currentAngle + snapDegrees;
33514
+ const oneStepClockwise = currentAngle - snapDegrees;
33515
+
33516
+ // find the rays that start from the vertex and point in the direction of
33517
+ // the angles we just computed.
33518
+ const counterClockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepCounterClockwise))];
33519
+ const clockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepClockwise))];
33520
+
33521
+ // find the intersections of those rays with the horizontal and vertical
33522
+ // rays extending from the sidePoint. These intersections are the points to
33523
+ // which sidePoint can move that will result in a rotation of `snapDegrees`.
33524
+ const left = findIntersectionOfRays(leftRay, counterClockwiseRay) ?? findIntersectionOfRays(leftRay, clockwiseRay);
33525
+ const right = findIntersectionOfRays(rightRay, counterClockwiseRay) ?? findIntersectionOfRays(rightRay, clockwiseRay);
33526
+ const up = findIntersectionOfRays(upRay, counterClockwiseRay) ?? findIntersectionOfRays(upRay, clockwiseRay);
33527
+ const down = findIntersectionOfRays(downRay, counterClockwiseRay) ?? findIntersectionOfRays(downRay, clockwiseRay);
33528
+ return {
33529
+ up: up ?? sidePoint,
33530
+ down: down ?? sidePoint,
33531
+ left: left ?? sidePoint,
33532
+ right: right ?? sidePoint
33533
+ };
33534
+ }
33535
+
33536
+ function getGradableGraph(state, initialGraph) {
33537
+ if (!state.hasBeenInteractedWith) {
33538
+ return {
33539
+ ...initialGraph
33540
+ };
33541
+ }
33542
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
33543
+ return {
33544
+ ...initialGraph,
33545
+ coords: state.coords
33546
+ };
33547
+ }
33548
+ if (state.type === "segment" && initialGraph.type === "segment") {
33549
+ return {
33550
+ ...initialGraph,
33551
+ coords: state.coords
33552
+ };
33553
+ }
33554
+ if (state.type === "linear" && initialGraph.type === "linear") {
33555
+ return {
33556
+ ...initialGraph,
33557
+ coords: state.coords
33558
+ };
33559
+ }
33560
+ if (state.type === "ray" && initialGraph.type === "ray") {
33561
+ return {
33562
+ ...initialGraph,
33563
+ coords: state.coords
33564
+ };
33565
+ }
33566
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
33567
+ return {
33568
+ ...initialGraph,
33569
+ coords: state.coords
33570
+ };
33571
+ }
33572
+ if (state.type === "point" && initialGraph.type === "point") {
33573
+ return {
33574
+ ...initialGraph,
33575
+ coords: state.coords
33576
+ };
33577
+ }
33578
+ if (state.type === "circle" && initialGraph.type === "circle") {
33579
+ return {
33580
+ ...initialGraph,
33581
+ center: state.center,
33582
+ radius: getRadius(state)
33583
+ };
33584
+ }
33585
+ if (state.type === "quadratic" && initialGraph.type === "quadratic") {
33586
+ return {
33587
+ ...initialGraph,
33588
+ coords: state.coords
33589
+ };
33590
+ }
33591
+ if (state.type === "sinusoid" && initialGraph.type === "sinusoid") {
33592
+ return {
33593
+ ...initialGraph,
33594
+ coords: state.coords
33595
+ };
33596
+ }
33597
+ if (state.type === "angle" && initialGraph.type === "angle") {
33598
+ // We're going to reverse the coords for scoring if the angle is clockwise and
33599
+ // we don't allow reflex angles. This is because the angle is largely defined
33600
+ // by the order of the points in the coords array, and we want to maintain
33601
+ // the same angle scoring with the legacy graph (which had a bug).
33602
+ // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33603
+ const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
33604
+ const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33605
+ const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33606
+ return {
33607
+ ...initialGraph,
33608
+ coords,
33609
+ allowReflexAngles: state.allowReflexAngles
33610
+ };
33611
+ }
33612
+ if (state.type === "none" && initialGraph.type === "none") {
33613
+ return {
33614
+ type: "none"
33615
+ };
33616
+ }
33617
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
33618
+ }
33619
+
33620
+ /**
33621
+ * determine radius of a circle graph
33622
+ *
33623
+ * @param graph - the graph object containing the circle
33624
+ * @returns the radius of the circle
33625
+ */
33626
+ function getRadius(graph) {
33627
+ const [centerX, centerY] = graph.center;
33628
+ const [edgeX, edgeY] = graph.radiusPoint;
33629
+ return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
33630
+ }
33631
+
33632
+ function renderCircleGraph(state, dispatch) {
33633
+ return {
33634
+ graph: /*#__PURE__*/React__namespace.createElement(CircleGraph, {
33635
+ graphState: state,
33636
+ dispatch: dispatch
33637
+ }),
33638
+ interactiveElementsDescription: null
33639
+ };
33640
+ }
33641
+ function CircleGraph(props) {
33642
+ const {
33643
+ dispatch,
33644
+ graphState
33645
+ } = props;
33646
+ const {
33647
+ center,
33648
+ radiusPoint
33649
+ } = graphState;
33650
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableCircle, {
33651
+ center: center,
33652
+ radius: getRadius(graphState),
33653
+ onMove: c => dispatch(actions.circle.moveCenter(c))
33654
+ }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33655
+ point: radiusPoint,
33656
+ cursor: "ew-resize",
33657
+ onMove: newRadiusPoint => {
33658
+ dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
32971
33659
  }
32972
- })));
32973
- };
33660
+ }));
33661
+ }
33662
+ function MovableCircle(props) {
33663
+ const {
33664
+ center,
33665
+ radius,
33666
+ onMove
33667
+ } = props;
33668
+ const {
33669
+ snapStep,
33670
+ disableKeyboardInteraction
33671
+ } = useGraphConfig();
33672
+ const draggableRef = React.useRef(null);
33673
+ const {
33674
+ dragging
33675
+ } = useDraggable({
33676
+ gestureTarget: draggableRef,
33677
+ point: center,
33678
+ onMove,
33679
+ constrainKeyboardMovement: p => snap(snapStep, p)
33680
+ });
33681
+ const [centerPx] = useTransformVectorsToPixels(center);
33682
+ const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
33683
+ return /*#__PURE__*/React__namespace.createElement("g", {
33684
+ ref: draggableRef,
33685
+ tabIndex: disableKeyboardInteraction ? -1 : 0,
33686
+ className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
33687
+ }, /*#__PURE__*/React__namespace.createElement("ellipse", {
33688
+ className: "focus-ring",
33689
+ cx: centerPx[X],
33690
+ cy: centerPx[Y],
33691
+ rx: radiiPx[X] + 3,
33692
+ ry: radiiPx[Y] + 3
33693
+ }), /*#__PURE__*/React__namespace.createElement("ellipse", {
33694
+ className: "circle",
33695
+ cx: centerPx[X],
33696
+ cy: centerPx[Y],
33697
+ rx: radiiPx[X],
33698
+ ry: radiiPx[Y]
33699
+ }), /*#__PURE__*/React__namespace.createElement(DragHandle, {
33700
+ center: center
33701
+ }));
33702
+ }
33703
+ const dragHandleDimensions = [24, 14];
33704
+ const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
33705
+ function DragHandle(props) {
33706
+ const {
33707
+ center
33708
+ } = props;
33709
+ const cornerRadius = Math.min(...dragHandleDimensions) / 2;
33710
+ const [centerPx] = useTransformVectorsToPixels(center);
33711
+ const topLeft = mafs.vec.sub(centerPx, mafs.vec.scale(dragHandleDimensions, 0.5));
33712
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
33713
+ className: "movable-circle-handle",
33714
+ x: topLeft[X],
33715
+ y: topLeft[Y],
33716
+ width: dragHandleDimensions[X],
33717
+ height: dragHandleDimensions[Y],
33718
+ rx: cornerRadius,
33719
+ ry: cornerRadius
33720
+ }), dragHandleDotPositions.map(offsetPx => {
33721
+ const [xPx, yPx] = mafs.vec.add(offsetPx, centerPx);
33722
+ return /*#__PURE__*/React__namespace.createElement("circle", {
33723
+ key: `circle-${xPx}-${yPx}`,
33724
+ className: "movable-circle-handle-dot",
33725
+ cx: xPx,
33726
+ cy: yPx
33727
+ });
33728
+ }));
33729
+ }
33730
+ function crossProduct(as, bs) {
33731
+ const result = [];
33732
+ for (const a of as) {
33733
+ for (const b of bs) {
33734
+ result.push([a, b]);
33735
+ }
33736
+ }
33737
+ return result;
33738
+ }
32974
33739
 
33740
+ function renderLinearGraph(state, dispatch) {
33741
+ return {
33742
+ graph: /*#__PURE__*/React__namespace.createElement(LinearGraph, {
33743
+ graphState: state,
33744
+ dispatch: dispatch
33745
+ }),
33746
+ interactiveElementsDescription: null
33747
+ };
33748
+ }
32975
33749
  const LinearGraph = (props, key) => {
32976
33750
  const {
32977
33751
  dispatch
@@ -32998,6 +33772,15 @@ const LinearGraph = (props, key) => {
32998
33772
  });
32999
33773
  };
33000
33774
 
33775
+ function renderLinearSystemGraph(state, dispatch) {
33776
+ return {
33777
+ graph: /*#__PURE__*/React__namespace.createElement(LinearSystemGraph, {
33778
+ graphState: state,
33779
+ dispatch: dispatch
33780
+ }),
33781
+ interactiveElementsDescription: null
33782
+ };
33783
+ }
33001
33784
  const LinearSystemGraph = props => {
33002
33785
  const {
33003
33786
  dispatch
@@ -33020,368 +33803,124 @@ const LinearSystemGraph = props => {
33020
33803
  })), ";");
33021
33804
  };
33022
33805
 
33023
- const RayGraph = props => {
33806
+ function renderPointGraph(state, dispatch) {
33807
+ return {
33808
+ graph: /*#__PURE__*/React__namespace.createElement(PointGraph, {
33809
+ graphState: state,
33810
+ dispatch: dispatch
33811
+ }),
33812
+ interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(PointGraphDescription, {
33813
+ state: state
33814
+ })
33815
+ };
33816
+ }
33817
+ function PointGraph(props) {
33818
+ const numPoints = props.graphState.numPoints;
33819
+ if (numPoints === "unlimited") {
33820
+ return UnlimitedPointGraph(props);
33821
+ }
33822
+ return LimitedPointGraph(props);
33823
+ }
33824
+ function LimitedPointGraph(props) {
33024
33825
  const {
33025
33826
  dispatch
33026
33827
  } = props;
33828
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33829
+ key: i,
33830
+ point: point,
33831
+ onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
33832
+ })));
33833
+ }
33834
+ function UnlimitedPointGraph(props) {
33027
33835
  const {
33028
- coords: line
33029
- } = props.graphState;
33030
- const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
33031
- const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
33032
-
33033
- // Ray graphs only have one line
33034
- return /*#__PURE__*/React__namespace.createElement(MovableLine, {
33035
- points: line,
33036
- onMoveLine: handleMoveLine,
33037
- onMovePoint: handleMovePoint,
33038
- extend: {
33039
- start: false,
33040
- end: true
33836
+ dispatch
33837
+ } = props;
33838
+ const graphConfig = useGraphConfig();
33839
+ const {
33840
+ range: [[minX, maxX], [minY, maxY]]
33841
+ } = graphConfig;
33842
+ const width = maxX - minX;
33843
+ const height = maxY - minY;
33844
+ const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
33845
+ const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
33846
+ const itemsRef = React__namespace.useRef([]);
33847
+ React__namespace.useEffect(() => {
33848
+ const focusedIndex = props.graphState.focusedPointIndex;
33849
+ if (focusedIndex != null) {
33850
+ itemsRef.current[focusedIndex]?.focus();
33041
33851
  }
33042
- });
33043
- };
33044
-
33045
- /**
33046
- * Wrapper with Mafs transform styles for SVG elements.
33047
- * Scales and translates the SVG element to match the Mafs view.
33048
- * Use if you do not need/want to use the hook in `use-transform.ts`.
33049
- */
33050
- const MafsCssTransformWrapper = _ref => {
33051
- let {
33052
- children
33053
- } = _ref;
33054
- return /*#__PURE__*/React__namespace.createElement("g", {
33852
+ }, [props.graphState.focusedPointIndex, itemsRef]);
33853
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
33055
33854
  style: {
33056
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
33855
+ fill: "rgba(0,0,0,0)",
33856
+ cursor: "crosshair"
33857
+ },
33858
+ width: widthPx,
33859
+ height: heightPx,
33860
+ x: left,
33861
+ y: top,
33862
+ onClick: event => {
33863
+ const elementRect = event.currentTarget.getBoundingClientRect();
33864
+ const x = event.clientX - elementRect.x;
33865
+ const y = event.clientY - elementRect.y;
33866
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
33867
+ dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
33057
33868
  }
33058
- }, children);
33059
- };
33060
-
33061
- const TextLabel = _ref => {
33062
- let {
33063
- children,
33064
- ...rest
33065
- } = _ref;
33066
- return /*#__PURE__*/React__default["default"].createElement(mafs.Text, _extends({
33067
- size: 16,
33068
- svgTextProps: {
33069
- filter: "url(#background)",
33070
- fontWeight: "bold"
33869
+ }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33870
+ key: i,
33871
+ point: point,
33872
+ onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
33873
+ ref: ref => {
33874
+ itemsRef.current[i] = ref;
33875
+ },
33876
+ onFocus: () => {
33877
+ dispatch(actions.pointGraph.focusPoint(i));
33878
+ },
33879
+ onClick: () => {
33880
+ dispatch(actions.pointGraph.clickPoint(i));
33071
33881
  }
33072
- }, rest), children);
33073
- };
33074
- const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs", null, /*#__PURE__*/React__default["default"].createElement("filter", {
33075
- id: "background",
33076
- x: "-5%",
33077
- width: "110%",
33078
- y: "0%",
33079
- height: "100%"
33080
- }, /*#__PURE__*/React__default["default"].createElement("feFlood", {
33081
- floodColor: "#FFF",
33082
- floodOpacity: "0.64"
33083
- }), /*#__PURE__*/React__default["default"].createElement("feComposite", {
33084
- operator: "over",
33085
- in: "SourceGraphic"
33086
- })));
33087
-
33088
- const PolygonAngle = _ref => {
33882
+ })));
33883
+ }
33884
+ function PointGraphDescription(_ref) {
33089
33885
  let {
33090
- centerPoint,
33091
- endPoints,
33092
- range,
33093
- polygonLines,
33094
- showAngles,
33095
- snapTo
33886
+ state
33096
33887
  } = _ref;
33097
- const [centerX, centerY] = centerPoint;
33098
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
33099
- const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
33100
- const radius = 0.3;
33101
- const a = mafs.vec.dist(centerPoint, endPoints[0]);
33102
- const b = mafs.vec.dist(centerPoint, endPoints[1]);
33103
- const c = mafs.vec.dist(endPoints[0], endPoints[1]);
33104
-
33105
- // Law of cosines
33106
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
33107
- const y1 = centerY + (startY - centerY) / a * radius;
33108
- const x2 = centerX + (endX - centerX) / b * radius;
33109
- const x1 = centerX + (startX - centerX) / a * radius;
33110
- const y2 = centerY + (endY - centerY) / b * radius;
33111
-
33112
- // Fourth point that would create a rhomboid
33113
- // Used to calculate the inside of the angle and to create square
33114
- const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
33115
- if (!showAngles) {
33116
- return isRightPolygonAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
33117
- start: [x1, y1],
33118
- vertex: [x2, y2],
33119
- end: [x3, y3]
33120
- }) : null;
33121
- }
33122
-
33123
- // Midpoint betwen ends of arc
33124
- const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
33125
- const largeArcFlag = isOutside ? 1 : 0;
33126
- const sweepFlag = isOutside ? 1 : 0;
33127
- const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
33128
- let angleInDegrees = angle * (180 / Math.PI);
33129
- // If we have triggered "largArcFlag", the angle should be greater than 180
33130
- if (isOutside) {
33131
- angleInDegrees = 360 - angleInDegrees;
33132
- }
33133
-
33134
- // Only want to show whole numbers when in angles mode
33135
- const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
33136
- const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
33137
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
33138
- id: "background",
33139
- x: "-5%",
33140
- width: "110%",
33141
- y: "0%",
33142
- height: "100%"
33143
- }, /*#__PURE__*/React__namespace.createElement("feFlood", {
33144
- floodColor: "#FFF",
33145
- floodOpacity: "0.5"
33146
- }), /*#__PURE__*/React__namespace.createElement("feComposite", {
33147
- operator: "over",
33148
- in: "SourceGraphic"
33149
- }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
33150
- start: [x1, y1],
33151
- vertex: [x2, y2],
33152
- end: [x3, y3]
33153
- }) : /*#__PURE__*/React__namespace.createElement(Arc, {
33154
- arc: arc
33155
- }), /*#__PURE__*/React__namespace.createElement(TextLabel, {
33156
- x: x3,
33157
- y: y3,
33158
- attach: y3 - centerY > 0 ? "s" : "n",
33159
- attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
33160
- }, angleLabel, "\xB0"));
33161
- };
33162
- const Angle = _ref2 => {
33163
- let {
33164
- vertex,
33165
- coords,
33166
- showAngles,
33167
- allowReflexAngles,
33168
- range
33169
- } = _ref2;
33170
- // Check if the points are clockwise or not, depending on whether we allow reflex angles
33171
- const areClockwise = clockwise$1([...coords, vertex]);
33172
- const shouldReverseCoords = areClockwise && !allowReflexAngles;
33173
-
33174
- // Reverse the coordinates accordingly to ensure the angle is calculated correctly
33175
- const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
33176
-
33177
- // Calculate the angles between the two points
33178
- const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
33179
- const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
33180
- const angle = (startAngle + 360 - endAngle) % 360;
33181
-
33182
- // Check if the angle is reflexive
33183
- const isReflexive = angle > 180;
33184
-
33185
- // Break out the necessary points for the arc calculation
33186
- const [centerX, centerY] = vertex;
33187
- const [point1, point2] = clockwiseCoords;
33188
- const [startX, startY] = point1;
33189
- const [endX, endY] = point2;
33190
- const a = mafs.vec.dist(vertex, point1);
33191
- const b = mafs.vec.dist(vertex, point2);
33192
-
33193
- // Set the radius of the arc
33194
- const radius = 2;
33195
-
33196
- // Calculate the end points of the arc
33197
- const y1 = centerY + (startY - centerY) / a * radius;
33198
- const x2 = centerX + (endX - centerX) / b * radius;
33199
- const x1 = centerX + (startX - centerX) / a * radius;
33200
- const y2 = centerY + (endY - centerY) / b * radius;
33201
-
33202
- // Used to calculate the inside of the angle and to create square
33203
- const [x3, y3] = mafs.vec.add(vertex, mafs.vec.add(mafs.vec.sub([x1, y1], vertex), mafs.vec.sub([x2, y2], vertex)));
33204
-
33205
- // Determine whether the arc should be drawn outside the angle
33206
- const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
33207
-
33208
- // Determine the flags for the arc
33209
- // If the angle is outside OR reflexive, we want to draw a large arc
33210
- // If the angle is outside AND reflexive, we want to sweep the arc
33211
- const largeArcFlag = isOutside || isReflexive ? 1 : 0;
33212
- const sweepFlag = isOutside && isReflexive ? 1 : 0;
33213
-
33214
- // Create the SVG path for the arc
33215
- const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
33216
-
33217
- // We only ever want to show whole angles
33218
- const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
33219
-
33220
- // Calculate the text position based on the angle and whether we allow reflex angles
33221
- // Let's try the angle bisector method to find the midpoint of the arc
33222
- const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
33223
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
33224
- id: "background",
33225
- x: "-5%",
33226
- width: "110%",
33227
- y: "0%",
33228
- height: "100%"
33229
- }, /*#__PURE__*/React__namespace.createElement("feFlood", {
33230
- floodColor: "#FFF",
33231
- floodOpacity: "0.5"
33232
- }), /*#__PURE__*/React__namespace.createElement("feComposite", {
33233
- operator: "over",
33234
- in: "SourceGraphic"
33235
- }))), isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
33236
- start: [x1, y1],
33237
- vertex: [x2, y2],
33238
- end: [x3, y3],
33239
- className: "arc-right-angle"
33240
- }) : /*#__PURE__*/React__namespace.createElement(Arc, {
33241
- arc: arc,
33242
- className: "angle-arc"
33243
- }), showAngles && /*#__PURE__*/React__namespace.createElement(TextLabel, {
33244
- x: textX,
33245
- y: textY,
33246
- color: wonderBlocksTokens.color.blue
33247
- }, angleLabel, "\xB0"));
33248
- };
33249
-
33250
- /**
33251
- * This is broken out into its own component so that it can be used for an early return
33252
- * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
33253
- */
33254
- const RightAngleSquare = _ref3 => {
33255
- let {
33256
- start: [x1, y1],
33257
- vertex: [x2, y2],
33258
- end: [x3, y3],
33259
- className
33260
- } = _ref3;
33261
- return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
33262
- d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
33263
- strokeWidth: 0.02,
33264
- fill: "none",
33265
- className: className
33266
- }));
33267
- };
33268
-
33269
- // We're conditionally adding the class name here so that we can style the arc differently
33270
- // based on whether it's an angle or a polygon angle
33271
- const Arc = _ref4 => {
33272
- let {
33273
- arc,
33274
- className
33275
- } = _ref4;
33276
- return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
33277
- d: arc,
33278
- strokeWidth: 0.02,
33279
- fill: "none",
33280
- className: className
33281
- }));
33282
- };
33283
- const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
33284
- const isRightAngle = angle => Math.round(angle) === 90;
33285
-
33286
- /**
33287
- * Determines if an angle is an inside (false) or outside (true) angle.
33288
- * They way, we know to flip the `largeArc` and `sweepArc` flags.
33289
- * Uses the priciple that a ray from a point inside a polygon will intersect
33290
- * with an odd number of lines, while a ray from a point outside the polygon
33291
- * will intersect with an even number of lines.
33292
- * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
33293
- */
33294
- const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
33295
- // Create a ray from the midpoint (inside angle) to the edge of the range
33296
- const rangeIntersectionPoint = getIntersectionOfRayWithBox(midpoint, vertex, range);
33297
- let lineIntersectionCount = 0;
33298
- polygonLines.forEach(line => segmentsIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
33299
-
33300
- // If the number of intersections is even, the angle is inside the polygon
33301
- return !isEven(lineIntersectionCount);
33302
- };
33303
- const isEven = n => n % 2 === 0;
33304
-
33305
- // This function calculates the bisector point of an angle formed by two points
33306
- // and a vertex. It is used to position the angle label so that it is always
33307
- // outside the arc at the exact midpoint of the angle.
33308
- // Reflex angles make use of polar coordinates to determine the correct direction
33309
- function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
33310
- const [originX, originY] = vertex;
33311
- const [x1, y1] = point1;
33312
- const [x2, y2] = point2;
33313
-
33314
- // Convert the Cartesian Coordinates to vectors
33315
- const vectorA = [x1 - originX, y1 - originY];
33316
- const vectorB = [x2 - originX, y2 - originY];
33317
-
33318
- // Calculate the average angle of the vectors
33319
- // so that we can point the bisector in the correct direction using polar coordinates
33320
- const angleA = Math.atan2(vectorA[1], vectorA[0]);
33321
- const angleB = Math.atan2(vectorB[1], vectorB[0]);
33322
-
33323
- // Calculate the average of these two angles
33324
- let averageAngle = (angleA + angleB) / 2;
33325
- const angleRadians = Math.abs(angleA - angleB);
33326
-
33327
- // Adjust the average angle to point in the correct direction
33328
- // and to account for reflex angles and a bug with 180 degrees
33329
- // that occurs due to our angleOffsetDegrees option in the graph
33330
- if (allowReflex) {
33331
- // If the resulting angles are less than 180 degrees,
33332
- // or if the user has flipped the points on the interactive element
33333
- // ensure that we have the correct direction for the reflex angle
33334
- if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
33335
- averageAngle += Math.PI; // Add 180 degrees
33336
- }
33337
- } else {
33338
- // If the resulting angles are greater than 180 degrees, adjust
33339
- // to get the correct angle, as we already know that the angle is not reflexive
33340
- // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
33341
- if (angleRadians > Math.PI) {
33342
- averageAngle -= Math.PI; // Subract 180 degrees
33343
- }
33344
- }
33345
-
33346
- // Convert the average angle back to Cartesian coordinates
33347
- const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
33348
-
33349
- // Calculate the magnitude of the sum to normalize it
33350
- const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
33351
- const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
33352
-
33353
- // Calculate the initial distance of the bisector direction from the origin
33354
- const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
33355
-
33356
- // Determine the minimum radius to ensure that the text is always outside the arc
33357
- const requiredDistance = arcRadius * 1.75;
33358
- let radius = requiredDistance / initialDistance;
33359
-
33360
- // Apply radius only if the initial distance is less than required
33361
- if (initialDistance >= requiredDistance) {
33362
- radius = 1; // Use the unit vector as is
33363
- }
33364
-
33365
- // Scale the bisector direction by the radius
33366
- const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
33367
-
33368
- // Add the vertex to the bisector point to get the final position
33369
- // to ensure that the angle label moves with the interactive element
33370
- const scaledBisector = mafs.vec.add(bisectorPoint, vertex);
33371
- return scaledBisector;
33888
+ // PointGraphDescription needs to `usePerseusI18n`, so it has to be a
33889
+ // component rather than a function that simply returns a string.
33890
+ const i18n = usePerseusI18n();
33891
+ return describePointGraph(state, i18n);
33372
33892
  }
33373
33893
 
33374
- const MovablePoint$1 = /*#__PURE__*/React__namespace.forwardRef((props, pointRef) => {
33894
+ // Exported for testing
33895
+ function describePointGraph(state, i18n) {
33375
33896
  const {
33376
- visiblePoint,
33377
- focusableHandle
33378
- } = useControlPoint({
33379
- ...props,
33380
- forwardedRef: pointRef
33897
+ strings,
33898
+ locale
33899
+ } = i18n;
33900
+ if (state.coords.length === 0) {
33901
+ return strings.srNoInteractiveElements;
33902
+ }
33903
+ const pointDescriptions = state.coords.map(_ref2 => {
33904
+ let [x, y] = _ref2;
33905
+ return strings.srPointAtCoordinates({
33906
+ x: srFormatNumber(x, locale),
33907
+ y: srFormatNumber(y, locale)
33908
+ });
33381
33909
  });
33382
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle, visiblePoint);
33383
- });
33910
+ return strings.srInteractiveElements({
33911
+ elements: pointDescriptions.join(", ")
33912
+ });
33913
+ }
33384
33914
 
33915
+ function renderPolygonGraph(state, dispatch) {
33916
+ return {
33917
+ graph: /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
33918
+ graphState: state,
33919
+ dispatch: dispatch
33920
+ }),
33921
+ interactiveElementsDescription: null
33922
+ };
33923
+ }
33385
33924
  const LimitedPolygonGraph = props => {
33386
33925
  const [hovered, setHovered] = React__namespace.useState(false);
33387
33926
  // This is more so required for the re-rendering that occurs when state
@@ -33663,201 +34202,15 @@ const PolygonGraph = props => {
33663
34202
  return numSides === "unlimited" ? UnlimitedPolygonGraph(props) : LimitedPolygonGraph(props);
33664
34203
  };
33665
34204
 
33666
- function getGradableGraph(state, initialGraph) {
33667
- if (!state.hasBeenInteractedWith) {
33668
- return {
33669
- ...initialGraph
33670
- };
33671
- }
33672
- if (initialGraph.type === "linear-system" && state.type === "linear-system") {
33673
- return {
33674
- ...initialGraph,
33675
- coords: state.coords
33676
- };
33677
- }
33678
- if (state.type === "segment" && initialGraph.type === "segment") {
33679
- return {
33680
- ...initialGraph,
33681
- coords: state.coords
33682
- };
33683
- }
33684
- if (state.type === "linear" && initialGraph.type === "linear") {
33685
- return {
33686
- ...initialGraph,
33687
- coords: state.coords
33688
- };
33689
- }
33690
- if (state.type === "ray" && initialGraph.type === "ray") {
33691
- return {
33692
- ...initialGraph,
33693
- coords: state.coords
33694
- };
33695
- }
33696
- if (state.type === "polygon" && initialGraph.type === "polygon") {
33697
- return {
33698
- ...initialGraph,
33699
- coords: state.coords
33700
- };
33701
- }
33702
- if (state.type === "point" && initialGraph.type === "point") {
33703
- return {
33704
- ...initialGraph,
33705
- coords: state.coords
33706
- };
33707
- }
33708
- if (state.type === "circle" && initialGraph.type === "circle") {
33709
- return {
33710
- ...initialGraph,
33711
- center: state.center,
33712
- radius: getRadius(state)
33713
- };
33714
- }
33715
- if (state.type === "quadratic" && initialGraph.type === "quadratic") {
33716
- return {
33717
- ...initialGraph,
33718
- coords: state.coords
33719
- };
33720
- }
33721
- if (state.type === "sinusoid" && initialGraph.type === "sinusoid") {
33722
- return {
33723
- ...initialGraph,
33724
- coords: state.coords
33725
- };
33726
- }
33727
- if (state.type === "angle" && initialGraph.type === "angle") {
33728
- // We're going to reverse the coords for scoring if the angle is clockwise and
33729
- // we don't allow reflex angles. This is because the angle is largely defined
33730
- // by the order of the points in the coords array, and we want to maintain
33731
- // the same angle scoring with the legacy graph (which had a bug).
33732
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33733
- const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
33734
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33735
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33736
- return {
33737
- ...initialGraph,
33738
- coords,
33739
- allowReflexAngles: state.allowReflexAngles
33740
- };
33741
- }
33742
- if (state.type === "none" && initialGraph.type === "none") {
33743
- return {
33744
- type: "none"
33745
- };
33746
- }
33747
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
33748
- }
33749
-
33750
- /**
33751
- * determine radius of a circle graph
33752
- *
33753
- * @param graph - the graph object containing the circle
33754
- * @returns the radius of the circle
33755
- */
33756
- function getRadius(graph) {
33757
- const [centerX, centerY] = graph.center;
33758
- const [edgeX, edgeY] = graph.radiusPoint;
33759
- return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
33760
- }
33761
-
33762
- function CircleGraph(props) {
33763
- const {
33764
- dispatch,
33765
- graphState
33766
- } = props;
33767
- const {
33768
- center,
33769
- radiusPoint
33770
- } = graphState;
33771
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableCircle, {
33772
- center: center,
33773
- radius: getRadius(graphState),
33774
- onMove: c => dispatch(actions.circle.moveCenter(c))
33775
- }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33776
- point: radiusPoint,
33777
- cursor: "ew-resize",
33778
- onMove: newRadiusPoint => {
33779
- dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
33780
- }
33781
- }));
33782
- }
33783
- function MovableCircle(props) {
33784
- const {
33785
- center,
33786
- radius,
33787
- onMove
33788
- } = props;
33789
- const {
33790
- snapStep,
33791
- disableKeyboardInteraction
33792
- } = useGraphConfig();
33793
- const draggableRef = React.useRef(null);
33794
- const {
33795
- dragging
33796
- } = useDraggable({
33797
- gestureTarget: draggableRef,
33798
- point: center,
33799
- onMove,
33800
- constrainKeyboardMovement: p => snap(snapStep, p)
33801
- });
33802
- const [centerPx] = useTransformVectorsToPixels(center);
33803
- const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
33804
- return /*#__PURE__*/React__namespace.createElement("g", {
33805
- ref: draggableRef,
33806
- tabIndex: disableKeyboardInteraction ? -1 : 0,
33807
- className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
33808
- }, /*#__PURE__*/React__namespace.createElement("ellipse", {
33809
- className: "focus-ring",
33810
- cx: centerPx[X],
33811
- cy: centerPx[Y],
33812
- rx: radiiPx[X] + 3,
33813
- ry: radiiPx[Y] + 3
33814
- }), /*#__PURE__*/React__namespace.createElement("ellipse", {
33815
- className: "circle",
33816
- cx: centerPx[X],
33817
- cy: centerPx[Y],
33818
- rx: radiiPx[X],
33819
- ry: radiiPx[Y]
33820
- }), /*#__PURE__*/React__namespace.createElement(DragHandle, {
33821
- center: center
33822
- }));
33823
- }
33824
- const dragHandleDimensions = [24, 14];
33825
- const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
33826
- function DragHandle(props) {
33827
- const {
33828
- center
33829
- } = props;
33830
- const cornerRadius = Math.min(...dragHandleDimensions) / 2;
33831
- const [centerPx] = useTransformVectorsToPixels(center);
33832
- const topLeft = mafs.vec.sub(centerPx, mafs.vec.scale(dragHandleDimensions, 0.5));
33833
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
33834
- className: "movable-circle-handle",
33835
- x: topLeft[X],
33836
- y: topLeft[Y],
33837
- width: dragHandleDimensions[X],
33838
- height: dragHandleDimensions[Y],
33839
- rx: cornerRadius,
33840
- ry: cornerRadius
33841
- }), dragHandleDotPositions.map(offsetPx => {
33842
- const [xPx, yPx] = mafs.vec.add(offsetPx, centerPx);
33843
- return /*#__PURE__*/React__namespace.createElement("circle", {
33844
- key: `circle-${xPx}-${yPx}`,
33845
- className: "movable-circle-handle-dot",
33846
- cx: xPx,
33847
- cy: yPx
33848
- });
33849
- }));
33850
- }
33851
- function crossProduct(as, bs) {
33852
- const result = [];
33853
- for (const a of as) {
33854
- for (const b of bs) {
33855
- result.push([a, b]);
33856
- }
33857
- }
33858
- return result;
34205
+ function renderQuadraticGraph(state, dispatch) {
34206
+ return {
34207
+ graph: /*#__PURE__*/React__namespace.createElement(QuadraticGraph, {
34208
+ graphState: state,
34209
+ dispatch: dispatch
34210
+ }),
34211
+ interactiveElementsDescription: null
34212
+ };
33859
34213
  }
33860
-
33861
34214
  function QuadraticGraph(props) {
33862
34215
  const {
33863
34216
  dispatch,
@@ -33912,6 +34265,74 @@ const getQuadraticCoefficients$1 = coords => {
33912
34265
  return [a, b, c];
33913
34266
  };
33914
34267
 
34268
+ function renderRayGraph(state, dispatch) {
34269
+ return {
34270
+ graph: /*#__PURE__*/React__namespace.createElement(RayGraph, {
34271
+ graphState: state,
34272
+ dispatch: dispatch
34273
+ }),
34274
+ interactiveElementsDescription: null
34275
+ };
34276
+ }
34277
+ const RayGraph = props => {
34278
+ const {
34279
+ dispatch
34280
+ } = props;
34281
+ const {
34282
+ coords: line
34283
+ } = props.graphState;
34284
+ const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
34285
+ const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
34286
+
34287
+ // Ray graphs only have one line
34288
+ return /*#__PURE__*/React__namespace.createElement(MovableLine, {
34289
+ points: line,
34290
+ onMoveLine: handleMoveLine,
34291
+ onMovePoint: handleMovePoint,
34292
+ extend: {
34293
+ start: false,
34294
+ end: true
34295
+ }
34296
+ });
34297
+ };
34298
+
34299
+ function renderSegmentGraph(state, dispatch) {
34300
+ return {
34301
+ graph: /*#__PURE__*/React__namespace.createElement(SegmentGraph, {
34302
+ graphState: state,
34303
+ dispatch: dispatch
34304
+ }),
34305
+ interactiveElementsDescription: null
34306
+ };
34307
+ }
34308
+ const SegmentGraph = props => {
34309
+ const {
34310
+ dispatch
34311
+ } = props;
34312
+ const {
34313
+ coords: segments
34314
+ } = props.graphState;
34315
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
34316
+ key: i,
34317
+ points: segment,
34318
+ onMoveLine: delta => {
34319
+ dispatch(actions.segment.moveLine(i, delta));
34320
+ },
34321
+ onMovePoint: (endpointIndex, destination) => {
34322
+ dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
34323
+ }
34324
+ })));
34325
+ };
34326
+
34327
+ function renderSinusoidGraph(state, dispatch) {
34328
+ return {
34329
+ graph: /*#__PURE__*/React__namespace.createElement(SinusoidGraph, {
34330
+ graphState: state,
34331
+ dispatch: dispatch
34332
+ }),
34333
+ interactiveElementsDescription: null
34334
+ };
34335
+ }
33915
34336
  function SinusoidGraph(props) {
33916
34337
  const {
33917
34338
  dispatch,
@@ -33987,186 +34408,6 @@ const getSinusoidCoefficients$1 = coords => {
33987
34408
  };
33988
34409
  };
33989
34410
 
33990
- function AngleGraph(props) {
33991
- const {
33992
- dispatch,
33993
- graphState
33994
- } = props;
33995
- const {
33996
- graphDimensionsInPixels
33997
- } = useGraphConfig();
33998
- const {
33999
- coords,
34000
- showAngles,
34001
- range,
34002
- allowReflexAngles,
34003
- snapDegrees
34004
- } = graphState;
34005
-
34006
- // Break the coords into the two end points and the center point
34007
- const endPoints = [coords[0], coords[2]];
34008
- const centerPoint = coords[1];
34009
-
34010
- // Convert the vectors to pixels for rendering the svg lines
34011
- const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
34012
- const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
34013
-
34014
- // Create the SVG lines for the angle
34015
- const svgLines = linePixelCoords.map((_ref, i) => {
34016
- let [startPtPx, endPtPx] = _ref;
34017
- const trimmedRange = trimRange(range, graphDimensionsInPixels);
34018
- const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
34019
- return /*#__PURE__*/React__namespace.createElement("g", {
34020
- key: `line-${i}`
34021
- }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
34022
- start: startPtPx,
34023
- end: endPtPx,
34024
- style: {
34025
- stroke: "var(--movable-line-stroke-color)",
34026
- strokeWidth: "var(--movable-line-stroke-weight)"
34027
- }
34028
- }), /*#__PURE__*/React__namespace.createElement(Vector, {
34029
- tail: angleLines[i][1],
34030
- tip: endExtend,
34031
- color: "var(--movable-line-stroke-color)"
34032
- }));
34033
- });
34034
-
34035
- // Create the angle indicator parameters
34036
- const angleParams = {
34037
- vertex: centerPoint,
34038
- coords: endPoints,
34039
- allowReflexAngles: allowReflexAngles || false,
34040
- // Whether to allow reflex angles or not
34041
- snapDegrees: snapDegrees || 1,
34042
- // The multiple of degrees to snap to
34043
- range: range,
34044
- showAngles: showAngles || false // Whether to show the angle or not
34045
- };
34046
-
34047
- // Render the lines, angle, and then movable points
34048
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34049
- point: coords[1],
34050
- constrain: p => p,
34051
- onMove: destination => dispatch(actions.angle.movePoint(1, destination))
34052
- }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34053
- point: coords[0],
34054
- constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
34055
- onMove: destination => dispatch(actions.angle.movePoint(0, destination))
34056
- }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34057
- point: coords[2],
34058
- constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
34059
- onMove: destination => dispatch(actions.angle.movePoint(2, destination))
34060
- }));
34061
- }
34062
- const positiveX = [1, 0];
34063
- const negativeX = [-1, 0];
34064
- const positiveY = [0, 1];
34065
- const negativeY = [0, -1];
34066
- function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
34067
- const currentAngle = calculateAngleInDegrees(mafs.vec.sub(sidePoint, vertex));
34068
-
34069
- // Find the rays that start at the current point and point up, down, left
34070
- // and right.
34071
- const leftRay = [sidePoint, mafs.vec.add(sidePoint, negativeX)];
34072
- const rightRay = [sidePoint, mafs.vec.add(sidePoint, positiveX)];
34073
- const upRay = [sidePoint, mafs.vec.add(sidePoint, positiveY)];
34074
- const downRay = [sidePoint, mafs.vec.add(sidePoint, negativeY)];
34075
-
34076
- // find the angles that lie one snap step clockwise and counter-clockwise
34077
- // from the current angle. These are the angles to which the side can be
34078
- // moved.
34079
- const oneStepCounterClockwise = currentAngle + snapDegrees;
34080
- const oneStepClockwise = currentAngle - snapDegrees;
34081
-
34082
- // find the rays that start from the vertex and point in the direction of
34083
- // the angles we just computed.
34084
- const counterClockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepCounterClockwise))];
34085
- const clockwiseRay = [vertex, mafs.vec.add(vertex, polar(1, oneStepClockwise))];
34086
-
34087
- // find the intersections of those rays with the horizontal and vertical
34088
- // rays extending from the sidePoint. These intersections are the points to
34089
- // which sidePoint can move that will result in a rotation of `snapDegrees`.
34090
- const left = findIntersectionOfRays(leftRay, counterClockwiseRay) ?? findIntersectionOfRays(leftRay, clockwiseRay);
34091
- const right = findIntersectionOfRays(rightRay, counterClockwiseRay) ?? findIntersectionOfRays(rightRay, clockwiseRay);
34092
- const up = findIntersectionOfRays(upRay, counterClockwiseRay) ?? findIntersectionOfRays(upRay, clockwiseRay);
34093
- const down = findIntersectionOfRays(downRay, counterClockwiseRay) ?? findIntersectionOfRays(downRay, clockwiseRay);
34094
- return {
34095
- up: up ?? sidePoint,
34096
- down: down ?? sidePoint,
34097
- left: left ?? sidePoint,
34098
- right: right ?? sidePoint
34099
- };
34100
- }
34101
-
34102
- function LimitedPointGraph(props) {
34103
- const {
34104
- dispatch
34105
- } = props;
34106
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34107
- key: i,
34108
- point: point,
34109
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
34110
- })));
34111
- }
34112
- function UnlimitedPointGraph(props) {
34113
- const {
34114
- dispatch
34115
- } = props;
34116
- const graphConfig = useGraphConfig();
34117
- const {
34118
- range: [[minX, maxX], [minY, maxY]]
34119
- } = graphConfig;
34120
- const width = maxX - minX;
34121
- const height = maxY - minY;
34122
- const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
34123
- const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
34124
- const itemsRef = React__namespace.useRef([]);
34125
- React__namespace.useEffect(() => {
34126
- const focusedIndex = props.graphState.focusedPointIndex;
34127
- if (focusedIndex != null) {
34128
- itemsRef.current[focusedIndex]?.focus();
34129
- }
34130
- }, [props.graphState.focusedPointIndex, itemsRef]);
34131
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
34132
- style: {
34133
- fill: "rgba(0,0,0,0)",
34134
- cursor: "crosshair"
34135
- },
34136
- width: widthPx,
34137
- height: heightPx,
34138
- x: left,
34139
- y: top,
34140
- onClick: event => {
34141
- const elementRect = event.currentTarget.getBoundingClientRect();
34142
- const x = event.clientX - elementRect.x;
34143
- const y = event.clientY - elementRect.y;
34144
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34145
- dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
34146
- }
34147
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34148
- key: i,
34149
- point: point,
34150
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
34151
- ref: ref => {
34152
- itemsRef.current[i] = ref;
34153
- },
34154
- onFocus: () => {
34155
- dispatch(actions.pointGraph.focusPoint(i));
34156
- },
34157
- onClick: () => {
34158
- dispatch(actions.pointGraph.clickPoint(i));
34159
- }
34160
- })));
34161
- }
34162
- function PointGraph(props) {
34163
- const numPoints = props.graphState.numPoints;
34164
- if (numPoints === "unlimited") {
34165
- return UnlimitedPointGraph(props);
34166
- }
34167
- return LimitedPointGraph(props);
34168
- }
34169
-
34170
34411
  const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
34171
34412
 
34172
34413
  // The vector from the center of the protractor to the top left corner of the
@@ -34330,6 +34571,7 @@ const MafsGraph = props => {
34330
34571
  const tickStep = props.step;
34331
34572
  const uniqueId = React__namespace.useId();
34332
34573
  const descriptionId = `interactive-graph-description-${uniqueId}`;
34574
+ const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
34333
34575
  const graphRef = React__namespace.useRef(null);
34334
34576
  const {
34335
34577
  analytics
@@ -34364,6 +34606,13 @@ const MafsGraph = props => {
34364
34606
  }
34365
34607
  });
34366
34608
  });
34609
+ const {
34610
+ graph,
34611
+ interactiveElementsDescription
34612
+ } = renderGraphElements({
34613
+ state,
34614
+ dispatch
34615
+ });
34367
34616
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
34368
34617
  value: {
34369
34618
  range: state.range,
@@ -34395,7 +34644,7 @@ const MafsGraph = props => {
34395
34644
  handleKeyboardEvent(event, state, dispatch);
34396
34645
  },
34397
34646
  "aria-label": fullGraphAriaLabel,
34398
- "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
34647
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
34399
34648
  ref: graphRef,
34400
34649
  tabIndex: 0,
34401
34650
  onFocus: event => {
@@ -34407,12 +34656,12 @@ const MafsGraph = props => {
34407
34656
  }, fullGraphAriaDescription && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
34408
34657
  id: descriptionId,
34409
34658
  tabIndex: -1,
34410
- style: {
34411
- width: 0,
34412
- height: 0,
34413
- overflow: "hidden"
34414
- }
34415
- }, fullGraphAriaDescription), /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
34659
+ className: "mafs-sr-only"
34660
+ }, fullGraphAriaDescription), interactiveElementsDescription && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
34661
+ id: interactiveElementsDescriptionId,
34662
+ tabIndex: -1,
34663
+ className: "mafs-sr-only"
34664
+ }, interactiveElementsDescription), /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
34416
34665
  box: props.box,
34417
34666
  backgroundImage: props.backgroundImage
34418
34667
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
@@ -34463,10 +34712,7 @@ const MafsGraph = props => {
34463
34712
  zoom: false,
34464
34713
  width: width,
34465
34714
  height: height
34466
- }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
34467
- state,
34468
- dispatch
34469
- }))))), interactionPrompt && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
34715
+ }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
34470
34716
  style: {
34471
34717
  textAlign: "center",
34472
34718
  backgroundColor: "white",
@@ -34698,7 +34944,7 @@ const calculateNestedSVGCoords = (range, width, height) => {
34698
34944
  viewboxY
34699
34945
  };
34700
34946
  };
34701
- const renderGraph = props => {
34947
+ const renderGraphElements = props => {
34702
34948
  const {
34703
34949
  state,
34704
34950
  dispatch
@@ -34708,62 +34954,44 @@ const renderGraph = props => {
34708
34954
  } = state;
34709
34955
  switch (type) {
34710
34956
  case "angle":
34711
- return /*#__PURE__*/React__namespace.createElement(AngleGraph, {
34712
- graphState: state,
34713
- dispatch: dispatch
34714
- });
34957
+ return renderAngleGraph(state, dispatch);
34715
34958
  case "segment":
34716
- return /*#__PURE__*/React__namespace.createElement(SegmentGraph, {
34717
- graphState: state,
34718
- dispatch: dispatch
34719
- });
34959
+ return renderSegmentGraph(state, dispatch);
34720
34960
  case "linear-system":
34721
- return /*#__PURE__*/React__namespace.createElement(LinearSystemGraph, {
34722
- graphState: state,
34723
- dispatch: dispatch
34724
- });
34961
+ return renderLinearSystemGraph(state, dispatch);
34725
34962
  case "linear":
34726
- return /*#__PURE__*/React__namespace.createElement(LinearGraph, {
34727
- graphState: state,
34728
- dispatch: dispatch
34729
- });
34963
+ return renderLinearGraph(state, dispatch);
34730
34964
  case "ray":
34731
- return /*#__PURE__*/React__namespace.createElement(RayGraph, {
34732
- graphState: state,
34733
- dispatch: dispatch
34734
- });
34965
+ return renderRayGraph(state, dispatch);
34735
34966
  case "polygon":
34736
- return /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
34737
- graphState: state,
34738
- dispatch: dispatch
34739
- });
34967
+ return renderPolygonGraph(state, dispatch);
34740
34968
  case "point":
34741
- return /*#__PURE__*/React__namespace.createElement(PointGraph, {
34742
- graphState: state,
34743
- dispatch: dispatch
34744
- });
34969
+ return renderPointGraph(state, dispatch);
34745
34970
  case "circle":
34746
- return /*#__PURE__*/React__namespace.createElement(CircleGraph, {
34747
- graphState: state,
34748
- dispatch: dispatch
34749
- });
34971
+ return renderCircleGraph(state, dispatch);
34750
34972
  case "quadratic":
34751
- return /*#__PURE__*/React__namespace.createElement(QuadraticGraph, {
34752
- graphState: state,
34753
- dispatch: dispatch
34754
- });
34973
+ return renderQuadraticGraph(state, dispatch);
34755
34974
  case "sinusoid":
34756
- return /*#__PURE__*/React__namespace.createElement(SinusoidGraph, {
34757
- graphState: state,
34758
- dispatch: dispatch
34759
- });
34975
+ return renderSinusoidGraph(state, dispatch);
34760
34976
  case "none":
34761
- return null;
34977
+ return {
34978
+ graph: null,
34979
+ interactiveElementsDescription: null
34980
+ };
34762
34981
  default:
34763
34982
  throw new wonderStuffCore.UnreachableCaseError(type);
34764
34983
  }
34765
34984
  };
34766
34985
 
34986
+ // Returns a space-separated string like "foo bar" given several optional
34987
+ // string IDs. If all args are falsy, returns undefined.
34988
+ function describedByIds() {
34989
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
34990
+ args[_key] = arguments[_key];
34991
+ }
34992
+ return args.filter(Boolean).join(" ") || undefined;
34993
+ }
34994
+
34767
34995
  // Converts the state of a StatefulMafsGraph back to the format used to
34768
34996
  // represent graph state in the widget JSON.
34769
34997
  //
@@ -44008,7 +44236,7 @@ const VideoTranscriptLink = props => {
44008
44236
  } = props;
44009
44237
  const {
44010
44238
  useVideo
44011
- } = getDependencies();
44239
+ } = useDependencies();
44012
44240
  const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
44013
44241
  // The result value conforms to the wonder-blocks-data `Result` type
44014
44242
  // which is used by our GraphQL framework.
@@ -44184,7 +44412,7 @@ const init = function (options) {
44184
44412
 
44185
44413
  // This file is processed by a Rollup plugin (replace) to inject the production
44186
44414
  const libName = "@khanacademy/perseus";
44187
- const libVersion = "40.0.0";
44415
+ const libVersion = "41.1.0";
44188
44416
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44189
44417
 
44190
44418
  const apiVersion = {