@khanacademy/perseus 53.0.0 → 53.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.
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ var Banner = require('@khanacademy/wonder-blocks-banner');
33
33
  var wonderBlocksDropdown = require('@khanacademy/wonder-blocks-dropdown');
34
34
  var caretDown = require('@phosphor-icons/core/regular/caret-down.svg');
35
35
  var caretUp = require('@phosphor-icons/core/regular/caret-up.svg');
36
- var debounce$1 = require('lodash.debounce');
36
+ require('lodash.debounce');
37
37
  var mafs = require('mafs');
38
38
  var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
39
39
  var questionIcon = require('@phosphor-icons/core/regular/question.svg');
@@ -82,7 +82,6 @@ var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
82
82
  var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
83
83
  var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
84
84
  var caretUp__default = /*#__PURE__*/_interopDefaultLegacy(caretUp);
85
- var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce$1);
86
85
  var questionIcon__default = /*#__PURE__*/_interopDefaultLegacy(questionIcon);
87
86
  var Switch__default = /*#__PURE__*/_interopDefaultLegacy(Switch);
88
87
  var Pill__default = /*#__PURE__*/_interopDefaultLegacy(Pill);
@@ -379,7 +378,7 @@ const mockStrings = {
379
378
  x,
380
379
  y
381
380
  } = _ref27;
382
- return `Point ${num} at ${x} comma ${y}`;
381
+ return `Point ${num} at ${x} comma ${y}.`;
383
382
  },
384
383
  srInteractiveElements: _ref28 => {
385
384
  let {
@@ -670,7 +669,50 @@ const mockStrings = {
670
669
  point3Y
671
670
  } = _ref61;
672
671
  return `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`;
673
- }
672
+ },
673
+ srPolygonGraph: "A polygon.",
674
+ srPolygonGraphCoordinatePlane: "A polygon on a coordinate plane.",
675
+ srPolygonGraphPointsNum: _ref62 => {
676
+ let {
677
+ num
678
+ } = _ref62;
679
+ return `The polygon has ${num} points.`;
680
+ },
681
+ srPolygonGraphPointsOne: "The polygon has 1 point.",
682
+ srPolygonElementsNum: _ref63 => {
683
+ let {
684
+ num
685
+ } = _ref63;
686
+ return `A polygon with ${num} points.`;
687
+ },
688
+ srPolygonElementsOne: "A polygon with 1 point.",
689
+ srPolygonPointAngleApprox: _ref64 => {
690
+ let {
691
+ angle
692
+ } = _ref64;
693
+ return `Angle approximately equal to ${angle} degrees.`;
694
+ },
695
+ srPolygonPointAngle: _ref65 => {
696
+ let {
697
+ angle
698
+ } = _ref65;
699
+ return `Angle equal to ${angle} degrees.`;
700
+ },
701
+ srPolygonSideLength: _ref66 => {
702
+ let {
703
+ pointNum,
704
+ length
705
+ } = _ref66;
706
+ return `A line segment, length equal to ${length} units, connects to point ${pointNum}.`;
707
+ },
708
+ srPolygonSideLengthApprox: _ref67 => {
709
+ let {
710
+ pointNum,
711
+ length
712
+ } = _ref67;
713
+ return `A line segment, length approximately equal to ${length} units, connects to point ${pointNum}.`;
714
+ },
715
+ srUnlimitedPolygonEmpty: "An empty coordinate plane."
674
716
  // The above strings are used for interactive graph SR descriptions.
675
717
  };
676
718
 
@@ -10059,7 +10101,7 @@ class WrappedLine extends WrappedDrawing {
10059
10101
 
10060
10102
  /* eslint-disable @babel/no-invalid-this */
10061
10103
  const {
10062
- clockwise: clockwise$3,
10104
+ clockwise: clockwise$2,
10063
10105
  reverseVector: reverseVector$1
10064
10106
  } = kmath.geometry;
10065
10107
  function scaledDistanceFromAngle(angle) {
@@ -11297,7 +11339,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11297
11339
  // Temporary objects
11298
11340
  ___default["default"].invoke(polygon.temp, "remove");
11299
11341
  polygon.temp = [];
11300
- const isClockwise = clockwise$3(polygon.coords);
11342
+ const isClockwise = clockwise$2(polygon.coords);
11301
11343
 
11302
11344
  // Update angle labels
11303
11345
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -14730,7 +14772,7 @@ function sum(array) {
14730
14772
  return memo + arg;
14731
14773
  }, 0);
14732
14774
  }
14733
- function clockwise$2(points) {
14775
+ function clockwise$1(points) {
14734
14776
  const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
14735
14777
  const areas = ___default["default"].map(segments, function (segment) {
14736
14778
  const p1 = segment[0];
@@ -14796,7 +14838,7 @@ const draw$1 = {
14796
14838
  const graphie = this.graphie;
14797
14839
  const self = this;
14798
14840
  const coords = ___default["default"].invoke(state.points, "coord");
14799
- const isClockwise = clockwise$2(coords);
14841
+ const isClockwise = clockwise$1(coords);
14800
14842
  const n = coords.length;
14801
14843
 
14802
14844
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -16542,9 +16584,28 @@ class Zoomable extends React__namespace.Component {
16542
16584
  if (!this._isMounted) {
16543
16585
  return;
16544
16586
  }
16587
+
16588
+ // When a zoomable item is contained within a collapsed parent (such as an Explanation widget),
16589
+ // the parent node has zero width, which causes the scale to be miscalculated.
16590
+ // Therefore, we check to see if the node has a valid width, and if it doesn't,
16591
+ // then we traverse up the DOM tree and look for the closest "perseus-renderer"
16592
+ // (which should have a width).
16593
+ // To be cautious, we also account for the possibility that we can't find any parents with a valid width
16594
+ // (not that they don't exist, just that we can't find them).
16595
+ let parentNode = this._node;
16596
+ let currentNode = parentNode;
16597
+ while (currentNode && currentNode.offsetWidth === 0) {
16598
+ // When traversing up the DOM tree, we need to start at the "parentElement"
16599
+ // because "closest()" can match the current element,
16600
+ // and if it does so, then we are stuck in an infinite loop.
16601
+ currentNode = currentNode.parentElement?.closest(".perseus-renderer") ?? null;
16602
+ }
16603
+ if (currentNode) {
16604
+ parentNode = currentNode;
16605
+ }
16545
16606
  const parentBounds = {
16546
- width: this._node.offsetWidth,
16547
- height: this._node.offsetHeight
16607
+ width: parentNode.offsetWidth,
16608
+ height: parentNode.offsetHeight
16548
16609
  };
16549
16610
  const childBounds = this.props.computeChildBounds(this._node, parentBounds);
16550
16611
 
@@ -23377,8 +23438,8 @@ const styles$s = aphrodite.StyleSheet.create({
23377
23438
  crossOutLine: {
23378
23439
  // Center the icon within the container.
23379
23440
  position: "absolute",
23380
- top: `0px`,
23381
- left: `0px`
23441
+ top: `2px`,
23442
+ left: `2px`
23382
23443
  }
23383
23444
  });
23384
23445
 
@@ -23405,43 +23466,6 @@ function ChoiceInner(props) {
23405
23466
  }
23406
23467
  return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
23407
23468
  }
23408
-
23409
- // Handle dynamic styling of the multiple choice icon. Most
23410
- // MC icon styles are constant, but we do allow the caller
23411
- // to specify the selected color, and thus must control styles
23412
- // related to the selected state dynamically.
23413
- function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
23414
- let backgroundColor;
23415
- let borderColor;
23416
- let color;
23417
- if (!showCorrectness && pressed) {
23418
- borderColor = wonderBlocksTokens.color.blue;
23419
- color = wonderBlocksTokens.color.blue;
23420
- backgroundColor = "transparent";
23421
- } else if (checked) {
23422
- const bg = showCorrectness && correct ? wonderBlocksTokens.color.green : wonderBlocksTokens.color.blue;
23423
- color = white;
23424
- backgroundColor = bg;
23425
- borderColor = bg;
23426
- } else {
23427
- borderColor = wonderBlocksTokens.color.offBlack64;
23428
- color = wonderBlocksTokens.color.offBlack64;
23429
- }
23430
-
23431
- // define shape
23432
- let borderRadius;
23433
- if (multipleSelect) {
23434
- borderRadius = 3;
23435
- } else {
23436
- borderRadius = CHOICE_ICON_SIZE;
23437
- }
23438
- return {
23439
- backgroundColor,
23440
- borderColor,
23441
- color,
23442
- borderRadius
23443
- };
23444
- }
23445
23469
  const ChoiceIcon = function (props) {
23446
23470
  const {
23447
23471
  checked,
@@ -23453,9 +23477,41 @@ const ChoiceIcon = function (props) {
23453
23477
  multipleSelect,
23454
23478
  pos,
23455
23479
  previouslyAnswered,
23480
+ // Used in "review mode"/"show rationale" to show that option was previously chosen
23456
23481
  pressed
23457
23482
  } = props;
23458
- const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
23483
+
23484
+ // Accounts for incorrect choice still displaying as learner tries again
23485
+ const choiceIsChecked = checked || showCorrectness && !correct && previouslyAnswered;
23486
+
23487
+ // Core styling
23488
+ const choiceStyling = [styles$r.choiceBase, multipleSelect ? styles$r.multiSelectShape : styles$r.singleSelectShape, showCorrectness ? styles$r.choiceHasIcon : styles$r.choiceHasLetter, choiceIsChecked ? styles$r.choiceIsChecked : styles$r.choiceIsUnchecked];
23489
+
23490
+ // Color styling
23491
+ // Handle dynamic styling of the multiple choice icon. Most
23492
+ // MC icon styles are constant, but we do allow the caller
23493
+ // to specify the selected color, and thus must control styles
23494
+ // related to the selected state dynamically.
23495
+ let crossOutColor;
23496
+ if (showCorrectness && correct && checked) {
23497
+ choiceStyling.push(styles$r.choiceCorrect);
23498
+ crossOutColor = wonderBlocksTokens.color.green;
23499
+ } else if (showCorrectness && !correct && (checked || previouslyAnswered)) {
23500
+ choiceStyling.push(styles$r.choiceIncorrect);
23501
+ crossOutColor = wonderBlocksTokens.color.red;
23502
+ } else if (checked) {
23503
+ // Show filled neutral blue color (showCorrectness is false)
23504
+ choiceStyling.push(styles$r.choiceNeutral);
23505
+ crossOutColor = wonderBlocksTokens.color.blue;
23506
+ } else if (pressed) {
23507
+ // Show outlined neutral blue color (showCorrectness is false)
23508
+ choiceStyling.push(styles$r.activeNeutral);
23509
+ crossOutColor = wonderBlocksTokens.color.blue;
23510
+ } else {
23511
+ // choice is not checked
23512
+ choiceStyling.push(styles$r.uncheckedColors);
23513
+ crossOutColor = wonderBlocksTokens.color.offBlack64;
23514
+ }
23459
23515
  return /*#__PURE__*/React__namespace.createElement("div", {
23460
23516
  className: aphrodite.css(styles$t.iconWrapper)
23461
23517
  }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
@@ -23463,9 +23519,8 @@ const ChoiceIcon = function (props) {
23463
23519
  visible: focused || hovered,
23464
23520
  multipleSelect: multipleSelect
23465
23521
  }, /*#__PURE__*/React__namespace.createElement("div", {
23466
- style: dynamicStyles,
23467
23522
  "data-testid": "choice-icon__library-choice-icon",
23468
- className: aphrodite.css(styles$r.circle, showCorrectness && correct && styles$r.circleCorrect, showCorrectness && !correct && styles$r.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$r.circleIncorrectAnswered)
23523
+ className: aphrodite.css(...choiceStyling)
23469
23524
  // used in BaseRadio editMode to check
23470
23525
  // if we actually clicked on the radio icon
23471
23526
  ,
@@ -23477,7 +23532,7 @@ const ChoiceIcon = function (props) {
23477
23532
  showCorrectness: showCorrectness,
23478
23533
  correct: correct
23479
23534
  })))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
23480
- color: dynamicStyles.borderColor
23535
+ color: crossOutColor
23481
23536
  }));
23482
23537
  };
23483
23538
  const styles$r = aphrodite.StyleSheet.create({
@@ -23489,8 +23544,7 @@ const styles$r = aphrodite.StyleSheet.create({
23489
23544
  alignItems: "center",
23490
23545
  justifyContent: "center"
23491
23546
  },
23492
- circle: {
23493
- // Make the circle
23547
+ choiceBase: {
23494
23548
  width: CHOICE_ICON_SIZE,
23495
23549
  height: CHOICE_ICON_SIZE,
23496
23550
  boxSizing: "border-box",
@@ -23502,30 +23556,48 @@ const styles$r = aphrodite.StyleSheet.create({
23502
23556
  // "bold font family" so that characters which fall back to the default
23503
23557
  // font get bolded too.
23504
23558
  fontWeight: "bold",
23505
- fontSize: 12,
23506
23559
  // Center the icon wrapper.
23507
23560
  display: "flex",
23508
23561
  alignItems: "center",
23509
- justifyContent: "center",
23510
- // HACK(emily): I don't know why adding this line height makes the text
23511
- // appear centered better than any other value, but it does. In
23512
- // particular, at large zoom levels this line height does almost
23513
- // nothing, but at the default size this shifts the letter down one
23514
- // pixel so it is much better centered.
23515
- lineHeight: "1px"
23516
- },
23517
- circleCorrect: {
23562
+ justifyContent: "center"
23563
+ },
23564
+ choiceHasLetter: {
23565
+ fontSize: 12
23566
+ },
23567
+ choiceHasIcon: {
23518
23568
  fontSize: CHOICE_ICON_SIZE
23519
23569
  },
23520
- circleIncorrect: {
23521
- fontSize: CHOICE_ICON_SIZE,
23522
- borderColor: gray68,
23523
- color: gray68
23570
+ choiceIsChecked: {
23571
+ color: wonderBlocksTokens.color.white
23572
+ },
23573
+ choiceIsUnchecked: {
23574
+ color: wonderBlocksTokens.color.offBlack64
23575
+ },
23576
+ choiceCorrect: {
23577
+ backgroundColor: wonderBlocksTokens.color.green,
23578
+ borderColor: wonderBlocksTokens.color.green
23524
23579
  },
23525
- circleIncorrectAnswered: {
23580
+ choiceIncorrect: {
23526
23581
  backgroundColor: wonderBlocksTokens.color.red,
23527
- borderColor: wonderBlocksTokens.color.red,
23528
- color: wonderBlocksTokens.color.white
23582
+ borderColor: wonderBlocksTokens.color.red
23583
+ },
23584
+ choiceNeutral: {
23585
+ backgroundColor: wonderBlocksTokens.color.blue,
23586
+ borderColor: wonderBlocksTokens.color.blue
23587
+ },
23588
+ activeNeutral: {
23589
+ color: wonderBlocksTokens.color.blue,
23590
+ borderColor: wonderBlocksTokens.color.blue,
23591
+ backgroundColor: "transparent"
23592
+ },
23593
+ multiSelectShape: {
23594
+ borderRadius: 3
23595
+ },
23596
+ singleSelectShape: {
23597
+ borderRadius: CHOICE_ICON_SIZE
23598
+ },
23599
+ uncheckedColors: {
23600
+ borderColor: wonderBlocksTokens.color.offBlack64
23529
23601
  }
23530
23602
  });
23531
23603
 
@@ -24673,7 +24745,8 @@ var Radio = {
24673
24745
  staticTransform: transform,
24674
24746
  version: perseusCore.radioLogic.version,
24675
24747
  propUpgrades: perseusCore.radioLogic.widgetOptionsUpgrades,
24676
- isLintable: true
24748
+ isLintable: true,
24749
+ getPublicWidgetOptions: perseusCore.getRadioPublicWidgetOptions
24677
24750
  };
24678
24751
 
24679
24752
  /* eslint-disable import/no-named-as-default */
@@ -26883,7 +26956,8 @@ var Grapher$1 = {
26883
26956
  hidden: true,
26884
26957
  widget: Grapher,
26885
26958
  transform: propTransform$2,
26886
- staticTransform: staticTransform$4
26959
+ staticTransform: staticTransform$4,
26960
+ getPublicWidgetOptions: perseusCore.getGrapherPublicWidgetOptions
26887
26961
  };
26888
26962
 
26889
26963
  const getPromptJSON$f = rendererJSON => {
@@ -27190,7 +27264,8 @@ var Iframe$1 = {
27190
27264
  displayName: "Iframe (deprecated)",
27191
27265
  widget: Iframe,
27192
27266
  // Let's not expose it to all content creators yet
27193
- hidden: true
27267
+ hidden: true,
27268
+ getPublicWidgetOptions: perseusCore.getIFramePublicWidgetOptions
27194
27269
  };
27195
27270
 
27196
27271
  const getPromptJSON$d = renderProps => {
@@ -29922,10 +29997,10 @@ function Vector(props) {
29922
29997
  }));
29923
29998
  }
29924
29999
 
29925
- function srFormatNumber(a, locale) {
30000
+ function srFormatNumber(a, locale, maximumFractionDigits) {
29926
30001
  // adding zero here converts negative zero to positive zero.
29927
30002
  return (0 + a).toLocaleString(locale, {
29928
- maximumFractionDigits: 3,
30003
+ maximumFractionDigits: maximumFractionDigits ?? 3,
29929
30004
  useGrouping: false // no thousands separators
29930
30005
  });
29931
30006
  }
@@ -30104,6 +30179,95 @@ a, b, c) {
30104
30179
  return [x1, x2];
30105
30180
  }
30106
30181
 
30182
+ /**
30183
+ * Given a list of points, return the angle made by the point at index i
30184
+ * with the points at indices i - 1 and i + 1.
30185
+ */
30186
+ function getAngleFromPoints(points, i) {
30187
+ // Make sure the index is valid.
30188
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30189
+ return null;
30190
+ }
30191
+
30192
+ // If there are fewer than 3 points, there is no angle.
30193
+ if (points.length < 3) {
30194
+ return null;
30195
+ }
30196
+
30197
+ // Get the points at the current index and the two adjacent points.
30198
+ const point = points.at(i);
30199
+ const pt1 = points.at(i - 1);
30200
+ const pt2 = points[(i + 1) % points.length];
30201
+ if (!point || !pt1 || !pt2) {
30202
+ return null;
30203
+ }
30204
+
30205
+ // Get the lengths of adjacent sides, making a triangle
30206
+ // we can use to calculate the angle.
30207
+ const a = mafs.vec.dist(point, pt1);
30208
+ const b = mafs.vec.dist(point, pt2);
30209
+ const c = mafs.vec.dist(pt1, pt2);
30210
+
30211
+ // Law of cosines
30212
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
30213
+ return angle;
30214
+ }
30215
+ function getSideLengthsFromPoints(points, i, isPolygonOpen) {
30216
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30217
+ return [];
30218
+ }
30219
+ if (points.length < 2) {
30220
+ return [];
30221
+ }
30222
+ const returnArray = [];
30223
+ const point = points[i];
30224
+ // If this point is the first point, then side 1 is the
30225
+ // last point in the list.
30226
+ const point1Index = i === 0 ? points.length - 1 : i - 1;
30227
+ const point1 = points[point1Index];
30228
+ // Make sure the previous point is not the same
30229
+ // as the current point.
30230
+ const side1 = i !== point1Index ? mafs.vec.dist(point, point1) : null;
30231
+ // The first side of the first point does not exist if
30232
+ // the polygon is open.
30233
+ if (side1 && !(isPolygonOpen && i === 0)) {
30234
+ returnArray.push({
30235
+ pointIndex: point1Index,
30236
+ sideLength: side1
30237
+ });
30238
+ }
30239
+
30240
+ // See if there is a side 2.
30241
+ const point2Index = (i + 1) % points.length;
30242
+ const point2 = points[point2Index];
30243
+ // Make sure that the next point is not the same as the
30244
+ // current point, and don't repeat the first point.
30245
+ const side2 = i !== point2Index && point2Index !== point1Index ? mafs.vec.dist(point, point2) : null;
30246
+ // The second side of the last point does not exist if
30247
+ // the polygon is open.
30248
+ if (side2 && !(isPolygonOpen && i === points.length - 1)) {
30249
+ returnArray.push({
30250
+ pointIndex: point2Index,
30251
+ sideLength: side2
30252
+ });
30253
+ }
30254
+ return returnArray;
30255
+ }
30256
+
30257
+ /**
30258
+ * Determine whether to determine the string with the
30259
+ * exact side length or the approximate side length.
30260
+ */
30261
+ function getPolygonSideString(sideLength, pointIndex, strings, locale) {
30262
+ return Number.isInteger(sideLength) ? strings.srPolygonSideLength({
30263
+ pointNum: pointIndex + 1,
30264
+ length: `${sideLength}`
30265
+ }) : strings.srPolygonSideLengthApprox({
30266
+ pointNum: pointIndex + 1,
30267
+ length: srFormatNumber(sideLength, locale, 1)
30268
+ });
30269
+ }
30270
+
30107
30271
  const {
30108
30272
  calculateAngleInDegrees: calculateAngleInDegrees$2
30109
30273
  } = kmath.angles;
@@ -30551,7 +30715,7 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
30551
30715
  })));
30552
30716
 
30553
30717
  const {
30554
- clockwise: clockwise$1
30718
+ clockwise
30555
30719
  } = kmath.geometry;
30556
30720
  const {
30557
30721
  getAngleFromVertex: getAngleFromVertex$1
@@ -30566,7 +30730,7 @@ const PolygonAngle = _ref => {
30566
30730
  snapTo
30567
30731
  } = _ref;
30568
30732
  const [centerX, centerY] = centerPoint;
30569
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
30733
+ const areClockwise = clockwise([centerPoint, ...endPoints]);
30570
30734
  const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
30571
30735
  const radius = 0.3;
30572
30736
  const a = mafs.vec.dist(centerPoint, endPoints[0]);
@@ -30639,7 +30803,7 @@ const Angle = _ref2 => {
30639
30803
  range
30640
30804
  } = _ref2;
30641
30805
  // Check if the points are clockwise or not, depending on whether we allow reflex angles
30642
- const areClockwise = clockwise$1([...coords, vertex]);
30806
+ const areClockwise = clockwise([...coords, vertex]);
30643
30807
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
30644
30808
 
30645
30809
  // Reverse the coordinates accordingly to ensure the angle is calculated correctly
@@ -31586,9 +31750,6 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
31586
31750
  };
31587
31751
  }
31588
31752
 
31589
- const {
31590
- clockwise
31591
- } = kmath.geometry;
31592
31753
  function getGradableGraph(state, initialGraph) {
31593
31754
  if (!state.hasBeenInteractedWith) {
31594
31755
  return {
@@ -31665,17 +31826,9 @@ function getGradableGraph(state, initialGraph) {
31665
31826
  };
31666
31827
  }
31667
31828
  if (state.type === "angle" && initialGraph.type === "angle") {
31668
- // We're going to reverse the coords for scoring if the angle is clockwise and
31669
- // we don't allow reflex angles. This is because the angle is largely defined
31670
- // by the order of the points in the coords array, and we want to maintain
31671
- // the same angle scoring with the legacy graph (which had a bug).
31672
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
31673
- const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
31674
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
31675
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
31676
31829
  return {
31677
31830
  ...initialGraph,
31678
- coords,
31831
+ coords: state.coords,
31679
31832
  allowReflexAngles: state.allowReflexAngles
31680
31833
  };
31681
31834
  }
@@ -32327,17 +32480,20 @@ function describePointGraph(state, i18n) {
32327
32480
  });
32328
32481
  });
32329
32482
  return strings.srInteractiveElements({
32330
- elements: pointDescriptions.join(", ")
32483
+ elements: pointDescriptions.join(" ")
32331
32484
  });
32332
32485
  }
32333
32486
 
32334
- function renderPolygonGraph(state, dispatch) {
32487
+ const {
32488
+ convertRadiansToDegrees
32489
+ } = kmath.angles;
32490
+ function renderPolygonGraph(state, dispatch, i18n, markings) {
32335
32491
  return {
32336
32492
  graph: /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
32337
32493
  graphState: state,
32338
32494
  dispatch: dispatch
32339
32495
  }),
32340
- interactiveElementsDescription: null
32496
+ interactiveElementsDescription: getPolygonGraphDescription(state, i18n, markings)
32341
32497
  };
32342
32498
  }
32343
32499
  const PolygonGraph = props => {
@@ -32422,7 +32578,7 @@ const PolygonGraph = props => {
32422
32578
  focusVisible,
32423
32579
  setFocusVisible
32424
32580
  };
32425
- return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
32581
+ return numSides === "unlimited" ? /*#__PURE__*/React__namespace.createElement(UnlimitedPolygonGraph, statefulProps) : /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, statefulProps);
32426
32582
  };
32427
32583
  const LimitedPolygonGraph = statefulProps => {
32428
32584
  const {
@@ -32447,8 +32603,39 @@ const LimitedPolygonGraph = statefulProps => {
32447
32603
  const {
32448
32604
  disableKeyboardInteraction
32449
32605
  } = graphConfig;
32606
+ const {
32607
+ strings,
32608
+ locale
32609
+ } = usePerseusI18n();
32610
+ const id = React__namespace.useId();
32611
+ const pointsOffArray = Array(points.length).fill("off");
32612
+ // When moving an element, set its aria-live to "polite" and the others
32613
+ // to "off". Otherwise, other connected elements that move at the same
32614
+ // time might override the currently focused element's aria live.
32615
+ const [ariaLives, setAriaLives] = React__namespace.useState([
32616
+ // First one represents the aria-live value for the polygon itself.
32617
+ "off",
32618
+ // The rest represent the points.
32619
+ ...pointsOffArray]);
32450
32620
  const lines = getLines(points);
32451
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32621
+ const polygonPointsNumId = id + "-points-num";
32622
+ const polygonPointsId = id + "-points";
32623
+
32624
+ // Aria label srings
32625
+ const {
32626
+ srPolygonGraph,
32627
+ srPolygonGraphPointsNum,
32628
+ srPolygonGraphPoints,
32629
+ srPolygonElementsNum
32630
+ } = describePolygonGraph(statefulProps.graphState, {
32631
+ strings,
32632
+ locale
32633
+ }, statefulProps.graphConfig.markings);
32634
+ return /*#__PURE__*/React__namespace.createElement("g", {
32635
+ // Outer graph minimal description
32636
+ "aria-label": srPolygonGraph,
32637
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32638
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32452
32639
  points: [...points],
32453
32640
  color: "var(--movable-line-stroke-color)",
32454
32641
  svgPolygonProps: {
@@ -32474,13 +32661,17 @@ const LimitedPolygonGraph = statefulProps => {
32474
32661
  });
32475
32662
  }), showSides && lines.map((_ref, i) => {
32476
32663
  let [start, end] = _ref;
32664
+ // Use x and y to find the position of the label
32477
32665
  const [x, y] = mafs.vec.midpoint(start, end);
32478
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
32666
+ const length = mafs.vec.dist(start, end);
32667
+ // Check if the length needs to indicate
32668
+ // that it's an approximation.
32669
+ const isApprox = !Number.isInteger(length);
32479
32670
  return /*#__PURE__*/React__namespace.createElement(TextLabel, {
32480
32671
  key: "side-" + i,
32481
32672
  x: x,
32482
32673
  y: y
32483
- }, !Number.isInteger(length) && "≈ ", length);
32674
+ }, isApprox ? `≈ ${length.toFixed(snapTo === "sides" ? 0 : 1)}` : length);
32484
32675
  }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32485
32676
  points: [...points],
32486
32677
  color: "transparent",
@@ -32500,27 +32691,80 @@ const LimitedPolygonGraph = statefulProps => {
32500
32691
  setFocusVisible(hasFocusVisible(polygonRef.current));
32501
32692
  },
32502
32693
  // Required for lines to darken on focus
32503
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32694
+ onFocus: () => {
32695
+ setFocusVisible(hasFocusVisible(polygonRef.current));
32696
+ setAriaLives(() => ["polite", ...pointsOffArray]);
32697
+ },
32504
32698
  // Required for line weighting to update on blur. Without this,
32505
32699
  // the user has to hover over the shape for it to update
32506
32700
  onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32507
- className: "movable-polygon"
32508
- }
32509
- }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32510
- key: "point-" + i,
32511
- constrain: constrain,
32512
- point: point,
32513
- sequenceNumber: i + 1,
32514
- onMove: destination => {
32515
- const now = Date.now();
32516
- const targetFPS = 40;
32517
- const moveThresholdTime = 1000 / targetFPS;
32518
- if (now - lastMoveTimeRef.current > moveThresholdTime) {
32519
- dispatch(actions.polygon.movePoint(i, destination));
32520
- lastMoveTimeRef.current = now;
32521
- }
32701
+ className: "movable-polygon",
32702
+ // Accessibility-related fields
32703
+ role: "button",
32704
+ "aria-label": srPolygonGraphPoints ? `${srPolygonElementsNum} ${srPolygonGraphPoints}` : srPolygonElementsNum,
32705
+ "aria-live": ariaLives[0]
32522
32706
  }
32523
- })));
32707
+ }), points.map((point, i) => {
32708
+ const angleId = `${id}-angle-${i}`;
32709
+ const side1Id = `${id}-point-${i}-side-1`;
32710
+ const side2Id = `${id}-point-${i}-side-2`;
32711
+
32712
+ // Limited polygons always have an angle at each vertex.
32713
+ const angle = getAngleFromPoints(points, i);
32714
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32715
+
32716
+ // Limited polygons always two sides attached to each vertex.
32717
+ const sidesArray = getSideLengthsFromPoints(points, i);
32718
+ const {
32719
+ pointIndex: point1Index,
32720
+ sideLength: side1Length
32721
+ } = sidesArray[0];
32722
+ const {
32723
+ pointIndex: point2Index,
32724
+ sideLength: side2Length
32725
+ } = sidesArray[1];
32726
+ return /*#__PURE__*/React__namespace.createElement("g", {
32727
+ key: "point-" + i
32728
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32729
+ ariaDescribedBy: `${angleId} ${side1Id} ${side2Id}`,
32730
+ ariaLive: ariaLives[i + 1],
32731
+ constrain: constrain,
32732
+ point: point,
32733
+ sequenceNumber: i + 1,
32734
+ onMove: destination => {
32735
+ const now = Date.now();
32736
+ const targetFPS = 40;
32737
+ const moveThresholdTime = 1000 / targetFPS;
32738
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
32739
+ dispatch(actions.polygon.movePoint(i, destination));
32740
+ lastMoveTimeRef.current = now;
32741
+ }
32742
+ },
32743
+ onFocus: () => {
32744
+ const newPointAriaLives = [...pointsOffArray];
32745
+ newPointAriaLives[i] = "polite";
32746
+ // Whole polygon is "off", and the current
32747
+ // point is "polite".
32748
+ setAriaLives(["off", ...newPointAriaLives]);
32749
+ }
32750
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32751
+ id: angleId
32752
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32753
+ angle: angleDegree
32754
+ }) : strings.srPolygonPointAngleApprox({
32755
+ angle: srFormatNumber(angleDegree, locale, 1)
32756
+ })), /*#__PURE__*/React__namespace.createElement("g", {
32757
+ id: side1Id
32758
+ }, getPolygonSideString(side1Length, point1Index, strings, locale)), /*#__PURE__*/React__namespace.createElement("g", {
32759
+ id: side2Id
32760
+ }, getPolygonSideString(side2Length, point2Index, strings, locale)));
32761
+ }), /*#__PURE__*/React__namespace.createElement("g", {
32762
+ id: polygonPointsNumId,
32763
+ style: a11y.srOnly
32764
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32765
+ id: polygonPointsId,
32766
+ style: a11y.srOnly
32767
+ }, srPolygonGraphPoints));
32524
32768
  };
32525
32769
  const UnlimitedPolygonGraph = statefulProps => {
32526
32770
  const {
@@ -32535,6 +32779,19 @@ const UnlimitedPolygonGraph = statefulProps => {
32535
32779
  coords,
32536
32780
  closedPolygon
32537
32781
  } = statefulProps.graphState;
32782
+ const {
32783
+ strings,
32784
+ locale
32785
+ } = usePerseusI18n();
32786
+ const id = React__namespace.useId();
32787
+ const polygonPointsNumId = id + "-points-num";
32788
+ const polygonPointsId = id + "-points";
32789
+
32790
+ // When moving an element, set its aria-live to "polite" and the others
32791
+ // to "off". Otherwise, other connected elements that move at the same
32792
+ // time might override the currently focused element's aria live.
32793
+ const pointsOffArray = Array(points.length).fill("off");
32794
+ const [ariaLives, setAriaLives] = React__namespace.useState(pointsOffArray);
32538
32795
 
32539
32796
  // If the polygon is closed, return a LimitedPolygon component.
32540
32797
  if (closedPolygon) {
@@ -32549,7 +32806,22 @@ const UnlimitedPolygonGraph = statefulProps => {
32549
32806
  } = graphConfig;
32550
32807
  const widthPx = graphDimensionsInPixels[0];
32551
32808
  const heightPx = graphDimensionsInPixels[1];
32552
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
32809
+
32810
+ // Aria label srings
32811
+ const emptyGraph = coords.length === 0;
32812
+ const {
32813
+ srPolygonGraph,
32814
+ srPolygonGraphPointsNum,
32815
+ srPolygonGraphPoints
32816
+ } = describePolygonGraph(statefulProps.graphState, {
32817
+ strings,
32818
+ locale
32819
+ }, statefulProps.graphConfig.markings);
32820
+ return /*#__PURE__*/React__namespace.createElement("g", {
32821
+ // Outer graph minimal description
32822
+ "aria-label": emptyGraph ? strings.srUnlimitedPolygonEmpty : srPolygonGraph,
32823
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32824
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
32553
32825
  points: [...points],
32554
32826
  color: "var(--movable-line-stroke-color)",
32555
32827
  svgPolylineProps: {
@@ -32574,28 +32846,71 @@ const UnlimitedPolygonGraph = statefulProps => {
32574
32846
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
32575
32847
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
32576
32848
  }
32577
- }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32578
- key: i,
32579
- point: point,
32580
- sequenceNumber: i + 1,
32581
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32582
- ref: ref => {
32583
- pointsRef.current[i] = ref;
32584
- },
32585
- onFocus: () => {
32586
- dispatch(actions.polygon.focusPoint(i));
32587
- },
32588
- onClick: () => {
32589
- // If the point being clicked is the first point and
32590
- // there's enough non-duplicated points to form
32591
- // a polygon (3 or more), close the shape before
32592
- // setting focus.
32593
- if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32594
- dispatch(actions.polygon.closePolygon());
32595
- }
32596
- dispatch(actions.polygon.clickPoint(i));
32849
+ }), coords.map((point, i) => {
32850
+ const angleId = `${id}-angle-${i}`;
32851
+ let sideIds = "";
32852
+
32853
+ // This point only has an angle if it's not the first
32854
+ // or last point in this open polygon.
32855
+ const hasAngle = i > 0 && i < coords.length - 1;
32856
+ const angle = hasAngle ? getAngleFromPoints(points, i) : null;
32857
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32858
+ const sidesArray = getSideLengthsFromPoints(points, i, true);
32859
+ for (let sideIndex = 0; sideIndex < sidesArray.length; sideIndex++) {
32860
+ sideIds += `${id}-point-${i}-side-${sideIndex} `;
32597
32861
  }
32598
- })));
32862
+ return /*#__PURE__*/React__namespace.createElement("g", {
32863
+ key: "point-" + i
32864
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32865
+ ariaDescribedBy: `${angleId} ${sideIds}`,
32866
+ ariaLive: ariaLives[i],
32867
+ point: point,
32868
+ sequenceNumber: i + 1,
32869
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32870
+ ref: ref => {
32871
+ pointsRef.current[i] = ref;
32872
+ },
32873
+ onFocus: () => {
32874
+ dispatch(actions.polygon.focusPoint(i));
32875
+ const newPointAriaLives = [...pointsOffArray];
32876
+ newPointAriaLives[i] = "polite";
32877
+ // Whole polygon is "off", and the current
32878
+ // point is "polite".
32879
+ setAriaLives([...newPointAriaLives]);
32880
+ },
32881
+ onClick: () => {
32882
+ // If the point being clicked is the first point and
32883
+ // there's enough non-duplicated points to form
32884
+ // a polygon (3 or more), close the shape before
32885
+ // setting focus.
32886
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32887
+ dispatch(actions.polygon.closePolygon());
32888
+ }
32889
+ dispatch(actions.polygon.clickPoint(i));
32890
+ }
32891
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32892
+ id: angleId
32893
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32894
+ angle: angleDegree
32895
+ }) : strings.srPolygonPointAngleApprox({
32896
+ angle: srFormatNumber(angleDegree, locale, 1)
32897
+ })), sidesArray.map((_ref2, j) => {
32898
+ let {
32899
+ pointIndex,
32900
+ sideLength
32901
+ } = _ref2;
32902
+ return /*#__PURE__*/React__namespace.createElement("g", {
32903
+ key: `${id}-point-${i}-side-${j}`,
32904
+ id: `${id}-point-${i}-side-${j}`
32905
+ }, getPolygonSideString(sideLength, pointIndex, strings, locale));
32906
+ }));
32907
+ }), coords.length > 0 && /*#__PURE__*/React__namespace.createElement("g", {
32908
+ id: polygonPointsNumId,
32909
+ style: a11y.srOnly
32910
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32911
+ id: polygonPointsId,
32912
+ style: a11y.srOnly
32913
+ }, srPolygonGraphPoints));
32599
32914
  };
32600
32915
  function getLines(points) {
32601
32916
  return points.map((point, i) => {
@@ -32614,6 +32929,54 @@ const hasFocusVisible = element => {
32614
32929
  return matches(":focus");
32615
32930
  }
32616
32931
  };
32932
+ function getPolygonGraphDescription(state, i18n, markings) {
32933
+ const strings = describePolygonGraph(state, i18n, markings);
32934
+ return strings.srPolygonInteractiveElements;
32935
+ }
32936
+ function describePolygonGraph(state, i18n, markings) {
32937
+ const {
32938
+ strings,
32939
+ locale
32940
+ } = i18n;
32941
+ const {
32942
+ coords
32943
+ } = state;
32944
+ const isCoordinatePlane = markings === "axes" || markings === "graph";
32945
+ const hasOnePoint = coords.length === 1;
32946
+
32947
+ // Figure out graph aria label based on markings.
32948
+ const srPolygonGraph = isCoordinatePlane ? strings.srPolygonGraphCoordinatePlane : strings.srPolygonGraph;
32949
+ const srPolygonGraphPointsNum = hasOnePoint ? strings.srPolygonGraphPointsOne : strings.srPolygonGraphPointsNum({
32950
+ num: coords.length
32951
+ });
32952
+ let srPolygonGraphPoints;
32953
+ // Figure out graph description based on markings.
32954
+ // If the graph is not on a coordinate plane, we should not include
32955
+ // the points' coordinates in the description.
32956
+ if (isCoordinatePlane) {
32957
+ const pointsString = coords.map((coord, i) => {
32958
+ return strings.srPointAtCoordinates({
32959
+ num: i + 1,
32960
+ x: srFormatNumber(coord[0], locale),
32961
+ y: srFormatNumber(coord[1], locale)
32962
+ });
32963
+ });
32964
+ srPolygonGraphPoints = pointsString.join(" ");
32965
+ }
32966
+ const srPolygonElementsNum = hasOnePoint ? strings.srPolygonElementsOne : strings.srPolygonElementsNum({
32967
+ num: coords.length
32968
+ });
32969
+ const srPolygonInteractiveElements = coords.length > 0 ? strings.srInteractiveElements({
32970
+ elements: [srPolygonElementsNum, srPolygonGraphPoints].join(" ")
32971
+ }) : null;
32972
+ return {
32973
+ srPolygonGraph,
32974
+ srPolygonGraphPointsNum,
32975
+ srPolygonGraphPoints,
32976
+ srPolygonElementsNum,
32977
+ srPolygonInteractiveElements
32978
+ };
32979
+ }
32617
32980
 
32618
32981
  function renderQuadraticGraph(state, dispatch, i18n) {
32619
32982
  return {
@@ -33320,7 +33683,8 @@ const MafsGraph = props => {
33320
33683
  } = renderGraphElements({
33321
33684
  state,
33322
33685
  dispatch,
33323
- i18n
33686
+ i18n,
33687
+ markings: props.markings
33324
33688
  });
33325
33689
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
33326
33690
  value: {
@@ -33355,7 +33719,7 @@ const MafsGraph = props => {
33355
33719
  handleKeyboardEvent(event, state, dispatch);
33356
33720
  },
33357
33721
  "aria-label": fullGraphAriaLabel,
33358
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
33722
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && unlimitedGraphKeyboardPromptId),
33359
33723
  ref: graphRef,
33360
33724
  tabIndex: 0,
33361
33725
  onFocus: event => {
@@ -33381,7 +33745,19 @@ const MafsGraph = props => {
33381
33745
  bottom: 0,
33382
33746
  left: 0
33383
33747
  }
33384
- }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33748
+ }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
33749
+ // If we have locked figures, we set aria-hidden
33750
+ // to false so that screen readers can read the
33751
+ // locked figures. If we don't have any locked
33752
+ // figures, we need to set aria-hidden to true so
33753
+ // that screen readers don't read this Mafs element
33754
+ // as an empty image.
33755
+ // Note: Adding this in a wrapping View element
33756
+ // so that `aria-hidden` is a valid property.
33757
+ // It does not work on `Mafs` or svg elements.
33758
+ , {
33759
+ "aria-hidden": props.lockedFigures && props.lockedFigures.length > 0 ? false : true
33760
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33385
33761
  preserveAspectRatio: false,
33386
33762
  viewBox: {
33387
33763
  x: state.range[X],
@@ -33401,10 +33777,10 @@ const MafsGraph = props => {
33401
33777
  height: height
33402
33778
  })),
33403
33779
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
33404
- (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
33780
+ (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), props.lockedFigures && props.lockedFigures.length > 0 && /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
33405
33781
  lockedFigures: props.lockedFigures,
33406
33782
  range: state.range
33407
- }))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33783
+ })))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33408
33784
  lockedFigures: props.lockedFigures
33409
33785
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
33410
33786
  style: {
@@ -33698,7 +34074,8 @@ const renderGraphElements = props => {
33698
34074
  const {
33699
34075
  state,
33700
34076
  dispatch,
33701
- i18n
34077
+ i18n,
34078
+ markings
33702
34079
  } = props;
33703
34080
  const {
33704
34081
  type
@@ -33715,7 +34092,7 @@ const renderGraphElements = props => {
33715
34092
  case "ray":
33716
34093
  return renderRayGraph(state, dispatch);
33717
34094
  case "polygon":
33718
- return renderPolygonGraph(state, dispatch);
34095
+ return renderPolygonGraph(state, dispatch, i18n, markings);
33719
34096
  case "point":
33720
34097
  return renderPointGraph(state, dispatch);
33721
34098
  case "circle":
@@ -34700,7 +35077,7 @@ const getChange = (coords, delta, constraintOpts) => {
34700
35077
  return [dx, dy];
34701
35078
  };
34702
35079
  // Less than or approximately equal
34703
- function leq$1(a, b) {
35080
+ function leq(a, b) {
34704
35081
  return a < b || perseusCore.approximateEqual(a, b);
34705
35082
  }
34706
35083
  function boundAndSnapToGrid(point, _ref) {
@@ -34871,7 +35248,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
34871
35248
 
34872
35249
  // Avoid degenerate triangles
34873
35250
  if (innerAngles.some(function (angle) {
34874
- return leq$1(angle, 1);
35251
+ return leq(angle, 1);
34875
35252
  })) {
34876
35253
  return startingPoint;
34877
35254
  }
@@ -34933,7 +35310,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
34933
35310
  });
34934
35311
 
34935
35312
  // Avoid degenerate triangles
34936
- if (leq$1(sides[1] + sides[2], sides[0]) || leq$1(sides[0] + sides[2], sides[1]) || leq$1(sides[0] + sides[1], sides[2])) {
35313
+ if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
34937
35314
  return startingPoint;
34938
35315
  }
34939
35316
 
@@ -35100,24 +35477,10 @@ const {
35100
35477
  sign,
35101
35478
  vector
35102
35479
  } = kmath.geometry;
35103
- const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
35104
35480
  const defaultBackgroundImage = {
35105
35481
  url: null
35106
35482
  };
35107
35483
  const UNLIMITED = "unlimited";
35108
-
35109
- // Sample background image:
35110
- // https://ka-perseus-graphie.s3.amazonaws.com/29c1b0fcd17fe63df0f148fe357044d5d5c7d0bb.png
35111
-
35112
- // default to defaultValue if actual is null or undefined
35113
- function defaultVal(actual, defaultValue) {
35114
- return actual == null ? defaultValue : actual;
35115
- }
35116
-
35117
- // Less than or approximately equal
35118
- function leq(a, b) {
35119
- return a < b || perseusCore.approximateEqual(a, b);
35120
- }
35121
35484
  function capitalize(str) {
35122
35485
  return str.replace(/(?:^|-)(.)/g, function (match, letter) {
35123
35486
  return letter.toUpperCase();
@@ -35126,14 +35489,6 @@ function capitalize(str) {
35126
35489
  function numSteps(range, step) {
35127
35490
  return Math.floor((range[1] - range[0]) / step);
35128
35491
  }
35129
- const _getShouldShowInstructions = props => {
35130
- return _isClickToAddPoints(props) && (
35131
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35132
- props.graph.coords == null || props.graph.coords.length === 0);
35133
- };
35134
- const _isClickToAddPoints = props => {
35135
- return props.graph.type === "point" && props.graph.numPoints === UNLIMITED || props.graph.type === "polygon" && props.graph.numSides === UNLIMITED;
35136
- };
35137
35492
  const makeInvalidTypeError = (functionName, graphType) => {
35138
35493
  return new perseusCore.PerseusError(`${functionName} called but current graph type is not a '${graphType}'`, perseusCore.Errors.NotAllowed, {
35139
35494
  metadata: {
@@ -35174,1200 +35529,6 @@ function getQuadraticCoefficients(coords) {
35174
35529
  const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
35175
35530
  return [a, b, c];
35176
35531
  }
35177
-
35178
- // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
35179
- // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
35180
- // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
35181
- // Once the legacy graph is removed, we should move this logic directly into the validate function below.
35182
- class LegacyInteractiveGraph extends React__namespace.Component {
35183
- static contextType = PerseusI18nContext;
35184
- angle;
35185
- circle;
35186
- graphie;
35187
- horizHairline;
35188
- line;
35189
- lines;
35190
- parabola;
35191
- pointA;
35192
- pointB;
35193
- pointC;
35194
- // @ts-expect-error - TS2564 - Property 'points' has no initializer and is not definitely assigned in the constructor.
35195
- points;
35196
- polygon;
35197
- // @ts-expect-error - TS2564 - Property 'shouldResetGraphie' has no initializer and is not definitely assigned in the constructor.
35198
- shouldResetGraphie;
35199
- sinusoid;
35200
- trashCan;
35201
- vertHairline;
35202
- static defaultProps = {
35203
- labels: ["x", "y"],
35204
- range: [[-10, 10], [-10, 10]],
35205
- step: [1, 1],
35206
- backgroundImage: defaultBackgroundImage,
35207
- markings: "graph",
35208
- showTooltips: false,
35209
- showProtractor: false,
35210
- graph: {
35211
- type: "linear"
35212
- }
35213
- };
35214
- state = {
35215
- shouldShowInstructions: _getShouldShowInstructions(this.props)
35216
- };
35217
- componentDidMount() {
35218
- if (this.refs.graph) {
35219
- // eslint-disable-next-line react/no-string-refs
35220
- // @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
35221
- this.setGraphie(this.refs.graph.graphie());
35222
- }
35223
- }
35224
- UNSAFE_componentWillReceiveProps(nextProps) {
35225
- if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) {
35226
- this.shouldResetGraphie = true;
35227
- this.setState({
35228
- shouldShowInstructions: _getShouldShowInstructions(nextProps)
35229
- });
35230
- }
35231
- if (this.props.backgroundImage?.url !== nextProps.backgroundImage?.url || this.props.backgroundImage !== nextProps.backgroundImage || this.props.containerSizeClass !== nextProps.containerSizeClass) {
35232
- this.shouldResetGraphie = true;
35233
- }
35234
- }
35235
- componentDidUpdate(prevProps, prevState) {
35236
- // TODO(jeremy): This feels underspecified. For example, if the props
35237
- // change the coords of a segment graph, but not the numSegments, we'd
35238
- // not count that as an update.
35239
- const oldType = prevProps.graph.type;
35240
- const newType = this.props.graph.type;
35241
- if (oldType !== newType || prevProps.graph.type === "angle" && this.props.graph.type === "angle" && this.areAngleGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "point" && this.props.graph.type === "point" && this.arePointGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "polygon" && this.props.graph.type === "polygon" && this.arePolygonGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "segment" && this.props.graph.type === "segment" && this.areSegmentGraphsEqual(prevProps.graph, this.props.graph)) {
35242
- this["remove" + capitalize(oldType) + "Controls"]();
35243
- this["add" + capitalize(newType) + "Controls"]();
35244
- }
35245
- if (this.shouldResetGraphie) {
35246
- this.resetGraphie();
35247
- }
35248
- }
35249
- _getShouldShowInstructions = props => {
35250
- props = props || this.props;
35251
- return this.isClickToAddPoints(props) && (
35252
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35253
- props.graph.coords == null || props.graph.coords.length === 0);
35254
- };
35255
- setGraphie = newGraphie => {
35256
- this.graphie = newGraphie;
35257
- this.setupGraphie();
35258
- };
35259
- handleAddPointsMouseDown = coord => {
35260
- // This function should only be called when this.isClickToAddPoints()
35261
- // is true
35262
- if (!this.isClickToAddPoints()) {
35263
- throw new perseusCore.PerseusError("handleAddPointsClick should not be registered" + "when isClickToAddPoints() is false", perseusCore.Errors.NotAllowed);
35264
- }
35265
- if (!this.isCoordInTrash(coord)) {
35266
- let point;
35267
- if (this.props.graph.type === "point") {
35268
- point = this.createPointForPointsType(coord, this.points.length);
35269
- if (!point.constrain()) {
35270
- point.remove();
35271
- return;
35272
- }
35273
- this.points.push(point);
35274
-
35275
- // interactive2 allows us to grab the point
35276
- const idx = this.points.length - 1;
35277
- this.points[idx].grab(coord);
35278
- this.updateCoordsFromPoints();
35279
- } else if (this.props.graph.type === "polygon") {
35280
- if (this.polygon?.closed()) {
35281
- return;
35282
- }
35283
- point = this.createPointForPolygonType(coord, this.points.length);
35284
- this.points.push(point);
35285
- const idx = this.points.length - 1;
35286
- this.points[idx].grab(coord);
35287
-
35288
- // We don't call updateCoordsFromPoints for
35289
- // polygons, since the polygon won't be
35290
- // closed yet.
35291
- this.updatePolygon();
35292
- }
35293
- this.setState({
35294
- shouldShowInstructions: false
35295
- });
35296
- }
35297
- };
35298
- resetGraphie = () => {
35299
- this.shouldResetGraphie = false;
35300
- this.parabola = null;
35301
- this.sinusoid = null;
35302
- // eslint-disable-next-line react/no-string-refs
35303
- // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
35304
- this.refs.graph?.reset();
35305
- };
35306
- setupGraphie = () => {
35307
- this.setTrashCanVisibility(0);
35308
- if (this.isClickToAddPoints()) {
35309
- this.setTrashCanVisibility(0.5);
35310
- }
35311
- if (this.props.apiOptions.isMobile) {
35312
- this.horizHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35313
- normalStyle: {
35314
- strokeWidth: 1
35315
- }
35316
- });
35317
- this.horizHairline.attr({
35318
- stroke: KhanColors.INTERACTIVE
35319
- });
35320
- this.horizHairline?.hide();
35321
- this.vertHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35322
- normalStyle: {
35323
- strokeWidth: 1
35324
- }
35325
- });
35326
- this.vertHairline.attr({
35327
- stroke: KhanColors.INTERACTIVE
35328
- });
35329
- this.vertHairline?.hide();
35330
- }
35331
- const type = this.props.graph.type;
35332
- this["add" + capitalize(type) + "Controls"]();
35333
- };
35334
- showHairlines = point => {
35335
- if (this.props.apiOptions.isMobile && this.props.markings !== "none") {
35336
- // Hairlines are already initialized when the graph is loaded, so
35337
- // here we just move them to the updated location and make them
35338
- // visible.
35339
- this.horizHairline?.moveTo([this.props.range[0][0], point[1]], [this.props.range[0][1], point[1]]);
35340
- this.horizHairline?.show();
35341
- this.vertHairline?.moveTo([point[0], this.props.range[1][0]], [point[0], this.props.range[1][1]]);
35342
- this.vertHairline?.show();
35343
- }
35344
- };
35345
- hideHairlines = () => {
35346
- if (this.props.apiOptions.isMobile) {
35347
- this.horizHairline?.hide();
35348
- this.vertHairline?.hide();
35349
- }
35350
- };
35351
- setTrashCanVisibility = opacity => {
35352
- const graphie = this.graphie;
35353
- if (kmath.number.equal(opacity, 0)) {
35354
- if (this.trashCan) {
35355
- this.trashCan.remove();
35356
- this.trashCan = null;
35357
- }
35358
- } else if (!this.props.apiOptions.isMobile) {
35359
- // Only if trash tooltips are not being used, we initialize the old
35360
- // trash can area.
35361
- if (!this.trashCan) {
35362
- this.trashCan = graphie?.raphael?.image(TRASH_ICON_URI, graphie.xpixels - 40, graphie.ypixels - 40, 40, 40);
35363
- }
35364
- this.trashCan?.attr({
35365
- opacity: opacity
35366
- });
35367
- }
35368
- };
35369
- isClickToAddPoints = props => {
35370
- props = props || this.props;
35371
- return _isClickToAddPoints(props);
35372
- };
35373
- areAngleGraphsEqual(prevGraph, currentGraph) {
35374
- return prevGraph.allowReflexAngles !== currentGraph.allowReflexAngles || prevGraph.angleOffsetDeg !== currentGraph.angleOffsetDeg || prevGraph.snapDegrees !== currentGraph.snapDegrees;
35375
- }
35376
- arePointGraphsEqual(prevGraph, currentGraph) {
35377
- return prevGraph.numPoints !== currentGraph.numPoints;
35378
- }
35379
- arePolygonGraphsEqual(prevGraph, currentGraph) {
35380
- return prevGraph.numSides !== currentGraph.numSides || prevGraph.showAngles !== currentGraph.showAngles || prevGraph.showSides !== currentGraph.showSides || prevGraph.snapTo !== currentGraph.snapTo;
35381
- }
35382
- areSegmentGraphsEqual(prevGraph, currentGraph) {
35383
- return prevGraph.numSegments !== currentGraph.numSegments;
35384
- }
35385
- _lineStroke = () => {
35386
- // This should probably use: this.props.apiOptions.isMobile
35387
- // @ts-expect-error - TS2339 - Property 'isMobile' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
35388
- return this.props.isMobile ? {
35389
- "stroke-width": 3
35390
- } : {};
35391
- };
35392
- addLine = type => {
35393
- const self = this;
35394
- const graphie = self.graphie;
35395
- const coords = InteractiveGraph.getLineCoords(self.props.graph, self.props);
35396
- const points = self.points = ___default["default"].map(coords, coord => {
35397
- return Interactive2.addMaybeMobileMovablePoint(this, {
35398
- coord: coord,
35399
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap()],
35400
- onMove: () => {
35401
- const graph = ___default["default"].extend({}, self.props.graph, {
35402
- coords: ___default["default"].invoke(points, "coord")
35403
- });
35404
- self.onChange({
35405
- graph: graph
35406
- });
35407
- }
35408
- });
35409
- });
35410
- const lineConfig = {
35411
- points: points,
35412
- static: true,
35413
- normalStyle: {
35414
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
35415
- ...this._lineStroke()
35416
- }
35417
- };
35418
- if (type === "line") {
35419
- // @ts-expect-error - TS2339 - Property 'extendLine' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
35420
- lineConfig.extendLine = true;
35421
- } else if (type === "ray") {
35422
- // @ts-expect-error - TS2339 - Property 'extendRay' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
35423
- lineConfig.extendRay = true;
35424
- }
35425
- self.line = Interactive2.addMovableLine(graphie, lineConfig);
35426
-
35427
- // A and B can't be in the same place
35428
- points[0].listen("constraints", "isLine", coord => {
35429
- return !kmath.point.equal(coord, points[1].coord());
35430
- });
35431
- points[1].listen("constraints", "isLine", coord => {
35432
- return !kmath.point.equal(coord, points[0].coord());
35433
- });
35434
- };
35435
- removeLine = () => {
35436
- ___default["default"].invoke(this.points, "remove");
35437
- this.line?.remove();
35438
- };
35439
- addNoneControls = () => {};
35440
- removeNoneControls = () => {};
35441
- addLinearControls = () => {
35442
- this.addLine("line");
35443
- };
35444
- removeLinearControls = () => {
35445
- this.removeLine();
35446
- };
35447
- addQuadraticControls = () => {
35448
- if (this.props.graph.type !== "quadratic") {
35449
- throw makeInvalidTypeError("addQuadraticControls", "quadratic");
35450
- }
35451
- let coords = this.props.graph.coords;
35452
- if (!coords) {
35453
- coords = InteractiveGraph.defaultQuadraticCoords(this.props);
35454
- }
35455
- const onMoveHandler = () => {
35456
- const graph = ___default["default"].extend({}, this.props.graph, {
35457
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'.
35458
- coords: [pointA.coord(), pointB.coord(), pointC.coord()]
35459
- });
35460
- this.onChange({
35461
- graph
35462
- });
35463
- this.updateQuadratic();
35464
- };
35465
-
35466
- // NOTE(jeresig): This code attempts to access the variable while it's
35467
- // still being defined, it should be refactored! The callbacks appear
35468
- // to be executed synchronously, which causes this issue.
35469
- let pointA = null;
35470
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35471
- coord: coords[0],
35472
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35473
- return !pointA ||
35474
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35475
- coord[0] !== pointB.coord()[0] &&
35476
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35477
- coord[0] !== pointC.coord()[0];
35478
- }],
35479
- onMove: onMoveHandler
35480
- });
35481
-
35482
- // NOTE(jeresig): This code attempts to access the variable while it's
35483
- // still being defined, it should be refactored! The callbacks appear
35484
- // to be executed synchronously, which causes this issue.
35485
- let pointB = null;
35486
- pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35487
- coord: coords[1],
35488
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35489
- return !pointB ||
35490
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35491
- coord[0] !== pointA.coord()[0] &&
35492
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35493
- coord[0] !== pointC.coord()[0];
35494
- }],
35495
- onMove: onMoveHandler
35496
- });
35497
-
35498
- // NOTE(jeresig): This code attempts to access the variable while it's
35499
- // still being defined, it should be refactored! The callbacks appear
35500
- // to be executed synchronously, which causes this issue.
35501
- let pointC = null;
35502
- pointC = this.pointC = Interactive2.addMaybeMobileMovablePoint(this, {
35503
- coord: coords[2],
35504
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35505
- return !pointC ||
35506
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35507
- coord[0] !== pointA.coord()[0] &&
35508
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35509
- coord[0] !== pointB.coord()[0];
35510
- }],
35511
- onMove: onMoveHandler
35512
- });
35513
- this.updateQuadratic();
35514
- };
35515
- updateQuadratic = () => {
35516
- const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(this.props);
35517
- if (!coeffs) {
35518
- return;
35519
- }
35520
-
35521
- // Extract coefficients the parabola
35522
- const a = coeffs[0];
35523
- const b = coeffs[1];
35524
- const c = coeffs[2];
35525
-
35526
- // Plot and style
35527
- if (this.parabola) {
35528
- const path = this.graphie?.svgParabolaPath(a, b, c);
35529
- this.parabola?.attr({
35530
- path: path
35531
- });
35532
- } else {
35533
- this.parabola = this.graphie?.parabola(a, b, c);
35534
- this.parabola?.attr({
35535
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
35536
- ...this._lineStroke()
35537
- });
35538
- this.parabola?.toBack();
35539
- }
35540
- };
35541
- removeQuadraticControls = () => {
35542
- this.pointA?.remove();
35543
- this.pointB?.remove();
35544
- this.pointC?.remove();
35545
- if (this.parabola) {
35546
- this.parabola.remove();
35547
- this.parabola = null;
35548
- }
35549
- };
35550
- addSinusoidControls = () => {
35551
- if (this.props.graph.type !== "sinusoid") {
35552
- throw makeInvalidTypeError("addSinusoidControls", "sinusoid");
35553
- }
35554
- let coords = this.props.graph.coords;
35555
- if (!coords) {
35556
- coords = InteractiveGraph.defaultSinusoidCoords(this.props);
35557
- }
35558
- const onMoveHandler = () => {
35559
- const graph = ___default["default"].extend({}, this.props.graph, {
35560
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35561
- coords: [pointA.coord(), pointB.coord()]
35562
- });
35563
- this.onChange({
35564
- graph: graph
35565
- });
35566
- this.updateSinusoid();
35567
- };
35568
-
35569
- // NOTE(jeresig): This code attempts to access the variable while it's
35570
- // still being defined, it should be refactored! The callbacks appear
35571
- // to be executed synchronously, which causes this issue.
35572
- let pointA = null;
35573
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35574
- coord: coords[0],
35575
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35576
- return !pointA || coord[0] !== pointB.coord()[0];
35577
- }],
35578
- onMove: onMoveHandler
35579
- });
35580
- const pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35581
- coord: coords[1],
35582
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35583
- // @ts-expect-error - TS2339 - Property 'coord' does not exist on type 'never'.
35584
- return !pointA || coord[0] !== pointA.coord()[0];
35585
- }],
35586
- onMove: onMoveHandler
35587
- });
35588
- this.updateSinusoid();
35589
- };
35590
- updateSinusoid = () => {
35591
- const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(this.props);
35592
- if (!coeffs) {
35593
- return;
35594
- }
35595
- const a = coeffs[0];
35596
- const b = coeffs[1];
35597
- const c = coeffs[2];
35598
- const d = coeffs[3];
35599
-
35600
- // Plot and style
35601
- if (this.sinusoid) {
35602
- const path = this.graphie?.svgSinusoidPath(a, b, c, d);
35603
- this.sinusoid?.attr({
35604
- path: path
35605
- });
35606
- } else {
35607
- this.sinusoid = this.graphie?.sinusoid(a, b, c, d);
35608
- this.sinusoid?.attr({
35609
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
35610
- ...this._lineStroke()
35611
- });
35612
- this.sinusoid?.toBack();
35613
- }
35614
- };
35615
- removeSinusoidControls = () => {
35616
- this.pointA?.remove();
35617
- this.pointB?.remove();
35618
- if (this.sinusoid) {
35619
- this.sinusoid.remove();
35620
- this.sinusoid = null;
35621
- }
35622
- };
35623
- addCircleControls = () => {
35624
- const graphie = this.graphie;
35625
- const minSnap = ___default["default"].min(graphie.snap);
35626
- const circle = this.circle = graphie?.addCircleGraph({
35627
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'.
35628
- center: this.props.graph.center || [0, 0],
35629
- // @ts-expect-error - TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35630
- radius: this.props.graph.radius || ___default["default"].min(this.props.step),
35631
- snapX: graphie.snap[0],
35632
- snapY: graphie.snap[1],
35633
- minRadius: minSnap,
35634
- snapRadius: minSnap
35635
- });
35636
- $__default["default"](circle).on("move", () => {
35637
- const graph = ___default["default"].extend({}, this.props.graph, {
35638
- center: circle.center,
35639
- radius: circle.radius
35640
- });
35641
- this.onChange({
35642
- graph: graph
35643
- });
35644
- });
35645
- };
35646
- removeCircleControls = () => {
35647
- this.circle?.remove();
35648
- };
35649
- addLinearSystemControls = () => {
35650
- const graphie = this.graphie;
35651
- const coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, this.props);
35652
- const segmentColors = [KhanColors.INTERACTIVE, KhanColors.GREEN];
35653
- const points = this.points = ___default["default"].map(coords, (segmentCoords, segmentIndex) => {
35654
- // NOTE(jeresig): This code attempts to access the variable
35655
- // while it's still being defined, it should be refactored!
35656
- // The callbacks appear to be executed synchronously, which
35657
- // causes this issue.
35658
- const segmentPoints = [];
35659
- for (let i = 0; i < segmentCoords.length; i += 1) {
35660
- const coord = segmentCoords[i];
35661
- segmentPoints.push(Interactive2.addMaybeMobileMovablePoint(this, {
35662
- coord: coord,
35663
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35664
- const otherSegment = segmentPoints[1 - i];
35665
- if (!otherSegment) {
35666
- // segment hasn't been defined yet
35667
- // because we're still creating them
35668
- return;
35669
- }
35670
- return !kmath.point.equal(coord, otherSegment.coord());
35671
- }],
35672
- onMove: () => {
35673
- const graph = ___default["default"].extend({}, this.props.graph, {
35674
- coords: ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "coord"))
35675
- });
35676
- this.onChange({
35677
- graph: graph
35678
- });
35679
- },
35680
- normalStyle: {
35681
- fill: segmentColors[segmentIndex]
35682
- },
35683
- highlightStyle: {
35684
- fill: segmentColors[segmentIndex]
35685
- }
35686
- }));
35687
- }
35688
- return segmentPoints;
35689
- });
35690
- this.lines = ___default["default"].map(points, (segmentPoints, segmentIndex) => {
35691
- return Interactive2.addMovableLine(graphie, {
35692
- points: segmentPoints,
35693
- static: true,
35694
- extendLine: true,
35695
- normalStyle: {
35696
- stroke: segmentColors[segmentIndex]
35697
- }
35698
- });
35699
- });
35700
- };
35701
- removeLinearSystemControls = () => {
35702
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
35703
- ___default["default"].invoke(this.lines, "remove");
35704
- ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "remove"));
35705
- };
35706
- isCoordInTrash = coord => {
35707
- if (this.props.apiOptions.isMobile) {
35708
- return false;
35709
- }
35710
- const graphie = this.graphie;
35711
- const screenPoint = graphie?.scalePoint(coord);
35712
- return screenPoint[0] >= graphie.xpixels - 40 && screenPoint[1] >= graphie.ypixels - 40;
35713
- };
35714
- createPointForPointsType = (coord, i) => {
35715
- const self = this;
35716
- const remove = () => {
35717
- self.points = ___default["default"].filter(self.points, function (pt) {
35718
- return pt !== point;
35719
- });
35720
- // update the correct answer box
35721
- self.updateCoordsFromPoints();
35722
-
35723
- // remove this movablePoint from graphie.
35724
- // we wait to do this until we're not inside of
35725
- // said point's onMoveEnd method so its state is
35726
- // consistent throughout this method call
35727
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35728
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35729
- setTimeout(point.remove.bind(point), 0); // eslint-disable-line no-restricted-syntax
35730
- };
35731
-
35732
- // NOTE(jeresig): This code attempts to access the variable while it's
35733
- // still being defined, it should be refactored! The callbacks appear
35734
- // to be executed synchronously, which causes this issue.
35735
- let point = null;
35736
- point = Interactive2.addMaybeMobileMovablePoint(this, {
35737
- coord: coord,
35738
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), function (coord) {
35739
- // TODO(jack): There ought to be a
35740
- // MovablePoint.constraints.avoid
35741
- // default that lets you do things like this
35742
- return ___default["default"].all(self.points, function (pt) {
35743
- return point === pt || !kmath.point.equal(coord, pt.coord());
35744
- });
35745
- }],
35746
- onMoveStart: function () {
35747
- if (self.isClickToAddPoints()) {
35748
- self.setTrashCanVisibility(1);
35749
- }
35750
- },
35751
- onMove: self.updateCoordsFromPoints,
35752
- onMoveEnd: function (coord) {
35753
- if (self.isClickToAddPoints()) {
35754
- if (self.isCoordInTrash(coord)) {
35755
- remove();
35756
- }
35757
- // In case we mouseup'd off the graphie and that
35758
- // stopped the move (in which case, we might not
35759
- // be in isCoordInTrash()
35760
- self.setTrashCanVisibility(0.5);
35761
- }
35762
- },
35763
- ...(this.props.apiOptions.isMobile && self.isClickToAddPoints() ? {
35764
- onRemove: remove
35765
- } : {})
35766
- });
35767
- return point;
35768
- };
35769
- removePoint = point => {
35770
- let index = null;
35771
- this.points = ___default["default"].filter(this.points, function (pt, i) {
35772
- if (pt === point) {
35773
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
35774
- index = i;
35775
- return false;
35776
- }
35777
- return true;
35778
- });
35779
- return index;
35780
- };
35781
- createPointForPolygonType = (coord, i) => {
35782
- if (this.props.graph.type !== "polygon") {
35783
- throw makeInvalidTypeError("createPointForPolygonType", "polygon");
35784
- }
35785
-
35786
- // TODO(alex): check against "grid" instead, use constants
35787
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
35788
-
35789
- // Index relative to current point -> absolute index
35790
- // NOTE: This does not work when isClickToAddPoints() == true,
35791
- // as `i` can be changed by dragging a point to the trash
35792
- // Currently this function is only called when !isClickToAddPoints()
35793
- const rel = j => {
35794
- return (i + j + this.points.length) % this.points.length;
35795
- };
35796
- const remove = () => {
35797
- // remove this point from points
35798
- const index = this.removePoint(point);
35799
- if (this.polygon?.closed()) {
35800
- // We should be checking if this.points is defined before rotating them.
35801
- // @ts-expect-error - TS4104 - The type 'readonly any[]' is 'readonly' and cannot be assigned to the mutable type 'any[]'. | TS2345 - Argument of type 'number | null | undefined' is not assignable to parameter of type 'number | undefined'.
35802
- this.points = rotate(this.points, index);
35803
- this.polygon?.update({
35804
- closed: false
35805
- });
35806
- }
35807
- this.updatePolygon();
35808
- // the polygon is now unclosed, so we need to
35809
- // remove any points props
35810
- this.clearCoords();
35811
-
35812
- // remove this movablePoint from graphie.
35813
- // wait to do this until we're not inside of
35814
- // said point's onMoveEnd method so state is
35815
- // consistent throughout the method call
35816
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35817
- // eslint-disable-next-line no-restricted-syntax
35818
- setTimeout(point.remove.bind(point), 0);
35819
- };
35820
- const setInitialMove = debounce__default["default"]((point, bool) => {
35821
- point.state.isInitialMove = bool;
35822
- }, 1000);
35823
- const onMoveEndHandler = coord => {
35824
- // If the user clicked and dragged a point over endpoint,
35825
- // join the them
35826
- const overlappedPoints = this.points.length > 1 && (point === this.points[0] && kmath.point.equal(coord, ___default["default"].last(this.points).coord()) || point === ___default["default"].last(this.points) && kmath.point.equal(coord, this.points[0].coord()));
35827
- if (this.isClickToAddPoints()) {
35828
- if (this.isCoordInTrash(coord)) {
35829
- remove();
35830
- } else if (overlappedPoints) {
35831
- const pointToRemove = this.points.pop();
35832
- if (this.points.length > 2) {
35833
- this.polygon?.update({
35834
- closed: true
35835
- });
35836
- this.updateCoordsFromPoints();
35837
- } else {
35838
- this.polygon?.update({
35839
- closed: false
35840
- });
35841
- this.clearCoords();
35842
- }
35843
- this.updatePolygon();
35844
- // remove this movablePoint from graphie.
35845
- // wait to do this until we're not inside of
35846
- // said point's onMoveEnd method so state is
35847
- // consistent throughout the method call
35848
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35849
- // eslint-disable-next-line no-restricted-syntax
35850
- setTimeout(pointToRemove.remove.bind(pointToRemove), 0);
35851
- } else {
35852
- // If the user clicked and dragged a point over any other
35853
- // existing point, fix shape
35854
- const shouldRemove = ___default["default"].any(this.points, function (pt) {
35855
- return pt !== point && kmath.point.equal(pt.coord(), coord);
35856
- });
35857
- if (shouldRemove) {
35858
- this.removePoint(point);
35859
- if (this.points.length < 3) {
35860
- this.polygon?.update({
35861
- closed: false
35862
- });
35863
- this.clearCoords();
35864
- } else if (this.polygon?.closed()) {
35865
- this.updateCoordsFromPoints();
35866
- }
35867
- this.updatePolygon();
35868
- // remove this movablePoint from graphie.
35869
- // wait to do this until we're not inside
35870
- // said point's onMoveEnd method so state
35871
- // is consistent throughout the method call
35872
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35873
- // eslint-disable-next-line no-restricted-syntax
35874
- setTimeout(point.remove.bind(point), 0);
35875
- } else {
35876
- // If this was
35877
- // * a click on the first or last point
35878
- // * and not a deletion
35879
- // * and not a drag,
35880
- // * and not a creation of a new point
35881
- // (see !point.state.isInitialMove, below),
35882
- // * and our polygon is not closed,
35883
- // * and we can close it (we need at least the same
35884
- // number of points as coordinates asked for in the
35885
- // question),
35886
- // then close it
35887
- const isFirstOrLastPoint = point === ___default["default"].head(this.points) || point === ___default["default"].last(this.points);
35888
- const pointNotMoved = !point.hasMoved();
35889
- // NOTE (jsatk): Currently different on mobile versus
35890
- // destkop
35891
- const notANewPoint = !point.state.isInitialMove;
35892
- const polygonNotClosed = !this.polygon?.closed();
35893
- const allPointsPlotted = this.points.length > 2;
35894
- const canCloseUnclosedPolygon = isFirstOrLastPoint && pointNotMoved && notANewPoint && polygonNotClosed && allPointsPlotted;
35895
- if (canCloseUnclosedPolygon) {
35896
- this.polygon?.update({
35897
- closed: true
35898
- });
35899
- this.updatePolygon();
35900
-
35901
- // We finally have a closed polygon, so save our
35902
- // points to props
35903
- this.updateCoordsFromPoints();
35904
- }
35905
- }
35906
- }
35907
-
35908
- // In case we mouseup'd off the graphie and that
35909
- // stopped the move
35910
- this.setTrashCanVisibility(0.5);
35911
- }
35912
- setInitialMove(point, false);
35913
- };
35914
- const graphConstraint = coord => {
35915
- // These constraints are all relative to the other points, so if
35916
- // we're creating the initial points and haven't added any others
35917
- // to the graph, we can't enforce them.
35918
- if (this.points == null || this.points.length === 0) {
35919
- return true;
35920
- }
35921
- const coords = ___default["default"].invoke(this.points, "coord");
35922
- coords[i] = coord;
35923
-
35924
- // Check for invalid positioning, but only if we aren't adding
35925
- // points one click at a time, since those added points could
35926
- // have already violated these constraints
35927
- if (!this.isClickToAddPoints()) {
35928
- // Polygons can't have consecutive collinear points
35929
- if (collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) || collinear(coords[rel(-1)], coords[i], coords[rel(1)]) || collinear(coords[i], coords[rel(1)], coords[rel(2)])) {
35930
- return false;
35931
- }
35932
- const segments = ___default["default"].zip(coords, rotate(coords));
35933
- if (this.points.length > 3) {
35934
- // Constrain to simple (non self-intersecting) polygon by
35935
- // testing whether adjacent segments intersect any others
35936
- for (let j = -1; j <= 0; j++) {
35937
- const segment = segments[rel(j)];
35938
- const others = ___default["default"].without(segments, segment, segments[rel(j - 1)], segments[rel(j + 1)]);
35939
- for (let k = 0; k < others.length; k++) {
35940
- const other = others[k];
35941
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Line'.
35942
- if (intersects(segment, other)) {
35943
- return false;
35944
- }
35945
- }
35946
- }
35947
- }
35948
- }
35949
- if (
35950
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35951
- this.props.graph.snapTo === "angles" && this.points.length > 2) {
35952
- // Snap to whole degree interior angles
35953
-
35954
- const angles = ___default["default"].map(angleMeasures(coords), function (rad) {
35955
- return rad * 180 / Math.PI;
35956
- });
35957
- ___default["default"].each([-1, 1], function (j) {
35958
- angles[rel(j)] = Math.round(angles[rel(j)]);
35959
- });
35960
- const getAngle = function (a, vertex, b) {
35961
- const angle = GraphUtils.findAngleDeprecated(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
35962
- return (angle + 360) % 360;
35963
- };
35964
- const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
35965
- innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
35966
-
35967
- // Avoid degenerate triangles
35968
- if (___default["default"].any(innerAngles, function (angle) {
35969
- return leq(angle, 1);
35970
- })) {
35971
- return false;
35972
- }
35973
- const knownSide = magnitude(
35974
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
35975
- vector(coords[rel(-1)], coords[rel(1)]));
35976
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
35977
-
35978
- // Solve for side by using the law of sines
35979
- const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
35980
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
35981
- const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
35982
- return this.graphie?.addPoints(coords[rel(-1)], offset);
35983
- }
35984
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35985
- if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
35986
- // Snap to whole unit side measures
35987
-
35988
- const sides = ___default["default"].map([[coords[rel(-1)], coords[i]], [coords[i], coords[rel(1)]], [coords[rel(-1)], coords[rel(1)]]], function (coords) {
35989
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35990
- return magnitude(vector(...coords));
35991
- });
35992
- ___default["default"].each([0, 1], function (j) {
35993
- sides[j] = Math.round(sides[j]);
35994
- });
35995
-
35996
- // Avoid degenerate triangles
35997
- if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
35998
- return false;
35999
- }
36000
-
36001
- // Solve for angle by using the law of cosines
36002
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
36003
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
36004
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
36005
- const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36006
- return this.graphie?.addPoints(coords[rel(-1)], offset);
36007
- }
36008
- // Snap to grid (already done)
36009
- return true;
36010
- };
36011
- const point = Interactive2.addMaybeMobileMovablePoint(this, {
36012
- coord: coord,
36013
- constraints: [Interactive2.MovablePoint.constraints.bound(), snapToGrid ? Interactive2.MovablePoint.constraints.snap() : null, graphConstraint],
36014
- onMoveStart: () => {
36015
- if (this.isClickToAddPoints()) {
36016
- this.setTrashCanVisibility(1);
36017
- }
36018
- },
36019
- onMove: () => {
36020
- if (this.polygon?.closed()) {
36021
- this.updateCoordsFromPoints();
36022
- }
36023
- },
36024
- onMoveEnd: onMoveEndHandler,
36025
- ...(this.props.apiOptions.isMobile && this.isClickToAddPoints() ? {
36026
- onRemove: remove
36027
- } : {})
36028
- });
36029
- point.state.isInitialMove = true;
36030
- return point;
36031
- };
36032
- updateCoordsFromPoints = () => {
36033
- const graph = ___default["default"].extend({}, this.props.graph, {
36034
- // Handle old movable points with .coord, or
36035
- // Interactive2.MovablePoint's with .coord()
36036
- coords: ___default["default"].map(this.points, function (point) {
36037
- return ___default["default"].result(point, "coord");
36038
- })
36039
- });
36040
- this.onChange({
36041
- graph: graph
36042
- });
36043
- };
36044
- clearCoords = () => {
36045
- const graph = ___default["default"].extend({}, this.props.graph, {
36046
- coords: null
36047
- });
36048
- this.onChange({
36049
- graph: graph
36050
- });
36051
- };
36052
- onChange = data => {
36053
- this.props.onChange(data);
36054
- this.props.trackInteraction();
36055
- };
36056
- addPointControls = () => {
36057
- if (this.props.graph.type !== "point") {
36058
- throw makeInvalidTypeError("addPointControls", "point");
36059
- }
36060
- const coords = InteractiveGraph.getPointCoords(this.props.graph, this.props);
36061
- // Clear out our old points so that newly added points don't
36062
- // "collide" with them and reposition when being added
36063
- // Without this, when added, each point checks whether it is on top
36064
- // of a point in this.points, which (a) shouldn't matter since
36065
- // we're clearing out this.points anyways, and (b) can cause problems
36066
- // if each of this.points is a MovablePoint instead of an
36067
- // Interactive2.MovablePoint, since one has a .coord and the other
36068
- // has .coord()
36069
- // TODO(jack): Figure out a better way to do this
36070
- this.points = [];
36071
- this.points = ___default["default"].map(coords, this.createPointForPointsType, this);
36072
- };
36073
- removePointControls = () => {
36074
- ___default["default"].invoke(this.points, "remove");
36075
- };
36076
- addSegmentControls = () => {
36077
- if (this.props.graph.type !== "segment") {
36078
- throw makeInvalidTypeError("addSegmentControls", "segment");
36079
- }
36080
- const self = this;
36081
- const graphie = this.graphie;
36082
- const coords = InteractiveGraph.getSegmentCoords(this.props.graph, this.props);
36083
- const createPoint = options => Interactive2.addMaybeMobileMovablePoint(this, options);
36084
- this.points = [];
36085
- this.lines = ___default["default"].map(coords, function (segment, i) {
36086
- const updateCoordProps = function () {
36087
- const graph = ___default["default"].extend({}, self.props.graph, {
36088
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36089
- coords: ___default["default"].invoke(self.lines, "coords")
36090
- });
36091
- self.onChange({
36092
- graph: graph
36093
- });
36094
- };
36095
-
36096
- // NOTE(jeresig): This code attempts to access the variable
36097
- // while it's still being defined, it should be refactored!
36098
- // The callbacks appear to be executed synchronously, which
36099
- // causes this issue.
36100
- const points = [];
36101
- for (let i = 0; i < segment.length; i += 1) {
36102
- const coord = segment[i];
36103
- points.push(createPoint({
36104
- coord: coord,
36105
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36106
- const otherPoint = points[1 - i];
36107
- if (!otherPoint) {
36108
- // point hasn't been defined yet
36109
- // because we're still creating them
36110
- // We return false as the points aren't
36111
- // going to be in the same position at
36112
- // this point.
36113
- return false;
36114
- }
36115
- return !kmath.point.equal(coord, otherPoint.coord());
36116
- }],
36117
- onMove: updateCoordProps
36118
- }));
36119
- }
36120
- self.points = self.points.concat(points);
36121
- const line = Interactive2.addMovableLine(graphie, {
36122
- points: points,
36123
- static: false,
36124
- constraints: [
36125
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36126
- Interactive2.MovableLine.constraints.bound(),
36127
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36128
- Interactive2.MovableLine.constraints.snap()],
36129
- onMove: [
36130
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36131
- Interactive2.MovableLine.onMove.updatePoints, updateCoordProps],
36132
- normalStyle: {
36133
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36134
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36135
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36136
- ...this._lineStroke()
36137
- },
36138
- highlightStyle: {
36139
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36140
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTING,
36141
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36142
- ...this._lineStroke()
36143
- }
36144
- });
36145
- ___default["default"].invoke(points, "toFront");
36146
- return line;
36147
- }, this);
36148
- };
36149
- removeSegmentControls = () => {
36150
- ___default["default"].invoke(this.points, "remove");
36151
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36152
- ___default["default"].invoke(this.lines, "remove");
36153
- };
36154
- addRayControls = () => {
36155
- this.addLine("ray");
36156
- };
36157
- removeRayControls = () => {
36158
- this.removeLine();
36159
- };
36160
- addPolygonControls = () => {
36161
- this.polygon = null;
36162
- const coords = InteractiveGraph.getPolygonCoords(this.props.graph, this.props);
36163
- // Clear out our old points so that newly added points don't
36164
- // "collide", as in `addPointControls`
36165
- this.points = [];
36166
- this.points = ___default["default"].map(coords, this.createPointForPolygonType, this);
36167
- this.updatePolygon();
36168
- };
36169
- updatePolygon = () => {
36170
- if (this.props.graph.type !== "polygon") {
36171
- throw makeInvalidTypeError("updatePolygon", "polygon");
36172
- }
36173
- let closed;
36174
- if (this.polygon) {
36175
- closed = this.polygon.closed();
36176
- } else if (this.points.length >= 3) {
36177
- closed = true;
36178
- } else {
36179
- // There will only be fewer than 3 points in click-to-add-vertices
36180
- // mode, so we don't need to explicitly check for that here.
36181
- closed = false;
36182
- }
36183
- const graphie = this.graphie;
36184
- const n = this.points.length;
36185
-
36186
- // TODO(alex): check against "grid" instead, use constants
36187
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
36188
- const angleLabels = ___default["default"].times(n, function (i) {
36189
- if (
36190
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36191
- !this.props.graph.showAngles || !closed && (i === 0 || i === n - 1)) {
36192
- return "";
36193
- }
36194
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36195
- if (this.props.graph.snapTo === "angles") {
36196
- return "$deg0";
36197
- }
36198
- return "$deg1";
36199
- }, this);
36200
- const showRightAngleMarkers = ___default["default"].times(n, function (i) {
36201
- return closed || i !== 0 && i !== n - 1;
36202
- }, this);
36203
- const numArcs = ___default["default"].times(n, function (i) {
36204
- if (
36205
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36206
- this.props.graph.showAngles && (closed || i !== 0 && i !== n - 1)) {
36207
- return 1;
36208
- }
36209
- return 0;
36210
- }, this);
36211
- const sideLabels = ___default["default"].times(n, function (i) {
36212
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36213
- if (!this.props.graph.showSides || !closed && i === n - 1) {
36214
- return "";
36215
- }
36216
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36217
- if (this.props.graph.snapTo === "sides") {
36218
- return "$len0";
36219
- }
36220
- return "$len1";
36221
- }, this);
36222
- if (this.polygon == null) {
36223
- const self = this;
36224
- self.polygon = Interactive2.addMovablePolygon(graphie, {
36225
- constraints: [
36226
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36227
- Interactive2.MovablePolygon.constraints.bound(), snapToGrid ?
36228
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36229
- Interactive2.MovablePolygon.constraints.snap() : null],
36230
- closed: closed,
36231
- points: self.points,
36232
- angleLabels: angleLabels,
36233
- showRightAngleMarkers: showRightAngleMarkers,
36234
- numArcs: numArcs,
36235
- sideLabels: sideLabels,
36236
- onMove: [
36237
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36238
- Interactive2.MovablePolygon.onMove.updatePoints, function () {
36239
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36240
- if (this.closed()) {
36241
- self.updateCoordsFromPoints();
36242
- }
36243
- }],
36244
- normalStyle: {
36245
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36246
- ...this._lineStroke()
36247
- }
36248
- });
36249
- } else {
36250
- // We only need to pass in the properties that might've changed
36251
- this.polygon.update({
36252
- closed: closed,
36253
- points: this.points,
36254
- angleLabels: angleLabels,
36255
- showRightAngleMarkers: showRightAngleMarkers,
36256
- numArcs: numArcs,
36257
- sideLabels: sideLabels
36258
- });
36259
- }
36260
- };
36261
- removePolygonControls = () => {
36262
- ___default["default"].invoke(this.points, "remove");
36263
- this.polygon?.remove();
36264
- };
36265
- addAngleControls = () => {
36266
- if (this.props.graph.type !== "angle") {
36267
- throw makeInvalidTypeError("addAngleControls", "angle");
36268
- }
36269
- const graph = this.props.graph;
36270
- const graphie = this.graphie;
36271
- const coords = InteractiveGraph.getAngleCoords(this.props.graph, this.props);
36272
-
36273
- // The vertex snaps to the grid, but the rays don't...
36274
- this.points = coords.map(function (coord, i) {
36275
- return graphie?.addMovablePoint(___default["default"].extend({
36276
- coord: coord,
36277
- normalStyle: {
36278
- stroke: KhanColors.INTERACTIVE,
36279
- fill: KhanColors.INTERACTIVE
36280
- }
36281
- }, i === 1 ? {
36282
- snapX: graphie.snap[0],
36283
- snapY: graphie.snap[1]
36284
- } : {}));
36285
- });
36286
-
36287
- // ...they snap to whole-degree angles from the vertex.
36288
- this.angle = graphie?.addMovableAngle({
36289
- points: this.points,
36290
- snapDegrees: graph.snapDegrees || 1,
36291
- snapOffsetDeg: graph.angleOffsetDeg || 0,
36292
- angleLabel: graph.showAngles ? "$deg0" : "",
36293
- pushOut: 2,
36294
- allowReflex: defaultVal(graph.allowReflexAngles, true)
36295
- });
36296
- $__default["default"](this.angle).on("move", () => {
36297
- this.onChange({
36298
- graph: {
36299
- ...graph,
36300
- coords: this.angle?.getClockwiseCoords()
36301
- }
36302
- });
36303
- });
36304
- };
36305
- removeAngleControls = () => {
36306
- ___default["default"].invoke(this.points, "remove");
36307
- this.angle?.remove();
36308
- };
36309
- toggleShowSides = () => {
36310
- if (this.props.graph.type !== "polygon") {
36311
- throw makeInvalidTypeError("toggleShowSides", "polygon");
36312
- }
36313
- const graph = ___default["default"].extend({}, this.props.graph, {
36314
- showSides: !this.props.graph.showSides
36315
- });
36316
- this.onChange({
36317
- graph: graph
36318
- });
36319
- };
36320
- getUserInput() {
36321
- return InteractiveGraph.getUserInputFromProps(this.props);
36322
- }
36323
- focus = $__default["default"].noop;
36324
- render() {
36325
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36326
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36327
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36328
- let instructions;
36329
- // isClickToAddPoints() only applies to points and polygons
36330
- if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
36331
- if (this.props.graph.type === "point") {
36332
- instructions = this.context.strings.addPoints;
36333
- } else if (this.props.graph.type === "polygon") {
36334
- instructions = this.context.strings.addVertices;
36335
- }
36336
- } else {
36337
- instructions = undefined;
36338
- }
36339
- let onMouseDown;
36340
- if (this.isClickToAddPoints()) {
36341
- onMouseDown = this.handleAddPointsMouseDown;
36342
- }
36343
- const isMobile = this.props.apiOptions.isMobile;
36344
- return /*#__PURE__*/React__namespace.createElement("div", {
36345
- className: "perseus-widget " + "perseus-widget-interactive-graph",
36346
- style: {
36347
- width: box[0],
36348
- height: box[1]
36349
- }
36350
- }, /*#__PURE__*/React__namespace.createElement(Graph, {
36351
- instructions: instructions
36352
- // eslint-disable-next-line react/no-string-refs
36353
- ,
36354
- ref: "graph",
36355
- box: box,
36356
- labels: this.props.labels,
36357
- range: this.props.range,
36358
- step: isMobile ? Util.constrainedTickStepsFromTickSteps(this.props.step, this.props.range) : this.props.step,
36359
- gridStep: gridStep,
36360
- snapStep: snapStep,
36361
- markings: this.props.markings,
36362
- backgroundImage: this.props.backgroundImage,
36363
- showProtractor: this.props.showProtractor,
36364
- onMouseDown: onMouseDown,
36365
- onGraphieUpdated: this.setGraphie,
36366
- setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
36367
- isMobile: isMobile
36368
- }));
36369
- }
36370
- }
36371
35532
  class InteractiveGraph extends React__namespace.Component {
36372
35533
  legacyGraphRef = /*#__PURE__*/React__namespace.createRef();
36373
35534
  mafsRef = /*#__PURE__*/React__namespace.createRef();
@@ -36396,25 +35557,17 @@ class InteractiveGraph extends React__namespace.Component {
36396
35557
  return getPromptJSON$b(this.props, this.getUserInput());
36397
35558
  }
36398
35559
  render() {
36399
- // Mafs shim
36400
- const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
36401
- if (shouldUseMafs(mafsFlags, this.props.graph)) {
36402
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36403
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36404
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36405
- return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
36406
- flags: this.props.apiOptions?.flags,
36407
- ref: this.mafsRef,
36408
- gridStep: gridStep,
36409
- snapStep: snapStep,
36410
- box: box,
36411
- showTooltips: !!this.props.showTooltips,
36412
- readOnly: this.props.apiOptions?.readOnly
36413
- }));
36414
- }
36415
- return /*#__PURE__*/React__namespace.createElement(LegacyInteractiveGraph, _extends({
36416
- ref: this.legacyGraphRef
36417
- }, this.props));
35560
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
35561
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
35562
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
35563
+ return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
35564
+ ref: this.mafsRef,
35565
+ gridStep: gridStep,
35566
+ snapStep: snapStep,
35567
+ box: box,
35568
+ showTooltips: !!this.props.showTooltips,
35569
+ readOnly: this.props.apiOptions?.readOnly
35570
+ }));
36418
35571
  }
36419
35572
 
36420
35573
  /**
@@ -36730,35 +35883,14 @@ class InteractiveGraph extends React__namespace.Component {
36730
35883
  }
36731
35884
  }
36732
35885
 
36733
- // exported for testing
36734
- function shouldUseMafs(mafsFlags, graph) {
36735
- if (typeof mafsFlags === "boolean" || typeof mafsFlags === "undefined") {
36736
- return false;
36737
- }
36738
- switch (graph.type) {
36739
- case "none":
36740
- return true;
36741
- case "point":
36742
- if (graph.numPoints === UNLIMITED) {
36743
- return Boolean(mafsFlags["unlimited-point"]);
36744
- }
36745
- return Boolean(mafsFlags["point"]);
36746
- case "polygon":
36747
- if (graph.numSides === UNLIMITED) {
36748
- return Boolean(mafsFlags["unlimited-polygon"]);
36749
- }
36750
- return Boolean(mafsFlags["polygon"]);
36751
- default:
36752
- return Boolean(mafsFlags[graph.type]);
36753
- }
36754
- }
36755
35886
  // We don't need to change any of the original props for static mode
36756
35887
  const staticTransform$3 = ___default["default"].identity;
36757
35888
  var InteractiveGraph$1 = {
36758
35889
  name: "interactive-graph",
36759
35890
  displayName: "Interactive graph (Assessments only)",
36760
35891
  widget: InteractiveGraph,
36761
- staticTransform: staticTransform$3
35892
+ staticTransform: staticTransform$3,
35893
+ getPublicWidgetOptions: perseusCore.getInteractiveGraphPublicWidgetOptions
36762
35894
  };
36763
35895
 
36764
35896
  // TODO: can we use WB typography?
@@ -39047,7 +38179,8 @@ var Matrix$1 = {
39047
38179
  widget: Matrix,
39048
38180
  transform: propTransform$1,
39049
38181
  staticTransform: staticTransform$2,
39050
- isLintable: true
38182
+ isLintable: true,
38183
+ getPublicWidgetOptions: perseusCore.getMatrixPublicWidgetOptions
39051
38184
  };
39052
38185
 
39053
38186
  const getPromptJSON$7 = () => {
@@ -42243,7 +41376,8 @@ var Plotter$1 = {
42243
41376
  displayName: "Plotter",
42244
41377
  hidden: true,
42245
41378
  widget: Plotter,
42246
- staticTransform: staticTransform
41379
+ staticTransform: staticTransform,
41380
+ getPublicWidgetOptions: perseusCore.getPlotterPublicWidgetOptions
42247
41381
  };
42248
41382
 
42249
41383
  const getPromptJSON$2 = () => {
@@ -42620,7 +41754,8 @@ var Table$1 = {
42620
41754
  widget: Table,
42621
41755
  transform: propTransform,
42622
41756
  hidden: true,
42623
- isLintable: true
41757
+ isLintable: true,
41758
+ getPublicWidgetOptions: perseusCore.getTablePublicWidgetOptions
42624
41759
  };
42625
41760
 
42626
41761
  const getPromptJSON = () => {
@@ -42801,7 +41936,7 @@ const init = function () {
42801
41936
 
42802
41937
  // This file is processed by a Rollup plugin (replace) to inject the production
42803
41938
  const libName = "@khanacademy/perseus";
42804
- const libVersion = "53.0.0";
41939
+ const libVersion = "53.1.0";
42805
41940
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
42806
41941
 
42807
41942
  const apiVersion = {