@khanacademy/perseus 23.5.0 → 23.6.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.
package/dist/es/index.js CHANGED
@@ -20964,254 +20964,6 @@ const styles$B = {
20964
20964
  }
20965
20965
  };
20966
20966
 
20967
- /*
20968
- * In this file, an `expression` is some portion of valid TeX enclosed in
20969
- * curly brackets.
20970
- */
20971
-
20972
- /*
20973
- * Find the index at which an expression ends, i.e., has an unmatched
20974
- * closing curly bracket. This method assumes that we start with a non-open
20975
- * bracket character and end when we've seen more left than right brackets
20976
- * (rather than assuming that we start with a bracket character and wait for
20977
- * bracket equality).
20978
- */
20979
- function findEndpoint(tex, currentIndex) {
20980
- let bracketDepth = 0;
20981
- for (let i = currentIndex, len = tex.length; i < len; i++) {
20982
- const c = tex[i];
20983
- if (c === "{") {
20984
- bracketDepth++;
20985
- } else if (c === "}") {
20986
- bracketDepth--;
20987
- }
20988
- if (bracketDepth < 0) {
20989
- return i;
20990
- }
20991
- }
20992
- // If we never see unbalanced curly brackets, default to the
20993
- // entire string
20994
- return tex.length;
20995
- }
20996
-
20997
- /*
20998
- * Parses an individual set of curly brackets into TeX.
20999
- */
21000
- function parseNextExpression(tex, currentIndex, handler) {
21001
- // Find the first '{' and grab subsequent TeX
21002
- // Ex) tex: '{3}{7}', and we want the '3'
21003
- const openBracketIndex = tex.indexOf("{", currentIndex);
21004
- const nextExpIndex = openBracketIndex + 1;
21005
-
21006
- // Truncate to only contain remaining TeX
21007
- const endpoint = findEndpoint(tex, nextExpIndex);
21008
- const expressionTeX = tex.substring(nextExpIndex, endpoint);
21009
- const parsedExp = walkTex(expressionTeX, handler);
21010
- return {
21011
- endpoint: endpoint,
21012
- expression: parsedExp
21013
- };
21014
- }
21015
- function getNextFracIndex(tex, currentIndex) {
21016
- const dfrac = "\\dfrac";
21017
- const frac = "\\frac";
21018
- const nextFrac = tex.indexOf(frac, currentIndex);
21019
- const nextDFrac = tex.indexOf(dfrac, currentIndex);
21020
- if (nextFrac > -1 && nextDFrac > -1) {
21021
- return Math.min(nextFrac, nextDFrac);
21022
- }
21023
- if (nextFrac > -1) {
21024
- return nextFrac;
21025
- }
21026
- if (nextDFrac > -1) {
21027
- return nextDFrac;
21028
- }
21029
- return -1;
21030
- }
21031
- function walkTex(tex, handler) {
21032
- if (!tex) {
21033
- return "";
21034
- }
21035
-
21036
- // Ex) tex: '2 \dfrac {3}{7}'
21037
- let parsedString = "";
21038
- let currentIndex = 0;
21039
- let nextFrac = getNextFracIndex(tex, currentIndex);
21040
-
21041
- // For each \dfrac, find the two expressions (wrapped in {}) and recur
21042
- while (nextFrac > -1) {
21043
- // Gather first fragment, preceding \dfrac
21044
- // Ex) parsedString: '2 '
21045
- parsedString += tex.substring(currentIndex, nextFrac);
21046
-
21047
- // Remove everything preceding \dfrac, which has been parsed
21048
- currentIndex = nextFrac;
21049
-
21050
- // Parse first expression and move index past it
21051
- // Ex) firstParsedExpression.expression: '3'
21052
- const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21053
- currentIndex = firstParsedExpression.endpoint + 1;
21054
-
21055
- // Parse second expression
21056
- // Ex) secondParsedExpression.expression: '7'
21057
- const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21058
- currentIndex = secondParsedExpression.endpoint + 1;
21059
-
21060
- // Add expressions to running total of parsed expressions
21061
- if (parsedString.length) {
21062
- parsedString += " ";
21063
- }
21064
-
21065
- // Apply a custom handler based on the parsed subexpressions
21066
- parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21067
-
21068
- // Find next DFrac, relative to currentIndex
21069
- nextFrac = getNextFracIndex(tex, currentIndex);
21070
- }
21071
-
21072
- // Add remaining TeX, which is \dfrac-free
21073
- parsedString += tex.slice(currentIndex);
21074
- return parsedString;
21075
- }
21076
-
21077
- /*
21078
- * Modify a TeX expression, returning another TeX expression. The resulting
21079
- * expression will have its innermost fractions stubbed out with \fracs
21080
- * (as opposed to \dfracs). All other content will remain untouched.
21081
- */
21082
- function modifyTex(tex) {
21083
- function isNestedFraction(tex) {
21084
- return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21085
- }
21086
- const handler = function handler(exp1, exp2) {
21087
- let prefix;
21088
- if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21089
- prefix = "\\dfrac";
21090
- } else {
21091
- prefix = "\\frac";
21092
- }
21093
- return prefix + " {" + exp1 + "}{" + exp2 + "}";
21094
- };
21095
- return walkTex(tex, handler);
21096
- }
21097
-
21098
- /*
21099
- * Parse a TeX expression into something interpretable by input-number.
21100
- * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21101
- * (2) removing backslash-escaping from certain characters (right now, only
21102
- * percent signs).
21103
- *
21104
- * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21105
- * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21106
- * recursion is to allow for nested \dfrac elements.
21107
- *
21108
- * Backslash-escapes are removed with a simple search-and-replace.
21109
- */
21110
- function parseTex(tex) {
21111
- const handler = function handler(exp1, exp2) {
21112
- return exp1 + "/" + exp2;
21113
- };
21114
- const texWithoutFracs = walkTex(tex, handler);
21115
- return texWithoutFracs.replace("\\%", "%");
21116
- }
21117
- var TexWrangler = {
21118
- parseTex,
21119
- modifyTex
21120
- };
21121
-
21122
- /* eslint-disable react/sort-comp */
21123
- const ModifyTex = TexWrangler.modifyTex;
21124
- class MathOutput extends React.Component {
21125
- constructor(...args) {
21126
- super(...args);
21127
- this.state = {
21128
- focused: false,
21129
- selectorNamespace: _.uniqueId("math-output")
21130
- };
21131
- this._getInputClassName = () => {
21132
- let className = "math-output " + ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
21133
- if (this.state.focused) {
21134
- className += " " + ClassNames.FOCUSED;
21135
- }
21136
- if (this.props.className) {
21137
- className += " " + this.props.className;
21138
- }
21139
- return className;
21140
- };
21141
- this._getDisplayValue = value => {
21142
- // Cast from (potentially a) number to string
21143
- let displayText;
21144
- if (value != null) {
21145
- displayText = "" + value;
21146
- } else {
21147
- displayText = "";
21148
- }
21149
- return ModifyTex(displayText);
21150
- };
21151
- this.getValue = () => {
21152
- return this.props.value;
21153
- };
21154
- this.focus = () => {
21155
- if (!this.state.focused) {
21156
- this.props.onFocus();
21157
- this._bindBlurHandler();
21158
- this.setState({
21159
- focused: true
21160
- });
21161
- }
21162
- };
21163
- this.blur = () => {
21164
- if (this.state.focused) {
21165
- this.props.onBlur();
21166
- this._unbindBlurHandler();
21167
- this.setState({
21168
- focused: false
21169
- });
21170
- }
21171
- };
21172
- this._bindBlurHandler = () => {
21173
- $(document).bind("vclick." + this.state.selectorNamespace, e => {
21174
- // Detect whether the target has our React DOM node as a parent
21175
- const $closestWidget = $(e.target).closest(
21176
- // @ts-expect-error - TS2769 - No overload matches this call.
21177
- ReactDOM__default.findDOMNode(this));
21178
- if (!$closestWidget.length) {
21179
- this.blur();
21180
- }
21181
- });
21182
- };
21183
- this._unbindBlurHandler = () => {
21184
- $(document).unbind("." + this.state.selectorNamespace);
21185
- };
21186
- }
21187
- render() {
21188
- const divStyle = {
21189
- textAlign: "center"
21190
- };
21191
- const {
21192
- TeX
21193
- } = getDependencies();
21194
- return /*#__PURE__*/React.createElement("span", {
21195
- // eslint-disable-next-line react/no-string-refs
21196
- ref: "input",
21197
- className: this._getInputClassName(),
21198
- "aria-label": this.props.labelText,
21199
- onMouseDown: this.focus,
21200
- onTouchStart: this.focus
21201
- }, /*#__PURE__*/React.createElement("div", {
21202
- style: divStyle
21203
- }, /*#__PURE__*/React.createElement(TeX, null, this._getDisplayValue(this.props.value))));
21204
- }
21205
- componentWillUnmount() {
21206
- this._unbindBlurHandler();
21207
- }
21208
- }
21209
- MathOutput.defaultProps = {
21210
- value: "",
21211
- onFocus: function () {},
21212
- onBlur: function () {}
21213
- };
21214
-
21215
20967
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
21216
20968
  let lastId = 0;
21217
20969
  function uniqueIdForInput(prefix = "input-") {
@@ -21566,12 +21318,6 @@ class InputWithExamples extends React.Component {
21566
21318
  return `input-with-examples-${btoa(this.props.id).replace(/=/g, "")}`;
21567
21319
  };
21568
21320
  this._getInputClassName = () => {
21569
- // <MathOutput> is a special component that manages its own class and
21570
- // state, as it's a <span> that wants to act like an <input>.
21571
- if (this.props.type === "tex") {
21572
- return this.props.className;
21573
- }
21574
-
21575
21321
  // Otherwise, we need to add these INPUT and FOCUSED tags here.
21576
21322
  let className = ClassNames.INPUT + " " + ClassNames.INTERACTIVE;
21577
21323
  if (this.state.focused) {
@@ -21582,8 +21328,7 @@ class InputWithExamples extends React.Component {
21582
21328
  }
21583
21329
  return className;
21584
21330
  };
21585
- this._getPropsForInputType = () => {
21586
- // Minimal set of props, used by each input type
21331
+ this._renderInput = () => {
21587
21332
  const id = this._getUniqueId();
21588
21333
  const inputProps = {
21589
21334
  id: id,
@@ -21595,52 +21340,15 @@ class InputWithExamples extends React.Component {
21595
21340
  onFocus: this._handleFocus,
21596
21341
  onBlur: this._handleBlur,
21597
21342
  disabled: this.props.disabled,
21598
- style: this.props.style
21599
- };
21600
- if (this.props.type === "tex") {
21601
- return inputProps;
21602
- }
21603
-
21604
- // Add useful props required for MATH and TEXT modes
21605
- _.extend(inputProps, {
21343
+ style: this.props.style,
21606
21344
  onChange: this.props.onChange,
21607
- onTouchStart: captureScratchpadTouchStart$2
21608
- });
21609
-
21610
- // And add final props that are MATH- and TEXT-specific
21611
- if (this.props.type === "math") {
21612
- return _.extend({
21613
- buttonSet: this.props.buttonSet,
21614
- buttonsVisible: this.props.buttonsVisible,
21615
- convertDotToTimes: this.props.convertDotToTimes
21616
- }, inputProps);
21617
- }
21618
- if (this.props.type === "text") {
21619
- return _.extend({
21620
- autoCapitalize: "off",
21621
- autoComplete: "off",
21622
- autoCorrect: "off",
21623
- spellCheck: "false"
21624
- }, inputProps);
21625
- }
21626
- };
21627
- this._getComponentForInputType = () => {
21628
- switch (this.props.type) {
21629
- case "tex":
21630
- return MathOutput;
21631
- case "math":
21632
- return MathInput;
21633
- case "text":
21634
- return TextInput;
21635
- default:
21636
- this.props.type;
21637
- return null;
21638
- }
21639
- };
21640
- this._renderInput = () => {
21641
- const inputProps = this._getPropsForInputType();
21642
- const InputComponent = this._getComponentForInputType();
21643
- return /*#__PURE__*/React.createElement(InputComponent, inputProps);
21345
+ onTouchStart: captureScratchpadTouchStart$2,
21346
+ autoCapitalize: "off",
21347
+ autoComplete: "off",
21348
+ autoCorrect: "off",
21349
+ spellCheck: "false"
21350
+ };
21351
+ return /*#__PURE__*/React.createElement(TextInput, inputProps);
21644
21352
  };
21645
21353
  this._handleFocus = () => {
21646
21354
  this.props.onFocus();
@@ -21682,14 +21390,6 @@ class InputWithExamples extends React.Component {
21682
21390
  }
21683
21391
  render() {
21684
21392
  const input = this._renderInput();
21685
-
21686
- // Static rendering, which doesn't include the 'tooltip' logic that the
21687
- // other types require, and is hence handled separately.
21688
- if (this.props.type === "tex") {
21689
- return input;
21690
- }
21691
-
21692
- // Else, we need to be able to show examples
21693
21393
  const examplesContent = _.map(this.props.examples, example => {
21694
21394
  return "- " + example;
21695
21395
  }).join("\n");
@@ -21716,7 +21416,6 @@ class InputWithExamples extends React.Component {
21716
21416
  }
21717
21417
  InputWithExamples.contextType = PerseusI18nContext;
21718
21418
  InputWithExamples.defaultProps = {
21719
- type: "text",
21720
21419
  shouldShowExamples: true,
21721
21420
  onFocus: function () {},
21722
21421
  onBlur: function () {},
@@ -21805,6 +21504,161 @@ SimpleKeypadInput.propTypes = {
21805
21504
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
21806
21505
  };
21807
21506
 
21507
+ /*
21508
+ * In this file, an `expression` is some portion of valid TeX enclosed in
21509
+ * curly brackets.
21510
+ */
21511
+
21512
+ /*
21513
+ * Find the index at which an expression ends, i.e., has an unmatched
21514
+ * closing curly bracket. This method assumes that we start with a non-open
21515
+ * bracket character and end when we've seen more left than right brackets
21516
+ * (rather than assuming that we start with a bracket character and wait for
21517
+ * bracket equality).
21518
+ */
21519
+ function findEndpoint(tex, currentIndex) {
21520
+ let bracketDepth = 0;
21521
+ for (let i = currentIndex, len = tex.length; i < len; i++) {
21522
+ const c = tex[i];
21523
+ if (c === "{") {
21524
+ bracketDepth++;
21525
+ } else if (c === "}") {
21526
+ bracketDepth--;
21527
+ }
21528
+ if (bracketDepth < 0) {
21529
+ return i;
21530
+ }
21531
+ }
21532
+ // If we never see unbalanced curly brackets, default to the
21533
+ // entire string
21534
+ return tex.length;
21535
+ }
21536
+
21537
+ /*
21538
+ * Parses an individual set of curly brackets into TeX.
21539
+ */
21540
+ function parseNextExpression(tex, currentIndex, handler) {
21541
+ // Find the first '{' and grab subsequent TeX
21542
+ // Ex) tex: '{3}{7}', and we want the '3'
21543
+ const openBracketIndex = tex.indexOf("{", currentIndex);
21544
+ const nextExpIndex = openBracketIndex + 1;
21545
+
21546
+ // Truncate to only contain remaining TeX
21547
+ const endpoint = findEndpoint(tex, nextExpIndex);
21548
+ const expressionTeX = tex.substring(nextExpIndex, endpoint);
21549
+ const parsedExp = walkTex(expressionTeX, handler);
21550
+ return {
21551
+ endpoint: endpoint,
21552
+ expression: parsedExp
21553
+ };
21554
+ }
21555
+ function getNextFracIndex(tex, currentIndex) {
21556
+ const dfrac = "\\dfrac";
21557
+ const frac = "\\frac";
21558
+ const nextFrac = tex.indexOf(frac, currentIndex);
21559
+ const nextDFrac = tex.indexOf(dfrac, currentIndex);
21560
+ if (nextFrac > -1 && nextDFrac > -1) {
21561
+ return Math.min(nextFrac, nextDFrac);
21562
+ }
21563
+ if (nextFrac > -1) {
21564
+ return nextFrac;
21565
+ }
21566
+ if (nextDFrac > -1) {
21567
+ return nextDFrac;
21568
+ }
21569
+ return -1;
21570
+ }
21571
+ function walkTex(tex, handler) {
21572
+ if (!tex) {
21573
+ return "";
21574
+ }
21575
+
21576
+ // Ex) tex: '2 \dfrac {3}{7}'
21577
+ let parsedString = "";
21578
+ let currentIndex = 0;
21579
+ let nextFrac = getNextFracIndex(tex, currentIndex);
21580
+
21581
+ // For each \dfrac, find the two expressions (wrapped in {}) and recur
21582
+ while (nextFrac > -1) {
21583
+ // Gather first fragment, preceding \dfrac
21584
+ // Ex) parsedString: '2 '
21585
+ parsedString += tex.substring(currentIndex, nextFrac);
21586
+
21587
+ // Remove everything preceding \dfrac, which has been parsed
21588
+ currentIndex = nextFrac;
21589
+
21590
+ // Parse first expression and move index past it
21591
+ // Ex) firstParsedExpression.expression: '3'
21592
+ const firstParsedExpression = parseNextExpression(tex, currentIndex, handler);
21593
+ currentIndex = firstParsedExpression.endpoint + 1;
21594
+
21595
+ // Parse second expression
21596
+ // Ex) secondParsedExpression.expression: '7'
21597
+ const secondParsedExpression = parseNextExpression(tex, currentIndex, handler);
21598
+ currentIndex = secondParsedExpression.endpoint + 1;
21599
+
21600
+ // Add expressions to running total of parsed expressions
21601
+ if (parsedString.length) {
21602
+ parsedString += " ";
21603
+ }
21604
+
21605
+ // Apply a custom handler based on the parsed subexpressions
21606
+ parsedString += handler(firstParsedExpression.expression, secondParsedExpression.expression);
21607
+
21608
+ // Find next DFrac, relative to currentIndex
21609
+ nextFrac = getNextFracIndex(tex, currentIndex);
21610
+ }
21611
+
21612
+ // Add remaining TeX, which is \dfrac-free
21613
+ parsedString += tex.slice(currentIndex);
21614
+ return parsedString;
21615
+ }
21616
+
21617
+ /*
21618
+ * Modify a TeX expression, returning another TeX expression. The resulting
21619
+ * expression will have its innermost fractions stubbed out with \fracs
21620
+ * (as opposed to \dfracs). All other content will remain untouched.
21621
+ */
21622
+ function modifyTex(tex) {
21623
+ function isNestedFraction(tex) {
21624
+ return tex.indexOf("\\frac") > -1 || tex.indexOf("\\dfrac") > -1;
21625
+ }
21626
+ const handler = function handler(exp1, exp2) {
21627
+ let prefix;
21628
+ if (isNestedFraction(exp1) || isNestedFraction(exp2)) {
21629
+ prefix = "\\dfrac";
21630
+ } else {
21631
+ prefix = "\\frac";
21632
+ }
21633
+ return prefix + " {" + exp1 + "}{" + exp2 + "}";
21634
+ };
21635
+ return walkTex(tex, handler);
21636
+ }
21637
+
21638
+ /*
21639
+ * Parse a TeX expression into something interpretable by input-number.
21640
+ * The process is concerned with: (1) parsing fractions, i.e., \dfracs; and
21641
+ * (2) removing backslash-escaping from certain characters (right now, only
21642
+ * percent signs).
21643
+ *
21644
+ * The basic algorithm for handling \dfracs splits on \dfracs and then recurs
21645
+ * on the subsequent "expressions", i.e., the {} pairs that follow \dfrac. The
21646
+ * recursion is to allow for nested \dfrac elements.
21647
+ *
21648
+ * Backslash-escapes are removed with a simple search-and-replace.
21649
+ */
21650
+ function parseTex(tex) {
21651
+ const handler = function handler(exp1, exp2) {
21652
+ return exp1 + "/" + exp2;
21653
+ };
21654
+ const texWithoutFracs = walkTex(tex, handler);
21655
+ return texWithoutFracs.replace("\\%", "%");
21656
+ }
21657
+ var TexWrangler = {
21658
+ parseTex,
21659
+ modifyTex
21660
+ };
21661
+
21808
21662
  /* eslint-disable react/sort-comp */
21809
21663
  const ParseTex$1 = TexWrangler.parseTex;
21810
21664
  const answerTypes = {
@@ -21983,7 +21837,6 @@ class InputNumber extends React.Component {
21983
21837
  value: this.props.currentValue,
21984
21838
  onChange: this.handleChange,
21985
21839
  style: inputStyles,
21986
- type: "text",
21987
21840
  examples: this.examples(),
21988
21841
  shouldShowExamples: this.shouldShowExamples(),
21989
21842
  onFocus: this._handleFocus,
@@ -22392,7 +22245,6 @@ class NumericInput extends React.Component {
22392
22245
  value: this.props.currentValue,
22393
22246
  onChange: this.handleChange,
22394
22247
  labelText: labelText,
22395
- type: "text",
22396
22248
  examples: this.examples(),
22397
22249
  shouldShowExamples: this.shouldShowExamples(),
22398
22250
  onFocus: this._handleFocus,
@@ -29120,6 +28972,10 @@ function snap(snapStep, point) {
29120
28972
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29121
28973
  }
29122
28974
 
28975
+ function useDraggable(params) {
28976
+ return useMovable(params);
28977
+ }
28978
+
29123
28979
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
29124
28980
  // graph.
29125
28981
  const hitboxSizePx = 48;
@@ -29273,7 +29129,7 @@ function useControlPoint(point, color, onMovePoint) {
29273
29129
  } = useGraphConfig();
29274
29130
  const [focused, setFocused] = useState(false);
29275
29131
  const keyboardHandleRef = useRef(null);
29276
- useMovable({
29132
+ useDraggable({
29277
29133
  gestureTarget: keyboardHandleRef,
29278
29134
  point,
29279
29135
  onMove: onMovePoint,
@@ -29282,7 +29138,7 @@ function useControlPoint(point, color, onMovePoint) {
29282
29138
  const visiblePointRef = useRef(null);
29283
29139
  const {
29284
29140
  dragging
29285
- } = useMovable({
29141
+ } = useDraggable({
29286
29142
  gestureTarget: visiblePointRef,
29287
29143
  point,
29288
29144
  onMove: onMovePoint,
@@ -29336,7 +29192,7 @@ const Line$1 = props => {
29336
29192
  const line = useRef(null);
29337
29193
  const {
29338
29194
  dragging
29339
- } = useMovable({
29195
+ } = useDraggable({
29340
29196
  gestureTarget: line,
29341
29197
  point: start,
29342
29198
  onMove: newPoint => {
@@ -29676,7 +29532,7 @@ const StyledMovablePoint = props => {
29676
29532
  const snapToValue = snapTo != null ? snapTo : "grid";
29677
29533
  const {
29678
29534
  dragging
29679
- } = useMovable({
29535
+ } = useDraggable({
29680
29536
  gestureTarget: elementRef,
29681
29537
  point,
29682
29538
  onMove,
@@ -29718,7 +29574,7 @@ const PolygonGraph = props => {
29718
29574
  const snapToValue = snapTo != null ? snapTo : "grid";
29719
29575
  const {
29720
29576
  dragging
29721
- } = useMovable({
29577
+ } = useDraggable({
29722
29578
  gestureTarget: ref,
29723
29579
  point: dragReferencePoint,
29724
29580
  onMove: newPoint => {
@@ -29881,7 +29737,7 @@ function MovableCircle(props) {
29881
29737
  const draggableRef = useRef(null);
29882
29738
  const {
29883
29739
  dragging
29884
- } = useMovable({
29740
+ } = useDraggable({
29885
29741
  gestureTarget: draggableRef,
29886
29742
  point: center,
29887
29743
  onMove,
@@ -30012,10 +29868,24 @@ function SinusoidGraph(props) {
30012
29868
  coords
30013
29869
  } = graphState;
30014
29870
 
30015
- // Get the coefficients for calculating the quadratic equation
29871
+ // The coefficients are used to calculate the sinusoid equation, plot the graph, and to indicate
29872
+ // to content creators the currently selected "correct answer" in the Content Editor.
29873
+ // While we should technically never have invalid coordinates, we want to ensure that
29874
+ // we have a fallback so that the graph can still be plotted without crashing.
29875
+ const coeffRef = React.useRef({
29876
+ amplitude: 1,
29877
+ angularFrequency: 1,
29878
+ phase: 1,
29879
+ verticalOffset: 0
29880
+ });
30016
29881
  const coeffs = getSinusoidCoefficients(coords);
29882
+
29883
+ // If the coefficients are valid, update the reference
29884
+ if (coeffs !== undefined) {
29885
+ coeffRef.current = coeffs;
29886
+ }
30017
29887
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
30018
- y: x => computeSine(x, coeffs),
29888
+ y: x => computeSine(x, coeffRef.current),
30019
29889
  color: color.blue
30020
29890
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
30021
29891
  key: "point-" + i,
@@ -30042,6 +29912,11 @@ const getSinusoidCoefficients = coords => {
30042
29912
  const p1 = coords[0];
30043
29913
  const p2 = coords[1];
30044
29914
 
29915
+ // If the x-coordinates are the same, we are unable to calculate the coefficients
29916
+ if (p2[0] === p1[0]) {
29917
+ return;
29918
+ }
29919
+
30045
29920
  // Resulting coefficients are canonical for this sine curve
30046
29921
  const amplitude = p2[1] - p1[1];
30047
29922
  const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
@@ -31515,11 +31390,14 @@ function doMovePoint(state, action) {
31515
31390
  }
31516
31391
  case "sinusoid":
31517
31392
  {
31518
- // First, we need to verify that the new coordinates are not on the same vertical line
31519
- // If they are, we don't want to move the point
31393
+ // First, we need to make sure to bound the new coordinates to the graph range
31520
31394
  const destination = action.destination;
31395
+ const boundDestination = boundAndSnapToGrid(destination, state);
31396
+
31397
+ // Then, we need to verify that the new coordinates are not on the same
31398
+ // vertical line. If they are, then we don't want to move the point
31521
31399
  const _newCoords = [...state.coords];
31522
- _newCoords[action.index] = action.destination;
31400
+ _newCoords[action.index] = boundDestination;
31523
31401
  if (_newCoords[0][0] === _newCoords[1][0]) {
31524
31402
  return state;
31525
31403
  }
@@ -31528,7 +31406,7 @@ function doMovePoint(state, action) {
31528
31406
  coords: setAtIndex({
31529
31407
  array: state.coords,
31530
31408
  index: action.index,
31531
- newValue: boundAndSnapToGrid(destination, state)
31409
+ newValue: boundDestination
31532
31410
  })
31533
31411
  });
31534
31412
  }
@@ -31536,7 +31414,10 @@ function doMovePoint(state, action) {
31536
31414
  {
31537
31415
  // Set up the new coords and check if the quadratic coefficients are valid
31538
31416
  const _newCoords2 = [...state.coords];
31539
- _newCoords2[action.index] = action.destination;
31417
+
31418
+ // Bind the new destination to the graph range/snapStep and then get the quadratic coefficients
31419
+ const boundDestination = boundAndSnapToGrid(action.destination, state);
31420
+ _newCoords2[action.index] = boundDestination;
31540
31421
  const QuadraticCoefficients = getQuadraticCoefficients(_newCoords2);
31541
31422
 
31542
31423
  // If the new destination results in an invalid quadratic equation, we don't want to move the point
@@ -31548,7 +31429,7 @@ function doMovePoint(state, action) {
31548
31429
  coords: setAtIndex({
31549
31430
  array: state.coords,
31550
31431
  index: action.index,
31551
- newValue: boundAndSnapToGrid(action.destination, state)
31432
+ newValue: boundDestination
31552
31433
  })
31553
31434
  });
31554
31435
  }
@@ -33360,6 +33241,12 @@ class InteractiveGraph extends React.Component {
33360
33241
  }
33361
33242
  render() {
33362
33243
  var _this$props$apiOption;
33244
+ if (this.props.graph.type === "polygon" && this.props.graph.numSides === "unlimited") {
33245
+ return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
33246
+ ref: this.legacyGraphRef
33247
+ }, this.props));
33248
+ }
33249
+
33363
33250
  // Mafs shim
33364
33251
  if ((_this$props$apiOption = this.props.apiOptions) != null && (_this$props$apiOption = _this$props$apiOption.flags) != null && (_this$props$apiOption = _this$props$apiOption["mafs"]) != null && _this$props$apiOption[this.props.graph.type]) {
33365
33252
  const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
@@ -44152,7 +44039,7 @@ const init = function init(options) {
44152
44039
 
44153
44040
  // This file is processed by a Rollup plugin (replace) to inject the production
44154
44041
  const libName = "@khanacademy/perseus";
44155
- const libVersion = "23.5.0";
44042
+ const libVersion = "23.6.0";
44156
44043
  addLibraryVersionToPerseusDebug(libName, libVersion);
44157
44044
 
44158
44045
  const apiVersion = {