Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: POC of Unlimited Side Polygon #1749

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a361144
Adding some of the basic components.
catandthemachines Oct 11, 2024
74d6e60
More code, still need the points to hook into the graph... hmm..
catandthemachines Oct 11, 2024
be25198
Got the thing working... Dang somehow.
catandthemachines Oct 11, 2024
095772e
Removing console logs.
catandthemachines Oct 11, 2024
88147ad
adding more logic to polygon stuff.
catandthemachines Oct 11, 2024
c74143c
Adding the functionality to remove points.
catandthemachines Oct 15, 2024
a89bfab
Fixed linting errors.
catandthemachines Oct 15, 2024
f020371
Adding changeset.
catandthemachines Oct 15, 2024
1340477
Applyting union typing utility to clean up code.
catandthemachines Oct 16, 2024
2604468
Cleaning up unlimited state function and usage.
catandthemachines Oct 16, 2024
6b3a4d9
Adding the unlimited signifier to another section of code.
catandthemachines Oct 16, 2024
0057664
Cleaning up types file and utils,
catandthemachines Oct 16, 2024
b1e785d
Moving around properties.
catandthemachines Oct 16, 2024
f007884
Removing comment, asking Ben about it.
catandthemachines Oct 16, 2024
0517447
Fixing circular dependency with mafs-graph.tsx
catandthemachines Oct 16, 2024
acf5042
Adding note about redundant code.
catandthemachines Oct 16, 2024
a518a48
Adding interactive graph reducer tests.
catandthemachines Oct 16, 2024
f9cbacd
Fixed focus issues in basic implementation.
catandthemachines Oct 16, 2024
1c6ee07
Adding sections of tests that should be added to ensure brevity of te…
catandthemachines Oct 16, 2024
15e9cbd
Added two more
catandthemachines Oct 16, 2024
604add2
Fixing linting errors.
catandthemachines Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fifty-boxes-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@khanacademy/perseus-dev-ui": minor
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

Adding new interactive graph type "unlimited-points" for mafs.
1 change: 1 addition & 0 deletions dev/gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const questions: [PerseusRenderer, number][] = pairWithIndices([
interactiveGraph.polygonWithAnglesAndFourSidesQuestion,
interactiveGraph.polygonWithFourSidesSnappingQuestion,
interactiveGraph.polygonWithAnglesAndAnglesSnapToQuestion,
interactiveGraph.polygonWithUnlimitedSidesQuestion,
interactiveGraph.rayQuestion,
interactiveGraph.sinusoidQuestion,
interactiveGraph.noneQuestion,
Expand Down
3 changes: 0 additions & 3 deletions packages/perseus/src/widgets/interactive-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2374,9 +2374,6 @@ export function shouldUseMafs(
return Boolean(mafsFlags["point"]);
case "polygon":
if (graph.numSides === UNLIMITED) {
// TODO(benchristel): add a feature flag for the "unlimited"
// case once we've implemented polygon graphs with unlimited
// sides
return Boolean(mafsFlags["unlimited-polygon"]);
}
return Boolean(mafsFlags["polygon"]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from "react";

import {REMOVE_BUTTON_ID} from "../mafs-graph";
import {actions} from "../reducer/interactive-graph-action";
import useGraphConfig from "../reducer/use-graph-config";
import {REMOVE_BUTTON_ID} from "../utils";

import {MovablePoint} from "./components/movable-point";
import {
Expand Down
230 changes: 228 additions & 2 deletions packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import * as React from "react";
import {snap} from "../math";
import {actions} from "../reducer/interactive-graph-action";
import useGraphConfig from "../reducer/use-graph-config";
import {TARGET_SIZE} from "../utils";
import {REMOVE_BUTTON_ID, TARGET_SIZE} from "../utils";

import {PolygonAngle} from "./components/angle-indicators";
import {MovablePoint} from "./components/movable-point";
import {TextLabel} from "./components/text-label";
import {useDraggable} from "./use-draggable";
import {
pixelsToVectors,
useTransformDimensionsToPixels,
useTransformVectorsToPixels,
} from "./use-transform";

import type {CollinearTuple} from "../../../perseus-types";
import type {MafsGraphProps, PolygonGraphState} from "../types";

type Props = MafsGraphProps<PolygonGraphState>;

export const PolygonGraph = (props: Props) => {
export const LimitedPolygonGraph = (props: Props) => {
const [hovered, setHovered] = React.useState(false);
// This is more so required for the re-rendering that occurs when state
// updates; specifically with regard to line weighting and polygon focus.
Expand Down Expand Up @@ -153,6 +158,218 @@ export const PolygonGraph = (props: Props) => {
);
};

export const UnlimitedPolygonGraph = (props: Props) => {
const [hovered, setHovered] = React.useState(false);
// This is more so required for the re-rendering that occurs when state
// updates; specifically with regard to line weighting and polygon focus.
const [focusVisible, setFocusVisible] = React.useState(false);

const {dispatch} = props;
const {
coords,
showAngles,
showSides,
range,
snapStep,
snapTo = "grid",
} = props.graphState;

const graphState = useGraphConfig();

// TODO [catjohnson]: Explore abstracting this code as it is similar to point.tsx
// and hopefully we can cut down ont the unlimited graph redundancy.
const {
range: [[minX, maxX], [minY, maxY]],
disableKeyboardInteraction,
} = graphState;

const width = maxX - minX;
const height = maxY - minY;
const [[widthPx, heightPx]] = useTransformDimensionsToPixels([
width,
height,
]);
const [[left, top]] = useTransformVectorsToPixels([minX, maxY]);
const pointRef = React.useRef<Array<SVGElement | null>>([]);

// TODO(benchristel): can the default set of points be removed here? I don't
// think coords can be null.
const points = coords ?? [[0, 0]];
const polygonRef = React.useRef<SVGPolygonElement>(null);
const dragReferencePoint = points[0];
const constrain = ["angles", "sides"].includes(snapTo)
? (p) => p
: (p) => snap(snapStep, p);
const {dragging} = useDraggable({
gestureTarget: polygonRef,
point: dragReferencePoint,
onMove: (newPoint) => {
const delta = vec.sub(newPoint, dragReferencePoint);
dispatch(actions.polygon.moveAll(delta));
},
constrainKeyboardMovement: constrain,
});

const lines = getLines(points);
React.useEffect(() => {
const focusedIndex = props.graphState.focusedPointIndex;
if (focusedIndex != null) {
pointRef.current[focusedIndex]?.focus();
}
}, [props.graphState.focusedPointIndex, pointRef]);

return (
<>
{/* This rect is here to grab clicks so that new points can be added */}
{/* It's important because it stops mouse events from propogating
when dragging a points around */}
<rect
style={{
fill: "rgba(0,0,0,0)",
cursor: "crosshair",
}}
width={widthPx}
height={heightPx}
x={left}
y={top}
onClick={(event) => {
const elementRect =
event.currentTarget.getBoundingClientRect();

const x = event.clientX - elementRect.x;
const y = event.clientY - elementRect.y;

const graphCoordinates = pixelsToVectors(
[[x, y]],
graphState,
);
dispatch(actions.polygon.addPoint(graphCoordinates[0]));
}}
/>
{/**
* TODO [catjohnson]: Will need to conditionally render then once a full polygon is created
* And handle when someone wants to remove the polygon connection.
*/}
<Polygon
points={[...points]}
color="var(--movable-line-stroke-color)"
svgPolygonProps={{
strokeWidth: focusVisible
? "var(--movable-line-stroke-weight-active)"
: "var(--movable-line-stroke-weight)",
style: {fill: "transparent"},
}}
/>
{props.graphState.coords.map((point, i) => {
const pt1 = points.at(i - 1);
const pt2 = points[(i + 1) % points.length];
if (!pt1 || !pt2) {
return null;
}
return (
<PolygonAngle
key={"angle-" + i}
centerPoint={point}
endPoints={[pt1, pt2]}
range={range}
polygonLines={lines}
showAngles={!!showAngles}
snapTo={snapTo}
/>
);
})}
{showSides &&
lines.map(([start, end], i) => {
const [x, y] = vec.midpoint(start, end);
const length = parseFloat(
vec
.dist(start, end)
.toFixed(snapTo === "sides" ? 0 : 1),
);
return (
<TextLabel key={"side-" + i} x={x} y={y}>
{!Number.isInteger(length) && "≈ "}
{length}
</TextLabel>
);
})}
{/**
* This transparent svg creates a nice big click/touch target,
* since the polygon itself can be made smaller than the spec.
*/}
{/**
* Will likely want to conditionally render then once a full polygon is created
* And handle when someone wants to remove the polygon connection?
*/}
<Polygon
points={[...points]}
color="transparent"
svgPolygonProps={{
ref: polygonRef,
tabIndex: disableKeyboardInteraction ? -1 : 0,
strokeWidth: TARGET_SIZE,
style: {
cursor: dragging ? "grabbing" : "grab",
fill: hovered ? "var(--mafs-blue)" : "transparent",
},
onMouseEnter: () => setHovered(true),
onMouseLeave: () => setHovered(false),
// Required to remove line weighting when user clicks away
// from the focused polygon
onKeyDownCapture: () => {
setFocusVisible(hasFocusVisible(polygonRef.current));
},
// Required for lines to darken on focus
onFocus: () =>
setFocusVisible(hasFocusVisible(polygonRef.current)),
// Required for line weighting to update on blur. Without this,
// the user has to hover over the shape for it to update
onBlur: () =>
setFocusVisible(hasFocusVisible(polygonRef.current)),
className: "movable-polygon",
}}
/>
{props.graphState.coords.map((point, i) => (
<MovablePoint
key={i}
point={point}
onMove={(destination) =>
dispatch(actions.polygon.movePoint(i, destination))
}
ref={(ref) => {
pointRef.current[i] = ref;
}}
onFocusChange={(event, isFocused) => {
if (isFocused) {
dispatch(actions.polygon.focusPoint(i));
} else {
if (event.relatedTarget?.id === REMOVE_BUTTON_ID) {
return;
// This is an optimization: If the next target
// is a point then don't blur because it casues
// the remove button to get taken off the page
// and then put back on The new point will
// receive focus and set the correct state in
// the reducer
} else if (
event.relatedTarget?.classList.contains(
"movable-point",
)
) {
return;
}
dispatch(actions.polygon.blurPoint());
}
}}
onClick={() => {
dispatch(actions.polygon.clickPoint(i));
}}
/>
))}
</>
);
};

function getLines(points: readonly vec.Vector2[]): CollinearTuple[] {
return points.map((point, i) => {
const next = points[(i + 1) % points.length];
Expand All @@ -173,3 +390,12 @@ export const hasFocusVisible = (
return matches(":focus");
}
};

export const PolygonGraph = (props: Props) => {
const numSides = props.graphState.numSides;
if (numSides === "unlimited") {
return UnlimitedPolygonGraph(props);
}

return LimitedPolygonGraph(props);
};
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,21 @@ export const polygonWithFourSidesSnappingQuestion: PerseusRenderer =
})
.build();

export const polygonWithUnlimitedSidesQuestion: PerseusRenderer =
interactiveGraphQuestionBuilder()
.withContent(
"**Example of unlimited polygon sides** \n[[\u2603 interactive-graph 1]] \n",
)
.withPolygon("grid", {
numSides: "unlimited",
coords: [
[0, 0],
[-2.5, 0],
[-1, 0],
],
})
.build();

export const polygonQuestionDefaultCorrect: PerseusRenderer =
interactiveGraphQuestionBuilder().withPolygon("grid").build();

Expand Down
Loading