@khanacademy/perseus 23.1.0 → 23.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/es/index.css +8 -0
  2. package/dist/es/index.js +578 -320
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.css +8 -0
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +593 -330
  7. package/dist/index.js.map +1 -1
  8. package/dist/perseus-api.d.ts +0 -1
  9. package/dist/perseus-types.d.ts +17 -9
  10. package/dist/server-item-renderer.d.ts +0 -1
  11. package/dist/types.d.ts +1 -1
  12. package/dist/util/geometry.d.ts +1 -0
  13. package/dist/widgets/expression.d.ts +0 -6
  14. package/dist/widgets/interactive-graphs/graphs/components/angle.d.ts +2 -1
  15. package/dist/widgets/interactive-graphs/graphs/components/css-cursor.d.ts +1 -0
  16. package/dist/widgets/interactive-graphs/graphs/components/movable-point-view.d.ts +2 -0
  17. package/dist/widgets/interactive-graphs/graphs/components/movable-point.d.ts +3 -0
  18. package/dist/widgets/interactive-graphs/graphs/index.d.ts +1 -0
  19. package/dist/widgets/interactive-graphs/graphs/linear-system.d.ts +5 -0
  20. package/dist/widgets/interactive-graphs/graphs/linear.d.ts +1 -1
  21. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -2
  22. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +6 -3
  23. package/dist/widgets/interactive-graphs/locked-ellipse.d.ts +4 -0
  24. package/dist/widgets/interactive-graphs/locked-vector.d.ts +4 -0
  25. package/dist/widgets/interactive-graphs/reducer/initialize-graph-state.d.ts +9 -0
  26. package/dist/widgets/interactive-graphs/reducer/interactive-graph-state.d.ts +2 -9
  27. package/dist/widgets/interactive-graphs/types.d.ts +9 -9
  28. package/dist/widgets/label-image.d.ts +0 -2
  29. package/package.json +3 -3
  30. package/dist/widgets/interactive-graphs/locked-circle.d.ts +0 -4
package/dist/index.js CHANGED
@@ -3304,7 +3304,7 @@ const DeprecationMixin$1 = {
3304
3304
  /**
3305
3305
  * Approximate equality on numbers and primitives.
3306
3306
  */
3307
- function eq$2(x, y) {
3307
+ function eq$3(x, y) {
3308
3308
  if (typeof x === "number" && typeof y === "number") {
3309
3309
  return Math.abs(x - y) < 1e-9;
3310
3310
  }
@@ -3331,7 +3331,7 @@ function deepEq$5(x, y) {
3331
3331
  return false;
3332
3332
  }
3333
3333
  if (typeof x === "function" && typeof y === "function") {
3334
- return eq$2(x, y);
3334
+ return eq$3(x, y);
3335
3335
  }
3336
3336
  if (typeof x === "function" || typeof y === "function") {
3337
3337
  return false;
@@ -3348,7 +3348,7 @@ function deepEq$5(x, y) {
3348
3348
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3349
3349
  return false;
3350
3350
  }
3351
- return eq$2(x, y);
3351
+ return eq$3(x, y);
3352
3352
  }
3353
3353
 
3354
3354
  /**
@@ -3647,7 +3647,7 @@ const Util = {
3647
3647
  tickStepFromNumTicks,
3648
3648
  constrainedTickStepsFromTickSteps,
3649
3649
  DeprecationMixin: DeprecationMixin$1,
3650
- eq: eq$2,
3650
+ eq: eq$3,
3651
3651
  deepEq: deepEq$5,
3652
3652
  parseQueryString,
3653
3653
  updateQueryString: updateQueryString$2,
@@ -10885,7 +10885,7 @@ class WrappedLine extends WrappedDrawing {
10885
10885
  * A collection of geomtry-related utility functions
10886
10886
  */
10887
10887
  const {
10888
- eq: eq$1,
10888
+ eq: eq$2,
10889
10889
  deepEq: deepEq$4
10890
10890
  } = Util;
10891
10891
 
@@ -10893,17 +10893,21 @@ const {
10893
10893
 
10894
10894
  // a, b, c
10895
10895
 
10896
+ // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10896
10897
  function sign(val) {
10897
- if (eq$1(val, 0)) {
10898
+ if (eq$2(val, 0)) {
10898
10899
  return 0;
10899
10900
  }
10900
10901
  return val > 0 ? 1 : -1;
10901
10902
  }
10903
+
10904
+ // Determine whether three points are collinear (0), for a clockwise turn (negative),
10905
+ // or counterclockwise turn (positive)
10902
10906
  function ccw(a, b, c) {
10903
10907
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10904
10908
  }
10905
10909
  function collinear(a, b, c) {
10906
- return eq$1(ccw(a, b, c), 0);
10910
+ return eq$2(ccw(a, b, c), 0);
10907
10911
  }
10908
10912
 
10909
10913
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -10928,6 +10932,33 @@ function intersects(ab, cd) {
10928
10932
  }
10929
10933
  return false;
10930
10934
  }
10935
+
10936
+ // Whether any two sides of a polygon intersect each other
10937
+ function polygonSidesIntersect(vertices) {
10938
+ for (let i = 0; i < vertices.length; i++) {
10939
+ for (let k = i + 1; k < vertices.length; k++) {
10940
+ // If any two vertices are the same point, sides overlap
10941
+ if (kmath.point.equal(vertices[i], vertices[k])) {
10942
+ return true;
10943
+ }
10944
+
10945
+ // Find the other end of the sides starting at vertices i and k
10946
+ const iNext = (i + 1) % vertices.length;
10947
+ const kNext = (k + 1) % vertices.length;
10948
+
10949
+ // Adjacent sides always intersect (at the vertex); skip those
10950
+ if (iNext === k || kNext === i) {
10951
+ continue;
10952
+ }
10953
+ const side1 = [vertices[i], vertices[iNext]];
10954
+ const side2 = [vertices[k], vertices[kNext]];
10955
+ if (intersects(side1, side2)) {
10956
+ return true;
10957
+ }
10958
+ }
10959
+ }
10960
+ return false;
10961
+ }
10931
10962
  function vector(a, b) {
10932
10963
  return ___default["default"].map(___default["default"].zip(a, b), function (pair) {
10933
10964
  return pair[0] - pair[1];
@@ -11021,7 +11052,7 @@ function similar(coords1, coords2, tolerance) {
11021
11052
  return pair[0] / pair[1];
11022
11053
  });
11023
11054
  const same = ___default["default"].all(factors, function (factor) {
11024
- return eq$1(factors[0], factor);
11055
+ return eq$2(factors[0], factor);
11025
11056
  });
11026
11057
  const congruentEnough = ___default["default"].all(sidePairs, function (pair) {
11027
11058
  return kmath.number.equal(pair[0], pair[1], tolerance);
@@ -11074,7 +11105,7 @@ function rotate(array, n) {
11074
11105
  return array.slice(n).concat(array.slice(0, n));
11075
11106
  }
11076
11107
  function getLineEquation(first, second) {
11077
- if (eq$1(first[0], second[0])) {
11108
+ if (eq$2(first[0], second[0])) {
11078
11109
  return "x = " + first[0].toFixed(3);
11079
11110
  }
11080
11111
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -28922,25 +28953,27 @@ const lockedFigureColors = {
28922
28953
  pink: "#B25071",
28923
28954
  red: "#D92916"
28924
28955
  };
28956
+ const lockedEllipseFillStyles = {
28957
+ none: 0,
28958
+ solid: 1,
28959
+ translucent: 0.4
28960
+ };
28925
28961
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28926
28962
 
28927
- const LockedCircle = props => {
28963
+ const LockedEllipse = props => {
28928
28964
  const {
28929
28965
  center,
28930
28966
  radius,
28967
+ angle,
28931
28968
  color,
28932
28969
  fillStyle,
28933
28970
  strokeStyle
28934
28971
  } = props;
28935
- const fill = {
28936
- none: 0,
28937
- solid: 1,
28938
- translucent: 0.4
28939
- };
28940
- return /*#__PURE__*/React__namespace.createElement(mafs.Circle, {
28972
+ return /*#__PURE__*/React__namespace.createElement(mafs.Ellipse, {
28941
28973
  center: center,
28942
28974
  radius: radius,
28943
- fillOpacity: fill[fillStyle],
28975
+ angle: angle,
28976
+ fillOpacity: lockedEllipseFillStyles[fillStyle],
28944
28977
  strokeStyle: strokeStyle,
28945
28978
  color: lockedFigureColors[color]
28946
28979
  });
@@ -29199,6 +29232,21 @@ const LockedPoint = props => {
29199
29232
  });
29200
29233
  };
29201
29234
 
29235
+ const LockedVector = props => {
29236
+ const {
29237
+ color,
29238
+ points
29239
+ } = props;
29240
+ const [tail, tip] = points;
29241
+ return /*#__PURE__*/React__namespace.createElement("g", {
29242
+ className: "locked-vector"
29243
+ }, /*#__PURE__*/React__namespace.createElement(Vector, {
29244
+ tail: tail,
29245
+ tip: tip,
29246
+ color: lockedFigureColors[color]
29247
+ }));
29248
+ };
29249
+
29202
29250
  const GraphLockedLayer = props => {
29203
29251
  const {
29204
29252
  lockedFigures
@@ -29214,9 +29262,13 @@ const GraphLockedLayer = props => {
29214
29262
  key: `line-${index}`,
29215
29263
  range: props.range
29216
29264
  }, figure));
29217
- case "circle":
29218
- return /*#__PURE__*/React__namespace.createElement(LockedCircle, _extends({
29219
- key: `circle-${index}`
29265
+ case "ellipse":
29266
+ return /*#__PURE__*/React__namespace.createElement(LockedEllipse, _extends({
29267
+ key: `ellipse-${index}`
29268
+ }, figure));
29269
+ case "vector":
29270
+ return /*#__PURE__*/React__namespace.createElement(LockedVector, _extends({
29271
+ key: `vector-${index}`
29220
29272
  }, figure));
29221
29273
  default:
29222
29274
  /**
@@ -29345,7 +29397,8 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
29345
29397
  point,
29346
29398
  color = wonderBlocksTokens.color.blue,
29347
29399
  dragging,
29348
- focusBehavior
29400
+ focusBehavior,
29401
+ cursor
29349
29402
  } = props;
29350
29403
 
29351
29404
  // WB Tooltip requires a color name for the background color.
@@ -29380,7 +29433,8 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
29380
29433
  ref: hitboxRef,
29381
29434
  className: pointClasses,
29382
29435
  style: {
29383
- "--movable-point-color": color
29436
+ "--movable-point-color": color,
29437
+ cursor
29384
29438
  },
29385
29439
  "data-testid": "movable-point",
29386
29440
  tabIndex: tabIndex(focusBehavior)
@@ -29492,6 +29546,7 @@ function useControlPoint(point, color, onMovePoint) {
29492
29546
  });
29493
29547
  const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
29494
29548
  "data-testid": "movable-point__focusable-handle",
29549
+ className: "movable-point__focusable-handle",
29495
29550
  tabIndex: 0,
29496
29551
  ref: keyboardHandleRef,
29497
29552
  onFocus: () => setFocused(true),
@@ -29628,14 +29683,39 @@ const SegmentGraph = props => {
29628
29683
  })));
29629
29684
  };
29630
29685
 
29631
- const LinearGraph = props => {
29686
+ const LinearGraph = (props, key) => {
29687
+ const {
29688
+ dispatch
29689
+ } = props;
29690
+ const {
29691
+ coords: line
29692
+ } = props.graphState;
29693
+
29694
+ // Linear graphs only have one line
29695
+ // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
29696
+ // and another action for moving multiple lines
29697
+ return /*#__PURE__*/React__namespace.createElement(MovableLine, {
29698
+ key: 0,
29699
+ points: line,
29700
+ onMoveLine: delta => {
29701
+ dispatch(moveLine(0, delta));
29702
+ },
29703
+ extend: {
29704
+ start: true,
29705
+ end: true
29706
+ },
29707
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, 0)),
29708
+ color: "var(--movable-line-stroke-color)"
29709
+ });
29710
+ };
29711
+
29712
+ const LinearSystemGraph = props => {
29632
29713
  const {
29633
29714
  dispatch
29634
29715
  } = props;
29635
29716
  const {
29636
29717
  coords: lines
29637
29718
  } = props.graphState;
29638
- const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29639
29719
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
29640
29720
  key: i,
29641
29721
  points: line,
@@ -29647,8 +29727,8 @@ const LinearGraph = props => {
29647
29727
  end: true
29648
29728
  },
29649
29729
  onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29650
- color: colors[i]
29651
- })));
29730
+ color: "var(--movable-line-stroke-color)"
29731
+ })), ";");
29652
29732
  };
29653
29733
 
29654
29734
  const RayGraph = props => {
@@ -29656,14 +29736,14 @@ const RayGraph = props => {
29656
29736
  dispatch
29657
29737
  } = props;
29658
29738
  const {
29659
- coords: lines
29739
+ coords: line
29660
29740
  } = props.graphState;
29661
29741
  const handleMoveLine = delta => dispatch(moveLine(0, delta));
29662
29742
  const handleMovePoint = (pointIndex, newPoint) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29663
- return /*#__PURE__*/React__namespace.createElement(MovableLine
29664
- // a ray only has one line
29665
- , {
29666
- points: lines[0],
29743
+
29744
+ // Ray graphs only have one line
29745
+ return /*#__PURE__*/React__namespace.createElement(MovableLine, {
29746
+ points: line,
29667
29747
  onMoveLine: handleMoveLine,
29668
29748
  onMovePoint: handleMovePoint,
29669
29749
  extend: {
@@ -29722,7 +29802,8 @@ const Angle = _ref => {
29722
29802
  endPoints,
29723
29803
  range,
29724
29804
  polygonLines,
29725
- showAngles
29805
+ showAngles,
29806
+ snapTo
29726
29807
  } = _ref;
29727
29808
  const [centerX, centerY] = centerPoint;
29728
29809
  const areClockwise = clockwise$1([centerPoint, ...endPoints]);
@@ -29760,7 +29841,9 @@ const Angle = _ref => {
29760
29841
  if (isOutside) {
29761
29842
  angleInDegrees = 360 - angleInDegrees;
29762
29843
  }
29763
- const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
29844
+
29845
+ // Only want to show whole numbers when in angles mode
29846
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
29764
29847
  const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
29765
29848
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
29766
29849
  id: "background",
@@ -29850,15 +29933,18 @@ const StyledMovablePoint = props => {
29850
29933
  const {
29851
29934
  point,
29852
29935
  onMove,
29853
- color = wonderBlocksTokens.color.blue
29936
+ cursor,
29937
+ color = wonderBlocksTokens.color.blue,
29938
+ snapTo
29854
29939
  } = props;
29940
+ const snapToValue = snapTo ?? "grid";
29855
29941
  const {
29856
29942
  dragging
29857
29943
  } = mafs.useMovable({
29858
29944
  gestureTarget: elementRef,
29859
29945
  point,
29860
29946
  onMove,
29861
- constrain: p => snap(snapStep, p)
29947
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29862
29948
  });
29863
29949
  return /*#__PURE__*/React__namespace.createElement(MovablePointView, {
29864
29950
  ref: elementRef,
@@ -29868,7 +29954,8 @@ const StyledMovablePoint = props => {
29868
29954
  focusBehavior: {
29869
29955
  type: "uncontrolled",
29870
29956
  tabIndex: 0
29871
- }
29957
+ },
29958
+ cursor: cursor
29872
29959
  });
29873
29960
  };
29874
29961
 
@@ -29883,7 +29970,8 @@ const PolygonGraph = props => {
29883
29970
  showAngles,
29884
29971
  showSides,
29885
29972
  range,
29886
- snapStep
29973
+ snapStep,
29974
+ snapTo
29887
29975
  } = props.graphState;
29888
29976
 
29889
29977
  // TODO(benchristel): can the default set of points be removed here? I don't
@@ -29891,6 +29979,7 @@ const PolygonGraph = props => {
29891
29979
  const points = coords ?? [[0, 0]];
29892
29980
  const ref = React__namespace.useRef(null);
29893
29981
  const dragReferencePoint = points[0];
29982
+ const snapToValue = snapTo ?? "grid";
29894
29983
  const {
29895
29984
  dragging
29896
29985
  } = mafs.useMovable({
@@ -29900,7 +29989,7 @@ const PolygonGraph = props => {
29900
29989
  const delta = mafs.vec.sub(newPoint, dragReferencePoint);
29901
29990
  dispatch(moveAll(delta));
29902
29991
  },
29903
- constrain: p => snap(snapStep, p)
29992
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29904
29993
  });
29905
29994
  const active = hovered || focused || dragging;
29906
29995
  const lines = getLines(points);
@@ -29923,12 +30012,13 @@ const PolygonGraph = props => {
29923
30012
  active: active,
29924
30013
  range: range,
29925
30014
  polygonLines: lines,
29926
- showAngles: !!showAngles
30015
+ showAngles: !!showAngles,
30016
+ snapTo: snapToValue
29927
30017
  });
29928
30018
  }), showSides && lines.map((_ref, i) => {
29929
30019
  let [start, end] = _ref;
29930
30020
  const [x, y] = mafs.vec.midpoint(start, end);
29931
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(1));
30021
+ const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
29932
30022
  return /*#__PURE__*/React__namespace.createElement(TextLabel, {
29933
30023
  key: "side-" + i,
29934
30024
  x: x,
@@ -29952,138 +30042,12 @@ const PolygonGraph = props => {
29952
30042
  }
29953
30043
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29954
30044
  key: "point-" + i,
30045
+ snapTo: snapTo,
29955
30046
  point: point,
29956
30047
  onMove: destination => dispatch(movePoint(i, destination))
29957
30048
  })));
29958
30049
  };
29959
30050
 
29960
- function initializeGraphState(params) {
29961
- const {
29962
- graph,
29963
- step,
29964
- snapStep,
29965
- range
29966
- } = params;
29967
- const shared = {
29968
- hasBeenInteractedWith: false,
29969
- range,
29970
- snapStep
29971
- };
29972
- switch (graph.type) {
29973
- case "segment":
29974
- return {
29975
- ...shared,
29976
- type: "segment",
29977
- coords: getDefaultSegments({
29978
- graph,
29979
- step,
29980
- range
29981
- })
29982
- };
29983
- case "linear":
29984
- case "linear-system":
29985
- case "ray":
29986
- return {
29987
- ...shared,
29988
- type: graph.type,
29989
- // Linear and ray graphs have a single tuple of points, while a
29990
- // linear system has two tuples of points.
29991
- coords: getLineCoords({
29992
- graph,
29993
- range,
29994
- step
29995
- })
29996
- };
29997
- case "polygon":
29998
- return {
29999
- ...shared,
30000
- type: "polygon",
30001
- showAngles: Boolean(graph.showAngles),
30002
- showSides: Boolean(graph.showSides),
30003
- coords: getPolygonCoords({
30004
- graph,
30005
- range,
30006
- step
30007
- })
30008
- };
30009
- case "point":
30010
- return {
30011
- ...shared,
30012
- type: graph.type,
30013
- coords: getDefaultPoints({
30014
- graph,
30015
- step,
30016
- range
30017
- })
30018
- };
30019
- case "circle":
30020
- return {
30021
- ...shared,
30022
- type: graph.type,
30023
- center: [0, 0],
30024
- radiusPoint: [1, 0]
30025
- };
30026
- case "quadratic":
30027
- return {
30028
- ...shared,
30029
- type: graph.type,
30030
- coords: getQuadraticCoords(graph, range, step)
30031
- };
30032
- case "sinusoid":
30033
- return {
30034
- ...shared,
30035
- type: graph.type,
30036
- coords: getSinusoidCoords(graph, range, step)
30037
- };
30038
- case "angle":
30039
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
30040
- default:
30041
- throw new wonderStuffCore.UnreachableCaseError(graph);
30042
- }
30043
- }
30044
- const getDefaultPoints = _ref => {
30045
- let {
30046
- graph,
30047
- range,
30048
- step
30049
- } = _ref;
30050
- const numPoints = graph.numPoints || 1;
30051
- let coords = graph.coords?.slice();
30052
- if (coords) {
30053
- return coords;
30054
- }
30055
- switch (numPoints) {
30056
- case 1:
30057
- // Back in the day, one point's coords were in graph.coord
30058
- coords = [graph.coord || [0, 0]];
30059
- break;
30060
- case 2:
30061
- coords = [[-5, 0], [5, 0]];
30062
- break;
30063
- case 3:
30064
- coords = [[-5, 0], [0, 0], [5, 0]];
30065
- break;
30066
- case 4:
30067
- coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
30068
- break;
30069
- case 5:
30070
- coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
30071
- break;
30072
- case 6:
30073
- coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
30074
- break;
30075
- default:
30076
- coords = [];
30077
- break;
30078
- }
30079
- // Transform coords from their -10 to 10 space to 0 to 1
30080
- // because of the old graph.coord, and also it's easier.
30081
- const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30082
- return normalizePoints(range, step, newCoords);
30083
- };
30084
-
30085
- // TS v4 doesn't narrow return types, while v5 does.
30086
- // Instead of updating to v5, using generic type to relate input and output types.
30087
30051
  function getGradableGraph(state, initialGraph) {
30088
30052
  if (!state.hasBeenInteractedWith) {
30089
30053
  return {
@@ -30105,13 +30069,13 @@ function getGradableGraph(state, initialGraph) {
30105
30069
  if (state.type === "linear" && initialGraph.type === "linear") {
30106
30070
  return {
30107
30071
  ...initialGraph,
30108
- coords: state.coords[0]
30072
+ coords: state.coords
30109
30073
  };
30110
30074
  }
30111
30075
  if (state.type === "ray" && initialGraph.type === "ray") {
30112
30076
  return {
30113
30077
  ...initialGraph,
30114
- coords: state.coords[0]
30078
+ coords: state.coords
30115
30079
  };
30116
30080
  }
30117
30081
  if (state.type === "polygon" && initialGraph.type === "polygon") {
@@ -30147,93 +30111,6 @@ function getGradableGraph(state, initialGraph) {
30147
30111
  }
30148
30112
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
30149
30113
  }
30150
- const getDefaultSegments = _ref2 => {
30151
- let {
30152
- graph,
30153
- range,
30154
- step
30155
- } = _ref2;
30156
- const ys = n => {
30157
- switch (n) {
30158
- case 2:
30159
- return [5, -5];
30160
- case 3:
30161
- return [5, 0, -5];
30162
- case 4:
30163
- return [6, 2, -2, -6];
30164
- case 5:
30165
- return [6, 3, 0, -3, -6];
30166
- case 6:
30167
- return [5, 3, 1, -1, -3, -5];
30168
- default:
30169
- return [5];
30170
- }
30171
- };
30172
- const defaultRange = [[-10, 10], [-10, 10]];
30173
- return ys(graph.numSegments).map(y => {
30174
- let endpoints = [[-5, y], [5, y]];
30175
- endpoints = normalizeCoords(endpoints, defaultRange);
30176
- endpoints = normalizePoints(range, step, endpoints);
30177
- return endpoints;
30178
- });
30179
- };
30180
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
30181
- const getLineCoords = _ref3 => {
30182
- let {
30183
- graph,
30184
- range,
30185
- step
30186
- } = _ref3;
30187
- // Return two lines for a linear system, one for a ray or linear
30188
- switch (graph.type) {
30189
- case "linear-system":
30190
- return defaultLinearCoords.map(points => normalizePoints(range, step, points));
30191
- case "linear":
30192
- case "ray":
30193
- return [normalizePoints(range, step, defaultLinearCoords[0])];
30194
- default:
30195
- throw new wonderStuffCore.UnreachableCaseError(graph);
30196
- }
30197
- };
30198
- const getPolygonCoords = _ref4 => {
30199
- let {
30200
- graph,
30201
- range,
30202
- step
30203
- } = _ref4;
30204
- let coords = graph.coords?.slice();
30205
- if (coords) {
30206
- return coords;
30207
- }
30208
- const n = graph.numSides || 3;
30209
- if (n === "unlimited") {
30210
- coords = [];
30211
- } else {
30212
- const angle = 2 * Math.PI / n;
30213
- const offset = (1 / n - 1 / 2) * Math.PI;
30214
-
30215
- // TODO(alex): Generalize this to more than just triangles so that
30216
- // all polygons have whole number side lengths if snapping to sides
30217
- const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
30218
-
30219
- // Generate coords of a regular polygon with n sides
30220
- coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
30221
- }
30222
- coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30223
- const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
30224
- coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
30225
- return coords;
30226
- };
30227
- const getSinusoidCoords = (graph, range, step) => {
30228
- let coords = [[0.5, 0.5], [0.65, 0.6]];
30229
- coords = normalizePoints(range, step, coords, true);
30230
- return coords;
30231
- };
30232
- const getQuadraticCoords = (graph, range, step) => {
30233
- let coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
30234
- coords = normalizePoints(range, step, coords, true);
30235
- return coords;
30236
- };
30237
30114
 
30238
30115
  /**
30239
30116
  * determine radius of a circle graph
@@ -30262,6 +30139,7 @@ function CircleGraph(props) {
30262
30139
  onMove: newCenter => dispatch(moveCenter(newCenter))
30263
30140
  }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
30264
30141
  point: radiusPoint,
30142
+ cursor: "ew-resize",
30265
30143
  onMove: newRadiusPoint => {
30266
30144
  dispatch(moveRadiusPoint(newRadiusPoint));
30267
30145
  }
@@ -30366,38 +30244,16 @@ function QuadraticGraph(props) {
30366
30244
 
30367
30245
  // Calculate the y value based on the current x value
30368
30246
  const y = x => (a * x + b) * x + c;
30369
- const handleOnMove = (destination, elementId) => {
30370
- // If the destination is invalid, we want do not want to move the point
30371
- const validDestination = isValidDestination(destination, elementId, coords);
30372
- if (validDestination === false) {
30373
- return;
30374
- }
30375
- dispatch(movePoint(elementId, destination));
30376
- };
30377
30247
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
30378
30248
  y: y,
30379
30249
  color: wonderBlocksTokens.color.blue
30380
30250
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
30381
30251
  key: "point-" + i,
30382
30252
  point: coord,
30383
- onMove: destination => handleOnMove(destination, i)
30253
+ onMove: destination => dispatch(movePoint(i, destination))
30384
30254
  })));
30385
30255
  }
30386
30256
 
30387
- // Ensure that we are only snapping to coordinates that result in a valid quadratic equation
30388
- const isValidDestination = (destination, elementId, coords) => {
30389
- // Set up the new coords and check if the quadratic coefficients are valid
30390
- const newCoords = [...coords];
30391
- newCoords[elementId] = destination;
30392
- const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
30393
-
30394
- // If the new destination results in an invalid quadratic equation, we don't want to move the point
30395
- if (QuadraticCoefficients === undefined) {
30396
- return false;
30397
- }
30398
- return true;
30399
- };
30400
-
30401
30257
  // Get the quadratic coefficients from the 3 control points
30402
30258
  // These equations were originally set up in 2013 and may require some
30403
30259
  // additional comments to help clarify the quadratic formula manipulations
@@ -31547,6 +31403,210 @@ const LegacyGrid = _ref => {
31547
31403
  return null;
31548
31404
  };
31549
31405
 
31406
+ function initializeGraphState(params) {
31407
+ const {
31408
+ graph,
31409
+ step,
31410
+ snapStep,
31411
+ range
31412
+ } = params;
31413
+ const shared = {
31414
+ hasBeenInteractedWith: false,
31415
+ range,
31416
+ snapStep
31417
+ };
31418
+ switch (graph.type) {
31419
+ case "segment":
31420
+ return {
31421
+ ...shared,
31422
+ type: "segment",
31423
+ coords: getSegmentCoords(graph, range, step)
31424
+ };
31425
+ case "linear":
31426
+ return {
31427
+ ...shared,
31428
+ type: graph.type,
31429
+ coords: getLineCoords(graph, range, step)
31430
+ };
31431
+ case "ray":
31432
+ return {
31433
+ ...shared,
31434
+ type: graph.type,
31435
+ coords: getLineCoords(graph, range, step)
31436
+ };
31437
+ case "linear-system":
31438
+ return {
31439
+ ...shared,
31440
+ type: graph.type,
31441
+ coords: getLinearSystemCoords(graph, range, step)
31442
+ };
31443
+ case "polygon":
31444
+ return {
31445
+ ...shared,
31446
+ type: "polygon",
31447
+ showAngles: Boolean(graph.showAngles),
31448
+ showSides: Boolean(graph.showSides),
31449
+ coords: getPolygonCoords(graph, range, step),
31450
+ snapTo: graph.snapTo ?? "grid"
31451
+ };
31452
+ case "point":
31453
+ return {
31454
+ ...shared,
31455
+ type: graph.type,
31456
+ coords: getPointCoords(graph, range, step)
31457
+ };
31458
+ case "circle":
31459
+ return {
31460
+ ...shared,
31461
+ type: graph.type,
31462
+ ...getCircleCoords(graph)
31463
+ };
31464
+ case "quadratic":
31465
+ return {
31466
+ ...shared,
31467
+ type: graph.type,
31468
+ coords: getQuadraticCoords(graph, range, step)
31469
+ };
31470
+ case "sinusoid":
31471
+ return {
31472
+ ...shared,
31473
+ type: graph.type,
31474
+ coords: getSinusoidCoords(graph, range, step)
31475
+ };
31476
+ case "angle":
31477
+ throw new Error("Mafs not yet implemented for graph type: " + graph.type);
31478
+ default:
31479
+ throw new wonderStuffCore.UnreachableCaseError(graph);
31480
+ }
31481
+ }
31482
+ function getPointCoords(graph, range, step) {
31483
+ const numPoints = graph.numPoints || 1;
31484
+ let coords = graph.coords?.slice();
31485
+ if (coords) {
31486
+ return coords;
31487
+ }
31488
+ switch (numPoints) {
31489
+ case 1:
31490
+ // Back in the day, one point's coords were in graph.coord
31491
+ coords = [graph.coord || [0, 0]];
31492
+ break;
31493
+ case 2:
31494
+ coords = [[-5, 0], [5, 0]];
31495
+ break;
31496
+ case 3:
31497
+ coords = [[-5, 0], [0, 0], [5, 0]];
31498
+ break;
31499
+ case 4:
31500
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
31501
+ break;
31502
+ case 5:
31503
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
31504
+ break;
31505
+ case 6:
31506
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
31507
+ break;
31508
+ default:
31509
+ coords = [];
31510
+ break;
31511
+ }
31512
+ // Transform coords from their -10 to 10 space to 0 to 1
31513
+ // because of the old graph.coord, and also it's easier.
31514
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31515
+ return normalizePoints(range, step, newCoords);
31516
+ }
31517
+ function getSegmentCoords(graph, range, step) {
31518
+ if (graph.coords) {
31519
+ return graph.coords;
31520
+ }
31521
+ const ys = n => {
31522
+ switch (n) {
31523
+ case 2:
31524
+ return [5, -5];
31525
+ case 3:
31526
+ return [5, 0, -5];
31527
+ case 4:
31528
+ return [6, 2, -2, -6];
31529
+ case 5:
31530
+ return [6, 3, 0, -3, -6];
31531
+ case 6:
31532
+ return [5, 3, 1, -1, -3, -5];
31533
+ default:
31534
+ return [5];
31535
+ }
31536
+ };
31537
+ const defaultRange = [[-10, 10], [-10, 10]];
31538
+ return ys(graph.numSegments).map(y => {
31539
+ let endpoints = [[-5, y], [5, y]];
31540
+ endpoints = normalizeCoords(endpoints, defaultRange);
31541
+ endpoints = normalizePoints(range, step, endpoints);
31542
+ return endpoints;
31543
+ });
31544
+ }
31545
+ const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
31546
+ function getLineCoords(graph, range, step) {
31547
+ if (graph.coords) {
31548
+ return graph.coords;
31549
+ }
31550
+ return normalizePoints(range, step, defaultLinearCoords[0]);
31551
+ }
31552
+ function getLinearSystemCoords(graph, range, step) {
31553
+ if (graph.coords) {
31554
+ return graph.coords;
31555
+ }
31556
+ return defaultLinearCoords.map(points => normalizePoints(range, step, points));
31557
+ }
31558
+ function getPolygonCoords(graph, range, step) {
31559
+ let coords = graph.coords?.slice();
31560
+ if (coords) {
31561
+ return coords;
31562
+ }
31563
+ const n = graph.numSides || 3;
31564
+ if (n === "unlimited") {
31565
+ coords = [];
31566
+ } else {
31567
+ const angle = 2 * Math.PI / n;
31568
+ const offset = (1 / n - 1 / 2) * Math.PI;
31569
+
31570
+ // TODO(alex): Generalize this to more than just triangles so that
31571
+ // all polygons have whole number side lengths if snapping to sides
31572
+ const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
31573
+
31574
+ // Generate coords of a regular polygon with n sides
31575
+ coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
31576
+ }
31577
+ coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31578
+ const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
31579
+ coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
31580
+ return coords;
31581
+ }
31582
+ function getSinusoidCoords(graph, range, step) {
31583
+ if (graph.coords) {
31584
+ return [graph.coords[0], graph.coords[1]];
31585
+ }
31586
+ let coords = [[0.5, 0.5], [0.65, 0.6]];
31587
+ coords = normalizePoints(range, step, coords, true);
31588
+ return coords;
31589
+ }
31590
+ function getQuadraticCoords(graph, range, step) {
31591
+ if (graph.coords) {
31592
+ return graph.coords;
31593
+ }
31594
+ const defaultCoords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
31595
+ return normalizePoints(range, step, defaultCoords, true);
31596
+ }
31597
+ function getCircleCoords(graph) {
31598
+ if (graph.center != null && graph.radius != null) {
31599
+ return {
31600
+ center: graph.center,
31601
+ radiusPoint: mafs.vec.add(graph.center, [graph.radius, 0])
31602
+ };
31603
+ }
31604
+ return {
31605
+ center: [0, 0],
31606
+ radiusPoint: [2, 0]
31607
+ };
31608
+ }
31609
+
31550
31610
  function interactiveGraphReducer(state, action) {
31551
31611
  switch (action.type) {
31552
31612
  case MOVE_CONTROL_POINT:
@@ -31570,15 +31630,9 @@ function interactiveGraphReducer(state, action) {
31570
31630
  }
31571
31631
  }
31572
31632
  function doMoveControlPoint(state, action) {
31573
- const {
31574
- snapStep,
31575
- range
31576
- } = state;
31577
31633
  switch (state.type) {
31578
31634
  case "segment":
31579
- case "linear":
31580
31635
  case "linear-system":
31581
- case "ray":
31582
31636
  {
31583
31637
  const newCoords = updateAtIndex({
31584
31638
  array: state.coords,
@@ -31586,11 +31640,7 @@ function doMoveControlPoint(state, action) {
31586
31640
  update: tuple => setAtIndex({
31587
31641
  array: tuple,
31588
31642
  index: action.pointIndex,
31589
- newValue: snap(snapStep, bound$1({
31590
- snapStep,
31591
- range,
31592
- point: action.destination
31593
- }))
31643
+ newValue: boundAndSnapToGrid(action.destination, state)
31594
31644
  })
31595
31645
  });
31596
31646
  const coordsToCheck = newCoords[action.itemIndex];
@@ -31603,6 +31653,20 @@ function doMoveControlPoint(state, action) {
31603
31653
  coords: newCoords
31604
31654
  };
31605
31655
  }
31656
+ case "linear":
31657
+ case "ray":
31658
+ {
31659
+ const newCoords = setAtIndex({
31660
+ array: state.coords,
31661
+ index: action.pointIndex,
31662
+ newValue: boundAndSnapToGrid(action.destination, state)
31663
+ });
31664
+ return {
31665
+ ...state,
31666
+ hasBeenInteractedWith: true,
31667
+ coords: newCoords
31668
+ };
31669
+ }
31606
31670
  case "circle":
31607
31671
  throw new Error("FIXME implement circle reducer");
31608
31672
  case "point":
@@ -31614,6 +31678,9 @@ function doMoveControlPoint(state, action) {
31614
31678
  throw new wonderStuffCore.UnreachableCaseError(state);
31615
31679
  }
31616
31680
  }
31681
+
31682
+ // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
31683
+ // and another action for moving multiple lines
31617
31684
  function doMoveLine(state, action) {
31618
31685
  const {
31619
31686
  snapStep,
@@ -31621,9 +31688,7 @@ function doMoveLine(state, action) {
31621
31688
  } = state;
31622
31689
  switch (state.type) {
31623
31690
  case "segment":
31624
- case "linear":
31625
31691
  case "linear-system":
31626
- case "ray":
31627
31692
  {
31628
31693
  if (action.itemIndex === undefined) {
31629
31694
  throw new Error("Please provide index of line to move");
@@ -31649,6 +31714,22 @@ function doMoveLine(state, action) {
31649
31714
  coords: newCoords
31650
31715
  };
31651
31716
  }
31717
+ case "linear":
31718
+ case "ray":
31719
+ {
31720
+ const currentLine = state.coords;
31721
+ const change = getChange(currentLine, action.delta, {
31722
+ snapStep,
31723
+ range
31724
+ });
31725
+ const newLine = [snap(snapStep, mafs.vec.add(currentLine[0], change)), snap(snapStep, mafs.vec.add(currentLine[1], change))];
31726
+ return {
31727
+ ...state,
31728
+ type: state.type,
31729
+ hasBeenInteractedWith: true,
31730
+ coords: newLine
31731
+ };
31732
+ }
31652
31733
  default:
31653
31734
  // The MoveLine action doesn't make sense for other graph types;
31654
31735
  // ignore it if it somehow happens
@@ -31663,11 +31744,20 @@ function doMoveAll(state, action) {
31663
31744
  switch (state.type) {
31664
31745
  case "polygon":
31665
31746
  {
31666
- const change = getChange(state.coords, action.delta, {
31667
- snapStep,
31668
- range
31669
- });
31670
- const newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
31747
+ let newCoords;
31748
+ if (state.snapTo === "sides" || state.snapTo === "angles") {
31749
+ const change = getChange(state.coords, action.delta, {
31750
+ snapStep: [0, 0],
31751
+ range
31752
+ });
31753
+ newCoords = state.coords.map(point => mafs.vec.add(point, change));
31754
+ } else {
31755
+ const change = getChange(state.coords, action.delta, {
31756
+ snapStep,
31757
+ range
31758
+ });
31759
+ newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
31760
+ }
31671
31761
  return {
31672
31762
  ...state,
31673
31763
  hasBeenInteractedWith: true,
@@ -31682,6 +31772,29 @@ function doMoveAll(state, action) {
31682
31772
  function doMovePoint(state, action) {
31683
31773
  switch (state.type) {
31684
31774
  case "polygon":
31775
+ let newValue;
31776
+ if (state.snapTo === "sides") {
31777
+ newValue = boundAndSnapToSides(action.destination, state, action.index);
31778
+ } else if (state.snapTo === "angles") {
31779
+ newValue = boundAndSnapToAngle(action.destination, state, action.index);
31780
+ } else {
31781
+ newValue = boundAndSnapToGrid(action.destination, state);
31782
+ }
31783
+ const newCoords = setAtIndex({
31784
+ array: state.coords,
31785
+ index: action.index,
31786
+ newValue: newValue
31787
+ });
31788
+
31789
+ // Reject the move if it would cause the sides of the polygon to cross
31790
+ if (polygonSidesIntersect(newCoords)) {
31791
+ return state;
31792
+ }
31793
+ return {
31794
+ ...state,
31795
+ hasBeenInteractedWith: true,
31796
+ coords: newCoords
31797
+ };
31685
31798
  case "point":
31686
31799
  {
31687
31800
  return {
@@ -31690,11 +31803,7 @@ function doMovePoint(state, action) {
31690
31803
  coords: setAtIndex({
31691
31804
  array: state.coords,
31692
31805
  index: action.index,
31693
- newValue: snap(state.snapStep, bound$1({
31694
- snapStep: state.snapStep,
31695
- range: state.range,
31696
- point: action.destination
31697
- }))
31806
+ newValue: boundAndSnapToGrid(action.destination, state)
31698
31807
  })
31699
31808
  };
31700
31809
  }
@@ -31714,27 +31823,28 @@ function doMovePoint(state, action) {
31714
31823
  coords: setAtIndex({
31715
31824
  array: state.coords,
31716
31825
  index: action.index,
31717
- newValue: snap(state.snapStep, bound$1({
31718
- snapStep: state.snapStep,
31719
- range: state.range,
31720
- point: destination
31721
- }))
31826
+ newValue: boundAndSnapToGrid(destination, state)
31722
31827
  })
31723
31828
  };
31724
31829
  }
31725
31830
  case "quadratic":
31726
31831
  {
31832
+ // Set up the new coords and check if the quadratic coefficients are valid
31833
+ const newCoords = [...state.coords];
31834
+ newCoords[action.index] = action.destination;
31835
+ const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
31836
+
31837
+ // If the new destination results in an invalid quadratic equation, we don't want to move the point
31838
+ if (QuadraticCoefficients === undefined) {
31839
+ return state;
31840
+ }
31727
31841
  return {
31728
31842
  ...state,
31729
31843
  hasBeenInteractedWith: true,
31730
31844
  coords: setAtIndex({
31731
31845
  array: state.coords,
31732
31846
  index: action.index,
31733
- newValue: snap(state.snapStep, bound$1({
31734
- snapStep: state.snapStep,
31735
- range: state.range,
31736
- point: action.destination
31737
- }))
31847
+ newValue: boundAndSnapToGrid(action.destination, state)
31738
31848
  })
31739
31849
  };
31740
31850
  }
@@ -31825,8 +31935,17 @@ function doChangeRange(state, action) {
31825
31935
  range: action.range
31826
31936
  };
31827
31937
  }
31828
- const getChange = (coords, delta, constraintOpts) => {
31938
+ const getDeltaVertex = (maxMoves, minMoves, delta) => {
31829
31939
  const [deltaX, deltaY] = delta;
31940
+ const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31941
+ const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31942
+ const minXMove = Math.max(...minMoves.map(move => move[0]));
31943
+ const minYMove = Math.max(...minMoves.map(move => move[1]));
31944
+ const dx = clamp(deltaX, minXMove, maxXMove);
31945
+ const dy = clamp(deltaY, minYMove, maxYMove);
31946
+ return [dx, dy];
31947
+ };
31948
+ const getChange = (coords, delta, constraintOpts) => {
31830
31949
  const maxMoves = coords.map(point => maxMove({
31831
31950
  ...constraintOpts,
31832
31951
  point
@@ -31835,35 +31954,175 @@ const getChange = (coords, delta, constraintOpts) => {
31835
31954
  ...constraintOpts,
31836
31955
  point
31837
31956
  }));
31838
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31839
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31840
- const minXMove = Math.max(...minMoves.map(move => move[0]));
31841
- const minYMove = Math.max(...minMoves.map(move => move[1]));
31842
- const dx = clamp(deltaX, minXMove, maxXMove);
31843
- const dy = clamp(deltaY, minYMove, maxYMove);
31957
+ const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
31844
31958
  return [dx, dy];
31845
31959
  };
31960
+ const eq$1 = Util.eq;
31961
+
31962
+ // Less than or approximately equal
31963
+ function leq$1(a, b) {
31964
+ return a < b || eq$1(a, b);
31965
+ }
31966
+ function boundAndSnapToGrid(point, _ref) {
31967
+ let {
31968
+ snapStep,
31969
+ range
31970
+ } = _ref;
31971
+ return snap(snapStep, bound$1({
31972
+ snapStep,
31973
+ range,
31974
+ point
31975
+ }));
31976
+ }
31977
+ function boundAndSnapToAngle(destinationPoint, _ref2, index) {
31978
+ let {
31979
+ range,
31980
+ coords
31981
+ } = _ref2;
31982
+ const startingPoint = coords[index];
31983
+
31984
+ // Needed to prevent updating the original coords before the checks for
31985
+ // degenerate triangles and overlapping sides
31986
+ const coordsCopy = [...coords];
31987
+
31988
+ // Takes the destination point and makes sure it is within the bounds of the graph
31989
+ // SnapStep is [0, 0] because we don't want to snap to the grid
31990
+ coordsCopy[index] = bound$1({
31991
+ snapStep: [0, 0],
31992
+ range,
31993
+ point: destinationPoint
31994
+ });
31995
+
31996
+ // Gets the radian angles between the coords and maps them to degrees
31997
+ const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
31998
+
31999
+ // Gets the relative index of a point
32000
+ const rel = j => {
32001
+ return (index + j + coordsCopy.length) % coordsCopy.length;
32002
+ };
32003
+
32004
+ // Round the angles to left and right of the current point
32005
+ ___default["default"].each([-1, 1], function (j) {
32006
+ angles[rel(j)] = Math.round(angles[rel(j)]);
32007
+ });
32008
+ const getAngle = function (a, vertex, b) {
32009
+ const angle = GraphUtils.findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
32010
+ return (angle + 360) % 360;
32011
+ };
32012
+ const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
32013
+ innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
32014
+
32015
+ // Avoid degenerate triangles
32016
+ if (innerAngles.some(function (angle) {
32017
+ return leq$1(angle, 1);
32018
+ })) {
32019
+ return startingPoint;
32020
+ }
32021
+ const knownSide = magnitude(
32022
+ // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
32023
+ vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
32024
+
32025
+ // Returns true if the points form a counter-clockwise turn;
32026
+ // a.k.a if the point is on the left or right of the polygon.
32027
+ // This is used to determine how to adjust the point: if the point is on the left,
32028
+ // we want to add the inner angle to the outer angle, and if it's on the right,
32029
+ // we want to subtract the inner angle from the outer angle. The angle solved
32030
+ // for is then used in the polar function to determine the new point.
32031
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
32032
+
32033
+ // Solve for side by using the law of sines
32034
+ const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
32035
+
32036
+ // Angle at the second vertex of the polygon
32037
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32038
+
32039
+ // Uses the length of the side of the polygon (radial coordinate)
32040
+ // and the angle between the first and second sides of the
32041
+ // polygon (angular coordinate) to determine how to adjust the point
32042
+ const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
32043
+ return kmath.vector.add(coordsCopy[rel(-1)], offset);
32044
+ }
32045
+ function boundAndSnapToSides(destinationPoint, _ref3, index) {
32046
+ let {
32047
+ range,
32048
+ coords
32049
+ } = _ref3;
32050
+ const startingPoint = coords[index];
32051
+
32052
+ // Needed to prevent updating the original coords before the checks for
32053
+ // degenerate triangles and overlapping sides
32054
+ const coordsCopy = [...coords];
32055
+
32056
+ // Takes the destination point and makes sure it is within the bounds of the graph
32057
+ // SnapStep is [0, 0] because we don't want to snap to the grid
32058
+ coordsCopy[index] = bound$1({
32059
+ snapStep: [0, 0],
32060
+ range,
32061
+ point: destinationPoint
32062
+ });
32063
+
32064
+ // Gets the relative index of a point
32065
+ const rel = j => {
32066
+ return (index + j + coordsCopy.length) % coordsCopy.length;
32067
+ };
32068
+ const sides = ___default["default"].map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
32069
+ // @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.
32070
+ return magnitude(vector(...coordsCopy));
32071
+ });
32072
+
32073
+ // Round the sides to left and right of the current point
32074
+ ___default["default"].each([0, 1], function (j) {
32075
+ sides[j] = Math.round(sides[j]);
32076
+ });
32077
+
32078
+ // Avoid degenerate triangles
32079
+ 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])) {
32080
+ return startingPoint;
32081
+ }
32082
+
32083
+ // Solve for angle by using the law of cosines
32084
+ // Angle at the first vertex of the polygon
32085
+ const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
32086
+
32087
+ // Angle at the second vertex of the polygon
32088
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32089
+
32090
+ // Returns true if the points form a counter-clockwise turn;
32091
+ // a.k.a if the point is on the left or right of the polygon.
32092
+ // This is used to determine how to adjust the point: if the point is on the left,
32093
+ // we want to add the inner angle to the outer angle, and if it's on the right,
32094
+ // we want to subtract the inner angle from the outer angle. The angle solved
32095
+ // for is then used in the polar function to determine the new point.
32096
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
32097
+
32098
+ // Uses the length of the first side of the polygon (radial coordinate)
32099
+ // and the angle between the first and second sides of the
32100
+ // polygon (angular coordinate) to determine how to adjust the point
32101
+ const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
32102
+ return kmath.vector.add(coordsCopy[rel(-1)], offset);
32103
+ }
32104
+
31846
32105
  // Returns the closest point to the given `point` that is within the graph
31847
32106
  // bounds given in `state`.
31848
- function bound$1(_ref) {
32107
+ function bound$1(_ref4) {
31849
32108
  let {
31850
32109
  snapStep,
31851
32110
  range,
31852
32111
  point
31853
- } = _ref;
32112
+ } = _ref4;
31854
32113
  const [requestedX, requestedY] = point;
31855
32114
  const [snapX, snapY] = snapStep;
31856
32115
  const [[minX, maxX], [minY, maxY]] = range;
31857
32116
  return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
31858
32117
  }
31859
32118
 
31860
- // Returns the vector from the given point to the top-right corner of the graph
31861
- function maxMove(_ref2) {
32119
+ // Returns the vector from the given point to the top-right corner of the graph when snapped to the grid
32120
+ function maxMove(_ref5) {
31862
32121
  let {
31863
32122
  snapStep,
31864
32123
  range,
31865
32124
  point
31866
- } = _ref2;
32125
+ } = _ref5;
31867
32126
  const topRight = bound$1({
31868
32127
  snapStep,
31869
32128
  range,
@@ -31872,14 +32131,13 @@ function maxMove(_ref2) {
31872
32131
  return mafs.vec.sub(topRight, point);
31873
32132
  }
31874
32133
 
31875
- // Returns the vector from the given point to the bottom-left corner of the
31876
- // graph
31877
- function minMove(_ref3) {
32134
+ // Returns the vector from the given point to the bottom-left corner of the graph when snapped to the grid
32135
+ function minMove(_ref6) {
31878
32136
  let {
31879
32137
  snapStep,
31880
32138
  range,
31881
32139
  point
31882
- } = _ref3;
32140
+ } = _ref6;
31883
32141
  const bottomLeft = bound$1({
31884
32142
  snapStep,
31885
32143
  range,
@@ -31935,8 +32193,12 @@ const renderGraph = props => {
31935
32193
  graphState: state,
31936
32194
  dispatch: dispatch
31937
32195
  });
31938
- case "linear":
31939
32196
  case "linear-system":
32197
+ return /*#__PURE__*/React__namespace.createElement(LinearSystemGraph, {
32198
+ graphState: state,
32199
+ dispatch: dispatch
32200
+ });
32201
+ case "linear":
31940
32202
  return /*#__PURE__*/React__namespace.createElement(LinearGraph, {
31941
32203
  graphState: state,
31942
32204
  dispatch: dispatch
@@ -44184,7 +44446,7 @@ const init = function (options) {
44184
44446
 
44185
44447
  // This file is processed by a Rollup plugin (replace) to inject the production
44186
44448
  const libName = "@khanacademy/perseus";
44187
- const libVersion = "23.1.0";
44449
+ const libVersion = "23.4.0";
44188
44450
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44189
44451
 
44190
44452
  const apiVersion = {
@@ -47284,6 +47546,7 @@ exports.itemDataVersion = itemDataVersion;
47284
47546
  exports.itemToTree = itemToTree;
47285
47547
  exports.itemVersion = ItemVersion;
47286
47548
  exports.libVersion = libVersion;
47549
+ exports.lockedEllipseFillStyles = lockedEllipseFillStyles;
47287
47550
  exports.lockedFigureColors = lockedFigureColors;
47288
47551
  exports.mediaQueries = mediaQueries;
47289
47552
  exports.parsePerseusItem = parsePerseusItem;