@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/index.js CHANGED
@@ -3304,7 +3304,7 @@ const DeprecationMixin$1 = {
3304
3304
  /**
3305
3305
  * Approximate equality on numbers and primitives.
3306
3306
  */
3307
- function eq$2(x, y) {
3307
+ function eq$3(x, y) {
3308
3308
  if (typeof x === "number" && typeof y === "number") {
3309
3309
  return Math.abs(x - y) < 1e-9;
3310
3310
  }
@@ -3331,7 +3331,7 @@ function deepEq$5(x, y) {
3331
3331
  return false;
3332
3332
  }
3333
3333
  if (typeof x === "function" && typeof y === "function") {
3334
- return eq$2(x, y);
3334
+ return eq$3(x, y);
3335
3335
  }
3336
3336
  if (typeof x === "function" || typeof y === "function") {
3337
3337
  return false;
@@ -3348,7 +3348,7 @@ function deepEq$5(x, y) {
3348
3348
  if (typeof x === "object" && !!x || typeof y === "object" && !!y) {
3349
3349
  return false;
3350
3350
  }
3351
- return eq$2(x, y);
3351
+ return eq$3(x, y);
3352
3352
  }
3353
3353
 
3354
3354
  /**
@@ -3647,7 +3647,7 @@ const Util = {
3647
3647
  tickStepFromNumTicks,
3648
3648
  constrainedTickStepsFromTickSteps,
3649
3649
  DeprecationMixin: DeprecationMixin$1,
3650
- eq: eq$2,
3650
+ eq: eq$3,
3651
3651
  deepEq: deepEq$5,
3652
3652
  parseQueryString,
3653
3653
  updateQueryString: updateQueryString$2,
@@ -10885,7 +10885,7 @@ class WrappedLine extends WrappedDrawing {
10885
10885
  * A collection of geomtry-related utility functions
10886
10886
  */
10887
10887
  const {
10888
- eq: eq$1,
10888
+ eq: eq$2,
10889
10889
  deepEq: deepEq$4
10890
10890
  } = Util;
10891
10891
 
@@ -10893,17 +10893,21 @@ const {
10893
10893
 
10894
10894
  // a, b, c
10895
10895
 
10896
+ // Given a number, return whether it is positive (1), negative (-1), or zero (0)
10896
10897
  function sign(val) {
10897
- if (eq$1(val, 0)) {
10898
+ if (eq$2(val, 0)) {
10898
10899
  return 0;
10899
10900
  }
10900
10901
  return val > 0 ? 1 : -1;
10901
10902
  }
10903
+
10904
+ // Determine whether three points are collinear (0), for a clockwise turn (negative),
10905
+ // or counterclockwise turn (positive)
10902
10906
  function ccw(a, b, c) {
10903
10907
  return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
10904
10908
  }
10905
10909
  function collinear(a, b, c) {
10906
- return eq$1(ccw(a, b, c), 0);
10910
+ return eq$2(ccw(a, b, c), 0);
10907
10911
  }
10908
10912
 
10909
10913
  // Given rect bounding points A and B, whether point C is inside the rect
@@ -11048,7 +11052,7 @@ function similar(coords1, coords2, tolerance) {
11048
11052
  return pair[0] / pair[1];
11049
11053
  });
11050
11054
  const same = ___default["default"].all(factors, function (factor) {
11051
- return eq$1(factors[0], factor);
11055
+ return eq$2(factors[0], factor);
11052
11056
  });
11053
11057
  const congruentEnough = ___default["default"].all(sidePairs, function (pair) {
11054
11058
  return kmath.number.equal(pair[0], pair[1], tolerance);
@@ -11101,7 +11105,7 @@ function rotate(array, n) {
11101
11105
  return array.slice(n).concat(array.slice(0, n));
11102
11106
  }
11103
11107
  function getLineEquation(first, second) {
11104
- if (eq$1(first[0], second[0])) {
11108
+ if (eq$2(first[0], second[0])) {
11105
11109
  return "x = " + first[0].toFixed(3);
11106
11110
  }
11107
11111
  const m = (second[1] - first[1]) / (second[0] - first[0]);
@@ -29542,6 +29546,7 @@ function useControlPoint(point, color, onMovePoint) {
29542
29546
  });
29543
29547
  const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
29544
29548
  "data-testid": "movable-point__focusable-handle",
29549
+ className: "movable-point__focusable-handle",
29545
29550
  tabIndex: 0,
29546
29551
  ref: keyboardHandleRef,
29547
29552
  onFocus: () => setFocused(true),
@@ -29711,7 +29716,6 @@ const LinearSystemGraph = props => {
29711
29716
  const {
29712
29717
  coords: lines
29713
29718
  } = props.graphState;
29714
- const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29715
29719
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(MovableLine, {
29716
29720
  key: i,
29717
29721
  points: line,
@@ -29723,7 +29727,7 @@ const LinearSystemGraph = props => {
29723
29727
  end: true
29724
29728
  },
29725
29729
  onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29726
- color: colors[i]
29730
+ color: "var(--movable-line-stroke-color)"
29727
29731
  })), ";");
29728
29732
  };
29729
29733
 
@@ -29798,7 +29802,8 @@ const Angle = _ref => {
29798
29802
  endPoints,
29799
29803
  range,
29800
29804
  polygonLines,
29801
- showAngles
29805
+ showAngles,
29806
+ snapTo
29802
29807
  } = _ref;
29803
29808
  const [centerX, centerY] = centerPoint;
29804
29809
  const areClockwise = clockwise$1([centerPoint, ...endPoints]);
@@ -29836,7 +29841,9 @@ const Angle = _ref => {
29836
29841
  if (isOutside) {
29837
29842
  angleInDegrees = 360 - angleInDegrees;
29838
29843
  }
29839
- const angleLabelNumber = parseFloat(angleInDegrees.toFixed(1));
29844
+
29845
+ // Only want to show whole numbers when in angles mode
29846
+ const angleLabelNumber = parseFloat(angleInDegrees.toFixed(snapTo === "angles" ? 0 : 1));
29840
29847
  const angleLabel = Number.isInteger(angleLabelNumber) ? angleLabelNumber : "≈ " + angleLabelNumber;
29841
29848
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
29842
29849
  id: "background",
@@ -29927,15 +29934,17 @@ const StyledMovablePoint = props => {
29927
29934
  point,
29928
29935
  onMove,
29929
29936
  cursor,
29930
- color = wonderBlocksTokens.color.blue
29937
+ color = wonderBlocksTokens.color.blue,
29938
+ snapTo
29931
29939
  } = props;
29940
+ const snapToValue = snapTo ?? "grid";
29932
29941
  const {
29933
29942
  dragging
29934
29943
  } = mafs.useMovable({
29935
29944
  gestureTarget: elementRef,
29936
29945
  point,
29937
29946
  onMove,
29938
- constrain: p => snap(snapStep, p)
29947
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29939
29948
  });
29940
29949
  return /*#__PURE__*/React__namespace.createElement(MovablePointView, {
29941
29950
  ref: elementRef,
@@ -29961,7 +29970,8 @@ const PolygonGraph = props => {
29961
29970
  showAngles,
29962
29971
  showSides,
29963
29972
  range,
29964
- snapStep
29973
+ snapStep,
29974
+ snapTo
29965
29975
  } = props.graphState;
29966
29976
 
29967
29977
  // TODO(benchristel): can the default set of points be removed here? I don't
@@ -29969,6 +29979,7 @@ const PolygonGraph = props => {
29969
29979
  const points = coords ?? [[0, 0]];
29970
29980
  const ref = React__namespace.useRef(null);
29971
29981
  const dragReferencePoint = points[0];
29982
+ const snapToValue = snapTo ?? "grid";
29972
29983
  const {
29973
29984
  dragging
29974
29985
  } = mafs.useMovable({
@@ -29978,7 +29989,7 @@ const PolygonGraph = props => {
29978
29989
  const delta = mafs.vec.sub(newPoint, dragReferencePoint);
29979
29990
  dispatch(moveAll(delta));
29980
29991
  },
29981
- constrain: p => snap(snapStep, p)
29992
+ constrain: p => ["angles", "sides"].includes(snapToValue) ? p : snap(snapStep, p)
29982
29993
  });
29983
29994
  const active = hovered || focused || dragging;
29984
29995
  const lines = getLines(points);
@@ -30001,12 +30012,13 @@ const PolygonGraph = props => {
30001
30012
  active: active,
30002
30013
  range: range,
30003
30014
  polygonLines: lines,
30004
- showAngles: !!showAngles
30015
+ showAngles: !!showAngles,
30016
+ snapTo: snapToValue
30005
30017
  });
30006
30018
  }), showSides && lines.map((_ref, i) => {
30007
30019
  let [start, end] = _ref;
30008
30020
  const [x, y] = mafs.vec.midpoint(start, end);
30009
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(1));
30021
+ const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
30010
30022
  return /*#__PURE__*/React__namespace.createElement(TextLabel, {
30011
30023
  key: "side-" + i,
30012
30024
  x: x,
@@ -30030,153 +30042,12 @@ const PolygonGraph = props => {
30030
30042
  }
30031
30043
  }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
30032
30044
  key: "point-" + i,
30045
+ snapTo: snapTo,
30033
30046
  point: point,
30034
30047
  onMove: destination => dispatch(movePoint(i, destination))
30035
30048
  })));
30036
30049
  };
30037
30050
 
30038
- function initializeGraphState(params) {
30039
- const {
30040
- graph,
30041
- step,
30042
- snapStep,
30043
- range
30044
- } = params;
30045
- const shared = {
30046
- hasBeenInteractedWith: false,
30047
- range,
30048
- snapStep
30049
- };
30050
- switch (graph.type) {
30051
- case "segment":
30052
- return {
30053
- ...shared,
30054
- type: "segment",
30055
- coords: getSegmentCoords({
30056
- graph,
30057
- step,
30058
- range
30059
- })
30060
- };
30061
- case "linear":
30062
- return {
30063
- ...shared,
30064
- type: graph.type,
30065
- coords: getLineCoords({
30066
- graph,
30067
- range,
30068
- step
30069
- })
30070
- };
30071
- case "ray":
30072
- return {
30073
- ...shared,
30074
- type: graph.type,
30075
- coords: getLineCoords({
30076
- graph,
30077
- range,
30078
- step
30079
- })
30080
- };
30081
- case "linear-system":
30082
- return {
30083
- ...shared,
30084
- type: graph.type,
30085
- coords: getLinearSystemCoords({
30086
- graph,
30087
- range,
30088
- step
30089
- })
30090
- };
30091
- case "polygon":
30092
- return {
30093
- ...shared,
30094
- type: "polygon",
30095
- showAngles: Boolean(graph.showAngles),
30096
- showSides: Boolean(graph.showSides),
30097
- coords: getPolygonCoords({
30098
- graph,
30099
- range,
30100
- step
30101
- })
30102
- };
30103
- case "point":
30104
- return {
30105
- ...shared,
30106
- type: graph.type,
30107
- coords: getDefaultPoints({
30108
- graph,
30109
- step,
30110
- range
30111
- })
30112
- };
30113
- case "circle":
30114
- return {
30115
- ...shared,
30116
- type: graph.type,
30117
- ...getCircleCoords(graph)
30118
- };
30119
- case "quadratic":
30120
- return {
30121
- ...shared,
30122
- type: graph.type,
30123
- coords: getQuadraticCoords(graph, range, step)
30124
- };
30125
- case "sinusoid":
30126
- return {
30127
- ...shared,
30128
- type: graph.type,
30129
- coords: getSinusoidCoords(graph, range, step)
30130
- };
30131
- case "angle":
30132
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
30133
- default:
30134
- throw new wonderStuffCore.UnreachableCaseError(graph);
30135
- }
30136
- }
30137
- const getDefaultPoints = _ref => {
30138
- let {
30139
- graph,
30140
- range,
30141
- step
30142
- } = _ref;
30143
- const numPoints = graph.numPoints || 1;
30144
- let coords = graph.coords?.slice();
30145
- if (coords) {
30146
- return coords;
30147
- }
30148
- switch (numPoints) {
30149
- case 1:
30150
- // Back in the day, one point's coords were in graph.coord
30151
- coords = [graph.coord || [0, 0]];
30152
- break;
30153
- case 2:
30154
- coords = [[-5, 0], [5, 0]];
30155
- break;
30156
- case 3:
30157
- coords = [[-5, 0], [0, 0], [5, 0]];
30158
- break;
30159
- case 4:
30160
- coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
30161
- break;
30162
- case 5:
30163
- coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
30164
- break;
30165
- case 6:
30166
- coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
30167
- break;
30168
- default:
30169
- coords = [];
30170
- break;
30171
- }
30172
- // Transform coords from their -10 to 10 space to 0 to 1
30173
- // because of the old graph.coord, and also it's easier.
30174
- const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30175
- return normalizePoints(range, step, newCoords);
30176
- };
30177
-
30178
- // TS v4 doesn't narrow return types, while v5 does.
30179
- // Instead of updating to v5, using generic type to relate input and output types.
30180
30051
  function getGradableGraph(state, initialGraph) {
30181
30052
  if (!state.hasBeenInteractedWith) {
30182
30053
  return {
@@ -30240,118 +30111,6 @@ function getGradableGraph(state, initialGraph) {
30240
30111
  }
30241
30112
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
30242
30113
  }
30243
- const getSegmentCoords = _ref2 => {
30244
- let {
30245
- graph,
30246
- range,
30247
- step
30248
- } = _ref2;
30249
- if (graph.coords) {
30250
- return graph.coords;
30251
- }
30252
- const ys = n => {
30253
- switch (n) {
30254
- case 2:
30255
- return [5, -5];
30256
- case 3:
30257
- return [5, 0, -5];
30258
- case 4:
30259
- return [6, 2, -2, -6];
30260
- case 5:
30261
- return [6, 3, 0, -3, -6];
30262
- case 6:
30263
- return [5, 3, 1, -1, -3, -5];
30264
- default:
30265
- return [5];
30266
- }
30267
- };
30268
- const defaultRange = [[-10, 10], [-10, 10]];
30269
- return ys(graph.numSegments).map(y => {
30270
- let endpoints = [[-5, y], [5, y]];
30271
- endpoints = normalizeCoords(endpoints, defaultRange);
30272
- endpoints = normalizePoints(range, step, endpoints);
30273
- return endpoints;
30274
- });
30275
- };
30276
- const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
30277
- const getLineCoords = _ref3 => {
30278
- let {
30279
- graph,
30280
- range,
30281
- step
30282
- } = _ref3;
30283
- if (graph.coords) {
30284
- return graph.coords;
30285
- }
30286
- return normalizePoints(range, step, defaultLinearCoords[0]);
30287
- };
30288
- const getLinearSystemCoords = _ref4 => {
30289
- let {
30290
- graph,
30291
- range,
30292
- step
30293
- } = _ref4;
30294
- if (graph.coords) {
30295
- return graph.coords;
30296
- }
30297
- return defaultLinearCoords.map(points => normalizePoints(range, step, points));
30298
- };
30299
- const getPolygonCoords = _ref5 => {
30300
- let {
30301
- graph,
30302
- range,
30303
- step
30304
- } = _ref5;
30305
- let coords = graph.coords?.slice();
30306
- if (coords) {
30307
- return coords;
30308
- }
30309
- const n = graph.numSides || 3;
30310
- if (n === "unlimited") {
30311
- coords = [];
30312
- } else {
30313
- const angle = 2 * Math.PI / n;
30314
- const offset = (1 / n - 1 / 2) * Math.PI;
30315
-
30316
- // TODO(alex): Generalize this to more than just triangles so that
30317
- // all polygons have whole number side lengths if snapping to sides
30318
- const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
30319
-
30320
- // Generate coords of a regular polygon with n sides
30321
- coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
30322
- }
30323
- coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
30324
- const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
30325
- coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
30326
- return coords;
30327
- };
30328
- const getSinusoidCoords = (graph, range, step) => {
30329
- if (graph.coords) {
30330
- return [graph.coords[0], graph.coords[1]];
30331
- }
30332
- let coords = [[0.5, 0.5], [0.65, 0.6]];
30333
- coords = normalizePoints(range, step, coords, true);
30334
- return coords;
30335
- };
30336
- const getQuadraticCoords = (graph, range, step) => {
30337
- if (graph.coords) {
30338
- return graph.coords;
30339
- }
30340
- const defaultCoords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
30341
- return normalizePoints(range, step, defaultCoords, true);
30342
- };
30343
- const getCircleCoords = (graph, range, step) => {
30344
- if (graph.center != null && graph.radius != null) {
30345
- return {
30346
- center: graph.center,
30347
- radiusPoint: mafs.vec.add(graph.center, [graph.radius, 0])
30348
- };
30349
- }
30350
- return {
30351
- center: [0, 0],
30352
- radiusPoint: [1, 0]
30353
- };
30354
- };
30355
30114
 
30356
30115
  /**
30357
30116
  * determine radius of a circle graph
@@ -30485,38 +30244,16 @@ function QuadraticGraph(props) {
30485
30244
 
30486
30245
  // Calculate the y value based on the current x value
30487
30246
  const y = x => (a * x + b) * x + c;
30488
- const handleOnMove = (destination, elementId) => {
30489
- // If the destination is invalid, we want do not want to move the point
30490
- const validDestination = isValidDestination(destination, elementId, coords);
30491
- if (validDestination === false) {
30492
- return;
30493
- }
30494
- dispatch(movePoint(elementId, destination));
30495
- };
30496
30247
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
30497
30248
  y: y,
30498
30249
  color: wonderBlocksTokens.color.blue
30499
30250
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
30500
30251
  key: "point-" + i,
30501
30252
  point: coord,
30502
- onMove: destination => handleOnMove(destination, i)
30253
+ onMove: destination => dispatch(movePoint(i, destination))
30503
30254
  })));
30504
30255
  }
30505
30256
 
30506
- // Ensure that we are only snapping to coordinates that result in a valid quadratic equation
30507
- const isValidDestination = (destination, elementId, coords) => {
30508
- // Set up the new coords and check if the quadratic coefficients are valid
30509
- const newCoords = [...coords];
30510
- newCoords[elementId] = destination;
30511
- const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
30512
-
30513
- // If the new destination results in an invalid quadratic equation, we don't want to move the point
30514
- if (QuadraticCoefficients === undefined) {
30515
- return false;
30516
- }
30517
- return true;
30518
- };
30519
-
30520
30257
  // Get the quadratic coefficients from the 3 control points
30521
30258
  // These equations were originally set up in 2013 and may require some
30522
30259
  // additional comments to help clarify the quadratic formula manipulations
@@ -31666,6 +31403,210 @@ const LegacyGrid = _ref => {
31666
31403
  return null;
31667
31404
  };
31668
31405
 
31406
+ function initializeGraphState(params) {
31407
+ const {
31408
+ graph,
31409
+ step,
31410
+ snapStep,
31411
+ range
31412
+ } = params;
31413
+ const shared = {
31414
+ hasBeenInteractedWith: false,
31415
+ range,
31416
+ snapStep
31417
+ };
31418
+ switch (graph.type) {
31419
+ case "segment":
31420
+ return {
31421
+ ...shared,
31422
+ type: "segment",
31423
+ coords: getSegmentCoords(graph, range, step)
31424
+ };
31425
+ case "linear":
31426
+ return {
31427
+ ...shared,
31428
+ type: graph.type,
31429
+ coords: getLineCoords(graph, range, step)
31430
+ };
31431
+ case "ray":
31432
+ return {
31433
+ ...shared,
31434
+ type: graph.type,
31435
+ coords: getLineCoords(graph, range, step)
31436
+ };
31437
+ case "linear-system":
31438
+ return {
31439
+ ...shared,
31440
+ type: graph.type,
31441
+ coords: getLinearSystemCoords(graph, range, step)
31442
+ };
31443
+ case "polygon":
31444
+ return {
31445
+ ...shared,
31446
+ type: "polygon",
31447
+ showAngles: Boolean(graph.showAngles),
31448
+ showSides: Boolean(graph.showSides),
31449
+ coords: getPolygonCoords(graph, range, step),
31450
+ snapTo: graph.snapTo ?? "grid"
31451
+ };
31452
+ case "point":
31453
+ return {
31454
+ ...shared,
31455
+ type: graph.type,
31456
+ coords: getPointCoords(graph, range, step)
31457
+ };
31458
+ case "circle":
31459
+ return {
31460
+ ...shared,
31461
+ type: graph.type,
31462
+ ...getCircleCoords(graph)
31463
+ };
31464
+ case "quadratic":
31465
+ return {
31466
+ ...shared,
31467
+ type: graph.type,
31468
+ coords: getQuadraticCoords(graph, range, step)
31469
+ };
31470
+ case "sinusoid":
31471
+ return {
31472
+ ...shared,
31473
+ type: graph.type,
31474
+ coords: getSinusoidCoords(graph, range, step)
31475
+ };
31476
+ case "angle":
31477
+ throw new Error("Mafs not yet implemented for graph type: " + graph.type);
31478
+ default:
31479
+ throw new wonderStuffCore.UnreachableCaseError(graph);
31480
+ }
31481
+ }
31482
+ function getPointCoords(graph, range, step) {
31483
+ const numPoints = graph.numPoints || 1;
31484
+ let coords = graph.coords?.slice();
31485
+ if (coords) {
31486
+ return coords;
31487
+ }
31488
+ switch (numPoints) {
31489
+ case 1:
31490
+ // Back in the day, one point's coords were in graph.coord
31491
+ coords = [graph.coord || [0, 0]];
31492
+ break;
31493
+ case 2:
31494
+ coords = [[-5, 0], [5, 0]];
31495
+ break;
31496
+ case 3:
31497
+ coords = [[-5, 0], [0, 0], [5, 0]];
31498
+ break;
31499
+ case 4:
31500
+ coords = [[-6, 0], [-2, 0], [2, 0], [6, 0]];
31501
+ break;
31502
+ case 5:
31503
+ coords = [[-6, 0], [-3, 0], [0, 0], [3, 0], [6, 0]];
31504
+ break;
31505
+ case 6:
31506
+ coords = [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]];
31507
+ break;
31508
+ default:
31509
+ coords = [];
31510
+ break;
31511
+ }
31512
+ // Transform coords from their -10 to 10 space to 0 to 1
31513
+ // because of the old graph.coord, and also it's easier.
31514
+ const newCoords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31515
+ return normalizePoints(range, step, newCoords);
31516
+ }
31517
+ function getSegmentCoords(graph, range, step) {
31518
+ if (graph.coords) {
31519
+ return graph.coords;
31520
+ }
31521
+ const ys = n => {
31522
+ switch (n) {
31523
+ case 2:
31524
+ return [5, -5];
31525
+ case 3:
31526
+ return [5, 0, -5];
31527
+ case 4:
31528
+ return [6, 2, -2, -6];
31529
+ case 5:
31530
+ return [6, 3, 0, -3, -6];
31531
+ case 6:
31532
+ return [5, 3, 1, -1, -3, -5];
31533
+ default:
31534
+ return [5];
31535
+ }
31536
+ };
31537
+ const defaultRange = [[-10, 10], [-10, 10]];
31538
+ return ys(graph.numSegments).map(y => {
31539
+ let endpoints = [[-5, y], [5, y]];
31540
+ endpoints = normalizeCoords(endpoints, defaultRange);
31541
+ endpoints = normalizePoints(range, step, endpoints);
31542
+ return endpoints;
31543
+ });
31544
+ }
31545
+ const defaultLinearCoords = [[[0.25, 0.75], [0.75, 0.75]], [[0.25, 0.25], [0.75, 0.25]]];
31546
+ function getLineCoords(graph, range, step) {
31547
+ if (graph.coords) {
31548
+ return graph.coords;
31549
+ }
31550
+ return normalizePoints(range, step, defaultLinearCoords[0]);
31551
+ }
31552
+ function getLinearSystemCoords(graph, range, step) {
31553
+ if (graph.coords) {
31554
+ return graph.coords;
31555
+ }
31556
+ return defaultLinearCoords.map(points => normalizePoints(range, step, points));
31557
+ }
31558
+ function getPolygonCoords(graph, range, step) {
31559
+ let coords = graph.coords?.slice();
31560
+ if (coords) {
31561
+ return coords;
31562
+ }
31563
+ const n = graph.numSides || 3;
31564
+ if (n === "unlimited") {
31565
+ coords = [];
31566
+ } else {
31567
+ const angle = 2 * Math.PI / n;
31568
+ const offset = (1 / n - 1 / 2) * Math.PI;
31569
+
31570
+ // TODO(alex): Generalize this to more than just triangles so that
31571
+ // all polygons have whole number side lengths if snapping to sides
31572
+ const radius = graph.snapTo === "sides" ? Math.sqrt(3) / 3 * 7 : 4;
31573
+
31574
+ // Generate coords of a regular polygon with n sides
31575
+ coords = [...Array(n).keys()].map(i => [radius * Math.cos(i * angle + offset), radius * Math.sin(i * angle + offset)]);
31576
+ }
31577
+ coords = normalizeCoords(coords, [[-10, 10], [-10, 10]]);
31578
+ const snapToGrid = !["angles", "sides"].includes(graph.snapTo || "");
31579
+ coords = normalizePoints(range, step, coords, /* noSnap */!snapToGrid);
31580
+ return coords;
31581
+ }
31582
+ function getSinusoidCoords(graph, range, step) {
31583
+ if (graph.coords) {
31584
+ return [graph.coords[0], graph.coords[1]];
31585
+ }
31586
+ let coords = [[0.5, 0.5], [0.65, 0.6]];
31587
+ coords = normalizePoints(range, step, coords, true);
31588
+ return coords;
31589
+ }
31590
+ function getQuadraticCoords(graph, range, step) {
31591
+ if (graph.coords) {
31592
+ return graph.coords;
31593
+ }
31594
+ const defaultCoords = [[0.25, 0.75], [0.5, 0.25], [0.75, 0.75]];
31595
+ return normalizePoints(range, step, defaultCoords, true);
31596
+ }
31597
+ function getCircleCoords(graph) {
31598
+ if (graph.center != null && graph.radius != null) {
31599
+ return {
31600
+ center: graph.center,
31601
+ radiusPoint: mafs.vec.add(graph.center, [graph.radius, 0])
31602
+ };
31603
+ }
31604
+ return {
31605
+ center: [0, 0],
31606
+ radiusPoint: [2, 0]
31607
+ };
31608
+ }
31609
+
31669
31610
  function interactiveGraphReducer(state, action) {
31670
31611
  switch (action.type) {
31671
31612
  case MOVE_CONTROL_POINT:
@@ -31803,11 +31744,20 @@ function doMoveAll(state, action) {
31803
31744
  switch (state.type) {
31804
31745
  case "polygon":
31805
31746
  {
31806
- const change = getChange(state.coords, action.delta, {
31807
- snapStep,
31808
- range
31809
- });
31810
- const newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
31747
+ let newCoords;
31748
+ if (state.snapTo === "sides" || state.snapTo === "angles") {
31749
+ const change = getChange(state.coords, action.delta, {
31750
+ snapStep: [0, 0],
31751
+ range
31752
+ });
31753
+ newCoords = state.coords.map(point => mafs.vec.add(point, change));
31754
+ } else {
31755
+ const change = getChange(state.coords, action.delta, {
31756
+ snapStep,
31757
+ range
31758
+ });
31759
+ newCoords = state.coords.map(point => snap(snapStep, mafs.vec.add(point, change)));
31760
+ }
31811
31761
  return {
31812
31762
  ...state,
31813
31763
  hasBeenInteractedWith: true,
@@ -31822,10 +31772,18 @@ function doMoveAll(state, action) {
31822
31772
  function doMovePoint(state, action) {
31823
31773
  switch (state.type) {
31824
31774
  case "polygon":
31775
+ let newValue;
31776
+ if (state.snapTo === "sides") {
31777
+ newValue = boundAndSnapToSides(action.destination, state, action.index);
31778
+ } else if (state.snapTo === "angles") {
31779
+ newValue = boundAndSnapToAngle(action.destination, state, action.index);
31780
+ } else {
31781
+ newValue = boundAndSnapToGrid(action.destination, state);
31782
+ }
31825
31783
  const newCoords = setAtIndex({
31826
31784
  array: state.coords,
31827
31785
  index: action.index,
31828
- newValue: boundAndSnapToGrid(action.destination, state)
31786
+ newValue: newValue
31829
31787
  });
31830
31788
 
31831
31789
  // Reject the move if it would cause the sides of the polygon to cross
@@ -31871,6 +31829,15 @@ function doMovePoint(state, action) {
31871
31829
  }
31872
31830
  case "quadratic":
31873
31831
  {
31832
+ // Set up the new coords and check if the quadratic coefficients are valid
31833
+ const newCoords = [...state.coords];
31834
+ newCoords[action.index] = action.destination;
31835
+ const QuadraticCoefficients = getQuadraticCoefficients(newCoords);
31836
+
31837
+ // If the new destination results in an invalid quadratic equation, we don't want to move the point
31838
+ if (QuadraticCoefficients === undefined) {
31839
+ return state;
31840
+ }
31874
31841
  return {
31875
31842
  ...state,
31876
31843
  hasBeenInteractedWith: true,
@@ -31968,8 +31935,17 @@ function doChangeRange(state, action) {
31968
31935
  range: action.range
31969
31936
  };
31970
31937
  }
31971
- const getChange = (coords, delta, constraintOpts) => {
31938
+ const getDeltaVertex = (maxMoves, minMoves, delta) => {
31972
31939
  const [deltaX, deltaY] = delta;
31940
+ const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31941
+ const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31942
+ const minXMove = Math.max(...minMoves.map(move => move[0]));
31943
+ const minYMove = Math.max(...minMoves.map(move => move[1]));
31944
+ const dx = clamp(deltaX, minXMove, maxXMove);
31945
+ const dy = clamp(deltaY, minYMove, maxYMove);
31946
+ return [dx, dy];
31947
+ };
31948
+ const getChange = (coords, delta, constraintOpts) => {
31973
31949
  const maxMoves = coords.map(point => maxMove({
31974
31950
  ...constraintOpts,
31975
31951
  point
@@ -31978,14 +31954,15 @@ const getChange = (coords, delta, constraintOpts) => {
31978
31954
  ...constraintOpts,
31979
31955
  point
31980
31956
  }));
31981
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31982
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31983
- const minXMove = Math.max(...minMoves.map(move => move[0]));
31984
- const minYMove = Math.max(...minMoves.map(move => move[1]));
31985
- const dx = clamp(deltaX, minXMove, maxXMove);
31986
- const dy = clamp(deltaY, minYMove, maxYMove);
31957
+ const [dx, dy] = getDeltaVertex(maxMoves, minMoves, delta);
31987
31958
  return [dx, dy];
31988
31959
  };
31960
+ const eq$1 = Util.eq;
31961
+
31962
+ // Less than or approximately equal
31963
+ function leq$1(a, b) {
31964
+ return a < b || eq$1(a, b);
31965
+ }
31989
31966
  function boundAndSnapToGrid(point, _ref) {
31990
31967
  let {
31991
31968
  snapStep,
@@ -31997,28 +31974,155 @@ function boundAndSnapToGrid(point, _ref) {
31997
31974
  point
31998
31975
  }));
31999
31976
  }
31977
+ function boundAndSnapToAngle(destinationPoint, _ref2, index) {
31978
+ let {
31979
+ range,
31980
+ coords
31981
+ } = _ref2;
31982
+ const startingPoint = coords[index];
31983
+
31984
+ // Needed to prevent updating the original coords before the checks for
31985
+ // degenerate triangles and overlapping sides
31986
+ const coordsCopy = [...coords];
31987
+
31988
+ // Takes the destination point and makes sure it is within the bounds of the graph
31989
+ // SnapStep is [0, 0] because we don't want to snap to the grid
31990
+ coordsCopy[index] = bound$1({
31991
+ snapStep: [0, 0],
31992
+ range,
31993
+ point: destinationPoint
31994
+ });
31995
+
31996
+ // Gets the radian angles between the coords and maps them to degrees
31997
+ const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
31998
+
31999
+ // Gets the relative index of a point
32000
+ const rel = j => {
32001
+ return (index + j + coordsCopy.length) % coordsCopy.length;
32002
+ };
32003
+
32004
+ // Round the angles to left and right of the current point
32005
+ ___default["default"].each([-1, 1], function (j) {
32006
+ angles[rel(j)] = Math.round(angles[rel(j)]);
32007
+ });
32008
+ const getAngle = function (a, vertex, b) {
32009
+ const angle = GraphUtils.findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
32010
+ return (angle + 360) % 360;
32011
+ };
32012
+ const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
32013
+ innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
32014
+
32015
+ // Avoid degenerate triangles
32016
+ if (innerAngles.some(function (angle) {
32017
+ return leq$1(angle, 1);
32018
+ })) {
32019
+ return startingPoint;
32020
+ }
32021
+ const knownSide = magnitude(
32022
+ // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
32023
+ vector(coordsCopy[rel(-1)], coordsCopy[rel(1)]));
32024
+
32025
+ // Returns true if the points form a counter-clockwise turn;
32026
+ // a.k.a if the point is on the left or right of the polygon.
32027
+ // This is used to determine how to adjust the point: if the point is on the left,
32028
+ // we want to add the inner angle to the outer angle, and if it's on the right,
32029
+ // we want to subtract the inner angle from the outer angle. The angle solved
32030
+ // for is then used in the polar function to determine the new point.
32031
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
32032
+
32033
+ // Solve for side by using the law of sines
32034
+ const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
32035
+
32036
+ // Angle at the second vertex of the polygon
32037
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32038
+
32039
+ // Uses the length of the side of the polygon (radial coordinate)
32040
+ // and the angle between the first and second sides of the
32041
+ // polygon (angular coordinate) to determine how to adjust the point
32042
+ const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
32043
+ return kmath.vector.add(coordsCopy[rel(-1)], offset);
32044
+ }
32045
+ function boundAndSnapToSides(destinationPoint, _ref3, index) {
32046
+ let {
32047
+ range,
32048
+ coords
32049
+ } = _ref3;
32050
+ const startingPoint = coords[index];
32051
+
32052
+ // Needed to prevent updating the original coords before the checks for
32053
+ // degenerate triangles and overlapping sides
32054
+ const coordsCopy = [...coords];
32055
+
32056
+ // Takes the destination point and makes sure it is within the bounds of the graph
32057
+ // SnapStep is [0, 0] because we don't want to snap to the grid
32058
+ coordsCopy[index] = bound$1({
32059
+ snapStep: [0, 0],
32060
+ range,
32061
+ point: destinationPoint
32062
+ });
32063
+
32064
+ // Gets the relative index of a point
32065
+ const rel = j => {
32066
+ return (index + j + coordsCopy.length) % coordsCopy.length;
32067
+ };
32068
+ const sides = ___default["default"].map([[coordsCopy[rel(-1)], coordsCopy[index]], [coordsCopy[index], coordsCopy[rel(1)]], [coordsCopy[rel(-1)], coordsCopy[rel(1)]]], function (coordsCopy) {
32069
+ // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
32070
+ return magnitude(vector(...coordsCopy));
32071
+ });
32072
+
32073
+ // Round the sides to left and right of the current point
32074
+ ___default["default"].each([0, 1], function (j) {
32075
+ sides[j] = Math.round(sides[j]);
32076
+ });
32077
+
32078
+ // Avoid degenerate triangles
32079
+ if (leq$1(sides[1] + sides[2], sides[0]) || leq$1(sides[0] + sides[2], sides[1]) || leq$1(sides[0] + sides[1], sides[2])) {
32080
+ return startingPoint;
32081
+ }
32082
+
32083
+ // Solve for angle by using the law of cosines
32084
+ // Angle at the first vertex of the polygon
32085
+ const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
32086
+
32087
+ // Angle at the second vertex of the polygon
32088
+ const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32089
+
32090
+ // Returns true if the points form a counter-clockwise turn;
32091
+ // a.k.a if the point is on the left or right of the polygon.
32092
+ // This is used to determine how to adjust the point: if the point is on the left,
32093
+ // we want to add the inner angle to the outer angle, and if it's on the right,
32094
+ // we want to subtract the inner angle from the outer angle. The angle solved
32095
+ // for is then used in the polar function to determine the new point.
32096
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
32097
+
32098
+ // Uses the length of the first side of the polygon (radial coordinate)
32099
+ // and the angle between the first and second sides of the
32100
+ // polygon (angular coordinate) to determine how to adjust the point
32101
+ const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
32102
+ return kmath.vector.add(coordsCopy[rel(-1)], offset);
32103
+ }
32000
32104
 
32001
32105
  // Returns the closest point to the given `point` that is within the graph
32002
32106
  // bounds given in `state`.
32003
- function bound$1(_ref2) {
32107
+ function bound$1(_ref4) {
32004
32108
  let {
32005
32109
  snapStep,
32006
32110
  range,
32007
32111
  point
32008
- } = _ref2;
32112
+ } = _ref4;
32009
32113
  const [requestedX, requestedY] = point;
32010
32114
  const [snapX, snapY] = snapStep;
32011
32115
  const [[minX, maxX], [minY, maxY]] = range;
32012
32116
  return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
32013
32117
  }
32014
32118
 
32015
- // Returns the vector from the given point to the top-right corner of the graph
32016
- function maxMove(_ref3) {
32119
+ // Returns the vector from the given point to the top-right corner of the graph when snapped to the grid
32120
+ function maxMove(_ref5) {
32017
32121
  let {
32018
32122
  snapStep,
32019
32123
  range,
32020
32124
  point
32021
- } = _ref3;
32125
+ } = _ref5;
32022
32126
  const topRight = bound$1({
32023
32127
  snapStep,
32024
32128
  range,
@@ -32027,14 +32131,13 @@ function maxMove(_ref3) {
32027
32131
  return mafs.vec.sub(topRight, point);
32028
32132
  }
32029
32133
 
32030
- // Returns the vector from the given point to the bottom-left corner of the
32031
- // graph
32032
- function minMove(_ref4) {
32134
+ // Returns the vector from the given point to the bottom-left corner of the graph when snapped to the grid
32135
+ function minMove(_ref6) {
32033
32136
  let {
32034
32137
  snapStep,
32035
32138
  range,
32036
32139
  point
32037
- } = _ref4;
32140
+ } = _ref6;
32038
32141
  const bottomLeft = bound$1({
32039
32142
  snapStep,
32040
32143
  range,
@@ -44343,7 +44446,7 @@ const init = function (options) {
44343
44446
 
44344
44447
  // This file is processed by a Rollup plugin (replace) to inject the production
44345
44448
  const libName = "@khanacademy/perseus";
44346
- const libVersion = "23.3.0";
44449
+ const libVersion = "23.4.0";
44347
44450
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44348
44451
 
44349
44452
  const apiVersion = {