@khanacademy/perseus 24.1.0 → 24.3.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 (32) hide show
  1. package/dist/es/index.css +7 -0
  2. package/dist/es/index.js +655 -130
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.css +7 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +678 -141
  7. package/dist/index.js.map +1 -1
  8. package/dist/perseus-api.d.ts +2 -0
  9. package/dist/perseus-types.d.ts +17 -2
  10. package/dist/server-item-renderer.d.ts +2 -0
  11. package/dist/types.d.ts +1 -1
  12. package/dist/widgets/expression.d.ts +12 -0
  13. package/dist/widgets/interactive-graph.d.ts +1 -1
  14. package/dist/widgets/interactive-graphs/graphs/angle.d.ts +5 -0
  15. package/dist/widgets/interactive-graphs/graphs/components/{angle.d.ts → angle-indicators.d.ts} +12 -2
  16. package/dist/widgets/interactive-graphs/graphs/components/movable-line.d.ts +11 -0
  17. package/dist/widgets/interactive-graphs/graphs/components/text-label.d.ts +1 -1
  18. package/dist/widgets/interactive-graphs/graphs/index.d.ts +1 -0
  19. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +3 -1
  20. package/dist/widgets/interactive-graphs/locked-figures/locked-function.d.ts +4 -0
  21. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +1 -1
  22. package/dist/widgets/interactive-graphs/math/angles.d.ts +3 -0
  23. package/dist/widgets/interactive-graphs/math/box.d.ts +4 -0
  24. package/dist/widgets/interactive-graphs/math/clamp.d.ts +1 -0
  25. package/dist/widgets/interactive-graphs/math/coordinates.d.ts +2 -0
  26. package/dist/widgets/interactive-graphs/math/index.d.ts +6 -0
  27. package/dist/widgets/interactive-graphs/math/interval.d.ts +5 -0
  28. package/dist/widgets/interactive-graphs/math/snap.d.ts +2 -0
  29. package/dist/widgets/interactive-graphs/types.d.ts +9 -1
  30. package/dist/widgets/interactive-graphs/utils.d.ts +0 -2
  31. package/dist/widgets/label-image.d.ts +4 -0
  32. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -9068,7 +9068,7 @@ var Tex$1 = {
9068
9068
  const {
9069
9069
  processMath: processMath$1
9070
9070
  } = Tex$1;
9071
- function polar(r, th) {
9071
+ function polar$1(r, th) {
9072
9072
  if (typeof r === "number") {
9073
9073
  r = [r, r];
9074
9074
  }
@@ -9404,8 +9404,8 @@ class Graphie$1 {
9404
9404
  endAngle = (endAngle % 360 + 360) % 360;
9405
9405
  const cent = this.scalePoint(center);
9406
9406
  const radii = this.scaleVector(radius);
9407
- const startVector = polar(radius, startAngle);
9408
- const endVector = polar(radius, endAngle);
9407
+ const startVector = polar$1(radius, startAngle);
9408
+ const endVector = polar$1(radius, endAngle);
9409
9409
 
9410
9410
  // We round the coordinates to make testing easier, because trig
9411
9411
  // operations return unround numbers.
@@ -13834,7 +13834,7 @@ ___default["default"].extend(MovableAngle.prototype, {
13834
13834
  let angle = GraphUtils.findAngle(newPoint, vertex);
13835
13835
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13836
13836
  const distance = GraphUtils.getDistance(newPoint, vertex);
13837
- return kmath.vector.add(vertex, polar(distance, angle));
13837
+ return kmath.vector.add(vertex, polar$1(distance, angle));
13838
13838
  }
13839
13839
  return true;
13840
13840
  };
@@ -16794,7 +16794,7 @@ const Path = GraphieClasses.createClass({
16794
16794
  this.path.toFront();
16795
16795
  }
16796
16796
  });
16797
- const Arc = GraphieClasses.createSimpleClass((graphie, props) => {
16797
+ const Arc$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16798
16798
  let center = props.center;
16799
16799
  let radius = props.radius;
16800
16800
  if (props.unscaled) {
@@ -16810,7 +16810,7 @@ const Rect$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16810
16810
  return graphie.rect(props.x, props.y, props.width, props.height, props.style);
16811
16811
  });
16812
16812
  var Movables = {
16813
- Arc: Arc,
16813
+ Arc: Arc$1,
16814
16814
  Circle: Circle,
16815
16815
  Label: Label$1,
16816
16816
  Line: Line$3,
@@ -28657,6 +28657,92 @@ const useTransformDimensionsToPixels = function () {
28657
28657
  return dimensionsToPixels(dimens, graphState);
28658
28658
  };
28659
28659
 
28660
+ // Restricts `value` to be between `min` and `max`.
28661
+ function clamp(value, min, max) {
28662
+ if (value < min) {
28663
+ return min;
28664
+ }
28665
+ if (value > max) {
28666
+ return max;
28667
+ }
28668
+ return value;
28669
+ }
28670
+
28671
+ // Rounds each component of `point` to the nearest multiple of the
28672
+ // corresponding component of `snapStep`. E.g. if the snap step is [2, 3],
28673
+ // the x-coord of the point will be rounded to the nearest multiple of 2, and
28674
+ // the y-coord of the point will be rounded to the nearest multiple of 3.
28675
+ function snap(snapStep, point) {
28676
+ const [requestedX, requestedY] = point;
28677
+ const [snapX, snapY] = snapStep;
28678
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28679
+ }
28680
+
28681
+ // Use X and Y to index into arrays that represent coordinate pairs.
28682
+ // e.g. point[X]
28683
+ const X = 0;
28684
+ const Y = 1;
28685
+
28686
+ // Use MIN and MAX to index into arrays that represent intervals.
28687
+ // e.g. range[MAX]
28688
+ const MIN = 0;
28689
+ const MAX = 1;
28690
+ function size(_ref) {
28691
+ let [min, max] = _ref;
28692
+ return max - min;
28693
+ }
28694
+
28695
+ // Removes an equal amount from each end of the interval.
28696
+ //
28697
+ // If the given amount cannot be removed (because the interval is too small),
28698
+ // half the interval's size is removed from each end instead, so the interval
28699
+ // ends up with size = 0, and both MAX and MIN equal to its former midpoint.
28700
+ //
28701
+ // If `amount` is negative, the interval is expanded instead.
28702
+ function trim(amount, interval) {
28703
+ if (amount * 2 > size(interval)) {
28704
+ const middle = average(...interval);
28705
+ return [middle, middle];
28706
+ }
28707
+ return [interval[MIN] + amount, interval[MAX] - amount];
28708
+ }
28709
+ function average(a, b) {
28710
+ return (a + b) / 2;
28711
+ }
28712
+
28713
+ // Restricts the `point` to be within `box`, by clamping each coordinate to the
28714
+ // corresponding interval.
28715
+ function clampToBox(box, point) {
28716
+ return [clamp(point[X], ...box[X]), clamp(point[Y], ...box[Y])];
28717
+ }
28718
+
28719
+ // Reduces the size of `box`, trimming each corner by the given positive
28720
+ // `amount`. If a component of `amount` is negative, the box is expanded in
28721
+ // that dimension instead.
28722
+ function inset(amount, box) {
28723
+ return [trim(amount[X], box[X]), trim(amount[Y], box[Y])];
28724
+ }
28725
+
28726
+ // This file contains helper functions for working with angles.
28727
+ function polar(r, th) {
28728
+ if (typeof r === "number") {
28729
+ r = [r, r];
28730
+ }
28731
+ th = th * Math.PI / 180;
28732
+ return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
28733
+ }
28734
+ const findAngle = (point1, point2, vertex) => {
28735
+ if (vertex === undefined) {
28736
+ const x = point1[0] - point2[0];
28737
+ const y = point1[1] - point2[1];
28738
+ if (!x && !y) {
28739
+ return 0;
28740
+ }
28741
+ return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
28742
+ }
28743
+ return findAngle(point1, vertex) - findAngle(point2, vertex);
28744
+ };
28745
+
28660
28746
  function AxisLabels() {
28661
28747
  const {
28662
28748
  range,
@@ -28664,8 +28750,8 @@ function AxisLabels() {
28664
28750
  width,
28665
28751
  height
28666
28752
  } = useGraphConfig();
28667
- const yAxisLabelLocation = [0, range[1][1]];
28668
- const xAxisLabelLocation = [range[0][1], 0];
28753
+ const yAxisLabelLocation = [0, range[Y][MAX]];
28754
+ const xAxisLabelLocation = [range[X][MAX], 0];
28669
28755
  const [xAxisLabelText, yAxisLabelText] = labels;
28670
28756
  const graphInfo = {
28671
28757
  range,
@@ -28836,6 +28922,32 @@ const LockedEllipse = props => {
28836
28922
  });
28837
28923
  };
28838
28924
 
28925
+ const LockedFunction = props => {
28926
+ const {
28927
+ color,
28928
+ strokeStyle,
28929
+ directionalAxis,
28930
+ domain
28931
+ } = props;
28932
+ const plotProps = {
28933
+ color: lockedFigureColors[color],
28934
+ style: strokeStyle,
28935
+ domain
28936
+ };
28937
+ const equation = props.equationParsed || KAS__namespace.parse(props.equation).expr;
28938
+ return /*#__PURE__*/React__namespace.createElement("g", {
28939
+ className: "locked-function"
28940
+ }, directionalAxis === "x" && /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, _extends({
28941
+ y: x => equation.eval({
28942
+ x
28943
+ })
28944
+ }, plotProps)), directionalAxis === "y" && /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfY, _extends({
28945
+ x: y => equation.eval({
28946
+ y
28947
+ })
28948
+ }, plotProps)));
28949
+ };
28950
+
28839
28951
  function pathBuilder() {
28840
28952
  return new PathBuilder();
28841
28953
  }
@@ -28934,7 +29046,7 @@ function Arrowhead(props) {
28934
29046
  const [point] = useTransformVectorsToPixels(props.tip);
28935
29047
  return /*#__PURE__*/React__namespace.createElement("g", {
28936
29048
  className: "interactive-graph-arrowhead",
28937
- transform: `translate(${point[0]} ${point[1]}) rotate(${props.angle})`
29049
+ transform: `translate(${point[X]} ${point[Y]}) rotate(${props.angle})`
28938
29050
  }, /*#__PURE__*/React__namespace.createElement("g", {
28939
29051
  transform: "translate(-1.5)"
28940
29052
  }, /*#__PURE__*/React__namespace.createElement("path", {
@@ -29004,10 +29116,10 @@ function SVGLine(props) {
29004
29116
  testId
29005
29117
  } = props;
29006
29118
  return /*#__PURE__*/React__namespace.createElement("line", {
29007
- x1: start[0],
29008
- y1: start[1],
29009
- x2: end[0],
29010
- y2: end[1],
29119
+ x1: start[X],
29120
+ y1: start[Y],
29121
+ x2: end[X],
29122
+ y2: end[Y],
29011
29123
  style: style,
29012
29124
  className: className,
29013
29125
  "data-testid": testId
@@ -29092,8 +29204,8 @@ const LockedLine = props => {
29092
29204
  return /*#__PURE__*/React__namespace.createElement("g", {
29093
29205
  className: kind === "ray" ? "locked-ray" : "locked-line"
29094
29206
  }, line, showPoint1 && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
29095
- x: point1.coord[0],
29096
- y: point1.coord[1],
29207
+ x: point1.coord[X],
29208
+ y: point1.coord[Y],
29097
29209
  svgCircleProps: {
29098
29210
  style: {
29099
29211
  fill: point1.filled ? lockedFigureColors[point1.color] : wonderBlocksTokens.color.white,
@@ -29102,8 +29214,8 @@ const LockedLine = props => {
29102
29214
  }
29103
29215
  }
29104
29216
  }), showPoint2 && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
29105
- x: point2.coord[0],
29106
- y: point2.coord[1],
29217
+ x: point2.coord[X],
29218
+ y: point2.coord[Y],
29107
29219
  svgCircleProps: {
29108
29220
  style: {
29109
29221
  fill: point2.filled ? lockedFigureColors[point2.color] : wonderBlocksTokens.color.white,
@@ -29151,8 +29263,8 @@ const LockedPolygon = props => {
29151
29263
  color: lockedFigureColors[color]
29152
29264
  }), showVertices && points.map((point, index) => /*#__PURE__*/React__namespace.createElement(mafs.Point, {
29153
29265
  key: `locked-polygon-point-${index}`,
29154
- x: point[0],
29155
- y: point[1],
29266
+ x: point[X],
29267
+ y: point[Y],
29156
29268
  color: lockedFigureColors[color]
29157
29269
  })));
29158
29270
  };
@@ -29199,6 +29311,10 @@ const GraphLockedLayer = props => {
29199
29311
  return /*#__PURE__*/React__namespace.createElement(LockedPolygon, _extends({
29200
29312
  key: `polygon-${index}`
29201
29313
  }, figure));
29314
+ case "function":
29315
+ return /*#__PURE__*/React__namespace.createElement(LockedFunction, _extends({
29316
+ key: `function-${index}`
29317
+ }, figure));
29202
29318
  default:
29203
29319
  /**
29204
29320
  * Devlopment-time future-proofing: This should
@@ -29291,24 +29407,18 @@ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coor
29291
29407
  coords.map((coord, i) => {
29292
29408
  const axisRange = range[i];
29293
29409
  if (noSnap) {
29294
- return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
29410
+ return axisRange[MIN] + size(axisRange) * coord;
29295
29411
  }
29296
29412
  const axisStep = step[i];
29297
- const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
29413
+ const nSteps = Math.floor(size(axisRange) / axisStep);
29298
29414
  const tick = Math.round(coord * nSteps);
29299
- return axisRange[0] + axisStep * tick;
29415
+ return axisRange[MIN] + axisStep * tick;
29300
29416
  }));
29301
29417
 
29302
29418
  // same as normalizeCoords in interactive-graph.tsx
29303
29419
  const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
29304
- const extent = ranges[i][1] - ranges[i][0];
29305
- return (coord + ranges[i][1]) / extent;
29420
+ return (coord + ranges[i][1]) / size(ranges[i]);
29306
29421
  }));
29307
- function snap(snapStep, point) {
29308
- const [requestedX, requestedY] = point;
29309
- const [snapX, snapY] = snapStep;
29310
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29311
- }
29312
29422
 
29313
29423
  // Returns the closest point to the given `point` that is within the graph
29314
29424
  // bounds given in `state`.
@@ -29318,19 +29428,8 @@ function bound$1(_ref) {
29318
29428
  range,
29319
29429
  point
29320
29430
  } = _ref;
29321
- const [requestedX, requestedY] = point;
29322
- const [snapX, snapY] = snapStep;
29323
- const [[minX, maxX], [minY, maxY]] = range;
29324
- return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
29325
- }
29326
- function clamp(value, min, max) {
29327
- if (value < min) {
29328
- return min;
29329
- }
29330
- if (value > max) {
29331
- return max;
29332
- }
29333
- return value;
29431
+ const boundingBox = inset(snapStep, range);
29432
+ return clampToBox(boundingBox, point);
29334
29433
  }
29335
29434
 
29336
29435
  var isProduction = process.env.NODE_ENV === 'production';
@@ -29400,8 +29499,8 @@ function useDraggable(args) {
29400
29499
  metaKey,
29401
29500
  shiftKey
29402
29501
  } = state;
29403
- const direction = [yDownDirection[0], -yDownDirection[1]];
29404
- const span = Math.abs(direction[0]) ? xSpan : ySpan;
29502
+ const direction = [yDownDirection[X], -yDownDirection[Y]];
29503
+ const span = Math.abs(direction[X]) ? xSpan : ySpan;
29405
29504
  let divisions = 50;
29406
29505
  if (altKey || metaKey) {
29407
29506
  divisions = 200;
@@ -29512,8 +29611,10 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
29512
29611
  })?.[0] ?? "blue";
29513
29612
  const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""} ${focusClass(focusBehavior)}`;
29514
29613
  const [[x, y]] = useTransformVectorsToPixels(point);
29515
- const [xMin, xMax] = range[0];
29516
- const [yMin, yMax] = range[1];
29614
+
29615
+ // TODO(benchristel): destructure range in one line
29616
+ const [xMin, xMax] = range[X];
29617
+ const [yMin, yMax] = range[Y];
29517
29618
  const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29518
29619
  const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29519
29620
  const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
@@ -29570,7 +29671,7 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
29570
29671
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
29571
29672
  autoUpdate: true,
29572
29673
  backgroundColor: wbColorName,
29573
- content: `(${point[0]}, ${point[1]})`,
29674
+ content: `(${point[X]}, ${point[Y]})`,
29574
29675
  contentStyle: {
29575
29676
  color: "white"
29576
29677
  }
@@ -29755,16 +29856,7 @@ function trimRange(range, graphDimensionsInPixels) {
29755
29856
  const graphUnitsPerPixelY = size(yRange) / pixelsTall;
29756
29857
  const graphUnitsToTrimX = pixelsToTrim * graphUnitsPerPixelX;
29757
29858
  const graphUnitsToTrimY = pixelsToTrim * graphUnitsPerPixelY;
29758
- return [trim(xRange, graphUnitsToTrimX), trim(yRange, graphUnitsToTrimY)];
29759
- }
29760
- function trim(interval, amount) {
29761
- if (size(interval) < amount * 2) {
29762
- return [0, 0];
29763
- }
29764
- return [interval[0] + amount, interval[1] - amount];
29765
- }
29766
- function size(interval) {
29767
- return interval[1] - interval[0];
29859
+ return inset([graphUnitsToTrimX, graphUnitsToTrimY], range);
29768
29860
  }
29769
29861
 
29770
29862
  const SegmentGraph = props => {
@@ -29899,7 +29991,7 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
29899
29991
  in: "SourceGraphic"
29900
29992
  })));
29901
29993
 
29902
- const Angle = _ref => {
29994
+ const PolygonAngle = _ref => {
29903
29995
  let {
29904
29996
  centerPoint,
29905
29997
  endPoints,
@@ -29927,7 +30019,7 @@ const Angle = _ref => {
29927
30019
  // Used to calculate the inside of the angle and to create square
29928
30020
  const [x3, y3] = mafs.vec.add(centerPoint, mafs.vec.add(mafs.vec.sub([x1, y1], centerPoint), mafs.vec.sub([x2, y2], centerPoint)));
29929
30021
  if (!showAngles) {
29930
- return isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
30022
+ return isRightPolygonAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
29931
30023
  start: [x1, y1],
29932
30024
  vertex: [x2, y2],
29933
30025
  end: [x3, y3]
@@ -29964,35 +30056,138 @@ const Angle = _ref => {
29964
30056
  start: [x1, y1],
29965
30057
  vertex: [x2, y2],
29966
30058
  end: [x3, y3]
29967
- }) : /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
29968
- d: arc,
29969
- strokeWidth: 0.02,
29970
- fill: "none"
29971
- })), /*#__PURE__*/React__namespace.createElement(TextLabel, {
30059
+ }) : /*#__PURE__*/React__namespace.createElement(Arc, {
30060
+ arc: arc
30061
+ }), /*#__PURE__*/React__namespace.createElement(TextLabel, {
29972
30062
  x: x3,
29973
30063
  y: y3,
29974
30064
  attach: y3 - centerY > 0 ? "s" : "n",
29975
30065
  attachDistance: Math.abs(y3 - centerY) < 0.2 || mafs.vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
29976
30066
  }, angleLabel, "\xB0"));
29977
30067
  };
30068
+ const Angle = _ref2 => {
30069
+ let {
30070
+ vertex,
30071
+ coords,
30072
+ showAngles,
30073
+ allowReflexAngles,
30074
+ range
30075
+ } = _ref2;
30076
+ // Check if the points are clockwise or not, depending on whether we allow reflex angles
30077
+ const areClockwise = clockwise$1([...coords, vertex]);
30078
+ const shouldReverseCoords = areClockwise && !allowReflexAngles;
30079
+
30080
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
30081
+ const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
30082
+
30083
+ // Calculate the angles between the two points
30084
+ const startAngle = findAngle(clockwiseCoords[0], vertex);
30085
+ const endAngle = findAngle(clockwiseCoords[1], vertex);
30086
+ const angle = (startAngle + 360 - endAngle) % 360;
30087
+
30088
+ // Check if the angle is reflexive
30089
+ const isReflexive = angle > 180;
30090
+
30091
+ // Break out the necessary points for the arc calculation
30092
+ const [centerX, centerY] = vertex;
30093
+ const [point1, point2] = clockwiseCoords;
30094
+ const [startX, startY] = point1;
30095
+ const [endX, endY] = point2;
30096
+ const a = mafs.vec.dist(vertex, point1);
30097
+ const b = mafs.vec.dist(vertex, point2);
30098
+
30099
+ // Set the radius of the arc
30100
+ const radius = 2;
30101
+
30102
+ // Calculate the end points of the arc
30103
+ const y1 = centerY + (startY - centerY) / a * radius;
30104
+ const x2 = centerX + (endX - centerX) / b * radius;
30105
+ const x1 = centerX + (startX - centerX) / a * radius;
30106
+ const y2 = centerY + (endY - centerY) / b * radius;
30107
+
30108
+ // Used to calculate the inside of the angle and to create square
30109
+ const [x3, y3] = mafs.vec.add(vertex, mafs.vec.add(mafs.vec.sub([x1, y1], vertex), mafs.vec.sub([x2, y2], vertex)));
30110
+
30111
+ // Determine whether the arc should be drawn outside the angle
30112
+ const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
30113
+
30114
+ // Determine the flags for the arc
30115
+ // If the angle is outside OR reflexive, we want to draw a large arc
30116
+ // If the angle is outside AND reflexive, we want to sweep the arc
30117
+ const largeArcFlag = isOutside || isReflexive ? 1 : 0;
30118
+ const sweepFlag = isOutside && isReflexive ? 1 : 0;
30119
+
30120
+ // Create the SVG path for the arc
30121
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
30122
+
30123
+ // We only ever want to show whole angles
30124
+ const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
30125
+
30126
+ // Calculate the text position based on the angle and whether we allow reflex angles
30127
+ // Let's try the angle bisector method to find the midpoint of the arc
30128
+ const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
30129
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
30130
+ id: "background",
30131
+ x: "-5%",
30132
+ width: "110%",
30133
+ y: "0%",
30134
+ height: "100%"
30135
+ }, /*#__PURE__*/React__namespace.createElement("feFlood", {
30136
+ floodColor: "#FFF",
30137
+ floodOpacity: "0.5"
30138
+ }), /*#__PURE__*/React__namespace.createElement("feComposite", {
30139
+ operator: "over",
30140
+ in: "SourceGraphic"
30141
+ }))), isRightAngle(angle) ? /*#__PURE__*/React__namespace.createElement(RightAngleSquare, {
30142
+ start: [x1, y1],
30143
+ vertex: [x2, y2],
30144
+ end: [x3, y3],
30145
+ className: "arc-right-angle"
30146
+ }) : /*#__PURE__*/React__namespace.createElement(Arc, {
30147
+ arc: arc,
30148
+ className: "angle-arc"
30149
+ }), showAngles && /*#__PURE__*/React__namespace.createElement(TextLabel, {
30150
+ x: textX,
30151
+ y: textY,
30152
+ color: wonderBlocksTokens.color.blue
30153
+ }, angleLabel, "\xB0"));
30154
+ };
29978
30155
 
29979
30156
  /**
29980
30157
  * This is broken out into its own component so that it can be used for an early return
29981
30158
  * (see line 55 or https://github.com/Khan/perseus/blob/84bbd882b11d16871e1b813a0b901f3b903d5479/packages/perseus/src/widgets/interactive-graphs/graphs/components/angle.tsx#L53-L57)
29982
30159
  */
29983
- const RightAngleSquare = _ref2 => {
30160
+ const RightAngleSquare = _ref3 => {
29984
30161
  let {
29985
30162
  start: [x1, y1],
29986
30163
  vertex: [x2, y2],
29987
- end: [x3, y3]
29988
- } = _ref2;
30164
+ end: [x3, y3],
30165
+ className
30166
+ } = _ref3;
29989
30167
  return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
29990
30168
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
29991
30169
  strokeWidth: 0.02,
29992
- fill: "none"
30170
+ fill: "none",
30171
+ className: className
29993
30172
  }));
29994
30173
  };
29995
- const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
30174
+
30175
+ // We're conditionally adding the class name here so that we can style the arc differently
30176
+ // based on whether it's an angle or a polygon angle
30177
+ const Arc = _ref4 => {
30178
+ let {
30179
+ arc,
30180
+ className
30181
+ } = _ref4;
30182
+ return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
30183
+ d: arc,
30184
+ strokeWidth: 0.02,
30185
+ fill: "none",
30186
+ className: className
30187
+ }));
30188
+ };
30189
+ const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
30190
+ const isRightAngle = angle => Math.round(angle) === 90;
29996
30191
 
29997
30192
  /**
29998
30193
  * Determines if an angle is an inside (false) or outside (true) angle.
@@ -30014,9 +30209,9 @@ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
30014
30209
 
30015
30210
  // https://stackoverflow.com/a/24392281/7347484
30016
30211
  // The "intersects" function in geometry doesn't seem to work for this use case
30017
- const linesIntersect = (_ref3, _ref4) => {
30018
- let [[a, b], [c, d]] = _ref3;
30019
- let [[p, q], [r, s]] = _ref4;
30212
+ const linesIntersect = (_ref5, _ref6) => {
30213
+ let [[a, b], [c, d]] = _ref5;
30214
+ let [[p, q], [r, s]] = _ref6;
30020
30215
  const determinant = (c - a) * (s - q) - (r - p) * (d - b);
30021
30216
  if (determinant === 0) {
30022
30217
  return false;
@@ -30028,6 +30223,75 @@ const linesIntersect = (_ref3, _ref4) => {
30028
30223
  };
30029
30224
  const isEven = n => n % 2 === 0;
30030
30225
 
30226
+ // This function calculates the bisector point of an angle formed by two points
30227
+ // and a vertex. It is used to position the angle label so that it is always
30228
+ // outside the arc at the exact midpoint of the angle.
30229
+ // Reflex angles make use of polar coordinates to determine the correct direction
30230
+ function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
30231
+ const [originX, originY] = vertex;
30232
+ const [x1, y1] = point1;
30233
+ const [x2, y2] = point2;
30234
+
30235
+ // Convert the Cartesian Coordinates to vectors
30236
+ const vectorA = [x1 - originX, y1 - originY];
30237
+ const vectorB = [x2 - originX, y2 - originY];
30238
+
30239
+ // Calculate the average angle of the vectors
30240
+ // so that we can point the bisector in the correct direction using polar coordinates
30241
+ const angleA = Math.atan2(vectorA[1], vectorA[0]);
30242
+ const angleB = Math.atan2(vectorB[1], vectorB[0]);
30243
+
30244
+ // Calculate the average of these two angles
30245
+ let averageAngle = (angleA + angleB) / 2;
30246
+ const angleRadians = Math.abs(angleA - angleB);
30247
+
30248
+ // Adjust the average angle to point in the correct direction
30249
+ // and to account for reflex angles and a bug with 180 degrees
30250
+ // that occurs due to our angleOffsetDegrees option in the graph
30251
+ if (allowReflex) {
30252
+ // If the resulting angles are less than 180 degrees,
30253
+ // or if the user has flipped the points on the interactive element
30254
+ // ensure that we have the correct direction for the reflex angle
30255
+ if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
30256
+ averageAngle += Math.PI; // Add 180 degrees
30257
+ }
30258
+ } else {
30259
+ // If the resulting angles are greater than 180 degrees, adjust
30260
+ // to get the correct angle, as we already know that the angle is not reflexive
30261
+ // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
30262
+ if (angleRadians > Math.PI) {
30263
+ averageAngle -= Math.PI; // Subract 180 degrees
30264
+ }
30265
+ }
30266
+
30267
+ // Convert the average angle back to Cartesian coordinates
30268
+ const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
30269
+
30270
+ // Calculate the magnitude of the sum to normalize it
30271
+ const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
30272
+ const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
30273
+
30274
+ // Calculate the initial distance of the bisector direction from the origin
30275
+ const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
30276
+
30277
+ // Determine the minimum radius to ensure that the text is always outside the arc
30278
+ const requiredDistance = arcRadius * 1.75;
30279
+ let radius = requiredDistance / initialDistance;
30280
+
30281
+ // Apply radius only if the initial distance is less than required
30282
+ if (initialDistance >= requiredDistance) {
30283
+ radius = 1; // Use the unit vector as is
30284
+ }
30285
+
30286
+ // Scale the bisector direction by the radius
30287
+ const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
30288
+
30289
+ // Add the vertex to the bisector point to get the final position
30290
+ // to ensure that the angle label moves with the interactive element
30291
+ const scaledBisector = mafs.vec.add(bisectorPoint, vertex);
30292
+ return scaledBisector;
30293
+ }
30294
+
30031
30295
  const StyledMovablePoint = props => {
30032
30296
  const {
30033
30297
  snapStep
@@ -30108,7 +30372,7 @@ const PolygonGraph = props => {
30108
30372
  if (!pt1 || !pt2) {
30109
30373
  return null;
30110
30374
  }
30111
- return /*#__PURE__*/React__namespace.createElement(Angle, {
30375
+ return /*#__PURE__*/React__namespace.createElement(PolygonAngle, {
30112
30376
  key: "angle-" + i,
30113
30377
  centerPoint: point,
30114
30378
  endPoints: [pt1, pt2],
@@ -30212,6 +30476,13 @@ function getGradableGraph(state, initialGraph) {
30212
30476
  coords: state.coords
30213
30477
  };
30214
30478
  }
30479
+ if (state.type === "angle" && initialGraph.type === "angle") {
30480
+ return {
30481
+ ...initialGraph,
30482
+ coords: state.coords,
30483
+ allowReflexAngles: state.allowReflexAngles
30484
+ };
30485
+ }
30215
30486
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
30216
30487
  }
30217
30488
 
@@ -30274,16 +30545,16 @@ function MovableCircle(props) {
30274
30545
  className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
30275
30546
  }, /*#__PURE__*/React__namespace.createElement("ellipse", {
30276
30547
  className: "focus-ring",
30277
- cx: centerPx[0],
30278
- cy: centerPx[1],
30279
- rx: radiiPx[0] + 3,
30280
- ry: radiiPx[1] + 3
30548
+ cx: centerPx[X],
30549
+ cy: centerPx[Y],
30550
+ rx: radiiPx[X] + 3,
30551
+ ry: radiiPx[Y] + 3
30281
30552
  }), /*#__PURE__*/React__namespace.createElement("ellipse", {
30282
30553
  className: "circle",
30283
- cx: centerPx[0],
30284
- cy: centerPx[1],
30285
- rx: radiiPx[0],
30286
- ry: radiiPx[1]
30554
+ cx: centerPx[X],
30555
+ cy: centerPx[Y],
30556
+ rx: radiiPx[X],
30557
+ ry: radiiPx[Y]
30287
30558
  }), /*#__PURE__*/React__namespace.createElement(DragHandle, {
30288
30559
  center: center
30289
30560
  }));
@@ -30299,15 +30570,16 @@ function DragHandle(props) {
30299
30570
  const topLeft = mafs.vec.sub(centerPx, mafs.vec.scale(dragHandleDimensions, 0.5));
30300
30571
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
30301
30572
  className: "movable-circle-handle",
30302
- x: topLeft[0],
30303
- y: topLeft[1],
30304
- width: dragHandleDimensions[0],
30305
- height: dragHandleDimensions[1],
30573
+ x: topLeft[X],
30574
+ y: topLeft[Y],
30575
+ width: dragHandleDimensions[X],
30576
+ height: dragHandleDimensions[Y],
30306
30577
  rx: cornerRadius,
30307
30578
  ry: cornerRadius
30308
30579
  }), dragHandleDotPositions.map(offsetPx => {
30309
30580
  const [xPx, yPx] = mafs.vec.add(offsetPx, centerPx);
30310
30581
  return /*#__PURE__*/React__namespace.createElement("circle", {
30582
+ key: `circle-${xPx}-${yPx}`,
30311
30583
  className: "movable-circle-handle-dot",
30312
30584
  cx: xPx,
30313
30585
  cy: yPx
@@ -30436,15 +30708,15 @@ const getSinusoidCoefficients = coords => {
30436
30708
  const p2 = coords[1];
30437
30709
 
30438
30710
  // If the x-coordinates are the same, we are unable to calculate the coefficients
30439
- if (p2[0] === p1[0]) {
30711
+ if (p2[X] === p1[X]) {
30440
30712
  return;
30441
30713
  }
30442
30714
 
30443
30715
  // Resulting coefficients are canonical for this sine curve
30444
- const amplitude = p2[1] - p1[1];
30445
- const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
30446
- const phase = p1[0] * angularFrequency;
30447
- const verticalOffset = p1[1];
30716
+ const amplitude = p2[Y] - p1[Y];
30717
+ const angularFrequency = Math.PI / (2 * (p2[X] - p1[X]));
30718
+ const phase = p1[X] * angularFrequency;
30719
+ const verticalOffset = p1[Y];
30448
30720
  return {
30449
30721
  amplitude,
30450
30722
  angularFrequency,
@@ -30453,6 +30725,75 @@ const getSinusoidCoefficients = coords => {
30453
30725
  };
30454
30726
  };
30455
30727
 
30728
+ function AngleGraph(props) {
30729
+ const {
30730
+ dispatch,
30731
+ graphState
30732
+ } = props;
30733
+ const {
30734
+ graphDimensionsInPixels
30735
+ } = useGraphConfig();
30736
+ const {
30737
+ coords,
30738
+ showAngles,
30739
+ range,
30740
+ allowReflexAngles,
30741
+ angleOffsetDeg,
30742
+ snapDegrees
30743
+ } = graphState;
30744
+
30745
+ // Break the coords into the two end points and the center point
30746
+ const endPoints = [coords[0], coords[2]];
30747
+ const centerPoint = coords[1];
30748
+
30749
+ // Convert the vectors to pixels for rendering the svg lines
30750
+ const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
30751
+ const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
30752
+
30753
+ // Create the SVG lines for the angle
30754
+ const svgLines = linePixelCoords.map((_ref, i) => {
30755
+ let [startPtPx, endPtPx] = _ref;
30756
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
30757
+ const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
30758
+ return /*#__PURE__*/React__namespace.createElement("g", {
30759
+ key: `line-${i}`
30760
+ }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
30761
+ start: startPtPx,
30762
+ end: endPtPx,
30763
+ style: {
30764
+ stroke: "var(--movable-line-stroke-color)",
30765
+ strokeWidth: "var(--movable-line-stroke-weight)"
30766
+ }
30767
+ }), /*#__PURE__*/React__namespace.createElement(Vector, {
30768
+ tail: angleLines[i][1],
30769
+ tip: endExtend,
30770
+ color: "var(--movable-line-stroke-color)"
30771
+ }));
30772
+ });
30773
+
30774
+ // Create the angle indicator parameters
30775
+ const angleParams = {
30776
+ vertex: centerPoint,
30777
+ coords: endPoints,
30778
+ allowReflexAngles: allowReflexAngles || false,
30779
+ // Whether to allow reflex angles or not
30780
+ angleOffsetDeg: angleOffsetDeg || 0,
30781
+ // The angle offset from the x-axis
30782
+ snapDegrees: snapDegrees || 1,
30783
+ // The multiple of degrees to snap to
30784
+ range: range,
30785
+ showAngles: showAngles || false // Whether to show the angle or not
30786
+ };
30787
+
30788
+ // Render the lines, angle, and then movable points
30789
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
30790
+ key: "point-" + i,
30791
+ snapTo: "angles",
30792
+ point: point,
30793
+ onMove: destination => dispatch(movePoint(i, destination))
30794
+ })));
30795
+ }
30796
+
30456
30797
  // Universal padding for the axis ticks to ensure that the labels
30457
30798
  // have enough space to render without overlapping the tick lines
30458
30799
  const tickPadding = 5;
@@ -30462,10 +30803,12 @@ const AxisTickLabels = () => {
30462
30803
  tickStep,
30463
30804
  range
30464
30805
  } = graphConfig;
30465
- const [xMin, xMax] = range[0];
30466
- const [yMin, yMax] = range[1];
30467
- const yTickStep = tickStep[1];
30468
- const xTickStep = tickStep[0];
30806
+
30807
+ // TODO(benchristel): use destructuring here
30808
+ const [xMin, xMax] = range[X];
30809
+ const [yMin, yMax] = range[Y];
30810
+ const yTickStep = tickStep[Y];
30811
+ const xTickStep = tickStep[X];
30469
30812
  const yGridTicks = generateTickLocations$1(yTickStep, yMin, yMax);
30470
30813
  const xGridTicks = generateTickLocations$1(xTickStep, xMin, xMax);
30471
30814
  return /*#__PURE__*/React__namespace.createElement("div", {
@@ -30747,13 +31090,13 @@ const YGridTick = _ref => {
30747
31090
 
30748
31091
  // If the graph is zoomed in, we want to make sure the ticks are still visible
30749
31092
  // even if they are outside the graph's range.
30750
- if (graphInfo.range[0][0] > 0) {
31093
+ if (graphInfo.range[X][MIN] > 0) {
30751
31094
  // If the graph is on the positive side of the x-axis, lock the ticks to the left side of the graph
30752
- xPointOnAxis = graphInfo.range[0][0];
31095
+ xPointOnAxis = graphInfo.range[X][MIN];
30753
31096
  }
30754
- if (graphInfo.range[0][1] < 0) {
31097
+ if (graphInfo.range[X][MAX] < 0) {
30755
31098
  // If the graph is on the negative side of the x-axis, lock the ticks to the right side of the graph
30756
- xPointOnAxis = graphInfo.range[0][1];
31099
+ xPointOnAxis = graphInfo.range[X][MAX];
30757
31100
  }
30758
31101
  const pointOnAxis = [xPointOnAxis, y];
30759
31102
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
@@ -30788,13 +31131,13 @@ const XGridTick = _ref2 => {
30788
31131
  let yPointOnAxis = 0;
30789
31132
  // If the graph is zoomed in, we want to make sure the ticks are still visible
30790
31133
  // even if they are outside the graph's range.
30791
- if (graphInfo.range[1][0] > 0) {
31134
+ if (graphInfo.range[Y][MIN] > 0) {
30792
31135
  // If the graph is on the positive side of the y-axis, lock the ticks to the top of the graph
30793
- yPointOnAxis = graphInfo.range[1][0];
31136
+ yPointOnAxis = graphInfo.range[Y][MIN];
30794
31137
  }
30795
- if (graphInfo.range[1][1] < 0) {
31138
+ if (graphInfo.range[Y][MAX] < 0) {
30796
31139
  // If the graph is on the negative side of the x-axis, lock the ticks to the bottom of the graph
30797
- yPointOnAxis = graphInfo.range[1][1];
31140
+ yPointOnAxis = graphInfo.range[Y][MAX];
30798
31141
  }
30799
31142
  const pointOnAxis = [x, yPointOnAxis];
30800
31143
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
@@ -30849,10 +31192,12 @@ const AxisTicks = () => {
30849
31192
  width,
30850
31193
  height
30851
31194
  };
30852
- const [xMin, xMax] = range[0];
30853
- const [yMin, yMax] = range[1];
30854
- const yTickStep = tickStep[1];
30855
- const xTickStep = tickStep[0];
31195
+
31196
+ // TODO(benchristel): destructure these in one line
31197
+ const [xMin, xMax] = range[X];
31198
+ const [yMin, yMax] = range[Y];
31199
+ const yTickStep = tickStep[Y];
31200
+ const xTickStep = tickStep[X];
30856
31201
  const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
30857
31202
  const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
30858
31203
  return /*#__PURE__*/React__namespace.createElement("g", {
@@ -30891,8 +31236,8 @@ const axisOptions = (props, axisIndex) => {
30891
31236
  };
30892
31237
  const Grid = props => {
30893
31238
  return props.markings === "none" ? null : /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Coordinates.Cartesian, {
30894
- xAxis: axisOptions(props, 0),
30895
- yAxis: axisOptions(props, 1)
31239
+ xAxis: axisOptions(props, X),
31240
+ yAxis: axisOptions(props, Y)
30896
31241
  }),
30897
31242
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
30898
31243
  props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)));
@@ -31500,7 +31845,7 @@ const LegacyGrid = _ref => {
31500
31845
  height
31501
31846
  } = backgroundImage ?? {};
31502
31847
  if (url && typeof url === "string") {
31503
- const scale = box[0] / interactiveSizes$1.defaultBoxSize;
31848
+ const scale = box[X] / interactiveSizes$1.defaultBoxSize;
31504
31849
  return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
31505
31850
  style: {
31506
31851
  position: "absolute",
@@ -31568,9 +31913,9 @@ function Protractor() {
31568
31913
  const angle = calculateAngleInDegrees(rotationHandleOffset) - calculateAngleInDegrees(centerToRotationHandle);
31569
31914
  return /*#__PURE__*/React__namespace.createElement("g", {
31570
31915
  ref: draggableRef,
31571
- transform: `translate(${topLeftPx[0]}, ${topLeftPx[1]}), rotate(${angle})`,
31916
+ transform: `translate(${topLeftPx[X]}, ${topLeftPx[Y]}), rotate(${angle})`,
31572
31917
  style: {
31573
- transformOrigin: `${-centerToTopLeft[0]}px ${-centerToTopLeft[1]}px`
31918
+ transformOrigin: `${-centerToTopLeft[X]}px ${-centerToTopLeft[Y]}px`
31574
31919
  }
31575
31920
  }, /*#__PURE__*/React__namespace.createElement("image", {
31576
31921
  href: protractorImage
@@ -31744,7 +32089,19 @@ function initializeGraphState(params) {
31744
32089
  coords: getSinusoidCoords(graph, range, step)
31745
32090
  };
31746
32091
  case "angle":
31747
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
32092
+ return {
32093
+ ...shared,
32094
+ type: graph.type,
32095
+ showAngles: Boolean(graph.showAngles),
32096
+ coords: getAngleCoords({
32097
+ graph,
32098
+ range,
32099
+ step
32100
+ }),
32101
+ angleOffsetDeg: Number(graph.angleOffsetDeg),
32102
+ allowReflexAngles: Boolean(graph.allowReflexAngles),
32103
+ snapDegrees: Number(graph.snapDegrees)
32104
+ };
31748
32105
  default:
31749
32106
  throw new wonderStuffCore.UnreachableCaseError(graph);
31750
32107
  }
@@ -31876,6 +32233,43 @@ function getCircleCoords(graph) {
31876
32233
  radiusPoint: [2, 0]
31877
32234
  };
31878
32235
  }
32236
+ const getAngleCoords = params => {
32237
+ const {
32238
+ graph,
32239
+ range,
32240
+ step
32241
+ } = params;
32242
+ if (graph.coords) {
32243
+ return graph.coords;
32244
+ }
32245
+ const {
32246
+ snapDegrees,
32247
+ angleOffsetDeg
32248
+ } = graph;
32249
+ const snap = snapDegrees || 1;
32250
+ let angle = snap;
32251
+ while (angle < 20) {
32252
+ angle += snap;
32253
+ }
32254
+ angle = angle * Math.PI / 180;
32255
+ const offset = (angleOffsetDeg || 0) * Math.PI / 180;
32256
+ let defaultCoords = [[0.85, 0.5], [0.5, 0.5]];
32257
+ defaultCoords = normalizePoints(range, step, defaultCoords, true);
32258
+
32259
+ // @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.
32260
+ const radius = magnitude(vector(...defaultCoords));
32261
+
32262
+ // We're adding a placeholder for the third point to appease ts and so that we
32263
+ // can calculate it after we've adjusted the first point by the angleOffsetDeg
32264
+ const coords = [...defaultCoords, [0, 0]];
32265
+
32266
+ // Adjust the lower point by angleOffsetDeg degrees
32267
+ coords[0] = [coords[1][0] + radius * Math.cos(offset), coords[1][1] + radius * Math.sin(offset)];
32268
+ // Position the upper point angle radians from the
32269
+ // lower point
32270
+ coords[2] = [coords[1][0] + radius * Math.cos(angle + offset), coords[1][1] + radius * Math.sin(angle + offset)];
32271
+ return coords;
32272
+ };
31879
32273
 
31880
32274
  function interactiveGraphReducer(state, action) {
31881
32275
  switch (action.type) {
@@ -31939,6 +32333,7 @@ function doMoveControlPoint(state, action) {
31939
32333
  coords: newCoords
31940
32334
  };
31941
32335
  }
32336
+ case "angle":
31942
32337
  case "circle":
31943
32338
  throw new Error("FIXME implement circle reducer");
31944
32339
  case "point":
@@ -32043,12 +32438,32 @@ function doMoveAll(state, action) {
32043
32438
  }
32044
32439
  function doMovePoint(state, action) {
32045
32440
  switch (state.type) {
32441
+ case "angle":
32442
+ // If the index is 1, we are moving the vertex of the angle,
32443
+ // which will move the other two points as well
32444
+ if (action.index === 1) {
32445
+ const updatedCoords = boundAndSnapAngleVertex(state, action);
32446
+ return {
32447
+ ...state,
32448
+ hasBeenInteractedWith: true,
32449
+ coords: updatedCoords
32450
+ };
32451
+ }
32452
+ return {
32453
+ ...state,
32454
+ hasBeenInteractedWith: true,
32455
+ coords: setAtIndex({
32456
+ array: state.coords,
32457
+ index: action.index,
32458
+ newValue: boundAndSnapAngleEndPoints(action.destination, state, action.index)
32459
+ })
32460
+ };
32046
32461
  case "polygon":
32047
32462
  let newValue;
32048
32463
  if (state.snapTo === "sides") {
32049
32464
  newValue = boundAndSnapToSides(action.destination, state, action.index);
32050
32465
  } else if (state.snapTo === "angles") {
32051
- newValue = boundAndSnapToAngle(action.destination, state, action.index);
32466
+ newValue = boundAndSnapToPolygonAngle(action.destination, state, action.index);
32052
32467
  } else {
32053
32468
  newValue = boundAndSnapToGrid(action.destination, state);
32054
32469
  }
@@ -32089,7 +32504,7 @@ function doMovePoint(state, action) {
32089
32504
  // vertical line. If they are, then we don't want to move the point
32090
32505
  const newCoords = [...state.coords];
32091
32506
  newCoords[action.index] = boundDestination;
32092
- if (newCoords[0][0] === newCoords[1][0]) {
32507
+ if (newCoords[0][X] === newCoords[1][X]) {
32093
32508
  return state;
32094
32509
  }
32095
32510
  return {
@@ -32149,13 +32564,13 @@ function doMoveCenter(state, action) {
32149
32564
  // if it otherwise would be off the chart
32150
32565
  // ex: if the handle is on the right and we move the center
32151
32566
  // to the rightmost position, move the handle to the left
32152
- const [xMin, xMax] = state.range[0];
32567
+ const [xMin, xMax] = state.range[X];
32153
32568
  const [radX] = newRadiusPoint;
32154
32569
  if (radX < xMin || radX > xMax) {
32155
- const xJumpDist = (radX - constrainedCenter[0]) * 2;
32570
+ const xJumpDist = (radX - constrainedCenter[X]) * 2;
32156
32571
  const possibleNewX = radX - xJumpDist;
32157
32572
  if (possibleNewX >= xMin && possibleNewX <= xMax) {
32158
- newRadiusPoint[0] = possibleNewX;
32573
+ newRadiusPoint[X] = possibleNewX;
32159
32574
  }
32160
32575
  }
32161
32576
  return {
@@ -32173,11 +32588,11 @@ function doMoveRadiusPoint(state, action) {
32173
32588
  switch (state.type) {
32174
32589
  case "circle":
32175
32590
  {
32176
- const [xMin, xMax] = state.range[0];
32591
+ const [xMin, xMax] = state.range[X];
32177
32592
  const nextRadiusPoint = [
32178
32593
  // Constrain to graph range
32179
32594
  // The +0 is to convert -0 to +0
32180
- Math.min(Math.max(xMin, action.destination[0] + 0), xMax), state.center[1]];
32595
+ clamp(action.destination[X] + 0, xMin, xMax), state.center[1]];
32181
32596
  if (___default["default"].isEqual(nextRadiusPoint, state.center)) {
32182
32597
  return state;
32183
32598
  }
@@ -32215,10 +32630,10 @@ function doChangeRange(state, action) {
32215
32630
  }
32216
32631
  const getDeltaVertex = (maxMoves, minMoves, delta) => {
32217
32632
  const [deltaX, deltaY] = delta;
32218
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
32219
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
32220
- const minXMove = Math.max(...minMoves.map(move => move[0]));
32221
- const minYMove = Math.max(...minMoves.map(move => move[1]));
32633
+ const maxXMove = Math.min(...maxMoves.map(move => move[X]));
32634
+ const maxYMove = Math.min(...maxMoves.map(move => move[Y]));
32635
+ const minXMove = Math.max(...minMoves.map(move => move[X]));
32636
+ const minYMove = Math.max(...minMoves.map(move => move[Y]));
32222
32637
  const dx = clamp(deltaX, minXMove, maxXMove);
32223
32638
  const dy = clamp(deltaY, minYMove, maxYMove);
32224
32639
  return [dx, dy];
@@ -32252,11 +32667,130 @@ function boundAndSnapToGrid(point, _ref) {
32252
32667
  point
32253
32668
  }));
32254
32669
  }
32255
- function boundAndSnapToAngle(destinationPoint, _ref2, index) {
32670
+ function boundAndSnapAngleVertex(_ref2, _ref3) {
32256
32671
  let {
32257
32672
  range,
32258
- coords
32673
+ coords,
32674
+ snapStep
32259
32675
  } = _ref2;
32676
+ let {
32677
+ destination
32678
+ } = _ref3;
32679
+ // Needed to prevent updating the original coords before the checks for
32680
+ // degenerate triangles and overlapping sides
32681
+ const coordsCopy = [...coords];
32682
+
32683
+ // Get the current and upcoming positions of the vertex
32684
+ const startingVertex = coordsCopy[1];
32685
+ const newVertex = bound$1({
32686
+ snapStep,
32687
+ range,
32688
+ point: snap(snapStep, destination)
32689
+ });
32690
+
32691
+ // Get the vector from the starting vertex to the new vertex
32692
+ const delta = mafs.vec.add(newVertex, reverseVector(startingVertex));
32693
+
32694
+ // Apply the delta to each of the other two points so that the angle is maintained
32695
+ let valid = true;
32696
+ const newPoints = {};
32697
+ for (const i of [0, 2]) {
32698
+ const oldPoint = coordsCopy[i];
32699
+ let newPoint = mafs.vec.add(oldPoint, delta);
32700
+ let angle = findAngle(newVertex, newPoint);
32701
+ angle *= Math.PI / 180;
32702
+ newPoint = constrainToBoundsOnAngle(newPoint, angle, range, snapStep);
32703
+ newPoints[i] = newPoint;
32704
+
32705
+ // Check if the new point is too close to the vertex
32706
+ if (tooClose(newVertex, newPoint)) {
32707
+ valid = false;
32708
+ }
32709
+ }
32710
+
32711
+ // Update the vertex after snapping to the snapStep
32712
+ newPoints[1] = newVertex;
32713
+ // Only move points if all new positions are valid
32714
+ if (valid) {
32715
+ Object.entries(newPoints).forEach(_ref4 => {
32716
+ let [i, newPoint] = _ref4;
32717
+ coordsCopy[i] = newPoint;
32718
+ });
32719
+ }
32720
+ return coordsCopy;
32721
+ }
32722
+
32723
+ // This function is used to ensure that the vertex of
32724
+ // an angle is not too close to the other points
32725
+ function tooClose(point1, point2, range) {
32726
+ const safeDistance = 2;
32727
+ const distance = mafs.vec.dist(point1, point2);
32728
+ return distance < safeDistance;
32729
+ }
32730
+ function constrainToBoundsOnAngle(point, angle, range, snapStep) {
32731
+ // We're subtracting the snapStep from the lower bound and adding it to the upper bound
32732
+ // to ensure that the point is within the bounds of the graph even after snapping to the nearest degree
32733
+ const lower = [range[0][0] + snapStep[0], range[1][0] + snapStep[0]];
32734
+ const upper = [range[0][1] - snapStep[1], range[1][1] - snapStep[1]];
32735
+ let result = point;
32736
+ if (result[0] < lower[0]) {
32737
+ result = [lower[0], result[1] + (lower[0] - result[0]) * Math.tan(angle)];
32738
+ } else if (result[0] > upper[0]) {
32739
+ result = [upper[0], result[1] - (result[0] - upper[0]) * Math.tan(angle)];
32740
+ }
32741
+ if (result[1] < lower[1]) {
32742
+ result = [result[0] + (lower[1] - result[1]) / Math.tan(angle), lower[1]];
32743
+ } else if (result[1] > upper[1]) {
32744
+ result = [result[0] - (result[1] - upper[1]) / Math.tan(angle), upper[1]];
32745
+ }
32746
+ return result;
32747
+ }
32748
+ function boundAndSnapAngleEndPoints(destinationPoint, _ref5, index) {
32749
+ let {
32750
+ range,
32751
+ coords,
32752
+ snapDegrees,
32753
+ angleOffsetDeg,
32754
+ snapStep
32755
+ } = _ref5;
32756
+ const snap = snapDegrees || 1;
32757
+ const offsetDegrees = angleOffsetDeg || 0;
32758
+
32759
+ // Needed to prevent updating the original coords before the checks for
32760
+ // degenerate triangles and overlapping sides
32761
+ const coordsCopy = [...coords];
32762
+
32763
+ // We want to subtract or add the snapStep to the lower and upper bounds
32764
+ // respectively to ensure that the point is within the bounds of the graph
32765
+ // even after snapping to the nearest degree
32766
+ const angleRange = [[range[0][0] + snapStep[0], range[0][1] - snapStep[0]], [range[1][0] + snapStep[1], range[1][1] - snapStep[1]]];
32767
+
32768
+ // Takes the destination point and makes sure it is within the bounds of the graph
32769
+ // SnapStep is [0, 0] because we don't want to snap these points to the grid at all
32770
+ const boundPoint = bound$1({
32771
+ snapStep: [0, 0],
32772
+ range: angleRange,
32773
+ point: destinationPoint
32774
+ });
32775
+ coordsCopy[index] = boundPoint;
32776
+
32777
+ // Get the vertex of the angle
32778
+ const vertex = coords[1];
32779
+
32780
+ // Gets the angle between the coords and the vertex
32781
+ let angle = findAngle(coordsCopy[index], vertex);
32782
+
32783
+ // Snap the angle to the nearest multiple of snapDegrees (if provided)
32784
+ angle = Math.round((angle - offsetDegrees) / snap) * snap + offsetDegrees;
32785
+ const distance = mafs.vec.dist(coordsCopy[index], vertex);
32786
+ const snappedValue = mafs.vec.add(vertex, polar(distance, angle));
32787
+ return snappedValue;
32788
+ }
32789
+ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
32790
+ let {
32791
+ range,
32792
+ coords
32793
+ } = _ref6;
32260
32794
  const startingPoint = coords[index];
32261
32795
 
32262
32796
  // Needed to prevent updating the original coords before the checks for
@@ -32284,7 +32818,7 @@ function boundAndSnapToAngle(destinationPoint, _ref2, index) {
32284
32818
  angles[rel(j)] = Math.round(angles[rel(j)]);
32285
32819
  });
32286
32820
  const getAngle = function (a, vertex, b) {
32287
- const angle = GraphUtils.findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
32821
+ const angle = findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
32288
32822
  return (angle + 360) % 360;
32289
32823
  };
32290
32824
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -32312,7 +32846,7 @@ function boundAndSnapToAngle(destinationPoint, _ref2, index) {
32312
32846
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
32313
32847
 
32314
32848
  // Angle at the second vertex of the polygon
32315
- const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32849
+ const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32316
32850
 
32317
32851
  // Uses the length of the side of the polygon (radial coordinate)
32318
32852
  // and the angle between the first and second sides of the
@@ -32320,11 +32854,11 @@ function boundAndSnapToAngle(destinationPoint, _ref2, index) {
32320
32854
  const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
32321
32855
  return kmath.vector.add(coordsCopy[rel(-1)], offset);
32322
32856
  }
32323
- function boundAndSnapToSides(destinationPoint, _ref3, index) {
32857
+ function boundAndSnapToSides(destinationPoint, _ref7, index) {
32324
32858
  let {
32325
32859
  range,
32326
32860
  coords
32327
- } = _ref3;
32861
+ } = _ref7;
32328
32862
  const startingPoint = coords[index];
32329
32863
 
32330
32864
  // Needed to prevent updating the original coords before the checks for
@@ -32363,7 +32897,7 @@ function boundAndSnapToSides(destinationPoint, _ref3, index) {
32363
32897
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
32364
32898
 
32365
32899
  // Angle at the second vertex of the polygon
32366
- const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32900
+ const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32367
32901
 
32368
32902
  // Returns true if the points form a counter-clockwise turn;
32369
32903
  // a.k.a if the point is on the left or right of the polygon.
@@ -32381,12 +32915,12 @@ function boundAndSnapToSides(destinationPoint, _ref3, index) {
32381
32915
  }
32382
32916
 
32383
32917
  // Returns the vector from the given point to the top-right corner of the graph when snapped to the grid
32384
- function maxMove(_ref4) {
32918
+ function maxMove(_ref8) {
32385
32919
  let {
32386
32920
  snapStep,
32387
32921
  range,
32388
32922
  point
32389
- } = _ref4;
32923
+ } = _ref8;
32390
32924
  const topRight = bound$1({
32391
32925
  snapStep,
32392
32926
  range,
@@ -32396,12 +32930,12 @@ function maxMove(_ref4) {
32396
32930
  }
32397
32931
 
32398
32932
  // Returns the vector from the given point to the bottom-left corner of the graph when snapped to the grid
32399
- function minMove(_ref5) {
32933
+ function minMove(_ref9) {
32400
32934
  let {
32401
32935
  snapStep,
32402
32936
  range,
32403
32937
  point
32404
- } = _ref5;
32938
+ } = _ref9;
32405
32939
  const bottomLeft = bound$1({
32406
32940
  snapStep,
32407
32941
  range,
@@ -32443,6 +32977,11 @@ const renderGraph = props => {
32443
32977
  type
32444
32978
  } = state;
32445
32979
  switch (type) {
32980
+ case "angle":
32981
+ return /*#__PURE__*/React__namespace.createElement(AngleGraph, {
32982
+ graphState: state,
32983
+ dispatch: dispatch
32984
+ });
32446
32985
  case "segment":
32447
32986
  return /*#__PURE__*/React__namespace.createElement(SegmentGraph, {
32448
32987
  graphState: state,
@@ -32606,8 +33145,8 @@ const MafsGraph = props => {
32606
33145
  }, props.markings === "graph" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null), /*#__PURE__*/React__namespace.createElement(AxisTickLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
32607
33146
  preserveAspectRatio: false,
32608
33147
  viewBox: {
32609
- x: state.range[0],
32610
- y: state.range[1],
33148
+ x: state.range[X],
33149
+ y: state.range[Y],
32611
33150
  padding: 0
32612
33151
  },
32613
33152
  pan: false,
@@ -33491,7 +34030,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
33491
34030
  // Solve for side by using the law of sines
33492
34031
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
33493
34032
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
33494
- const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
34033
+ const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
33495
34034
  return this.graphie?.addPoints(coords[rel(-1)], offset);
33496
34035
  }
33497
34036
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -33515,7 +34054,7 @@ class LegacyInteractiveGraph extends React__namespace.Component {
33515
34054
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
33516
34055
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
33517
34056
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
33518
- const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
34057
+ const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
33519
34058
  return this.graphie?.addPoints(coords[rel(-1)], offset);
33520
34059
  }
33521
34060
  // Snap to grid (already done)
@@ -34122,11 +34661,9 @@ class InteractiveGraph extends React__namespace.Component {
34122
34661
  const radius = magnitude(vector(...coords));
34123
34662
 
34124
34663
  // Adjust the lower point by angleOffsetDeg degrees
34125
- // @ts-expect-error - TS2542 - Index signature in type 'readonly Coord[]' only permits reading.
34126
34664
  coords[0] = [coords[1][0] + radius * Math.cos(offset), coords[1][1] + radius * Math.sin(offset)];
34127
34665
  // Position the upper point angle radians from the
34128
34666
  // lower point
34129
- // @ts-expect-error - TS2542 - Index signature in type 'readonly Coord[]' only permits reading.
34130
34667
  coords[2] = [coords[1][0] + radius * Math.cos(angle + offset), coords[1][1] + radius * Math.sin(angle + offset)];
34131
34668
  return coords;
34132
34669
  }
@@ -44746,7 +45283,7 @@ const init = function (options) {
44746
45283
 
44747
45284
  // This file is processed by a Rollup plugin (replace) to inject the production
44748
45285
  const libName = "@khanacademy/perseus";
44749
- const libVersion = "24.1.0";
45286
+ const libVersion = "24.3.0";
44750
45287
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
44751
45288
 
44752
45289
  const apiVersion = {