diff --git a/.changeset/gorgeous-boats-join.md b/.changeset/gorgeous-boats-join.md new file mode 100644 index 00000000000..e70ca3ef602 --- /dev/null +++ b/.changeset/gorgeous-boats-join.md @@ -0,0 +1,7 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-core": minor +"@khanacademy/perseus-editor": minor +--- + +Add `showAngles` option to polygon locked figure diff --git a/__docs__/sample-data.ts b/__docs__/sample-data.ts index 05ba30dccd4..d18d3c11efd 100644 --- a/__docs__/sample-data.ts +++ b/__docs__/sample-data.ts @@ -90,6 +90,7 @@ export const graphExample: PerseusRenderer = { [2, -3], ], showVertices: false, + showAngles: false, strokeStyle: "solid", }, ], diff --git a/packages/perseus-core/src/data-schema.ts b/packages/perseus-core/src/data-schema.ts index 74bded5eb3f..07a6afcab2d 100644 --- a/packages/perseus-core/src/data-schema.ts +++ b/packages/perseus-core/src/data-schema.ts @@ -1055,6 +1055,7 @@ export type LockedPolygonType = { points: Coord[]; color: LockedFigureColor; showVertices: boolean; + showAngles: boolean; fillStyle: LockedFigureFillType; strokeStyle: LockedLineStyle; weight: StrokeWeight; diff --git a/packages/perseus-core/src/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts b/packages/perseus-core/src/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts index c45fc3a6c43..70537deb545 100644 --- a/packages/perseus-core/src/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts +++ b/packages/perseus-core/src/parse-perseus-json/perseus-parsers/interactive-graph-widget.ts @@ -239,6 +239,7 @@ const parseLockedPolygonType = object({ points: array(pairOfNumbers), color: parseLockedFigureColor, showVertices: boolean, + showAngles: defaulted(boolean, () => false), fillStyle: parseLockedFigureFillType, strokeStyle: parseLockedLineStyle, weight: parseStrokeWeight, diff --git a/packages/perseus-core/src/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-regression.test.ts.snap b/packages/perseus-core/src/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-regression.test.ts.snap index 0d8ef1d4724..81584aaeafa 100644 --- a/packages/perseus-core/src/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-regression.test.ts.snap +++ b/packages/perseus-core/src/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-regression.test.ts.snap @@ -6752,6 +6752,7 @@ exports[`parseAndMigratePerseusItem given interactive-graph-locked-figures-missi -3, ], ], + "showAngles": false, "showVertices": false, "strokeStyle": "solid", "type": "polygon", @@ -7024,6 +7025,7 @@ exports[`parseAndMigratePerseusItem given interactive-graph-locked-figures-missi -3, ], ], + "showAngles": false, "showVertices": false, "strokeStyle": "solid", "type": "polygon", @@ -20239,6 +20241,14 @@ exports[`parseAndMigratePerseusItem given transformer-widget.ts returns the same } `; +exports[`parseAndMigratePerseusRenderer given basic-renderer.ts returns the same result as before 1`] = ` +{ + "content": "Hello from a renderer fixture.", + "images": {}, + "widgets": {}, +} +`; + exports[`parseAndMigrateUserInputMap given the data from categorizer.ts returns the same result as before 1`] = ` { "categorizer 1": { @@ -20822,11 +20832,3 @@ exports[`parseAndMigrateUserInputMap given the data from table.ts returns the sa ], } `; - -exports[`parseAndMigratePerseusRenderer given basic-renderer.ts returns the same result as before 1`] = ` -{ - "content": "Hello from a renderer fixture.", - "images": {}, - "widgets": {}, -} -`; diff --git a/packages/perseus-core/src/parse-perseus-json/regression-tests/item-data/interactive-graph-locked-figures-missing-weights.ts b/packages/perseus-core/src/parse-perseus-json/regression-tests/item-data/interactive-graph-locked-figures-missing-weights.ts index 0a0ddb4f9e0..63656191fa0 100644 --- a/packages/perseus-core/src/parse-perseus-json/regression-tests/item-data/interactive-graph-locked-figures-missing-weights.ts +++ b/packages/perseus-core/src/parse-perseus-json/regression-tests/item-data/interactive-graph-locked-figures-missing-weights.ts @@ -146,6 +146,7 @@ export default { ], color: "pink", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", labels: [], diff --git a/packages/perseus-core/src/utils/generators/interactive-graph-widget-generator.test.ts b/packages/perseus-core/src/utils/generators/interactive-graph-widget-generator.test.ts index 4c41d3d9e46..a495901edf0 100644 --- a/packages/perseus-core/src/utils/generators/interactive-graph-widget-generator.test.ts +++ b/packages/perseus-core/src/utils/generators/interactive-graph-widget-generator.test.ts @@ -1177,6 +1177,7 @@ describe("generateIGLockedPolygon", () => { ], color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: "medium", @@ -1194,6 +1195,7 @@ describe("generateIGLockedPolygon", () => { ], color: "blue", showVertices: true, + showAngles: true, fillStyle: "solid", strokeStyle: "dashed", weight: "medium", @@ -1219,6 +1221,7 @@ describe("generateIGLockedPolygon", () => { ], color: "blue", showVertices: true, + showAngles: true, fillStyle: "solid", strokeStyle: "dashed", weight: "medium", diff --git a/packages/perseus-core/src/utils/get-default-figure-for-type.test.ts b/packages/perseus-core/src/utils/get-default-figure-for-type.test.ts index a2a463ce4cf..764feff31ad 100644 --- a/packages/perseus-core/src/utils/get-default-figure-for-type.test.ts +++ b/packages/perseus-core/src/utils/get-default-figure-for-type.test.ts @@ -82,6 +82,7 @@ describe("getDefaultFigureForType", () => { ], color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: "medium", diff --git a/packages/perseus-core/src/utils/get-default-figure-for-type.ts b/packages/perseus-core/src/utils/get-default-figure-for-type.ts index e9c6d301329..b0a9ef80af4 100644 --- a/packages/perseus-core/src/utils/get-default-figure-for-type.ts +++ b/packages/perseus-core/src/utils/get-default-figure-for-type.ts @@ -84,6 +84,7 @@ export function getDefaultFigureForType(type: LockedFigureType): LockedFigure { ], color: DEFAULT_COLOR, showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: "medium", diff --git a/packages/perseus-editor/src/__testdata__/interactive-graph-question-builder.ts b/packages/perseus-editor/src/__testdata__/interactive-graph-question-builder.ts index 2b4659217d6..06274f91e76 100644 --- a/packages/perseus-editor/src/__testdata__/interactive-graph-question-builder.ts +++ b/packages/perseus-editor/src/__testdata__/interactive-graph-question-builder.ts @@ -481,6 +481,7 @@ class InteractiveGraphQuestionBuilder { options?: { color?: LockedFigureColor; showVertices?: boolean; + showAngles?: boolean; fillStyle?: LockedFigureFillType; strokeStyle?: LockedLineStyle; weight?: StrokeWeight; @@ -493,6 +494,7 @@ class InteractiveGraphQuestionBuilder { points: points, color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: options?.weight ?? "medium", diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx index 9d48c32a18f..7a548b63bff 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.test.tsx @@ -742,4 +742,25 @@ describe("LockedPolygonSettings", () => { }); }); }); + + test("calls onChangeProps when show angle measures switch is toggled", async () => { + // Arrange + const onChangeProps = jest.fn(); + render( + , + {wrapper: RenderStateRoot}, + ); + + // Act + const toggle = screen.getByRole("switch", { + name: "show angle measures", + }); + await userEvent.click(toggle); + + // Assert + expect(onChangeProps).toHaveBeenCalledWith({showAngles: true}); + }); }); diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx index 79e81febf89..275bc0d8b7f 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx @@ -55,6 +55,7 @@ const LockedPolygonSettings = (props: Props) => { points, color, showVertices, + showAngles, fillStyle, strokeStyle, weight, @@ -256,6 +257,16 @@ const LockedPolygonSettings = (props: Props) => { style={styles.spaceUnder} /> + {/* Show angle measures switch */} + + onChangeProps({showAngles: newValue}) + } + style={styles.spaceUnder} + /> + 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 7881397b37a..37a4971abcf 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 @@ -1225,6 +1225,7 @@ describe("InteractiveGraphQuestionBuilder", () => { ], color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: "medium", @@ -1264,6 +1265,7 @@ describe("InteractiveGraphQuestionBuilder", () => { ], color: "green", showVertices: true, + showAngles: false, fillStyle: "translucent", strokeStyle: "dashed", weight: "thin", @@ -1306,6 +1308,7 @@ describe("InteractiveGraphQuestionBuilder", () => { ], color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: "medium", 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 0306601876a..5b53630c9d6 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 @@ -510,6 +510,7 @@ class InteractiveGraphQuestionBuilder { options?: { color?: LockedFigureColor; showVertices?: boolean; + showAngles?: boolean; fillStyle?: LockedFigureFillType; strokeStyle?: LockedLineStyle; weight?: StrokeWeight; @@ -522,6 +523,7 @@ class InteractiveGraphQuestionBuilder { points: points, color: "grayH", showVertices: false, + showAngles: false, fillStyle: "none", strokeStyle: "solid", weight: options?.weight ?? "medium", diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.test.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.test.tsx new file mode 100644 index 00000000000..efd5df51af0 --- /dev/null +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.test.tsx @@ -0,0 +1,157 @@ +import {render, screen} from "@testing-library/react"; +import * as React from "react"; + +import * as Dependencies from "../../../dependencies"; +import {testDependencies} from "../../../testing/test-dependencies"; +import {MafsGraph} from "../mafs-graph"; +import {getBaseMafsGraphPropsForTests} from "../utils"; + +import type {NoneGraphState} from "../types"; +import type {LockedPolygonType} from "@khanacademy/perseus-core"; +import type {vec} from "mafs"; + +const baseMafsGraphProps = getBaseMafsGraphPropsForTests(); +const baseGraphState = { + type: "none", + range: [ + [-10, 10], + [-10, 10], + ], + hasBeenInteractedWith: false, + snapStep: [1, 1], +} satisfies NoneGraphState; + +const baseLockedPolygonProps = { + type: "polygon", + points: [ + [0, 2], + [-1, 0], + [1, 0], + ], + color: "grayH", + showVertices: false, + showAngles: false, + fillStyle: "none", + strokeStyle: "solid", + weight: "medium", + labels: [], +} satisfies LockedPolygonType; + +// Polygon that looks like a chevron, with a concave vertex on the left, +// drawn from the top left. +const concavePolygonClockwise = [ + [-7, 5], + [1, 5], + [6, 0], + [1, -5], + [-7, -5], + [-2, 0], // concave vertex +] satisfies vec.Vector2[]; + +describe("LockedPolygon", () => { + beforeEach(() => { + jest.spyOn(Dependencies, "getDependencies").mockReturnValue( + testDependencies, + ); + }); + + it("renders the polygon element", () => { + // Arrange, Act + const {container} = render( + , + ); + + // Assert + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + const lockedPolygon = container.querySelector(".locked-polygon"); + expect(lockedPolygon).toBeInTheDocument(); + }); + + it("does not show angle indicators when showAngles is false", () => { + // Arrange, Act + render( + , + ); + + // Assert + const angleIndicators = screen.queryAllByText(/°/); + expect(angleIndicators).toHaveLength(0); + }); + + it("shows correct angles for concave polygons when points are clockwise", () => { + // Arrange + + // Act - render with angles showing + render( + , + ); + + const angleIndicators = screen.getAllByText(/°/); + + // Assert + expect(angleIndicators).toHaveLength(concavePolygonClockwise.length); + // Checking angles in render order + expect(angleIndicators[0]).toHaveTextContent("45°"); + expect(angleIndicators[1]).toHaveTextContent("135°"); + expect(angleIndicators[2]).toHaveTextContent("90°"); + expect(angleIndicators[3]).toHaveTextContent("135°"); + expect(angleIndicators[4]).toHaveTextContent("45°"); + // Concave vertex, greater than 180 degrees + expect(angleIndicators[5]).toHaveTextContent("270°"); + }); + + it("should show correct angles for concave polygons when the points are counter-clockwise", () => { + // Arrange + + // Act - render with angles showing + render( + , + ); + + const angleIndicators = screen.getAllByText(/°/); + + // Assert + expect(angleIndicators).toHaveLength(concavePolygonClockwise.length); + // Concave vertex, greater than 180 degrees + expect(angleIndicators[0]).toHaveTextContent("270°"); + expect(angleIndicators[1]).toHaveTextContent("45°"); + expect(angleIndicators[2]).toHaveTextContent("135°"); + expect(angleIndicators[3]).toHaveTextContent("90°"); + expect(angleIndicators[4]).toHaveTextContent("135°"); + expect(angleIndicators[5]).toHaveTextContent("45°"); + }); +}); diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.tsx index eef5a0cdb4b..caaa0c60142 100644 --- a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-polygon.tsx @@ -1,3 +1,4 @@ +import {geometry} from "@khanacademy/kmath"; import { lockedFigureColors, lockedFigureFillStyles, @@ -6,14 +7,25 @@ import {semanticColor} from "@khanacademy/wonder-blocks-tokens"; import {Point, Polygon} from "mafs"; import * as React from "react"; +import {PolygonAngle} from "../graphs/components/angle-indicators"; import {X, Y} from "../math"; import {strokeWeights} from "./utils"; import type {LockedPolygonType} from "@khanacademy/perseus-core"; +const {clockwise} = geometry; + const LockedPolygon = (props: LockedPolygonType) => { - const {points, color, showVertices, fillStyle, strokeStyle, weight} = props; + const { + points, + color, + showVertices, + showAngles, + fillStyle, + strokeStyle, + weight, + } = props; const hasAria = !!props.ariaLabel; @@ -51,6 +63,24 @@ const LockedPolygon = (props: LockedPolygonType) => { color={lockedFigureColors[color]} /> ))} + {points.map((point, i) => { + const pt1 = points.at(i - 1); + const pt2 = points[(i + 1) % points.length]; + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!pt1 || !pt2) { + return null; + } + return ( + + ); + })} ); };