@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/es/index.js CHANGED
@@ -29,7 +29,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
29
29
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
30
30
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
31
31
  import debounce$2 from 'lodash.debounce';
32
- import { vec, Circle as Circle$1, Line as Line$4, Point as Point$2, useMovable, Text as Text$1, Polygon, Plot as Plot$3, Coordinates, Mafs } from 'mafs';
32
+ import { vec, Ellipse, Line as Line$4, Point as Point$2, useMovable, Text as Text$1, Polygon, Plot as Plot$3, Coordinates, Mafs } from 'mafs';
33
33
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
34
34
  import { Body, LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
35
35
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
@@ -3202,7 +3202,7 @@ const DeprecationMixin$1 = {
3202
3202
  /**
3203
3203
  * Approximate equality on numbers and primitives.
3204
3204
  */
3205
- function eq$2(x, y) {
3205
+ function eq$3(x, y) {
3206
3206
  if (typeof x === "number" && typeof y === "number") {
3207
3207
  return Math.abs(x - y) < 1e-9;
3208
3208
  }
@@ -3229,7 +3229,7 @@ function deepEq$5(x, y) {
3229
3229
  return false;
3230
3230
  }
3231
3231
  if (typeof x === "function" && typeof y === "function") {
3232
- return eq$2(x, y);
3232
+ return eq$3(x, y);
3233
3233
  }
3234
3234
  if (typeof x === "function" || typeof y === "function") {
3235
3235
  return false;
@@ -3246,7 +3246,7 @@ function deepEq$5(x, y) {
3246
3246
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3247
3247
  return false;
3248
3248
  }
3249
- return eq$2(x, y);
3249
+ return eq$3(x, y);
3250
3250
  }
3251
3251
 
3252
3252
  /**
@@ -3547,7 +3547,7 @@ const Util = {
3547
3547
  tickStepFromNumTicks,
3548
3548
  constrainedTickStepsFromTickSteps,
3549
3549
  DeprecationMixin: DeprecationMixin$1,
3550
- eq: eq$2,
3550
+ eq: eq$3,
3551
3551
  deepEq: deepEq$5,
3552
3552
  parseQueryString,
3553
3553
  updateQueryString: updateQueryString$2,
@@ -10784,7 +10784,7 @@ class WrappedLine extends WrappedDrawing {
10784
10784
  * A collection of geomtry-related utility functions
10785
10785
  */
10786
10786
  const {
10787
- eq: eq$1,
10787
+ eq: eq$2,
10788
10788
  deepEq: deepEq$4
10789
10789
  } = Util;
10790
10790
 
@@ -10792,17 +10792,21 @@ const {
10792
10792
 
10793
10793
  // a, b, c
10794
10794
 
10795
+ // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10795
10796
  function sign(val) {
10796
- if (eq$1(val, 0)) {
10797
+ if (eq$2(val, 0)) {
10797
10798
  return 0;
10798
10799
  }
10799
10800
  return val > 0 ? 1 : -1;
10800
10801
  }
10802
+
10803
+ // Determine whether three points are collinear (0), for a clockwise turn (negative),
10804
+ // or counterclockwise turn (positive)
10801
10805
  function ccw(a, b, c) {
10802
10806
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10803
10807
  }
10804
10808
  function collinear(a, b, c) {
10805
- return eq$1(ccw(a, b, c), 0);
10809
+ return eq$2(ccw(a, b, c), 0);
10806
10810
  }
10807
10811
 
10808
10812
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -10827,6 +10831,33 @@ function intersects(ab, cd) {
10827
10831
  }
10828
10832
  return false;
10829
10833
  }
10834
+
10835
+ // Whether any two sides of a polygon intersect each other
10836
+ function polygonSidesIntersect(vertices) {
10837
+ for (let i = 0; i < vertices.length; i++) {
10838
+ for (let k = i + 1; k < vertices.length; k++) {
10839
+ // If any two vertices are the same point, sides overlap
10840
+ if (point.equal(vertices[i], vertices[k])) {
10841
+ return true;
10842
+ }
10843
+
10844
+ // Find the other end of the sides starting at vertices i and k
10845
+ const iNext = (i + 1) % vertices.length;
10846
+ const kNext = (k + 1) % vertices.length;
10847
+
10848
+ // Adjacent sides always intersect (at the vertex); skip those
10849
+ if (iNext === k || kNext === i) {
10850
+ continue;
10851
+ }
10852
+ const side1 = [vertices[i], vertices[iNext]];
10853
+ const side2 = [vertices[k], vertices[kNext]];
10854
+ if (intersects(side1, side2)) {
10855
+ return true;
10856
+ }
10857
+ }
10858
+ }
10859
+ return false;
10860
+ }
10830
10861
  function vector(a, b) {
10831
10862
  return _.map(_.zip(a, b), function (pair) {
10832
10863
  return pair[0] - pair[1];
@@ -10920,7 +10951,7 @@ function similar(coords1, coords2, tolerance) {
10920
10951
  return pair[0] / pair[1];
10921
10952
  });
10922
10953
  const same = _.all(factors, function (factor) {
10923
- return eq$1(factors[0], factor);
10954
+ return eq$2(factors[0], factor);
10924
10955
  });
10925
10956
  const congruentEnough = _.all(sidePairs, function (pair) {
10926
10957
  return number.equal(pair[0], pair[1], tolerance);
@@ -10972,7 +11003,7 @@ function rotate(array, n) {
10972
11003
  return array.slice(n).concat(array.slice(0, n));
10973
11004
  }
10974
11005
  function getLineEquation(first, second) {
10975
- if (eq$1(first[0], second[0])) {
11006
+ if (eq$2(first[0], second[0])) {
10976
11007
  return "x = " + first[0].toFixed(3);
10977
11008
  }
10978
11009
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -28631,25 +28662,27 @@ const lockedFigureColors = {
28631
28662
  pink: "#B25071",
28632
28663
  red: "#D92916"
28633
28664
  };
28665
+ const lockedEllipseFillStyles = {
28666
+ none: 0,
28667
+ solid: 1,
28668
+ translucent: 0.4
28669
+ };
28634
28670
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28635
28671
 
28636
- const LockedCircle = props => {
28672
+ const LockedEllipse = props => {
28637
28673
  const {
28638
28674
  center,
28639
28675
  radius,
28676
+ angle,
28640
28677
  color,
28641
28678
  fillStyle,
28642
28679
  strokeStyle
28643
28680
  } = props;
28644
- const fill = {
28645
- none: 0,
28646
- solid: 1,
28647
- translucent: 0.4
28648
- };
28649
- return /*#__PURE__*/React.createElement(Circle$1, {
28681
+ return /*#__PURE__*/React.createElement(Ellipse, {
28650
28682
  center: center,
28651
28683
  radius: radius,
28652
- fillOpacity: fill[fillStyle],
28684
+ angle: angle,
28685
+ fillOpacity: lockedEllipseFillStyles[fillStyle],
28653
28686
  strokeStyle: strokeStyle,
28654
28687
  color: lockedFigureColors[color]
28655
28688
  });
@@ -28909,6 +28942,21 @@ const LockedPoint = props => {
28909
28942
  });
28910
28943
  };
28911
28944
 
28945
+ const LockedVector = props => {
28946
+ const {
28947
+ color,
28948
+ points
28949
+ } = props;
28950
+ const [tail, tip] = points;
28951
+ return /*#__PURE__*/React.createElement("g", {
28952
+ className: "locked-vector"
28953
+ }, /*#__PURE__*/React.createElement(Vector, {
28954
+ tail: tail,
28955
+ tip: tip,
28956
+ color: lockedFigureColors[color]
28957
+ }));
28958
+ };
28959
+
28912
28960
  const GraphLockedLayer = props => {
28913
28961
  const {
28914
28962
  lockedFigures
@@ -28924,9 +28972,13 @@ const GraphLockedLayer = props => {
28924
28972
  key: `line-${index}`,
28925
28973
  range: props.range
28926
28974
  }, figure));
28927
- case "circle":
28928
- return /*#__PURE__*/React.createElement(LockedCircle, _extends({
28929
- key: `circle-${index}`
28975
+ case "ellipse":
28976
+ return /*#__PURE__*/React.createElement(LockedEllipse, _extends({
28977
+ key: `ellipse-${index}`
28978
+ }, figure));
28979
+ case "vector":
28980
+ return /*#__PURE__*/React.createElement(LockedVector, _extends({
28981
+ key: `vector-${index}`
28930
28982
  }, figure));
28931
28983
  default:
28932
28984
  /**
@@ -29056,7 +29108,8 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
29056
29108
  point,
29057
29109
  color: color$1 = color.blue,
29058
29110
  dragging,
29059
- focusBehavior
29111
+ focusBehavior,
29112
+ cursor
29060
29113
  } = props;
29061
29114
 
29062
29115
  // WB Tooltip requires a color name for the background color.
@@ -29088,7 +29141,8 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
29088
29141
  ref: hitboxRef,
29089
29142
  className: pointClasses,
29090
29143
  style: {
29091
- "--movable-point-color": color$1
29144
+ "--movable-point-color": color$1,
29145
+ cursor
29092
29146
  },
29093
29147
  "data-testid": "movable-point",
29094
29148
  tabIndex: tabIndex(focusBehavior)
@@ -29200,6 +29254,7 @@ function useControlPoint(point, color, onMovePoint) {
29200
29254
  });
29201
29255
  const focusableHandle = /*#__PURE__*/React.createElement("g", {
29202
29256
  "data-testid": "movable-point__focusable-handle",
29257
+ className: "movable-point__focusable-handle",
29203
29258
  tabIndex: 0,
29204
29259
  ref: keyboardHandleRef,
29205
29260
  onFocus: () => setFocused(true),
@@ -29336,14 +29391,39 @@ const SegmentGraph = props => {
29336
29391
  })));
29337
29392
  };
29338
29393
 
29339
- const LinearGraph = props => {
29394
+ const LinearGraph = (props, key) => {
29395
+ const {
29396
+ dispatch
29397
+ } = props;
29398
+ const {
29399
+ coords: line
29400
+ } = props.graphState;
29401
+
29402
+ // Linear graphs only have one line
29403
+ // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
29404
+ // and another action for moving multiple lines
29405
+ return /*#__PURE__*/React.createElement(MovableLine, {
29406
+ key: 0,
29407
+ points: line,
29408
+ onMoveLine: delta => {
29409
+ dispatch(moveLine(0, delta));
29410
+ },
29411
+ extend: {
29412
+ start: true,
29413
+ end: true
29414
+ },
29415
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, 0)),
29416
+ color: "var(--movable-line-stroke-color)"
29417
+ });
29418
+ };
29419
+
29420
+ const LinearSystemGraph = props => {
29340
29421
  const {
29341
29422
  dispatch
29342
29423
  } = props;
29343
29424
  const {
29344
29425
  coords: lines
29345
29426
  } = props.graphState;
29346
- const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29347
29427
  return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(MovableLine, {
29348
29428
  key: i,
29349
29429
  points: line,
@@ -29355,8 +29435,8 @@ const LinearGraph = props => {
29355
29435
  end: true
29356
29436
  },
29357
29437
  onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29358
- color: colors[i]
29359
- })));
29438
+ color: "var(--movable-line-stroke-color)"
29439
+ })), ";");
29360
29440
  };
29361
29441
 
29362
29442
  const RayGraph = props => {
@@ -29364,14 +29444,14 @@ const RayGraph = props => {
29364
29444
  dispatch
29365
29445
  } = props;
29366
29446
  const {
29367
- coords: lines
29447
+ coords: line
29368
29448
  } = props.graphState;
29369
29449
  const handleMoveLine = delta => dispatch(moveLine(0, delta));
29370
29450
  const handleMovePoint = (pointIndex, newPoint) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29371
- return /*#__PURE__*/React.createElement(MovableLine
29372
- // a ray only has one line
29373
- , {
29374
- points: lines[0],
29451
+
29452
+ // Ray graphs only have one line
29453
+ return /*#__PURE__*/React.createElement(MovableLine, {
29454
+ points: line,
29375
29455
  onMoveLine: handleMoveLine,
29376
29456
  onMovePoint: handleMovePoint,
29377
29457
  extend: {
@@ -29427,7 +29507,8 @@ const Angle = ({
29427
29507
  endPoints,
29428
29508
  range,
29429
29509
  polygonLines,
29430
- showAngles
29510
+ showAngles,
29511
+ snapTo
29431
29512
  }) => {
29432
29513
  const [centerX, centerY] = centerPoint;
29433
29514
  const areClockwise = clockwise$1([centerPoint, ...endPoints]);
@@ -29465,7 +29546,9 @@ const Angle = ({
29465
29546
  if (isOutside) {
29466
29547
  angleInDegrees = 360 - angleInDegrees;
29467
29548
  }
29468
- const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
29549
+
29550
+ // Only want to show whole numbers when in angles mode
29551
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
29469
29552
  const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
29470
29553
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
29471
29554
  id: "background",
@@ -29550,15 +29633,18 @@ const StyledMovablePoint = props => {
29550
29633
  const {
29551
29634
  point,
29552
29635
  onMove,
29553
- color: color$1 = color.blue
29636
+ cursor,
29637
+ color: color$1 = color.blue,
29638
+ snapTo
29554
29639
  } = props;
29640
+ const snapToValue = snapTo != null ? snapTo : "grid";
29555
29641
  const {
29556
29642
  dragging
29557
29643
  } = useMovable({
29558
29644
  gestureTarget: elementRef,
29559
29645
  point,
29560
29646
  onMove,
29561
- constrain: p => snap(snapStep, p)
29647
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29562
29648
  });
29563
29649
  return /*#__PURE__*/React.createElement(MovablePointView, {
29564
29650
  ref: elementRef,
@@ -29568,7 +29654,8 @@ const StyledMovablePoint = props => {
29568
29654
  focusBehavior: {
29569
29655
  type: "uncontrolled",
29570
29656
  tabIndex: 0
29571
- }
29657
+ },
29658
+ cursor: cursor
29572
29659
  });
29573
29660
  };
29574
29661
 
@@ -29583,7 +29670,8 @@ const PolygonGraph = props => {
29583
29670
  showAngles,
29584
29671
  showSides,
29585
29672
  range,
29586
- snapStep
29673
+ snapStep,
29674
+ snapTo
29587
29675
  } = props.graphState;
29588
29676
 
29589
29677
  // TODO(benchristel): can the default set of points be removed here? I don't
@@ -29591,6 +29679,7 @@ const PolygonGraph = props => {
29591
29679
  const points = coords != null ? coords : [[0, 0]];
29592
29680
  const ref = React.useRef(null);
29593
29681
  const dragReferencePoint = points[0];
29682
+ const snapToValue = snapTo != null ? snapTo : "grid";
29594
29683
  const {
29595
29684
  dragging
29596
29685
  } = useMovable({
@@ -29600,7 +29689,7 @@ const PolygonGraph = props => {
29600
29689
  const delta = vec.sub(newPoint, dragReferencePoint);
29601
29690
  dispatch(moveAll(delta));
29602
29691
  },
29603
- constrain: p => snap(snapStep, p)
29692
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29604
29693
  });
29605
29694
  const active = hovered || focused || dragging;
29606
29695
  const lines = getLines(points);
@@ -29623,11 +29712,12 @@ const PolygonGraph = props => {
29623
29712
  active: active,
29624
29713
  range: range,
29625
29714
  polygonLines: lines,
29626
- showAngles: !!showAngles
29715
+ showAngles: !!showAngles,
29716
+ snapTo: snapToValue
29627
29717
  });
29628
29718
  }), showSides && lines.map(([start, end], i) => {
29629
29719
  const [x, y] = vec.midpoint(start, end);
29630
- const length = parseFloat(vec.dist(start, end).toFixed(1));
29720
+ const length = parseFloat(vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
29631
29721
  return /*#__PURE__*/React.createElement(TextLabel, {
29632
29722
  key: "side-" + i,
29633
29723
  x: x,
@@ -29651,131 +29741,12 @@ const PolygonGraph = props => {
29651
29741
  }
29652
29742
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
29653
29743
  key: "point-" + i,
29744
+ snapTo: snapTo,
29654
29745
  point: point,
29655
29746
  onMove: destination => dispatch(movePoint(i, destination))
29656
29747
  })));
29657
29748
  };
29658
29749
 
29659
- function initializeGraphState(params) {
29660
- const {
29661
- graph,
29662
- step,
29663
- snapStep,
29664
- range
29665
- } = params;
29666
- const shared = {
29667
- hasBeenInteractedWith: false,
29668
- range,
29669
- snapStep
29670
- };
29671
- switch (graph.type) {
29672
- case "segment":
29673
- return _extends({}, shared, {
29674
- type: "segment",
29675
- coords: getDefaultSegments({
29676
- graph,
29677
- step,
29678
- range
29679
- })
29680
- });
29681
- case "linear":
29682
- case "linear-system":
29683
- case "ray":
29684
- return _extends({}, shared, {
29685
- type: graph.type,
29686
- // Linear and ray graphs have a single tuple of points, while a
29687
- // linear system has two tuples of points.
29688
- coords: getLineCoords({
29689
- graph,
29690
- range,
29691
- step
29692
- })
29693
- });
29694
- case "polygon":
29695
- return _extends({}, shared, {
29696
- type: "polygon",
29697
- showAngles: Boolean(graph.showAngles),
29698
- showSides: Boolean(graph.showSides),
29699
- coords: getPolygonCoords({
29700
- graph,
29701
- range,
29702
- step
29703
- })
29704
- });
29705
- case "point":
29706
- return _extends({}, shared, {
29707
- type: graph.type,
29708
- coords: getDefaultPoints({
29709
- graph,
29710
- step,
29711
- range
29712
- })
29713
- });
29714
- case "circle":
29715
- return _extends({}, shared, {
29716
- type: graph.type,
29717
- center: [0, 0],
29718
- radiusPoint: [1, 0]
29719
- });
29720
- case "quadratic":
29721
- return _extends({}, shared, {
29722
- type: graph.type,
29723
- coords: getQuadraticCoords(graph, range, step)
29724
- });
29725
- case "sinusoid":
29726
- return _extends({}, shared, {
29727
- type: graph.type,
29728
- coords: getSinusoidCoords(graph, range, step)
29729
- });
29730
- case "angle":
29731
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
29732
- default:
29733
- throw new UnreachableCaseError(graph);
29734
- }
29735
- }
29736
- const getDefaultPoints = ({
29737
- graph,
29738
- range,
29739
- step
29740
- }) => {
29741
- var _graph$coords;
29742
- const numPoints = graph.numPoints || 1;
29743
- let coords = (_graph$coords = graph.coords) == null ? void 0 : _graph$coords.slice();
29744
- if (coords) {
29745
- return coords;
29746
- }
29747
- switch (numPoints) {
29748
- case 1:
29749
- // Back in the day, one point's coords were in graph.coord
29750
- coords = [graph.coord || [0, 0]];
29751
- break;
29752
- case 2:
29753
- coords = [[-5, 0], [5, 0]];
29754
- break;
29755
- case 3:
29756
- coords = [[-5, 0], [0, 0], [5, 0]];
29757
- break;
29758
- case 4:
29759
- coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
29760
- break;
29761
- case 5:
29762
- coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
29763
- break;
29764
- case 6:
29765
- coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
29766
- break;
29767
- default:
29768
- coords = [];
29769
- break;
29770
- }
29771
- // Transform coords from their -10 to 10 space to 0 to 1
29772
- // because of the old graph.coord, and also it's easier.
29773
- const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29774
- return normalizePoints(range, step, newCoords);
29775
- };
29776
-
29777
- // TS v4 doesn't narrow return types, while v5 does.
29778
- // Instead of updating to v5, using generic type to relate input and output types.
29779
29750
  function getGradableGraph(state, initialGraph) {
29780
29751
  if (!state.hasBeenInteractedWith) {
29781
29752
  return _extends({}, initialGraph);
@@ -29792,12 +29763,12 @@ function getGradableGraph(state, initialGraph) {
29792
29763
  }
29793
29764
  if (state.type === "linear" && initialGraph.type === "linear") {
29794
29765
  return _extends({}, initialGraph, {
29795
- coords: state.coords[0]
29766
+ coords: state.coords
29796
29767
  });
29797
29768
  }
29798
29769
  if (state.type === "ray" && initialGraph.type === "ray") {
29799
29770
  return _extends({}, initialGraph, {
29800
- coords: state.coords[0]
29771
+ coords: state.coords
29801
29772
  });
29802
29773
  }
29803
29774
  if (state.type === "polygon" && initialGraph.type === "polygon") {
@@ -29828,91 +29799,6 @@ function getGradableGraph(state, initialGraph) {
29828
29799
  }
29829
29800
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29830
29801
  }
29831
- const getDefaultSegments = ({
29832
- graph,
29833
- range,
29834
- step
29835
- }) => {
29836
- const ys = n => {
29837
- switch (n) {
29838
- case 2:
29839
- return [5, -5];
29840
- case 3:
29841
- return [5, 0, -5];
29842
- case 4:
29843
- return [6, 2, -2, -6];
29844
- case 5:
29845
- return [6, 3, 0, -3, -6];
29846
- case 6:
29847
- return [5, 3, 1, -1, -3, -5];
29848
- default:
29849
- return [5];
29850
- }
29851
- };
29852
- const defaultRange = [[-10, 10], [-10, 10]];
29853
- return ys(graph.numSegments).map(y => {
29854
- let endpoints = [[-5, y], [5, y]];
29855
- endpoints = normalizeCoords(endpoints, defaultRange);
29856
- endpoints = normalizePoints(range, step, endpoints);
29857
- return endpoints;
29858
- });
29859
- };
29860
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29861
- const getLineCoords = ({
29862
- graph,
29863
- range,
29864
- step
29865
- }) => {
29866
- // Return two lines for a linear system, one for a ray or linear
29867
- switch (graph.type) {
29868
- case "linear-system":
29869
- return defaultLinearCoords.map(points => normalizePoints(range, step, points));
29870
- case "linear":
29871
- case "ray":
29872
- return [normalizePoints(range, step, defaultLinearCoords[0])];
29873
- default:
29874
- throw new UnreachableCaseError(graph);
29875
- }
29876
- };
29877
- const getPolygonCoords = ({
29878
- graph,
29879
- range,
29880
- step
29881
- }) => {
29882
- var _graph$coords2;
29883
- let coords = (_graph$coords2 = graph.coords) == null ? void 0 : _graph$coords2.slice();
29884
- if (coords) {
29885
- return coords;
29886
- }
29887
- const n = graph.numSides || 3;
29888
- if (n === "unlimited") {
29889
- coords = [];
29890
- } else {
29891
- const angle = 2 * Math.PI / n;
29892
- const offset = (1 / n - 1 / 2) * Math.PI;
29893
-
29894
- // TODO(alex): Generalize this to more than just triangles so that
29895
- // all polygons have whole number side lengths if snapping to sides
29896
- const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
29897
-
29898
- // Generate coords of a regular polygon with n sides
29899
- coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
29900
- }
29901
- coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29902
- const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
29903
- coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
29904
- return coords;
29905
- };
29906
- const getSinusoidCoords = (graph, range, step) => {
29907
- let coords = [[0.5, 0.5], [0.65, 0.6]];
29908
- coords = normalizePoints(range, step, coords, true);
29909
- return coords;
29910
- };
29911
- const getQuadraticCoords = (graph, range, step) => {
29912
- let coords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
29913
- coords = normalizePoints(range, step, coords, true);
29914
- return coords;
29915
- };
29916
29802
 
29917
29803
  /**
29918
29804
  * determine radius of a circle graph
@@ -29941,6 +29827,7 @@ function CircleGraph(props) {
29941
29827
  onMove: newCenter => dispatch(moveCenter(newCenter))
29942
29828
  }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29943
29829
  point: radiusPoint,
29830
+ cursor: "ew-resize",
29944
29831
  onMove: newRadiusPoint => {
29945
29832
  dispatch(moveRadiusPoint(newRadiusPoint));
29946
29833
  }
@@ -30045,38 +29932,16 @@ function QuadraticGraph(props) {
30045
29932
 
30046
29933
  // Calculate the y value based on the current x value
30047
29934
  const y = x => (a * x + b) * x + c;
30048
- const handleOnMove = (destination, elementId) => {
30049
- // If the destination is invalid, we want do not want to move the point
30050
- const validDestination = isValidDestination(destination, elementId, coords);
30051
- if (validDestination === false) {
30052
- return;
30053
- }
30054
- dispatch(movePoint(elementId, destination));
30055
- };
30056
29935
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
30057
29936
  y: y,
30058
29937
  color: color.blue
30059
29938
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
30060
29939
  key: "point-" + i,
30061
29940
  point: coord,
30062
- onMove: destination => handleOnMove(destination, i)
29941
+ onMove: destination => dispatch(movePoint(i, destination))
30063
29942
  })));
30064
29943
  }
30065
29944
 
30066
- // Ensure that we are only snapping to coordinates that result in a valid quadratic equation
30067
- const isValidDestination = (destination, elementId, coords) => {
30068
- // Set up the new coords and check if the quadratic coefficients are valid
30069
- const newCoords = [...coords];
30070
- newCoords[elementId] = destination;
30071
- const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
30072
-
30073
- // If the new destination results in an invalid quadratic equation, we don't want to move the point
30074
- if (QuadraticCoefficients === undefined) {
30075
- return false;
30076
- }
30077
- return true;
30078
- };
30079
-
30080
29945
  // Get the quadratic coefficients from the 3 control points
30081
29946
  // These equations were originally set up in 2013 and may require some
30082
29947
  // additional comments to help clarify the quadratic formula manipulations
@@ -31220,6 +31085,203 @@ const LegacyGrid = ({
31220
31085
  return null;
31221
31086
  };
31222
31087
 
31088
+ function initializeGraphState(params) {
31089
+ var _graph$snapTo;
31090
+ const {
31091
+ graph,
31092
+ step,
31093
+ snapStep,
31094
+ range
31095
+ } = params;
31096
+ const shared = {
31097
+ hasBeenInteractedWith: false,
31098
+ range,
31099
+ snapStep
31100
+ };
31101
+ switch (graph.type) {
31102
+ case "segment":
31103
+ return _extends({}, shared, {
31104
+ type: "segment",
31105
+ coords: getSegmentCoords(graph, range, step)
31106
+ });
31107
+ case "linear":
31108
+ return _extends({}, shared, {
31109
+ type: graph.type,
31110
+ coords: getLineCoords(graph, range, step)
31111
+ });
31112
+ case "ray":
31113
+ return _extends({}, shared, {
31114
+ type: graph.type,
31115
+ coords: getLineCoords(graph, range, step)
31116
+ });
31117
+ case "linear-system":
31118
+ return _extends({}, shared, {
31119
+ type: graph.type,
31120
+ coords: getLinearSystemCoords(graph, range, step)
31121
+ });
31122
+ case "polygon":
31123
+ return _extends({}, shared, {
31124
+ type: "polygon",
31125
+ showAngles: Boolean(graph.showAngles),
31126
+ showSides: Boolean(graph.showSides),
31127
+ coords: getPolygonCoords(graph, range, step),
31128
+ snapTo: (_graph$snapTo = graph.snapTo) != null ? _graph$snapTo : "grid"
31129
+ });
31130
+ case "point":
31131
+ return _extends({}, shared, {
31132
+ type: graph.type,
31133
+ coords: getPointCoords(graph, range, step)
31134
+ });
31135
+ case "circle":
31136
+ return _extends({}, shared, {
31137
+ type: graph.type
31138
+ }, getCircleCoords(graph));
31139
+ case "quadratic":
31140
+ return _extends({}, shared, {
31141
+ type: graph.type,
31142
+ coords: getQuadraticCoords(graph, range, step)
31143
+ });
31144
+ case "sinusoid":
31145
+ return _extends({}, shared, {
31146
+ type: graph.type,
31147
+ coords: getSinusoidCoords(graph, range, step)
31148
+ });
31149
+ case "angle":
31150
+ throw new Error("Mafs not yet implemented for graph type: " + graph.type);
31151
+ default:
31152
+ throw new UnreachableCaseError(graph);
31153
+ }
31154
+ }
31155
+ function getPointCoords(graph, range, step) {
31156
+ var _graph$coords;
31157
+ const numPoints = graph.numPoints || 1;
31158
+ let coords = (_graph$coords = graph.coords) == null ? void 0 : _graph$coords.slice();
31159
+ if (coords) {
31160
+ return coords;
31161
+ }
31162
+ switch (numPoints) {
31163
+ case 1:
31164
+ // Back in the day, one point's coords were in graph.coord
31165
+ coords = [graph.coord || [0, 0]];
31166
+ break;
31167
+ case 2:
31168
+ coords = [[-5, 0], [5, 0]];
31169
+ break;
31170
+ case 3:
31171
+ coords = [[-5, 0], [0, 0], [5, 0]];
31172
+ break;
31173
+ case 4:
31174
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
31175
+ break;
31176
+ case 5:
31177
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
31178
+ break;
31179
+ case 6:
31180
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
31181
+ break;
31182
+ default:
31183
+ coords = [];
31184
+ break;
31185
+ }
31186
+ // Transform coords from their -10 to 10 space to 0 to 1
31187
+ // because of the old graph.coord, and also it's easier.
31188
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31189
+ return normalizePoints(range, step, newCoords);
31190
+ }
31191
+ function getSegmentCoords(graph, range, step) {
31192
+ if (graph.coords) {
31193
+ return graph.coords;
31194
+ }
31195
+ const ys = n => {
31196
+ switch (n) {
31197
+ case 2:
31198
+ return [5, -5];
31199
+ case 3:
31200
+ return [5, 0, -5];
31201
+ case 4:
31202
+ return [6, 2, -2, -6];
31203
+ case 5:
31204
+ return [6, 3, 0, -3, -6];
31205
+ case 6:
31206
+ return [5, 3, 1, -1, -3, -5];
31207
+ default:
31208
+ return [5];
31209
+ }
31210
+ };
31211
+ const defaultRange = [[-10, 10], [-10, 10]];
31212
+ return ys(graph.numSegments).map(y => {
31213
+ let endpoints = [[-5, y], [5, y]];
31214
+ endpoints = normalizeCoords(endpoints, defaultRange);
31215
+ endpoints = normalizePoints(range, step, endpoints);
31216
+ return endpoints;
31217
+ });
31218
+ }
31219
+ const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
31220
+ function getLineCoords(graph, range, step) {
31221
+ if (graph.coords) {
31222
+ return graph.coords;
31223
+ }
31224
+ return normalizePoints(range, step, defaultLinearCoords[0]);
31225
+ }
31226
+ function getLinearSystemCoords(graph, range, step) {
31227
+ if (graph.coords) {
31228
+ return graph.coords;
31229
+ }
31230
+ return defaultLinearCoords.map(points => normalizePoints(range, step, points));
31231
+ }
31232
+ function getPolygonCoords(graph, range, step) {
31233
+ var _graph$coords2;
31234
+ let coords = (_graph$coords2 = graph.coords) == null ? void 0 : _graph$coords2.slice();
31235
+ if (coords) {
31236
+ return coords;
31237
+ }
31238
+ const n = graph.numSides || 3;
31239
+ if (n === "unlimited") {
31240
+ coords = [];
31241
+ } else {
31242
+ const angle = 2 * Math.PI / n;
31243
+ const offset = (1 / n - 1 / 2) * Math.PI;
31244
+
31245
+ // TODO(alex): Generalize this to more than just triangles so that
31246
+ // all polygons have whole number side lengths if snapping to sides
31247
+ const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
31248
+
31249
+ // Generate coords of a regular polygon with n sides
31250
+ coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
31251
+ }
31252
+ coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31253
+ const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
31254
+ coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
31255
+ return coords;
31256
+ }
31257
+ function getSinusoidCoords(graph, range, step) {
31258
+ if (graph.coords) {
31259
+ return [graph.coords[0], graph.coords[1]];
31260
+ }
31261
+ let coords = [[0.5, 0.5], [0.65, 0.6]];
31262
+ coords = normalizePoints(range, step, coords, true);
31263
+ return coords;
31264
+ }
31265
+ function getQuadraticCoords(graph, range, step) {
31266
+ if (graph.coords) {
31267
+ return graph.coords;
31268
+ }
31269
+ const defaultCoords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
31270
+ return normalizePoints(range, step, defaultCoords, true);
31271
+ }
31272
+ function getCircleCoords(graph) {
31273
+ if (graph.center != null && graph.radius != null) {
31274
+ return {
31275
+ center: graph.center,
31276
+ radiusPoint: vec.add(graph.center, [graph.radius, 0])
31277
+ };
31278
+ }
31279
+ return {
31280
+ center: [0, 0],
31281
+ radiusPoint: [2, 0]
31282
+ };
31283
+ }
31284
+
31223
31285
  function interactiveGraphReducer(state, action) {
31224
31286
  switch (action.type) {
31225
31287
  case MOVE_CONTROL_POINT:
@@ -31243,15 +31305,9 @@ function interactiveGraphReducer(state, action) {
31243
31305
  }
31244
31306
  }
31245
31307
  function doMoveControlPoint(state, action) {
31246
- const {
31247
- snapStep,
31248
- range
31249
- } = state;
31250
31308
  switch (state.type) {
31251
31309
  case "segment":
31252
- case "linear":
31253
31310
  case "linear-system":
31254
- case "ray":
31255
31311
  {
31256
31312
  const newCoords = updateAtIndex({
31257
31313
  array: state.coords,
@@ -31259,11 +31315,7 @@ function doMoveControlPoint(state, action) {
31259
31315
  update: tuple => setAtIndex({
31260
31316
  array: tuple,
31261
31317
  index: action.pointIndex,
31262
- newValue: snap(snapStep, bound$1({
31263
- snapStep,
31264
- range,
31265
- point: action.destination
31266
- }))
31318
+ newValue: boundAndSnapToGrid(action.destination, state)
31267
31319
  })
31268
31320
  });
31269
31321
  const coordsToCheck = newCoords[action.itemIndex];
@@ -31275,6 +31327,19 @@ function doMoveControlPoint(state, action) {
31275
31327
  coords: newCoords
31276
31328
  });
31277
31329
  }
31330
+ case "linear":
31331
+ case "ray":
31332
+ {
31333
+ const newCoords = setAtIndex({
31334
+ array: state.coords,
31335
+ index: action.pointIndex,
31336
+ newValue: boundAndSnapToGrid(action.destination, state)
31337
+ });
31338
+ return _extends({}, state, {
31339
+ hasBeenInteractedWith: true,
31340
+ coords: newCoords
31341
+ });
31342
+ }
31278
31343
  case "circle":
31279
31344
  throw new Error("FIXME implement circle reducer");
31280
31345
  case "point":
@@ -31286,6 +31351,9 @@ function doMoveControlPoint(state, action) {
31286
31351
  throw new UnreachableCaseError(state);
31287
31352
  }
31288
31353
  }
31354
+
31355
+ // (LEMS-2050): Update the reducer so that we have a separate action for moving one line
31356
+ // and another action for moving multiple lines
31289
31357
  function doMoveLine(state, action) {
31290
31358
  const {
31291
31359
  snapStep,
@@ -31293,9 +31361,7 @@ function doMoveLine(state, action) {
31293
31361
  } = state;
31294
31362
  switch (state.type) {
31295
31363
  case "segment":
31296
- case "linear":
31297
31364
  case "linear-system":
31298
- case "ray":
31299
31365
  {
31300
31366
  if (action.itemIndex === undefined) {
31301
31367
  throw new Error("Please provide index of line to move");
@@ -31320,6 +31386,21 @@ function doMoveLine(state, action) {
31320
31386
  coords: newCoords
31321
31387
  });
31322
31388
  }
31389
+ case "linear":
31390
+ case "ray":
31391
+ {
31392
+ const currentLine = state.coords;
31393
+ const change = getChange(currentLine, action.delta, {
31394
+ snapStep,
31395
+ range
31396
+ });
31397
+ const newLine = [snap(snapStep, vec.add(currentLine[0], change)), snap(snapStep, vec.add(currentLine[1], change))];
31398
+ return _extends({}, state, {
31399
+ type: state.type,
31400
+ hasBeenInteractedWith: true,
31401
+ coords: newLine
31402
+ });
31403
+ }
31323
31404
  default:
31324
31405
  // The MoveLine action doesn't make sense for other graph types;
31325
31406
  // ignore it if it somehow happens
@@ -31334,11 +31415,20 @@ function doMoveAll(state, action) {
31334
31415
  switch (state.type) {
31335
31416
  case "polygon":
31336
31417
  {
31337
- const change = getChange(state.coords, action.delta, {
31338
- snapStep,
31339
- range
31340
- });
31341
- const newCoords = state.coords.map(point => snap(snapStep, vec.add(point, change)));
31418
+ let newCoords;
31419
+ if (state.snapTo === "sides" || state.snapTo === "angles") {
31420
+ const change = getChange(state.coords, action.delta, {
31421
+ snapStep: [0, 0],
31422
+ range
31423
+ });
31424
+ newCoords = state.coords.map(point => vec.add(point, change));
31425
+ } else {
31426
+ const change = getChange(state.coords, action.delta, {
31427
+ snapStep,
31428
+ range
31429
+ });
31430
+ newCoords = state.coords.map(point => snap(snapStep, vec.add(point, change)));
31431
+ }
31342
31432
  return _extends({}, state, {
31343
31433
  hasBeenInteractedWith: true,
31344
31434
  coords: newCoords
@@ -31352,6 +31442,28 @@ function doMoveAll(state, action) {
31352
31442
  function doMovePoint(state, action) {
31353
31443
  switch (state.type) {
31354
31444
  case "polygon":
31445
+ let newValue;
31446
+ if (state.snapTo === "sides") {
31447
+ newValue = boundAndSnapToSides(action.destination, state, action.index);
31448
+ } else if (state.snapTo === "angles") {
31449
+ newValue = boundAndSnapToAngle(action.destination, state, action.index);
31450
+ } else {
31451
+ newValue = boundAndSnapToGrid(action.destination, state);
31452
+ }
31453
+ const newCoords = setAtIndex({
31454
+ array: state.coords,
31455
+ index: action.index,
31456
+ newValue: newValue
31457
+ });
31458
+
31459
+ // Reject the move if it would cause the sides of the polygon to cross
31460
+ if (polygonSidesIntersect(newCoords)) {
31461
+ return state;
31462
+ }
31463
+ return _extends({}, state, {
31464
+ hasBeenInteractedWith: true,
31465
+ coords: newCoords
31466
+ });
31355
31467
  case "point":
31356
31468
  {
31357
31469
  return _extends({}, state, {
@@ -31359,11 +31471,7 @@ function doMovePoint(state, action) {
31359
31471
  coords: setAtIndex({
31360
31472
  array: state.coords,
31361
31473
  index: action.index,
31362
- newValue: snap(state.snapStep, bound$1({
31363
- snapStep: state.snapStep,
31364
- range: state.range,
31365
- point: action.destination
31366
- }))
31474
+ newValue: boundAndSnapToGrid(action.destination, state)
31367
31475
  })
31368
31476
  });
31369
31477
  }
@@ -31372,9 +31480,9 @@ function doMovePoint(state, action) {
31372
31480
  // First, we need to verify that the new coordinates are not on the same vertical line
31373
31481
  // If they are, we don't want to move the point
31374
31482
  const destination = action.destination;
31375
- const newCoords = [...state.coords];
31376
- newCoords[action.index] = action.destination;
31377
- if (newCoords[0][0] === newCoords[1][0]) {
31483
+ const _newCoords = [...state.coords];
31484
+ _newCoords[action.index] = action.destination;
31485
+ if (_newCoords[0][0] === _newCoords[1][0]) {
31378
31486
  return state;
31379
31487
  }
31380
31488
  return _extends({}, state, {
@@ -31382,26 +31490,27 @@ function doMovePoint(state, action) {
31382
31490
  coords: setAtIndex({
31383
31491
  array: state.coords,
31384
31492
  index: action.index,
31385
- newValue: snap(state.snapStep, bound$1({
31386
- snapStep: state.snapStep,
31387
- range: state.range,
31388
- point: destination
31389
- }))
31493
+ newValue: boundAndSnapToGrid(destination, state)
31390
31494
  })
31391
31495
  });
31392
31496
  }
31393
31497
  case "quadratic":
31394
31498
  {
31499
+ // Set up the new coords and check if the quadratic coefficients are valid
31500
+ const _newCoords2 = [...state.coords];
31501
+ _newCoords2[action.index] = action.destination;
31502
+ const QuadraticCoefficients = getQuadraticCoefficients(_newCoords2);
31503
+
31504
+ // If the new destination results in an invalid quadratic equation, we don't want to move the point
31505
+ if (QuadraticCoefficients === undefined) {
31506
+ return state;
31507
+ }
31395
31508
  return _extends({}, state, {
31396
31509
  hasBeenInteractedWith: true,
31397
31510
  coords: setAtIndex({
31398
31511
  array: state.coords,
31399
31512
  index: action.index,
31400
- newValue: snap(state.snapStep, bound$1({
31401
- snapStep: state.snapStep,
31402
- range: state.range,
31403
- point: action.destination
31404
- }))
31513
+ newValue: boundAndSnapToGrid(action.destination, state)
31405
31514
  })
31406
31515
  });
31407
31516
  }
@@ -31488,14 +31597,8 @@ function doChangeRange(state, action) {
31488
31597
  range: action.range
31489
31598
  });
31490
31599
  }
31491
- const getChange = (coords, delta, constraintOpts) => {
31600
+ const getDeltaVertex = (maxMoves, minMoves, delta) => {
31492
31601
  const [deltaX, deltaY] = delta;
31493
- const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
31494
- point
31495
- })));
31496
- const minMoves = coords.map(point => minMove(_extends({}, constraintOpts, {
31497
- point
31498
- })));
31499
31602
  const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31500
31603
  const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31501
31604
  const minXMove = Math.max(...minMoves.map(move => move[0]));
@@ -31504,6 +31607,158 @@ const getChange = (coords, delta, constraintOpts) => {
31504
31607
  const dy = clamp(deltaY, minYMove, maxYMove);
31505
31608
  return [dx, dy];
31506
31609
  };
31610
+ const getChange = (coords, delta, constraintOpts) => {
31611
+ const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
31612
+ point
31613
+ })));
31614
+ const minMoves = coords.map(point => minMove(_extends({}, constraintOpts, {
31615
+ point
31616
+ })));
31617
+ const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
31618
+ return [dx, dy];
31619
+ };
31620
+ const eq$1 = Util.eq;
31621
+
31622
+ // Less than or approximately equal
31623
+ function leq$1(a, b) {
31624
+ return a < b || eq$1(a, b);
31625
+ }
31626
+ function boundAndSnapToGrid(point, {
31627
+ snapStep,
31628
+ range
31629
+ }) {
31630
+ return snap(snapStep, bound$1({
31631
+ snapStep,
31632
+ range,
31633
+ point
31634
+ }));
31635
+ }
31636
+ function boundAndSnapToAngle(destinationPoint, {
31637
+ range,
31638
+ coords
31639
+ }, index) {
31640
+ const startingPoint = coords[index];
31641
+
31642
+ // Needed to prevent updating the original coords before the checks for
31643
+ // degenerate triangles and overlapping sides
31644
+ const coordsCopy = [...coords];
31645
+
31646
+ // Takes the destination point and makes sure it is within the bounds of the graph
31647
+ // SnapStep is [0, 0] because we don't want to snap to the grid
31648
+ coordsCopy[index] = bound$1({
31649
+ snapStep: [0, 0],
31650
+ range,
31651
+ point: destinationPoint
31652
+ });
31653
+
31654
+ // Gets the radian angles between the coords and maps them to degrees
31655
+ const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
31656
+
31657
+ // Gets the relative index of a point
31658
+ const rel = j => {
31659
+ return (index + j + coordsCopy.length) % coordsCopy.length;
31660
+ };
31661
+
31662
+ // Round the angles to left and right of the current point
31663
+ _.each([-1, 1], function (j) {
31664
+ angles[rel(j)] = Math.round(angles[rel(j)]);
31665
+ });
31666
+ const getAngle = function getAngle(a, vertex, b) {
31667
+ const angle = GraphUtils.findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
31668
+ return (angle + 360) % 360;
31669
+ };
31670
+ const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
31671
+ innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
31672
+
31673
+ // Avoid degenerate triangles
31674
+ if (innerAngles.some(function (angle) {
31675
+ return leq$1(angle, 1);
31676
+ })) {
31677
+ return startingPoint;
31678
+ }
31679
+ const knownSide = magnitude(
31680
+ // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
31681
+ vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
31682
+
31683
+ // Returns true if the points form a counter-clockwise turn;
31684
+ // a.k.a if the point is on the left or right of the polygon.
31685
+ // This is used to determine how to adjust the point: if the point is on the left,
31686
+ // we want to add the inner angle to the outer angle, and if it's on the right,
31687
+ // we want to subtract the inner angle from the outer angle. The angle solved
31688
+ // for is then used in the polar function to determine the new point.
31689
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
31690
+
31691
+ // Solve for side by using the law of sines
31692
+ const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
31693
+
31694
+ // Angle at the second vertex of the polygon
31695
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
31696
+
31697
+ // Uses the length of the side of the polygon (radial coordinate)
31698
+ // and the angle between the first and second sides of the
31699
+ // polygon (angular coordinate) to determine how to adjust the point
31700
+ const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
31701
+ return vector$1.add(coordsCopy[rel(-1)], offset);
31702
+ }
31703
+ function boundAndSnapToSides(destinationPoint, {
31704
+ range,
31705
+ coords
31706
+ }, index) {
31707
+ const startingPoint = coords[index];
31708
+
31709
+ // Needed to prevent updating the original coords before the checks for
31710
+ // degenerate triangles and overlapping sides
31711
+ const coordsCopy = [...coords];
31712
+
31713
+ // Takes the destination point and makes sure it is within the bounds of the graph
31714
+ // SnapStep is [0, 0] because we don't want to snap to the grid
31715
+ coordsCopy[index] = bound$1({
31716
+ snapStep: [0, 0],
31717
+ range,
31718
+ point: destinationPoint
31719
+ });
31720
+
31721
+ // Gets the relative index of a point
31722
+ const rel = j => {
31723
+ return (index + j + coordsCopy.length) % coordsCopy.length;
31724
+ };
31725
+ const sides = _.map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
31726
+ // @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.
31727
+ return magnitude(vector(...coordsCopy));
31728
+ });
31729
+
31730
+ // Round the sides to left and right of the current point
31731
+ _.each([0, 1], function (j) {
31732
+ sides[j] = Math.round(sides[j]);
31733
+ });
31734
+
31735
+ // Avoid degenerate triangles
31736
+ 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])) {
31737
+ return startingPoint;
31738
+ }
31739
+
31740
+ // Solve for angle by using the law of cosines
31741
+ // Angle at the first vertex of the polygon
31742
+ const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
31743
+
31744
+ // Angle at the second vertex of the polygon
31745
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
31746
+
31747
+ // Returns true if the points form a counter-clockwise turn;
31748
+ // a.k.a if the point is on the left or right of the polygon.
31749
+ // This is used to determine how to adjust the point: if the point is on the left,
31750
+ // we want to add the inner angle to the outer angle, and if it's on the right,
31751
+ // we want to subtract the inner angle from the outer angle. The angle solved
31752
+ // for is then used in the polar function to determine the new point.
31753
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
31754
+
31755
+ // Uses the length of the first side of the polygon (radial coordinate)
31756
+ // and the angle between the first and second sides of the
31757
+ // polygon (angular coordinate) to determine how to adjust the point
31758
+ const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
31759
+ return vector$1.add(coordsCopy[rel(-1)], offset);
31760
+ }
31761
+
31507
31762
  // Returns the closest point to the given `point` that is within the graph
31508
31763
  // bounds given in `state`.
31509
31764
  function bound$1({
@@ -31517,7 +31772,7 @@ function bound$1({
31517
31772
  return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
31518
31773
  }
31519
31774
 
31520
- // Returns the vector from the given point to the top-right corner of the graph
31775
+ // Returns the vector from the given point to the top-right corner of the graph when snapped to the grid
31521
31776
  function maxMove({
31522
31777
  snapStep,
31523
31778
  range,
@@ -31531,8 +31786,7 @@ function maxMove({
31531
31786
  return vec.sub(topRight, point);
31532
31787
  }
31533
31788
 
31534
- // Returns the vector from the given point to the bottom-left corner of the
31535
- // graph
31789
+ // Returns the vector from the given point to the bottom-left corner of the graph when snapped to the grid
31536
31790
  function minMove({
31537
31791
  snapStep,
31538
31792
  range,
@@ -31593,8 +31847,12 @@ const renderGraph = props => {
31593
31847
  graphState: state,
31594
31848
  dispatch: dispatch
31595
31849
  });
31596
- case "linear":
31597
31850
  case "linear-system":
31851
+ return /*#__PURE__*/React.createElement(LinearSystemGraph, {
31852
+ graphState: state,
31853
+ dispatch: dispatch
31854
+ });
31855
+ case "linear":
31598
31856
  return /*#__PURE__*/React.createElement(LinearGraph, {
31599
31857
  graphState: state,
31600
31858
  dispatch: dispatch
@@ -43843,7 +44101,7 @@ const init = function init(options) {
43843
44101
 
43844
44102
  // This file is processed by a Rollup plugin (replace) to inject the production
43845
44103
  const libName = "@khanacademy/perseus";
43846
- const libVersion = "23.1.0";
44104
+ const libVersion = "23.4.0";
43847
44105
  addLibraryVersionToPerseusDebug(libName, libVersion);
43848
44106
 
43849
44107
  const apiVersion = {
@@ -46844,5 +47102,5 @@ const EditorJsonify = {
46844
47102
  }
46845
47103
  };
46846
47104
 
46847
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
47105
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedEllipseFillStyles, lockedFigureColors, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
46848
47106
  //# sourceMappingURL=index.js.map