@khanacademy/perseus 87.1.0 → 87.1.1

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.
package/dist/es/index.js CHANGED
@@ -21,7 +21,7 @@ import * as ReactDOM from 'react-dom';
21
21
  import ReactDOM__default, { createPortal } from 'react-dom';
22
22
  import _, { debounce as debounce$1 } from 'underscore';
23
23
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
24
- import { point, KhanMath, number, vector as vector$3, geometry, angles, coefficients } from '@khanacademy/kmath';
24
+ import { point, KhanMath, number, vector as vector$3, geometry, angles, coefficients, line } from '@khanacademy/kmath';
25
25
  import { entries, UnreachableCaseError, isTruthy, keys } from '@khanacademy/wonder-stuff-core';
26
26
  import $ from 'jquery';
27
27
  import { ModalDialog, ModalPanel, ModalLauncher, FlexibleDialog } from '@khanacademy/wonder-blocks-modal';
@@ -1986,7 +1986,7 @@ const{magnitude: magnitude$1,vector: vector$1}=geometry;function initializeGraph
1986
1986
 
1987
1987
  const{getAngleFromVertex,getClockwiseAngle,polar}=angles;const{angleMeasures,ccw,lawOfCosines,magnitude,polygonSidesIntersect,reverseVector,sign,vector}=geometry;const{getAbsoluteValueCoefficients,getLogarithmCoefficients,getExponentialCoefficients,getQuadraticCoefficients}=coefficients;const minDistanceBetweenAngleVertexAndSidePoint=2;function interactiveGraphReducer(state,action){switch(action.type){case REINITIALIZE:return initializeGraphState(action.params);case MOVE_POINT_IN_FIGURE:return doMovePointInFigure(state,action);case MOVE_LINE:return doMoveLine(state,action);case MOVE_ALL:return doMoveAll(state,action);case MOVE_POINT:return doMovePoint(state,action);case MOVE_CENTER:return doMoveCenter(state,action);case MOVE_RADIUS_POINT:return doMoveRadiusPoint(state,action);case CHANGE_SNAP_STEP:return doChangeSnapStep(state,action);case CHANGE_RANGE:return doChangeRange(state,action);case ADD_POINT:return doAddPoint(state,action);case REMOVE_POINT:return doRemovePoint(state,action);case FOCUS_POINT:return doFocusPoint(state,action);case BLUR_POINT:return doBlurPoint(state);case DELETE_INTENT:return doDeleteIntent(state);case CLICK_POINT:return doClickPoint(state,action);case CLOSE_POLYGON:return doClosePolygon(state);case OPEN_POLYGON:return doOpenPolygon(state);case CHANGE_INTERACTION_MODE:return doChangeInteractionMode(state,action);case CHANGE_KEYBOARD_INVITATION_VISIBILITY:return doChangeKeyboardInvitationVisibility(state,action);default:throw new UnreachableCaseError(action)}}function doDeleteIntent(state,action){if(isUnlimitedGraphState(state)){if(state.focusedPointIndex!==null){return doRemovePoint(state,actions.pointGraph.removePoint(state.focusedPointIndex))}}return state}function doFocusPoint(state,action){switch(state.type){case "polygon":case "point":return {...state,focusedPointIndex:action.index};default:return state}}function doBlurPoint(state,action){switch(state.type){case "polygon":case "point":const nextState={...state,showRemovePointButton:false};if(state.interactionMode==="mouse"){nextState.focusedPointIndex=null;}return nextState;default:return state}}function doClickPoint(state,action){if(isUnlimitedGraphState(state)){return {...state,focusedPointIndex:action.index,showRemovePointButton:true}}return state}function doClosePolygon(state){if(isUnlimitedGraphState(state)&&state.type==="polygon"){const noDupedPoints=getArrayWithoutDuplicates(state.coords);return {...state,coords:noDupedPoints,closedPolygon:true}}return state}function doOpenPolygon(state){if(isUnlimitedGraphState(state)&&state.type==="polygon"){return {...state,closedPolygon:false}}return state}function doChangeInteractionMode(state,action){if(isUnlimitedGraphState(state)){const nextKeyboardInvitation=action.mode==="keyboard"?false:state.showKeyboardInteractionInvitation;return {...state,interactionMode:action.mode,showKeyboardInteractionInvitation:nextKeyboardInvitation}}return state}function doChangeKeyboardInvitationVisibility(state,action){if(isUnlimitedGraphState(state)){return {...state,showKeyboardInteractionInvitation:action.shouldShow,hasBeenInteractedWith:true}}return state}function doMovePointInFigure(state,action){switch(state.type){case "segment":case "linear-system":{const newValue=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=updateAtIndex({array:state.coords,index:action.figureIndex,update:tuple=>setAtIndex({array:tuple,index:action.pointIndex,newValue})});const coordsToCheck=newCoords[action.figureIndex];if(coordsOverlap(coordsToCheck)){return state}const sharedAnnouncement={pointIndex:action.pointIndex,pointLabel:getCustomPointLabel(state.pointLabels,action.figureIndex*2+action.pointIndex),x:newValue[X],y:newValue[Y]};const stateAnnouncement=state.type==="segment"?{type:"move-segment-point",segmentIndex:action.figureIndex,totalSegments:state.coords.length,...sharedAnnouncement}:{type:"move-linear-system-point",lineIndex:action.figureIndex,...sharedAnnouncement};return {...state,hasBeenInteractedWith:true,coords:newCoords,stateAnnouncement}}case "ray":case "linear":case "vector":{const newValue=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=setAtIndex({array:state.coords,index:action.pointIndex,newValue});if(coordsOverlap(newCoords)){return state}let stateAnnouncement;if(state.type==="ray"){stateAnnouncement={type:"move-ray-point",pointIndex:action.pointIndex,pointLabel:getCustomPointLabel(state.pointLabels,action.pointIndex),x:newValue[X],y:newValue[Y]};}else if(state.type==="vector"){stateAnnouncement={type:"move-vector-point",pointIndex:action.pointIndex,x:newValue[X],y:newValue[Y]};}else {stateAnnouncement={type:"move-point",pointLabel:resolvePointLabel(state.pointLabels,action.pointIndex),x:newValue[X],y:newValue[Y]};}return {...state,hasBeenInteractedWith:true,coords:newCoords,stateAnnouncement}}case "circle":throw new Error(`Don't use movePointInFigure for circle graphs. Use moveCenter or moveRadiusPoint.`);case "angle":case "none":case "point":case "polygon":case "quadratic":case "sinusoid":case "absolute-value":case "tangent":case "exponential":case "logarithm":throw new Error(`Don't use movePointInFigure for ${state.type} graphs. Use movePoint instead!`);default:throw new UnreachableCaseError(state)}}function doMoveLine(state,action){const{snapStep,range}=state;const{newStart}=action;switch(state.type){case "segment":case "linear-system":{if(action.itemIndex===undefined){throw new Error("Please provide index of line to move")}const currentLine=state.coords[action.itemIndex];const constrainedLine=constrainShapePreservingMove(currentLine,newStart,{snapStep,range});const newCoords=setAtIndex({array:state.coords,index:action.itemIndex,newValue:constrainedLine});const stateAnnouncement=state.type==="segment"?{type:"move-segment-line",coords:constrainedLine}:{type:"move-linear-system-line",lineIndex:action.itemIndex,coords:constrainedLine};return {...state,type:state.type,hasBeenInteractedWith:true,coords:newCoords,stateAnnouncement}}case "ray":case "linear":case "vector":{const constrainedLine=constrainShapePreservingMove(state.coords,newStart,{snapStep,range});let type;if(state.type==="ray"){type="move-ray-line";}else if(state.type==="vector"){type="move-vector-line";}else {type="move-linear-line";}const stateAnnouncement={type,coords:constrainedLine};return {...state,type:state.type,hasBeenInteractedWith:true,coords:constrainedLine,stateAnnouncement}}default:return state}}function doMoveAll(state,action){const{snapStep,range}=state;const{newStart}=action;switch(state.type){case "polygon":{const useGridSnap=state.snapTo!=="sides"&&state.snapTo!=="angles";const desiredDelta=vec.sub(newStart,state.coords[0]);const change=getChange(state.coords,desiredDelta,{snapStep:useGridSnap?snapStep:[0,0],range});const newCoords=state.coords.map(point=>useGridSnap?snap(snapStep,vec.add(point,change)):vec.add(point,change));return {...state,hasBeenInteractedWith:true,coords:newCoords,stateAnnouncement:{type:"move-polygon",coords:newCoords,pointLabels:state.pointLabels}}}default:return state}}function doMovePoint(state,action){switch(state.type){case "angle":const newState=(()=>{if(action.index===1){const updatedCoords=boundAndSnapAngleVertex(state,action);return {...state,hasBeenInteractedWith:true,coords:updatedCoords}}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundAndSnapAngleEndPoints(action.destination,state,action.index)})}})();if(angleSidePointsTooCloseToVertex(newState)){return state}return {...newState,stateAnnouncement:{type:"move-angle-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:newState.coords[action.index][X],y:newState.coords[action.index][Y],angleMeasure:getClockwiseAngle(newState.coords,newState.allowReflexAngles??false)}};case "polygon":let newValue;if(state.snapTo==="sides"){newValue=boundAndSnapToSides(action.destination,state,action.index);}else if(state.snapTo==="angles"){newValue=boundAndSnapToPolygonAngle(action.destination,state,action.index);}else {newValue=boundToEdgeAndSnapToGrid(action.destination,state);}const newCoords=setAtIndex({array:state.coords,index:action.index,newValue:newValue});const polygonSidesCanIntersect=state.numSides==="unlimited"&&!state.closedPolygon;if(!polygonSidesCanIntersect&&polygonSidesIntersect(newCoords)){return state}return {...state,hasBeenInteractedWith:true,coords:newCoords,stateAnnouncement:{type:"move-point",pointLabel:resolvePointLabel(state.pointLabels,action.index),x:newValue[X],y:newValue[Y]}};case "point":{const newCoord=boundToEdgeAndSnapToGrid(action.destination,state);return {...state,hasBeenInteractedWith:true,focusedPointIndex:action.index,coords:setAtIndex({array:state.coords,index:action.index,newValue:newCoord}),stateAnnouncement:{type:"move-point",pointLabel:resolvePointLabel(state.pointLabels,action.index),x:newCoord[X],y:newCoord[Y]}}}case "sinusoid":{const destination=action.destination;const boundDestination=boundToEdgeAndSnapToGrid(destination,state);const newCoords=[...state.coords];newCoords[action.index]=boundDestination;if(newCoords[0][X]===newCoords[1][X]){return state}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-sinusoid-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:newCoords[action.index][X],y:newCoords[action.index][Y],otherY:newCoords[1-action.index][Y]}}}case "exponential":{const boundDestination=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=[...state.coords];newCoords[action.index]=boundDestination;if(newCoords[0][X]===newCoords[1][X]){return state}const expHandle=getAsymptoteHandleCoord("horizontal",state.range,state.asymptote);if(boundDestination[X]===expHandle[X]&&boundDestination[Y]===expHandle[Y]){return state}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-exponential-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:boundDestination[X],y:boundDestination[Y],hasCurve:getExponentialCoefficients(newCoords,state.asymptote)!==undefined}}}case "logarithm":{const boundDestination=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=[...state.coords];newCoords[action.index]=boundDestination;if(newCoords[0][X]===newCoords[1][X]){return state}if(newCoords[0][Y]===newCoords[1][Y]){return state}const logHandle=getAsymptoteHandleCoord("vertical",state.range,state.asymptote);if(boundDestination[X]===logHandle[X]&&boundDestination[Y]===logHandle[Y]){return state}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-logarithm-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:boundDestination[X],y:boundDestination[Y],hasCurve:getLogarithmCoefficients(newCoords,state.asymptote)!==undefined}}}case "absolute-value":{const boundDestination=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=[...state.coords];newCoords[action.index]=boundDestination;if(newCoords[0][X]===newCoords[1][X]){return state}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-absolute-value-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:boundDestination[X],y:boundDestination[Y],slope:getAbsoluteValueCoefficients(newCoords)[0]}}}case "tangent":{const boundDestination=boundToEdgeAndSnapToGrid(action.destination,state);const newCoords=[...state.coords];newCoords[action.index]=boundDestination;if(newCoords[0][X]===newCoords[1][X]||newCoords[0][Y]===newCoords[1][Y]){return state}return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-tangent-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:boundDestination[X],y:boundDestination[Y]}}}case "quadratic":{const newCoords=[...state.coords];const boundDestination=boundToEdgeAndSnapToGrid(action.destination,state);newCoords[action.index]=boundDestination;const QuadraticCoefficients=getQuadraticCoefficients(newCoords);if(QuadraticCoefficients===undefined){return state}const vertex=getQuadraticVertex(QuadraticCoefficients);return {...state,hasBeenInteractedWith:true,coords:setAtIndex({array:state.coords,index:action.index,newValue:boundDestination}),stateAnnouncement:{type:"move-quadratic-point",pointIndex:action.index,pointLabel:getCustomPointLabel(state.pointLabels,action.index),x:boundDestination[X],y:boundDestination[Y],vertex}}}default:throw new Error("The movePoint action is only for point, quadratic, and polygon graphs")}}function doMoveCenter(state,action){switch(state.type){case "circle":{const constrainedCenter=boundToEdgeAndSnapToGrid(action.destination,state);const newRadiusPoint=[...vec.add(state.radiusPoint,vec.sub(constrainedCenter,state.center))];const[xMin,xMax]=state.range[X];const[radX]=newRadiusPoint;if(radX<xMin||radX>xMax){const xJumpDist=(radX-constrainedCenter[X])*2;const possibleNewX=radX-xJumpDist;if(possibleNewX>=xMin&&possibleNewX<=xMax){newRadiusPoint[X]=possibleNewX;}else {return state}}return {...state,hasBeenInteractedWith:true,center:constrainedCenter,radiusPoint:newRadiusPoint,stateAnnouncement:{type:"move-center",x:constrainedCenter[X],y:constrainedCenter[Y]}}}case "exponential":{const newY=boundAndSnapToGrid(action.destination,state)[Y];if(newY===state.asymptote){return state}const expFutureHandle=getAsymptoteHandleCoord("horizontal",state.range,newY);if(state.coords.some(c=>c[X]===expFutureHandle[X]&&c[Y]===expFutureHandle[Y])){return state}return {...state,hasBeenInteractedWith:true,asymptote:newY,stateAnnouncement:{type:"move-exponential-asymptote",asymptoteY:newY}}}case "logarithm":{const newX=boundAndSnapToGrid(action.destination,state)[X];if(newX===state.asymptote){return state}const logFutureHandle=getAsymptoteHandleCoord("vertical",state.range,newX);if(state.coords.some(c=>c[X]===logFutureHandle[X]&&c[Y]===logFutureHandle[Y])){return state}return {...state,hasBeenInteractedWith:true,asymptote:newX,stateAnnouncement:{type:"move-logarithm-asymptote",asymptoteX:newX}}}default:throw new Error("The doMoveCenter action is only for circle, exponential, or logarithm graphs")}}function doMoveRadiusPoint(state,action){switch(state.type){case "circle":{const nextRadiusPoint=boundToEdgeAndSnapToGrid([action.destination[X],state.center[1]],state);if(vector$3.equal(nextRadiusPoint,state.center)){return state}return {...state,hasBeenInteractedWith:true,radiusPoint:nextRadiusPoint,stateAnnouncement:{type:"move-radius-point",x:nextRadiusPoint[X],y:nextRadiusPoint[Y],centerX:state.center[X],radius:vec.dist(state.center,nextRadiusPoint),pointLabel:getCustomPointLabel(state.pointLabels,0)}}}default:throw new Error("The doMoveRadiusPoint action is only for circle graphs")}}function doChangeSnapStep(state,action){if(_.isEqual(state.snapStep,action.snapStep)){return state}return {...state,snapStep:action.snapStep}}function doChangeRange(state,action){if(_.isEqual(state.range,action.range)){return state}return {...state,range:action.range}}function doAddPoint(state,action){if(!isUnlimitedGraphState(state)){return state}const{snapStep}=state;const snappedPoint=snap(snapStep,action.location);for(const point of state.coords){if(point[X]===snappedPoint[X]&&point[Y]===snappedPoint[Y]){return state}}const newCoords=[...state.coords,snappedPoint];return {...state,hasBeenInteractedWith:true,coords:newCoords,showRemovePointButton:true,focusedPointIndex:newCoords.length-1}}function doRemovePoint(state,action){if(!isUnlimitedGraphState(state)){return state}const nextFocusedPointIndex=state.coords.length>1?state.coords.length-2:null;return {...state,coords:state.coords.filter((_,i)=>i!==action.index),focusedPointIndex:nextFocusedPointIndex,showRemovePointButton:nextFocusedPointIndex!==null?true:false}}const getDeltaVertex=(maxMoves,minMoves,delta)=>{const[deltaX,deltaY]=delta;const maxXMove=Math.min(...maxMoves.map(move=>move[X]));const maxYMove=Math.min(...maxMoves.map(move=>move[Y]));const minXMove=Math.max(...minMoves.map(move=>move[X]));const minYMove=Math.max(...minMoves.map(move=>move[Y]));const dx=clamp(deltaX,minXMove,maxXMove);const dy=clamp(deltaY,minYMove,maxYMove);return [dx,dy]};const getChange=(coords,delta,constraintOpts)=>{const maxMoves=coords.map(point=>maxMove({...constraintOpts,point}));const minMoves=coords.map(point=>minMove({...constraintOpts,point}));const[dx,dy]=getDeltaVertex(maxMoves,minMoves,delta);return [dx,dy]};const constrainShapePreservingMove=(currentLine,newStart,constraintOpts)=>{const desiredDelta=vec.sub(newStart,currentLine[0]);const change=getChange(currentLine,desiredDelta,constraintOpts);const{snapStep}=constraintOpts;return [snap(snapStep,vec.add(currentLine[0],change)),snap(snapStep,vec.add(currentLine[1],change))]};function leq(a,b){return a<b||approximateEqual(a,b)}function boundAndSnapToGrid(point,{snapStep,range}){return snap(snapStep,bound$1({snapStep,range,point}))}function boundAndSnapAngleVertex({range,coords,snapStep},{destination}){const coordsCopy=[...coords];const startingVertex=coordsCopy[1];const insetAmount=vec.add(snapStep,[minDistanceBetweenAngleVertexAndSidePoint,minDistanceBetweenAngleVertexAndSidePoint]);const newVertex=clampToBox(inset(insetAmount,range),snap(snapStep,destination));const delta=vec.add(newVertex,reverseVector(startingVertex));const newPoints={};for(const i of [0,2]){const oldPoint=coordsCopy[i];let newPoint=vec.add(oldPoint,delta);let angle=getAngleFromVertex(newVertex,newPoint);angle*=Math.PI/180;newPoint=constrainToBoundsOnAngle(newPoint,angle,range,snapStep);newPoints[i]=newPoint;}newPoints[1]=newVertex;Object.entries(newPoints).forEach(([i,newPoint])=>{coordsCopy[i]=newPoint;});return coordsCopy}function tooClose(point1,point2,range){const safeDistance=2;const distance=vec.dist(point1,point2);return distance<safeDistance}function constrainToBoundsOnAngle(point,angle,range,snapStep){const lower=[range[0][0]+snapStep[0],range[1][0]+snapStep[0]];const upper=[range[0][1]-snapStep[1],range[1][1]-snapStep[1]];let result=point;if(result[0]<lower[0]){result=[lower[0],result[1]+(lower[0]-result[0])*Math.tan(angle)];}else if(result[0]>upper[0]){result=[upper[0],result[1]-(result[0]-upper[0])*Math.tan(angle)];}if(result[1]<lower[1]){result=[result[0]+(lower[1]-result[1])/Math.tan(angle),lower[1]];}else if(result[1]>upper[1]){result=[result[0]-(result[1]-upper[1])/Math.tan(angle),upper[1]];}return result}function boundAndSnapAngleEndPoints(destinationPoint,{range,coords,snapDegrees,angleOffsetDeg,snapStep},index){const snap=snapDegrees||1;const offsetDegrees=angleOffsetDeg||0;const coordsCopy=[...coords];const angleRange=[[range[0][0]+snapStep[0],range[0][1]-snapStep[0]],[range[1][0]+snapStep[1],range[1][1]-snapStep[1]]];const boundPoint=bound$1({snapStep:[0,0],range:angleRange,point:destinationPoint});coordsCopy[index]=boundPoint;const vertex=coords[1];let angle=getAngleFromVertex(coordsCopy[index],vertex);angle=Math.round((angle-offsetDegrees)/snap)*snap+offsetDegrees;const minDistance=minDistanceBetweenAngleVertexAndSidePoint+.01;const distance=Math.max(vec.dist(coordsCopy[index],vertex),minDistance);const snappedValue=vec.add(vertex,polar(distance,angle));return snappedValue}function angleSidePointsTooCloseToVertex(state){return tooClose(state.coords[0],state.coords[1],state.range)||tooClose(state.coords[2],state.coords[1],state.range)}function boundAndSnapToPolygonAngle(destinationPoint,{range,coords},index){const startingPoint=coords[index];return calculateAngleSnap(destinationPoint,range,coords,index,startingPoint)}function calculateAngleSnap(destinationPoint,range,coords,index,startingPoint){const coordsCopy=[...coords];coordsCopy[index]=bound$1({snapStep:[0,0],range,point:destinationPoint});const angles=angleMeasures(coordsCopy).map(angle=>angle*180/Math.PI);const rel=j=>{return (index+j+coordsCopy.length)%coordsCopy.length};_.each([-1,1],function(j){angles[rel(j)]=Math.round(angles[rel(j)]);});const getAngle=function(a,vertex,b){const angle=getClockwiseAngle([coordsCopy[rel(a)],coordsCopy[rel(vertex)],coordsCopy[rel(b)]]);return angle};const innerAngles=[angles[rel(-1)]-getAngle(-2,-1,1),angles[rel(1)]-getAngle(-1,1,2)];innerAngles[2]=180-(innerAngles[0]+innerAngles[1]);if(innerAngles.some(function(angle){return leq(angle,1)})){return startingPoint}const knownSide=magnitude(vector(coordsCopy[rel(-1)],coordsCopy[rel(1)]));const onLeft=sign(ccw(coordsCopy[rel(-1)],coordsCopy[rel(1)],coordsCopy[index]))===1;const side=Math.sin(innerAngles[1]*Math.PI/180)/Math.sin(innerAngles[2]*Math.PI/180)*knownSide;const outerAngle=getAngleFromVertex(coordsCopy[rel(1)],coordsCopy[rel(-1)]);const offset=polar(side,outerAngle+(onLeft?1:-1)*innerAngles[0]);return vector$3.add(coordsCopy[rel(-1)],offset)}function boundAndSnapToSides(destinationPoint,{range,coords},index){const startingPoint=coords[index];return calculateSideSnap(destinationPoint,range,coords,index,startingPoint)}function calculateSideSnap(destinationPoint,range,coords,index,startingPoint){const boundedDestinationPoint=bound$1({snapStep:[0,0],range,point:destinationPoint});const rel=j=>{return (index+j+coords.length)%coords.length};const sides=[[coords[rel(-1)],boundedDestinationPoint],[boundedDestinationPoint,coords[rel(1)]],[coords[rel(-1)],coords[rel(1)]]].map(function(coords){return magnitude(vector(...coords))});_.each([0,1],function(j){sides[j]=Math.round(sides[j]);});if(leq(sides[1]+sides[2],sides[0])||leq(sides[0]+sides[2],sides[1])||leq(sides[0]+sides[1],sides[2])){return startingPoint}const innerAngle=lawOfCosines(sides[0],sides[2],sides[1]);const outerAngle=getAngleFromVertex(coords[rel(1)],coords[rel(-1)]);const onLeft=sign(ccw(coords[rel(-1)],coords[rel(1)],boundedDestinationPoint))===1;const offset=polar(sides[0],outerAngle+(onLeft?1:-1)*innerAngle);return vector$3.add(coords[rel(-1)],offset)}function maxMove({snapStep,range,point}){const topRight=boundToEdgeAndSnapToGrid([Infinity,Infinity],{snapStep,range});return vec.sub(topRight,point)}function minMove({snapStep,range,point}){const bottomLeft=boundToEdgeAndSnapToGrid([-Infinity,-Infinity],{snapStep,range});return vec.sub(bottomLeft,point)}const coordsOverlap=coords=>coords.some((coord,i)=>coords.some((c,j)=>i!==j&&vector$3.equal(coord,c)));function updateAtIndex(args){const{array,index,update}=args;const newValue=update(array[index]);return setAtIndex({array,index,newValue})}function setAtIndex(args){const{array,index,newValue}=args;const copy=[...array];copy[index]=newValue;return copy}
1988
1988
 
1989
- const{clockwise}=geometry;const{convertRadiansToDegrees}=angles;function renderPolygonGraph(state,dispatch,i18n,markings){return {graph:jsxRuntimeExports.jsx(PolygonGraph,{graphState:state,dispatch:dispatch}),interactiveElementsDescription:describePolygonGraph(state,i18n,markings).srPolygonInteractiveElements}}const PolygonGraph=props=>{const{dispatch}=props;const{numSides,coords,snapStep,snapTo="grid"}=props.graphState;const graphConfig=useGraphConfig();const polygonRef=React.useRef(null);const polygonHitboxRef=React.useRef(null);const pointsRef=React.useRef([]);const lastMoveTimeRef=React.useRef(0);const{range:[x,y]}=graphConfig;const[[left,top]]=useTransformVectorsToPixels([x[0],y[1]]);const dragReferencePoint=coords[0];const constrain=getKeyboardMovementConstraintForPolygon(snapStep,snapTo);const moveAll=newStart=>{dispatch(actions.polygon.moveAll(newStart));};useDraggable({gestureTarget:polygonRef,point:dragReferencePoint,onMove:moveAll,constrainKeyboardMovement:constrain});const{dragging}=useDraggable({gestureTarget:polygonHitboxRef,point:dragReferencePoint,onMove:moveAll,constrainKeyboardMovement:constrain});const[hovered,setHovered]=React.useState(false);const[focusVisible,setFocusVisible]=React.useState(false);const polygonHitbox=useHitbox({shape:{kind:"polygon",vertices:coords},hitboxRef:polygonHitboxRef,layer:"body",dragging,onHoverChange:setHovered,testId:"movable-polygon__hitbox"});React.useEffect(()=>{const focusedIndex=props.graphState.focusedPointIndex;if(focusedIndex!=null){pointsRef.current[focusedIndex]?.focus();}},[props.graphState.focusedPointIndex,props.graphState.coords.length,pointsRef]);React.useEffect(()=>{if(numSides==="unlimited"&&props.graphState.coords.length>2){dispatch(actions.polygon.closePolygon());}},[]);const statefulProps={...props,graphConfig,polygonRef,pointsRef,lastMoveTimeRef,left,top,dragging,points:coords,hovered,setHovered,focusVisible,setFocusVisible};return jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment,{children:[polygonHitbox,numSides==="unlimited"?jsxRuntimeExports.jsx(UnlimitedPolygonGraph,{...statefulProps}):jsxRuntimeExports.jsx(LimitedPolygonGraph,{...statefulProps})]})};const LimitedPolygonGraph=statefulProps=>{const{dispatch,hovered,setHovered,focusVisible,setFocusVisible,graphConfig,polygonRef,lastMoveTimeRef,dragging,points}=statefulProps;const{showAngles,showSides,range,snapTo="grid",snapStep,pointLabels}=statefulProps.graphState;const{disableKeyboardInteraction,interactiveColor}=graphConfig;const{strings,locale}=usePerseusI18n();const buildLabel=usePointAriaLabel(pointLabels);const id=React.useId();const lines=getLines(points);const polygonPointsNumId=id+"-points-num";const polygonPointsId=id+"-points";const{srPolygonGraph,srPolygonGraphPointsNum,srPolygonGraphPoints,srPolygonElementsNum}=describePolygonGraph(statefulProps.graphState,{strings,locale},statefulProps.graphConfig.markings);return jsxRuntimeExports.jsxs("g",{"aria-label":srPolygonGraph,"aria-describedby":`${polygonPointsNumId} ${polygonPointsId}`,children:[jsxRuntimeExports.jsx(Polygon,{points:[...points],color:interactiveColor,svgPolygonProps:{strokeWidth:focusVisible?"var(--movable-line-stroke-weight-active)":"var(--movable-line-stroke-weight)",style:{fill:"transparent"},"aria-hidden":true}}),points.map((point,i)=>{const pt1=points.at(i-1);const pt2=points[(i+1)%points.length];if(!pt1||!pt2){return null}return jsxRuntimeExports.jsx(PolygonAngle,{centerPoint:point,endPoints:[pt1,pt2],areEndPointsClockwise:clockwise(points),showAngles:!!showAngles,snapTo:snapTo},"angle-"+i)}),showSides&&lines.map(([start,end],i)=>{const[x,y]=vec.midpoint(start,end);const length=vec.dist(start,end);const isApprox=!Number.isInteger(length);return jsxRuntimeExports.jsx(TextLabel,{x:x,y:y,children:isApprox?`≈ ${length.toFixed(snapTo==="sides"?0:1)}`:length},"side-"+i)}),jsxRuntimeExports.jsx(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),onKeyDownCapture:()=>{setFocusVisible(hasFocusVisible(polygonRef.current));},onFocus:()=>{setFocusVisible(hasFocusVisible(polygonRef.current));},onBlur:()=>setFocusVisible(hasFocusVisible(polygonRef.current)),className:"movable-polygon",role:"button","aria-label":srPolygonGraphPoints?`${srPolygonElementsNum} ${srPolygonGraphPoints}`:srPolygonElementsNum,"aria-disabled":disableKeyboardInteraction}}),points.map((point,i)=>{const angleId=`${id}-angle-${i}`;const side1Id=`${id}-point-${i}-side-1`;const side2Id=`${id}-point-${i}-side-2`;const angle=getAngleFromPoints(points,i);const angleDegree=angle?convertRadiansToDegrees(angle):null;const sidesArray=getSideLengthsFromPoints(points,i);const{pointIndex:point1Index,sideLength:side1Length}=sidesArray[0];const{pointIndex:point2Index,sideLength:side2Length}=sidesArray[1];return jsxRuntimeExports.jsxs("g",{children:[jsxRuntimeExports.jsx(MovablePoint$3,{ariaDescribedBy:`${angleId} ${side1Id} ${side2Id}`,ariaLabel:buildLabel(i,point),constrain:getKeyboardMovementConstraintForPoint(points,i,range,snapStep,snapTo),point:point,sequenceNumber:i+1,onMove:destination=>{const now=Date.now();const targetFPS=40;const moveThresholdTime=1e3/targetFPS;if(now-lastMoveTimeRef.current>moveThresholdTime){dispatch(actions.polygon.movePoint(i,destination));lastMoveTimeRef.current=now;}}}),angleDegree&&jsxRuntimeExports.jsx(SRDescInSVG,{id:angleId,children:Number.isInteger(angleDegree)?strings.srPolygonPointAngle({angle:angleDegree}):strings.srPolygonPointAngleApprox({angle:srFormatNumber(angleDegree,locale,1)})}),jsxRuntimeExports.jsx(SRDescInSVG,{id:side1Id,children:getPolygonSideString(side1Length,point1Index,strings,locale)}),jsxRuntimeExports.jsx(SRDescInSVG,{id:side2Id,children:getPolygonSideString(side2Length,point2Index,strings,locale)})]},"point-"+i)}),jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsNumId,children:srPolygonGraphPointsNum}),srPolygonGraphPoints&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsId,children:srPolygonGraphPoints})]})};const UnlimitedPolygonGraph=statefulProps=>{const{dispatch,graphConfig,left,top,pointsRef,points}=statefulProps;const{coords,closedPolygon,pointLabels}=statefulProps.graphState;const{strings,locale}=usePerseusI18n();const buildLabel=usePointAriaLabel(pointLabels);const{interactiveColor}=useGraphConfig();const[isCurrentlyDragging,setIsCurrentlyDragging]=useState(false);const dragEndCallbackTimer=useTimeout(()=>setIsCurrentlyDragging(false),400);const id=React.useId();const polygonPointsNumId=id+"-points-num";const polygonPointsId=id+"-points";if(closedPolygon){const closedPolygonProps={...statefulProps,numSides:coords.length};return jsxRuntimeExports.jsx(LimitedPolygonGraph,{...closedPolygonProps})}const{graphDimensionsInPixels}=graphConfig;const widthPx=graphDimensionsInPixels[0];const heightPx=graphDimensionsInPixels[1];const emptyGraph=coords.length===0;const{srPolygonGraph,srPolygonGraphPointsNum,srPolygonGraphPoints}=describePolygonGraph(statefulProps.graphState,{strings,locale},statefulProps.graphConfig.markings);return jsxRuntimeExports.jsxs("g",{"aria-label":emptyGraph?strings.srUnlimitedPolygonEmpty:srPolygonGraph,"aria-describedby":`${polygonPointsNumId} ${polygonPointsId}`,children:[jsxRuntimeExports.jsx(Polyline,{points:[...points],color:interactiveColor,svgPolylineProps:{strokeWidth:"var(--movable-line-stroke-weight)",style:{fill:"transparent"},"aria-hidden":true}}),jsxRuntimeExports.jsx("rect",{"aria-hidden":true,style:{fill:"none",pointerEvents:"all",cursor:"crosshair"},width:widthPx,height:heightPx,x:left,y:top,onClick:event=>{if(isCurrentlyDragging){return}const elementRect=event.currentTarget.getBoundingClientRect();const zoomFactor=getCSSZoomFactor(event.currentTarget);const x=(event.clientX-elementRect.x)/zoomFactor;const y=(event.clientY-elementRect.y)/zoomFactor;const graphCoordinates=pixelsToVectors([[x,y]],graphConfig);dispatch(actions.polygon.addPoint(graphCoordinates[0]));}}),coords.map((point,i)=>{const angleId=`${id}-angle-${i}`;let sideIds="";const hasAngle=i>0&&i<coords.length-1;const angle=hasAngle?getAngleFromPoints(points,i):null;const angleDegree=angle?convertRadiansToDegrees(angle):null;const sidesArray=getSideLengthsFromPoints(points,i,true);for(let sideIndex=0;sideIndex<sidesArray.length;sideIndex++){sideIds+=`${id}-point-${i}-side-${sideIndex} `;}return jsxRuntimeExports.jsxs("g",{children:[jsxRuntimeExports.jsx(MovablePoint$3,{ariaDescribedBy:`${angleId} ${sideIds}`,ariaLabel:buildLabel(i,point),point:point,sequenceNumber:i+1,onDragStart:()=>{dragEndCallbackTimer.clear();setIsCurrentlyDragging(true);},onMove:destination=>{dispatch(actions.polygon.movePoint(i,destination));},onDragEnd:()=>{dragEndCallbackTimer.set();},ref:ref=>{pointsRef.current[i]=ref;},onFocus:()=>{dispatch(actions.polygon.focusPoint(i));},onClick:()=>{if(i===0&&getArrayWithoutDuplicates(coords).length>=3){dispatch(actions.polygon.closePolygon());}dispatch(actions.polygon.clickPoint(i));}}),angleDegree&&jsxRuntimeExports.jsx(SRDescInSVG,{id:angleId,children:Number.isInteger(angleDegree)?strings.srPolygonPointAngle({angle:angleDegree}):strings.srPolygonPointAngleApprox({angle:srFormatNumber(angleDegree,locale,1)})}),sidesArray.map(({pointIndex,sideLength},j)=>jsxRuntimeExports.jsx(SRDescInSVG,{id:`${id}-point-${i}-side-${j}`,children:getPolygonSideString(sideLength,pointIndex,strings,locale)},`${id}-point-${i}-side-${j}`))]},"point-"+i)}),coords.length>0&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsNumId,children:srPolygonGraphPointsNum}),srPolygonGraphPoints&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsId,children:srPolygonGraphPoints})]})};function getLines(points){return points.map((point,i)=>{const next=points[(i+1)%points.length];return [point,next]})}const hasFocusVisible=element=>{const matches=selector=>element?.matches(selector)??false;try{return matches(":focus-visible")}catch{return matches(":focus")}};function getKeyboardMovementConstraintForPoint(points,index,range,snapStep,snapTo){switch(snapTo){case "grid":return p=>snap(snapStep,p);case "sides":return getSideSnapConstraint(points,index,range);case "angles":return getAngleSnapConstraint(points,index,range);default:throw new UnreachableCaseError(snapTo)}}function getKeyboardMovementConstraintForPolygon(snapStep,snapTo){switch(snapTo){case "grid":return p=>snap(snapStep,p);case "sides":case "angles":return p=>p;default:throw new UnreachableCaseError(snapTo)}}function getSideSnapConstraint(points,index,range){const newPoints=[...points];const pointToBeMoved=newPoints[index];const movePointWithConstraint=moveFunc=>{let destinationAttempt=moveFunc(pointToBeMoved);let newPoint=pointToBeMoved;while(newPoint[0]===pointToBeMoved[0]&&newPoint[1]===pointToBeMoved[1]&&isInBound({range,point:destinationAttempt})){newPoint=calculateSideSnap(destinationAttempt,range,newPoints,index,pointToBeMoved);destinationAttempt=moveFunc(destinationAttempt);}return newPoint};return {up:movePointWithConstraint(coord=>vec.add(coord,[0,1])),down:movePointWithConstraint(coord=>vec.sub(coord,[0,1])),left:movePointWithConstraint(coord=>vec.sub(coord,[1,0])),right:movePointWithConstraint(coord=>vec.add(coord,[1,0]))}}function getAngleSnapConstraint(points,index,range){const newPoints=[...points];const pointToBeMoved=newPoints[index];const movePointWithConstraint=moveFunc=>{let destinationAttempt=bound$1({snapStep:[0,0],range,point:moveFunc(pointToBeMoved)});let newPoint=pointToBeMoved;while(newPoint[0]===pointToBeMoved[0]&&newPoint[1]===pointToBeMoved[1]&&isInBound({range,point:destinationAttempt})){newPoint=calculateAngleSnap(destinationAttempt,range,newPoints,index,pointToBeMoved);destinationAttempt=moveFunc(destinationAttempt);}return newPoint};return {up:movePointWithConstraint(coord=>vec.add(coord,[0,.1])),down:movePointWithConstraint(coord=>vec.sub(coord,[0,.1])),left:movePointWithConstraint(coord=>vec.sub(coord,[.1,0])),right:movePointWithConstraint(coord=>vec.add(coord,[.1,0]))}}
1989
+ const{clockwise}=geometry;const{convertRadiansToDegrees}=angles;function renderPolygonGraph(state,dispatch,i18n,markings){return {graph:jsxRuntimeExports.jsx(PolygonGraph,{graphState:state,dispatch:dispatch}),interactiveElementsDescription:describePolygonGraph(state,i18n,markings).srPolygonInteractiveElements}}const PolygonGraph=props=>{const{dispatch}=props;const{numSides,coords,snapStep,snapTo="grid"}=props.graphState;const graphConfig=useGraphConfig();const polygonRef=React.useRef(null);const polygonHitboxRef=React.useRef(null);const pointsRef=React.useRef([]);const lastMoveTimeRef=React.useRef(0);const{range:[x,y]}=graphConfig;const[[left,top]]=useTransformVectorsToPixels([x[0],y[1]]);const dragReferencePoint=coords[0];const constrain=getKeyboardMovementConstraintForPolygon(snapStep,snapTo);const moveAll=newStart=>{dispatch(actions.polygon.moveAll(newStart));};useDraggable({gestureTarget:polygonRef,point:dragReferencePoint,onMove:moveAll,constrainKeyboardMovement:constrain});const{dragging}=useDraggable({gestureTarget:polygonHitboxRef,point:dragReferencePoint,onMove:moveAll,constrainKeyboardMovement:constrain});const[hovered,setHovered]=React.useState(false);const[focusVisible,setFocusVisible]=React.useState(false);const polygonHitbox=useHitbox({shape:{kind:"polygon",vertices:coords},hitboxRef:polygonHitboxRef,layer:"body",dragging,onHoverChange:setHovered,testId:"movable-polygon__hitbox"});React.useEffect(()=>{const focusedIndex=props.graphState.focusedPointIndex;if(focusedIndex!=null){pointsRef.current[focusedIndex]?.focus();}},[props.graphState.focusedPointIndex,props.graphState.coords.length,pointsRef]);React.useEffect(()=>{if(numSides==="unlimited"&&props.graphState.coords.length>2){dispatch(actions.polygon.closePolygon());}},[]);const statefulProps={...props,graphConfig,polygonRef,pointsRef,lastMoveTimeRef,left,top,dragging,points:coords,hovered,setHovered,focusVisible,setFocusVisible};return jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment,{children:[polygonHitbox,numSides==="unlimited"?jsxRuntimeExports.jsx(UnlimitedPolygonGraph,{...statefulProps}):jsxRuntimeExports.jsx(LimitedPolygonGraph,{...statefulProps})]})};const LimitedPolygonGraph=statefulProps=>{const{dispatch,hovered,setHovered,focusVisible,setFocusVisible,graphConfig,polygonRef,lastMoveTimeRef,dragging,points}=statefulProps;const{showAngles,showSides,range,snapTo="grid",snapStep,pointLabels}=statefulProps.graphState;const{disableKeyboardInteraction,interactiveColor}=graphConfig;const{strings,locale}=usePerseusI18n();const buildLabel=usePointAriaLabel(pointLabels);const id=React.useId();const lines=getLines(points);const polygonPointsNumId=id+"-points-num";const polygonPointsId=id+"-points";const{srPolygonGraph,srPolygonGraphPointsNum,srPolygonGraphPoints,srPolygonElementsNum}=describePolygonGraph(statefulProps.graphState,{strings,locale},statefulProps.graphConfig.markings);return jsxRuntimeExports.jsxs("g",{"aria-label":srPolygonGraph,"aria-describedby":`${polygonPointsNumId} ${polygonPointsId}`,children:[jsxRuntimeExports.jsx(Polygon,{points:[...points],color:interactiveColor,svgPolygonProps:{strokeWidth:focusVisible?"var(--movable-line-stroke-weight-active)":"var(--movable-line-stroke-weight)",style:{fill:"transparent"},"aria-hidden":true}}),points.map((point,i)=>{const pt1=points.at(i-1);const pt2=points[(i+1)%points.length];if(!pt1||!pt2){return null}return jsxRuntimeExports.jsx(PolygonAngle,{centerPoint:point,endPoints:[pt1,pt2],areEndPointsClockwise:clockwise(points),showAngles:!!showAngles,snapTo:snapTo},"angle-"+i)}),showSides&&lines.map(([start,end],i)=>{const[x,y]=line.midpoint([start,end]);const length=vec.dist(start,end);const isApprox=!Number.isInteger(length);return jsxRuntimeExports.jsx(TextLabel,{x:x,y:y,children:isApprox?`≈ ${length.toFixed(snapTo==="sides"?0:1)}`:length},"side-"+i)}),jsxRuntimeExports.jsx(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),onKeyDownCapture:()=>{setFocusVisible(hasFocusVisible(polygonRef.current));},onFocus:()=>{setFocusVisible(hasFocusVisible(polygonRef.current));},onBlur:()=>setFocusVisible(hasFocusVisible(polygonRef.current)),className:"movable-polygon",role:"button","aria-label":srPolygonGraphPoints?`${srPolygonElementsNum} ${srPolygonGraphPoints}`:srPolygonElementsNum,"aria-disabled":disableKeyboardInteraction}}),points.map((point,i)=>{const angleId=`${id}-angle-${i}`;const side1Id=`${id}-point-${i}-side-1`;const side2Id=`${id}-point-${i}-side-2`;const angle=getAngleFromPoints(points,i);const angleDegree=angle?convertRadiansToDegrees(angle):null;const sidesArray=getSideLengthsFromPoints(points,i);const{pointIndex:point1Index,sideLength:side1Length}=sidesArray[0];const{pointIndex:point2Index,sideLength:side2Length}=sidesArray[1];return jsxRuntimeExports.jsxs("g",{children:[jsxRuntimeExports.jsx(MovablePoint$3,{ariaDescribedBy:`${angleId} ${side1Id} ${side2Id}`,ariaLabel:buildLabel(i,point),constrain:getKeyboardMovementConstraintForPoint(points,i,range,snapStep,snapTo),point:point,sequenceNumber:i+1,onMove:destination=>{const now=Date.now();const targetFPS=40;const moveThresholdTime=1e3/targetFPS;if(now-lastMoveTimeRef.current>moveThresholdTime){dispatch(actions.polygon.movePoint(i,destination));lastMoveTimeRef.current=now;}}}),angleDegree&&jsxRuntimeExports.jsx(SRDescInSVG,{id:angleId,children:Number.isInteger(angleDegree)?strings.srPolygonPointAngle({angle:angleDegree}):strings.srPolygonPointAngleApprox({angle:srFormatNumber(angleDegree,locale,1)})}),jsxRuntimeExports.jsx(SRDescInSVG,{id:side1Id,children:getPolygonSideString(side1Length,point1Index,strings,locale)}),jsxRuntimeExports.jsx(SRDescInSVG,{id:side2Id,children:getPolygonSideString(side2Length,point2Index,strings,locale)})]},"point-"+i)}),jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsNumId,children:srPolygonGraphPointsNum}),srPolygonGraphPoints&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsId,children:srPolygonGraphPoints})]})};const UnlimitedPolygonGraph=statefulProps=>{const{dispatch,graphConfig,left,top,pointsRef,points}=statefulProps;const{coords,closedPolygon,pointLabels}=statefulProps.graphState;const{strings,locale}=usePerseusI18n();const buildLabel=usePointAriaLabel(pointLabels);const{interactiveColor}=useGraphConfig();const[isCurrentlyDragging,setIsCurrentlyDragging]=useState(false);const dragEndCallbackTimer=useTimeout(()=>setIsCurrentlyDragging(false),400);const id=React.useId();const polygonPointsNumId=id+"-points-num";const polygonPointsId=id+"-points";if(closedPolygon){const closedPolygonProps={...statefulProps,numSides:coords.length};return jsxRuntimeExports.jsx(LimitedPolygonGraph,{...closedPolygonProps})}const{graphDimensionsInPixels}=graphConfig;const widthPx=graphDimensionsInPixels[0];const heightPx=graphDimensionsInPixels[1];const emptyGraph=coords.length===0;const{srPolygonGraph,srPolygonGraphPointsNum,srPolygonGraphPoints}=describePolygonGraph(statefulProps.graphState,{strings,locale},statefulProps.graphConfig.markings);return jsxRuntimeExports.jsxs("g",{"aria-label":emptyGraph?strings.srUnlimitedPolygonEmpty:srPolygonGraph,"aria-describedby":`${polygonPointsNumId} ${polygonPointsId}`,children:[jsxRuntimeExports.jsx(Polyline,{points:[...points],color:interactiveColor,svgPolylineProps:{strokeWidth:"var(--movable-line-stroke-weight)",style:{fill:"transparent"},"aria-hidden":true}}),jsxRuntimeExports.jsx("rect",{"aria-hidden":true,style:{fill:"none",pointerEvents:"all",cursor:"crosshair"},width:widthPx,height:heightPx,x:left,y:top,onClick:event=>{if(isCurrentlyDragging){return}const elementRect=event.currentTarget.getBoundingClientRect();const zoomFactor=getCSSZoomFactor(event.currentTarget);const x=(event.clientX-elementRect.x)/zoomFactor;const y=(event.clientY-elementRect.y)/zoomFactor;const graphCoordinates=pixelsToVectors([[x,y]],graphConfig);dispatch(actions.polygon.addPoint(graphCoordinates[0]));}}),coords.map((point,i)=>{const angleId=`${id}-angle-${i}`;let sideIds="";const hasAngle=i>0&&i<coords.length-1;const angle=hasAngle?getAngleFromPoints(points,i):null;const angleDegree=angle?convertRadiansToDegrees(angle):null;const sidesArray=getSideLengthsFromPoints(points,i,true);for(let sideIndex=0;sideIndex<sidesArray.length;sideIndex++){sideIds+=`${id}-point-${i}-side-${sideIndex} `;}return jsxRuntimeExports.jsxs("g",{children:[jsxRuntimeExports.jsx(MovablePoint$3,{ariaDescribedBy:`${angleId} ${sideIds}`,ariaLabel:buildLabel(i,point),point:point,sequenceNumber:i+1,onDragStart:()=>{dragEndCallbackTimer.clear();setIsCurrentlyDragging(true);},onMove:destination=>{dispatch(actions.polygon.movePoint(i,destination));},onDragEnd:()=>{dragEndCallbackTimer.set();},ref:ref=>{pointsRef.current[i]=ref;},onFocus:()=>{dispatch(actions.polygon.focusPoint(i));},onClick:()=>{if(i===0&&getArrayWithoutDuplicates(coords).length>=3){dispatch(actions.polygon.closePolygon());}dispatch(actions.polygon.clickPoint(i));}}),angleDegree&&jsxRuntimeExports.jsx(SRDescInSVG,{id:angleId,children:Number.isInteger(angleDegree)?strings.srPolygonPointAngle({angle:angleDegree}):strings.srPolygonPointAngleApprox({angle:srFormatNumber(angleDegree,locale,1)})}),sidesArray.map(({pointIndex,sideLength},j)=>jsxRuntimeExports.jsx(SRDescInSVG,{id:`${id}-point-${i}-side-${j}`,children:getPolygonSideString(sideLength,pointIndex,strings,locale)},`${id}-point-${i}-side-${j}`))]},"point-"+i)}),coords.length>0&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsNumId,children:srPolygonGraphPointsNum}),srPolygonGraphPoints&&jsxRuntimeExports.jsx(SRDescInSVG,{id:polygonPointsId,children:srPolygonGraphPoints})]})};function getLines(points){return points.map((point,i)=>{const next=points[(i+1)%points.length];return [point,next]})}const hasFocusVisible=element=>{const matches=selector=>element?.matches(selector)??false;try{return matches(":focus-visible")}catch{return matches(":focus")}};function getKeyboardMovementConstraintForPoint(points,index,range,snapStep,snapTo){switch(snapTo){case "grid":return p=>snap(snapStep,p);case "sides":return getSideSnapConstraint(points,index,range);case "angles":return getAngleSnapConstraint(points,index,range);default:throw new UnreachableCaseError(snapTo)}}function getKeyboardMovementConstraintForPolygon(snapStep,snapTo){switch(snapTo){case "grid":return p=>snap(snapStep,p);case "sides":case "angles":return p=>p;default:throw new UnreachableCaseError(snapTo)}}function getSideSnapConstraint(points,index,range){const newPoints=[...points];const pointToBeMoved=newPoints[index];const movePointWithConstraint=moveFunc=>{let destinationAttempt=moveFunc(pointToBeMoved);let newPoint=pointToBeMoved;while(newPoint[0]===pointToBeMoved[0]&&newPoint[1]===pointToBeMoved[1]&&isInBound({range,point:destinationAttempt})){newPoint=calculateSideSnap(destinationAttempt,range,newPoints,index,pointToBeMoved);destinationAttempt=moveFunc(destinationAttempt);}return newPoint};return {up:movePointWithConstraint(coord=>vec.add(coord,[0,1])),down:movePointWithConstraint(coord=>vec.sub(coord,[0,1])),left:movePointWithConstraint(coord=>vec.sub(coord,[1,0])),right:movePointWithConstraint(coord=>vec.add(coord,[1,0]))}}function getAngleSnapConstraint(points,index,range){const newPoints=[...points];const pointToBeMoved=newPoints[index];const movePointWithConstraint=moveFunc=>{let destinationAttempt=bound$1({snapStep:[0,0],range,point:moveFunc(pointToBeMoved)});let newPoint=pointToBeMoved;while(newPoint[0]===pointToBeMoved[0]&&newPoint[1]===pointToBeMoved[1]&&isInBound({range,point:destinationAttempt})){newPoint=calculateAngleSnap(destinationAttempt,range,newPoints,index,pointToBeMoved);destinationAttempt=moveFunc(destinationAttempt);}return newPoint};return {up:movePointWithConstraint(coord=>vec.add(coord,[0,.1])),down:movePointWithConstraint(coord=>vec.sub(coord,[0,.1])),left:movePointWithConstraint(coord=>vec.sub(coord,[.1,0])),right:movePointWithConstraint(coord=>vec.add(coord,[.1,0]))}}
1990
1990
 
1991
1991
  function renderQuadraticGraph(state,dispatch,i18n){return {graph:jsxRuntimeExports.jsx(QuadraticGraph,{graphState:state,dispatch:dispatch}),interactiveElementsDescription:describeQuadraticGraph(state,i18n).srQuadraticInteractiveElements}}function QuadraticGraph(props){const{dispatch,graphState}=props;const{coords,pointLabels,snapStep}=graphState;const{interactiveColor}=useGraphConfig();const{strings,locale}=usePerseusI18n();const id=React.useId();const quadraticDirectionId=id+"-direction";const quadraticVertexId=id+"-vertex";const quadraticInterceptsId=id+"-intercepts";const coeffRef=React.useRef([0,0,0]);const coeffs=getQuadraticCoefficients$2(coords);if(coeffs!==undefined){coeffRef.current=coeffs;}const[a,b,c]=coeffRef.current;const y=x=>(a*x+b)*x+c;const{srQuadraticGraph,srQuadraticDirection,srQuadraticVertex,srQuadraticXIntercepts,srQuadraticYIntercept}=describeQuadraticGraph(graphState,{strings,locale});return jsxRuntimeExports.jsxs("g",{"aria-label":srQuadraticGraph,"aria-describedby":`${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`,children:[jsxRuntimeExports.jsx(ClipToGraphBounds,{children:jsxRuntimeExports.jsx(Plot$2.OfX,{y:y,color:interactiveColor,svgPathProps:{"aria-hidden":true}})}),coords.map((coord,i)=>{const srQuadraticPoint=getQuadraticPointString(resolvePointLabel(pointLabels,i),coord,strings,locale);const srVertex=srQuadraticVertex?` ${srQuadraticVertex}`:"";return jsxRuntimeExports.jsx(MovablePoint$3,{ariaLabel:`${srQuadraticPoint}${srVertex}`,point:coord,sequenceNumber:i+1,constrain:getQuadraticKeyboardConstraint(coords,snapStep,i),onMove:destination=>dispatch(actions.quadratic.movePoint(i,destination))},"point-"+i)}),srQuadraticDirection&&jsxRuntimeExports.jsx(SRDescInSVG,{id:quadraticDirectionId,children:srQuadraticDirection}),srQuadraticVertex&&jsxRuntimeExports.jsx(SRDescInSVG,{id:quadraticVertexId,children:srQuadraticVertex}),jsxRuntimeExports.jsx(SRDescInSVG,{id:quadraticInterceptsId,children:srQuadraticXIntercepts?`${srQuadraticXIntercepts} ${srQuadraticYIntercept}`:`${srQuadraticYIntercept}`})]})}const getQuadraticKeyboardConstraint=(coords,snapStep,pointMoved)=>{const newCoords=[coords[0],coords[1],coords[2]];const coordToBeMoved=newCoords[pointMoved];const movePointWithConstraint=moveFunc=>{let movedCoord=moveFunc(coordToBeMoved);newCoords[pointMoved]=movedCoord;if(areCoordsValid(newCoords)){return movedCoord}movedCoord=moveFunc(movedCoord);newCoords[pointMoved]=movedCoord;if(areCoordsValid(newCoords)){return movedCoord}return moveFunc(movedCoord)};return {up:vec.add(coordToBeMoved,[0,snapStep[1]]),down:vec.sub(coordToBeMoved,[0,snapStep[1]]),left:movePointWithConstraint(coord=>vec.sub(coord,[snapStep[0],0])),right:movePointWithConstraint(coord=>vec.add(coord,[snapStep[0],0]))}};const areCoordsValid=coords=>{const p1=coords[0];const p2=coords[1];const p3=coords[2];if(p1[0]===p2[0]||p2[0]===p3[0]||p1[0]===p3[0]){return false}return true};
1992
1992
 
@@ -2122,7 +2122,7 @@ var extraWidgets = [Blank,CSProgram$1,Categorizer$1,Definition$1,DeprecatedStand
2122
2122
 
2123
2123
  const init=function(){registerWidgets(basicWidgets);registerWidgets(extraWidgets);replaceDeprecatedWidgets();};
2124
2124
 
2125
- const libName="@khanacademy/perseus";const libVersion="87.1.0";addLibraryVersionToPerseusDebug(libName,libVersion);
2125
+ const libName="@khanacademy/perseus";const libVersion="87.1.1";addLibraryVersionToPerseusDebug(libName,libVersion);
2126
2126
 
2127
2127
  const apiVersion={major:12,minor:0};
2128
2128