diff --git a/.changeset/metal-apricots-brake.md b/.changeset/metal-apricots-brake.md
new file mode 100644
index 0000000000..5a96848802
--- /dev/null
+++ b/.changeset/metal-apricots-brake.md
@@ -0,0 +1,6 @@
+---
+"@khanacademy/perseus": minor
+"@khanacademy/perseus-editor": minor
+---
+
+[Interactive Graph] View a locked polygon
diff --git a/packages/perseus-editor/src/components/__tests__/util.test.ts b/packages/perseus-editor/src/components/__tests__/util.test.ts
index 54a2642f79..fe10a6551f 100644
--- a/packages/perseus-editor/src/components/__tests__/util.test.ts
+++ b/packages/perseus-editor/src/components/__tests__/util.test.ts
@@ -37,6 +37,18 @@ describe("getDefaultFigureForType", () => {
});
});
+ test("should return a vector with default values", () => {
+ const figure = getDefaultFigureForType("vector");
+ expect(figure).toEqual({
+ type: "vector",
+ points: [
+ [0, 0],
+ [2, 2],
+ ],
+ color: "grayH",
+ });
+ });
+
test("should return an ellipse with default values", () => {
const figure = getDefaultFigureForType("ellipse");
expect(figure).toEqual({
@@ -50,15 +62,19 @@ describe("getDefaultFigureForType", () => {
});
});
- test("should return a vector with default values", () => {
- const figure = getDefaultFigureForType("vector");
+ test("should return a polygon with default values", () => {
+ const figure = getDefaultFigureForType("polygon");
expect(figure).toEqual({
- type: "vector",
+ type: "polygon",
points: [
- [0, 0],
- [2, 2],
+ [0, 2],
+ [-1, 0],
+ [1, 0],
],
color: "grayH",
+ showVertices: false,
+ fillStyle: "none",
+ strokeStyle: "solid",
});
});
});
diff --git a/packages/perseus-editor/src/components/ellipse-swatch.tsx b/packages/perseus-editor/src/components/ellipse-swatch.tsx
index 6808c74556..3218ee7e82 100644
--- a/packages/perseus-editor/src/components/ellipse-swatch.tsx
+++ b/packages/perseus-editor/src/components/ellipse-swatch.tsx
@@ -1,7 +1,4 @@
-import {
- lockedFigureColors,
- lockedEllipseFillStyles,
-} from "@khanacademy/perseus";
+import {lockedFigureColors, lockedFigureFillStyles} from "@khanacademy/perseus";
import {View} from "@khanacademy/wonder-blocks-core";
import {color as wbColor, spacing} from "@khanacademy/wonder-blocks-tokens";
import {StyleSheet} from "aphrodite";
@@ -9,12 +6,12 @@ import * as React from "react";
import type {
LockedFigureColor,
- LockedEllipseFillType,
+ LockedFigureFillType,
} from "@khanacademy/perseus";
type Props = {
color: LockedFigureColor;
- fillStyle: LockedEllipseFillType;
+ fillStyle: LockedFigureFillType;
strokeStyle: "solid" | "dashed";
};
@@ -36,7 +33,7 @@ const EllipseSwatch = (props: Props) => {
styles.innerCircle,
{
backgroundColor: lockedFigureColors[color],
- opacity: lockedEllipseFillStyles[fillStyle],
+ opacity: lockedFigureFillStyles[fillStyle],
},
]}
/>
diff --git a/packages/perseus-editor/src/components/locked-ellipse-settings.tsx b/packages/perseus-editor/src/components/locked-ellipse-settings.tsx
index f620167815..fee8933a8a 100644
--- a/packages/perseus-editor/src/components/locked-ellipse-settings.tsx
+++ b/packages/perseus-editor/src/components/locked-ellipse-settings.tsx
@@ -1,4 +1,4 @@
-import {components, lockedEllipseFillStyles} from "@khanacademy/perseus";
+import {components, lockedFigureFillStyles} from "@khanacademy/perseus";
import {View} from "@khanacademy/wonder-blocks-core";
import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
import {Strut} from "@khanacademy/wonder-blocks-layout";
@@ -17,7 +17,7 @@ import LockedFigureSettingsActions from "./locked-figure-settings-actions";
import type {AccordionProps} from "./locked-figure-settings";
import type {
Coord,
- LockedEllipseFillType,
+ LockedFigureFillType,
LockedEllipseType,
LockedFigureColor,
} from "@khanacademy/perseus";
@@ -118,13 +118,13 @@ const LockedEllipseSettings = (props: Props) => {
+ onChange={(value: LockedFigureFillType) =>
onChangeProps({fillStyle: value})
}
// Placeholder is required, but never gets used.
placeholder=""
>
- {Object.keys(lockedEllipseFillStyles).map((option) => (
+ {Object.keys(lockedFigureFillStyles).map((option) => (
{
return (
{figures?.map((figure, index) => {
+ if (figure.type === "polygon") {
+ // TODO(LEMS-1943): Implement locked polygon settings.
+ // Remove this block once locked polygon settings are
+ // implemented.
+ return;
+ }
+
return (
= {
export type LockedFigure =
| LockedPointType
| LockedLineType
+ | LockedVectorType
| LockedEllipseType
- | LockedVectorType;
+ | LockedPolygonType;
export type LockedFigureType = LockedFigure["type"];
export type LockedPointType = {
@@ -698,8 +699,14 @@ export type LockedLineType = {
showPoint2: boolean;
};
-export type LockedEllipseFillType = "none" | "solid" | "translucent";
-export const lockedEllipseFillStyles: Record = {
+export type LockedVectorType = {
+ type: "vector";
+ points: [tail: Coord, tip: Coord];
+ color: LockedFigureColor;
+};
+
+export type LockedFigureFillType = "none" | "solid" | "translucent";
+export const lockedFigureFillStyles: Record = {
none: 0,
solid: 1,
translucent: 0.4,
@@ -711,14 +718,17 @@ export type LockedEllipseType = {
radius: [x: number, y: number];
angle: number;
color: LockedFigureColor;
- fillStyle: LockedEllipseFillType;
+ fillStyle: LockedFigureFillType;
strokeStyle: "solid" | "dashed";
};
-export type LockedVectorType = {
- type: "vector";
- points: [tail: Coord, tip: Coord];
+export type LockedPolygonType = {
+ type: "polygon";
+ points: ReadonlyArray;
color: LockedFigureColor;
+ showVertices: boolean;
+ fillStyle: LockedFigureFillType;
+ strokeStyle: "solid" | "dashed";
};
export type PerseusGraphType =
diff --git a/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx b/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx
index ac89e6471c..85c5ff2fc1 100644
--- a/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx
+++ b/packages/perseus/src/widgets/__stories__/interactive-graph.stories.tsx
@@ -20,6 +20,7 @@ import {
sinusoidQuestion,
segmentWithLockedEllipses,
segmentWithLockedVectors,
+ segmentWithLockedPolygons,
} from "../__testdata__/interactive-graph.testdata";
export default {
@@ -117,6 +118,10 @@ export const AllLockedRays = (args: StoryArgs): React.ReactElement => (
/>
);
+export const LockedVector = (args: StoryArgs): React.ReactElement => (
+
+);
+
export const LockedEllipse = (args: StoryArgs): React.ReactElement => (
(
/>
);
-export const LockedVector = (args: StoryArgs): React.ReactElement => (
-
+export const LockedPolygon = (args: StoryArgs): React.ReactElement => (
+
);
export const Sinusoid = (args: StoryArgs): React.ReactElement => (
diff --git a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts
index 6a6217efd7..1dbbbbf466 100644
--- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts
+++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts
@@ -2089,6 +2089,12 @@ export const segmentWithAllLockedRayVariations: PerseusRenderer = {
},
};
+export const segmentWithLockedVectors: PerseusRenderer =
+ interactiveGraphQuestionBuilder()
+ .addLockedVector([0, 0], [2, 2])
+ .addLockedVector([2, 2], [-2, 4], "green")
+ .build();
+
export const segmentWithLockedEllipses: PerseusRenderer =
interactiveGraphQuestionBuilder()
.addLockedEllipse([0, 0], [5, 5])
@@ -2106,10 +2112,42 @@ export const segmentWithLockedEllipses: PerseusRenderer =
})
.build();
-export const segmentWithLockedVectors: PerseusRenderer =
+export const segmentWithLockedPolygons: PerseusRenderer =
interactiveGraphQuestionBuilder()
- .addLockedVector([0, 0], [2, 2])
- .addLockedVector([2, 2], [-2, 4], "green")
+ .addLockedPolygon([
+ [-3, 4],
+ [-5, 1],
+ [-1, 1],
+ ])
+ .addLockedPolygon(
+ [
+ [1, 4],
+ [4, 4],
+ [4, 1],
+ [1, 1],
+ ],
+ {
+ color: "green",
+ showVertices: true,
+ fillStyle: "translucent",
+ strokeStyle: "dashed",
+ },
+ )
+ .addLockedPolygon(
+ [
+ [0, -1],
+ [-2, -3],
+ [-1, -5],
+ [1, -5],
+ [2, -3],
+ ],
+ {
+ color: "purple",
+ showVertices: false,
+ fillStyle: "solid",
+ strokeStyle: "solid",
+ },
+ )
.build();
export const segmentWithLockedFigures: PerseusRenderer =
diff --git a/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts b/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts
index 3c263f3c2a..7e4e1199dc 100644
--- a/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts
+++ b/packages/perseus/src/widgets/__tests__/interactive-graph.test.ts
@@ -31,6 +31,7 @@ import {
segmentWithLockedLineQuestion,
segmentWithLockedPointsQuestion,
segmentWithLockedPointsWithColorQuestion,
+ segmentWithLockedPolygons,
segmentWithLockedVectors,
sinusoidQuestionWithDefaultCorrect,
} from "../__testdata__/interactive-graph.testdata";
@@ -538,6 +539,54 @@ describe("locked layer", () => {
});
});
+ test("should render locked vectors", async () => {
+ // Arrange
+ const {container} = renderQuestion(segmentWithLockedVectors, {
+ flags: {
+ mafs: {
+ segment: true,
+ },
+ },
+ });
+
+ // Act
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+ const vectors = container.querySelectorAll(".locked-vector");
+
+ // Assert
+ expect(vectors).toHaveLength(2);
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+ let vector = vectors[0].children[0];
+ expect(vector).toHaveStyle({
+ "stroke-width": "2",
+ stroke: lockedFigureColors["grayH"],
+ });
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+ let arrowheads = vector.querySelectorAll(
+ ".interactive-graph-arrowhead",
+ );
+ expect(arrowheads).toHaveLength(1);
+ // Arrowhead should be at the end (tip) of the vector, and rotated
+ expect(arrowheads[0]).toHaveAttribute(
+ "transform",
+ "translate(40 -40) rotate(-45)",
+ );
+
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+ vector = vectors[1].children[0];
+ expect(vector).toHaveStyle({
+ "stroke-width": "2",
+ stroke: lockedFigureColors["green"],
+ });
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
+ arrowheads = vector.querySelectorAll(".interactive-graph-arrowhead");
+ expect(arrowheads).toHaveLength(1);
+ expect(arrowheads[0]).toHaveAttribute(
+ "transform",
+ "translate(-40 -80) rotate(-153.43494882292202)",
+ );
+ });
+
test("should render locked ellipses", async () => {
// Arrange
const {container} = renderQuestion(segmentWithLockedEllipses, {
@@ -568,9 +617,9 @@ describe("locked layer", () => {
});
});
- test("should render locked vectors", async () => {
+ test("should render locked polygons with style", async () => {
// Arrange
- const {container} = renderQuestion(segmentWithLockedVectors, {
+ const {container} = renderQuestion(segmentWithLockedPolygons, {
flags: {
mafs: {
segment: true,
@@ -580,39 +629,56 @@ describe("locked layer", () => {
// Act
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
- const vectors = container.querySelectorAll(".locked-vector");
+ const polygons = container.querySelectorAll(".locked-polygon polygon");
// Assert
- expect(vectors).toHaveLength(2);
- // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
- let vector = vectors[0].children[0];
- expect(vector).toHaveStyle({
- "stroke-width": "2",
+ expect(polygons).toHaveLength(3);
+ expect(polygons[0]).toHaveStyle({
+ "fill-opacity": "0",
stroke: lockedFigureColors["grayH"],
});
- // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
- let arrowheads = vector.querySelectorAll(
- ".interactive-graph-arrowhead",
- );
- expect(arrowheads).toHaveLength(1);
- // Arrowhead should be at the end (tip) of the vector, and rotated
- expect(arrowheads[0]).toHaveAttribute(
- "transform",
- "translate(40 -40) rotate(-45)",
- );
-
- // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
- vector = vectors[1].children[0];
- expect(vector).toHaveStyle({
- "stroke-width": "2",
+ expect(polygons[1]).toHaveStyle({
+ "fill-opacity": "0.4",
stroke: lockedFigureColors["green"],
});
+ expect(polygons[2]).toHaveStyle({
+ "fill-opacity": "1",
+ stroke: lockedFigureColors["purple"],
+ });
+ });
+
+ test("should render vertices of locked polygons with showVertices", async () => {
+ // Arrange
+ const {container} = renderQuestion(segmentWithLockedPolygons, {
+ flags: {
+ mafs: {
+ segment: true,
+ },
+ },
+ });
+
+ // Act
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
- arrowheads = vector.querySelectorAll(".interactive-graph-arrowhead");
- expect(arrowheads).toHaveLength(1);
- expect(arrowheads[0]).toHaveAttribute(
- "transform",
- "translate(-40 -80) rotate(-153.43494882292202)",
+ const polygonVertices = container.querySelectorAll(
+ ".locked-polygon circle",
);
+
+ // Assert
+ // There should be 4 vertices on the square polygon
+ expect(polygonVertices).toHaveLength(4);
+
+ // The square polygon is green
+ expect(polygonVertices[0]).toHaveStyle({
+ fill: lockedFigureColors["green"],
+ });
+ expect(polygonVertices[1]).toHaveStyle({
+ fill: lockedFigureColors["green"],
+ });
+ expect(polygonVertices[2]).toHaveStyle({
+ fill: lockedFigureColors["green"],
+ });
+ expect(polygonVertices[3]).toHaveStyle({
+ fill: lockedFigureColors["green"],
+ });
});
});
diff --git a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx
index adf284eb42..9df183a77f 100644
--- a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx
+++ b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx
@@ -4,6 +4,7 @@ import * as React from "react";
import LockedEllipse from "./locked-ellipse";
import LockedLine from "./locked-line";
import LockedPoint from "./locked-point";
+import LockedPolygon from "./locked-polygon";
import LockedVector from "./locked-vector";
import type {LockedFigure} from "../../perseus-types";
@@ -32,6 +33,10 @@ const GraphLockedLayer = (props: Props) => {
{...figure}
/>
);
+ case "vector":
+ return (
+
+ );
case "ellipse":
return (
{
{...figure}
/>
);
- case "vector":
+ case "polygon":
return (
-
+
);
default:
/**
diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts
index 48b88f9d8d..755867bc14 100644
--- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts
+++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts
@@ -202,6 +202,42 @@ describe("InteractiveGraphQuestionBuilder", () => {
]);
});
+ it("adds a locked vector", () => {
+ const question: PerseusRenderer = interactiveGraphQuestionBuilder()
+ .addLockedVector([1, 2], [3, 4])
+ .build();
+ const graph = question.widgets["interactive-graph 1"];
+
+ expect(graph.options.lockedFigures).toEqual([
+ {
+ type: "vector",
+ points: [
+ [1, 2],
+ [3, 4],
+ ],
+ color: "grayH",
+ },
+ ]);
+ });
+
+ it("adds a locked vector with a specified color", () => {
+ const question: PerseusRenderer = interactiveGraphQuestionBuilder()
+ .addLockedVector([1, 2], [3, 4], "green")
+ .build();
+ const graph = question.widgets["interactive-graph 1"];
+
+ expect(graph.options.lockedFigures).toEqual([
+ {
+ type: "vector",
+ points: [
+ [1, 2],
+ [3, 4],
+ ],
+ color: "green",
+ },
+ ]);
+ });
+
it("adds a locked ellipse", () => {
const question: PerseusRenderer = interactiveGraphQuestionBuilder()
.addLockedEllipse([1, 2], [3, 3])
@@ -245,38 +281,62 @@ describe("InteractiveGraphQuestionBuilder", () => {
]);
});
- it("adds a locked vector", () => {
+ it("adds a locked polygon", () => {
const question: PerseusRenderer = interactiveGraphQuestionBuilder()
- .addLockedVector([1, 2], [3, 4])
+ .addLockedPolygon([
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ])
.build();
const graph = question.widgets["interactive-graph 1"];
expect(graph.options.lockedFigures).toEqual([
{
- type: "vector",
+ type: "polygon",
points: [
[1, 2],
[3, 4],
+ [5, 6],
],
color: "grayH",
+ showVertices: false,
+ fillStyle: "none",
+ strokeStyle: "solid",
},
]);
});
- it("adds a locked vector with a specified color", () => {
+ it("adds a locked polygon with options", () => {
const question: PerseusRenderer = interactiveGraphQuestionBuilder()
- .addLockedVector([1, 2], [3, 4], "green")
+ .addLockedPolygon(
+ [
+ [1, 2],
+ [3, 4],
+ [5, 6],
+ ],
+ {
+ color: "green",
+ showVertices: true,
+ fillStyle: "translucent",
+ strokeStyle: "dashed",
+ },
+ )
.build();
const graph = question.widgets["interactive-graph 1"];
expect(graph.options.lockedFigures).toEqual([
{
- type: "vector",
+ type: "polygon",
points: [
[1, 2],
[3, 4],
+ [5, 6],
],
color: "green",
+ showVertices: true,
+ fillStyle: "translucent",
+ strokeStyle: "dashed",
},
]);
});
diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts
index 9b443640f8..78022194b3 100644
--- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts
+++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts
@@ -1,5 +1,5 @@
import type {
- LockedEllipseFillType,
+ LockedFigureFillType,
LockedEllipseType,
LockedFigure,
LockedFigureColor,
@@ -8,6 +8,7 @@ import type {
LockedVectorType,
PerseusGraphType,
PerseusRenderer,
+ LockedPolygonType,
} from "../../perseus-types";
import type {Interval, vec} from "mafs";
@@ -140,13 +141,27 @@ class InteractiveGraphQuestionBuilder {
return this;
}
+ addLockedVector(
+ tail: vec.Vector2,
+ tip: vec.Vector2,
+ color?: LockedFigureColor,
+ ): InteractiveGraphQuestionBuilder {
+ const vector: LockedVectorType = {
+ type: "vector",
+ color: color ?? "grayH",
+ points: [tail, tip],
+ };
+ this.addLockedFigure(vector);
+ return this;
+ }
+
addLockedEllipse(
center: vec.Vector2,
radius: [x: number, y: number],
options?: {
angle?: number;
color?: LockedFigureColor;
- fillStyle?: LockedEllipseFillType;
+ fillStyle?: LockedFigureFillType;
strokeStyle?: "solid" | "dashed";
},
): InteractiveGraphQuestionBuilder {
@@ -165,17 +180,26 @@ class InteractiveGraphQuestionBuilder {
return this;
}
- addLockedVector(
- tail: vec.Vector2,
- tip: vec.Vector2,
- color?: LockedFigureColor,
+ addLockedPolygon(
+ points: vec.Vector2[],
+ options?: {
+ color?: LockedFigureColor;
+ showVertices?: boolean;
+ fillStyle?: LockedFigureFillType;
+ strokeStyle?: "solid" | "dashed";
+ },
): InteractiveGraphQuestionBuilder {
- const vector: LockedVectorType = {
- type: "vector",
- color: color ?? "grayH",
- points: [tail, tip],
+ const polygon: LockedPolygonType = {
+ type: "polygon",
+ points: points,
+ color: "grayH",
+ showVertices: false,
+ fillStyle: "none",
+ strokeStyle: "solid",
+ ...options,
};
- this.addLockedFigure(vector);
+
+ this.addLockedFigure(polygon);
return this;
}
diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-ellipse.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-ellipse.tsx
index 109a7d6c53..4e3756421e 100644
--- a/packages/perseus/src/widgets/interactive-graphs/locked-ellipse.tsx
+++ b/packages/perseus/src/widgets/interactive-graphs/locked-ellipse.tsx
@@ -2,7 +2,7 @@ import {Ellipse} from "mafs";
import * as React from "react";
import {
- lockedEllipseFillStyles,
+ lockedFigureFillStyles,
lockedFigureColors,
type LockedEllipseType,
} from "../../perseus-types";
@@ -15,7 +15,7 @@ const LockedEllipse = (props: LockedEllipseType) => {
center={center}
radius={radius}
angle={angle}
- fillOpacity={lockedEllipseFillStyles[fillStyle]}
+ fillOpacity={lockedFigureFillStyles[fillStyle]}
strokeStyle={strokeStyle}
color={lockedFigureColors[color]}
/>
diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-polygon.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-polygon.tsx
new file mode 100644
index 0000000000..d4735965b9
--- /dev/null
+++ b/packages/perseus/src/widgets/interactive-graphs/locked-polygon.tsx
@@ -0,0 +1,32 @@
+import {Point, Polygon} from "mafs";
+import * as React from "react";
+
+import {lockedFigureColors, lockedFigureFillStyles} from "../../perseus-types";
+
+import type {LockedPolygonType} from "../../perseus-types";
+
+const LockedPolygon = (props: LockedPolygonType) => {
+ const {points, color, showVertices, fillStyle, strokeStyle} = props;
+
+ return (
+
+
+ {showVertices &&
+ points.map((point, index) => (
+
+ ))}
+
+ );
+};
+
+export default LockedPolygon;