@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/es/index.js CHANGED
@@ -32,7 +32,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
32
32
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
33
33
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
34
34
  import debounce$2 from 'lodash.debounce';
35
- import { vec, Coordinates, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, useTransformContext, Text as Text$1, Mafs } from 'mafs';
35
+ import { vec, Coordinates, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Mafs } from 'mafs';
36
36
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
37
37
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
38
38
  import { useDrag } from '@use-gesture/react';
@@ -238,7 +238,15 @@ const mockStrings = {
238
238
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
239
239
  addPoint: "Add Point",
240
240
  removePoint: "Remove Point",
241
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
241
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
242
+ srPointAtCoordinates: ({
243
+ x,
244
+ y
245
+ }) => `Point at ${x} comma ${y}`,
246
+ srInteractiveElements: ({
247
+ elements
248
+ }) => `Interactive elements: ${elements}`,
249
+ srNoInteractiveElements: "No interactive elements"
242
250
  };
243
251
 
244
252
  /**
@@ -246,11 +254,13 @@ const mockStrings = {
246
254
  * used inside the Perseus package.
247
255
  *
248
256
  */
249
- // @ts-expect-error - TS2322 - Type 'Context<{ strings: {}; locale: string; }>' is not assignable to type 'Context<I18nContextType>'.
250
- const PerseusI18nContext = /*#__PURE__*/React.createContext(process.env.NODE_ENV === "test" || process.env.STORYBOOK ? {
257
+ const mockPerseusI18nContext = {
251
258
  strings: mockStrings,
252
259
  locale: "en"
253
- } :
260
+ };
261
+
262
+ // @ts-expect-error - TS2322 - Type 'Context<{ strings: {}; locale: string; }>' is not assignable to type 'Context<I18nContextType>'.
263
+ const PerseusI18nContext = /*#__PURE__*/React.createContext(process.env.NODE_ENV === "test" || process.env.STORYBOOK ? mockPerseusI18nContext :
254
264
  // We want to return null here, not an empty object, so that we
255
265
  // are will throw an error when attempting to access the
256
266
  // undefined locale or strings, making it easier to debug.
@@ -531,7 +541,7 @@ class InnerMathInput extends React.Component {
531
541
  closeButtonVisible: true,
532
542
  style: styles$E.popoverContent
533
543
  }, /*#__PURE__*/React.createElement(DesktopKeypad, _extends({
534
- onAnalyticsEvent: this.props.analytics.onAnalyticsEvent,
544
+ onAnalyticsEvent: this.props.onAnalyticsEvent,
535
545
  extraKeys: this.props.extraKeys,
536
546
  onClickKey: this.handleKeypadPress,
537
547
  cursorContext: this.state.cursorContext,
@@ -705,6 +715,9 @@ const getDependencies = () => {
705
715
  const DependenciesContext = /*#__PURE__*/React.createContext({
706
716
  analytics: {
707
717
  onAnalyticsEvent: async () => {}
718
+ },
719
+ useVideo: () => {
720
+ throw new Error("useVideo dependency not provided in Perseus dependencies");
708
721
  }
709
722
  });
710
723
 
@@ -958,7 +971,7 @@ class TextListEditor extends React.Component {
958
971
  }
959
972
  TextListEditor.propTypes = {
960
973
  options: PropTypes.array,
961
- layout: PropTypes.string,
974
+ layout: PropTypes.oneOf(["horizontal", "vertical"]),
962
975
  onChange: PropTypes.func.isRequired
963
976
  };
964
977
  TextListEditor.defaultProps = {
@@ -966,6 +979,8 @@ TextListEditor.defaultProps = {
966
979
  layout: "horizontal"
967
980
  };
968
981
 
982
+ const EMPTY_ARRAY = [];
983
+
969
984
  /**
970
985
  * Stub Tag Editor.
971
986
  *
@@ -979,7 +994,6 @@ TextListEditor.defaultProps = {
979
994
  * It also gives a nicer interface for the group metadata editor
980
995
  * in local demo mode.
981
996
  */
982
- const EMPTY_ARRAY = [];
983
997
  class StubTagEditor extends React.Component {
984
998
  render() {
985
999
  return /*#__PURE__*/React.createElement("div", null, this.props.showTitle && /*#__PURE__*/React.createElement("div", {
@@ -2408,7 +2422,7 @@ class Expression extends React.Component {
2408
2422
  }, cb);
2409
2423
  }
2410
2424
  render() {
2411
- var _this$props$keypadCon, _this$props$analytics2;
2425
+ var _this$props$keypadCon, _this$props$analytics2, _this$props$analytics3;
2412
2426
  if (this.props.apiOptions.customKeypad) {
2413
2427
  return /*#__PURE__*/React.createElement(View, {
2414
2428
  className: css(styles$D.mobileLabelInputWrapper)
@@ -2481,9 +2495,7 @@ class Expression extends React.Component {
2481
2495
  hasError: this.state.showErrorStyle,
2482
2496
  ariaLabel: this.props.ariaLabel || this.context.strings.mathInputBox,
2483
2497
  extraKeys: (_this$props$keypadCon = this.props.keypadConfiguration) == null ? void 0 : _this$props$keypadCon.extraKeys,
2484
- analytics: (_this$props$analytics2 = this.props.analytics) != null ? _this$props$analytics2 : {
2485
- onAnalyticsEvent: async function () {}
2486
- }
2498
+ onAnalyticsEvent: (_this$props$analytics2 = (_this$props$analytics3 = this.props.analytics) == null ? void 0 : _this$props$analytics3.onAnalyticsEvent) != null ? _this$props$analytics2 : async function () {}
2487
2499
  }))));
2488
2500
  }
2489
2501
  }
@@ -14800,11 +14812,7 @@ _.extend(MovableLine$4.prototype, {
14800
14812
  * A stripped version of Icon.jsx from webapp. Takes an SVG icon and renders it
14801
14813
  * inline like Font Awesome did.
14802
14814
  *
14803
- * If you are looking for an icon that we've used before you should look in
14804
- * webapp's `icon-paths.js` which is a reference file for all the SVG paths
14805
- * that we've used. You'll need to copy the object from that file into
14806
- * whichever file you're using the icon and explicitly pass it in to the
14807
- * <InlineIcon/> React component.
14815
+ * You can refer to `icon-paths.ts` for all available SVG icons.
14808
14816
  *
14809
14817
  * We assume that the viewBox is cropped and aligned to (0, 0), but icons can
14810
14818
  * be defined differently. At some point we might want to add these attributes
@@ -14812,13 +14820,14 @@ _.extend(MovableLine$4.prototype, {
14812
14820
  *
14813
14821
  * Sample usage:
14814
14822
  *
14815
- * const editIcon = {
14823
+ * ```
14824
+ * const editIcon = {
14816
14825
  * 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",
14817
14826
  * width: 100,
14818
14827
  * height: 78.912,
14819
- * };
14820
- * <InlineIcon {...editIcon} />
14821
- *
14828
+ * };
14829
+ * <InlineIcon {...editIcon} />
14830
+ * ```
14822
14831
  */
14823
14832
  const InlineIcon = ({
14824
14833
  path,
@@ -14841,11 +14850,7 @@ const InlineIcon = ({
14841
14850
  }));
14842
14851
 
14843
14852
  /**
14844
- * Icon paths to be used with `inline-icon.jsx`.
14845
- *
14846
- * These paths are taken directly from webapp's `icon-paths.js`. Unlike the
14847
- * webapp equivalent, these can be directly required within Perseus files since
14848
- * this is all bundled together anyway.
14853
+ * Icon paths to be used with `inline-icon.tsx` and `icon.tsx`.
14849
14854
  */
14850
14855
 
14851
14856
  const iconCheck = {
@@ -16791,6 +16796,15 @@ const Status = {
16791
16796
  LOADED: "loaded",
16792
16797
  FAILED: "failed"
16793
16798
  };
16799
+ /**
16800
+ * Component to display an image (or other React components) while the desired
16801
+ * image is loading.
16802
+ *
16803
+ * Derived from
16804
+ * https://github.com/hzdg/react-imageloader/blob/master/src/index.js
16805
+ * to better suit our environment/build tools. Additionally, this one does
16806
+ * not introduce a wrapper element, which makes styling easier.
16807
+ */
16794
16808
  class ImageLoader extends React.Component {
16795
16809
  constructor(props) {
16796
16810
  super(props);
@@ -20920,7 +20934,7 @@ const Triangle = props => {
20920
20934
  width: 0,
20921
20935
  position: "absolute",
20922
20936
  left: props.left,
20923
- top: props["top"],
20937
+ top: props.top,
20924
20938
  borderLeft: borderLeft,
20925
20939
  borderRight: borderRight,
20926
20940
  borderTop: borderTop,
@@ -21001,6 +21015,47 @@ const HORIZONTAL_ALIGNMNENTS = {
21001
21015
  arrowLeft: arrowSize => -arrowSize - 2
21002
21016
  }
21003
21017
  };
21018
+ /**
21019
+ * DEPRECATED! Use Wonder Blocks tooltip instead.
21020
+ *
21021
+ * A generic tooltip library for React.js
21022
+ *
21023
+ * ```
21024
+ * import Tooltip from "./tooltip";
21025
+ * <Tooltip
21026
+ * className="class-for-tooltip-contents"
21027
+ * horizontalPosition={HoriziontalDirection.Left}
21028
+ * horizontalAlign={HoriziontalDirection.Left}
21029
+ * verticalPosition={VerticalDirection.Top}
21030
+ * arrowSize={10} // arrow size in pixels
21031
+ * borderColor="#ccc" // color of the border for the tooltip
21032
+ * show={true} // whether the tooltip should currently be visible
21033
+ * targetContainerStyle={targetContainerStyle}
21034
+ * >
21035
+ * <TargetElementOfTheTooltip />
21036
+ * <TooltipContents1 />
21037
+ * <TooltipContents2 />
21038
+ * </Tooltip>
21039
+ * ```
21040
+ *
21041
+ * To show/hide the tooltip, the parent component should call the
21042
+ * `.show()` and `.hide()` methods of the tooltip when appropriate.
21043
+ * (These are usually set up as handlers of events on the target element.)
21044
+ *
21045
+ * Notes:
21046
+ * `className` should not specify a border; that is handled by `borderColor`
21047
+ * so that the arrow and tooltip match
21048
+ *
21049
+ * ```
21050
+ * __,,--``\\
21051
+ * _,,-''`` \\ ,
21052
+ * '----------_.------'-.___|\__
21053
+ * _.--''`` `)__ )__ @\__
21054
+ * ( .. ''---/___,,E/__,E'------`
21055
+ * `-''`''
21056
+ * Here be dragons.
21057
+ * ```
21058
+ */
21004
21059
  class Tooltip extends React.Component {
21005
21060
  constructor(...args) {
21006
21061
  super(...args);
@@ -21227,10 +21282,7 @@ class InputWithExamples extends React.Component {
21227
21282
  return "- " + example;
21228
21283
  }).join("\n");
21229
21284
  const showExamples = this.props.shouldShowExamples && this.state.showExamples;
21230
- return /*#__PURE__*/React.createElement(Tooltip
21231
- // eslint-disable-next-line react/no-string-refs
21232
- , {
21233
- ref: "tooltip",
21285
+ return /*#__PURE__*/React.createElement(Tooltip, {
21234
21286
  className: "perseus-formats-tooltip preview-measure",
21235
21287
  horizontalPosition: HorizontalDirection.Left,
21236
21288
  horizontalAlign: HorizontalDirection.Left,
@@ -24747,6 +24799,37 @@ const BASE_ICON_SIZE = 10;
24747
24799
  // If you pass in an object you need to specify an SVG path to
24748
24800
  // render along with the width and height of the path (to ensure
24749
24801
  // that it gets scaled correctly).
24802
+ /**
24803
+ * An SVG Icon
24804
+ *
24805
+ * This component is designed to take in SVG paths and render icons based upon
24806
+ * them. If you are looking for an icon that we've used before you should look
24807
+ * in `icon-paths.js` which is a reference file for all the SVG paths that
24808
+ * we've used. You'll need to copy the object from that file into whichever
24809
+ * file you're using the icon and explicitly pass it in to the <Icon/> React
24810
+ * component.
24811
+ *
24812
+ * Sample usage:
24813
+ *
24814
+ * ```
24815
+ * const dropdownIcon = `M5,6L0,0L10,0`;
24816
+ * <Icon icon={dropdownIcon} />
24817
+ * ```
24818
+ *
24819
+ * Or:
24820
+ *
24821
+ * ```
24822
+ * const dropdownIcon = {
24823
+ * path: `M5,6L0,0L10,0`,
24824
+ * height: 10,
24825
+ * width: 10,
24826
+ * };
24827
+ * <Icon icon={dropdownIcon} size={20} />
24828
+ * ```
24829
+ *
24830
+ * If you want to add an entirely new icon please read the note inside
24831
+ * the `icon-paths.ts` file.
24832
+ */
24750
24833
  class Icon extends React.Component {
24751
24834
  render() {
24752
24835
  const {
@@ -24969,7 +25052,7 @@ function ChoiceInner(props) {
24969
25052
  } = usePerseusI18n();
24970
25053
  const letter = getChoiceLetter(pos, strings);
24971
25054
  if (!showCorrectness) {
24972
- return letter;
25055
+ return /*#__PURE__*/React.createElement("span", null, letter);
24973
25056
  }
24974
25057
  if (correct) {
24975
25058
  return /*#__PURE__*/React.createElement(InlineIcon, _extends({}, iconCheck, {
@@ -25039,7 +25122,6 @@ const ChoiceIcon = function ChoiceIcon(props) {
25039
25122
  visible: focused || hovered,
25040
25123
  multipleSelect: multipleSelect
25041
25124
  }, /*#__PURE__*/React.createElement("div", {
25042
- // @ts-expect-error - TS2322 - Type '{ backgroundColor: string | null | undefined; borderColor: string; color: string; borderRadius: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
25043
25125
  style: dynamicStyles,
25044
25126
  "data-testid": "choice-icon__library-choice-icon",
25045
25127
  className: css(styles$s.circle, showCorrectness && correct && styles$s.circleCorrect, showCorrectness && !correct && styles$s.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$s.circleIncorrectAnswered)
@@ -30873,6 +30955,10 @@ const HiddenIcon = () => /*#__PURE__*/React.createElement("svg", {
30873
30955
  }, /*#__PURE__*/React.createElement("path", {
30874
30956
  d: "M0 0h24v24H0z"
30875
30957
  }))));
30958
+ /**
30959
+ * A "heads-up display" (HUD) indicator that includes a short message (usually
30960
+ * used for linting errors). The indicator can be disabled.
30961
+ */
30876
30962
  const HUD = ({
30877
30963
  message,
30878
30964
  enabled,
@@ -31093,18 +31179,24 @@ const {
31093
31179
  const toNumericString = KhanMath.toNumericString;
31094
31180
  const getNumericFormat = KhanMath.getNumericFormat;
31095
31181
 
31096
- /* An input box that accepts only numeric strings
31182
+ /**
31183
+ * An input box that accepts only numeric strings
31097
31184
  *
31098
- * Calls onChange(value, format) for valid numbers.
31099
- * Reverts to the current value onBlur or on [ENTER],
31185
+ * Calls `onChange(value, format)` for valid numbers.
31186
+ *
31187
+ * Reverts to the current value `onBlur` or on [ENTER],
31100
31188
  * but maintains the format (i.e. 3/2, 1 1/2, 150%)
31101
- * Accepts empty input and sends it to onChange as null
31102
- * if no numeric placeholder is set.
31103
- * If given a checkValidity function, will turn
31104
- * the background/outline red when invalid
31105
- * If useArrowKeys is set to true, up/down arrows will
31106
- * increment/decrement integers
31107
- * Optionally takes a size ("mini", "small", "normal")
31189
+ *
31190
+ * Accepts empty input and sends it to `onChange` as `null` if no numeric
31191
+ * placeholder is set.
31192
+ *
31193
+ * If given a `checkValidity` function, will turn the background/outline red
31194
+ * when invalid.
31195
+ *
31196
+ * If `useArrowKeys` is set to `true`, up/down arrows will increment/decrement
31197
+ * integers.
31198
+ *
31199
+ * Optionally takes a `size` (`"mini"`, `"small"`,` `"normal"`)
31108
31200
  */
31109
31201
  class NumberInput extends React.Component {
31110
31202
  constructor(...args) {
@@ -31263,7 +31355,7 @@ NumberInput.propTypes = {
31263
31355
  onChange: PropTypes.func.isRequired,
31264
31356
  onFormatChange: PropTypes.func,
31265
31357
  checkValidity: PropTypes.func,
31266
- size: PropTypes.string,
31358
+ size: PropTypes.oneOf(["mini", "small", "normal"]),
31267
31359
  label: PropTypes.oneOf(["put your labels outside your inputs!"])
31268
31360
  };
31269
31361
  NumberInput.defaultProps = {
@@ -31277,8 +31369,8 @@ NumberInput.defaultProps = {
31277
31369
 
31278
31370
  const truth = () => true;
31279
31371
 
31280
- /* A minor abstraction on top of NumberInput for ranges
31281
- *
31372
+ /**
31373
+ * A minor abstraction on top of `NumberInput` for ranges
31282
31374
  */
31283
31375
  class RangeInput extends React.Component {
31284
31376
  constructor(...args) {
@@ -32129,6 +32221,327 @@ function reinitialize(params) {
32129
32221
  };
32130
32222
  }
32131
32223
 
32224
+ /**
32225
+ * Wrapper with Mafs transform styles for SVG elements.
32226
+ * Scales and translates the SVG element to match the Mafs view.
32227
+ * Use if you do not need/want to use the hook in `use-transform.ts`.
32228
+ */
32229
+ const MafsCssTransformWrapper = ({
32230
+ children
32231
+ }) => /*#__PURE__*/React.createElement("g", {
32232
+ style: {
32233
+ transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
32234
+ }
32235
+ }, children);
32236
+
32237
+ const _excluded$1 = ["children"];
32238
+ const TextLabel = _ref => {
32239
+ let {
32240
+ children
32241
+ } = _ref,
32242
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded$1);
32243
+ return /*#__PURE__*/React__default.createElement(Text$1, _extends({
32244
+ size: 16,
32245
+ svgTextProps: {
32246
+ filter: "url(#background)",
32247
+ fontWeight: "bold"
32248
+ }
32249
+ }, rest), children);
32250
+ };
32251
+ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*#__PURE__*/React__default.createElement("filter", {
32252
+ id: "background",
32253
+ x: "-5%",
32254
+ width: "110%",
32255
+ y: "0%",
32256
+ height: "100%"
32257
+ }, /*#__PURE__*/React__default.createElement("feFlood", {
32258
+ floodColor: "#FFF",
32259
+ floodOpacity: "0.64"
32260
+ }), /*#__PURE__*/React__default.createElement("feComposite", {
32261
+ operator: "over",
32262
+ in: "SourceGraphic"
32263
+ })));
32264
+
32265
+ const PolygonAngle = ({
32266
+ centerPoint,
32267
+ endPoints,
32268
+ range,
32269
+ polygonLines,
32270
+ showAngles,
32271
+ snapTo
32272
+ }) => {
32273
+ const [centerX, centerY] = centerPoint;
32274
+ const areClockwise = clockwise$1([centerPoint, ...endPoints]);
32275
+ const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
32276
+ const radius = 0.3;
32277
+ const a = vec.dist(centerPoint, endPoints[0]);
32278
+ const b = vec.dist(centerPoint, endPoints[1]);
32279
+ const c = vec.dist(endPoints[0], endPoints[1]);
32280
+
32281
+ // Law of cosines
32282
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
32283
+ const y1 = centerY + (startY - centerY) / a * radius;
32284
+ const x2 = centerX + (endX - centerX) / b * radius;
32285
+ const x1 = centerX + (startX - centerX) / a * radius;
32286
+ const y2 = centerY + (endY - centerY) / b * radius;
32287
+
32288
+ // Fourth point that would create a rhomboid
32289
+ // Used to calculate the inside of the angle and to create square
32290
+ const [x3, y3] = vec.add(centerPoint, vec.add(vec.sub([x1, y1], centerPoint), vec.sub([x2, y2], centerPoint)));
32291
+ if (!showAngles) {
32292
+ return isRightPolygonAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32293
+ start: [x1, y1],
32294
+ vertex: [x2, y2],
32295
+ end: [x3, y3]
32296
+ }) : null;
32297
+ }
32298
+
32299
+ // Midpoint betwen ends of arc
32300
+ const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
32301
+ const largeArcFlag = isOutside ? 1 : 0;
32302
+ const sweepFlag = isOutside ? 1 : 0;
32303
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32304
+ let angleInDegrees = angle * (180 / Math.PI);
32305
+ // If we have triggered "largArcFlag", the angle should be greater than 180
32306
+ if (isOutside) {
32307
+ angleInDegrees = 360 - angleInDegrees;
32308
+ }
32309
+
32310
+ // Only want to show whole numbers when in angles mode
32311
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
32312
+ const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
32313
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
32314
+ id: "background",
32315
+ x: "-5%",
32316
+ width: "110%",
32317
+ y: "0%",
32318
+ height: "100%"
32319
+ }, /*#__PURE__*/React.createElement("feFlood", {
32320
+ floodColor: "#FFF",
32321
+ floodOpacity: "0.5"
32322
+ }), /*#__PURE__*/React.createElement("feComposite", {
32323
+ operator: "over",
32324
+ in: "SourceGraphic"
32325
+ }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32326
+ start: [x1, y1],
32327
+ vertex: [x2, y2],
32328
+ end: [x3, y3]
32329
+ }) : /*#__PURE__*/React.createElement(Arc, {
32330
+ arc: arc
32331
+ }), /*#__PURE__*/React.createElement(TextLabel, {
32332
+ x: x3,
32333
+ y: y3,
32334
+ attach: y3 - centerY > 0 ? "s" : "n",
32335
+ attachDistance: Math.abs(y3 - centerY) < 0.2 || vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
32336
+ }, angleLabel, "\xB0"));
32337
+ };
32338
+ const Angle = ({
32339
+ vertex,
32340
+ coords,
32341
+ showAngles,
32342
+ allowReflexAngles,
32343
+ range
32344
+ }) => {
32345
+ // Check if the points are clockwise or not, depending on whether we allow reflex angles
32346
+ const areClockwise = clockwise$1([...coords, vertex]);
32347
+ const shouldReverseCoords = areClockwise && !allowReflexAngles;
32348
+
32349
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
32350
+ const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32351
+
32352
+ // Calculate the angles between the two points
32353
+ const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32354
+ const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
32355
+ const angle = (startAngle + 360 - endAngle) % 360;
32356
+
32357
+ // Check if the angle is reflexive
32358
+ const isReflexive = angle > 180;
32359
+
32360
+ // Break out the necessary points for the arc calculation
32361
+ const [centerX, centerY] = vertex;
32362
+ const [point1, point2] = clockwiseCoords;
32363
+ const [startX, startY] = point1;
32364
+ const [endX, endY] = point2;
32365
+ const a = vec.dist(vertex, point1);
32366
+ const b = vec.dist(vertex, point2);
32367
+
32368
+ // Set the radius of the arc
32369
+ const radius = 2;
32370
+
32371
+ // Calculate the end points of the arc
32372
+ const y1 = centerY + (startY - centerY) / a * radius;
32373
+ const x2 = centerX + (endX - centerX) / b * radius;
32374
+ const x1 = centerX + (startX - centerX) / a * radius;
32375
+ const y2 = centerY + (endY - centerY) / b * radius;
32376
+
32377
+ // Used to calculate the inside of the angle and to create square
32378
+ const [x3, y3] = vec.add(vertex, vec.add(vec.sub([x1, y1], vertex), vec.sub([x2, y2], vertex)));
32379
+
32380
+ // Determine whether the arc should be drawn outside the angle
32381
+ const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
32382
+
32383
+ // Determine the flags for the arc
32384
+ // If the angle is outside OR reflexive, we want to draw a large arc
32385
+ // If the angle is outside AND reflexive, we want to sweep the arc
32386
+ const largeArcFlag = isOutside || isReflexive ? 1 : 0;
32387
+ const sweepFlag = isOutside && isReflexive ? 1 : 0;
32388
+
32389
+ // Create the SVG path for the arc
32390
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32391
+
32392
+ // We only ever want to show whole angles
32393
+ const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
32394
+
32395
+ // Calculate the text position based on the angle and whether we allow reflex angles
32396
+ // Let's try the angle bisector method to find the midpoint of the arc
32397
+ const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
32398
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
32399
+ id: "background",
32400
+ x: "-5%",
32401
+ width: "110%",
32402
+ y: "0%",
32403
+ height: "100%"
32404
+ }, /*#__PURE__*/React.createElement("feFlood", {
32405
+ floodColor: "#FFF",
32406
+ floodOpacity: "0.5"
32407
+ }), /*#__PURE__*/React.createElement("feComposite", {
32408
+ operator: "over",
32409
+ in: "SourceGraphic"
32410
+ }))), isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32411
+ start: [x1, y1],
32412
+ vertex: [x2, y2],
32413
+ end: [x3, y3],
32414
+ className: "arc-right-angle"
32415
+ }) : /*#__PURE__*/React.createElement(Arc, {
32416
+ arc: arc,
32417
+ className: "angle-arc"
32418
+ }), showAngles && /*#__PURE__*/React.createElement(TextLabel, {
32419
+ x: textX,
32420
+ y: textY,
32421
+ color: color.blue
32422
+ }, angleLabel, "\xB0"));
32423
+ };
32424
+
32425
+ /**
32426
+ * This is broken out into its own component so that it can be used for an early return
32427
+ * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
32428
+ */
32429
+ const RightAngleSquare = ({
32430
+ start: [x1, y1],
32431
+ vertex: [x2, y2],
32432
+ end: [x3, y3],
32433
+ className
32434
+ }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
32435
+ d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
32436
+ strokeWidth: 0.02,
32437
+ fill: "none",
32438
+ className: className
32439
+ }));
32440
+
32441
+ // We're conditionally adding the class name here so that we can style the arc differently
32442
+ // based on whether it's an angle or a polygon angle
32443
+ const Arc = ({
32444
+ arc,
32445
+ className
32446
+ }) => {
32447
+ return /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
32448
+ d: arc,
32449
+ strokeWidth: 0.02,
32450
+ fill: "none",
32451
+ className: className
32452
+ }));
32453
+ };
32454
+ const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
32455
+ const isRightAngle = angle => Math.round(angle) === 90;
32456
+
32457
+ /**
32458
+ * Determines if an angle is an inside (false) or outside (true) angle.
32459
+ * They way, we know to flip the `largeArc` and `sweepArc` flags.
32460
+ * Uses the priciple that a ray from a point inside a polygon will intersect
32461
+ * with an odd number of lines, while a ray from a point outside the polygon
32462
+ * will intersect with an even number of lines.
32463
+ * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
32464
+ */
32465
+ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
32466
+ // Create a ray from the midpoint (inside angle) to the edge of the range
32467
+ const rangeIntersectionPoint = getIntersectionOfRayWithBox(midpoint, vertex, range);
32468
+ let lineIntersectionCount = 0;
32469
+ polygonLines.forEach(line => segmentsIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
32470
+
32471
+ // If the number of intersections is even, the angle is inside the polygon
32472
+ return !isEven(lineIntersectionCount);
32473
+ };
32474
+ const isEven = n => n % 2 === 0;
32475
+
32476
+ // This function calculates the bisector point of an angle formed by two points
32477
+ // and a vertex. It is used to position the angle label so that it is always
32478
+ // outside the arc at the exact midpoint of the angle.
32479
+ // Reflex angles make use of polar coordinates to determine the correct direction
32480
+ function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
32481
+ const [originX, originY] = vertex;
32482
+ const [x1, y1] = point1;
32483
+ const [x2, y2] = point2;
32484
+
32485
+ // Convert the Cartesian Coordinates to vectors
32486
+ const vectorA = [x1 - originX, y1 - originY];
32487
+ const vectorB = [x2 - originX, y2 - originY];
32488
+
32489
+ // Calculate the average angle of the vectors
32490
+ // so that we can point the bisector in the correct direction using polar coordinates
32491
+ const angleA = Math.atan2(vectorA[1], vectorA[0]);
32492
+ const angleB = Math.atan2(vectorB[1], vectorB[0]);
32493
+
32494
+ // Calculate the average of these two angles
32495
+ let averageAngle = (angleA + angleB) / 2;
32496
+ const angleRadians = Math.abs(angleA - angleB);
32497
+
32498
+ // Adjust the average angle to point in the correct direction
32499
+ // and to account for reflex angles and a bug with 180 degrees
32500
+ // that occurs due to our angleOffsetDegrees option in the graph
32501
+ if (allowReflex) {
32502
+ // If the resulting angles are less than 180 degrees,
32503
+ // or if the user has flipped the points on the interactive element
32504
+ // ensure that we have the correct direction for the reflex angle
32505
+ if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
32506
+ averageAngle += Math.PI; // Add 180 degrees
32507
+ }
32508
+ } else {
32509
+ // If the resulting angles are greater than 180 degrees, adjust
32510
+ // to get the correct angle, as we already know that the angle is not reflexive
32511
+ // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
32512
+ if (angleRadians > Math.PI) {
32513
+ averageAngle -= Math.PI; // Subract 180 degrees
32514
+ }
32515
+ }
32516
+
32517
+ // Convert the average angle back to Cartesian coordinates
32518
+ const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
32519
+
32520
+ // Calculate the magnitude of the sum to normalize it
32521
+ const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
32522
+ const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
32523
+
32524
+ // Calculate the initial distance of the bisector direction from the origin
32525
+ const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
32526
+
32527
+ // Determine the minimum radius to ensure that the text is always outside the arc
32528
+ const requiredDistance = arcRadius * 1.75;
32529
+ let radius = requiredDistance / initialDistance;
32530
+
32531
+ // Apply radius only if the initial distance is less than required
32532
+ if (initialDistance >= requiredDistance) {
32533
+ radius = 1; // Use the unit vector as is
32534
+ }
32535
+
32536
+ // Scale the bisector direction by the radius
32537
+ const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
32538
+
32539
+ // Add the vertex to the bisector point to get the final position
32540
+ // to ensure that the angle label moves with the interactive element
32541
+ const scaledBisector = vec.add(bisectorPoint, vertex);
32542
+ return scaledBisector;
32543
+ }
32544
+
32132
32545
  /**
32133
32546
  * 44 is touch best practice and AAA compliant for WCAG
32134
32547
  * https://www.w3.org/WAI/WCAG21/Understanding/target-size.html
@@ -32334,6 +32747,14 @@ function range(min, max, step = 1) {
32334
32747
  return result;
32335
32748
  }
32336
32749
 
32750
+ function srFormatNumber(a, locale) {
32751
+ // adding zero here converts negative zero to positive zero.
32752
+ return (0 + a).toLocaleString(locale, {
32753
+ maximumFractionDigits: 3,
32754
+ useGrouping: false // no thousands separators
32755
+ });
32756
+ }
32757
+
32337
32758
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
32338
32759
  // graph.
32339
32760
  const hitboxSizePx = 48;
@@ -32448,6 +32869,10 @@ function useControlPoint(params) {
32448
32869
  onBlur = noop$1,
32449
32870
  forwardedRef = noop$1
32450
32871
  } = params;
32872
+ const {
32873
+ strings,
32874
+ locale
32875
+ } = usePerseusI18n();
32451
32876
  const [focused, setFocused] = useState(false);
32452
32877
  const focusableHandleRef = useRef(null);
32453
32878
  useDraggable({
@@ -32473,6 +32898,15 @@ function useControlPoint(params) {
32473
32898
  className: "movable-point__focusable-handle",
32474
32899
  tabIndex: disableKeyboardInteraction ? -1 : 0,
32475
32900
  ref: focusableHandleRef,
32901
+ role: "button",
32902
+ "aria-label": strings.srPointAtCoordinates({
32903
+ x: srFormatNumber(point[X], locale),
32904
+ y: srFormatNumber(point[Y], locale)
32905
+ })
32906
+ // aria-live="assertive" causes the new location of the point to be
32907
+ // announced immediately on move.
32908
+ ,
32909
+ "aria-live": "assertive",
32476
32910
  onFocus: event => {
32477
32911
  onFocus(event);
32478
32912
  setFocused(true);
@@ -32653,25 +33087,338 @@ function trimRange(range, graphDimensionsInPixels) {
32653
33087
  return inset([graphUnitsToTrimX, graphUnitsToTrimY], range);
32654
33088
  }
32655
33089
 
32656
- const SegmentGraph = props => {
33090
+ const MovablePoint$1 = /*#__PURE__*/React.forwardRef((props, pointRef) => {
32657
33091
  const {
32658
- dispatch
33092
+ visiblePoint,
33093
+ focusableHandle
33094
+ } = useControlPoint(_extends({}, props, {
33095
+ forwardedRef: pointRef
33096
+ }));
33097
+ return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle, visiblePoint);
33098
+ });
33099
+
33100
+ function renderAngleGraph(state, dispatch) {
33101
+ return {
33102
+ graph: /*#__PURE__*/React.createElement(AngleGraph, {
33103
+ graphState: state,
33104
+ dispatch: dispatch
33105
+ }),
33106
+ interactiveElementsDescription: null
33107
+ };
33108
+ }
33109
+ function AngleGraph(props) {
33110
+ const {
33111
+ dispatch,
33112
+ graphState
32659
33113
  } = props;
32660
33114
  const {
32661
- coords: segments
32662
- } = props.graphState;
32663
- return /*#__PURE__*/React.createElement(React.Fragment, null, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement(MovableLine, {
32664
- key: i,
32665
- points: segment,
32666
- onMoveLine: delta => {
32667
- dispatch(actions.segment.moveLine(i, delta));
32668
- },
32669
- onMovePoint: (endpointIndex, destination) => {
32670
- dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
33115
+ graphDimensionsInPixels
33116
+ } = useGraphConfig();
33117
+ const {
33118
+ coords,
33119
+ showAngles,
33120
+ range,
33121
+ allowReflexAngles,
33122
+ snapDegrees
33123
+ } = graphState;
33124
+
33125
+ // Break the coords into the two end points and the center point
33126
+ const endPoints = [coords[0], coords[2]];
33127
+ const centerPoint = coords[1];
33128
+
33129
+ // Convert the vectors to pixels for rendering the svg lines
33130
+ const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
33131
+ const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
33132
+
33133
+ // Create the SVG lines for the angle
33134
+ const svgLines = linePixelCoords.map(([startPtPx, endPtPx], i) => {
33135
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
33136
+ const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
33137
+ return /*#__PURE__*/React.createElement("g", {
33138
+ key: `line-${i}`
33139
+ }, /*#__PURE__*/React.createElement(SVGLine, {
33140
+ start: startPtPx,
33141
+ end: endPtPx,
33142
+ style: {
33143
+ stroke: "var(--movable-line-stroke-color)",
33144
+ strokeWidth: "var(--movable-line-stroke-weight)"
33145
+ }
33146
+ }), /*#__PURE__*/React.createElement(Vector, {
33147
+ tail: angleLines[i][1],
33148
+ tip: endExtend,
33149
+ color: "var(--movable-line-stroke-color)"
33150
+ }));
33151
+ });
33152
+
33153
+ // Create the angle indicator parameters
33154
+ const angleParams = {
33155
+ vertex: centerPoint,
33156
+ coords: endPoints,
33157
+ allowReflexAngles: allowReflexAngles || false,
33158
+ // Whether to allow reflex angles or not
33159
+ snapDegrees: snapDegrees || 1,
33160
+ // The multiple of degrees to snap to
33161
+ range: range,
33162
+ showAngles: showAngles || false // Whether to show the angle or not
33163
+ };
33164
+
33165
+ // Render the lines, angle, and then movable points
33166
+ return /*#__PURE__*/React.createElement(React.Fragment, null, svgLines, /*#__PURE__*/React.createElement(Angle, angleParams), /*#__PURE__*/React.createElement(MovablePoint$1, {
33167
+ point: coords[1],
33168
+ constrain: p => p,
33169
+ onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33170
+ }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33171
+ point: coords[0],
33172
+ constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33173
+ onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33174
+ }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33175
+ point: coords[2],
33176
+ constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33177
+ onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33178
+ }));
33179
+ }
33180
+ const positiveX = [1, 0];
33181
+ const negativeX = [-1, 0];
33182
+ const positiveY = [0, 1];
33183
+ const negativeY = [0, -1];
33184
+ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33185
+ var _findIntersectionOfRa, _findIntersectionOfRa2, _findIntersectionOfRa3, _findIntersectionOfRa4;
33186
+ const currentAngle = calculateAngleInDegrees(vec.sub(sidePoint, vertex));
33187
+
33188
+ // Find the rays that start at the current point and point up, down, left
33189
+ // and right.
33190
+ const leftRay = [sidePoint, vec.add(sidePoint, negativeX)];
33191
+ const rightRay = [sidePoint, vec.add(sidePoint, positiveX)];
33192
+ const upRay = [sidePoint, vec.add(sidePoint, positiveY)];
33193
+ const downRay = [sidePoint, vec.add(sidePoint, negativeY)];
33194
+
33195
+ // find the angles that lie one snap step clockwise and counter-clockwise
33196
+ // from the current angle. These are the angles to which the side can be
33197
+ // moved.
33198
+ const oneStepCounterClockwise = currentAngle + snapDegrees;
33199
+ const oneStepClockwise = currentAngle - snapDegrees;
33200
+
33201
+ // find the rays that start from the vertex and point in the direction of
33202
+ // the angles we just computed.
33203
+ const counterClockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepCounterClockwise))];
33204
+ const clockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepClockwise))];
33205
+
33206
+ // find the intersections of those rays with the horizontal and vertical
33207
+ // rays extending from the sidePoint. These intersections are the points to
33208
+ // which sidePoint can move that will result in a rotation of `snapDegrees`.
33209
+ const left = (_findIntersectionOfRa = findIntersectionOfRays(leftRay, counterClockwiseRay)) != null ? _findIntersectionOfRa : findIntersectionOfRays(leftRay, clockwiseRay);
33210
+ const right = (_findIntersectionOfRa2 = findIntersectionOfRays(rightRay, counterClockwiseRay)) != null ? _findIntersectionOfRa2 : findIntersectionOfRays(rightRay, clockwiseRay);
33211
+ const up = (_findIntersectionOfRa3 = findIntersectionOfRays(upRay, counterClockwiseRay)) != null ? _findIntersectionOfRa3 : findIntersectionOfRays(upRay, clockwiseRay);
33212
+ const down = (_findIntersectionOfRa4 = findIntersectionOfRays(downRay, counterClockwiseRay)) != null ? _findIntersectionOfRa4 : findIntersectionOfRays(downRay, clockwiseRay);
33213
+ return {
33214
+ up: up != null ? up : sidePoint,
33215
+ down: down != null ? down : sidePoint,
33216
+ left: left != null ? left : sidePoint,
33217
+ right: right != null ? right : sidePoint
33218
+ };
33219
+ }
33220
+
33221
+ function getGradableGraph(state, initialGraph) {
33222
+ if (!state.hasBeenInteractedWith) {
33223
+ return _extends({}, initialGraph);
33224
+ }
33225
+ if (initialGraph.type === "linear-system" && state.type === "linear-system") {
33226
+ return _extends({}, initialGraph, {
33227
+ coords: state.coords
33228
+ });
33229
+ }
33230
+ if (state.type === "segment" && initialGraph.type === "segment") {
33231
+ return _extends({}, initialGraph, {
33232
+ coords: state.coords
33233
+ });
33234
+ }
33235
+ if (state.type === "linear" && initialGraph.type === "linear") {
33236
+ return _extends({}, initialGraph, {
33237
+ coords: state.coords
33238
+ });
33239
+ }
33240
+ if (state.type === "ray" && initialGraph.type === "ray") {
33241
+ return _extends({}, initialGraph, {
33242
+ coords: state.coords
33243
+ });
33244
+ }
33245
+ if (state.type === "polygon" && initialGraph.type === "polygon") {
33246
+ return _extends({}, initialGraph, {
33247
+ coords: state.coords
33248
+ });
33249
+ }
33250
+ if (state.type === "point" && initialGraph.type === "point") {
33251
+ return _extends({}, initialGraph, {
33252
+ coords: state.coords
33253
+ });
33254
+ }
33255
+ if (state.type === "circle" && initialGraph.type === "circle") {
33256
+ return _extends({}, initialGraph, {
33257
+ center: state.center,
33258
+ radius: getRadius(state)
33259
+ });
33260
+ }
33261
+ if (state.type === "quadratic" && initialGraph.type === "quadratic") {
33262
+ return _extends({}, initialGraph, {
33263
+ coords: state.coords
33264
+ });
33265
+ }
33266
+ if (state.type === "sinusoid" && initialGraph.type === "sinusoid") {
33267
+ return _extends({}, initialGraph, {
33268
+ coords: state.coords
33269
+ });
33270
+ }
33271
+ if (state.type === "angle" && initialGraph.type === "angle") {
33272
+ // We're going to reverse the coords for scoring if the angle is clockwise and
33273
+ // we don't allow reflex angles. This is because the angle is largely defined
33274
+ // by the order of the points in the coords array, and we want to maintain
33275
+ // the same angle scoring with the legacy graph (which had a bug).
33276
+ // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33277
+ const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
33278
+ const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33279
+ const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33280
+ return _extends({}, initialGraph, {
33281
+ coords,
33282
+ allowReflexAngles: state.allowReflexAngles
33283
+ });
33284
+ }
33285
+ if (state.type === "none" && initialGraph.type === "none") {
33286
+ return {
33287
+ type: "none"
33288
+ };
33289
+ }
33290
+ throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
33291
+ }
33292
+
33293
+ /**
33294
+ * determine radius of a circle graph
33295
+ *
33296
+ * @param graph - the graph object containing the circle
33297
+ * @returns the radius of the circle
33298
+ */
33299
+ function getRadius(graph) {
33300
+ const [centerX, centerY] = graph.center;
33301
+ const [edgeX, edgeY] = graph.radiusPoint;
33302
+ return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
33303
+ }
33304
+
33305
+ function renderCircleGraph(state, dispatch) {
33306
+ return {
33307
+ graph: /*#__PURE__*/React.createElement(CircleGraph, {
33308
+ graphState: state,
33309
+ dispatch: dispatch
33310
+ }),
33311
+ interactiveElementsDescription: null
33312
+ };
33313
+ }
33314
+ function CircleGraph(props) {
33315
+ const {
33316
+ dispatch,
33317
+ graphState
33318
+ } = props;
33319
+ const {
33320
+ center,
33321
+ radiusPoint
33322
+ } = graphState;
33323
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MovableCircle, {
33324
+ center: center,
33325
+ radius: getRadius(graphState),
33326
+ onMove: c => dispatch(actions.circle.moveCenter(c))
33327
+ }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33328
+ point: radiusPoint,
33329
+ cursor: "ew-resize",
33330
+ onMove: newRadiusPoint => {
33331
+ dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
32671
33332
  }
32672
- })));
32673
- };
33333
+ }));
33334
+ }
33335
+ function MovableCircle(props) {
33336
+ const {
33337
+ center,
33338
+ radius,
33339
+ onMove
33340
+ } = props;
33341
+ const {
33342
+ snapStep,
33343
+ disableKeyboardInteraction
33344
+ } = useGraphConfig();
33345
+ const draggableRef = useRef(null);
33346
+ const {
33347
+ dragging
33348
+ } = useDraggable({
33349
+ gestureTarget: draggableRef,
33350
+ point: center,
33351
+ onMove,
33352
+ constrainKeyboardMovement: p => snap(snapStep, p)
33353
+ });
33354
+ const [centerPx] = useTransformVectorsToPixels(center);
33355
+ const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
33356
+ return /*#__PURE__*/React.createElement("g", {
33357
+ ref: draggableRef,
33358
+ tabIndex: disableKeyboardInteraction ? -1 : 0,
33359
+ className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
33360
+ }, /*#__PURE__*/React.createElement("ellipse", {
33361
+ className: "focus-ring",
33362
+ cx: centerPx[X],
33363
+ cy: centerPx[Y],
33364
+ rx: radiiPx[X] + 3,
33365
+ ry: radiiPx[Y] + 3
33366
+ }), /*#__PURE__*/React.createElement("ellipse", {
33367
+ className: "circle",
33368
+ cx: centerPx[X],
33369
+ cy: centerPx[Y],
33370
+ rx: radiiPx[X],
33371
+ ry: radiiPx[Y]
33372
+ }), /*#__PURE__*/React.createElement(DragHandle, {
33373
+ center: center
33374
+ }));
33375
+ }
33376
+ const dragHandleDimensions = [24, 14];
33377
+ const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
33378
+ function DragHandle(props) {
33379
+ const {
33380
+ center
33381
+ } = props;
33382
+ const cornerRadius = Math.min(...dragHandleDimensions) / 2;
33383
+ const [centerPx] = useTransformVectorsToPixels(center);
33384
+ const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
33385
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
33386
+ className: "movable-circle-handle",
33387
+ x: topLeft[X],
33388
+ y: topLeft[Y],
33389
+ width: dragHandleDimensions[X],
33390
+ height: dragHandleDimensions[Y],
33391
+ rx: cornerRadius,
33392
+ ry: cornerRadius
33393
+ }), dragHandleDotPositions.map(offsetPx => {
33394
+ const [xPx, yPx] = vec.add(offsetPx, centerPx);
33395
+ return /*#__PURE__*/React.createElement("circle", {
33396
+ key: `circle-${xPx}-${yPx}`,
33397
+ className: "movable-circle-handle-dot",
33398
+ cx: xPx,
33399
+ cy: yPx
33400
+ });
33401
+ }));
33402
+ }
33403
+ function crossProduct(as, bs) {
33404
+ const result = [];
33405
+ for (const a of as) {
33406
+ for (const b of bs) {
33407
+ result.push([a, b]);
33408
+ }
33409
+ }
33410
+ return result;
33411
+ }
32674
33412
 
33413
+ function renderLinearGraph(state, dispatch) {
33414
+ return {
33415
+ graph: /*#__PURE__*/React.createElement(LinearGraph, {
33416
+ graphState: state,
33417
+ dispatch: dispatch
33418
+ }),
33419
+ interactiveElementsDescription: null
33420
+ };
33421
+ }
32675
33422
  const LinearGraph = (props, key) => {
32676
33423
  const {
32677
33424
  dispatch
@@ -32698,6 +33445,15 @@ const LinearGraph = (props, key) => {
32698
33445
  });
32699
33446
  };
32700
33447
 
33448
+ function renderLinearSystemGraph(state, dispatch) {
33449
+ return {
33450
+ graph: /*#__PURE__*/React.createElement(LinearSystemGraph, {
33451
+ graphState: state,
33452
+ dispatch: dispatch
33453
+ }),
33454
+ interactiveElementsDescription: null
33455
+ };
33456
+ }
32701
33457
  const LinearSystemGraph = props => {
32702
33458
  const {
32703
33459
  dispatch
@@ -32720,359 +33476,121 @@ const LinearSystemGraph = props => {
32720
33476
  })), ";");
32721
33477
  };
32722
33478
 
32723
- const RayGraph = props => {
33479
+ function renderPointGraph(state, dispatch) {
33480
+ return {
33481
+ graph: /*#__PURE__*/React.createElement(PointGraph, {
33482
+ graphState: state,
33483
+ dispatch: dispatch
33484
+ }),
33485
+ interactiveElementsDescription: /*#__PURE__*/React.createElement(PointGraphDescription, {
33486
+ state: state
33487
+ })
33488
+ };
33489
+ }
33490
+ function PointGraph(props) {
33491
+ const numPoints = props.graphState.numPoints;
33492
+ if (numPoints === "unlimited") {
33493
+ return UnlimitedPointGraph(props);
33494
+ }
33495
+ return LimitedPointGraph(props);
33496
+ }
33497
+ function LimitedPointGraph(props) {
32724
33498
  const {
32725
33499
  dispatch
32726
33500
  } = props;
33501
+ return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33502
+ key: i,
33503
+ point: point,
33504
+ onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
33505
+ })));
33506
+ }
33507
+ function UnlimitedPointGraph(props) {
32727
33508
  const {
32728
- coords: line
32729
- } = props.graphState;
32730
- const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
32731
- const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
32732
-
32733
- // Ray graphs only have one line
32734
- return /*#__PURE__*/React.createElement(MovableLine, {
32735
- points: line,
32736
- onMoveLine: handleMoveLine,
32737
- onMovePoint: handleMovePoint,
32738
- extend: {
32739
- start: false,
32740
- end: true
32741
- }
32742
- });
32743
- };
32744
-
32745
- /**
32746
- * Wrapper with Mafs transform styles for SVG elements.
32747
- * Scales and translates the SVG element to match the Mafs view.
32748
- * Use if you do not need/want to use the hook in `use-transform.ts`.
32749
- */
32750
- const MafsCssTransformWrapper = ({
32751
- children
32752
- }) => /*#__PURE__*/React.createElement("g", {
32753
- style: {
32754
- transform: `var(--mafs-view-transform) var(--mafs-user-transform)`
32755
- }
32756
- }, children);
32757
-
32758
- const _excluded$1 = ["children"];
32759
- const TextLabel = _ref => {
32760
- let {
32761
- children
32762
- } = _ref,
32763
- rest = _objectWithoutPropertiesLoose(_ref, _excluded$1);
32764
- return /*#__PURE__*/React__default.createElement(Text$1, _extends({
32765
- size: 16,
32766
- svgTextProps: {
32767
- filter: "url(#background)",
32768
- fontWeight: "bold"
33509
+ dispatch
33510
+ } = props;
33511
+ const graphConfig = useGraphConfig();
33512
+ const {
33513
+ range: [[minX, maxX], [minY, maxY]]
33514
+ } = graphConfig;
33515
+ const width = maxX - minX;
33516
+ const height = maxY - minY;
33517
+ const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
33518
+ const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
33519
+ const itemsRef = React.useRef([]);
33520
+ React.useEffect(() => {
33521
+ const focusedIndex = props.graphState.focusedPointIndex;
33522
+ if (focusedIndex != null) {
33523
+ var _itemsRef$current$foc;
33524
+ (_itemsRef$current$foc = itemsRef.current[focusedIndex]) == null || _itemsRef$current$foc.focus();
32769
33525
  }
32770
- }, rest), children);
32771
- };
32772
- const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*#__PURE__*/React__default.createElement("filter", {
32773
- id: "background",
32774
- x: "-5%",
32775
- width: "110%",
32776
- y: "0%",
32777
- height: "100%"
32778
- }, /*#__PURE__*/React__default.createElement("feFlood", {
32779
- floodColor: "#FFF",
32780
- floodOpacity: "0.64"
32781
- }), /*#__PURE__*/React__default.createElement("feComposite", {
32782
- operator: "over",
32783
- in: "SourceGraphic"
32784
- })));
32785
-
32786
- const PolygonAngle = ({
32787
- centerPoint,
32788
- endPoints,
32789
- range,
32790
- polygonLines,
32791
- showAngles,
32792
- snapTo
32793
- }) => {
32794
- const [centerX, centerY] = centerPoint;
32795
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
32796
- const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
32797
- const radius = 0.3;
32798
- const a = vec.dist(centerPoint, endPoints[0]);
32799
- const b = vec.dist(centerPoint, endPoints[1]);
32800
- const c = vec.dist(endPoints[0], endPoints[1]);
32801
-
32802
- // Law of cosines
32803
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
32804
- const y1 = centerY + (startY - centerY) / a * radius;
32805
- const x2 = centerX + (endX - centerX) / b * radius;
32806
- const x1 = centerX + (startX - centerX) / a * radius;
32807
- const y2 = centerY + (endY - centerY) / b * radius;
32808
-
32809
- // Fourth point that would create a rhomboid
32810
- // Used to calculate the inside of the angle and to create square
32811
- const [x3, y3] = vec.add(centerPoint, vec.add(vec.sub([x1, y1], centerPoint), vec.sub([x2, y2], centerPoint)));
32812
- if (!showAngles) {
32813
- return isRightPolygonAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32814
- start: [x1, y1],
32815
- vertex: [x2, y2],
32816
- end: [x3, y3]
32817
- }) : null;
32818
- }
32819
-
32820
- // Midpoint betwen ends of arc
32821
- const isOutside = shouldDrawArcOutside([x3, y3], centerPoint, range, polygonLines);
32822
- const largeArcFlag = isOutside ? 1 : 0;
32823
- const sweepFlag = isOutside ? 1 : 0;
32824
- const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32825
- let angleInDegrees = angle * (180 / Math.PI);
32826
- // If we have triggered "largArcFlag", the angle should be greater than 180
32827
- if (isOutside) {
32828
- angleInDegrees = 360 - angleInDegrees;
32829
- }
32830
-
32831
- // Only want to show whole numbers when in angles mode
32832
- const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
32833
- const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
32834
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
32835
- id: "background",
32836
- x: "-5%",
32837
- width: "110%",
32838
- y: "0%",
32839
- height: "100%"
32840
- }, /*#__PURE__*/React.createElement("feFlood", {
32841
- floodColor: "#FFF",
32842
- floodOpacity: "0.5"
32843
- }), /*#__PURE__*/React.createElement("feComposite", {
32844
- operator: "over",
32845
- in: "SourceGraphic"
32846
- }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32847
- start: [x1, y1],
32848
- vertex: [x2, y2],
32849
- end: [x3, y3]
32850
- }) : /*#__PURE__*/React.createElement(Arc, {
32851
- arc: arc
32852
- }), /*#__PURE__*/React.createElement(TextLabel, {
32853
- x: x3,
32854
- y: y3,
32855
- attach: y3 - centerY > 0 ? "s" : "n",
32856
- attachDistance: Math.abs(y3 - centerY) < 0.2 || vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
32857
- }, angleLabel, "\xB0"));
32858
- };
32859
- const Angle = ({
32860
- vertex,
32861
- coords,
32862
- showAngles,
32863
- allowReflexAngles,
32864
- range
32865
- }) => {
32866
- // Check if the points are clockwise or not, depending on whether we allow reflex angles
32867
- const areClockwise = clockwise$1([...coords, vertex]);
32868
- const shouldReverseCoords = areClockwise && !allowReflexAngles;
32869
-
32870
- // Reverse the coordinates accordingly to ensure the angle is calculated correctly
32871
- const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
32872
-
32873
- // Calculate the angles between the two points
32874
- const startAngle = getAngleFromVertex(clockwiseCoords[0], vertex);
32875
- const endAngle = getAngleFromVertex(clockwiseCoords[1], vertex);
32876
- const angle = (startAngle + 360 - endAngle) % 360;
32877
-
32878
- // Check if the angle is reflexive
32879
- const isReflexive = angle > 180;
32880
-
32881
- // Break out the necessary points for the arc calculation
32882
- const [centerX, centerY] = vertex;
32883
- const [point1, point2] = clockwiseCoords;
32884
- const [startX, startY] = point1;
32885
- const [endX, endY] = point2;
32886
- const a = vec.dist(vertex, point1);
32887
- const b = vec.dist(vertex, point2);
32888
-
32889
- // Set the radius of the arc
32890
- const radius = 2;
32891
-
32892
- // Calculate the end points of the arc
32893
- const y1 = centerY + (startY - centerY) / a * radius;
32894
- const x2 = centerX + (endX - centerX) / b * radius;
32895
- const x1 = centerX + (startX - centerX) / a * radius;
32896
- const y2 = centerY + (endY - centerY) / b * radius;
32897
-
32898
- // Used to calculate the inside of the angle and to create square
32899
- const [x3, y3] = vec.add(vertex, vec.add(vec.sub([x1, y1], vertex), vec.sub([x2, y2], vertex)));
32900
-
32901
- // Determine whether the arc should be drawn outside the angle
32902
- const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
32903
-
32904
- // Determine the flags for the arc
32905
- // If the angle is outside OR reflexive, we want to draw a large arc
32906
- // If the angle is outside AND reflexive, we want to sweep the arc
32907
- const largeArcFlag = isOutside || isReflexive ? 1 : 0;
32908
- const sweepFlag = isOutside && isReflexive ? 1 : 0;
32909
-
32910
- // Create the SVG path for the arc
32911
- const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
32912
-
32913
- // We only ever want to show whole angles
32914
- const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
32915
-
32916
- // Calculate the text position based on the angle and whether we allow reflex angles
32917
- // Let's try the angle bisector method to find the midpoint of the arc
32918
- const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
32919
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
32920
- id: "background",
32921
- x: "-5%",
32922
- width: "110%",
32923
- y: "0%",
32924
- height: "100%"
32925
- }, /*#__PURE__*/React.createElement("feFlood", {
32926
- floodColor: "#FFF",
32927
- floodOpacity: "0.5"
32928
- }), /*#__PURE__*/React.createElement("feComposite", {
32929
- operator: "over",
32930
- in: "SourceGraphic"
32931
- }))), isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
32932
- start: [x1, y1],
32933
- vertex: [x2, y2],
32934
- end: [x3, y3],
32935
- className: "arc-right-angle"
32936
- }) : /*#__PURE__*/React.createElement(Arc, {
32937
- arc: arc,
32938
- className: "angle-arc"
32939
- }), showAngles && /*#__PURE__*/React.createElement(TextLabel, {
32940
- x: textX,
32941
- y: textY,
32942
- color: color.blue
32943
- }, angleLabel, "\xB0"));
32944
- };
32945
-
32946
- /**
32947
- * This is broken out into its own component so that it can be used for an early return
32948
- * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
32949
- */
32950
- const RightAngleSquare = ({
32951
- start: [x1, y1],
32952
- vertex: [x2, y2],
32953
- end: [x3, y3],
32954
- className
32955
- }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
32956
- d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
32957
- strokeWidth: 0.02,
32958
- fill: "none",
32959
- className: className
32960
- }));
32961
-
32962
- // We're conditionally adding the class name here so that we can style the arc differently
32963
- // based on whether it's an angle or a polygon angle
32964
- const Arc = ({
32965
- arc,
32966
- className
32967
- }) => {
32968
- return /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
32969
- d: arc,
32970
- strokeWidth: 0.02,
32971
- fill: "none",
32972
- className: className
32973
- }));
32974
- };
32975
- const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
32976
- const isRightAngle = angle => Math.round(angle) === 90;
32977
-
32978
- /**
32979
- * Determines if an angle is an inside (false) or outside (true) angle.
32980
- * They way, we know to flip the `largeArc` and `sweepArc` flags.
32981
- * Uses the priciple that a ray from a point inside a polygon will intersect
32982
- * with an odd number of lines, while a ray from a point outside the polygon
32983
- * will intersect with an even number of lines.
32984
- * https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon
32985
- */
32986
- const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
32987
- // Create a ray from the midpoint (inside angle) to the edge of the range
32988
- const rangeIntersectionPoint = getIntersectionOfRayWithBox(midpoint, vertex, range);
32989
- let lineIntersectionCount = 0;
32990
- polygonLines.forEach(line => segmentsIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
32991
-
32992
- // If the number of intersections is even, the angle is inside the polygon
32993
- return !isEven(lineIntersectionCount);
32994
- };
32995
- const isEven = n => n % 2 === 0;
32996
-
32997
- // This function calculates the bisector point of an angle formed by two points
32998
- // and a vertex. It is used to position the angle label so that it is always
32999
- // outside the arc at the exact midpoint of the angle.
33000
- // Reflex angles make use of polar coordinates to determine the correct direction
33001
- function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
33002
- const [originX, originY] = vertex;
33003
- const [x1, y1] = point1;
33004
- const [x2, y2] = point2;
33005
-
33006
- // Convert the Cartesian Coordinates to vectors
33007
- const vectorA = [x1 - originX, y1 - originY];
33008
- const vectorB = [x2 - originX, y2 - originY];
33009
-
33010
- // Calculate the average angle of the vectors
33011
- // so that we can point the bisector in the correct direction using polar coordinates
33012
- const angleA = Math.atan2(vectorA[1], vectorA[0]);
33013
- const angleB = Math.atan2(vectorB[1], vectorB[0]);
33014
-
33015
- // Calculate the average of these two angles
33016
- let averageAngle = (angleA + angleB) / 2;
33017
- const angleRadians = Math.abs(angleA - angleB);
33018
-
33019
- // Adjust the average angle to point in the correct direction
33020
- // and to account for reflex angles and a bug with 180 degrees
33021
- // that occurs due to our angleOffsetDegrees option in the graph
33022
- if (allowReflex) {
33023
- // If the resulting angles are less than 180 degrees,
33024
- // or if the user has flipped the points on the interactive element
33025
- // ensure that we have the correct direction for the reflex angle
33026
- if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
33027
- averageAngle += Math.PI; // Add 180 degrees
33526
+ }, [props.graphState.focusedPointIndex, itemsRef]);
33527
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
33528
+ style: {
33529
+ fill: "rgba(0,0,0,0)",
33530
+ cursor: "crosshair"
33531
+ },
33532
+ width: widthPx,
33533
+ height: heightPx,
33534
+ x: left,
33535
+ y: top,
33536
+ onClick: event => {
33537
+ const elementRect = event.currentTarget.getBoundingClientRect();
33538
+ const x = event.clientX - elementRect.x;
33539
+ const y = event.clientY - elementRect.y;
33540
+ const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
33541
+ dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
33028
33542
  }
33029
- } else {
33030
- // If the resulting angles are greater than 180 degrees, adjust
33031
- // to get the correct angle, as we already know that the angle is not reflexive
33032
- // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
33033
- if (angleRadians > Math.PI) {
33034
- averageAngle -= Math.PI; // Subract 180 degrees
33543
+ }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33544
+ key: i,
33545
+ point: point,
33546
+ onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
33547
+ ref: ref => {
33548
+ itemsRef.current[i] = ref;
33549
+ },
33550
+ onFocus: () => {
33551
+ dispatch(actions.pointGraph.focusPoint(i));
33552
+ },
33553
+ onClick: () => {
33554
+ dispatch(actions.pointGraph.clickPoint(i));
33035
33555
  }
33036
- }
33037
-
33038
- // Convert the average angle back to Cartesian coordinates
33039
- const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
33040
-
33041
- // Calculate the magnitude of the sum to normalize it
33042
- const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
33043
- const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
33044
-
33045
- // Calculate the initial distance of the bisector direction from the origin
33046
- const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
33047
-
33048
- // Determine the minimum radius to ensure that the text is always outside the arc
33049
- const requiredDistance = arcRadius * 1.75;
33050
- let radius = requiredDistance / initialDistance;
33051
-
33052
- // Apply radius only if the initial distance is less than required
33053
- if (initialDistance >= requiredDistance) {
33054
- radius = 1; // Use the unit vector as is
33055
- }
33056
-
33057
- // Scale the bisector direction by the radius
33058
- const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
33059
-
33060
- // Add the vertex to the bisector point to get the final position
33061
- // to ensure that the angle label moves with the interactive element
33062
- const scaledBisector = vec.add(bisectorPoint, vertex);
33063
- return scaledBisector;
33556
+ })));
33557
+ }
33558
+ function PointGraphDescription({
33559
+ state
33560
+ }) {
33561
+ // PointGraphDescription needs to `usePerseusI18n`, so it has to be a
33562
+ // component rather than a function that simply returns a string.
33563
+ const i18n = usePerseusI18n();
33564
+ return describePointGraph(state, i18n);
33064
33565
  }
33065
33566
 
33066
- const MovablePoint$1 = /*#__PURE__*/React.forwardRef((props, pointRef) => {
33567
+ // Exported for testing
33568
+ function describePointGraph(state, i18n) {
33067
33569
  const {
33068
- visiblePoint,
33069
- focusableHandle
33070
- } = useControlPoint(_extends({}, props, {
33071
- forwardedRef: pointRef
33570
+ strings,
33571
+ locale
33572
+ } = i18n;
33573
+ if (state.coords.length === 0) {
33574
+ return strings.srNoInteractiveElements;
33575
+ }
33576
+ const pointDescriptions = state.coords.map(([x, y]) => strings.srPointAtCoordinates({
33577
+ x: srFormatNumber(x, locale),
33578
+ y: srFormatNumber(y, locale)
33072
33579
  }));
33073
- return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle, visiblePoint);
33074
- });
33580
+ return strings.srInteractiveElements({
33581
+ elements: pointDescriptions.join(", ")
33582
+ });
33583
+ }
33075
33584
 
33585
+ function renderPolygonGraph(state, dispatch) {
33586
+ return {
33587
+ graph: /*#__PURE__*/React.createElement(PolygonGraph, {
33588
+ graphState: state,
33589
+ dispatch: dispatch
33590
+ }),
33591
+ interactiveElementsDescription: null
33592
+ };
33593
+ }
33076
33594
  const LimitedPolygonGraph = props => {
33077
33595
  const [hovered, setHovered] = React.useState(false);
33078
33596
  // This is more so required for the re-rendering that occurs when state
@@ -33356,189 +33874,15 @@ const PolygonGraph = props => {
33356
33874
  return numSides === "unlimited" ? UnlimitedPolygonGraph(props) : LimitedPolygonGraph(props);
33357
33875
  };
33358
33876
 
33359
- function getGradableGraph(state, initialGraph) {
33360
- if (!state.hasBeenInteractedWith) {
33361
- return _extends({}, initialGraph);
33362
- }
33363
- if (initialGraph.type === "linear-system" && state.type === "linear-system") {
33364
- return _extends({}, initialGraph, {
33365
- coords: state.coords
33366
- });
33367
- }
33368
- if (state.type === "segment" && initialGraph.type === "segment") {
33369
- return _extends({}, initialGraph, {
33370
- coords: state.coords
33371
- });
33372
- }
33373
- if (state.type === "linear" && initialGraph.type === "linear") {
33374
- return _extends({}, initialGraph, {
33375
- coords: state.coords
33376
- });
33377
- }
33378
- if (state.type === "ray" && initialGraph.type === "ray") {
33379
- return _extends({}, initialGraph, {
33380
- coords: state.coords
33381
- });
33382
- }
33383
- if (state.type === "polygon" && initialGraph.type === "polygon") {
33384
- return _extends({}, initialGraph, {
33385
- coords: state.coords
33386
- });
33387
- }
33388
- if (state.type === "point" && initialGraph.type === "point") {
33389
- return _extends({}, initialGraph, {
33390
- coords: state.coords
33391
- });
33392
- }
33393
- if (state.type === "circle" && initialGraph.type === "circle") {
33394
- return _extends({}, initialGraph, {
33395
- center: state.center,
33396
- radius: getRadius(state)
33397
- });
33398
- }
33399
- if (state.type === "quadratic" && initialGraph.type === "quadratic") {
33400
- return _extends({}, initialGraph, {
33401
- coords: state.coords
33402
- });
33403
- }
33404
- if (state.type === "sinusoid" && initialGraph.type === "sinusoid") {
33405
- return _extends({}, initialGraph, {
33406
- coords: state.coords
33407
- });
33408
- }
33409
- if (state.type === "angle" && initialGraph.type === "angle") {
33410
- // We're going to reverse the coords for scoring if the angle is clockwise and
33411
- // we don't allow reflex angles. This is because the angle is largely defined
33412
- // by the order of the points in the coords array, and we want to maintain
33413
- // the same angle scoring with the legacy graph (which had a bug).
33414
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
33415
- const areClockwise = clockwise$1([state.coords[0], state.coords[2], state.coords[1]]);
33416
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
33417
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
33418
- return _extends({}, initialGraph, {
33419
- coords,
33420
- allowReflexAngles: state.allowReflexAngles
33421
- });
33422
- }
33423
- if (state.type === "none" && initialGraph.type === "none") {
33424
- return {
33425
- type: "none"
33426
- };
33427
- }
33428
- throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
33429
- }
33430
-
33431
- /**
33432
- * determine radius of a circle graph
33433
- *
33434
- * @param graph - the graph object containing the circle
33435
- * @returns the radius of the circle
33436
- */
33437
- function getRadius(graph) {
33438
- const [centerX, centerY] = graph.center;
33439
- const [edgeX, edgeY] = graph.radiusPoint;
33440
- return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
33441
- }
33442
-
33443
- function CircleGraph(props) {
33444
- const {
33445
- dispatch,
33446
- graphState
33447
- } = props;
33448
- const {
33449
- center,
33450
- radiusPoint
33451
- } = graphState;
33452
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MovableCircle, {
33453
- center: center,
33454
- radius: getRadius(graphState),
33455
- onMove: c => dispatch(actions.circle.moveCenter(c))
33456
- }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33457
- point: radiusPoint,
33458
- cursor: "ew-resize",
33459
- onMove: newRadiusPoint => {
33460
- dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
33461
- }
33462
- }));
33463
- }
33464
- function MovableCircle(props) {
33465
- const {
33466
- center,
33467
- radius,
33468
- onMove
33469
- } = props;
33470
- const {
33471
- snapStep,
33472
- disableKeyboardInteraction
33473
- } = useGraphConfig();
33474
- const draggableRef = useRef(null);
33475
- const {
33476
- dragging
33477
- } = useDraggable({
33478
- gestureTarget: draggableRef,
33479
- point: center,
33480
- onMove,
33481
- constrainKeyboardMovement: p => snap(snapStep, p)
33482
- });
33483
- const [centerPx] = useTransformVectorsToPixels(center);
33484
- const [radiiPx] = useTransformDimensionsToPixels([radius, radius]);
33485
- return /*#__PURE__*/React.createElement("g", {
33486
- ref: draggableRef,
33487
- tabIndex: disableKeyboardInteraction ? -1 : 0,
33488
- className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
33489
- }, /*#__PURE__*/React.createElement("ellipse", {
33490
- className: "focus-ring",
33491
- cx: centerPx[X],
33492
- cy: centerPx[Y],
33493
- rx: radiiPx[X] + 3,
33494
- ry: radiiPx[Y] + 3
33495
- }), /*#__PURE__*/React.createElement("ellipse", {
33496
- className: "circle",
33497
- cx: centerPx[X],
33498
- cy: centerPx[Y],
33499
- rx: radiiPx[X],
33500
- ry: radiiPx[Y]
33501
- }), /*#__PURE__*/React.createElement(DragHandle, {
33502
- center: center
33503
- }));
33504
- }
33505
- const dragHandleDimensions = [24, 14];
33506
- const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
33507
- function DragHandle(props) {
33508
- const {
33509
- center
33510
- } = props;
33511
- const cornerRadius = Math.min(...dragHandleDimensions) / 2;
33512
- const [centerPx] = useTransformVectorsToPixels(center);
33513
- const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
33514
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
33515
- className: "movable-circle-handle",
33516
- x: topLeft[X],
33517
- y: topLeft[Y],
33518
- width: dragHandleDimensions[X],
33519
- height: dragHandleDimensions[Y],
33520
- rx: cornerRadius,
33521
- ry: cornerRadius
33522
- }), dragHandleDotPositions.map(offsetPx => {
33523
- const [xPx, yPx] = vec.add(offsetPx, centerPx);
33524
- return /*#__PURE__*/React.createElement("circle", {
33525
- key: `circle-${xPx}-${yPx}`,
33526
- className: "movable-circle-handle-dot",
33527
- cx: xPx,
33528
- cy: yPx
33529
- });
33530
- }));
33531
- }
33532
- function crossProduct(as, bs) {
33533
- const result = [];
33534
- for (const a of as) {
33535
- for (const b of bs) {
33536
- result.push([a, b]);
33537
- }
33538
- }
33539
- return result;
33877
+ function renderQuadraticGraph(state, dispatch) {
33878
+ return {
33879
+ graph: /*#__PURE__*/React.createElement(QuadraticGraph, {
33880
+ graphState: state,
33881
+ dispatch: dispatch
33882
+ }),
33883
+ interactiveElementsDescription: null
33884
+ };
33540
33885
  }
33541
-
33542
33886
  function QuadraticGraph(props) {
33543
33887
  const {
33544
33888
  dispatch,
@@ -33593,6 +33937,74 @@ const getQuadraticCoefficients$1 = coords => {
33593
33937
  return [a, b, c];
33594
33938
  };
33595
33939
 
33940
+ function renderRayGraph(state, dispatch) {
33941
+ return {
33942
+ graph: /*#__PURE__*/React.createElement(RayGraph, {
33943
+ graphState: state,
33944
+ dispatch: dispatch
33945
+ }),
33946
+ interactiveElementsDescription: null
33947
+ };
33948
+ }
33949
+ const RayGraph = props => {
33950
+ const {
33951
+ dispatch
33952
+ } = props;
33953
+ const {
33954
+ coords: line
33955
+ } = props.graphState;
33956
+ const handleMoveLine = delta => dispatch(actions.ray.moveRay(delta));
33957
+ const handleMovePoint = (pointIndex, newPoint) => dispatch(actions.ray.movePoint(pointIndex, newPoint));
33958
+
33959
+ // Ray graphs only have one line
33960
+ return /*#__PURE__*/React.createElement(MovableLine, {
33961
+ points: line,
33962
+ onMoveLine: handleMoveLine,
33963
+ onMovePoint: handleMovePoint,
33964
+ extend: {
33965
+ start: false,
33966
+ end: true
33967
+ }
33968
+ });
33969
+ };
33970
+
33971
+ function renderSegmentGraph(state, dispatch) {
33972
+ return {
33973
+ graph: /*#__PURE__*/React.createElement(SegmentGraph, {
33974
+ graphState: state,
33975
+ dispatch: dispatch
33976
+ }),
33977
+ interactiveElementsDescription: null
33978
+ };
33979
+ }
33980
+ const SegmentGraph = props => {
33981
+ const {
33982
+ dispatch
33983
+ } = props;
33984
+ const {
33985
+ coords: segments
33986
+ } = props.graphState;
33987
+ return /*#__PURE__*/React.createElement(React.Fragment, null, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement(MovableLine, {
33988
+ key: i,
33989
+ points: segment,
33990
+ onMoveLine: delta => {
33991
+ dispatch(actions.segment.moveLine(i, delta));
33992
+ },
33993
+ onMovePoint: (endpointIndex, destination) => {
33994
+ dispatch(actions.segment.movePointInFigure(i, endpointIndex, destination));
33995
+ }
33996
+ })));
33997
+ };
33998
+
33999
+ function renderSinusoidGraph(state, dispatch) {
34000
+ return {
34001
+ graph: /*#__PURE__*/React.createElement(SinusoidGraph, {
34002
+ graphState: state,
34003
+ dispatch: dispatch
34004
+ }),
34005
+ interactiveElementsDescription: null
34006
+ };
34007
+ }
33596
34008
  function SinusoidGraph(props) {
33597
34009
  const {
33598
34010
  dispatch,
@@ -33668,187 +34080,6 @@ const getSinusoidCoefficients$1 = coords => {
33668
34080
  };
33669
34081
  };
33670
34082
 
33671
- function AngleGraph(props) {
33672
- const {
33673
- dispatch,
33674
- graphState
33675
- } = props;
33676
- const {
33677
- graphDimensionsInPixels
33678
- } = useGraphConfig();
33679
- const {
33680
- coords,
33681
- showAngles,
33682
- range,
33683
- allowReflexAngles,
33684
- snapDegrees
33685
- } = graphState;
33686
-
33687
- // Break the coords into the two end points and the center point
33688
- const endPoints = [coords[0], coords[2]];
33689
- const centerPoint = coords[1];
33690
-
33691
- // Convert the vectors to pixels for rendering the svg lines
33692
- const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
33693
- const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
33694
-
33695
- // Create the SVG lines for the angle
33696
- const svgLines = linePixelCoords.map(([startPtPx, endPtPx], i) => {
33697
- const trimmedRange = trimRange(range, graphDimensionsInPixels);
33698
- const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
33699
- return /*#__PURE__*/React.createElement("g", {
33700
- key: `line-${i}`
33701
- }, /*#__PURE__*/React.createElement(SVGLine, {
33702
- start: startPtPx,
33703
- end: endPtPx,
33704
- style: {
33705
- stroke: "var(--movable-line-stroke-color)",
33706
- strokeWidth: "var(--movable-line-stroke-weight)"
33707
- }
33708
- }), /*#__PURE__*/React.createElement(Vector, {
33709
- tail: angleLines[i][1],
33710
- tip: endExtend,
33711
- color: "var(--movable-line-stroke-color)"
33712
- }));
33713
- });
33714
-
33715
- // Create the angle indicator parameters
33716
- const angleParams = {
33717
- vertex: centerPoint,
33718
- coords: endPoints,
33719
- allowReflexAngles: allowReflexAngles || false,
33720
- // Whether to allow reflex angles or not
33721
- snapDegrees: snapDegrees || 1,
33722
- // The multiple of degrees to snap to
33723
- range: range,
33724
- showAngles: showAngles || false // Whether to show the angle or not
33725
- };
33726
-
33727
- // Render the lines, angle, and then movable points
33728
- return /*#__PURE__*/React.createElement(React.Fragment, null, svgLines, /*#__PURE__*/React.createElement(Angle, angleParams), /*#__PURE__*/React.createElement(MovablePoint$1, {
33729
- point: coords[1],
33730
- constrain: p => p,
33731
- onMove: destination => dispatch(actions.angle.movePoint(1, destination))
33732
- }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33733
- point: coords[0],
33734
- constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
33735
- onMove: destination => dispatch(actions.angle.movePoint(0, destination))
33736
- }), /*#__PURE__*/React.createElement(MovablePoint$1, {
33737
- point: coords[2],
33738
- constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
33739
- onMove: destination => dispatch(actions.angle.movePoint(2, destination))
33740
- }));
33741
- }
33742
- const positiveX = [1, 0];
33743
- const negativeX = [-1, 0];
33744
- const positiveY = [0, 1];
33745
- const negativeY = [0, -1];
33746
- function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
33747
- var _findIntersectionOfRa, _findIntersectionOfRa2, _findIntersectionOfRa3, _findIntersectionOfRa4;
33748
- const currentAngle = calculateAngleInDegrees(vec.sub(sidePoint, vertex));
33749
-
33750
- // Find the rays that start at the current point and point up, down, left
33751
- // and right.
33752
- const leftRay = [sidePoint, vec.add(sidePoint, negativeX)];
33753
- const rightRay = [sidePoint, vec.add(sidePoint, positiveX)];
33754
- const upRay = [sidePoint, vec.add(sidePoint, positiveY)];
33755
- const downRay = [sidePoint, vec.add(sidePoint, negativeY)];
33756
-
33757
- // find the angles that lie one snap step clockwise and counter-clockwise
33758
- // from the current angle. These are the angles to which the side can be
33759
- // moved.
33760
- const oneStepCounterClockwise = currentAngle + snapDegrees;
33761
- const oneStepClockwise = currentAngle - snapDegrees;
33762
-
33763
- // find the rays that start from the vertex and point in the direction of
33764
- // the angles we just computed.
33765
- const counterClockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepCounterClockwise))];
33766
- const clockwiseRay = [vertex, vec.add(vertex, polar(1, oneStepClockwise))];
33767
-
33768
- // find the intersections of those rays with the horizontal and vertical
33769
- // rays extending from the sidePoint. These intersections are the points to
33770
- // which sidePoint can move that will result in a rotation of `snapDegrees`.
33771
- const left = (_findIntersectionOfRa = findIntersectionOfRays(leftRay, counterClockwiseRay)) != null ? _findIntersectionOfRa : findIntersectionOfRays(leftRay, clockwiseRay);
33772
- const right = (_findIntersectionOfRa2 = findIntersectionOfRays(rightRay, counterClockwiseRay)) != null ? _findIntersectionOfRa2 : findIntersectionOfRays(rightRay, clockwiseRay);
33773
- const up = (_findIntersectionOfRa3 = findIntersectionOfRays(upRay, counterClockwiseRay)) != null ? _findIntersectionOfRa3 : findIntersectionOfRays(upRay, clockwiseRay);
33774
- const down = (_findIntersectionOfRa4 = findIntersectionOfRays(downRay, counterClockwiseRay)) != null ? _findIntersectionOfRa4 : findIntersectionOfRays(downRay, clockwiseRay);
33775
- return {
33776
- up: up != null ? up : sidePoint,
33777
- down: down != null ? down : sidePoint,
33778
- left: left != null ? left : sidePoint,
33779
- right: right != null ? right : sidePoint
33780
- };
33781
- }
33782
-
33783
- function LimitedPointGraph(props) {
33784
- const {
33785
- dispatch
33786
- } = props;
33787
- return /*#__PURE__*/React.createElement(React.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33788
- key: i,
33789
- point: point,
33790
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination))
33791
- })));
33792
- }
33793
- function UnlimitedPointGraph(props) {
33794
- const {
33795
- dispatch
33796
- } = props;
33797
- const graphConfig = useGraphConfig();
33798
- const {
33799
- range: [[minX, maxX], [minY, maxY]]
33800
- } = graphConfig;
33801
- const width = maxX - minX;
33802
- const height = maxY - minY;
33803
- const [[widthPx, heightPx]] = useTransformDimensionsToPixels([width, height]);
33804
- const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
33805
- const itemsRef = React.useRef([]);
33806
- React.useEffect(() => {
33807
- const focusedIndex = props.graphState.focusedPointIndex;
33808
- if (focusedIndex != null) {
33809
- var _itemsRef$current$foc;
33810
- (_itemsRef$current$foc = itemsRef.current[focusedIndex]) == null || _itemsRef$current$foc.focus();
33811
- }
33812
- }, [props.graphState.focusedPointIndex, itemsRef]);
33813
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
33814
- style: {
33815
- fill: "rgba(0,0,0,0)",
33816
- cursor: "crosshair"
33817
- },
33818
- width: widthPx,
33819
- height: heightPx,
33820
- x: left,
33821
- y: top,
33822
- onClick: event => {
33823
- const elementRect = event.currentTarget.getBoundingClientRect();
33824
- const x = event.clientX - elementRect.x;
33825
- const y = event.clientY - elementRect.y;
33826
- const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
33827
- dispatch(actions.pointGraph.addPoint(graphCoordinates[0]));
33828
- }
33829
- }), props.graphState.coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33830
- key: i,
33831
- point: point,
33832
- onMove: destination => dispatch(actions.pointGraph.movePoint(i, destination)),
33833
- ref: ref => {
33834
- itemsRef.current[i] = ref;
33835
- },
33836
- onFocus: () => {
33837
- dispatch(actions.pointGraph.focusPoint(i));
33838
- },
33839
- onClick: () => {
33840
- dispatch(actions.pointGraph.clickPoint(i));
33841
- }
33842
- })));
33843
- }
33844
- function PointGraph(props) {
33845
- const numPoints = props.graphState.numPoints;
33846
- if (numPoints === "unlimited") {
33847
- return UnlimitedPointGraph(props);
33848
- }
33849
- return LimitedPointGraph(props);
33850
- }
33851
-
33852
34083
  const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
33853
34084
 
33854
34085
  // The vector from the center of the protractor to the top left corner of the
@@ -34013,6 +34244,7 @@ const MafsGraph = props => {
34013
34244
  const tickStep = props.step;
34014
34245
  const uniqueId = React.useId();
34015
34246
  const descriptionId = `interactive-graph-description-${uniqueId}`;
34247
+ const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
34016
34248
  const graphRef = React.useRef(null);
34017
34249
  const {
34018
34250
  analytics
@@ -34047,6 +34279,13 @@ const MafsGraph = props => {
34047
34279
  }
34048
34280
  });
34049
34281
  });
34282
+ const {
34283
+ graph,
34284
+ interactiveElementsDescription
34285
+ } = renderGraphElements({
34286
+ state,
34287
+ dispatch
34288
+ });
34050
34289
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
34051
34290
  value: {
34052
34291
  range: state.range,
@@ -34078,7 +34317,7 @@ const MafsGraph = props => {
34078
34317
  handleKeyboardEvent(event, state, dispatch);
34079
34318
  },
34080
34319
  "aria-label": fullGraphAriaLabel,
34081
- "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
34320
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId),
34082
34321
  ref: graphRef,
34083
34322
  tabIndex: 0,
34084
34323
  onFocus: event => {
@@ -34090,12 +34329,12 @@ const MafsGraph = props => {
34090
34329
  }, fullGraphAriaDescription && /*#__PURE__*/React.createElement(View, {
34091
34330
  id: descriptionId,
34092
34331
  tabIndex: -1,
34093
- style: {
34094
- width: 0,
34095
- height: 0,
34096
- overflow: "hidden"
34097
- }
34098
- }, fullGraphAriaDescription), /*#__PURE__*/React.createElement(LegacyGrid, {
34332
+ className: "mafs-sr-only"
34333
+ }, fullGraphAriaDescription), interactiveElementsDescription && /*#__PURE__*/React.createElement(View, {
34334
+ id: interactiveElementsDescriptionId,
34335
+ tabIndex: -1,
34336
+ className: "mafs-sr-only"
34337
+ }, interactiveElementsDescription), /*#__PURE__*/React.createElement(LegacyGrid, {
34099
34338
  box: props.box,
34100
34339
  backgroundImage: props.backgroundImage
34101
34340
  }), /*#__PURE__*/React.createElement(View, {
@@ -34146,10 +34385,7 @@ const MafsGraph = props => {
34146
34385
  zoom: false,
34147
34386
  width: width,
34148
34387
  height: height
34149
- }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
34150
- state,
34151
- dispatch
34152
- }))))), interactionPrompt && /*#__PURE__*/React.createElement(View, {
34388
+ }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), graph)))), interactionPrompt && /*#__PURE__*/React.createElement(View, {
34153
34389
  style: {
34154
34390
  textAlign: "center",
34155
34391
  backgroundColor: "white",
@@ -34382,7 +34618,7 @@ const calculateNestedSVGCoords = (range, width, height) => {
34382
34618
  viewboxY
34383
34619
  };
34384
34620
  };
34385
- const renderGraph = props => {
34621
+ const renderGraphElements = props => {
34386
34622
  const {
34387
34623
  state,
34388
34624
  dispatch
@@ -34392,62 +34628,41 @@ const renderGraph = props => {
34392
34628
  } = state;
34393
34629
  switch (type) {
34394
34630
  case "angle":
34395
- return /*#__PURE__*/React.createElement(AngleGraph, {
34396
- graphState: state,
34397
- dispatch: dispatch
34398
- });
34631
+ return renderAngleGraph(state, dispatch);
34399
34632
  case "segment":
34400
- return /*#__PURE__*/React.createElement(SegmentGraph, {
34401
- graphState: state,
34402
- dispatch: dispatch
34403
- });
34633
+ return renderSegmentGraph(state, dispatch);
34404
34634
  case "linear-system":
34405
- return /*#__PURE__*/React.createElement(LinearSystemGraph, {
34406
- graphState: state,
34407
- dispatch: dispatch
34408
- });
34635
+ return renderLinearSystemGraph(state, dispatch);
34409
34636
  case "linear":
34410
- return /*#__PURE__*/React.createElement(LinearGraph, {
34411
- graphState: state,
34412
- dispatch: dispatch
34413
- });
34637
+ return renderLinearGraph(state, dispatch);
34414
34638
  case "ray":
34415
- return /*#__PURE__*/React.createElement(RayGraph, {
34416
- graphState: state,
34417
- dispatch: dispatch
34418
- });
34639
+ return renderRayGraph(state, dispatch);
34419
34640
  case "polygon":
34420
- return /*#__PURE__*/React.createElement(PolygonGraph, {
34421
- graphState: state,
34422
- dispatch: dispatch
34423
- });
34641
+ return renderPolygonGraph(state, dispatch);
34424
34642
  case "point":
34425
- return /*#__PURE__*/React.createElement(PointGraph, {
34426
- graphState: state,
34427
- dispatch: dispatch
34428
- });
34643
+ return renderPointGraph(state, dispatch);
34429
34644
  case "circle":
34430
- return /*#__PURE__*/React.createElement(CircleGraph, {
34431
- graphState: state,
34432
- dispatch: dispatch
34433
- });
34645
+ return renderCircleGraph(state, dispatch);
34434
34646
  case "quadratic":
34435
- return /*#__PURE__*/React.createElement(QuadraticGraph, {
34436
- graphState: state,
34437
- dispatch: dispatch
34438
- });
34647
+ return renderQuadraticGraph(state, dispatch);
34439
34648
  case "sinusoid":
34440
- return /*#__PURE__*/React.createElement(SinusoidGraph, {
34441
- graphState: state,
34442
- dispatch: dispatch
34443
- });
34649
+ return renderSinusoidGraph(state, dispatch);
34444
34650
  case "none":
34445
- return null;
34651
+ return {
34652
+ graph: null,
34653
+ interactiveElementsDescription: null
34654
+ };
34446
34655
  default:
34447
34656
  throw new UnreachableCaseError(type);
34448
34657
  }
34449
34658
  };
34450
34659
 
34660
+ // Returns a space-separated string like "foo bar" given several optional
34661
+ // string IDs. If all args are falsy, returns undefined.
34662
+ function describedByIds(...args) {
34663
+ return args.filter(Boolean).join(" ") || undefined;
34664
+ }
34665
+
34451
34666
  // Converts the state of a StatefulMafsGraph back to the format used to
34452
34667
  // represent graph state in the widget JSON.
34453
34668
  //
@@ -43657,7 +43872,7 @@ const VideoTranscriptLink = props => {
43657
43872
  } = props;
43658
43873
  const {
43659
43874
  useVideo
43660
- } = getDependencies();
43875
+ } = useDependencies();
43661
43876
  const [id, kind] = IS_URL$1.test(location) ? [getYoutubeId(location), "YOUTUBE_ID"] : [location, "READABLE_ID"];
43662
43877
  // The result value conforms to the wonder-blocks-data `Result` type
43663
43878
  // which is used by our GraphQL framework.
@@ -43831,7 +44046,7 @@ const init = function init(options) {
43831
44046
 
43832
44047
  // This file is processed by a Rollup plugin (replace) to inject the production
43833
44048
  const libName = "@khanacademy/perseus";
43834
- const libVersion = "40.0.0";
44049
+ const libVersion = "41.1.0";
43835
44050
  addLibraryVersionToPerseusDebug(libName, libVersion);
43836
44051
 
43837
44052
  const apiVersion = {