@khanacademy/perseus 21.0.0 → 21.2.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
@@ -3281,6 +3281,15 @@ const textarea = {
3281
3281
  */
3282
3282
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
3283
3283
  const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
3284
+
3285
+ // TODO(benchristel): in the future, we may want to make deepClone work for
3286
+ // Record<string, Cloneable> as well. Currently, it only does arrays.
3287
+ function deepClone(obj) {
3288
+ if (Array.isArray(obj)) {
3289
+ return obj.map(deepClone);
3290
+ }
3291
+ return obj;
3292
+ }
3284
3293
  const Util = {
3285
3294
  inputPathsEqual,
3286
3295
  nestedMap: nestedMap$2,
@@ -3326,7 +3335,8 @@ const Util = {
3326
3335
  getDataUrl: getDataUrl,
3327
3336
  textarea,
3328
3337
  unescapeMathMode: unescapeMathMode$1,
3329
- random: random$1
3338
+ random: random$1,
3339
+ deepClone
3330
3340
  };
3331
3341
 
3332
3342
  // Derived from the MIT-licensed:
@@ -25026,9 +25036,22 @@ class Explanation extends React__namespace.Component {
25026
25036
  // Special styling is needed to fit the button in a block of text without throwing off the line spacing.
25027
25037
  // While the button is not normally included in a block of text, it needs to be able to accommodate such a case.
25028
25038
  const buttonStyleOverrides = {
25029
- height: "22px",
25039
+ height: "auto",
25030
25040
  marginLeft: "-2px",
25031
- padding: "0 2px"
25041
+ // transfer space on the left side to the padding
25042
+ marginRight: "2px",
25043
+ // adds space between the right border and any text that follows
25044
+ paddingLeft: "2px" // adds space between the left border and the button text
25045
+ };
25046
+
25047
+ const labelStyle = {
25048
+ fontSize: "18px",
25049
+ lineHeight: "20px",
25050
+ // The following property adjusts the large space between the button text and the caret icon.
25051
+ // Since we are unable to adjust the styling of the icon (where the extra space exists),
25052
+ // we are adjusting it on the text label by using a negative margin.
25053
+ marginRight: "-6px",
25054
+ "white-space": "normal"
25032
25055
  };
25033
25056
  const contentStyling = [styles$p.content, this.state.expanded ? styles$p.contentExpanded : styles$p.contentCollapsed, allowTransition && (this.state.expanded ? styles$p.transitionExpanded : styles$p.transitionCollapsed)];
25034
25057
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
@@ -25039,8 +25062,9 @@ class Explanation extends React__namespace.Component {
25039
25062
  "aria-controls": ids.get("content"),
25040
25063
  endIcon: caretIcon,
25041
25064
  kind: "tertiary",
25065
+ labelStyle: labelStyle,
25042
25066
  onClick: this._onClick,
25043
- size: "large",
25067
+ size: "small",
25044
25068
  style: buttonStyleOverrides
25045
25069
  }, promptText), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
25046
25070
  id: ids.get("content"),
@@ -28342,6 +28366,36 @@ const moveAll = delta => ({
28342
28366
  type: MOVE_ALL,
28343
28367
  delta
28344
28368
  });
28369
+ const MOVE_POINT = "move-point";
28370
+ function movePoint(index, destination) {
28371
+ return {
28372
+ type: MOVE_POINT,
28373
+ index,
28374
+ destination
28375
+ };
28376
+ }
28377
+ const CHANGE_SNAP_STEP = "change-snap-step";
28378
+ function changeSnapStep(snapStep) {
28379
+ return {
28380
+ type: CHANGE_SNAP_STEP,
28381
+ snapStep
28382
+ };
28383
+ }
28384
+ const CHANGE_RANGE = "change-range";
28385
+ function changeRange(range) {
28386
+ return {
28387
+ type: CHANGE_RANGE,
28388
+ range
28389
+ };
28390
+ }
28391
+
28392
+ const GraphStateContext = /*#__PURE__*/React.createContext({
28393
+ state: {},
28394
+ dispatch: () => {}
28395
+ });
28396
+ function useGraphState() {
28397
+ return React__default["default"].useContext(GraphStateContext);
28398
+ }
28345
28399
 
28346
28400
  /**
28347
28401
  * 44 is touch best practice and AAA compliant for WCAG
@@ -28368,6 +28422,11 @@ const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.
28368
28422
  const extent = ranges[i][1] - ranges[i][0];
28369
28423
  return (coord + ranges[i][1]) / extent;
28370
28424
  }));
28425
+ function snap(snapStep, point) {
28426
+ const [requestedX, requestedY] = point;
28427
+ const [snapX, snapY] = snapStep;
28428
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28429
+ }
28371
28430
 
28372
28431
  const useTransform = function () {
28373
28432
  const {
@@ -28426,6 +28485,9 @@ const getLines = points => points.map((point, i) => {
28426
28485
 
28427
28486
  const defaultStroke = "var(--movable-line-stroke-color)";
28428
28487
  const MovableLine = props => {
28488
+ const {
28489
+ state
28490
+ } = useGraphState();
28429
28491
  const {
28430
28492
  start,
28431
28493
  end,
@@ -28450,7 +28512,7 @@ const MovableLine = props => {
28450
28512
  onMove: newPoint => {
28451
28513
  onMove(mafs.vec.sub(newPoint, midpoint));
28452
28514
  },
28453
- constrain: p => p
28515
+ constrain: p => snap(state.snapStep, p)
28454
28516
  });
28455
28517
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("g", {
28456
28518
  ref: line,
@@ -28501,7 +28563,13 @@ function SVGLine(props) {
28501
28563
  });
28502
28564
  }
28503
28565
 
28566
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
28567
+ // graph.
28568
+ const hitboxSizePx = 48;
28504
28569
  const StyledMovablePoint = props => {
28570
+ const {
28571
+ state
28572
+ } = useGraphState();
28505
28573
  const hitboxRef = React.useRef(null);
28506
28574
  const {
28507
28575
  point,
@@ -28514,7 +28582,7 @@ const StyledMovablePoint = props => {
28514
28582
  gestureTarget: hitboxRef,
28515
28583
  point,
28516
28584
  onMove,
28517
- constrain: p => p
28585
+ constrain: p => snap(state.snapStep, p)
28518
28586
  });
28519
28587
  const [[x, y]] = useTransform(point);
28520
28588
  return /*#__PURE__*/React__namespace.createElement("g", {
@@ -28529,7 +28597,7 @@ const StyledMovablePoint = props => {
28529
28597
  }
28530
28598
  }, /*#__PURE__*/React__namespace.createElement("circle", {
28531
28599
  className: "movable-point-hitbox",
28532
- r: TARGET_SIZE / 2,
28600
+ r: hitboxSizePx / 2,
28533
28601
  cx: x,
28534
28602
  cy: y
28535
28603
  }), /*#__PURE__*/React__namespace.createElement("circle", {
@@ -28567,7 +28635,9 @@ const SegmentGraph = props => {
28567
28635
  onMoveLine: delta => {
28568
28636
  dispatch(moveLine(i, delta));
28569
28637
  },
28570
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
28638
+ onMovePoint: (endpointIndex, destination) => {
28639
+ dispatch(moveControlPoint(endpointIndex, destination, i));
28640
+ },
28571
28641
  "data-testid": "segment" + i
28572
28642
  })));
28573
28643
  };
@@ -28863,7 +28933,8 @@ const PolygonGraph = props => {
28863
28933
  type,
28864
28934
  showAngles,
28865
28935
  showSides,
28866
- range
28936
+ range,
28937
+ snapStep
28867
28938
  } = props.graphState;
28868
28939
  const points = coords ?? [[0, 0]];
28869
28940
  const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
@@ -28878,7 +28949,7 @@ const PolygonGraph = props => {
28878
28949
  const delta = mafs.vec.sub(newPoint, midpoint);
28879
28950
  dispatch(moveAll(delta));
28880
28951
  },
28881
- constrain: p => p
28952
+ constrain: p => snap(snapStep, p)
28882
28953
  });
28883
28954
  const active = hovered || focused || dragging;
28884
28955
  const lines = getLines(points);
@@ -28931,25 +29002,181 @@ const PolygonGraph = props => {
28931
29002
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
28932
29003
  key: "point-" + i,
28933
29004
  point: point,
28934
- onMove: destination => dispatch(moveControlPoint(i, destination)),
29005
+ onMove: destination => dispatch(movePoint(i, destination)),
28935
29006
  "data-testid": type + i
28936
29007
  })));
28937
29008
  };
28938
29009
 
28939
- const renderLineLabel = (n, _ref) => {
28940
- let [min, max] = _ref;
28941
- return n !== -1 && n !== min && n !== max;
29010
+ function PointGraph(props) {
29011
+ const {
29012
+ dispatch
29013
+ } = props;
29014
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, props.graphState.coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29015
+ key: i,
29016
+ point: point,
29017
+ onMove: destination => dispatch(movePoint(i, destination))
29018
+ })));
29019
+ }
29020
+
29021
+ function Arrow(props) {
29022
+ const {
29023
+ userTransform,
29024
+ viewTransform
29025
+ } = mafs.useTransformContext();
29026
+ const point = [props.x, props.y];
29027
+ const userTransformedPoint = mafs.vec.transform(point, userTransform);
29028
+ const viewTransformedPoint = mafs.vec.transform(userTransformedPoint, viewTransform);
29029
+ return /*#__PURE__*/React__default["default"].createElement("g", {
29030
+ transform: `translate(${viewTransformedPoint[0]} ${viewTransformedPoint[1]}) rotate(${props.rotate}) scale(1.25)`
29031
+ }, /*#__PURE__*/React__default["default"].createElement("g", {
29032
+ transform: "translate(-1)"
29033
+ }, /*#__PURE__*/React__default["default"].createElement("path", {
29034
+ d: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
29035
+ fill: "none"
29036
+ })));
29037
+ }
29038
+ function AxisArrows() {
29039
+ const {
29040
+ state
29041
+ } = useGraphState();
29042
+ const range = state.range;
29043
+ const [xMin, xMax] = range[0];
29044
+ const [yMin, yMax] = range[1];
29045
+ return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement(Arrow, {
29046
+ x: xMax,
29047
+ y: 0,
29048
+ rotate: 0
29049
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29050
+ x: 0,
29051
+ y: yMin,
29052
+ rotate: 90
29053
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29054
+ x: xMin,
29055
+ y: 0,
29056
+ rotate: 180
29057
+ }), /*#__PURE__*/React__default["default"].createElement(Arrow, {
29058
+ x: 0,
29059
+ y: yMax,
29060
+ rotate: 270
29061
+ }));
29062
+ }
29063
+
29064
+ const tickSize = 10;
29065
+ const tickStyle = {
29066
+ stroke: "black",
29067
+ strokeWidth: 1
29068
+ };
29069
+ const YGridTick = _ref => {
29070
+ let {
29071
+ y
29072
+ } = _ref;
29073
+ const pointOnAxis = [0, y];
29074
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29075
+ return /*#__PURE__*/React__namespace.createElement("g", {
29076
+ className: "y-axis-ticks"
29077
+ }, /*#__PURE__*/React__namespace.createElement("line", {
29078
+ x1: xPosition - tickSize / 2,
29079
+ y1: yPosition,
29080
+ x2: xPosition + tickSize / 2,
29081
+ y2: yPosition,
29082
+ style: tickStyle
29083
+ }));
29084
+ };
29085
+ const XGridTick = _ref2 => {
29086
+ let {
29087
+ x
29088
+ } = _ref2;
29089
+ const pointOnAxis = [x, 0];
29090
+ const [[xPosition, yPosition]] = useTransform(pointOnAxis);
29091
+ return /*#__PURE__*/React__namespace.createElement("g", {
29092
+ className: "x-axis-ticks"
29093
+ }, /*#__PURE__*/React__namespace.createElement("line", {
29094
+ x1: xPosition,
29095
+ y1: yPosition + tickSize / 2,
29096
+ x2: xPosition,
29097
+ y2: yPosition - tickSize / 2,
29098
+ style: tickStyle
29099
+ }));
29100
+ };
29101
+ function generateTickLocations(tickStep, min, max) {
29102
+ const ticks = [];
29103
+ for (let i = 0 + tickStep; i < max; i += tickStep) {
29104
+ ticks.push(i);
29105
+ }
29106
+ for (let i = 0 - tickStep; i > min; i -= tickStep) {
29107
+ ticks.push(i);
29108
+ }
29109
+ return ticks;
29110
+ }
29111
+ const AxisTicks = props => {
29112
+ const range = props.range;
29113
+ const [xMin, xMax] = range[0];
29114
+ const [yMin, yMax] = range[1];
29115
+ const yTickStep = props.tickStep[1];
29116
+ const xTickStep = props.tickStep[0];
29117
+ const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
29118
+ const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
29119
+ return /*#__PURE__*/React__namespace.createElement("g", {
29120
+ className: "axis-ticks"
29121
+ }, yGridTicks.map(y => {
29122
+ return /*#__PURE__*/React__namespace.createElement(YGridTick, {
29123
+ y: y,
29124
+ key: `y-grid-tick-${y}`
29125
+ });
29126
+ }), xGridTicks.map(x => {
29127
+ return /*#__PURE__*/React__namespace.createElement(XGridTick, {
29128
+ x: x,
29129
+ key: `x-grid-tick-${x}`
29130
+ });
29131
+ }));
29132
+ };
29133
+
29134
+ /**
29135
+ * lineLabelText get the text that should
29136
+ * be displayed for a specific tick place
29137
+ *
29138
+ * @param {number} n the tick number
29139
+ * @param {number} tickStep how frequently a label should appear
29140
+ * @param {vec.Vector2} range the min/max range on the axis
29141
+ */
29142
+ const lineLabelText = (n, tickStep, range) => {
29143
+ const [min, max] = range;
29144
+ const isOnStep = n % tickStep === 0;
29145
+ const isNegativeOne = n === -1;
29146
+ const isMin = n === min;
29147
+ const isMax = n === max;
29148
+ const shouldRender = isOnStep && !isNegativeOne && !isMin && !isMax;
29149
+ return shouldRender ? `${n}` : "";
29150
+ };
29151
+
29152
+ /**
29153
+ * axisOptions determine axis options for Mafs
29154
+ *
29155
+ * axisIndex is for grabbing data in an array that contains
29156
+ * data for multiple axes. For example range: [[-10, 10], [-10, 10]]
29157
+ * range[0] is data for the x axis and range[1] is data for the y axis
29158
+ *
29159
+ * @param {GridProps} props
29160
+ * @param {number} axisIndex which axis we're getting options for
29161
+ */
29162
+ const axisOptions = (props, axisIndex) => {
29163
+ const axisStep = props.tickStep[axisIndex];
29164
+ const axisRange = props.range[axisIndex];
29165
+ return {
29166
+ axis: props.markings === "graph",
29167
+ lines: props.gridStep[axisIndex],
29168
+ labels: n => lineLabelText(n, axisStep, axisRange)
29169
+ };
29170
+ };
29171
+ const Grid = props => {
29172
+ return props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
29173
+ xAxis: axisOptions(props, 0),
29174
+ yAxis: axisOptions(props, 1)
29175
+ }), /*#__PURE__*/React__namespace.createElement(AxisTicks, {
29176
+ range: props.range,
29177
+ tickStep: props.tickStep
29178
+ }), props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(AxisArrows, null));
28942
29179
  };
28943
- const axisOptions = (props, index) => ({
28944
- axis: props.markings === "graph",
28945
- lines: props.step[index],
28946
- subdivisions: props.step[index] / props.gridStep[index],
28947
- labels: n => renderLineLabel(n, props.range[index]) ? n : ""
28948
- });
28949
- const Grid = props => props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
28950
- xAxis: axisOptions(props, 0),
28951
- yAxis: axisOptions(props, 1)
28952
- });
28953
29180
 
28954
29181
  // Displays a stylized open eye: lint warnings are visible
28955
29182
  const VisibleIcon = () => /*#__PURE__*/React__namespace.createElement("svg", {
@@ -29647,53 +29874,50 @@ const LegacyGrid = _ref => {
29647
29874
  return null;
29648
29875
  };
29649
29876
 
29650
- /** Determine if coords is type CollinearTuple[] */
29651
- const isCollinearTuples = coords => Array.isArray(coords[0][0]);
29652
-
29653
- // Generic type makes returned state match input state
29654
29877
  function interactiveGraphReducer(state, action) {
29878
+ switch (action.type) {
29879
+ case MOVE_CONTROL_POINT:
29880
+ return doMoveControlPoint(state, action);
29881
+ case MOVE_LINE:
29882
+ return doMoveLine(state, action);
29883
+ case MOVE_ALL:
29884
+ return doMoveAll(state, action);
29885
+ case MOVE_POINT:
29886
+ return doMovePoint(state, action);
29887
+ case CHANGE_SNAP_STEP:
29888
+ return doChangeSnapStep(state, action);
29889
+ case CHANGE_RANGE:
29890
+ return doChangeRange(state, action);
29891
+ default:
29892
+ throw new wonderStuffCore.UnreachableCaseError(action);
29893
+ }
29894
+ }
29895
+ function doMoveControlPoint(state, action) {
29655
29896
  const {
29656
29897
  snapStep,
29657
29898
  range
29658
29899
  } = state;
29659
- if (!state.coords) {
29660
- throw new Error("Graph state must have been initialized with coords");
29661
- }
29662
- switch (action.type) {
29663
- case MOVE_CONTROL_POINT:
29900
+ switch (state.type) {
29901
+ case "segment":
29902
+ case "linear":
29903
+ case "linear-system":
29904
+ case "ray":
29664
29905
  {
29665
- const newCoords = action.itemIndex !== undefined && state.coords && isCollinearTuples(state.coords) ? updateAtIndex({
29906
+ const newCoords = updateAtIndex({
29666
29907
  array: state.coords,
29667
29908
  index: action.itemIndex,
29668
29909
  update: tuple => setAtIndex({
29669
29910
  array: tuple,
29670
29911
  index: action.pointIndex,
29671
- newValue: snap({
29672
- snapStep,
29673
- point: bound$1({
29674
- snapStep,
29675
- range,
29676
- point: action.destination
29677
- })
29678
- })
29679
- })
29680
- }) : setAtIndex({
29681
- array: state.coords,
29682
- index: action.pointIndex,
29683
- newValue: snap({
29684
- snapStep,
29685
- point: bound$1({
29912
+ newValue: snap(snapStep, bound$1({
29686
29913
  snapStep,
29687
29914
  range,
29688
29915
  point: action.destination
29689
- })
29916
+ }))
29690
29917
  })
29691
29918
  });
29692
-
29693
- // Cannot type narrow both conditions within function parameters,
29694
- // so this may seem redundant, but it's necessary for type safety.
29695
- const coordsToCheck = isCollinearTuples(newCoords) && action.itemIndex !== undefined ? newCoords[action.itemIndex] : newCoords;
29696
- if (!isCollinearTuples(coordsToCheck) && coordsOverlap(coordsToCheck)) {
29919
+ const coordsToCheck = newCoords[action.itemIndex];
29920
+ if (coordsOverlap(coordsToCheck)) {
29697
29921
  return state;
29698
29922
  }
29699
29923
  return {
@@ -29702,15 +29926,30 @@ function interactiveGraphReducer(state, action) {
29702
29926
  coords: newCoords
29703
29927
  };
29704
29928
  }
29705
- case MOVE_LINE:
29929
+ case "circle":
29930
+ throw new Error("FIXME implement circle reducer");
29931
+ case "point":
29932
+ case "polygon":
29933
+ throw new Error(`Don't use moveControlPoint for ${state.type} graphs. Use movePoint instead!`);
29934
+ default:
29935
+ throw new wonderStuffCore.UnreachableCaseError(state);
29936
+ }
29937
+ }
29938
+ function doMoveLine(state, action) {
29939
+ const {
29940
+ snapStep,
29941
+ range
29942
+ } = state;
29943
+ switch (state.type) {
29944
+ case "segment":
29945
+ case "linear":
29946
+ case "linear-system":
29947
+ case "ray":
29706
29948
  {
29707
- if (!isCollinearTuples(state.coords)) {
29708
- throw new Error("Cannot call this action unless coords is array of CollinearTuple.");
29709
- }
29710
29949
  if (action.itemIndex === undefined) {
29711
29950
  throw new Error("Please provide index of line to move");
29712
29951
  }
29713
- const currentLine = state.coords?.[action.itemIndex];
29952
+ const currentLine = state.coords[action.itemIndex];
29714
29953
  if (!currentLine) {
29715
29954
  throw new Error("No line to move");
29716
29955
  }
@@ -29718,13 +29957,7 @@ function interactiveGraphReducer(state, action) {
29718
29957
  snapStep,
29719
29958
  range
29720
29959
  });
29721
- const newLine = [snap({
29722
- snapStep,
29723
- point: mafs.vec.add(currentLine[0], change)
29724
- }), snap({
29725
- snapStep,
29726
- point: mafs.vec.add(currentLine[1], change)
29727
- })];
29960
+ const newLine = [snap(snapStep, mafs.vec.add(currentLine[0], change)), snap(snapStep, mafs.vec.add(currentLine[1], change))];
29728
29961
  const newCoords = setAtIndex({
29729
29962
  array: state.coords,
29730
29963
  index: action.itemIndex,
@@ -29732,23 +29965,30 @@ function interactiveGraphReducer(state, action) {
29732
29965
  });
29733
29966
  return {
29734
29967
  ...state,
29968
+ type: state.type,
29735
29969
  hasBeenInteractedWith: true,
29736
29970
  coords: newCoords
29737
29971
  };
29738
29972
  }
29739
- case MOVE_ALL:
29973
+ default:
29974
+ // The MoveLine action doesn't make sense for other graph types;
29975
+ // ignore it if it somehow happens
29976
+ return state;
29977
+ }
29978
+ }
29979
+ function doMoveAll(state, action) {
29980
+ const {
29981
+ snapStep,
29982
+ range
29983
+ } = state;
29984
+ switch (state.type) {
29985
+ case "polygon":
29740
29986
  {
29741
- if (isCollinearTuples(state.coords)) {
29742
- throw new Error("Cannot call this action unless coords is array of vectors.");
29743
- }
29744
29987
  const change = getChange(state.coords, action.delta, {
29745
29988
  snapStep,
29746
29989
  range
29747
29990
  });
29748
- const newCoords = state.coords.map(point => snap({
29749
- snapStep,
29750
- point: mafs.vec.add(point, change)
29751
- }));
29991
+ const newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
29752
29992
  return {
29753
29993
  ...state,
29754
29994
  hasBeenInteractedWith: true,
@@ -29756,8 +29996,54 @@ function interactiveGraphReducer(state, action) {
29756
29996
  };
29757
29997
  }
29758
29998
  default:
29759
- throw new wonderStuffCore.UnreachableCaseError(action);
29999
+ // MoveAll is not supported for other state types; just ignore it.
30000
+ return state;
30001
+ }
30002
+ }
30003
+ function doMovePoint(state, action) {
30004
+ switch (state.type) {
30005
+ case "point":
30006
+ case "polygon":
30007
+ {
30008
+ return {
30009
+ ...state,
30010
+ hasBeenInteractedWith: true,
30011
+ coords: setAtIndex({
30012
+ array: state.coords,
30013
+ index: action.index,
30014
+ newValue: snap(state.snapStep, bound$1({
30015
+ snapStep: state.snapStep,
30016
+ range: state.range,
30017
+ point: action.destination
30018
+ }))
30019
+ })
30020
+ };
30021
+ }
30022
+ default:
30023
+ throw new Error("The movePoint action is only for point and polygon graphs");
30024
+ }
30025
+ }
30026
+ function doChangeSnapStep(state, action) {
30027
+ if (
30028
+ // Deep equality check since these are arrays
30029
+ ___default["default"].isEqual(state.snapStep, action.snapStep)) {
30030
+ return state;
30031
+ }
30032
+ return {
30033
+ ...state,
30034
+ snapStep: action.snapStep
30035
+ };
30036
+ }
30037
+ function doChangeRange(state, action) {
30038
+ if (
30039
+ // Deep equality check since these are arrays
30040
+ ___default["default"].isEqual(state.range, action.range)) {
30041
+ return state;
29760
30042
  }
30043
+ return {
30044
+ ...state,
30045
+ range: action.range
30046
+ };
29761
30047
  }
29762
30048
  const getChange = (coords, delta, constraintOpts) => {
29763
30049
  const [deltaX, deltaY] = delta;
@@ -29777,24 +30063,14 @@ const getChange = (coords, delta, constraintOpts) => {
29777
30063
  const dy = clamp(deltaY, minYMove, maxYMove);
29778
30064
  return [dx, dy];
29779
30065
  };
29780
- function snap(_ref) {
29781
- let {
29782
- snapStep,
29783
- point
29784
- } = _ref;
29785
- const [requestedX, requestedY] = point;
29786
- const [snapX, snapY] = snapStep;
29787
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29788
- }
29789
-
29790
30066
  // Returns the closest point to the given `point` that is within the graph
29791
30067
  // bounds given in `state`.
29792
- function bound$1(_ref2) {
30068
+ function bound$1(_ref) {
29793
30069
  let {
29794
30070
  snapStep,
29795
30071
  range,
29796
30072
  point
29797
- } = _ref2;
30073
+ } = _ref;
29798
30074
  const [requestedX, requestedY] = point;
29799
30075
  const [snapX, snapY] = snapStep;
29800
30076
  const [[minX, maxX], [minY, maxY]] = range;
@@ -29802,12 +30078,12 @@ function bound$1(_ref2) {
29802
30078
  }
29803
30079
 
29804
30080
  // Returns the vector from the given point to the top-right corner of the graph
29805
- function maxMove(_ref3) {
30081
+ function maxMove(_ref2) {
29806
30082
  let {
29807
30083
  snapStep,
29808
30084
  range,
29809
30085
  point
29810
- } = _ref3;
30086
+ } = _ref2;
29811
30087
  const topRight = bound$1({
29812
30088
  snapStep,
29813
30089
  range,
@@ -29818,12 +30094,12 @@ function maxMove(_ref3) {
29818
30094
 
29819
30095
  // Returns the vector from the given point to the bottom-left corner of the
29820
30096
  // graph
29821
- function minMove(_ref4) {
30097
+ function minMove(_ref3) {
29822
30098
  let {
29823
30099
  snapStep,
29824
30100
  range,
29825
30101
  point
29826
- } = _ref4;
30102
+ } = _ref3;
29827
30103
  const bottomLeft = bound$1({
29828
30104
  snapStep,
29829
30105
  range,
@@ -29843,7 +30119,7 @@ function clamp(value, min, max) {
29843
30119
  const coordsOverlap = coords => coords.some((coord, i) => coords.some((c, j) => i !== j && kmath.vector.equal(coord, c)));
29844
30120
  function updateAtIndex(args) {
29845
30121
  const {
29846
- array = [],
30122
+ array,
29847
30123
  index,
29848
30124
  update
29849
30125
  } = args;
@@ -29856,13 +30132,12 @@ function updateAtIndex(args) {
29856
30132
  }
29857
30133
  function setAtIndex(args) {
29858
30134
  const {
29859
- array = [],
30135
+ array,
29860
30136
  index,
29861
30137
  newValue
29862
30138
  } = args;
29863
30139
  const copy = [...array];
29864
30140
  copy[index] = newValue;
29865
- // restoring readonly to array
29866
30141
  return copy;
29867
30142
  }
29868
30143
 
@@ -29904,18 +30179,31 @@ function initializeGraphState(params) {
29904
30179
  };
29905
30180
  case "polygon":
29906
30181
  return {
30182
+ type: "polygon",
29907
30183
  hasBeenInteractedWith: false,
29908
30184
  range,
29909
30185
  snapStep,
29910
- ...graph,
30186
+ showAngles: Boolean(graph.showAngles),
30187
+ showSides: Boolean(graph.showSides),
29911
30188
  coords: getPolygonCoords({
29912
30189
  graph,
29913
30190
  range,
29914
30191
  step
29915
30192
  })
29916
30193
  };
29917
- case "angle":
29918
30194
  case "point":
30195
+ return {
30196
+ type: graph.type,
30197
+ hasBeenInteractedWith: false,
30198
+ range,
30199
+ snapStep,
30200
+ coords: getDefaultPoints({
30201
+ graph,
30202
+ step,
30203
+ range
30204
+ })
30205
+ };
30206
+ case "angle":
29919
30207
  case "circle":
29920
30208
  case "sinusoid":
29921
30209
  case "quadratic":
@@ -29924,6 +30212,46 @@ function initializeGraphState(params) {
29924
30212
  throw new wonderStuffCore.UnreachableCaseError(graph);
29925
30213
  }
29926
30214
  }
30215
+ const getDefaultPoints = _ref => {
30216
+ let {
30217
+ graph,
30218
+ range,
30219
+ step
30220
+ } = _ref;
30221
+ const numPoints = graph.numPoints || 1;
30222
+ let coords = graph.coords?.slice();
30223
+ if (coords) {
30224
+ return coords;
30225
+ }
30226
+ switch (numPoints) {
30227
+ case 1:
30228
+ // Back in the day, one point's coords were in graph.coord
30229
+ coords = [graph.coord || [0, 0]];
30230
+ break;
30231
+ case 2:
30232
+ coords = [[-5, 0], [5, 0]];
30233
+ break;
30234
+ case 3:
30235
+ coords = [[-5, 0], [0, 0], [5, 0]];
30236
+ break;
30237
+ case 4:
30238
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
30239
+ break;
30240
+ case 5:
30241
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
30242
+ break;
30243
+ case 6:
30244
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
30245
+ break;
30246
+ default:
30247
+ coords = [];
30248
+ break;
30249
+ }
30250
+ // Transform coords from their -10 to 10 space to 0 to 1
30251
+ // because of the old graph.coord, and also it's easier.
30252
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30253
+ return normalizePoints(range, step, newCoords);
30254
+ };
29927
30255
 
29928
30256
  // TS v4 doesn't narrow return types, while v5 does.
29929
30257
  // Instead of updating to v5, using generic type to relate input and output types.
@@ -29948,13 +30276,13 @@ function getGradableGraph(state, initialGraph) {
29948
30276
  if (state.type === "linear" && initialGraph.type === "linear") {
29949
30277
  return {
29950
30278
  ...initialGraph,
29951
- coords: state.coords?.[0]
30279
+ coords: state.coords[0]
29952
30280
  };
29953
30281
  }
29954
30282
  if (state.type === "ray" && initialGraph.type === "ray") {
29955
30283
  return {
29956
30284
  ...initialGraph,
29957
- coords: state.coords?.[0]
30285
+ coords: state.coords[0]
29958
30286
  };
29959
30287
  }
29960
30288
  if (state.type === "polygon" && initialGraph.type === "polygon") {
@@ -29963,14 +30291,20 @@ function getGradableGraph(state, initialGraph) {
29963
30291
  coords: state.coords
29964
30292
  };
29965
30293
  }
30294
+ if (state.type === "point" && initialGraph.type === "point") {
30295
+ return {
30296
+ ...initialGraph,
30297
+ coords: state.coords
30298
+ };
30299
+ }
29966
30300
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29967
30301
  }
29968
- const getDefaultSegments = _ref => {
30302
+ const getDefaultSegments = _ref2 => {
29969
30303
  let {
29970
30304
  graph,
29971
30305
  range,
29972
30306
  step
29973
- } = _ref;
30307
+ } = _ref2;
29974
30308
  const ys = n => {
29975
30309
  switch (n) {
29976
30310
  case 2:
@@ -29996,24 +30330,24 @@ const getDefaultSegments = _ref => {
29996
30330
  });
29997
30331
  };
29998
30332
  const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29999
- const getLineCoords = _ref2 => {
30333
+ const getLineCoords = _ref3 => {
30000
30334
  let {
30001
30335
  graph,
30002
30336
  range,
30003
30337
  step
30004
- } = _ref2;
30338
+ } = _ref3;
30005
30339
  return (
30006
30340
  // Return two lines for a linear system, one for a ray or linear
30007
30341
  graph.coords ?? graph.type === "linear-system" ? defaultLinearCoords.map(collinear => normalizePoints(range, step, collinear)) : [normalizePoints(range, step, defaultLinearCoords[0])]
30008
30342
  );
30009
30343
  };
30010
- const getPolygonCoords = _ref3 => {
30344
+ const getPolygonCoords = _ref4 => {
30011
30345
  let {
30012
30346
  graph,
30013
30347
  range,
30014
30348
  step
30015
- } = _ref3;
30016
- let coords = graph.coords;
30349
+ } = _ref4;
30350
+ let coords = graph.coords?.slice();
30017
30351
  if (coords) {
30018
30352
  return coords;
30019
30353
  }
@@ -30067,6 +30401,13 @@ const renderGraph = props => {
30067
30401
  graphState: state,
30068
30402
  dispatch: dispatch
30069
30403
  });
30404
+ case "point":
30405
+ return /*#__PURE__*/React__namespace.createElement(PointGraph, {
30406
+ graphState: state,
30407
+ dispatch: dispatch
30408
+ });
30409
+ case "circle":
30410
+ throw new Error("the circle graph type is not yet implemented");
30070
30411
  default:
30071
30412
  return new wonderStuffCore.UnreachableCaseError(type);
30072
30413
  }
@@ -30074,10 +30415,36 @@ const renderGraph = props => {
30074
30415
  const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30075
30416
  const [width, height] = props.box;
30076
30417
  const [state, dispatch] = React__namespace.useReducer(interactiveGraphReducer, props, initializeGraphState);
30418
+ const prevState = React.useRef(state);
30419
+ React.useEffect(() => {
30420
+ if (prevState.current !== state) {
30421
+ props.onChange({
30422
+ graph: state
30423
+ });
30424
+ }
30425
+ prevState.current = state;
30426
+ }, [props, state]);
30427
+
30428
+ // Destructuring first to keep useEffect from making excess calls
30429
+ const [xSnap, ySnap] = props.snapStep;
30430
+ React.useEffect(() => {
30431
+ dispatch(changeSnapStep([xSnap, ySnap]));
30432
+ }, [xSnap, ySnap]);
30433
+
30434
+ // Destructuring first to keep useEffect from making excess calls
30435
+ const [[xMinRange, xMaxRange], [yMinRange, yMaxRange]] = props.range;
30436
+ React.useEffect(() => {
30437
+ dispatch(changeRange([[xMinRange, xMaxRange], [yMinRange, yMaxRange]]));
30438
+ }, [xMinRange, xMaxRange, yMinRange, yMaxRange]);
30077
30439
  React__namespace.useImperativeHandle(ref, () => ({
30078
30440
  getUserInput: () => getGradableGraph(state, props.graph)
30079
30441
  }));
30080
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30442
+ return /*#__PURE__*/React__namespace.createElement(GraphStateContext.Provider, {
30443
+ value: {
30444
+ state,
30445
+ dispatch
30446
+ }
30447
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
30081
30448
  style: {
30082
30449
  width,
30083
30450
  height,
@@ -30103,12 +30470,18 @@ const MafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref) => {
30103
30470
  zoom: false,
30104
30471
  width: width,
30105
30472
  height: height
30106
- }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, props), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30473
+ }, /*#__PURE__*/React__namespace.createElement(SvgDefs, null), /*#__PURE__*/React__namespace.createElement(Grid, {
30474
+ tickStep: props.step,
30475
+ gridStep: props.gridStep,
30476
+ range: props.range,
30477
+ containerSizeClass: props.containerSizeClass,
30478
+ markings: props.markings
30479
+ }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
30107
30480
  lockedFigures: props.lockedFigures
30108
30481
  }), renderGraph({
30109
30482
  state,
30110
30483
  dispatch
30111
- }))));
30484
+ })))));
30112
30485
  });
30113
30486
 
30114
30487
  const {
@@ -31275,6 +31648,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
31275
31648
  });
31276
31649
  $__default["default"](this.angle).on("move", () => {
31277
31650
  this.onChange({
31651
+ // @ts-expect-error Type '{ coords: any; type: "angle"; showAngles?: boolean | undefined; allowReflexAngles?: boolean | undefined; angleOffsetDeg?: number | undefined; snapDegrees?: number | undefined; match?: "congruent" | undefined; }' is not assignable to type 'InteractiveGraphState | undefined'.
31278
31652
  graph: {
31279
31653
  ...graph,
31280
31654
  coords: this.angle?.getClockwiseCoords()
@@ -31809,7 +32183,7 @@ class InteractiveGraph extends React__namespace.Component {
31809
32183
  }
31810
32184
  } else if (userInput.type === "point" && rubric.correct.type === "point" && userInput.coords != null) {
31811
32185
  let correct = InteractiveGraph.getPointCoords(rubric.correct, component);
31812
- const guess = userInput.coords?.slice();
32186
+ const guess = userInput.coords.slice();
31813
32187
  correct = correct.slice();
31814
32188
  // Everything's already rounded so we shouldn't need to do an
31815
32189
  // eq() comparison but _.isEqual(0, -0) is false, so we'll use
@@ -31852,8 +32226,8 @@ class InteractiveGraph extends React__namespace.Component {
31852
32226
  };
31853
32227
  }
31854
32228
  } else if (userInput.type === "segment" && rubric.correct.type === "segment" && userInput.coords != null) {
31855
- let guess = userInput.coords.slice();
31856
- let correct = rubric.correct.coords?.slice();
32229
+ let guess = Util.deepClone(userInput.coords);
32230
+ let correct = Util.deepClone(rubric.correct?.coords);
31857
32231
  guess = ___default["default"].invoke(guess, "sort").sort();
31858
32232
  // @ts-expect-error - TS2345 - Argument of type '(readonly Coord[])[] | undefined' is not assignable to parameter of type 'Collection<any>'.
31859
32233
  correct = ___default["default"].invoke(correct, "sort").sort();
@@ -42115,7 +42489,7 @@ const init = function (options) {
42115
42489
 
42116
42490
  // This file is processed by a Rollup plugin (replace) to inject the production
42117
42491
  const libName = "@khanacademy/perseus";
42118
- const libVersion = "21.0.0";
42492
+ const libVersion = "21.2.0";
42119
42493
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
42120
42494
 
42121
42495
  const apiVersion = {
@@ -43244,6 +43618,9 @@ const ItemExtras = [
43244
43618
  // A background image applied to various widgets.
43245
43619
 
43246
43620
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
43621
+
43622
+ // 2D range: xMin, xMax, yMin, yMax
43623
+
43247
43624
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
43248
43625
 
43249
43626
  /**
@@ -44617,7 +44994,7 @@ function getAnswersFromWidgets(widgets) {
44617
44994
  // E.g. 'item 1\nitem 2\nitem 3'
44618
44995
  const orderer = widget;
44619
44996
  if (orderer.options?.correctOptions) {
44620
- answers.push(orderer.options.correctOptions.map(option => option.content).join("\n"));
44997
+ answers.push(joinOptionContents(orderer.options.correctOptions));
44621
44998
  }
44622
44999
  break;
44623
45000
  case "sorter":
@@ -44676,6 +45053,31 @@ function getAnswersFromWidgets(widgets) {
44676
45053
  return answers;
44677
45054
  }
44678
45055
 
45056
+ /**
45057
+ * Join the content of the options together.
45058
+ * @param {Array<{content: string}>} options
45059
+ * @returns {string}
45060
+ */
45061
+ const joinOptionContents = options => options.map(_ref => {
45062
+ let {
45063
+ content
45064
+ } = _ref;
45065
+ return content;
45066
+ }).join("\n");
45067
+
45068
+ /**
45069
+ * Convert an index to an option letter.
45070
+ * @param {number} index
45071
+ * @returns {string}
45072
+ * @example
45073
+ * toOptionLetter(0) // 'A'
45074
+ * toOptionLetter(1) // 'B'
45075
+ * toOptionLetter(25) // 'Z'
45076
+ * // Once the index goes past 25, it will start returning special characters
45077
+ * toOptionLetter(26) // '['
45078
+ */
45079
+ const toOptionLetter = index => String.fromCharCode("A".charCodeAt(0) + index);
45080
+
44679
45081
  /**
44680
45082
  * Inject a string equivalent of the widgets into the content.
44681
45083
  *
@@ -44686,7 +45088,7 @@ function getAnswersFromWidgets(widgets) {
44686
45088
  * @param {PerseusRenderer["widgets"]} widgets
44687
45089
  * @returns
44688
45090
  */
44689
- function injectWidgets(content, widgets) {
45091
+ function injectWidgets(content, widgets, widgetProps) {
44690
45092
  // The types for taskProgress.itemData are not well defined,
44691
45093
  // so there is a chance that widgets or content could be undefined.
44692
45094
  // In that scenario, simply exit early and return the content as is.
@@ -44705,11 +45107,22 @@ function injectWidgets(content, widgets) {
44705
45107
  switch (widget.type) {
44706
45108
  case "radio":
44707
45109
  // Replace radio with the radio options
44708
- // '[[☃ Radio 1]]' -> 'option 1\noption 2\noption 3'
45110
+ // '[[☃ Radio 1]]' -> 'choice 1\nchoice 2\nchoice 3'
45111
+ // or if the current widget state is available with proper order,
45112
+ // '[[☃ Radio 1]]' ->
45113
+ // 'Option A: choice 1\nOption B: choice 2\nOption C: choice 3'
44709
45114
  const radio = widget;
45115
+ const radioProps = widgetProps?.[widgetID];
44710
45116
  if (radio.options?.choices?.length) {
44711
- let radioContext = radio.options.choices.map(choice => choice.content).join("\n");
44712
- if (radio.options?.randomize) {
45117
+ let radioContext = joinOptionContents(radioProps ? radioProps.choices.map((_ref2, i) => {
45118
+ let {
45119
+ content
45120
+ } = _ref2;
45121
+ return {
45122
+ content: `Option ${toOptionLetter(i)}: ${content}`
45123
+ };
45124
+ }) : radio.options.choices);
45125
+ if (!radioProps && radio.options?.randomize) {
44713
45126
  radioContext += "\nThose options are displayed in a different order to the user. If the user says the letter, number, or ordinal number, always ask them clarify which option they are referring to.\n";
44714
45127
  }
44715
45128
  context = context.replace(`[[☃ ${widgetID}]]`, radioContext);
@@ -44817,7 +45230,7 @@ function injectWidgets(content, widgets) {
44817
45230
  // '[[☃ Orderer 1]]' -> 'option 1\noption 2\noption 3'
44818
45231
  const orderer = widget;
44819
45232
  if (orderer.options?.options) {
44820
- context = context.replace(`[[☃ ${widgetID}]]`, orderer.options.options.map(option => option.content).join("\n"));
45233
+ context = context.replace(`[[☃ ${widgetID}]]`, joinOptionContents(orderer.options.options));
44821
45234
  }
44822
45235
  break;
44823
45236
  case "sorter":