Skip to content

Commit

Permalink
Fix for Polygon crashing in editor!
Browse files Browse the repository at this point in the history
  • Loading branch information
catandthemachines committed Dec 11, 2024
1 parent 69b0209 commit 16f28fe
Showing 1 changed file with 165 additions and 118 deletions.
283 changes: 165 additions & 118 deletions packages/perseus/src/widgets/interactive-graphs/graphs/polygon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {TextLabel} from "./components/text-label";
import {useDraggable} from "./use-draggable";
import {pixelsToVectors, useTransformVectorsToPixels} from "./use-transform";

import type {Coord} from "../../../interactive2/types";
import type {CollinearTuple} from "../../../perseus-types";
import type {GraphConfig} from "../reducer/use-graph-config";
import type {
Dispatch,
InteractiveGraphElementSuite,
Expand All @@ -31,35 +33,52 @@ export function renderPolygonGraph(
}

type Props = MafsGraphProps<PolygonGraphState>;
type StatefulProps = MafsGraphProps<PolygonGraphState> & {
graphConfig: GraphConfig;
polygonRef: React.RefObject<SVGPolygonElement>;
pointsRef: React.MutableRefObject<(SVGElement | null)[]>;
lastMoveTimeRef: React.MutableRefObject<number>;
left: number;
top: number;
dragging: boolean;
points: Coord[];
constrain: (p: any) => any;
hovered: boolean;
setHovered: React.Dispatch<React.SetStateAction<boolean>>;
focusVisible: boolean;
setFocusVisible: React.Dispatch<React.SetStateAction<boolean>>;
};

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.
const [focusVisible, setFocusVisible] = React.useState(false);

const PolygonGraph = (props: Props) => {
const {dispatch} = props;
const {numSides, coords, snapStep, snapTo = "grid"} = props.graphState;
const graphConfig = useGraphConfig();

// Ref to implement the dragging behavior on a Limited/Closed Polygon.
const polygonRef = React.useRef<SVGPolygonElement>(null);
// Ref to manage dynamic focus for the points in Unlimited Polygon
const pointsRef = React.useRef<Array<SVGElement | null>>([]);
// Ref to manage the last move tim for a Limited Polygon.
const lastMoveTimeRef = React.useRef<number>(0);

// Dimensions to build the graph overlay for Unlimited Polgyon.
const {
coords,
showAngles,
showSides,
range,
snapStep,
snapTo = "grid",
} = props.graphState;
const {disableKeyboardInteraction} = useGraphConfig();
range: [x, y],
} = graphConfig;
const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);

// 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 ref = React.useRef<SVGPolygonElement>(null);
// Logic to build the dragging experience. Primarily used by Limited Polygon.
const dragReferencePoint = points[0];
const constrain = ["angles", "sides"].includes(snapTo)
? (p) => p
: (p) => snap(snapStep, p);

const {dragging} = useDraggable({
gestureTarget: ref,
gestureTarget: polygonRef,
point: dragReferencePoint,
onMove: (newPoint) => {
const delta = vec.sub(newPoint, dragReferencePoint);
Expand All @@ -68,7 +87,63 @@ const LimitedPolygonGraph = (props: Props) => {
constrainKeyboardMovement: constrain,
});

const lastMoveTime = React.useRef<number>(0);
// Hover/focus state for Limited Polgyon effects.
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);

// This useEffect is to handle the focus snapping for Unlimited Polygon.
React.useEffect(() => {
const focusedIndex = props.graphState.focusedPointIndex;
if (focusedIndex != null) {
pointsRef.current[focusedIndex]?.focus();
}
}, [props.graphState.focusedPointIndex, pointsRef]);

const statefulProps: StatefulProps = {
...props,
graphConfig,
polygonRef,
pointsRef,
lastMoveTimeRef,
left,
top,
dragging,
points,
constrain,
hovered,
setHovered,
focusVisible,
setFocusVisible,
};

return numSides === "unlimited"
? UnlimitedPolygonGraph(statefulProps)
: LimitedPolygonGraph(statefulProps);
};

const LimitedPolygonGraph = (statefulProps: StatefulProps) => {
const {
dispatch,
hovered,
setHovered,
focusVisible,
setFocusVisible,
graphConfig,
polygonRef,
lastMoveTimeRef,
dragging,
points,
constrain,
} = statefulProps;
const {
showAngles,
showSides,
range,
snapTo = "grid",
} = statefulProps.graphState;
const {disableKeyboardInteraction} = graphConfig;

const lines = getLines(points);

Expand Down Expand Up @@ -125,7 +200,7 @@ const LimitedPolygonGraph = (props: Props) => {
points={[...points]}
color="transparent"
svgPolygonProps={{
ref,
ref: polygonRef,
tabIndex: disableKeyboardInteraction ? -1 : 0,
strokeWidth: TARGET_SIZE,
style: {
Expand All @@ -137,14 +212,15 @@ const LimitedPolygonGraph = (props: Props) => {
// Required to remove line weighting when user clicks away
// from the focused polygon
onKeyDownCapture: () => {
setFocusVisible(hasFocusVisible(ref.current));
setFocusVisible(hasFocusVisible(polygonRef.current));
},
// Required for lines to darken on focus
onFocus: () =>
setFocusVisible(hasFocusVisible(ref.current)),
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(ref.current)),
onBlur: () =>
setFocusVisible(hasFocusVisible(polygonRef.current)),
className: "movable-polygon",
}}
/>
Expand All @@ -159,9 +235,9 @@ const LimitedPolygonGraph = (props: Props) => {
const targetFPS = 40;
const moveThresholdTime = 1000 / targetFPS;

if (now - lastMoveTime.current > moveThresholdTime) {
if (now - lastMoveTimeRef.current > moveThresholdTime) {
dispatch(actions.polygon.movePoint(i, destination));
lastMoveTime.current = now;
lastMoveTimeRef.current = now;
}
}}
/>
Expand All @@ -170,105 +246,84 @@ const LimitedPolygonGraph = (props: Props) => {
);
};

const UnlimitedPolygonGraph = (props: Props) => {
const {dispatch} = props;
const {coords, closedPolygon} = props.graphState;
const UnlimitedPolygonGraph = (statefulProps: StatefulProps) => {
const {dispatch, graphConfig, left, top, pointsRef, points} = statefulProps;
const {coords, closedPolygon} = statefulProps.graphState;

const graphConfig = useGraphConfig();
// If the polygon is closed, return a LimitedPolygon component.
if (closedPolygon) {
const closedPolygonProps = {...statefulProps, numSides: coords.length};
return <LimitedPolygonGraph {...closedPolygonProps} />;
}

const {
range: [x, y],
graphDimensionsInPixels,
} = graphConfig;
const {graphDimensionsInPixels} = graphConfig;

const widthPx = graphDimensionsInPixels[0];
const heightPx = graphDimensionsInPixels[1];

const [[left, top]] = useTransformVectorsToPixels([x[0], y[1]]);
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]];

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

if (closedPolygon) {
const closedPolygonProps = {...props, numSides: coords.length};
return <LimitedPolygonGraph {...closedPolygonProps} />;
} else {
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
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();
<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 x = event.clientX - elementRect.x;
const y = event.clientY - elementRect.y;

const graphCoordinates = pixelsToVectors(
[[x, y]],
graphConfig,
);
dispatch(actions.polygon.addPoint(graphCoordinates[0]));
const graphCoordinates = pixelsToVectors(
[[x, y]],
graphConfig,
);
dispatch(actions.polygon.addPoint(graphCoordinates[0]));
}}
/>
<Polyline
points={[...points]}
color="var(--movable-line-stroke-color)"
svgPolylineProps={{
strokeWidth: "var(--movable-line-stroke-weight)",
style: {fill: "transparent"},
}}
/>
{coords.map((point, i) => (
<MovablePoint
key={i}
point={point}
sequenceNumber={i + 1}
onMove={(destination) =>
dispatch(actions.polygon.movePoint(i, destination))
}
ref={(ref) => {
pointsRef.current[i] = ref;
}}
/>
<Polyline
points={[...points]}
color="var(--movable-line-stroke-color)"
svgPolylineProps={{
strokeWidth: "var(--movable-line-stroke-weight)",
style: {fill: "transparent"},
onFocus={() => {
dispatch(actions.polygon.focusPoint(i));
}}
/>
{props.graphState.coords.map((point, i) => (
<MovablePoint
key={i}
point={point}
sequenceNumber={i + 1}
onMove={(destination) =>
dispatch(actions.polygon.movePoint(i, destination))
onClick={() => {
// If the point being clicked is the first point and
// there's enough points to form a polygon (3 or more)
// Close the shape before setting focus.
if (i === 0 && coords.length >= 3) {
dispatch(actions.polygon.closePolygon());
}
ref={(ref) => {
pointRef.current[i] = ref;
}}
onFocus={() => {
dispatch(actions.polygon.focusPoint(i));
}}
onClick={() => {
// If the point being clicked is the first point and
// there's enough points to form a polygon (3 or more)
// Close the shape before setting focus.
if (
i === 0 &&
props.graphState.coords.length >= 3
) {
dispatch(actions.polygon.closePolygon());
}
dispatch(actions.polygon.clickPoint(i));
}}
/>
))}
</>
);
}
dispatch(actions.polygon.clickPoint(i));
}}
/>
))}
</>
);
};

function getLines(points: readonly vec.Vector2[]): CollinearTuple[] {
Expand All @@ -291,11 +346,3 @@ export const hasFocusVisible = (
return matches(":focus");
}
};

const PolygonGraph = (props: Props) => {
const numSides = props.graphState.numSides;

return numSides === "unlimited"
? UnlimitedPolygonGraph(props)
: LimitedPolygonGraph(props);
};

0 comments on commit 16f28fe

Please sign in to comment.