From f84503dc562c4a0c62deec4b57e1c6a5cbe235af Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Mon, 6 Mar 2023 14:53:37 -0600 Subject: [PATCH] pegboard for graph (#1968) --- cypress/e2e/DoenetML/tagSpecific/graph.cy.js | 35 +++ src/Core/ComponentTypes.js | 2 + src/Core/components/Pegboard.js | 45 ++++ src/Viewer/renderers/pegboard.jsx | 215 +++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 src/Core/components/Pegboard.js create mode 100644 src/Viewer/renderers/pegboard.jsx diff --git a/cypress/e2e/DoenetML/tagSpecific/graph.cy.js b/cypress/e2e/DoenetML/tagSpecific/graph.cy.js index 7332a6761a..2f701b19b3 100644 --- a/cypress/e2e/DoenetML/tagSpecific/graph.cy.js +++ b/cypress/e2e/DoenetML/tagSpecific/graph.cy.js @@ -1942,6 +1942,41 @@ describe('Graph Tag Tests', function () { cy.get('#\\/pdc5d').should('have.text', '-45.03233, 8.28572, -5.58234, 7.8371') + }); + + it('pegboard', () => { + cy.window().then(async (win) => { + win.postMessage({ + doenetML: ` + a + + + + + + + + + `}, "*"); + }); + + cy.get('#\\/_text1').should('have.text', 'a') //wait for page to load + + // not sure what to test as don't know how to check renderer... + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables["/_pegboard1"].stateValues.dx).eq(1); + expect(stateVariables["/_pegboard1"].stateValues.dy).eq(1); + expect(stateVariables["/_pegboard1"].stateValues.xoffset).eq(0); + expect(stateVariables["/_pegboard1"].stateValues.yoffset).eq(0); + expect(stateVariables["/_pegboard2"].stateValues.dx).eq(3); + expect(stateVariables["/_pegboard2"].stateValues.dy).eq(2); + expect(stateVariables["/_pegboard2"].stateValues.xoffset).eq(1); + expect(stateVariables["/_pegboard2"].stateValues.yoffset).eq(-1); + }) + + + }); }); \ No newline at end of file diff --git a/src/Core/ComponentTypes.js b/src/Core/ComponentTypes.js index 3a2cb35d09..dcc3f5f404 100644 --- a/src/Core/ComponentTypes.js +++ b/src/Core/ComponentTypes.js @@ -82,6 +82,7 @@ import Map from './components/Map'; import Sources from './components/Sources'; import Slider from './components/Slider'; import Markers from './components/Markers'; +import Pegboard from './components/Pegboard'; import Constraints from './components/Constraints'; import ConstrainToGrid from './components/ConstrainToGrid'; import ConstrainToGraph from './components/ConstrainToGraph'; @@ -258,6 +259,7 @@ const componentTypeArray = [ Markers, Panel, Map, Sources, + Pegboard, Constraints, ConstrainToGrid, ConstrainToGraph, diff --git a/src/Core/components/Pegboard.js b/src/Core/components/Pegboard.js new file mode 100644 index 0000000000..bcd2bc9afc --- /dev/null +++ b/src/Core/components/Pegboard.js @@ -0,0 +1,45 @@ +import GraphicalComponent from './abstract/GraphicalComponent'; + +export default class Pegboard extends GraphicalComponent { + static componentType = "pegboard"; + + static createAttributesObject() { + let attributes = super.createAttributesObject(); + + attributes.dx = { + createComponentOfType: "number", + createStateVariable: "dx", + defaultValue: 1, + public: true, + forRenderer: true + }; + + attributes.dy = { + createComponentOfType: "number", + createStateVariable: "dy", + defaultValue: 1, + public: true, + forRenderer: true + }; + + attributes.xoffset = { + createComponentOfType: "number", + createStateVariable: "xoffset", + defaultValue: 0, + public: true, + forRenderer: true + }; + + attributes.yoffset = { + createComponentOfType: "number", + createStateVariable: "yoffset", + defaultValue: 0, + public: true, + forRenderer: true + }; + + return attributes; + + } + +} \ No newline at end of file diff --git a/src/Viewer/renderers/pegboard.jsx b/src/Viewer/renderers/pegboard.jsx new file mode 100644 index 0000000000..400d9bf46f --- /dev/null +++ b/src/Viewer/renderers/pegboard.jsx @@ -0,0 +1,215 @@ +import React, { useContext, useEffect, useState, useRef } from 'react'; +import useDoenetRender from './useDoenetRenderer'; +import { BoardContext } from './graph'; + +export default React.memo(function Pegboard(props) { + let { name, id, SVs, actions, sourceOfUpdate, callAction } = useDoenetRender(props); + + Pegboard.ignoreActionsWithoutCore = true; + + const board = useContext(BoardContext); + + let pegboardJXG = useRef(null); + + let previousBounds = useRef(null); + + let dx = useRef(null); + let dy = useRef(null); + let xoffset = useRef(null); + let yoffset = useRef(null); + + dx.current = SVs.dx; + dy.current = SVs.dy; + xoffset.current = SVs.xoffset; + yoffset.current = SVs.yoffset; + + let jsxPointAttributes = useRef({ + visible: !SVs.hidden, + fixed: true, + withlabel: false, + layer: 10 * SVs.layer, + fillColor: "var(--canvastext)", + strokeColor: "var(--canvastext)", + size: 0.1, + face: "circle", + highlight: false, + showinfobox: false, + }); + + jsxPointAttributes.current.visible = !SVs.hidden; + jsxPointAttributes.current.layer = 10 * SVs.layer; + + useEffect(() => { + //On unmount + return () => { + deletePegboardJXG(); + } + }, []) + + + + function createPegboardJXG() { + + let [xmin, ymax, xmax, ymin] = board.getBoundingBox(); + + let xind1 = (xmin - xoffset.current) / dx.current; + let xind2 = (xmax - xoffset.current) / dx.current; + let yind1 = (ymin - yoffset.current) / dy.current; + let yind2 = (ymax - yoffset.current) / dy.current; + + let minXind = Math.floor(Math.min(xind1, xind2)); + let maxXind = Math.ceil(Math.max(xind1, xind2)); + let minYind = Math.floor(Math.min(yind1, yind2)); + let maxYind = Math.ceil(Math.max(yind1, yind2)); + + previousBounds.current = [minXind, maxXind, minYind, maxYind]; + + if (Number.isFinite(minXind) && Number.isFinite(maxXind) && Number.isFinite(minYind) && Number.isFinite(maxYind)) { + + let pegs = []; + + for (let yind = minYind; yind <= maxYind; yind++) { + let y = yind * SVs.dy + SVs.yoffset; + let row = []; + for (let xind = minXind; xind <= maxXind; xind++) { + row.push(board.create('point', [xind * SVs.dx + SVs.xoffset, y], jsxPointAttributes.current)); + } + pegs.push(row); + } + + pegboardJXG.current = pegs; + + } + + + board.on('boundingbox', () => { + + let [xmin, ymax, xmax, ymin] = board.getBoundingBox(); + + let xind1 = (xmin - xoffset.current) / dx.current; + let xind2 = (xmax - xoffset.current) / dx.current; + let yind1 = (ymin - yoffset.current) / dy.current; + let yind2 = (ymax - yoffset.current) / dy.current; + + let minXind = Math.floor(Math.min(xind1, xind2)); + let maxXind = Math.ceil(Math.max(xind1, xind2)); + let minYind = Math.floor(Math.min(yind1, yind2)); + let maxYind = Math.ceil(Math.max(yind1, yind2)); + + let [prevXmin, prevXmax, prevYmin, prevYmax] = previousBounds.current; + + if (minXind !== prevXmin || maxXind !== prevXmax || minYind !== prevYmin || maxYind !== prevYmax) { + + recalculatePegboard(minXind, maxXind, minYind, maxYind) + } + + }) + } + + function deletePegboardJXG() { + if (pegboardJXG.current !== null) { + for (let row of pegboardJXG.current) { + for (let point of row) { + board.removeObject(point) + } + } + } + + pegboardJXG.current = null; + + } + + + function recalculatePegboard(minXind, maxXind, minYind, maxYind) { + + if (pegboardJXG.current === null) { + return createPegboardJXG(); + } + + if (!Number.isFinite(minXind) || !Number.isFinite(maxXind) || !Number.isFinite(minYind) || !Number.isFinite(maxYind)) { + return deletePegboardJXG(); + } + + + let [prevXmin, prevXmax, prevYmin, prevYmax] = previousBounds.current; + + let nRows = maxYind - minYind + 1; + let prevNrows = prevYmax - prevYmin + 1; + let nCols = maxXind - minXind + 1; + let prevNcols = prevXmax - prevXmin + 1; + + for (let i = 0; i < Math.min(nRows, prevNrows); i++) { + let row = pegboardJXG.current[i]; + let y = (i + minYind) * dy.current + yoffset.current; + + for (let j = 0; j < Math.min(nCols, prevNcols); j++) { + + let x = (j + minXind) * dx.current + xoffset.current; + + row[j].coords.setCoordinates(JXG.COORDS_BY_USER, [x, y]); + + row[j].needsUpdate = true; + row[j].update(); + } + if (prevNcols > nCols) { + for (let j = nCols; j < prevNcols; j++) { + let point = row.pop(); + board.removeObject(point) + } + } else if (prevNcols < nCols) { + for (let j = prevNcols; j < nCols; j++) { + let x = (j + minXind) * dx.current + xoffset.current; + row.push(board.create('point', [x, y], jsxPointAttributes.current)); + } + } + } + + if (prevNrows > nRows) { + for (let i = nRows; i < prevNrows; i++) { + let row = pegboardJXG.current.pop(); + for (let j = 0; j < prevNcols; j++) { + let point = row.pop(); + board.removeObject(point) + } + } + } else if (prevNrows < nRows) { + for (let i = prevNrows; i < nRows; i++) { + let row = []; + let y = (i + minYind) * dy.current + yoffset.current; + for (let j = 0; j < nCols; j++) { + let x = (j + minXind) * dx.current + xoffset.current; + row.push(board.create('point', [x, y], jsxPointAttributes.current)); + } + pegboardJXG.current.push(row); + } + } + + previousBounds.current = [minXind, maxXind, minYind, maxYind]; + + board.updateRenderer(); + } + + if (board) { + if (pegboardJXG.current === null) { + createPegboardJXG(); + } else { + let [xmin, ymax, xmax, ymin] = board.getBoundingBox(); + + let xind1 = (xmin - xoffset.current) / dx.current; + let xind2 = (xmax - xoffset.current) / dx.current; + let yind1 = (ymin - yoffset.current) / dy.current; + let yind2 = (ymax - yoffset.current) / dy.current; + + let minXind = Math.floor(Math.min(xind1, xind2)); + let maxXind = Math.ceil(Math.max(xind1, xind2)); + let minYind = Math.floor(Math.min(yind1, yind2)); + let maxYind = Math.ceil(Math.max(yind1, yind2)); + + recalculatePegboard(minXind, maxXind, minYind, maxYind) + + } + } + + return null; + +}) \ No newline at end of file