@khanacademy/perseus 23.3.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.
package/dist/es/index.js CHANGED
@@ -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
@@ -10947,7 +10951,7 @@ function similar(coords1, coords2, tolerance) {
10947
10951
  return pair[0] / pair[1];
10948
10952
  });
10949
10953
  const same = _.all(factors, function (factor) {
10950
- return eq$1(factors[0], factor);
10954
+ return eq$2(factors[0], factor);
10951
10955
  });
10952
10956
  const congruentEnough = _.all(sidePairs, function (pair) {
10953
10957
  return number.equal(pair[0], pair[1], tolerance);
@@ -10999,7 +11003,7 @@ function rotate(array, n) {
10999
11003
  return array.slice(n).concat(array.slice(0, n));
11000
11004
  }
11001
11005
  function getLineEquation(first, second) {
11002
- if (eq$1(first[0], second[0])) {
11006
+ if (eq$2(first[0], second[0])) {
11003
11007
  return "x = " + first[0].toFixed(3);
11004
11008
  }
11005
11009
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -29250,6 +29254,7 @@ function useControlPoint(point, color, onMovePoint) {
29250
29254
  });
29251
29255
  const focusableHandle = /*#__PURE__*/React.createElement("g", {
29252
29256
  "data-testid": "movable-point__focusable-handle",
29257
+ className: "movable-point__focusable-handle",
29253
29258
  tabIndex: 0,
29254
29259
  ref: keyboardHandleRef,
29255
29260
  onFocus: () => setFocused(true),
@@ -29419,7 +29424,6 @@ const LinearSystemGraph = props => {
29419
29424
  const {
29420
29425
  coords: lines
29421
29426
  } = props.graphState;
29422
- const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29423
29427
  return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(MovableLine, {
29424
29428
  key: i,
29425
29429
  points: line,
@@ -29431,7 +29435,7 @@ const LinearSystemGraph = props => {
29431
29435
  end: true
29432
29436
  },
29433
29437
  onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29434
- color: colors[i]
29438
+ color: "var(--movable-line-stroke-color)"
29435
29439
  })), ";");
29436
29440
  };
29437
29441
 
@@ -29503,7 +29507,8 @@ const Angle = ({
29503
29507
  endPoints,
29504
29508
  range,
29505
29509
  polygonLines,
29506
- showAngles
29510
+ showAngles,
29511
+ snapTo
29507
29512
  }) => {
29508
29513
  const [centerX, centerY] = centerPoint;
29509
29514
  const areClockwise = clockwise$1([centerPoint, ...endPoints]);
@@ -29541,7 +29546,9 @@ const Angle = ({
29541
29546
  if (isOutside) {
29542
29547
  angleInDegrees = 360 - angleInDegrees;
29543
29548
  }
29544
- 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));
29545
29552
  const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
29546
29553
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
29547
29554
  id: "background",
@@ -29627,15 +29634,17 @@ const StyledMovablePoint = props => {
29627
29634
  point,
29628
29635
  onMove,
29629
29636
  cursor,
29630
- color: color$1 = color.blue
29637
+ color: color$1 = color.blue,
29638
+ snapTo
29631
29639
  } = props;
29640
+ const snapToValue = snapTo != null ? snapTo : "grid";
29632
29641
  const {
29633
29642
  dragging
29634
29643
  } = useMovable({
29635
29644
  gestureTarget: elementRef,
29636
29645
  point,
29637
29646
  onMove,
29638
- constrain: p => snap(snapStep, p)
29647
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29639
29648
  });
29640
29649
  return /*#__PURE__*/React.createElement(MovablePointView, {
29641
29650
  ref: elementRef,
@@ -29661,7 +29670,8 @@ const PolygonGraph = props => {
29661
29670
  showAngles,
29662
29671
  showSides,
29663
29672
  range,
29664
- snapStep
29673
+ snapStep,
29674
+ snapTo
29665
29675
  } = props.graphState;
29666
29676
 
29667
29677
  // TODO(benchristel): can the default set of points be removed here? I don't
@@ -29669,6 +29679,7 @@ const PolygonGraph = props => {
29669
29679
  const points = coords != null ? coords : [[0, 0]];
29670
29680
  const ref = React.useRef(null);
29671
29681
  const dragReferencePoint = points[0];
29682
+ const snapToValue = snapTo != null ? snapTo : "grid";
29672
29683
  const {
29673
29684
  dragging
29674
29685
  } = useMovable({
@@ -29678,7 +29689,7 @@ const PolygonGraph = props => {
29678
29689
  const delta = vec.sub(newPoint, dragReferencePoint);
29679
29690
  dispatch(moveAll(delta));
29680
29691
  },
29681
- constrain: p => snap(snapStep, p)
29692
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29682
29693
  });
29683
29694
  const active = hovered || focused || dragging;
29684
29695
  const lines = getLines(points);
@@ -29701,11 +29712,12 @@ const PolygonGraph = props => {
29701
29712
  active: active,
29702
29713
  range: range,
29703
29714
  polygonLines: lines,
29704
- showAngles: !!showAngles
29715
+ showAngles: !!showAngles,
29716
+ snapTo: snapToValue
29705
29717
  });
29706
29718
  }), showSides && lines.map(([start, end], i) => {
29707
29719
  const [x, y] = vec.midpoint(start, end);
29708
- const length = parseFloat(vec.dist(start, end).toFixed(1));
29720
+ const length = parseFloat(vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
29709
29721
  return /*#__PURE__*/React.createElement(TextLabel, {
29710
29722
  key: "side-" + i,
29711
29723
  x: x,
@@ -29729,143 +29741,12 @@ const PolygonGraph = props => {
29729
29741
  }
29730
29742
  }), points.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
29731
29743
  key: "point-" + i,
29744
+ snapTo: snapTo,
29732
29745
  point: point,
29733
29746
  onMove: destination => dispatch(movePoint(i, destination))
29734
29747
  })));
29735
29748
  };
29736
29749
 
29737
- function initializeGraphState(params) {
29738
- const {
29739
- graph,
29740
- step,
29741
- snapStep,
29742
- range
29743
- } = params;
29744
- const shared = {
29745
- hasBeenInteractedWith: false,
29746
- range,
29747
- snapStep
29748
- };
29749
- switch (graph.type) {
29750
- case "segment":
29751
- return _extends({}, shared, {
29752
- type: "segment",
29753
- coords: getSegmentCoords({
29754
- graph,
29755
- step,
29756
- range
29757
- })
29758
- });
29759
- case "linear":
29760
- return _extends({}, shared, {
29761
- type: graph.type,
29762
- coords: getLineCoords({
29763
- graph,
29764
- range,
29765
- step
29766
- })
29767
- });
29768
- case "ray":
29769
- return _extends({}, shared, {
29770
- type: graph.type,
29771
- coords: getLineCoords({
29772
- graph,
29773
- range,
29774
- step
29775
- })
29776
- });
29777
- case "linear-system":
29778
- return _extends({}, shared, {
29779
- type: graph.type,
29780
- coords: getLinearSystemCoords({
29781
- graph,
29782
- range,
29783
- step
29784
- })
29785
- });
29786
- case "polygon":
29787
- return _extends({}, shared, {
29788
- type: "polygon",
29789
- showAngles: Boolean(graph.showAngles),
29790
- showSides: Boolean(graph.showSides),
29791
- coords: getPolygonCoords({
29792
- graph,
29793
- range,
29794
- step
29795
- })
29796
- });
29797
- case "point":
29798
- return _extends({}, shared, {
29799
- type: graph.type,
29800
- coords: getDefaultPoints({
29801
- graph,
29802
- step,
29803
- range
29804
- })
29805
- });
29806
- case "circle":
29807
- return _extends({}, shared, {
29808
- type: graph.type
29809
- }, getCircleCoords(graph));
29810
- case "quadratic":
29811
- return _extends({}, shared, {
29812
- type: graph.type,
29813
- coords: getQuadraticCoords(graph, range, step)
29814
- });
29815
- case "sinusoid":
29816
- return _extends({}, shared, {
29817
- type: graph.type,
29818
- coords: getSinusoidCoords(graph, range, step)
29819
- });
29820
- case "angle":
29821
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
29822
- default:
29823
- throw new UnreachableCaseError(graph);
29824
- }
29825
- }
29826
- const getDefaultPoints = ({
29827
- graph,
29828
- range,
29829
- step
29830
- }) => {
29831
- var _graph$coords;
29832
- const numPoints = graph.numPoints || 1;
29833
- let coords = (_graph$coords = graph.coords) == null ? void 0 : _graph$coords.slice();
29834
- if (coords) {
29835
- return coords;
29836
- }
29837
- switch (numPoints) {
29838
- case 1:
29839
- // Back in the day, one point's coords were in graph.coord
29840
- coords = [graph.coord || [0, 0]];
29841
- break;
29842
- case 2:
29843
- coords = [[-5, 0], [5, 0]];
29844
- break;
29845
- case 3:
29846
- coords = [[-5, 0], [0, 0], [5, 0]];
29847
- break;
29848
- case 4:
29849
- coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
29850
- break;
29851
- case 5:
29852
- coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
29853
- break;
29854
- case 6:
29855
- coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
29856
- break;
29857
- default:
29858
- coords = [];
29859
- break;
29860
- }
29861
- // Transform coords from their -10 to 10 space to 0 to 1
29862
- // because of the old graph.coord, and also it's easier.
29863
- const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29864
- return normalizePoints(range, step, newCoords);
29865
- };
29866
-
29867
- // TS v4 doesn't narrow return types, while v5 does.
29868
- // Instead of updating to v5, using generic type to relate input and output types.
29869
29750
  function getGradableGraph(state, initialGraph) {
29870
29751
  if (!state.hasBeenInteractedWith) {
29871
29752
  return _extends({}, initialGraph);
@@ -29918,115 +29799,6 @@ function getGradableGraph(state, initialGraph) {
29918
29799
  }
29919
29800
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29920
29801
  }
29921
- const getSegmentCoords = ({
29922
- graph,
29923
- range,
29924
- step
29925
- }) => {
29926
- if (graph.coords) {
29927
- return graph.coords;
29928
- }
29929
- const ys = n => {
29930
- switch (n) {
29931
- case 2:
29932
- return [5, -5];
29933
- case 3:
29934
- return [5, 0, -5];
29935
- case 4:
29936
- return [6, 2, -2, -6];
29937
- case 5:
29938
- return [6, 3, 0, -3, -6];
29939
- case 6:
29940
- return [5, 3, 1, -1, -3, -5];
29941
- default:
29942
- return [5];
29943
- }
29944
- };
29945
- const defaultRange = [[-10, 10], [-10, 10]];
29946
- return ys(graph.numSegments).map(y => {
29947
- let endpoints = [[-5, y], [5, y]];
29948
- endpoints = normalizeCoords(endpoints, defaultRange);
29949
- endpoints = normalizePoints(range, step, endpoints);
29950
- return endpoints;
29951
- });
29952
- };
29953
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
29954
- const getLineCoords = ({
29955
- graph,
29956
- range,
29957
- step
29958
- }) => {
29959
- if (graph.coords) {
29960
- return graph.coords;
29961
- }
29962
- return normalizePoints(range, step, defaultLinearCoords[0]);
29963
- };
29964
- const getLinearSystemCoords = ({
29965
- graph,
29966
- range,
29967
- step
29968
- }) => {
29969
- if (graph.coords) {
29970
- return graph.coords;
29971
- }
29972
- return defaultLinearCoords.map(points => normalizePoints(range, step, points));
29973
- };
29974
- const getPolygonCoords = ({
29975
- graph,
29976
- range,
29977
- step
29978
- }) => {
29979
- var _graph$coords2;
29980
- let coords = (_graph$coords2 = graph.coords) == null ? void 0 : _graph$coords2.slice();
29981
- if (coords) {
29982
- return coords;
29983
- }
29984
- const n = graph.numSides || 3;
29985
- if (n === "unlimited") {
29986
- coords = [];
29987
- } else {
29988
- const angle = 2 * Math.PI / n;
29989
- const offset = (1 / n - 1 / 2) * Math.PI;
29990
-
29991
- // TODO(alex): Generalize this to more than just triangles so that
29992
- // all polygons have whole number side lengths if snapping to sides
29993
- const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
29994
-
29995
- // Generate coords of a regular polygon with n sides
29996
- coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
29997
- }
29998
- coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
29999
- const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
30000
- coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
30001
- return coords;
30002
- };
30003
- const getSinusoidCoords = (graph, range, step) => {
30004
- if (graph.coords) {
30005
- return [graph.coords[0], graph.coords[1]];
30006
- }
30007
- let coords = [[0.5, 0.5], [0.65, 0.6]];
30008
- coords = normalizePoints(range, step, coords, true);
30009
- return coords;
30010
- };
30011
- const getQuadraticCoords = (graph, range, step) => {
30012
- if (graph.coords) {
30013
- return graph.coords;
30014
- }
30015
- const defaultCoords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
30016
- return normalizePoints(range, step, defaultCoords, true);
30017
- };
30018
- const getCircleCoords = (graph, range, step) => {
30019
- if (graph.center != null && graph.radius != null) {
30020
- return {
30021
- center: graph.center,
30022
- radiusPoint: vec.add(graph.center, [graph.radius, 0])
30023
- };
30024
- }
30025
- return {
30026
- center: [0, 0],
30027
- radiusPoint: [1, 0]
30028
- };
30029
- };
30030
29802
 
30031
29803
  /**
30032
29804
  * determine radius of a circle graph
@@ -30160,38 +29932,16 @@ function QuadraticGraph(props) {
30160
29932
 
30161
29933
  // Calculate the y value based on the current x value
30162
29934
  const y = x => (a * x + b) * x + c;
30163
- const handleOnMove = (destination, elementId) => {
30164
- // If the destination is invalid, we want do not want to move the point
30165
- const validDestination = isValidDestination(destination, elementId, coords);
30166
- if (validDestination === false) {
30167
- return;
30168
- }
30169
- dispatch(movePoint(elementId, destination));
30170
- };
30171
29935
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$3.OfX, {
30172
29936
  y: y,
30173
29937
  color: color.blue
30174
29938
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
30175
29939
  key: "point-" + i,
30176
29940
  point: coord,
30177
- onMove: destination => handleOnMove(destination, i)
29941
+ onMove: destination => dispatch(movePoint(i, destination))
30178
29942
  })));
30179
29943
  }
30180
29944
 
30181
- // Ensure that we are only snapping to coordinates that result in a valid quadratic equation
30182
- const isValidDestination = (destination, elementId, coords) => {
30183
- // Set up the new coords and check if the quadratic coefficients are valid
30184
- const newCoords = [...coords];
30185
- newCoords[elementId] = destination;
30186
- const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
30187
-
30188
- // If the new destination results in an invalid quadratic equation, we don't want to move the point
30189
- if (QuadraticCoefficients === undefined) {
30190
- return false;
30191
- }
30192
- return true;
30193
- };
30194
-
30195
29945
  // Get the quadratic coefficients from the 3 control points
30196
29946
  // These equations were originally set up in 2013 and may require some
30197
29947
  // additional comments to help clarify the quadratic formula manipulations
@@ -31335,6 +31085,203 @@ const LegacyGrid = ({
31335
31085
  return null;
31336
31086
  };
31337
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
+
31338
31285
  function interactiveGraphReducer(state, action) {
31339
31286
  switch (action.type) {
31340
31287
  case MOVE_CONTROL_POINT:
@@ -31468,11 +31415,20 @@ function doMoveAll(state, action) {
31468
31415
  switch (state.type) {
31469
31416
  case "polygon":
31470
31417
  {
31471
- const change = getChange(state.coords, action.delta, {
31472
- snapStep,
31473
- range
31474
- });
31475
- 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
+ }
31476
31432
  return _extends({}, state, {
31477
31433
  hasBeenInteractedWith: true,
31478
31434
  coords: newCoords
@@ -31486,10 +31442,18 @@ function doMoveAll(state, action) {
31486
31442
  function doMovePoint(state, action) {
31487
31443
  switch (state.type) {
31488
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
+ }
31489
31453
  const newCoords = setAtIndex({
31490
31454
  array: state.coords,
31491
31455
  index: action.index,
31492
- newValue: boundAndSnapToGrid(action.destination, state)
31456
+ newValue: newValue
31493
31457
  });
31494
31458
 
31495
31459
  // Reject the move if it would cause the sides of the polygon to cross
@@ -31532,6 +31496,15 @@ function doMovePoint(state, action) {
31532
31496
  }
31533
31497
  case "quadratic":
31534
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
+ }
31535
31508
  return _extends({}, state, {
31536
31509
  hasBeenInteractedWith: true,
31537
31510
  coords: setAtIndex({
@@ -31624,14 +31597,8 @@ function doChangeRange(state, action) {
31624
31597
  range: action.range
31625
31598
  });
31626
31599
  }
31627
- const getChange = (coords, delta, constraintOpts) => {
31600
+ const getDeltaVertex = (maxMoves, minMoves, delta) => {
31628
31601
  const [deltaX, deltaY] = delta;
31629
- const maxMoves = coords.map(point => maxMove(_extends({}, constraintOpts, {
31630
- point
31631
- })));
31632
- const minMoves = coords.map(point => minMove(_extends({}, constraintOpts, {
31633
- point
31634
- })));
31635
31602
  const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31636
31603
  const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31637
31604
  const minXMove = Math.max(...minMoves.map(move => move[0]));
@@ -31640,6 +31607,22 @@ const getChange = (coords, delta, constraintOpts) => {
31640
31607
  const dy = clamp(deltaY, minYMove, maxYMove);
31641
31608
  return [dx, dy];
31642
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
+ }
31643
31626
  function boundAndSnapToGrid(point, {
31644
31627
  snapStep,
31645
31628
  range
@@ -31650,6 +31633,131 @@ function boundAndSnapToGrid(point, {
31650
31633
  point
31651
31634
  }));
31652
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
+ }
31653
31761
 
31654
31762
  // Returns the closest point to the given `point` that is within the graph
31655
31763
  // bounds given in `state`.
@@ -31664,7 +31772,7 @@ function bound$1({
31664
31772
  return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
31665
31773
  }
31666
31774
 
31667
- // 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
31668
31776
  function maxMove({
31669
31777
  snapStep,
31670
31778
  range,
@@ -31678,8 +31786,7 @@ function maxMove({
31678
31786
  return vec.sub(topRight, point);
31679
31787
  }
31680
31788
 
31681
- // Returns the vector from the given point to the bottom-left corner of the
31682
- // graph
31789
+ // Returns the vector from the given point to the bottom-left corner of the graph when snapped to the grid
31683
31790
  function minMove({
31684
31791
  snapStep,
31685
31792
  range,
@@ -43994,7 +44101,7 @@ const init = function init(options) {
43994
44101
 
43995
44102
  // This file is processed by a Rollup plugin (replace) to inject the production
43996
44103
  const libName = "@khanacademy/perseus";
43997
- const libVersion = "23.3.0";
44104
+ const libVersion = "23.4.0";
43998
44105
  addLibraryVersionToPerseusDebug(libName, libVersion);
43999
44106
 
44000
44107
  const apiVersion = {