@khanacademy/perseus 24.2.0 → 25.0.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 (40) hide show
  1. package/dist/es/index.css +7 -0
  2. package/dist/es/index.js +845 -377
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.css +7 -0
  5. package/dist/index.d.ts +3 -4
  6. package/dist/index.js +927 -449
  7. package/dist/index.js.map +1 -1
  8. package/dist/logging/log.d.ts +1 -41
  9. package/dist/perseus-api.d.ts +1 -0
  10. package/dist/perseus-types.d.ts +17 -2
  11. package/dist/server-item-renderer.d.ts +2 -1
  12. package/dist/types.d.ts +1 -1
  13. package/dist/widgets/expression.d.ts +8 -2
  14. package/dist/widgets/interactive-graph.d.ts +1 -1
  15. package/dist/widgets/interactive-graphs/graphs/angle.d.ts +5 -0
  16. package/dist/widgets/interactive-graphs/graphs/components/{angle.d.ts → angle-indicators.d.ts} +12 -2
  17. package/dist/widgets/interactive-graphs/graphs/components/movable-line.d.ts +11 -0
  18. package/dist/widgets/interactive-graphs/graphs/components/text-label.d.ts +1 -1
  19. package/dist/widgets/interactive-graphs/graphs/index.d.ts +1 -0
  20. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +0 -3
  21. package/dist/widgets/interactive-graphs/index.d.ts +1 -1
  22. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +3 -1
  23. package/dist/widgets/interactive-graphs/locked-figures/locked-function.d.ts +4 -0
  24. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +0 -19
  25. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +1 -1
  26. package/dist/widgets/interactive-graphs/math/angles.d.ts +5 -0
  27. package/dist/widgets/interactive-graphs/math/box.d.ts +4 -0
  28. package/dist/widgets/interactive-graphs/math/clamp.d.ts +1 -0
  29. package/dist/widgets/interactive-graphs/math/coordinates.d.ts +2 -0
  30. package/dist/widgets/interactive-graphs/math/geometry.d.ts +3 -0
  31. package/dist/widgets/interactive-graphs/math/index.d.ts +8 -0
  32. package/dist/widgets/interactive-graphs/math/interpolation.d.ts +1 -0
  33. package/dist/widgets/interactive-graphs/math/interval.d.ts +5 -0
  34. package/dist/widgets/interactive-graphs/math/snap.d.ts +2 -0
  35. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +21 -0
  36. package/dist/widgets/interactive-graphs/types.d.ts +9 -1
  37. package/dist/widgets/interactive-graphs/utils.d.ts +0 -2
  38. package/dist/widgets/label-image.d.ts +2 -0
  39. package/package.json +8 -8
  40. package/dist/perseus-error.d.ts +0 -11
package/dist/es/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
+ import { PerseusError, Errors, addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
3
4
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
5
  import { linterContextDefault, Rule, linterContextProps } from '@khanacademy/perseus-linter';
5
6
  import { View, RenderStateRoot, UniqueIDProvider, useLatestRef, useUniqueIdWithMock, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
@@ -29,7 +30,7 @@ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-block
29
30
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
30
31
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
31
32
  import debounce$2 from 'lodash.debounce';
32
- import { vec, Ellipse, Line as Line$4, Point as Point$2, Polygon, useTransformContext, Text as Text$1, Plot as Plot$3, Coordinates, Mafs } from 'mafs';
33
+ import { vec, Ellipse, Plot as Plot$3, Line as Line$4, Point as Point$2, Polygon, useTransformContext, Text as Text$1, Coordinates, Mafs } from 'mafs';
33
34
  import { useDrag } from '@use-gesture/react';
34
35
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
35
36
  import { Body, LabelSmall, LabelMedium } from '@khanacademy/wonder-blocks-typography';
@@ -39,7 +40,6 @@ import Switch from '@khanacademy/wonder-blocks-switch';
39
40
  import Pill from '@khanacademy/wonder-blocks-pill';
40
41
  import { Popper } from 'react-popper';
41
42
  import Link from '@khanacademy/wonder-blocks-link';
42
- import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
43
43
 
44
44
  function _extends() {
45
45
  _extends = Object.assign ? Object.assign.bind() : function (target) {
@@ -699,52 +699,9 @@ var dependencies = /*#__PURE__*/Object.freeze({
699
699
  // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
700
700
  // getDependencies() internally... simplify usage to just using a "singleton"
701
701
  // which get's the injected logger.
702
- /**
703
- * @typedef {Object} Errors utility for referencing the Perseus error taxonomy.
704
- */
705
- const Errors = Object.freeze({
706
- /**
707
- * @property {ErrorKind} Unknown The kind of error is not known.
708
- */
709
- Unknown: "Unknown",
710
- /**
711
- * @property {ErrorKind} Internal The error is internal to the executing code.
712
- */
713
- Internal: "Internal",
714
- /**
715
- * @property {ErrorKind} InvalidInput There was a problem with the provided
716
- * input, such as the wrong format or a null value.
717
- */
718
- InvalidInput: "InvalidInput",
719
- /**
720
- * @property {ErrorKind} NotAllowed There was a problem due to the state of
721
- * the system not matching the requested operation or input. For example,
722
- * trying to create a username that is valid, but is already taken by
723
- * another user. Use {@link InvalidInput} instead when the input isn't
724
- * valid regardless of the state of the system. Use {@link NotFound} when
725
- * the failure is due to not being able to find a resource.
726
- */
727
- NotAllowed: "NotAllowed",
728
- /**
729
- * @property {ErrorKind} TransientService There was a problem when making a
730
- * request to a service.
731
- */
732
- TransientService: "TransientService",
733
- /**
734
- * @property {ErrorKind} Service There was a non-transient problem when
735
- * making a request to service.
736
- */
737
- Service: "Service"
738
- });
739
-
740
- /**
741
- * @type {ErrorKind} The kind of error being reported
742
- */
743
-
744
702
  /**
745
703
  * Provides logging infrastructure for Perseus
746
704
  */
747
-
748
705
  const Log = {
749
706
  // Logs a message
750
707
  log: (message, extra) => {
@@ -756,16 +713,6 @@ const Log = {
756
713
  }
757
714
  };
758
715
 
759
- class PerseusError extends Error {
760
- constructor(message, kind, options) {
761
- super(message);
762
- this.kind = void 0;
763
- this.metadata = void 0;
764
- this.kind = kind;
765
- this.metadata = options == null ? void 0 : options.metadata;
766
- }
767
- }
768
-
769
716
  /**
770
717
  * These are things that widgets should exclude when serializing themselves.
771
718
  *
@@ -8969,7 +8916,7 @@ var Tex$1 = {
8969
8916
  const {
8970
8917
  processMath: processMath$1
8971
8918
  } = Tex$1;
8972
- function polar(r, th) {
8919
+ function polar$1(r, th) {
8973
8920
  if (typeof r === "number") {
8974
8921
  r = [r, r];
8975
8922
  }
@@ -9458,8 +9405,8 @@ class Graphie$1 {
9458
9405
  endAngle = (endAngle % 360 + 360) % 360;
9459
9406
  const cent = this.scalePoint(center);
9460
9407
  const radii = this.scaleVector(radius);
9461
- const startVector = polar(radius, startAngle);
9462
- const endVector = polar(radius, endAngle);
9408
+ const startVector = polar$1(radius, startAngle);
9409
+ const endVector = polar$1(radius, endAngle);
9463
9410
 
9464
9411
  // We round the coordinates to make testing easier, because trig
9465
9412
  // operations return unround numbers.
@@ -13732,7 +13679,7 @@ _.extend(MovableAngle.prototype, {
13732
13679
  let angle = GraphUtils.findAngle(newPoint, vertex);
13733
13680
  angle = Math.round((angle - snapOffset) / snap) * snap + snapOffset;
13734
13681
  const distance = GraphUtils.getDistance(newPoint, vertex);
13735
- return vector$1.add(vertex, polar(distance, angle));
13682
+ return vector$1.add(vertex, polar$1(distance, angle));
13736
13683
  }
13737
13684
  return true;
13738
13685
  };
@@ -16648,7 +16595,7 @@ const Path = GraphieClasses.createClass({
16648
16595
  this.path.toFront();
16649
16596
  }
16650
16597
  });
16651
- const Arc = GraphieClasses.createSimpleClass((graphie, props) => {
16598
+ const Arc$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16652
16599
  let center = props.center;
16653
16600
  let radius = props.radius;
16654
16601
  if (props.unscaled) {
@@ -16664,7 +16611,7 @@ const Rect$1 = GraphieClasses.createSimpleClass((graphie, props) => {
16664
16611
  return graphie.rect(props.x, props.y, props.width, props.height, props.style);
16665
16612
  });
16666
16613
  var Movables = {
16667
- Arc: Arc,
16614
+ Arc: Arc$1,
16668
16615
  Circle: Circle,
16669
16616
  Label: Label$1,
16670
16617
  Line: Line$3,
@@ -28363,6 +28310,125 @@ const useTransformDimensionsToPixels = (...dimens) => {
28363
28310
  return dimensionsToPixels(dimens, graphState);
28364
28311
  };
28365
28312
 
28313
+ // Restricts `value` to be between `min` and `max`.
28314
+ function clamp(value, min, max) {
28315
+ if (value < min) {
28316
+ return min;
28317
+ }
28318
+ if (value > max) {
28319
+ return max;
28320
+ }
28321
+ return value;
28322
+ }
28323
+
28324
+ // Rounds each component of `point` to the nearest multiple of the
28325
+ // corresponding component of `snapStep`. E.g. if the snap step is [2, 3],
28326
+ // the x-coord of the point will be rounded to the nearest multiple of 2, and
28327
+ // the y-coord of the point will be rounded to the nearest multiple of 3.
28328
+ function snap(snapStep, point) {
28329
+ const [requestedX, requestedY] = point;
28330
+ const [snapX, snapY] = snapStep;
28331
+ return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
28332
+ }
28333
+
28334
+ // Use X and Y to index into arrays that represent coordinate pairs.
28335
+ // e.g. point[X]
28336
+ const X = 0;
28337
+ const Y = 1;
28338
+
28339
+ // Use MIN and MAX to index into arrays that represent intervals.
28340
+ // e.g. range[MAX]
28341
+ const MIN = 0;
28342
+ const MAX = 1;
28343
+ function size([min, max]) {
28344
+ return max - min;
28345
+ }
28346
+
28347
+ // Removes an equal amount from each end of the interval.
28348
+ //
28349
+ // If the given amount cannot be removed (because the interval is too small),
28350
+ // half the interval's size is removed from each end instead, so the interval
28351
+ // ends up with size = 0, and both MAX and MIN equal to its former midpoint.
28352
+ //
28353
+ // If `amount` is negative, the interval is expanded instead.
28354
+ function trim(amount, interval) {
28355
+ if (amount * 2 > size(interval)) {
28356
+ const middle = average(...interval);
28357
+ return [middle, middle];
28358
+ }
28359
+ return [interval[MIN] + amount, interval[MAX] - amount];
28360
+ }
28361
+ function average(a, b) {
28362
+ return (a + b) / 2;
28363
+ }
28364
+
28365
+ // Restricts the `point` to be within `box`, by clamping each coordinate to the
28366
+ // corresponding interval.
28367
+ function clampToBox(box, point) {
28368
+ return [clamp(point[X], ...box[X]), clamp(point[Y], ...box[Y])];
28369
+ }
28370
+
28371
+ // Reduces the size of `box`, trimming each corner by the given positive
28372
+ // `amount`. If a component of `amount` is negative, the box is expanded in
28373
+ // that dimension instead.
28374
+ function inset(amount, box) {
28375
+ return [trim(amount[X], box[X]), trim(amount[Y], box[Y])];
28376
+ }
28377
+
28378
+ // [L]inear Int[erp]olation: gets the weighted average of two values `a` and
28379
+ // `b`, with the given `fraction` specifying how much weight `a` and `b` get:
28380
+ // - if `fraction` is 0, `lerp` returns `a`.
28381
+ // - if `fraction` is 0.5, `lerp` returns the average of `a` and `b`.
28382
+ // - if `fraction` is 1, `lerp` returns `b`.
28383
+ function lerp(a, b, fraction) {
28384
+ return (b - a) * clamp(fraction, 0, 1) + a;
28385
+ }
28386
+
28387
+ // Determines whether two line segments have exactly one intersection point.
28388
+ // The segments are treated as "open" - that is, not inclusive of their
28389
+ // endpoints.
28390
+ // Algorithm from https://stackoverflow.com/a/24392281/7347484
28391
+ const segmentsIntersect = ([[a, b], [c, d]], [[p, q], [r, s]]) => {
28392
+ const determinant = (c - a) * (s - q) - (r - p) * (d - b);
28393
+ if (determinant === 0) {
28394
+ return false;
28395
+ } else {
28396
+ const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
28397
+ const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
28398
+ return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
28399
+ }
28400
+ };
28401
+
28402
+ // This file contains helper functions for working with angles.
28403
+
28404
+ function convertDegreesToRadians(degrees) {
28405
+ return degrees / 180 * Math.PI;
28406
+ }
28407
+
28408
+ // Returns a value between -180 and 180, inclusive. The angle is measured
28409
+ // between the positive x-axis and the given vector.
28410
+ function calculateAngleInDegrees([x, y]) {
28411
+ return Math.atan2(y, x) * 180 / Math.PI;
28412
+ }
28413
+ function polar(r, th) {
28414
+ if (typeof r === "number") {
28415
+ r = [r, r];
28416
+ }
28417
+ th = th * Math.PI / 180;
28418
+ return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
28419
+ }
28420
+ const findAngle = (point1, point2, vertex) => {
28421
+ if (vertex === undefined) {
28422
+ const x = point1[0] - point2[0];
28423
+ const y = point1[1] - point2[1];
28424
+ if (!x && !y) {
28425
+ return 0;
28426
+ }
28427
+ return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
28428
+ }
28429
+ return findAngle(point1, vertex) - findAngle(point2, vertex);
28430
+ };
28431
+
28366
28432
  function AxisLabels() {
28367
28433
  const {
28368
28434
  range,
@@ -28370,8 +28436,8 @@ function AxisLabels() {
28370
28436
  width,
28371
28437
  height
28372
28438
  } = useGraphConfig();
28373
- const yAxisLabelLocation = [0, range[1][1]];
28374
- const xAxisLabelLocation = [range[0][1], 0];
28439
+ const yAxisLabelLocation = [0, range[Y][MAX]];
28440
+ const xAxisLabelLocation = [range[X][MAX], 0];
28375
28441
  const [xAxisLabelText, yAxisLabelText] = labels;
28376
28442
  const graphInfo = {
28377
28443
  range,
@@ -28542,6 +28608,32 @@ const LockedEllipse = props => {
28542
28608
  });
28543
28609
  };
28544
28610
 
28611
+ const LockedFunction = props => {
28612
+ const {
28613
+ color,
28614
+ strokeStyle,
28615
+ directionalAxis,
28616
+ domain
28617
+ } = props;
28618
+ const plotProps = {
28619
+ color: lockedFigureColors[color],
28620
+ style: strokeStyle,
28621
+ domain
28622
+ };
28623
+ const equation = props.equationParsed || KAS.parse(props.equation).expr;
28624
+ return /*#__PURE__*/React.createElement("g", {
28625
+ className: "locked-function"
28626
+ }, directionalAxis === "x" && /*#__PURE__*/React.createElement(Plot$3.OfX, _extends({
28627
+ y: x => equation.eval({
28628
+ x
28629
+ })
28630
+ }, plotProps)), directionalAxis === "y" && /*#__PURE__*/React.createElement(Plot$3.OfY, _extends({
28631
+ x: y => equation.eval({
28632
+ y
28633
+ })
28634
+ }, plotProps)));
28635
+ };
28636
+
28545
28637
  function pathBuilder() {
28546
28638
  return new PathBuilder();
28547
28639
  }
@@ -28640,7 +28732,7 @@ function Arrowhead(props) {
28640
28732
  const [point] = useTransformVectorsToPixels(props.tip);
28641
28733
  return /*#__PURE__*/React.createElement("g", {
28642
28734
  className: "interactive-graph-arrowhead",
28643
- transform: `translate(${point[0]} ${point[1]}) rotate(${props.angle})`
28735
+ transform: `translate(${point[X]} ${point[Y]}) rotate(${props.angle})`
28644
28736
  }, /*#__PURE__*/React.createElement("g", {
28645
28737
  transform: "translate(-1.5)"
28646
28738
  }, /*#__PURE__*/React.createElement("path", {
@@ -28655,51 +28747,6 @@ function Arrowhead(props) {
28655
28747
  })));
28656
28748
  }
28657
28749
 
28658
- function calculateAngleInDegrees([x, y]) {
28659
- return Math.atan2(y, x) * 180 / Math.PI;
28660
- }
28661
-
28662
- /**
28663
- * Given a ray and a rectangular box, find the point where the ray intersects
28664
- * the edge of the box. Assumes the `initialPoint` is inside the box.
28665
- * @param initialPoint - The starting point of the ray.
28666
- * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
28667
- * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
28668
- */
28669
- const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
28670
- const [[xMin, xMax], [yMin, yMax]] = box;
28671
- const [aX, aY] = initialPoint;
28672
- const [bX, bY] = throughPoint;
28673
- const yDiff = bY - aY;
28674
- const xDiff = bX - aX;
28675
- const slope = yDiff / xDiff;
28676
- const inverseSlope = 1 / slope;
28677
- const xExtreme = xDiff < 0 ? xMin : xMax;
28678
- const yExtreme = yDiff < 0 ? yMin : yMax;
28679
- const yAtXExtreme = aY + (xExtreme - aX) * slope;
28680
- const xAtYExtreme = aX + (yExtreme - aY) * inverseSlope;
28681
- switch (true) {
28682
- // does the ray exit the graph bounding box via the left or right edge?
28683
- case isBetween(yAtXExtreme, yMin, yMax):
28684
- return [xExtreme, yAtXExtreme];
28685
-
28686
- // does the ray exit the graph bounding box via the top or bottom edge?
28687
- case isBetween(xAtYExtreme, xMin, xMax):
28688
- return [xAtYExtreme, yExtreme];
28689
- default:
28690
- return [xExtreme, yExtreme];
28691
- }
28692
- };
28693
- const getLines = points => {
28694
- return points.map((point, i) => {
28695
- const next = points[(i + 1) % points.length];
28696
- return [point, next];
28697
- });
28698
- };
28699
- function isBetween(x, low, high) {
28700
- return x >= low && x <= high;
28701
- }
28702
-
28703
28750
  function SVGLine(props) {
28704
28751
  const {
28705
28752
  start,
@@ -28709,10 +28756,10 @@ function SVGLine(props) {
28709
28756
  testId
28710
28757
  } = props;
28711
28758
  return /*#__PURE__*/React.createElement("line", {
28712
- x1: start[0],
28713
- y1: start[1],
28714
- x2: end[0],
28715
- y2: end[1],
28759
+ x1: start[X],
28760
+ y1: start[Y],
28761
+ x2: end[X],
28762
+ y2: end[Y],
28716
28763
  style: style,
28717
28764
  className: className,
28718
28765
  "data-testid": testId
@@ -28745,6 +28792,41 @@ function Vector(props) {
28745
28792
  }));
28746
28793
  }
28747
28794
 
28795
+ /**
28796
+ * Given a ray and a rectangular box, find the point where the ray intersects
28797
+ * the edge of the box. Assumes the `initialPoint` is inside the box.
28798
+ * @param initialPoint - The starting point of the ray.
28799
+ * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
28800
+ * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
28801
+ */
28802
+ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
28803
+ const [[xMin, xMax], [yMin, yMax]] = box;
28804
+ const [aX, aY] = initialPoint;
28805
+ const [bX, bY] = throughPoint;
28806
+ const yDiff = bY - aY;
28807
+ const xDiff = bX - aX;
28808
+ const slope = yDiff / xDiff;
28809
+ const inverseSlope = 1 / slope;
28810
+ const xExtreme = xDiff < 0 ? xMin : xMax;
28811
+ const yExtreme = yDiff < 0 ? yMin : yMax;
28812
+ const yAtXExtreme = aY + (xExtreme - aX) * slope;
28813
+ const xAtYExtreme = aX + (yExtreme - aY) * inverseSlope;
28814
+ switch (true) {
28815
+ // does the ray exit the graph bounding box via the left or right edge?
28816
+ case isBetween(yAtXExtreme, yMin, yMax):
28817
+ return [xExtreme, yAtXExtreme];
28818
+
28819
+ // does the ray exit the graph bounding box via the top or bottom edge?
28820
+ case isBetween(xAtYExtreme, xMin, xMax):
28821
+ return [xAtYExtreme, yExtreme];
28822
+ default:
28823
+ return [xExtreme, yExtreme];
28824
+ }
28825
+ };
28826
+ function isBetween(x, low, high) {
28827
+ return x >= low && x <= high;
28828
+ }
28829
+
28748
28830
  const LockedLine = props => {
28749
28831
  const {
28750
28832
  color: color$1,
@@ -28797,8 +28879,8 @@ const LockedLine = props => {
28797
28879
  return /*#__PURE__*/React.createElement("g", {
28798
28880
  className: kind === "ray" ? "locked-ray" : "locked-line"
28799
28881
  }, line, showPoint1 && /*#__PURE__*/React.createElement(Point$2, {
28800
- x: point1.coord[0],
28801
- y: point1.coord[1],
28882
+ x: point1.coord[X],
28883
+ y: point1.coord[Y],
28802
28884
  svgCircleProps: {
28803
28885
  style: {
28804
28886
  fill: point1.filled ? lockedFigureColors[point1.color] : color.white,
@@ -28807,8 +28889,8 @@ const LockedLine = props => {
28807
28889
  }
28808
28890
  }
28809
28891
  }), showPoint2 && /*#__PURE__*/React.createElement(Point$2, {
28810
- x: point2.coord[0],
28811
- y: point2.coord[1],
28892
+ x: point2.coord[X],
28893
+ y: point2.coord[Y],
28812
28894
  svgCircleProps: {
28813
28895
  style: {
28814
28896
  fill: point2.filled ? lockedFigureColors[point2.color] : color.white,
@@ -28856,8 +28938,8 @@ const LockedPolygon = props => {
28856
28938
  color: lockedFigureColors[color]
28857
28939
  }), showVertices && points.map((point, index) => /*#__PURE__*/React.createElement(Point$2, {
28858
28940
  key: `locked-polygon-point-${index}`,
28859
- x: point[0],
28860
- y: point[1],
28941
+ x: point[X],
28942
+ y: point[Y],
28861
28943
  color: lockedFigureColors[color]
28862
28944
  })));
28863
28945
  };
@@ -28904,6 +28986,10 @@ const GraphLockedLayer = props => {
28904
28986
  return /*#__PURE__*/React.createElement(LockedPolygon, _extends({
28905
28987
  key: `polygon-${index}`
28906
28988
  }, figure));
28989
+ case "function":
28990
+ return /*#__PURE__*/React.createElement(LockedFunction, _extends({
28991
+ key: `function-${index}`
28992
+ }, figure));
28907
28993
  default:
28908
28994
  /**
28909
28995
  * Devlopment-time future-proofing: This should
@@ -28996,24 +29082,18 @@ const normalizePoints = (range, step, coordsList, noSnap) => coordsList.map(coor
28996
29082
  coords.map((coord, i) => {
28997
29083
  const axisRange = range[i];
28998
29084
  if (noSnap) {
28999
- return axisRange[0] + (axisRange[1] - axisRange[0]) * coord;
29085
+ return axisRange[MIN] + size(axisRange) * coord;
29000
29086
  }
29001
29087
  const axisStep = step[i];
29002
- const nSteps = Math.floor((axisRange[1] - axisRange[0]) / axisStep);
29088
+ const nSteps = Math.floor(size(axisRange) / axisStep);
29003
29089
  const tick = Math.round(coord * nSteps);
29004
- return axisRange[0] + axisStep * tick;
29090
+ return axisRange[MIN] + axisStep * tick;
29005
29091
  }));
29006
29092
 
29007
29093
  // same as normalizeCoords in interactive-graph.tsx
29008
29094
  const normalizeCoords = (coordsList, ranges) => coordsList.map(coords => coords.map((coord, i) => {
29009
- const extent = ranges[i][1] - ranges[i][0];
29010
- return (coord + ranges[i][1]) / extent;
29095
+ return (coord + ranges[i][1]) / size(ranges[i]);
29011
29096
  }));
29012
- function snap(snapStep, point) {
29013
- const [requestedX, requestedY] = point;
29014
- const [snapX, snapY] = snapStep;
29015
- return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29016
- }
29017
29097
 
29018
29098
  // Returns the closest point to the given `point` that is within the graph
29019
29099
  // bounds given in `state`.
@@ -29022,19 +29102,8 @@ function bound$1({
29022
29102
  range,
29023
29103
  point
29024
29104
  }) {
29025
- const [requestedX, requestedY] = point;
29026
- const [snapX, snapY] = snapStep;
29027
- const [[minX, maxX], [minY, maxY]] = range;
29028
- return [clamp(requestedX, minX + snapX, maxX - snapX), clamp(requestedY, minY + snapY, maxY - snapY)];
29029
- }
29030
- function clamp(value, min, max) {
29031
- if (value < min) {
29032
- return min;
29033
- }
29034
- if (value > max) {
29035
- return max;
29036
- }
29037
- return value;
29105
+ const boundingBox = inset(snapStep, range);
29106
+ return clampToBox(boundingBox, point);
29038
29107
  }
29039
29108
 
29040
29109
  var isProduction = process.env.NODE_ENV === 'production';
@@ -29104,8 +29173,8 @@ function useDraggable(args) {
29104
29173
  metaKey,
29105
29174
  shiftKey
29106
29175
  } = state;
29107
- const direction = [yDownDirection[0], -yDownDirection[1]];
29108
- const span = Math.abs(direction[0]) ? xSpan : ySpan;
29176
+ const direction = [yDownDirection[X], -yDownDirection[Y]];
29177
+ const span = Math.abs(direction[X]) ? xSpan : ySpan;
29109
29178
  let divisions = 50;
29110
29179
  if (altKey || metaKey) {
29111
29180
  divisions = 200;
@@ -29213,8 +29282,7 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
29213
29282
  const wbColorName = (_Object$entries$find$ = (_Object$entries$find = Object.entries(color).find(([_, value]) => value === color$1)) == null ? void 0 : _Object$entries$find[0]) != null ? _Object$entries$find$ : "blue";
29214
29283
  const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""} ${focusClass(focusBehavior)}`;
29215
29284
  const [[x, y]] = useTransformVectorsToPixels(point);
29216
- const [xMin, xMax] = range[0];
29217
- const [yMin, yMax] = range[1];
29285
+ const [[xMin, xMax], [yMin, yMax]] = range;
29218
29286
  const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29219
29287
  const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29220
29288
  const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
@@ -29271,7 +29339,7 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
29271
29339
  return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
29272
29340
  autoUpdate: true,
29273
29341
  backgroundColor: wbColorName,
29274
- content: `(${point[0]}, ${point[1]})`,
29342
+ content: `(${point[X]}, ${point[Y]})`,
29275
29343
  contentStyle: {
29276
29344
  color: "white"
29277
29345
  }
@@ -29456,16 +29524,7 @@ function trimRange(range, graphDimensionsInPixels) {
29456
29524
  const graphUnitsPerPixelY = size(yRange) / pixelsTall;
29457
29525
  const graphUnitsToTrimX = pixelsToTrim * graphUnitsPerPixelX;
29458
29526
  const graphUnitsToTrimY = pixelsToTrim * graphUnitsPerPixelY;
29459
- return [trim(xRange, graphUnitsToTrimX), trim(yRange, graphUnitsToTrimY)];
29460
- }
29461
- function trim(interval, amount) {
29462
- if (size(interval) < amount * 2) {
29463
- return [0, 0];
29464
- }
29465
- return [interval[0] + amount, interval[1] - amount];
29466
- }
29467
- function size(interval) {
29468
- return interval[1] - interval[0];
29527
+ return inset([graphUnitsToTrimX, graphUnitsToTrimY], range);
29469
29528
  }
29470
29529
 
29471
29530
  const SegmentGraph = props => {
@@ -29598,7 +29657,7 @@ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*
29598
29657
  in: "SourceGraphic"
29599
29658
  })));
29600
29659
 
29601
- const Angle = ({
29660
+ const PolygonAngle = ({
29602
29661
  centerPoint,
29603
29662
  endPoints,
29604
29663
  range,
@@ -29625,7 +29684,7 @@ const Angle = ({
29625
29684
  // Used to calculate the inside of the angle and to create square
29626
29685
  const [x3, y3] = vec.add(centerPoint, vec.add(vec.sub([x1, y1], centerPoint), vec.sub([x2, y2], centerPoint)));
29627
29686
  if (!showAngles) {
29628
- return isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
29687
+ return isRightPolygonAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
29629
29688
  start: [x1, y1],
29630
29689
  vertex: [x2, y2],
29631
29690
  end: [x3, y3]
@@ -29662,17 +29721,101 @@ const Angle = ({
29662
29721
  start: [x1, y1],
29663
29722
  vertex: [x2, y2],
29664
29723
  end: [x3, y3]
29665
- }) : /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29666
- d: arc,
29667
- strokeWidth: 0.02,
29668
- fill: "none"
29669
- })), /*#__PURE__*/React.createElement(TextLabel, {
29724
+ }) : /*#__PURE__*/React.createElement(Arc, {
29725
+ arc: arc
29726
+ }), /*#__PURE__*/React.createElement(TextLabel, {
29670
29727
  x: x3,
29671
29728
  y: y3,
29672
29729
  attach: y3 - centerY > 0 ? "s" : "n",
29673
29730
  attachDistance: Math.abs(y3 - centerY) < 0.2 || vec.dist([x3, y3], centerPoint) < 0.3 ? 20 : 10
29674
29731
  }, angleLabel, "\xB0"));
29675
29732
  };
29733
+ const Angle = ({
29734
+ vertex,
29735
+ coords,
29736
+ showAngles,
29737
+ allowReflexAngles,
29738
+ range
29739
+ }) => {
29740
+ // Check if the points are clockwise or not, depending on whether we allow reflex angles
29741
+ const areClockwise = clockwise$1([...coords, vertex]);
29742
+ const shouldReverseCoords = areClockwise && !allowReflexAngles;
29743
+
29744
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
29745
+ const clockwiseCoords = shouldReverseCoords ? coords : coords.reverse();
29746
+
29747
+ // Calculate the angles between the two points
29748
+ const startAngle = findAngle(clockwiseCoords[0], vertex);
29749
+ const endAngle = findAngle(clockwiseCoords[1], vertex);
29750
+ const angle = (startAngle + 360 - endAngle) % 360;
29751
+
29752
+ // Check if the angle is reflexive
29753
+ const isReflexive = angle > 180;
29754
+
29755
+ // Break out the necessary points for the arc calculation
29756
+ const [centerX, centerY] = vertex;
29757
+ const [point1, point2] = clockwiseCoords;
29758
+ const [startX, startY] = point1;
29759
+ const [endX, endY] = point2;
29760
+ const a = vec.dist(vertex, point1);
29761
+ const b = vec.dist(vertex, point2);
29762
+
29763
+ // Set the radius of the arc
29764
+ const radius = 2;
29765
+
29766
+ // Calculate the end points of the arc
29767
+ const y1 = centerY + (startY - centerY) / a * radius;
29768
+ const x2 = centerX + (endX - centerX) / b * radius;
29769
+ const x1 = centerX + (startX - centerX) / a * radius;
29770
+ const y2 = centerY + (endY - centerY) / b * radius;
29771
+
29772
+ // Used to calculate the inside of the angle and to create square
29773
+ const [x3, y3] = vec.add(vertex, vec.add(vec.sub([x1, y1], vertex), vec.sub([x2, y2], vertex)));
29774
+
29775
+ // Determine whether the arc should be drawn outside the angle
29776
+ const isOutside = shouldDrawArcOutside([x3, y3], vertex, range, [[vertex, point1], [vertex, point2]]);
29777
+
29778
+ // Determine the flags for the arc
29779
+ // If the angle is outside OR reflexive, we want to draw a large arc
29780
+ // If the angle is outside AND reflexive, we want to sweep the arc
29781
+ const largeArcFlag = isOutside || isReflexive ? 1 : 0;
29782
+ const sweepFlag = isOutside && isReflexive ? 1 : 0;
29783
+
29784
+ // Create the SVG path for the arc
29785
+ const arc = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${x2} ${y2}`;
29786
+
29787
+ // We only ever want to show whole angles
29788
+ const angleLabel = parseFloat(angle.toFixed(0)); // Only want to show whole angles
29789
+
29790
+ // Calculate the text position based on the angle and whether we allow reflex angles
29791
+ // Let's try the angle bisector method to find the midpoint of the arc
29792
+ const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
29793
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
29794
+ id: "background",
29795
+ x: "-5%",
29796
+ width: "110%",
29797
+ y: "0%",
29798
+ height: "100%"
29799
+ }, /*#__PURE__*/React.createElement("feFlood", {
29800
+ floodColor: "#FFF",
29801
+ floodOpacity: "0.5"
29802
+ }), /*#__PURE__*/React.createElement("feComposite", {
29803
+ operator: "over",
29804
+ in: "SourceGraphic"
29805
+ }))), isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
29806
+ start: [x1, y1],
29807
+ vertex: [x2, y2],
29808
+ end: [x3, y3],
29809
+ className: "arc-right-angle"
29810
+ }) : /*#__PURE__*/React.createElement(Arc, {
29811
+ arc: arc,
29812
+ className: "angle-arc"
29813
+ }), showAngles && /*#__PURE__*/React.createElement(TextLabel, {
29814
+ x: textX,
29815
+ y: textY,
29816
+ color: color.blue
29817
+ }, angleLabel, "\xB0"));
29818
+ };
29676
29819
 
29677
29820
  /**
29678
29821
  * This is broken out into its own component so that it can be used for an early return
@@ -29681,13 +29824,30 @@ const Angle = ({
29681
29824
  const RightAngleSquare = ({
29682
29825
  start: [x1, y1],
29683
29826
  vertex: [x2, y2],
29684
- end: [x3, y3]
29827
+ end: [x3, y3],
29828
+ className
29685
29829
  }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29686
29830
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
29687
29831
  strokeWidth: 0.02,
29688
- fill: "none"
29832
+ fill: "none",
29833
+ className: className
29689
29834
  }));
29690
- const isRightAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
29835
+
29836
+ // We're conditionally adding the class name here so that we can style the arc differently
29837
+ // based on whether it's an angle or a polygon angle
29838
+ const Arc = ({
29839
+ arc,
29840
+ className
29841
+ }) => {
29842
+ return /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29843
+ d: arc,
29844
+ strokeWidth: 0.02,
29845
+ fill: "none",
29846
+ className: className
29847
+ }));
29848
+ };
29849
+ const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
29850
+ const isRightAngle = angle => Math.round(angle) === 90;
29691
29851
 
29692
29852
  /**
29693
29853
  * Determines if an angle is an inside (false) or outside (true) angle.
@@ -29701,25 +29861,81 @@ const shouldDrawArcOutside = (midpoint, vertex, range, polygonLines) => {
29701
29861
  // Create a ray from the midpoint (inside angle) to the edge of the range
29702
29862
  const rangeIntersectionPoint = getIntersectionOfRayWithBox(midpoint, vertex, range);
29703
29863
  let lineIntersectionCount = 0;
29704
- polygonLines.forEach(line => linesIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
29864
+ polygonLines.forEach(line => segmentsIntersect([vertex, rangeIntersectionPoint], line) && lineIntersectionCount++);
29705
29865
 
29706
29866
  // If the number of intersections is even, the angle is inside the polygon
29707
29867
  return !isEven(lineIntersectionCount);
29708
29868
  };
29869
+ const isEven = n => n % 2 === 0;
29709
29870
 
29710
- // https://stackoverflow.com/a/24392281/7347484
29711
- // The "intersects" function in geometry doesn't seem to work for this use case
29712
- const linesIntersect = ([[a, b], [c, d]], [[p, q], [r, s]]) => {
29713
- const determinant = (c - a) * (s - q) - (r - p) * (d - b);
29714
- if (determinant === 0) {
29715
- return false;
29871
+ // This function calculates the bisector point of an angle formed by two points
29872
+ // and a vertex. It is used to position the angle label so that it is always
29873
+ // outside the arc at the exact midpoint of the angle.
29874
+ // Reflex angles make use of polar coordinates to determine the correct direction
29875
+ function calculateBisectorPoint(point1, point2, vertex, isReflex, allowReflex, arcRadius) {
29876
+ const [originX, originY] = vertex;
29877
+ const [x1, y1] = point1;
29878
+ const [x2, y2] = point2;
29879
+
29880
+ // Convert the Cartesian Coordinates to vectors
29881
+ const vectorA = [x1 - originX, y1 - originY];
29882
+ const vectorB = [x2 - originX, y2 - originY];
29883
+
29884
+ // Calculate the average angle of the vectors
29885
+ // so that we can point the bisector in the correct direction using polar coordinates
29886
+ const angleA = Math.atan2(vectorA[1], vectorA[0]);
29887
+ const angleB = Math.atan2(vectorB[1], vectorB[0]);
29888
+
29889
+ // Calculate the average of these two angles
29890
+ let averageAngle = (angleA + angleB) / 2;
29891
+ const angleRadians = Math.abs(angleA - angleB);
29892
+
29893
+ // Adjust the average angle to point in the correct direction
29894
+ // and to account for reflex angles and a bug with 180 degrees
29895
+ // that occurs due to our angleOffsetDegrees option in the graph
29896
+ if (allowReflex) {
29897
+ // If the resulting angles are less than 180 degrees,
29898
+ // or if the user has flipped the points on the interactive element
29899
+ // ensure that we have the correct direction for the reflex angle
29900
+ if (angleRadians <= Math.PI && isReflex || angleB > angleA) {
29901
+ averageAngle += Math.PI; // Add 180 degrees
29902
+ }
29716
29903
  } else {
29717
- const lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / determinant;
29718
- const gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / determinant;
29719
- return 0 < lambda && lambda < 1 && 0 < gamma && gamma < 1;
29904
+ // If the resulting angles are greater than 180 degrees, adjust
29905
+ // to get the correct angle, as we already know that the angle is not reflexive
29906
+ // This fixes a bug that occurs at 180 degrees due to our angleOffsetDegrees option in the graph
29907
+ if (angleRadians > Math.PI) {
29908
+ averageAngle -= Math.PI; // Subract 180 degrees
29909
+ }
29720
29910
  }
29721
- };
29722
- const isEven = n => n % 2 === 0;
29911
+
29912
+ // Convert the average angle back to Cartesian coordinates
29913
+ const sum = [Math.cos(averageAngle), Math.sin(averageAngle)];
29914
+
29915
+ // Calculate the magnitude of the sum to normalize it
29916
+ const sumMagnitude = Math.sqrt(sum[0] ** 2 + sum[1] ** 2);
29917
+ const bisectorDirection = [sum[0] / sumMagnitude, sum[1] / sumMagnitude];
29918
+
29919
+ // Calculate the initial distance of the bisector direction from the origin
29920
+ const initialDistance = Math.sqrt(bisectorDirection[0] ** 2 + bisectorDirection[1] ** 2);
29921
+
29922
+ // Determine the minimum radius to ensure that the text is always outside the arc
29923
+ const requiredDistance = arcRadius * 1.75;
29924
+ let radius = requiredDistance / initialDistance;
29925
+
29926
+ // Apply radius only if the initial distance is less than required
29927
+ if (initialDistance >= requiredDistance) {
29928
+ radius = 1; // Use the unit vector as is
29929
+ }
29930
+
29931
+ // Scale the bisector direction by the radius
29932
+ const bisectorPoint = [bisectorDirection[0] * radius, bisectorDirection[1] * radius];
29933
+
29934
+ // Add the vertex to the bisector point to get the final position
29935
+ // to ensure that the angle label moves with the interactive element
29936
+ const scaledBisector = vec.add(bisectorPoint, vertex);
29937
+ return scaledBisector;
29938
+ }
29723
29939
 
29724
29940
  const StyledMovablePoint = props => {
29725
29941
  const {
@@ -29801,7 +30017,7 @@ const PolygonGraph = props => {
29801
30017
  if (!pt1 || !pt2) {
29802
30018
  return null;
29803
30019
  }
29804
- return /*#__PURE__*/React.createElement(Angle, {
30020
+ return /*#__PURE__*/React.createElement(PolygonAngle, {
29805
30021
  key: "angle-" + i,
29806
30022
  centerPoint: point,
29807
30023
  endPoints: [pt1, pt2],
@@ -29842,6 +30058,12 @@ const PolygonGraph = props => {
29842
30058
  onMove: destination => dispatch(movePoint(i, destination))
29843
30059
  })));
29844
30060
  };
30061
+ function getLines(points) {
30062
+ return points.map((point, i) => {
30063
+ const next = points[(i + 1) % points.length];
30064
+ return [point, next];
30065
+ });
30066
+ }
29845
30067
 
29846
30068
  function getGradableGraph(state, initialGraph) {
29847
30069
  if (!state.hasBeenInteractedWith) {
@@ -29893,6 +30115,12 @@ function getGradableGraph(state, initialGraph) {
29893
30115
  coords: state.coords
29894
30116
  });
29895
30117
  }
30118
+ if (state.type === "angle" && initialGraph.type === "angle") {
30119
+ return _extends({}, initialGraph, {
30120
+ coords: state.coords,
30121
+ allowReflexAngles: state.allowReflexAngles
30122
+ });
30123
+ }
29896
30124
  throw new Error("Mafs is not yet implemented for graph type: " + initialGraph.type);
29897
30125
  }
29898
30126
 
@@ -29955,16 +30183,16 @@ function MovableCircle(props) {
29955
30183
  className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
29956
30184
  }, /*#__PURE__*/React.createElement("ellipse", {
29957
30185
  className: "focus-ring",
29958
- cx: centerPx[0],
29959
- cy: centerPx[1],
29960
- rx: radiiPx[0] + 3,
29961
- ry: radiiPx[1] + 3
30186
+ cx: centerPx[X],
30187
+ cy: centerPx[Y],
30188
+ rx: radiiPx[X] + 3,
30189
+ ry: radiiPx[Y] + 3
29962
30190
  }), /*#__PURE__*/React.createElement("ellipse", {
29963
30191
  className: "circle",
29964
- cx: centerPx[0],
29965
- cy: centerPx[1],
29966
- rx: radiiPx[0],
29967
- ry: radiiPx[1]
30192
+ cx: centerPx[X],
30193
+ cy: centerPx[Y],
30194
+ rx: radiiPx[X],
30195
+ ry: radiiPx[Y]
29968
30196
  }), /*#__PURE__*/React.createElement(DragHandle, {
29969
30197
  center: center
29970
30198
  }));
@@ -29980,10 +30208,10 @@ function DragHandle(props) {
29980
30208
  const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
29981
30209
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
29982
30210
  className: "movable-circle-handle",
29983
- x: topLeft[0],
29984
- y: topLeft[1],
29985
- width: dragHandleDimensions[0],
29986
- height: dragHandleDimensions[1],
30211
+ x: topLeft[X],
30212
+ y: topLeft[Y],
30213
+ width: dragHandleDimensions[X],
30214
+ height: dragHandleDimensions[Y],
29987
30215
  rx: cornerRadius,
29988
30216
  ry: cornerRadius
29989
30217
  }), dragHandleDotPositions.map(offsetPx => {
@@ -30118,15 +30346,15 @@ const getSinusoidCoefficients = coords => {
30118
30346
  const p2 = coords[1];
30119
30347
 
30120
30348
  // If the x-coordinates are the same, we are unable to calculate the coefficients
30121
- if (p2[0] === p1[0]) {
30349
+ if (p2[X] === p1[X]) {
30122
30350
  return;
30123
30351
  }
30124
30352
 
30125
30353
  // Resulting coefficients are canonical for this sine curve
30126
- const amplitude = p2[1] - p1[1];
30127
- const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
30128
- const phase = p1[0] * angularFrequency;
30129
- const verticalOffset = p1[1];
30354
+ const amplitude = p2[Y] - p1[Y];
30355
+ const angularFrequency = Math.PI / (2 * (p2[X] - p1[X]));
30356
+ const phase = p1[X] * angularFrequency;
30357
+ const verticalOffset = p1[Y];
30130
30358
  return {
30131
30359
  amplitude,
30132
30360
  angularFrequency,
@@ -30135,6 +30363,74 @@ const getSinusoidCoefficients = coords => {
30135
30363
  };
30136
30364
  };
30137
30365
 
30366
+ function AngleGraph(props) {
30367
+ const {
30368
+ dispatch,
30369
+ graphState
30370
+ } = props;
30371
+ const {
30372
+ graphDimensionsInPixels
30373
+ } = useGraphConfig();
30374
+ const {
30375
+ coords,
30376
+ showAngles,
30377
+ range,
30378
+ allowReflexAngles,
30379
+ angleOffsetDeg,
30380
+ snapDegrees
30381
+ } = graphState;
30382
+
30383
+ // Break the coords into the two end points and the center point
30384
+ const endPoints = [coords[0], coords[2]];
30385
+ const centerPoint = coords[1];
30386
+
30387
+ // Convert the vectors to pixels for rendering the svg lines
30388
+ const angleLines = [[centerPoint, endPoints[0]], [centerPoint, endPoints[1]]];
30389
+ const linePixelCoords = [useTransformVectorsToPixels(centerPoint, endPoints[0]), useTransformVectorsToPixels(centerPoint, endPoints[1])];
30390
+
30391
+ // Create the SVG lines for the angle
30392
+ const svgLines = linePixelCoords.map(([startPtPx, endPtPx], i) => {
30393
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
30394
+ const endExtend = getIntersectionOfRayWithBox(angleLines[i][0], angleLines[i][1], trimmedRange);
30395
+ return /*#__PURE__*/React.createElement("g", {
30396
+ key: `line-${i}`
30397
+ }, /*#__PURE__*/React.createElement(SVGLine, {
30398
+ start: startPtPx,
30399
+ end: endPtPx,
30400
+ style: {
30401
+ stroke: "var(--movable-line-stroke-color)",
30402
+ strokeWidth: "var(--movable-line-stroke-weight)"
30403
+ }
30404
+ }), /*#__PURE__*/React.createElement(Vector, {
30405
+ tail: angleLines[i][1],
30406
+ tip: endExtend,
30407
+ color: "var(--movable-line-stroke-color)"
30408
+ }));
30409
+ });
30410
+
30411
+ // Create the angle indicator parameters
30412
+ const angleParams = {
30413
+ vertex: centerPoint,
30414
+ coords: endPoints,
30415
+ allowReflexAngles: allowReflexAngles || false,
30416
+ // Whether to allow reflex angles or not
30417
+ angleOffsetDeg: angleOffsetDeg || 0,
30418
+ // The angle offset from the x-axis
30419
+ snapDegrees: snapDegrees || 1,
30420
+ // The multiple of degrees to snap to
30421
+ range: range,
30422
+ showAngles: showAngles || false // Whether to show the angle or not
30423
+ };
30424
+
30425
+ // Render the lines, angle, and then movable points
30426
+ return /*#__PURE__*/React.createElement(React.Fragment, null, svgLines, /*#__PURE__*/React.createElement(Angle, angleParams), coords.map((point, i) => /*#__PURE__*/React.createElement(StyledMovablePoint, {
30427
+ key: "point-" + i,
30428
+ snapTo: "angles",
30429
+ point: point,
30430
+ onMove: destination => dispatch(movePoint(i, destination))
30431
+ })));
30432
+ }
30433
+
30138
30434
  // Universal padding for the axis ticks to ensure that the labels
30139
30435
  // have enough space to render without overlapping the tick lines
30140
30436
  const tickPadding = 5;
@@ -30144,10 +30440,8 @@ const AxisTickLabels = () => {
30144
30440
  tickStep,
30145
30441
  range
30146
30442
  } = graphConfig;
30147
- const [xMin, xMax] = range[0];
30148
- const [yMin, yMax] = range[1];
30149
- const yTickStep = tickStep[1];
30150
- const xTickStep = tickStep[0];
30443
+ const [[xMin, xMax], [yMin, yMax]] = range;
30444
+ const [xTickStep, yTickStep] = tickStep;
30151
30445
  const yGridTicks = generateTickLocations$1(yTickStep, yMin, yMax);
30152
30446
  const xGridTicks = generateTickLocations$1(xTickStep, xMin, xMax);
30153
30447
  return /*#__PURE__*/React.createElement("div", {
@@ -30426,13 +30720,13 @@ const YGridTick = ({
30426
30720
 
30427
30721
  // If the graph is zoomed in, we want to make sure the ticks are still visible
30428
30722
  // even if they are outside the graph's range.
30429
- if (graphInfo.range[0][0] > 0) {
30723
+ if (graphInfo.range[X][MIN] > 0) {
30430
30724
  // If the graph is on the positive side of the x-axis, lock the ticks to the left side of the graph
30431
- xPointOnAxis = graphInfo.range[0][0];
30725
+ xPointOnAxis = graphInfo.range[X][MIN];
30432
30726
  }
30433
- if (graphInfo.range[0][1] < 0) {
30727
+ if (graphInfo.range[X][MAX] < 0) {
30434
30728
  // If the graph is on the negative side of the x-axis, lock the ticks to the right side of the graph
30435
- xPointOnAxis = graphInfo.range[0][1];
30729
+ xPointOnAxis = graphInfo.range[X][MAX];
30436
30730
  }
30437
30731
  const pointOnAxis = [xPointOnAxis, y];
30438
30732
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
@@ -30466,13 +30760,13 @@ const XGridTick = ({
30466
30760
  let yPointOnAxis = 0;
30467
30761
  // If the graph is zoomed in, we want to make sure the ticks are still visible
30468
30762
  // even if they are outside the graph's range.
30469
- if (graphInfo.range[1][0] > 0) {
30763
+ if (graphInfo.range[Y][MIN] > 0) {
30470
30764
  // If the graph is on the positive side of the y-axis, lock the ticks to the top of the graph
30471
- yPointOnAxis = graphInfo.range[1][0];
30765
+ yPointOnAxis = graphInfo.range[Y][MIN];
30472
30766
  }
30473
- if (graphInfo.range[1][1] < 0) {
30767
+ if (graphInfo.range[Y][MAX] < 0) {
30474
30768
  // If the graph is on the negative side of the x-axis, lock the ticks to the bottom of the graph
30475
- yPointOnAxis = graphInfo.range[1][1];
30769
+ yPointOnAxis = graphInfo.range[Y][MAX];
30476
30770
  }
30477
30771
  const pointOnAxis = [x, yPointOnAxis];
30478
30772
  const [[xPosition, yPosition]] = useTransformVectorsToPixels(pointOnAxis);
@@ -30527,10 +30821,8 @@ const AxisTicks = () => {
30527
30821
  width,
30528
30822
  height
30529
30823
  };
30530
- const [xMin, xMax] = range[0];
30531
- const [yMin, yMax] = range[1];
30532
- const yTickStep = tickStep[1];
30533
- const xTickStep = tickStep[0];
30824
+ const [[xMin, xMax], [yMin, yMax]] = range;
30825
+ const [xTickStep, yTickStep] = tickStep;
30534
30826
  const yGridTicks = generateTickLocations(yTickStep, yMin, yMax);
30535
30827
  const xGridTicks = generateTickLocations(xTickStep, xMin, xMax);
30536
30828
  return /*#__PURE__*/React.createElement("g", {
@@ -30569,8 +30861,8 @@ const axisOptions = (props, axisIndex) => {
30569
30861
  };
30570
30862
  const Grid = props => {
30571
30863
  return props.markings === "none" ? null : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Coordinates.Cartesian, {
30572
- xAxis: axisOptions(props, 0),
30573
- yAxis: axisOptions(props, 1)
30864
+ xAxis: axisOptions(props, X),
30865
+ yAxis: axisOptions(props, Y)
30574
30866
  }),
30575
30867
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
30576
30868
  props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)));
@@ -31179,7 +31471,7 @@ const LegacyGrid = ({
31179
31471
  height
31180
31472
  } = backgroundImage != null ? backgroundImage : {};
31181
31473
  if (url && typeof url === "string") {
31182
- const scale = box[0] / interactiveSizes$1.defaultBoxSize;
31474
+ const scale = box[X] / interactiveSizes$1.defaultBoxSize;
31183
31475
  return /*#__PURE__*/React.createElement(View, {
31184
31476
  style: {
31185
31477
  position: "absolute",
@@ -31244,9 +31536,9 @@ function Protractor() {
31244
31536
  const angle = calculateAngleInDegrees(rotationHandleOffset) - calculateAngleInDegrees(centerToRotationHandle);
31245
31537
  return /*#__PURE__*/React.createElement("g", {
31246
31538
  ref: draggableRef,
31247
- transform: `translate(${topLeftPx[0]}, ${topLeftPx[1]}), rotate(${angle})`,
31539
+ transform: `translate(${topLeftPx[X]}, ${topLeftPx[Y]}), rotate(${angle})`,
31248
31540
  style: {
31249
- transformOrigin: `${-centerToTopLeft[0]}px ${-centerToTopLeft[1]}px`
31541
+ transformOrigin: `${-centerToTopLeft[X]}px ${-centerToTopLeft[Y]}px`
31250
31542
  }
31251
31543
  }, /*#__PURE__*/React.createElement("image", {
31252
31544
  href: protractorImage
@@ -31258,7 +31550,7 @@ function Protractor() {
31258
31550
  function RotationArrow() {
31259
31551
  const radius = 175;
31260
31552
  const angleDeg = 10;
31261
- const angleRad = degreesToRadians(angleDeg);
31553
+ const angleRad = convertDegreesToRadians(angleDeg);
31262
31554
  const endX = radius * (1 - Math.cos(angleRad));
31263
31555
  const endY = radius * -Math.sin(angleRad);
31264
31556
  const rotationArrow = pathBuilder().move(0, 0).circularArc(radius, endX, endY, {
@@ -31337,17 +31629,135 @@ function useDraggablePx(args) {
31337
31629
  });
31338
31630
  }
31339
31631
 
31340
- // [L]inear Int[erp]olation: gets the weighted average of two values `a` and
31341
- // `b`, with the given `fraction` specifying how much weight `a` and `b` get:
31342
- // - if `fraction` is 0, `lerp` returns `a`.
31343
- // - if `fraction` is 0.5, `lerp` returns the average of `a` and `b`.
31344
- // - if `fraction` is 1, `lerp` returns `b`.
31345
- function lerp(a, b, fraction) {
31346
- return (b - a) * fraction + a;
31347
- }
31348
- function degreesToRadians(degrees) {
31349
- return degrees / 180 * Math.PI;
31350
- }
31632
+ const MafsGraph = props => {
31633
+ const {
31634
+ state,
31635
+ dispatch,
31636
+ labels
31637
+ } = props;
31638
+ const [width, height] = props.box;
31639
+ const tickStep = props.step;
31640
+ return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
31641
+ value: {
31642
+ range: state.range,
31643
+ snapStep: state.snapStep,
31644
+ markings: props.markings,
31645
+ tickStep: tickStep,
31646
+ gridStep: props.gridStep,
31647
+ showTooltips: !!props.showTooltips,
31648
+ graphDimensionsInPixels: props.box,
31649
+ width,
31650
+ height,
31651
+ labels
31652
+ }
31653
+ }, /*#__PURE__*/React.createElement(View, {
31654
+ className: "mafs-graph",
31655
+ style: {
31656
+ width,
31657
+ height,
31658
+ position: "relative",
31659
+ padding: "25px 25px 0 0",
31660
+ boxSizing: "content-box",
31661
+ marginLeft: "20px",
31662
+ marginBottom: "20px"
31663
+ }
31664
+ }, /*#__PURE__*/React.createElement(LegacyGrid, {
31665
+ box: props.box,
31666
+ backgroundImage: props.backgroundImage
31667
+ }), /*#__PURE__*/React.createElement(View, {
31668
+ style: {
31669
+ position: "absolute",
31670
+ bottom: 0,
31671
+ left: 0
31672
+ }
31673
+ }, props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(AxisTickLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
31674
+ preserveAspectRatio: false,
31675
+ viewBox: {
31676
+ x: state.range[X],
31677
+ y: state.range[Y],
31678
+ padding: 0
31679
+ },
31680
+ pan: false,
31681
+ zoom: false,
31682
+ width: width,
31683
+ height: height
31684
+ }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
31685
+ tickStep: props.step,
31686
+ gridStep: props.gridStep,
31687
+ range: state.range,
31688
+ containerSizeClass: props.containerSizeClass,
31689
+ markings: props.markings
31690
+ }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
31691
+ lockedFigures: props.lockedFigures,
31692
+ range: state.range
31693
+ }), "/* Protractor */", props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
31694
+ state,
31695
+ dispatch
31696
+ })))));
31697
+ };
31698
+ const renderGraph = props => {
31699
+ const {
31700
+ state,
31701
+ dispatch
31702
+ } = props;
31703
+ const {
31704
+ type
31705
+ } = state;
31706
+ switch (type) {
31707
+ case "angle":
31708
+ return /*#__PURE__*/React.createElement(AngleGraph, {
31709
+ graphState: state,
31710
+ dispatch: dispatch
31711
+ });
31712
+ case "segment":
31713
+ return /*#__PURE__*/React.createElement(SegmentGraph, {
31714
+ graphState: state,
31715
+ dispatch: dispatch
31716
+ });
31717
+ case "linear-system":
31718
+ return /*#__PURE__*/React.createElement(LinearSystemGraph, {
31719
+ graphState: state,
31720
+ dispatch: dispatch
31721
+ });
31722
+ case "linear":
31723
+ return /*#__PURE__*/React.createElement(LinearGraph, {
31724
+ graphState: state,
31725
+ dispatch: dispatch
31726
+ });
31727
+ case "ray":
31728
+ return /*#__PURE__*/React.createElement(RayGraph, {
31729
+ graphState: state,
31730
+ dispatch: dispatch
31731
+ });
31732
+ case "polygon":
31733
+ return /*#__PURE__*/React.createElement(PolygonGraph, {
31734
+ graphState: state,
31735
+ dispatch: dispatch
31736
+ });
31737
+ case "point":
31738
+ return /*#__PURE__*/React.createElement(PointGraph, {
31739
+ graphState: state,
31740
+ dispatch: dispatch
31741
+ });
31742
+ case "circle":
31743
+ return /*#__PURE__*/React.createElement(CircleGraph, {
31744
+ graphState: state,
31745
+ dispatch: dispatch
31746
+ });
31747
+ case "quadratic":
31748
+ return /*#__PURE__*/React.createElement(QuadraticGraph, {
31749
+ graphState: state,
31750
+ dispatch: dispatch
31751
+ });
31752
+ case "sinusoid":
31753
+ return /*#__PURE__*/React.createElement(SinusoidGraph, {
31754
+ graphState: state,
31755
+ dispatch: dispatch
31756
+ });
31757
+ default:
31758
+ return new UnreachableCaseError(type);
31759
+ }
31760
+ };
31351
31761
 
31352
31762
  function initializeGraphState(params) {
31353
31763
  var _graph$snapTo;
@@ -31411,7 +31821,18 @@ function initializeGraphState(params) {
31411
31821
  coords: getSinusoidCoords(graph, range, step)
31412
31822
  });
31413
31823
  case "angle":
31414
- throw new Error("Mafs not yet implemented for graph type: " + graph.type);
31824
+ return _extends({}, shared, {
31825
+ type: graph.type,
31826
+ showAngles: Boolean(graph.showAngles),
31827
+ coords: getAngleCoords({
31828
+ graph,
31829
+ range,
31830
+ step
31831
+ }),
31832
+ angleOffsetDeg: Number(graph.angleOffsetDeg),
31833
+ allowReflexAngles: Boolean(graph.allowReflexAngles),
31834
+ snapDegrees: Number(graph.snapDegrees)
31835
+ });
31415
31836
  default:
31416
31837
  throw new UnreachableCaseError(graph);
31417
31838
  }
@@ -31545,6 +31966,43 @@ function getCircleCoords(graph) {
31545
31966
  radiusPoint: [2, 0]
31546
31967
  };
31547
31968
  }
31969
+ const getAngleCoords = params => {
31970
+ const {
31971
+ graph,
31972
+ range,
31973
+ step
31974
+ } = params;
31975
+ if (graph.coords) {
31976
+ return graph.coords;
31977
+ }
31978
+ const {
31979
+ snapDegrees,
31980
+ angleOffsetDeg
31981
+ } = graph;
31982
+ const snap = snapDegrees || 1;
31983
+ let angle = snap;
31984
+ while (angle < 20) {
31985
+ angle += snap;
31986
+ }
31987
+ angle = angle * Math.PI / 180;
31988
+ const offset = (angleOffsetDeg || 0) * Math.PI / 180;
31989
+ let defaultCoords = [[0.85, 0.5], [0.5, 0.5]];
31990
+ defaultCoords = normalizePoints(range, step, defaultCoords, true);
31991
+
31992
+ // @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.
31993
+ const radius = magnitude(vector(...defaultCoords));
31994
+
31995
+ // We're adding a placeholder for the third point to appease ts and so that we
31996
+ // can calculate it after we've adjusted the first point by the angleOffsetDeg
31997
+ const coords = [...defaultCoords, [0, 0]];
31998
+
31999
+ // Adjust the lower point by angleOffsetDeg degrees
32000
+ coords[0] = [coords[1][0] + radius * Math.cos(offset), coords[1][1] + radius * Math.sin(offset)];
32001
+ // Position the upper point angle radians from the
32002
+ // lower point
32003
+ coords[2] = [coords[1][0] + radius * Math.cos(angle + offset), coords[1][1] + radius * Math.sin(angle + offset)];
32004
+ return coords;
32005
+ };
31548
32006
 
31549
32007
  function interactiveGraphReducer(state, action) {
31550
32008
  switch (action.type) {
@@ -31606,6 +32064,7 @@ function doMoveControlPoint(state, action) {
31606
32064
  coords: newCoords
31607
32065
  });
31608
32066
  }
32067
+ case "angle":
31609
32068
  case "circle":
31610
32069
  throw new Error("FIXME implement circle reducer");
31611
32070
  case "point":
@@ -31707,12 +32166,30 @@ function doMoveAll(state, action) {
31707
32166
  }
31708
32167
  function doMovePoint(state, action) {
31709
32168
  switch (state.type) {
32169
+ case "angle":
32170
+ // If the index is 1, we are moving the vertex of the angle,
32171
+ // which will move the other two points as well
32172
+ if (action.index === 1) {
32173
+ const updatedCoords = boundAndSnapAngleVertex(state, action);
32174
+ return _extends({}, state, {
32175
+ hasBeenInteractedWith: true,
32176
+ coords: updatedCoords
32177
+ });
32178
+ }
32179
+ return _extends({}, state, {
32180
+ hasBeenInteractedWith: true,
32181
+ coords: setAtIndex({
32182
+ array: state.coords,
32183
+ index: action.index,
32184
+ newValue: boundAndSnapAngleEndPoints(action.destination, state, action.index)
32185
+ })
32186
+ });
31710
32187
  case "polygon":
31711
32188
  let newValue;
31712
32189
  if (state.snapTo === "sides") {
31713
32190
  newValue = boundAndSnapToSides(action.destination, state, action.index);
31714
32191
  } else if (state.snapTo === "angles") {
31715
- newValue = boundAndSnapToAngle(action.destination, state, action.index);
32192
+ newValue = boundAndSnapToPolygonAngle(action.destination, state, action.index);
31716
32193
  } else {
31717
32194
  newValue = boundAndSnapToGrid(action.destination, state);
31718
32195
  }
@@ -31751,7 +32228,7 @@ function doMovePoint(state, action) {
31751
32228
  // vertical line. If they are, then we don't want to move the point
31752
32229
  const _newCoords = [...state.coords];
31753
32230
  _newCoords[action.index] = boundDestination;
31754
- if (_newCoords[0][0] === _newCoords[1][0]) {
32231
+ if (_newCoords[0][X] === _newCoords[1][X]) {
31755
32232
  return state;
31756
32233
  }
31757
32234
  return _extends({}, state, {
@@ -31809,13 +32286,13 @@ function doMoveCenter(state, action) {
31809
32286
  // if it otherwise would be off the chart
31810
32287
  // ex: if the handle is on the right and we move the center
31811
32288
  // to the rightmost position, move the handle to the left
31812
- const [xMin, xMax] = state.range[0];
32289
+ const [xMin, xMax] = state.range[X];
31813
32290
  const [radX] = newRadiusPoint;
31814
32291
  if (radX < xMin || radX > xMax) {
31815
- const xJumpDist = (radX - constrainedCenter[0]) * 2;
32292
+ const xJumpDist = (radX - constrainedCenter[X]) * 2;
31816
32293
  const possibleNewX = radX - xJumpDist;
31817
32294
  if (possibleNewX >= xMin && possibleNewX <= xMax) {
31818
- newRadiusPoint[0] = possibleNewX;
32295
+ newRadiusPoint[X] = possibleNewX;
31819
32296
  }
31820
32297
  }
31821
32298
  return _extends({}, state, {
@@ -31832,11 +32309,11 @@ function doMoveRadiusPoint(state, action) {
31832
32309
  switch (state.type) {
31833
32310
  case "circle":
31834
32311
  {
31835
- const [xMin, xMax] = state.range[0];
32312
+ const [xMin, xMax] = state.range[X];
31836
32313
  const nextRadiusPoint = [
31837
32314
  // Constrain to graph range
31838
32315
  // The +0 is to convert -0 to +0
31839
- Math.min(Math.max(xMin, action.destination[0] + 0), xMax), state.center[1]];
32316
+ clamp(action.destination[X] + 0, xMin, xMax), state.center[1]];
31840
32317
  if (_.isEqual(nextRadiusPoint, state.center)) {
31841
32318
  return state;
31842
32319
  }
@@ -31871,10 +32348,10 @@ function doChangeRange(state, action) {
31871
32348
  }
31872
32349
  const getDeltaVertex = (maxMoves, minMoves, delta) => {
31873
32350
  const [deltaX, deltaY] = delta;
31874
- const maxXMove = Math.min(...maxMoves.map(move => move[0]));
31875
- const maxYMove = Math.min(...maxMoves.map(move => move[1]));
31876
- const minXMove = Math.max(...minMoves.map(move => move[0]));
31877
- const minYMove = Math.max(...minMoves.map(move => move[1]));
32351
+ const maxXMove = Math.min(...maxMoves.map(move => move[X]));
32352
+ const maxYMove = Math.min(...maxMoves.map(move => move[Y]));
32353
+ const minXMove = Math.max(...minMoves.map(move => move[X]));
32354
+ const minYMove = Math.max(...minMoves.map(move => move[Y]));
31878
32355
  const dx = clamp(deltaX, minXMove, maxXMove);
31879
32356
  const dy = clamp(deltaY, minYMove, maxYMove);
31880
32357
  return [dx, dy];
@@ -31905,7 +32382,122 @@ function boundAndSnapToGrid(point, {
31905
32382
  point
31906
32383
  }));
31907
32384
  }
31908
- function boundAndSnapToAngle(destinationPoint, {
32385
+ function boundAndSnapAngleVertex({
32386
+ range,
32387
+ coords,
32388
+ snapStep
32389
+ }, {
32390
+ destination
32391
+ }) {
32392
+ // Needed to prevent updating the original coords before the checks for
32393
+ // degenerate triangles and overlapping sides
32394
+ const coordsCopy = [...coords];
32395
+
32396
+ // Get the current and upcoming positions of the vertex
32397
+ const startingVertex = coordsCopy[1];
32398
+ const newVertex = bound$1({
32399
+ snapStep,
32400
+ range,
32401
+ point: snap(snapStep, destination)
32402
+ });
32403
+
32404
+ // Get the vector from the starting vertex to the new vertex
32405
+ const delta = vec.add(newVertex, reverseVector(startingVertex));
32406
+
32407
+ // Apply the delta to each of the other two points so that the angle is maintained
32408
+ let valid = true;
32409
+ const newPoints = {};
32410
+ for (const i of [0, 2]) {
32411
+ const oldPoint = coordsCopy[i];
32412
+ let newPoint = vec.add(oldPoint, delta);
32413
+ let angle = findAngle(newVertex, newPoint);
32414
+ angle *= Math.PI / 180;
32415
+ newPoint = constrainToBoundsOnAngle(newPoint, angle, range, snapStep);
32416
+ newPoints[i] = newPoint;
32417
+
32418
+ // Check if the new point is too close to the vertex
32419
+ if (tooClose(newVertex, newPoint)) {
32420
+ valid = false;
32421
+ }
32422
+ }
32423
+
32424
+ // Update the vertex after snapping to the snapStep
32425
+ newPoints[1] = newVertex;
32426
+ // Only move points if all new positions are valid
32427
+ if (valid) {
32428
+ Object.entries(newPoints).forEach(([i, newPoint]) => {
32429
+ coordsCopy[i] = newPoint;
32430
+ });
32431
+ }
32432
+ return coordsCopy;
32433
+ }
32434
+
32435
+ // This function is used to ensure that the vertex of
32436
+ // an angle is not too close to the other points
32437
+ function tooClose(point1, point2, range) {
32438
+ const safeDistance = 2;
32439
+ const distance = vec.dist(point1, point2);
32440
+ return distance < safeDistance;
32441
+ }
32442
+ function constrainToBoundsOnAngle(point, angle, range, snapStep) {
32443
+ // We're subtracting the snapStep from the lower bound and adding it to the upper bound
32444
+ // to ensure that the point is within the bounds of the graph even after snapping to the nearest degree
32445
+ const lower = [range[0][0] + snapStep[0], range[1][0] + snapStep[0]];
32446
+ const upper = [range[0][1] - snapStep[1], range[1][1] - snapStep[1]];
32447
+ let result = point;
32448
+ if (result[0] < lower[0]) {
32449
+ result = [lower[0], result[1] + (lower[0] - result[0]) * Math.tan(angle)];
32450
+ } else if (result[0] > upper[0]) {
32451
+ result = [upper[0], result[1] - (result[0] - upper[0]) * Math.tan(angle)];
32452
+ }
32453
+ if (result[1] < lower[1]) {
32454
+ result = [result[0] + (lower[1] - result[1]) / Math.tan(angle), lower[1]];
32455
+ } else if (result[1] > upper[1]) {
32456
+ result = [result[0] - (result[1] - upper[1]) / Math.tan(angle), upper[1]];
32457
+ }
32458
+ return result;
32459
+ }
32460
+ function boundAndSnapAngleEndPoints(destinationPoint, {
32461
+ range,
32462
+ coords,
32463
+ snapDegrees,
32464
+ angleOffsetDeg,
32465
+ snapStep
32466
+ }, index) {
32467
+ const snap = snapDegrees || 1;
32468
+ const offsetDegrees = angleOffsetDeg || 0;
32469
+
32470
+ // Needed to prevent updating the original coords before the checks for
32471
+ // degenerate triangles and overlapping sides
32472
+ const coordsCopy = [...coords];
32473
+
32474
+ // We want to subtract or add the snapStep to the lower and upper bounds
32475
+ // respectively to ensure that the point is within the bounds of the graph
32476
+ // even after snapping to the nearest degree
32477
+ const angleRange = [[range[0][0] + snapStep[0], range[0][1] - snapStep[0]], [range[1][0] + snapStep[1], range[1][1] - snapStep[1]]];
32478
+
32479
+ // Takes the destination point and makes sure it is within the bounds of the graph
32480
+ // SnapStep is [0, 0] because we don't want to snap these points to the grid at all
32481
+ const boundPoint = bound$1({
32482
+ snapStep: [0, 0],
32483
+ range: angleRange,
32484
+ point: destinationPoint
32485
+ });
32486
+ coordsCopy[index] = boundPoint;
32487
+
32488
+ // Get the vertex of the angle
32489
+ const vertex = coords[1];
32490
+
32491
+ // Gets the angle between the coords and the vertex
32492
+ let angle = findAngle(coordsCopy[index], vertex);
32493
+
32494
+ // Snap the angle to the nearest multiple of snapDegrees (if provided)
32495
+ angle = Math.round((angle - offsetDegrees) / snap) * snap + offsetDegrees;
32496
+ const distance = vec.dist(coordsCopy[index], vertex);
32497
+ const snappedValue = vec.add(vertex, polar(distance, angle));
32498
+ return snappedValue;
32499
+ }
32500
+ function boundAndSnapToPolygonAngle(destinationPoint, {
31909
32501
  range,
31910
32502
  coords
31911
32503
  }, index) {
@@ -31936,7 +32528,7 @@ function boundAndSnapToAngle(destinationPoint, {
31936
32528
  angles[rel(j)] = Math.round(angles[rel(j)]);
31937
32529
  });
31938
32530
  const getAngle = function getAngle(a, vertex, b) {
31939
- const angle = GraphUtils.findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
32531
+ const angle = findAngle(coordsCopy[rel(a)], coordsCopy[rel(b)], coordsCopy[rel(vertex)]);
31940
32532
  return (angle + 360) % 360;
31941
32533
  };
31942
32534
  const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
@@ -31964,7 +32556,7 @@ function boundAndSnapToAngle(destinationPoint, {
31964
32556
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
31965
32557
 
31966
32558
  // Angle at the second vertex of the polygon
31967
- const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32559
+ const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
31968
32560
 
31969
32561
  // Uses the length of the side of the polygon (radial coordinate)
31970
32562
  // and the angle between the first and second sides of the
@@ -32014,7 +32606,7 @@ function boundAndSnapToSides(destinationPoint, {
32014
32606
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
32015
32607
 
32016
32608
  // Angle at the second vertex of the polygon
32017
- const outerAngle = GraphUtils.findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32609
+ const outerAngle = findAngle(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
32018
32610
 
32019
32611
  // Returns true if the points form a counter-clockwise turn;
32020
32612
  // a.k.a if the point is on the left or right of the polygon.
@@ -32083,65 +32675,6 @@ function setAtIndex(args) {
32083
32675
  return copy;
32084
32676
  }
32085
32677
 
32086
- const renderGraph = props => {
32087
- const {
32088
- state,
32089
- dispatch
32090
- } = props;
32091
- const {
32092
- type
32093
- } = state;
32094
- switch (type) {
32095
- case "segment":
32096
- return /*#__PURE__*/React.createElement(SegmentGraph, {
32097
- graphState: state,
32098
- dispatch: dispatch
32099
- });
32100
- case "linear-system":
32101
- return /*#__PURE__*/React.createElement(LinearSystemGraph, {
32102
- graphState: state,
32103
- dispatch: dispatch
32104
- });
32105
- case "linear":
32106
- return /*#__PURE__*/React.createElement(LinearGraph, {
32107
- graphState: state,
32108
- dispatch: dispatch
32109
- });
32110
- case "ray":
32111
- return /*#__PURE__*/React.createElement(RayGraph, {
32112
- graphState: state,
32113
- dispatch: dispatch
32114
- });
32115
- case "polygon":
32116
- return /*#__PURE__*/React.createElement(PolygonGraph, {
32117
- graphState: state,
32118
- dispatch: dispatch
32119
- });
32120
- case "point":
32121
- return /*#__PURE__*/React.createElement(PointGraph, {
32122
- graphState: state,
32123
- dispatch: dispatch
32124
- });
32125
- case "circle":
32126
- return /*#__PURE__*/React.createElement(CircleGraph, {
32127
- graphState: state,
32128
- dispatch: dispatch
32129
- });
32130
- case "quadratic":
32131
- return /*#__PURE__*/React.createElement(QuadraticGraph, {
32132
- graphState: state,
32133
- dispatch: dispatch
32134
- });
32135
- case "sinusoid":
32136
- return /*#__PURE__*/React.createElement(SinusoidGraph, {
32137
- graphState: state,
32138
- dispatch: dispatch
32139
- });
32140
- default:
32141
- return new UnreachableCaseError(type);
32142
- }
32143
- };
32144
-
32145
32678
  // Rather than be tightly bound to how data was structured in
32146
32679
  // the legacy interactive graph, this lets us store state
32147
32680
  // however we want and we just transform it before handing it off
@@ -32207,72 +32740,6 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
32207
32740
  dispatch: dispatch
32208
32741
  }));
32209
32742
  });
32210
- const MafsGraph = props => {
32211
- const {
32212
- state,
32213
- dispatch,
32214
- labels
32215
- } = props;
32216
- const [width, height] = props.box;
32217
- const tickStep = props.step;
32218
- return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
32219
- value: {
32220
- range: state.range,
32221
- snapStep: state.snapStep,
32222
- markings: props.markings,
32223
- tickStep: tickStep,
32224
- gridStep: props.gridStep,
32225
- showTooltips: !!props.showTooltips,
32226
- graphDimensionsInPixels: props.box,
32227
- width,
32228
- height,
32229
- labels
32230
- }
32231
- }, /*#__PURE__*/React.createElement(View, {
32232
- className: "mafs-graph",
32233
- style: {
32234
- width,
32235
- height,
32236
- position: "relative",
32237
- padding: "25px 25px 0 0",
32238
- boxSizing: "content-box",
32239
- marginLeft: "20px",
32240
- marginBottom: "20px"
32241
- }
32242
- }, /*#__PURE__*/React.createElement(LegacyGrid, {
32243
- box: props.box,
32244
- backgroundImage: props.backgroundImage
32245
- }), /*#__PURE__*/React.createElement(View, {
32246
- style: {
32247
- position: "absolute",
32248
- bottom: 0,
32249
- left: 0
32250
- }
32251
- }, props.markings === "graph" && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null), /*#__PURE__*/React.createElement(AxisTickLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
32252
- preserveAspectRatio: false,
32253
- viewBox: {
32254
- x: state.range[0],
32255
- y: state.range[1],
32256
- padding: 0
32257
- },
32258
- pan: false,
32259
- zoom: false,
32260
- width: width,
32261
- height: height
32262
- }, /*#__PURE__*/React.createElement(SvgDefs, null), /*#__PURE__*/React.createElement(Grid, {
32263
- tickStep: props.step,
32264
- gridStep: props.gridStep,
32265
- range: state.range,
32266
- containerSizeClass: props.containerSizeClass,
32267
- markings: props.markings
32268
- }), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
32269
- lockedFigures: props.lockedFigures,
32270
- range: state.range
32271
- }), "/* Protractor */", props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
32272
- state,
32273
- dispatch
32274
- })))));
32275
- };
32276
32743
 
32277
32744
  const {
32278
32745
  DeprecationMixin
@@ -33093,7 +33560,7 @@ class LegacyInteractiveGraph extends React.Component {
33093
33560
  // Solve for side by using the law of sines
33094
33561
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
33095
33562
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
33096
- const offset = polar(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
33563
+ const offset = polar$1(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
33097
33564
  return (_this$graphie5 = this.graphie) == null ? void 0 : _this$graphie5.addPoints(coords[rel(-1)], offset);
33098
33565
  }
33099
33566
  // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
@@ -33118,7 +33585,7 @@ class LegacyInteractiveGraph extends React.Component {
33118
33585
  const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
33119
33586
  const outerAngle = GraphUtils.findAngle(coords[rel(1)], coords[rel(-1)]);
33120
33587
  const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
33121
- const offset = polar(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
33588
+ const offset = polar$1(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
33122
33589
  return (_this$graphie6 = this.graphie) == null ? void 0 : _this$graphie6.addPoints(coords[rel(-1)], offset);
33123
33590
  }
33124
33591
  // Snap to grid (already done)
@@ -33779,11 +34246,9 @@ class InteractiveGraph extends React.Component {
33779
34246
  const radius = magnitude(vector(...coords));
33780
34247
 
33781
34248
  // Adjust the lower point by angleOffsetDeg degrees
33782
- // @ts-expect-error - TS2542 - Index signature in type 'readonly Coord[]' only permits reading.
33783
34249
  coords[0] = [coords[1][0] + radius * Math.cos(offset), coords[1][1] + radius * Math.sin(offset)];
33784
34250
  // Position the upper point angle radians from the
33785
34251
  // lower point
33786
- // @ts-expect-error - TS2542 - Index signature in type 'readonly Coord[]' only permits reading.
33787
34252
  coords[2] = [coords[1][0] + radius * Math.cos(angle + offset), coords[1][1] + radius * Math.sin(angle + offset)];
33788
34253
  return coords;
33789
34254
  }
@@ -41341,6 +41806,7 @@ const rules = {
41341
41806
  }, /*#__PURE__*/React.createElement("span", {
41342
41807
  style: SQUARE_LABEL_STYLE
41343
41808
  }, node.content)), /*#__PURE__*/React.createElement(AltText, {
41809
+ key: "alt-text",
41344
41810
  id: "questionMarker",
41345
41811
  number: node.content
41346
41812
  }), node.space ? "\u00A0" : null];
@@ -41365,6 +41831,7 @@ const rules = {
41365
41831
  }, /*#__PURE__*/React.createElement("span", {
41366
41832
  style: CIRCLE_LABEL_STYLE
41367
41833
  }, node.content)), /*#__PURE__*/React.createElement(AltText, {
41834
+ key: "alt-text",
41368
41835
  id: "circleMarker",
41369
41836
  number: node.content
41370
41837
  }), node.space ? "\u00A0" : null];
@@ -41388,6 +41855,7 @@ const rules = {
41388
41855
  className: "perseus-passage-bracket-label",
41389
41856
  "aria-hidden": "true"
41390
41857
  }, "[", node.content, "]"), /*#__PURE__*/React.createElement(AltText, {
41858
+ key: "alt-text",
41391
41859
  id: "sentenceMarker",
41392
41860
  number: node.content
41393
41861
  }), node.space ? "\u00A0" : null];
@@ -44396,7 +44864,7 @@ const init = function init(options) {
44396
44864
 
44397
44865
  // This file is processed by a Rollup plugin (replace) to inject the production
44398
44866
  const libName = "@khanacademy/perseus";
44399
- const libVersion = "24.2.0";
44867
+ const libVersion = "25.0.0";
44400
44868
  addLibraryVersionToPerseusDebug(libName, libVersion);
44401
44869
 
44402
44870
  const apiVersion = {
@@ -47397,5 +47865,5 @@ const EditorJsonify = {
47397
47865
  }
47398
47866
  };
47399
47867
 
47400
- export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Errors, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusError, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
47868
+ export { ApiOptions, ArrowPicker, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, ColorPicker, DashPicker, dependencies as Dependencies, EditorJsonify, ElementContainer, Expression, util as GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, ItemExtras, JiptParagraphs, KhanColors, KhanMath, context as LoadingContext, Log, Matrix$1 as MatrixWidget, MultiItems, NumericInput$1 as NumericInput, PerseusExpressionAnswerFormConsidered, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, QUESTION_WIDGETS, Radio, Renderer, ref as ServerItemRenderer, sizingUtils as SizingUtils, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, WidgetJsonifyDeprecated, widgets$1 as Widgets, addWidget, apiVersion, buildEmptyItemTreeForShape, buildMapper, components, constants, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAnswerFromUserInput, getAnswersFromWidgets, getCorrectAnswerForWidgetId, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetIdsFromContent, getWidgetIdsFromContentByType, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, globalConstants, globalStyles, iconPaths as icons, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, itemToTree, ItemVersion as itemVersion, libVersion, lockedFigureColors, lockedFigureFillStyles, mediaQueries, parsePerseusItem, plotterPlotTypes, preprocessTex, registerAllWidgetsForTesting, shapes, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
47401
47869
  //# sourceMappingURL=index.js.map