diff --git a/cypress/e2e/AssignedActivity/creditAchievedMenu.cy.js b/cypress/e2e/AssignedActivity/creditAchievedMenu.cy.js index b586c6f757..d6648f7de2 100644 --- a/cypress/e2e/AssignedActivity/creditAchievedMenu.cy.js +++ b/cypress/e2e/AssignedActivity/creditAchievedMenu.cy.js @@ -41,13 +41,13 @@ describe('Credit achieved menu tests', function () {

Enter x: x

` - cy.createActivity({courseId,doenetId,parentDoenetId:courseId,pageDoenetId, doenetML}); + cy.createActivity({ courseId, doenetId, parentDoenetId: courseId, pageDoenetId, doenetML }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - + cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') @@ -60,7 +60,7 @@ describe('Credit achieved menu tests', function () { cy.get('[data-test="Item 1 Credit"]').should('have.text', '0%') - cy.get('#\\/ans textarea').type("x{enter}", {force: true}) + cy.get('#\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="New Attempt"]').click(); @@ -77,27 +77,29 @@ describe('Credit achieved menu tests', function () {

Enter x: x

` -const doenetML2 = ` + const doenetML2 = `

No questions here

`; -const doenetML3 = ` + const doenetML3 = `

Enter y: y

` -const doenetML4 = `

No questions here, either

`; + const doenetML4 = `

No questions here, either

`; - cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, + cy.createMultipageActivity({ + courseId, doenetId, parentDoenetId: courseId, pageDoenetId1: pageDoenetId, pageDoenetId2, pageDoenetId3, pageDoenetId4, - doenetML1, doenetML2, doenetML3, doenetML4 }); + doenetML1, doenetML2, doenetML3, doenetML4 + }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - + cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') @@ -113,8 +115,8 @@ const doenetML4 = `

No questions here, either

`; cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 4 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '0%') - - cy.get('#page1\\/ans textarea').type("x{enter}", {force: true}) + + cy.get('#page1\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') @@ -141,7 +143,7 @@ const doenetML4 = `

No questions here, either

`; cy.get('[data-test="Item 4 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '50%') - cy.get('#page3\\/ans textarea').type("y{enter}", {force: true}) + cy.get('#page3\\/ans textarea').type("y{enter}", { force: true }) cy.get('[data-test="Item 3 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') @@ -169,37 +171,39 @@ const doenetML4 = `

No questions here, either

`; ` -const doenetML2 = ` + const doenetML2 = `

No questions here

`; -const doenetML3 = ` + const doenetML3 = `

Enter y: y

` -const doenetML4 = ` + const doenetML4 = `

No questions here, either

`; - cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, + cy.createMultipageActivity({ + courseId, doenetId, parentDoenetId: courseId, pageDoenetId1: pageDoenetId, pageDoenetId2, pageDoenetId3, pageDoenetId4, - doenetML1, doenetML2, doenetML3, doenetML4 }); + doenetML1, doenetML2, doenetML3, doenetML4 + }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - + cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') - + cy.wait(100); cy.get('[data-test="Paginate"').click(); cy.wait(100) //TODO: need the UI to let us know this was successful @@ -214,8 +218,8 @@ const doenetML4 = ` cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 4 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '0%') - - cy.get('#page1\\/ans textarea').type("x{enter}", {force: true}) + + cy.get('#page1\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') @@ -242,7 +246,7 @@ const doenetML4 = ` cy.get('[data-test="Item 4 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '50%') - cy.get('#page3\\/ans textarea').type("y{enter}", {force: true}) + cy.get('#page3\\/ans textarea').type("y{enter}", { force: true }) cy.get('[data-test="Item 3 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') @@ -270,28 +274,30 @@ const doenetML4 = `

Enter x: x

` -const doenetML2 = ` + const doenetML2 = `

No questions here

`; -const doenetML3 = ` + const doenetML3 = `

Enter y: y

` -const doenetML4 = `

No questions here, either

`; + const doenetML4 = `

No questions here, either

`; - cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, + cy.createMultipageActivity({ + courseId, doenetId, parentDoenetId: courseId, pageDoenetId1: pageDoenetId, pageDoenetId2, pageDoenetId3, pageDoenetId4, - doenetML1, doenetML2, doenetML3, doenetML4 }); + doenetML1, doenetML2, doenetML3, doenetML4 + }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - cy.get('[data-test="Item Weights"]').clear().type("2 0 1 0", {force: true}).blur() + cy.get('[data-test="Item Weights"]').clear().type("2 0 1 0", { force: true }).blur() cy.wait(100) //TODO: need the UI to let us know this was successful cy.get('[data-test="Assign Activity"]').click(); @@ -310,8 +316,8 @@ const doenetML4 = `

No questions here, either

`; cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 4 Credit"]').should('have.text', 'Not started') cy.get('[data-test="Assignment Percent"]').should('have.text', '0%') - - cy.get('#page1\\/ans textarea').type("x{enter}", {force: true}) + + cy.get('#page1\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', 'Not started') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') @@ -338,7 +344,7 @@ const doenetML4 = `

No questions here, either

`; cy.get('[data-test="Item 4 Credit"]').should('have.text', 'Not started') cy.get('[data-test="Assignment Percent"]').should('have.text', '66.7%') - cy.get('#page3\\/ans textarea').type("y{enter}", {force: true}) + cy.get('#page3\\/ans textarea').type("y{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', 'Complete') @@ -365,26 +371,28 @@ const doenetML4 = `

No questions here, either

`;

Enter x: x

` -const doenetML2 = ` + const doenetML2 = `

Enter y: y

`; -const doenetML3 = ` + const doenetML3 = `

Enter z: z

` - cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, + cy.createMultipageActivity({ + courseId, doenetId, parentDoenetId: courseId, pageDoenetId1: pageDoenetId, pageDoenetId2, pageDoenetId3, - doenetML1, doenetML2, doenetML3 }); + doenetML1, doenetML2, doenetML3 + }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - + cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') @@ -403,8 +411,8 @@ const doenetML3 = ` cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '0%') - - cy.get('#page1\\/ans textarea').type("x{enter}", {force: true}) + + cy.get('#page1\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') @@ -419,7 +427,7 @@ const doenetML3 = ` cy.url().should('match', /#page3$/) - cy.get('#page3\\/ans textarea').type("z{enter}", {force: true}) + cy.get('#page3\\/ans textarea').type("z{enter}", { force: true }) cy.get('[data-test="Item 3 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') @@ -434,7 +442,7 @@ const doenetML3 = ` cy.url().should('match', /#page2$/) - cy.get('#page2\\/ans textarea').type("y{enter}", {force: true}) + cy.get('#page2\\/ans textarea').type("y{enter}", { force: true }) cy.get('[data-test="Item 2 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') @@ -459,31 +467,34 @@ const doenetML3 = ` ` -const doenetML2 = ` + const doenetML2 = `

Enter y: y

`; -const doenetML3 = ` + const doenetML3 = `

Enter z: z

` - cy.createMultipageActivity({ courseId, doenetId, parentDoenetId: courseId, + cy.createMultipageActivity({ + courseId, doenetId, parentDoenetId: courseId, pageDoenetId1: pageDoenetId, pageDoenetId2, pageDoenetId3, - doenetML1, doenetML2, doenetML3 }); + doenetML1, doenetML2, doenetML3 + }); cy.visit(`http://localhost/course?tool=navigation&courseId=${courseId}`) - cy.get('.navigationRow').should('have.length',1); //Need this to wait for the row to appear + cy.get('.navigationRow').should('have.length', 1); //Need this to wait for the row to appear cy.get('.navigationRow').eq(0).get('.navigationColumn1').click(); - + cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') + cy.wait(100) cy.get('[data-test="Paginate"').click(); cy.wait(100) //TODO: need the UI to let us know this was successful @@ -508,8 +519,8 @@ const doenetML3 = ` cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') cy.get('[data-test="Assignment Percent"]').should('have.text', '0%') - - cy.get('#page1\\/ans textarea').type("x{enter}", {force: true}) + + cy.get('#page1\\/ans textarea').type("x{enter}", { force: true }) cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 2 Credit"]').should('have.text', '0%') cy.get('[data-test="Item 3 Credit"]').should('have.text', '0%') @@ -523,7 +534,7 @@ const doenetML3 = ` cy.url().should('match', /#page3$/) - cy.get('#page3\\/ans textarea').type("z{enter}", {force: true}) + cy.get('#page3\\/ans textarea').type("z{enter}", { force: true }) cy.get('[data-test="Item 3 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') @@ -536,7 +547,7 @@ const doenetML3 = ` cy.url().should('match', /#page2$/) - cy.get('#page2\\/ans textarea').type("y{enter}", {force: true}) + cy.get('#page2\\/ans textarea').type("y{enter}", { force: true }) cy.get('[data-test="Item 2 Credit"]').should('have.text', '100%') cy.get('[data-test="Item 1 Credit"]').should('have.text', '100%') diff --git a/cypress/e2e/AssignedActivity/multipageActivities.cy.js b/cypress/e2e/AssignedActivity/multipageActivities.cy.js index e2816de5e2..e3c91fb05e 100644 --- a/cypress/e2e/AssignedActivity/multipageActivities.cy.js +++ b/cypress/e2e/AssignedActivity/multipageActivities.cy.js @@ -176,6 +176,7 @@ describe('Multipage activity tests', function () { cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') + cy.wait(100) cy.get('[data-test="Paginate"').click(); cy.wait(100) //TODO: need the UI to let us know this was successful @@ -1138,6 +1139,7 @@ describe('Multipage activity tests', function () { cy.get('[data-test="Assign Activity"]').click(); cy.get('[data-test="Unassign Activity"]').should('be.visible') + cy.wait(100); cy.signin({ userId: studentUserId }) diff --git a/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js b/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js index d170e0a03d..672cd8f407 100644 --- a/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js +++ b/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js @@ -744,6 +744,314 @@ describe('Rectangle Tag Tests', function () { }); + it('draggable, vertices draggable', () => { + cy.window().then(async (win) => { + win.postMessage({ + doenetML: ` + + + +

To wait:

+

draggable:

+

vertices draggable:

+

corner vertices: $p.vertex1 $p.vertex3

+ `}, "*"); + }); + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([1, 3]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + cy.log('cannot move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 0: [4, 7] } + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'true') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([1, 3]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + cy.log('cannot move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[4, 7], [8, 10], [1, 9], [3, 2]] + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'false') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([1, 3]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + cy.log('only vertices draggable') + + cy.get('#\\/verticesDraggable').click() + cy.get('#\\/vd2').should('have.text', 'true') + + + cy.log('can move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 0: [4, 7] } + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(4,7)') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'true') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(4,7)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([4, 7]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('cannot move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[3, 8], [8, 10], [1, 9], [3, 2]] + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'true') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'true') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(4,7)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([4, 7]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('vertices and polygon draggable') + + cy.get('#\\/draggable').click() + cy.get('#\\/d2').should('have.text', 'true') + + + cy.log('can move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 2: [-3, 2] } + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(−3,2)') + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'true') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(4,7)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(−3,2)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([4, 7]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([-3, 2]); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('can move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[3, 8], [5, 8], [5, 1], [3, 1]] + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(3,8)') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'true') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(3,8)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,1)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([3, 8]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 1]); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + cy.log('polygon but not vertices draggable') + + cy.get('#\\/verticesDraggable').click() + cy.get('#\\/vd2').should('have.text', 'false') + + + cy.log('cannot move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 2: [9, 3] } + } + }) + }) + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'false') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(3,8)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,1)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([3, 8]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([5, 1]); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + cy.log('can move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[-4, 1], [3, 1], [3, 7], [-4, 7]] + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(−4,1)') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'false') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(−4,1)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(3,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([-4, 1]); + expect((stateVariables['/p'].stateValues.vertices)[2]).eqls([3, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + }) + + }); function setupScene({ rectangleProperties, rectangleChildren }) { diff --git a/cypress/e2e/DoenetML/tagSpecific/regularPolygon.cy.js b/cypress/e2e/DoenetML/tagSpecific/regularPolygon.cy.js new file mode 100644 index 0000000000..50d5d37e55 --- /dev/null +++ b/cypress/e2e/DoenetML/tagSpecific/regularPolygon.cy.js @@ -0,0 +1,1622 @@ + +describe('Regular Polygon Tag Tests', function () { + + beforeEach(() => { + cy.clearIndexedDB(); + cy.visit('/cypressTest') + + }) + + it('regular polygon with no parameters (gives triangle)', () => { + + setupScene({ + attributes: {}, + }); + + runTests({ + nVertices: 3, + vertex1: [1, 0], + center: [0, 0], + conservedWhenChangeNvertices: "circumradius" + }); + + }); + + it('specify area for square', () => { + + setupScene({ + attributes: { + nVertices: "4", + area: "100" + }, + }); + + runTests({ + nVertices: 4, + vertex1: [Math.sqrt(2) * 5, 0], + center: [0, 0], + conservedWhenChangeNvertices: "area" + }); + + }); + + it('specify sidelength, center for pentegon', () => { + + setupScene({ + attributes: { + nVertices: "5", + sideLength: "2", + center: "(4,2)" + }, + }); + + runTests({ + nVertices: 5, + vertex1: [4 + 2 / (2 * Math.sin(Math.PI / 5)), 2], + center: [4, 2], + conservedWhenChangeNvertices: "sideLength" + }); + + }); + + it('specify inRadius, center for hexagon', () => { + + setupScene({ + attributes: { + nVertices: "6", + inRadius: "3", + center: "(-2,5)" + }, + }); + + runTests({ + nVertices: 6, + vertex1: [-2 + 3 / (Math.cos(Math.PI / 6)), 5], + center: [-2, 5], + conservedWhenChangeNvertices: "inradius" + }); + + }); + + it('specify apothem for heptagon', () => { + + setupScene({ + attributes: { + nVertices: "7", + apothem: "4", + }, + }); + + runTests({ + nVertices: 7, + vertex1: [0 + 4 / (Math.cos(Math.PI / 7)), 0], + center: [0, 0], + conservedWhenChangeNvertices: "inradius", + abbreviated: true, + }); + + }); + + it('specify perimeter, center for octagon', () => { + + setupScene({ + attributes: { + nVertices: "8", + perimeter: "20", + center: "(-4,7)" + }, + }); + + runTests({ + nVertices: 8, + vertex1: [-4 + 20 / 8 / (2 * Math.sin(Math.PI / 8)), 7], + center: [-4, 7], + conservedWhenChangeNvertices: "perimeter", + abbreviated: true, + }); + + }); + + it('specify circumradius, center for triangle', () => { + + setupScene({ + attributes: { + nVertices: "3", + circumradius: "6", + center: "(-5,8)" + }, + }); + + runTests({ + nVertices: 3, + vertex1: [-5 + 6, 8], + center: [-5, 8], + conservedWhenChangeNvertices: "circumradius" + }); + + }); + + it('specify radius for square', () => { + + setupScene({ + attributes: { + nVertices: "4", + radius: "7", + center: "(-6,-2)" + }, + }); + + runTests({ + nVertices: 4, + vertex1: [-6 + 7, -2], + center: [-6, -2], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + }); + + it('specify center for pentagon', () => { + + setupScene({ + attributes: { + nVertices: "5", + center: "(-5,-3)" + }, + }); + + runTests({ + nVertices: 5, + vertex1: [-5 + 1, -3], + center: [-5, -3], + conservedWhenChangeNvertices: "circumradius" + }); + + }); + + it('specify one vertex for square', () => { + + setupScene({ + attributes: { + nVertices: 4, + vertices: "(2,-5)" + }, + }); + + runTests({ + nVertices: 4, + vertex1: [2, -5], + center: [1, -5], + conservedWhenChangeNvertices: "circumradius" + }); + + }); + + it('specify two vertices for pentagon', () => { + + setupScene({ + attributes: { + nVertices: "5", + vertices: "(2,-5) (5,1)" + }, + }); + + let nVertices = 5; + + let vertex1 = [2, -5]; + let vertex2 = [5, 1]; + + let sideVector = [vertex2[0] - vertex1[0], vertex2[1] - vertex1[1]]; + let midpoint = [(vertex1[0] + vertex2[0]) / 2, (vertex1[1] + vertex2[1]) / 2]; + let sideLength = Math.sqrt(sideVector[0] ** 2 + sideVector[1] ** 2); + let inradius = sideLength / (2 * Math.tan(Math.PI / nVertices)); + + let inradiusDirection = [-sideVector[1] / sideLength, sideVector[0] / sideLength]; + + let center = [midpoint[0] + inradiusDirection[0] * inradius, midpoint[1] + inradiusDirection[1] * inradius]; + + + runTests({ + nVertices, + vertex1, + center, + conservedWhenChangeNvertices: "twoVertices" + }); + + }); + + it('specify center and one vertex for triangle', () => { + + setupScene({ + attributes: { + nVertices: "3", + vertices: "(2,-5)", + center: "(-1,-3)" + }, + }); + + runTests({ + nVertices: 3, + vertex1: [2, -5], + center: [-1, -3], + conservedWhenChangeNvertices: "circumradius" + }); + + }); + + it('specify center and two vertices for triangle, ignore second vertex', () => { + + setupScene({ + attributes: { + nVertices: "3", + vertices: "(2,-5) (10,12)", + center: "(-1,-3)" + }, + }); + + runTests({ + nVertices: 3, + vertex1: [2, -5], + center: [-1, -3], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + }); + + it('specify center and vertex for triangle, ignore all size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + vertices: "(2,-5)", + center: "(-1,-3)", + circumradius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [2, -5], + center: [-1, -3], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + }) + + it('specify center and circumradius for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + center: "(-1,-3)", + circumradius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [10, -3], + center: [-1, -3], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + + }); + + it('specify vertex and circumradius for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + vertices: "(2,-5)", + circumradius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [2, -5], + center: [-9, -5], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + + }); + + it('specify two vertices for triangle, ingnore all size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + vertices: "(2,-5) (5,1)", + circumradius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + let nVertices = 3; + + let vertex1 = [2, -5]; + let vertex2 = [5, 1]; + + let sideVector = [vertex2[0] - vertex1[0], vertex2[1] - vertex1[1]]; + let midpoint = [(vertex1[0] + vertex2[0]) / 2, (vertex1[1] + vertex2[1]) / 2]; + let sideLength = Math.sqrt(sideVector[0] ** 2 + sideVector[1] ** 2); + let inradius = sideLength / (2 * Math.tan(Math.PI / nVertices)); + + let inradiusDirection = [-sideVector[1] / sideLength, sideVector[0] / sideLength]; + + let center = [midpoint[0] + inradiusDirection[0] * inradius, midpoint[1] + inradiusDirection[1] * inradius]; + + + runTests({ + nVertices, + vertex1, + center, + conservedWhenChangeNvertices: "twoVertices", + abbreviated: true, + }); + + }); + + it('specify circumradius for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + circumradius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [11, 0], + center: [0, 0], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + + }); + + it('specify radius for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + radius: "11", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [11, 0], + center: [0, 0], + conservedWhenChangeNvertices: "circumradius", + abbreviated: true, + }); + + + }); + + it('specify inradius for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + inradius: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [3 / (Math.cos(Math.PI / 3)), 0], + center: [0, 0], + conservedWhenChangeNvertices: "inradius", + abbreviated: true, + }); + + + }); + + it('specify center and apothem for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + center: "(-1,-3)", + apothem: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [-1 + 3 / (Math.cos(Math.PI / 3)), -3], + center: [-1, -3], + conservedWhenChangeNvertices: "inradius", + abbreviated: true, + }); + + + }); + + it('specify sideLength for triangle, ignore all other size attributes', () => { + + setupScene({ + attributes: { + nVertices: "3", + sideLength: "5", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [5 / (2 * Math.sin(Math.PI / 3)), 0], + center: [0, 0], + conservedWhenChangeNvertices: "sideLength", + abbreviated: true, + }); + + + }); + + it('specify center and perimeter for triangle, ignore area', () => { + + setupScene({ + attributes: { + nVertices: "3", + center: "(-1,-3)", + perimeter: "10", + area: "99", + }, + }); + + runTests({ + nVertices: 3, + vertex1: [-1 + 10 / (3 * 2 * Math.sin(Math.PI / 3)), -3], + center: [-1, -3], + conservedWhenChangeNvertices: "perimeter", + abbreviated: true, + }); + + + }); + + it('draggable, vertices draggable', () => { + cy.window().then(async (win) => { + win.postMessage({ + doenetML: ` + + + +

To wait:

+

draggable:

+

vertices draggable:

+

two vertices: $p.vertex1 $p.vertex2

+ `}, "*"); + }); + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + cy.log('cannot move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 0: [4, 7] } + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'true') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + cy.log('cannot move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[4, 7], [8, 10], [1, 9]] + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'false') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(1,3)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,7)') + + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + cy.log('only vertices draggable') + + cy.get('#\\/verticesDraggable').click() + cy.get('#\\/vd2').should('have.text', 'true') + + + cy.log('can move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 0: [4, 7] } + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(4,7)') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'true') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(4,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('cannot move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[3, 8], [8, 10], [1, 9]] + } + }) + }) + + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'true') + + cy.get("#\\/d2").should('have.text', 'false') + cy.get("#\\/vd2").should('have.text', 'true') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(4,7)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect((stateVariables['/p'].stateValues.vertices)[0]).eqls([4, 7]); + expect(stateVariables['/p'].stateValues.draggable).eq(false); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('vertices and polygon draggable') + + cy.get('#\\/draggable').click() + cy.get('#\\/d2').should('have.text', 'true') + + + cy.log('can move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 1: [-3, 2] } + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(−3,2)') + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'true') + + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(−3,2)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + + cy.log('can move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[3, 8], [5, 8], [4, 8 + Math.sqrt(3)]] + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(3,8)') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'true') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(3,8)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,8)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(true); + }) + + + cy.log('polygon but not vertices draggable') + + cy.get('#\\/verticesDraggable').click() + cy.get('#\\/vd2').should('have.text', 'false') + + + cy.log('cannot move single vertex') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: { 0: [9, 3] } + } + }) + }) + + // wait for core to process click + cy.get('#\\/bi').click() + cy.get('#\\/bi2').should('have.text', 'false') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'false') + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(3,8)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(5,8)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + cy.log('can move all vertices') + cy.window().then(async (win) => { + + await win.callAction1({ + actionName: "movePolygon", + componentName: "/p", + args: { + pointCoords: [[-4, 1], [-4, 5], [-4 - 2 * Math.sqrt(3), 3]] + } + }) + }) + + + cy.get("#\\/pvert .mjx-mrow").should('contain.text', '(−4,1)') + + + cy.get("#\\/d2").should('have.text', 'true') + cy.get("#\\/vd2").should('have.text', 'false') + + + cy.get("#\\/pvert .mjx-mrow").eq(0).should('have.text', '(−4,1)') + cy.get("#\\/pvert .mjx-mrow").eq(2).should('have.text', '(−4,5)') + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + expect(stateVariables['/p'].stateValues.draggable).eq(true); + expect(stateVariables['/p'].stateValues.verticesDraggable).eq(false); + }) + + + + }) + + +}); + +function setupScene({ attributes }) { + + let attributesString = Object.keys(attributes).map(attr => `${attr} = "${attributes[attr]}"`).join(" "); + + cy.window().then(async (win) => { + win.postMessage({ + doenetML: ` + a + + + + + + + + + + + +

circumradius:

+

radius:

+ +

inradius:

+

apothem:

+ +

side length:

+

perimeter:

+ + +

area:

+ +

n vertices:

+ + + + + + + + `}, "*"); + }); +} + +function runTests({ center, vertex1, nVertices, conservedWhenChangeNvertices = "radius", abbreviated = false }) { + cy.get('#\\/_text1').should('have.text', 'a'); // to wait for page to load + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + + let polygonName = "/rp"; + let centerPointName = "/centerPoint"; + let allVertexNames = ["/v1", "/v2", "/v3", "/v4", "/v5", "/v6", "/v7", "/v8", "/v9", "/v10"]; + let polygonCopyName = "/rp2"; + let polygonCopy2Name = "/g4/rp2"; + + let inputs = { + polygonNames: [polygonName, polygonCopyName, polygonCopy2Name], + vertexNames: allVertexNames.slice(0, nVertices), + centerPointName + } + + cy.window().then(async (win) => { + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + + cy.log("move vertices individually"); + + for (let i = 0; i < 3; i++) { + let index = (i * Math.round(nVertices / 3)) % nVertices; + + cy.window().then(async (win) => { + + let vertex = [index, index + 1]; + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonName, + args: { + pointCoords: { [index]: vertex } + } + }) + + stateVariables = await win.returnAllStateVariables1(); + + let angle = -index * 2 * Math.PI / nVertices; + let c = Math.cos(angle); + let s = Math.sin(angle); + + + let directionWithRadius = [vertex[0] - center[0], vertex[1] - center[1]]; + + vertex1 = [directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]] + + + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + } + + + if (!abbreviated) { + cy.log("move polygon points together"); + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let dx = 3, dy = -2; + + let currentVertices = stateVariables[polygonName].stateValues.vertices; + let pointCoords = {}; + + for (let i = 0; i < nVertices; i++) { + pointCoords[i] = [currentVertices[i][0] + dx, currentVertices[i][1] + dy] + } + + vertex1 = pointCoords[0]; + center = [center[0] + dx, center[1] + dy] + + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonName, + args: { + pointCoords + } + }) + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + cy.log("move center point"); + cy.window().then(async (win) => { + + vertex1 = [vertex1[0] - center[0], vertex1[1] - center[1]] + center = [0, 0]; + + await win.callAction1({ + actionName: "movePoint", + componentName: centerPointName, + args: { x: center[0], y: center[1] } + }); + + + + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + + cy.log("move copied vertices"); + for (let i = 0; i < 3; i++) { + let index = (i * Math.round(nVertices / 3) + 1) % nVertices; + cy.window().then(async (win) => { + + let vertex = [index / 2 + 3, -1.5 * index]; + + await win.callAction1({ + actionName: "movePoint", + componentName: allVertexNames[index], + args: { x: vertex[0], y: vertex[1] } + }); + + + stateVariables = await win.returnAllStateVariables1(); + + let angle = -index * 2 * Math.PI / nVertices; + let c = Math.cos(angle); + let s = Math.sin(angle); + + + let directionWithRadius = [vertex[0] - center[0], vertex[1] - center[1]]; + + vertex1 = [directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]] + + + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + } + + + cy.log("move polygonCopy vertices individually"); + for (let i = 0; i < 3; i++) { + let index = (i * Math.round(nVertices / 3) + 2) % nVertices; + + cy.window().then(async (win) => { + + let vertex = [-index - 1, 2 * index]; + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonCopyName, + args: { + pointCoords: { [index]: vertex } + } + }) + + stateVariables = await win.returnAllStateVariables1(); + + let angle = -index * 2 * Math.PI / nVertices; + let c = Math.cos(angle); + let s = Math.sin(angle); + + + let directionWithRadius = [vertex[0] - center[0], vertex[1] - center[1]]; + + vertex1 = [directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]] + + + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + } + } + + cy.log("polygonCopy vertices together"); + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let dx = -2, dy = -4; + + let currentVertices = stateVariables[polygonCopyName].stateValues.vertices; + let pointCoords = {}; + + for (let i = 0; i < nVertices; i++) { + pointCoords[i] = [currentVertices[i][0] + dx, currentVertices[i][1] + dy] + } + + vertex1 = pointCoords[0]; + center = [center[0] + dx, center[1] + dy] + + + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonCopyName, + args: { + pointCoords + } + }) + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + + if (!abbreviated) { + + cy.log("move polygonCopy2 vertices individually"); + for (let i = 0; i < 3; i++) { + let index = (i * Math.round(nVertices / 3) + 3) % nVertices; + cy.window().then(async (win) => { + + let vertex = [-2 * index - 1, index + 4]; + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonCopy2Name, + args: { + pointCoords: { [index]: vertex } + } + }) + + stateVariables = await win.returnAllStateVariables1(); + + let angle = -index * 2 * Math.PI / nVertices; + let c = Math.cos(angle); + let s = Math.sin(angle); + + + let directionWithRadius = [vertex[0] - center[0], vertex[1] - center[1]]; + + vertex1 = [directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]] + + + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + + } + + + cy.log("polygonCopy2 vertices together"); + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let dx = 1, dy = -3; + + let currentVertices = stateVariables[polygonCopyName].stateValues.vertices; + let pointCoords = {}; + + for (let i = 0; i < nVertices; i++) { + pointCoords[i] = [currentVertices[i][0] + dx, currentVertices[i][1] + dy] + } + + vertex1 = pointCoords[0]; + center = [center[0] + dx, center[1] + dy] + + + + await win.callAction1({ + actionName: "movePolygon", + componentName: polygonCopy2Name, + args: { + pointCoords + } + }) + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + } + + + cy.log("Change circumradius") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldCr = stateVariables[polygonName].stateValues.circumradius; + + let circumradius = 1; + + cy.get("#\\/micr textarea").type(`{home}{shift+end}{backspace}${circumradius}{enter}`, { force: true }) + cy.get("#\\/cr").should('have.text', `${circumradius}`) + + vertex1 = [ + (vertex1[0] - center[0]) * circumradius / oldCr + center[0], + (vertex1[1] - center[1]) * circumradius / oldCr + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + if (!abbreviated) { + + cy.log("Change radius") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldR = stateVariables[polygonName].stateValues.circumradius; + + let radius = 3; + + cy.get("#\\/mir textarea").type(`{home}{shift+end}{backspace}${radius}{enter}`, { force: true }) + cy.get("#\\/r").should('have.text', `${radius}`) + + vertex1 = [ + (vertex1[0] - center[0]) * radius / oldR + center[0], + (vertex1[1] - center[1]) * radius / oldR + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + cy.log("Change inradius") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldIr = stateVariables[polygonName].stateValues.inradius; + + let inradius = 5; + + cy.get("#\\/miir textarea").type(`{home}{shift+end}{backspace}${inradius}{enter}`, { force: true }) + cy.get("#\\/ir").should('have.text', `${inradius}`) + + vertex1 = [ + (vertex1[0] - center[0]) * inradius / oldIr + center[0], + (vertex1[1] - center[1]) * inradius / oldIr + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + cy.log("Change apothem") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldAp = stateVariables[polygonName].stateValues.inradius; + + let apothem = 4; + + cy.get("#\\/miap textarea").type(`{home}{shift+end}{backspace}${apothem}{enter}`, { force: true }) + cy.get("#\\/ap").should('have.text', `${apothem}`) + + vertex1 = [ + (vertex1[0] - center[0]) * apothem / oldAp + center[0], + (vertex1[1] - center[1]) * apothem / oldAp + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + cy.log("Change sideLength") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldSl = stateVariables[polygonName].stateValues.sideLength; + + let sideLength = 2; + + cy.get("#\\/misl textarea").type(`{home}{shift+end}{backspace}${sideLength}{enter}`, { force: true }) + cy.get("#\\/sl").should('have.text', `${sideLength}`) + + vertex1 = [ + (vertex1[0] - center[0]) * sideLength / oldSl + center[0], + (vertex1[1] - center[1]) * sideLength / oldSl + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + cy.log("Change perimeter") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldSl = stateVariables[polygonName].stateValues.perimeter; + + let perimeter = 9; + + cy.get("#\\/mip textarea").type(`{home}{shift+end}{backspace}${perimeter}{enter}`, { force: true }) + cy.get("#\\/p").should('have.text', `${perimeter}`) + + vertex1 = [ + (vertex1[0] - center[0]) * perimeter / oldSl + center[0], + (vertex1[1] - center[1]) * perimeter / oldSl + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + cy.log("Change area") + cy.window().then(async (win) => { + + stateVariables = await win.returnAllStateVariables1(); + + let oldAr = stateVariables[polygonName].stateValues.area; + + let area = 13; + + cy.get("#\\/miar textarea").type(`{home}{shift+end}{backspace}${area}{enter}`, { force: true }) + cy.get("#\\/ar").should('have.text', `${area}`) + + vertex1 = [ + (vertex1[0] - center[0]) * Math.sqrt(area / oldAr) + center[0], + (vertex1[1] - center[1]) * Math.sqrt(area / oldAr) + center[1] + ] + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + } + + + + cy.log("Add two vertices") + cy.window().then(async (win) => { + + + let result = adjustVertex1CenterWhenChangeNVertices(vertex1, center, nVertices, nVertices + 2, conservedWhenChangeNvertices); + + vertex1 = result.vertex1; + center = result.center; + + nVertices += 2; + + cy.get("#\\/minv textarea").type(`{end}+2{enter}`, { force: true }) + cy.get("#\\/nv").should('have.text', `${nVertices}`) + + + inputs.vertexNames = allVertexNames.slice(0, nVertices); + + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + + + + if (!abbreviated) { + + cy.log("Remove a vertex") + cy.window().then(async (win) => { + + let result = adjustVertex1CenterWhenChangeNVertices(vertex1, center, nVertices, nVertices - 1, conservedWhenChangeNvertices); + + vertex1 = result.vertex1; + center = result.center; + + + nVertices -= 1; + + cy.get("#\\/minv textarea").type(`{end}-1{enter}`, { force: true }) + cy.get("#\\/nv").should('have.text', `${nVertices}`) + + + inputs.vertexNames = allVertexNames.slice(0, nVertices); + + + cy.window().then(async (win) => { + stateVariables = await win.returnAllStateVariables1(); + checkPolygonValues( + inputs, + { + nVertices, + vertex1, + center, + }, + stateVariables + ); + }) + }) + } + + + + }) +} + +function adjustVertex1CenterWhenChangeNVertices(vertex1, center, nVerticesOld, nVerticesNew, conservedWhenChangeNvertices) { + + let radiusRatio = 1; + + if (conservedWhenChangeNvertices === "inradius") { + + radiusRatio = Math.cos(Math.PI / nVerticesOld) / Math.cos(Math.PI / nVerticesNew) + + } else if (conservedWhenChangeNvertices === "sideLength") { + + radiusRatio = (2 * Math.sin(Math.PI / nVerticesOld)) / (2 * Math.sin(Math.PI / nVerticesNew)) + + } else if (conservedWhenChangeNvertices === "perimeter") { + + radiusRatio = (2 * nVerticesOld * Math.sin(Math.PI / nVerticesOld)) / (2 * nVerticesNew * Math.sin(Math.PI / nVerticesNew)) + + } else if (conservedWhenChangeNvertices === "area") { + radiusRatio = Math.sqrt((nVerticesOld / 2 * Math.sin(2 * Math.PI / nVerticesOld)) + / (nVerticesNew / 2 * Math.sin(2 * Math.PI / nVerticesNew))) + + } else if (conservedWhenChangeNvertices === "twoVertices") { + + + // calculate vertex2 + let directionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]]; + + let angleOld = 2 * Math.PI / nVerticesOld; + let c = Math.cos(angleOld); + let s = Math.sin(angleOld); + + let vertex2 = [directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]]; + + // calculate center based on this vertex 2 and new nVertices + let sideVector = [vertex2[0] - vertex1[0], vertex2[1] - vertex1[1]]; + let midpoint = [(vertex1[0] + vertex2[0]) / 2, (vertex1[1] + vertex2[1]) / 2]; + let sideLength = Math.sqrt(sideVector[0] ** 2 + sideVector[1] ** 2); + let inradius = sideLength / (2 * Math.tan(Math.PI / nVerticesNew)); + + let inradiusDirection = [-sideVector[1] / sideLength, sideVector[0] / sideLength]; + + center = [midpoint[0] + inradiusDirection[0] * inradius, midpoint[1] + inradiusDirection[1] * inradius]; + + } + + + vertex1 = [ + (vertex1[0] - center[0]) * radiusRatio + center[0], + (vertex1[1] - center[1]) * radiusRatio + center[1] + ] + + return { vertex1, center }; + +} + + +function checkPolygonValues({ + polygonNames, + vertexNames, + centerPointName +}, { + nVertices, + center, + vertex1, +}, + stateVariables +) { + + + let vertexCoords = [vertex1]; + + let directionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]]; + + let circumradius = Math.sqrt(directionWithRadius[0] ** 2 + directionWithRadius[1] ** 2); + let inradius = circumradius * Math.cos(Math.PI / nVertices); + let sideLength = circumradius * (2 * Math.sin(Math.PI / nVertices)); + let perimeter = circumradius * (2 * nVertices * Math.sin(Math.PI / nVertices)); + let area = circumradius ** 2 * (nVertices / 2 * Math.sin(2 * Math.PI / nVertices)) + + + + for (let i = 1; i < nVertices; i++) { + let angle = i * 2 * Math.PI / nVertices; + let c = Math.cos(angle); + let s = Math.sin(angle); + + vertexCoords.push([directionWithRadius[0] * c - directionWithRadius[1] * s + center[0], directionWithRadius[0] * s + directionWithRadius[1] * c + center[1]]) + + } + + for (let polygonName of polygonNames) { + let polygon = stateVariables[polygonName]; + for (let i = 0; i < nVertices; i++) { + expect(polygon.stateValues.vertices[i][0]).closeTo(vertexCoords[i][0], 1E-14 * Math.abs(vertexCoords[i][0]) + 1E-14) + expect(polygon.stateValues.vertices[i][1]).closeTo(vertexCoords[i][1], 1E-14 * Math.abs(vertexCoords[i][1]) + 1E-14) + } + expect(polygon.stateValues.center[0]).closeTo(center[0], 1E-14 * Math.abs(center[0]) + 1E-14); + expect(polygon.stateValues.center[1]).closeTo(center[1], 1E-14 * Math.abs(center[1]) + 1E-14); + + expect((polygon.stateValues.nVertices)).eq(nVertices); + + expect((polygon.stateValues.circumradius)).closeTo(circumradius, 1E-14 * circumradius); + expect((polygon.stateValues.inradius)).closeTo(inradius, 1E-14 * inradius); + expect((polygon.stateValues.sideLength)).closeTo(sideLength, 1E-14 * sideLength); + expect((polygon.stateValues.perimeter)).closeTo(perimeter, 1E-14 * perimeter); + expect((polygon.stateValues.area)).closeTo(area, 1E-14 * area); + + } + + if (vertexNames) { + for (let [i, vertexName] of vertexNames.entries()) { + let vertex = stateVariables[vertexName]; + expect(vertex.stateValues.xs[0]).closeTo(vertexCoords[i][0], 1E-14 * Math.abs(vertexCoords[i][0]) + 1E-14) + expect(vertex.stateValues.xs[1]).closeTo(vertexCoords[i][1], 1E-14 * Math.abs(vertexCoords[i][1]) + 1E-14) + } + } + + if (centerPointName) { + let centerPoint = stateVariables[centerPointName]; + expect(centerPoint.stateValues.xs[0]).closeTo(center[0], 1E-14 * Math.abs(center[0]) + 1E-14); + expect(centerPoint.stateValues.xs[1]).closeTo(center[1], 1E-14 * Math.abs(center[1]) + 1E-14); + + } +} \ No newline at end of file diff --git a/src/Core/ComponentTypes.js b/src/Core/ComponentTypes.js index dcc3f5f404..4a02983eca 100644 --- a/src/Core/ComponentTypes.js +++ b/src/Core/ComponentTypes.js @@ -48,6 +48,7 @@ import Polyline from './components/Polyline'; import Polygon from './components/Polygon'; import Triangle from './components/Triangle'; import Rectangle from './components/Rectangle'; +import RegularPolygon from './components/RegularPolygon'; import Circle from './components/Circle'; import Parabola from './components/Parabola'; import Curve from './components/Curve'; @@ -228,6 +229,7 @@ const componentTypeArray = [ Polygon, Triangle, Rectangle, + RegularPolygon, Circle, Parabola, Curve, diff --git a/src/Core/Core.js b/src/Core/Core.js index 3f11a729f2..cc93678668 100644 --- a/src/Core/Core.js +++ b/src/Core/Core.js @@ -10449,7 +10449,7 @@ export default class Core { } else { startInd = range.openBegin !== undefined ? range.openBegin : range.selfCloseBegin; - endInd = range.closeEnd !== undefined ? range.closeEnd : range.selfCloseEnd + 1; + endInd = range.closeEnd !== undefined ? range.closeEnd : range.selfCloseEnd; } let componentDoenetML = this.doenetML.slice(startInd - 1, endInd); diff --git a/src/Core/components/Rectangle.js b/src/Core/components/Rectangle.js index 359e431a11..40e1c126c6 100644 --- a/src/Core/components/Rectangle.js +++ b/src/Core/components/Rectangle.js @@ -898,12 +898,6 @@ export default class Rectangle extends Polygon { // console.log("inverse definition of vertices of rectangle", // desiredStateVariableValues, dependencyValuesByKey, stateValues); - // if not draggable, then disallow initial change - - if (initialChange && !await stateValues.draggable) { - return { success: false }; - } - if (!workspace.v0) { let vertices = await stateValues.vertices; workspace.v0 = [...vertices[0]]; @@ -1155,6 +1149,21 @@ export default class Rectangle extends Polygon { async movePolygon({ pointCoords, transient, sourceDetails, actionId, sourceInformation = {}, skipRendererUpdate = false, }) { + + let nVerticesMoved = Object.keys(pointCoords).length; + + if (nVerticesMoved === 1) { + // single vertex dragged + if (!await this.stateValues.verticesDraggable) { + return await this.coreFunctions.resolveAction({ actionId }); + } + } else { + // whole rectangle dragged + if (!await this.stateValues.draggable) { + return await this.coreFunctions.resolveAction({ actionId }); + } + } + let updateInstructions = []; let vertexComponents = {}; diff --git a/src/Core/components/RegularPolygon.js b/src/Core/components/RegularPolygon.js new file mode 100644 index 0000000000..8197324e02 --- /dev/null +++ b/src/Core/components/RegularPolygon.js @@ -0,0 +1,1524 @@ +import Polygon from './Polygon'; +import me from "math-expressions"; + +export default class RegularPolygon extends Polygon { + static componentType = "regularPolygon"; + static rendererType = "polygon"; + + + static createAttributesObject() { + let attributes = super.createAttributesObject(); + + attributes.nVertices = { + createComponentOfType: "number", + createStateVariable: "nVertices", + defaultValue: 3, + public: true, + forRenderer: true, + }; + + // Note: vertices is already an attribute from polygon + + attributes.center = { + createComponentOfType: "point" + } + + + // if center and vertex or two vertices are specified + // then the following size attributes are ignored + + // circumradius and radrius are the same thing and either attribute can be used + // If both specified, circumradius is used + attributes.circumradius = { + createComponentOfType: "number" + } + attributes.radius = { + createComponentOfType: "number" + } + + + // inradius and apothem are the same thing and either attribute can be used + // If both specified, inradius is used. + // If circumradius is specified, inradius is ignored + attributes.inradius = { + createComponentOfType: "number" + } + attributes.apothem = { + createComponentOfType: "number" + } + + // if circumradius or inradius is specified, sideLength is ignored + attributes.sideLength = { + createComponentOfType: "number" + } + + // if circumradius, inradius, or sideLength is specified, perimeter is ignored + attributes.perimeter = { + createComponentOfType: "number" + } + + // if circumradius, inradius, sideLength, or perimeter is specified, area is ignored + attributes.area = { + createComponentOfType: "number" + } + + return attributes; + } + + static returnStateVariableDefinitions() { + + let stateVariableDefinitions = super.returnStateVariableDefinitions(); + + delete stateVariableDefinitions.nVertices; + + let styleDescriptionWithNounDeps = stateVariableDefinitions.styleDescriptionWithNoun.returnDependencies(); + styleDescriptionWithNounDeps.nSides = { + dependencyType: "stateVariable", + variableName: "nSides" + } + + stateVariableDefinitions.styleDescriptionWithNoun.returnDependencies = () => styleDescriptionWithNounDeps; + + + let styleDescriptionWithNounDef = stateVariableDefinitions.styleDescriptionWithNoun.definition; + + stateVariableDefinitions.styleDescriptionWithNoun.definition = function ({ dependencyValues }) { + let styleDescriptionWithNoun = styleDescriptionWithNounDef({ dependencyValues }).setValue.styleDescriptionWithNoun; + + styleDescriptionWithNoun = styleDescriptionWithNoun.replaceAll("polygon", `${dependencyValues.nSides}-sided regular polygon`); + + return { setValue: { styleDescriptionWithNoun } } + } + + stateVariableDefinitions.nVerticesSpecified = { + + returnDependencies: () => ({ + verticesAttr: { + dependencyType: "attributeComponent", + attributeName: "vertices", + variableNames: ["nPoints"] + } + }), + definition: function ({ dependencyValues }) { + if (dependencyValues.verticesAttr !== null) { + return { setValue: { nVerticesSpecified: dependencyValues.verticesAttr.stateValues.nPoints } } + } else { + return { setValue: { nVerticesSpecified: 0 } } + } + + } + } + + stateVariableDefinitions.essentialDirection = { + isArray: true, + entryPrefixes: ["essentialVertexX"], + defaultValueByArrayKey: () => 0, + hasEssential: true, + returnArraySizeDependencies: () => ({ + nVerticesSpecified: { + dependencyType: "stateVariable", + variableName: "nVerticesSpecified", + }, + haveSpecifiedCenter: { + dependencyType: "stateVariable", + variableName: "haveSpecifiedCenter", + } + }), + returnArraySize({ dependencyValues }) { + let needDir = (dependencyValues.haveSpecifiedCenter ? 1 : 0) + dependencyValues.nVerticesSpecified <= 1; + return [needDir ? 2 : 0]; + }, + + returnArrayDependenciesByKey() { + return {}; + }, + + arrayDefinitionByKey: function ({ arrayKeys }) { + let essentialDirection = {}; + + for (let arrayKey of arrayKeys) { + if (arrayKey === "0") { + essentialDirection[arrayKey] = { defaultValue: 1 } + } else { + // uses defaultValueByArrayKey + essentialDirection[arrayKey] = true; + } + } + return { useEssentialOrDefaultValue: { essentialDirection } }; + + }, + + inverseArrayDefinitionByKey({ desiredStateVariableValues }) { + + let instructions = []; + + for (let arrayKey in desiredStateVariableValues.essentialDirection) { + + instructions.push({ + setEssentialValue: "essentialDirection", + value: { [arrayKey]: desiredStateVariableValues.essentialDirection[arrayKey] }, + }); + } + + return { + success: true, + instructions + } + } + } + + stateVariableDefinitions.haveSpecifiedCenter = { + returnDependencies: () => ({ + centerAttr: { + dependencyType: "attributeComponent", + attributeName: "center" + } + }), + definition: ({ dependencyValues }) => ({ + setValue: { + haveSpecifiedCenter: dependencyValues.centerAttr !== null + } + }) + } + + stateVariableDefinitions.specifiedCenter = { + isArray: true, + entryPrefixes: ["specifiedCenterX"], + returnArraySizeDependencies: () => ({ + haveSpecifiedCenter: { + dependencyType: "stateVariable", + variableName: "haveSpecifiedCenter", + }, + }), + returnArraySize({ dependencyValues }) { + return [dependencyValues.haveSpecifiedCenter ? 2 : 0]; + }, + + returnArrayDependenciesByKey({ arrayKeys }) { + + let dependenciesByKey = {}; + + for (let arrayKey of arrayKeys) { + let varEnding = Number(arrayKey) + 1; + dependenciesByKey[arrayKey] = { + centerAttr: { + dependencyType: "attributeComponent", + attributeName: "center", + variableNames: ["x" + varEnding], + }, + } + } + + return { dependenciesByKey } + }, + + arrayDefinitionByKey: function ({ dependencyValuesByKey, arrayKeys }) { + + let specifiedCenter = {}; + + for (let arrayKey of arrayKeys) { + let varEnding = Number(arrayKey) + 1; + + if (dependencyValuesByKey[arrayKey].centerAttr !== null) { + specifiedCenter[arrayKey] = evaluate_to_constant(dependencyValuesByKey[arrayKey].centerAttr.stateValues["x" + varEnding]); + } + } + + return { setValue: { specifiedCenter } } + }, + + inverseArrayDefinitionByKey({ desiredStateVariableValues, + dependencyValuesByKey, dependencyNamesByKey, + }) { + let instructions = []; + + for (let arrayKey in desiredStateVariableValues.specifiedCenter) { + + if (dependencyValuesByKey[arrayKey].centerAttr && + dependencyValuesByKey[arrayKey].centerAttr !== null + ) { + + instructions.push({ + setDependency: dependencyNamesByKey[arrayKey].centerAttr, + desiredValue: me.fromAst(desiredStateVariableValues.specifiedCenter[arrayKey]), + variableIndex: 0, + }) + } + } + + return { + success: true, + instructions + }; + + } + } + + + stateVariableDefinitions.essentialCenter = { + isArray: true, + entryPrefixes: ["essentialCenterX"], + defaultValueByArrayKey: () => 0, + hasEssential: true, + returnArraySizeDependencies: () => ({ + haveSpecifiedCenter: { + dependencyType: "stateVariable", + variableName: "haveSpecifiedCenter", + } + }), + returnArraySize({ dependencyValues }) { + return [dependencyValues.haveSpecifiedCenter ? 0 : 2]; + }, + + returnArrayDependenciesByKey() { + return {}; + }, + + arrayDefinitionByKey: function ({ arrayKeys }) { + let essentialCenter = {}; + + for (let arrayKey of arrayKeys) { + essentialCenter[arrayKey] = true; + } + return { useEssentialOrDefaultValue: { essentialCenter } }; + + }, + + inverseArrayDefinitionByKey({ desiredStateVariableValues }) { + + let instructions = []; + + for (let arrayKey in desiredStateVariableValues.essentialCenter) { + + instructions.push({ + setEssentialValue: "essentialCenter", + value: { [arrayKey]: desiredStateVariableValues.essentialCenter[arrayKey] }, + }); + } + + return { + success: true, + instructions + } + } + } + + + + stateVariableDefinitions.specifiedCircumradius = { + returnDependencies() { + return { + circumradiusAttr: { + dependencyType: "attributeComponent", + attributeName: "circumradius", + variableNames: ["value"] + }, + radiusAttr: { + dependencyType: "attributeComponent", + attributeName: "radius", + variableNames: ["value"] + }, + nVerticesSpecified: { + dependencyType: "stateVariable", + variableName: "nVerticesSpecified" + }, + haveSpecifiedCenter: { + dependencyType: "stateVariable", + variableName: "haveSpecifiedCenter" + } + } + }, + + definition({ dependencyValues }) { + if (dependencyValues.circumradiusAttr !== null) { + return { setValue: { specifiedCircumradius: dependencyValues.circumradiusAttr.stateValues.value } }; + + } else if (dependencyValues.radiusAttr !== null) { + return { setValue: { specifiedCircumradius: dependencyValues.radiusAttr.stateValues.value } }; + + } else { + return { setValue: { specifiedCircumradius: null } } + } + }, + + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + if (dependencyValues.circumradiusAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "circumradiusAttr", + desiredValue: desiredStateVariableValues.specifiedCircumradius, + childIndex: 0, + variableIndex: 0 + }] + } + } else if (dependencyValues.radiusAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "radiusAttr", + desiredValue: desiredStateVariableValues.specifiedCircumradius, + childIndex: 0, + variableIndex: 0 + }] + } + } else { + return { success: false } + } + } + } + + + stateVariableDefinitions.specifiedInradius = { + returnDependencies() { + return { + inradiusAttr: { + dependencyType: "attributeComponent", + attributeName: "inradius", + variableNames: ["value"] + }, + apothemAttr: { + dependencyType: "attributeComponent", + attributeName: "apothem", + variableNames: ["value"] + }, + } + }, + + definition({ dependencyValues }) { + if (dependencyValues.inradiusAttr !== null) { + return { setValue: { specifiedInradius: dependencyValues.inradiusAttr.stateValues.value } }; + + } else if (dependencyValues.apothemAttr !== null) { + return { setValue: { specifiedInradius: dependencyValues.apothemAttr.stateValues.value } }; + + } else { + return { setValue: { specifiedInradius: null } } + } + }, + + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + if (dependencyValues.inradiusAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "inradiusAttr", + desiredValue: desiredStateVariableValues.specifiedInradius, + childIndex: 0, + variableIndex: 0 + }] + } + } else if (dependencyValues.apothemAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "apothemAttr", + desiredValue: desiredStateVariableValues.specifiedInradius, + childIndex: 0, + variableIndex: 0 + }] + } + } else { + return { sucess: false } + } + } + } + + + stateVariableDefinitions.specifiedSideLength = { + returnDependencies() { + return { + sideLengthAttr: { + dependencyType: "attributeComponent", + attributeName: "sideLength", + variableNames: ["value"] + }, + } + }, + + definition({ dependencyValues }) { + if (dependencyValues.sideLengthAttr !== null) { + return { setValue: { specifiedSideLength: dependencyValues.sideLengthAttr.stateValues.value } }; + + } else { + return { setValue: { specifiedSideLength: null } } + } + }, + + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + if (dependencyValues.sideLengthAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "sideLengthAttr", + desiredValue: desiredStateVariableValues.specifiedSideLength, + childIndex: 0, + variableIndex: 0 + }] + } + } else { + return { sucess: false } + } + } + } + + + stateVariableDefinitions.specifiedPerimeter = { + returnDependencies() { + return { + perimeterAttr: { + dependencyType: "attributeComponent", + attributeName: "perimeter", + variableNames: ["value"] + }, + } + }, + + definition({ dependencyValues }) { + if (dependencyValues.perimeterAttr !== null) { + return { setValue: { specifiedPerimeter: dependencyValues.perimeterAttr.stateValues.value } }; + + } else { + return { setValue: { specifiedPerimeter: null } } + } + }, + + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + if (dependencyValues.perimeterAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "perimeterAttr", + desiredValue: desiredStateVariableValues.specifiedPerimeter, + childIndex: 0, + variableIndex: 0 + }] + } + } else { + return { sucess: false } + } + } + } + + + stateVariableDefinitions.specifiedArea = { + returnDependencies() { + return { + areaAttr: { + dependencyType: "attributeComponent", + attributeName: "area", + variableNames: ["value"] + }, + } + }, + + definition({ dependencyValues }) { + if (dependencyValues.areaAttr !== null) { + return { setValue: { specifiedArea: dependencyValues.areaAttr.stateValues.value } }; + + } else { + return { setValue: { specifiedArea: null } } + } + }, + + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + if (dependencyValues.areaAttr !== null) { + return { + success: true, + instructions: [{ + setDependency: "areaAttr", + desiredValue: desiredStateVariableValues.specifiedArea, + childIndex: 0, + variableIndex: 0 + }] + } + } else { + return { sucess: false } + } + } + } + + + stateVariableDefinitions.essentialCircumradius = { + hasEssential: true, + defaultValue: 1, + returnDependencies: () => ({}), + definition: () => ({ useEssentialOrDefaultValue: { essentialCircumradius: true } }), + inverseDefinition({ desiredStateVariableValues }) { + return { + success: true, + instructions: [{ + setEssentialValue: "essentialCircumradius", + value: desiredStateVariableValues.essentialCircumradius + }] + } + } + } + + + // Note: we create the non-array centerComponents + // because we currently can't use additionalStateVariablesDefined with arrays + // unless all state variables are arrays of the same size + stateVariableDefinitions.centerComponents = { + additionalStateVariablesDefined: ["directionWithRadius"], + returnDependencies: () => ({ + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + nVerticesSpecified: { + dependencyType: "stateVariable", + variableName: "nVerticesSpecified" + }, + haveSpecifiedCenter: { + dependencyType: "stateVariable", + variableName: "haveSpecifiedCenter" + }, + + specifiedCircumradius: { + dependencyType: "stateVariable", + variableName: "specifiedCircumradius" + }, + specifiedInradius: { + dependencyType: "stateVariable", + variableName: "specifiedInradius" + }, + specifiedSideLength: { + dependencyType: "stateVariable", + variableName: "specifiedSideLength" + }, + specifiedPerimeter: { + dependencyType: "stateVariable", + variableName: "specifiedPerimeter" + }, + specifiedArea: { + dependencyType: "stateVariable", + variableName: "specifiedArea" + }, + + essentialCircumradius: { + dependencyType: "stateVariable", + variableName: "essentialCircumradius" + }, + essentialDirection: { + dependencyType: "stateVariable", + variableName: "essentialDirection" + }, + + verticesAttr: { + dependencyType: "attributeComponent", + attributeName: "vertices", + variableNames: ["points"] + }, + + specifiedCenter: { + dependencyType: "stateVariable", + variableName: "specifiedCenter", + }, + + essentialCenter: { + dependencyType: "stateVariable", + variableName: "essentialCenter", + }, + }), + definition({ dependencyValues }) { + + + let nVertices = dependencyValues.nVertices; + + let center; + let directionWithRadius; + + if (dependencyValues.nVerticesSpecified === 0) { + + // with no vertices, use center (specified or essential), direction, and a measure of size + + if (dependencyValues.haveSpecifiedCenter) { + center = dependencyValues.specifiedCenter; + } else { + center = dependencyValues.essentialCenter; + } + + + let circumradius; + + if (dependencyValues.specifiedCircumradius !== null) { + circumradius = dependencyValues.specifiedCircumradius; + } else if (dependencyValues.specifiedInradius !== null) { + circumradius = dependencyValues.specifiedInradius / Math.cos(Math.PI / nVertices); + } else if (dependencyValues.specifiedSideLength !== null) { + circumradius = dependencyValues.specifiedSideLength / (2 * Math.sin(Math.PI / nVertices)); + } else if (dependencyValues.specifiedPerimeter !== null) { + circumradius = dependencyValues.specifiedPerimeter / (2 * nVertices * Math.sin(Math.PI / nVertices)) + } else if (dependencyValues.specifiedArea !== null) { + circumradius = Math.sqrt(dependencyValues.specifiedArea + / (nVertices / 2 * Math.sin(2 * Math.PI / nVertices))) + } else { + circumradius = dependencyValues.essentialCircumradius; + } + + directionWithRadius = dependencyValues.essentialDirection.map(x => x * circumradius); + + + } else if (dependencyValues.haveSpecifiedCenter) { + + // base polygon on center and first vertex + + center = dependencyValues.specifiedCenter; + + let vertex = dependencyValues.verticesAttr.stateValues.points[0].map(evaluate_to_constant); + + directionWithRadius = [vertex[0] - center[0], vertex[1] - center[1]] + + } else if (dependencyValues.nVerticesSpecified === 1) { + // one vertex, no center + // use vertex, direction, and a measure of size + + let circumradius; + + if (dependencyValues.specifiedCircumradius !== null) { + circumradius = dependencyValues.specifiedCircumradius; + } else if (dependencyValues.specifiedInradius !== null) { + circumradius = dependencyValues.specifiedInradius / Math.cos(Math.PI / nVertices); + } else if (dependencyValues.specifiedSideLength !== null) { + circumradius = dependencyValues.specifiedSideLength / (2 * Math.sin(Math.PI / nVertices)); + } else if (dependencyValues.specifiedPerimeter !== null) { + circumradius = dependencyValues.specifiedPerimeter / (2 * nVertices * Math.sin(Math.PI / nVertices)) + } else if (dependencyValues.specifiedArea !== null) { + circumradius = Math.sqrt(dependencyValues.specifiedArea + / (nVertices / 2 * Math.sin(2 * Math.PI / nVertices))) + } else { + circumradius = dependencyValues.essentialCircumradius; + } + + directionWithRadius = dependencyValues.essentialDirection.map(x => x * circumradius); + + + let vertex = dependencyValues.verticesAttr.stateValues.points[0].map(evaluate_to_constant); + + + center = [vertex[0] - directionWithRadius[0], vertex[1] - directionWithRadius[1]]; + + } else { + // have at least two vertices specified, use the first 2 + // these vertices are adjacent vertices of the polygon, in counterclockwise order + + let vertex1 = dependencyValues.verticesAttr.stateValues.points[0].map(evaluate_to_constant); + let vertex2 = dependencyValues.verticesAttr.stateValues.points[1].map(evaluate_to_constant); + + let sideVector = [vertex2[0] - vertex1[0], vertex2[1] - vertex1[1]]; + let midpoint = [(vertex1[0] + vertex2[0]) / 2, (vertex1[1] + vertex2[1]) / 2]; + let sideLength = Math.sqrt(sideVector[0] ** 2 + sideVector[1] ** 2); + let inradius = sideLength / (2 * Math.tan(Math.PI / nVertices)); + + let inradiusDirection = [-sideVector[1] / sideLength, sideVector[0] / sideLength]; + + center = [midpoint[0] + inradiusDirection[0] * inradius, midpoint[1] + inradiusDirection[1] * inradius]; + + directionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]] + + } + + return { setValue: { centerComponents: center, directionWithRadius } } + + }, + async inverseDefinition({ desiredStateVariableValues, dependencyValues, workspace, stateValues }) { + + let nVertices = dependencyValues.nVertices; + + let instructions = []; + + + let desiredCenter = desiredStateVariableValues.centerComponents; + if (!desiredCenter) { + desiredCenter = workspace.desiredCenter; + } + if (!desiredCenter) { + desiredCenter = (await stateValues.center).map(evaluate_to_constant); + } + + let desiredDirectionWithRadius = desiredStateVariableValues.directionWithRadius; + if (!desiredDirectionWithRadius) { + desiredDirectionWithRadius = workspace.desiredDirectionWithRadius + }; + if (!desiredDirectionWithRadius) { + let center = (await stateValues.center).map(evaluate_to_constant); + let vertex1 = (await stateValues.vertices)[0].map(evaluate_to_constant); + desiredDirectionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]] + } + + workspace.desiredCenter = desiredCenter; + workspace.desiredDirectionWithRadius = desiredDirectionWithRadius; + + + if (dependencyValues.nVerticesSpecified === 0) { + + // with no vertices, use center (specified or essential), direction, and a measure of size + + if (dependencyValues.haveSpecifiedCenter) { + instructions.push({ + setDependency: "specifiedCenter", + desiredValue: desiredCenter, + }) + } else { + instructions.push({ + setDependency: "essentialCenter", + desiredValue: desiredCenter, + }) + } + + let desiredCircumradius = Math.sqrt(desiredDirectionWithRadius[0] ** 2 + desiredDirectionWithRadius[1] ** 2); + let desiredDirection = desiredDirectionWithRadius.map(x => x / desiredCircumradius); + + + if (dependencyValues.specifiedCircumradius !== null) { + instructions.push({ + setDependency: "specifiedCircumradius", + desiredValue: desiredCircumradius, + }) + } else if (dependencyValues.specifiedInradius !== null) { + instructions.push({ + setDependency: "specifiedInradius", + desiredValue: desiredCircumradius * Math.cos(Math.PI / nVertices), + }) + } else if (dependencyValues.specifiedSideLength !== null) { + instructions.push({ + setDependency: "specifiedSideLength", + desiredValue: desiredCircumradius * (2 * Math.sin(Math.PI / nVertices)), + }) + } else if (dependencyValues.specifiedPerimeter !== null) { + instructions.push({ + setDependency: "specifiedPerimeter", + desiredValue: desiredCircumradius * (2 * nVertices * Math.sin(Math.PI / nVertices)), + }) + } else if (dependencyValues.specifiedArea !== null) { + instructions.push({ + setDependency: "specifiedArea", + desiredValue: desiredCircumradius ** 2 * (nVertices / 2 * Math.sin(2 * Math.PI / nVertices)), + }) + } else { + instructions.push({ + setDependency: "essentialCircumradius", + desiredValue: desiredCircumradius, + }) + } + + instructions.push({ + setDependency: "essentialDirection", + desiredValue: desiredDirection, + }) + + + } else if (dependencyValues.haveSpecifiedCenter) { + + // base polygon on center and first vertex + + instructions.push({ + setDependency: "specifiedCenter", + desiredValue: desiredCenter, + }) + + let desiredVertices = { + "0,0": me.fromAst(desiredDirectionWithRadius[0] + desiredCenter[0]), + "0,1": me.fromAst(desiredDirectionWithRadius[1] + desiredCenter[1]) + }; + + instructions.push({ + setDependency: "verticesAttr", + desiredValue: desiredVertices, + variableIndex: 0, + }) + + } else if (dependencyValues.nVerticesSpecified === 1) { + // one vertex, no center + // use vertex, direction, and a measure of size + + let desiredCircumradius = Math.sqrt(desiredDirectionWithRadius[0] ** 2 + desiredDirectionWithRadius[1] ** 2); + let desiredDirection = desiredDirectionWithRadius.map(x => x / desiredCircumradius); + + if (dependencyValues.specifiedCircumradius !== null) { + instructions.push({ + setDependency: "specifiedCircumradius", + desiredValue: desiredCircumradius, + }) + } else if (dependencyValues.specifiedInradius !== null) { + instructions.push({ + setDependency: "specifiedInradius", + desiredValue: desiredCircumradius * Math.cos(Math.PI / nVertices), + }) + } else if (dependencyValues.specifiedSideLength !== null) { + instructions.push({ + setDependency: "specifiedSideLength", + desiredValue: desiredCircumradius * (2 * Math.sin(Math.PI / nVertices)), + }) + } else if (dependencyValues.specifiedPerimeter !== null) { + instructions.push({ + setDependency: "specifiedPerimeter", + desiredValue: desiredCircumradius * (2 * nVertices * Math.sin(Math.PI / nVertices)), + }) + } else if (dependencyValues.specifiedArea !== null) { + instructions.push({ + setDependency: "specifiedArea", + desiredValue: desiredCircumradius ** 2 * (nVertices / 2 * Math.sin(2 * Math.PI / nVertices)), + }) + } else { + instructions.push({ + setDependency: "essentialCircumradius", + desiredValue: desiredCircumradius, + }) + } + + instructions.push({ + setDependency: "essentialDirection", + desiredValue: desiredDirection, + }) + + let desiredVertices = { + "0,0": me.fromAst(desiredDirectionWithRadius[0] + desiredCenter[0]), + "0,1": me.fromAst(desiredDirectionWithRadius[1] + desiredCenter[1]) + }; + + instructions.push({ + setDependency: "verticesAttr", + desiredValue: desiredVertices, + variableIndex: 0, + }) + + } else { + // have at least two vertices specified + // these vertices are adjacent vertices of the polygon, in counterclockwise order + + let angle = 2 * Math.PI / nVertices; + + let c = Math.cos(angle); + let s = Math.sin(angle); + + let desiredDirectionWithRadius2 = [desiredDirectionWithRadius[0] * c - desiredDirectionWithRadius[1] * s, desiredDirectionWithRadius[0] * s + desiredDirectionWithRadius[1] * c]; + + let desiredVertices = { + "0,0": me.fromAst(desiredDirectionWithRadius[0] + desiredCenter[0]), + "0,1": me.fromAst(desiredDirectionWithRadius[1] + desiredCenter[1]), + "1,0": me.fromAst(desiredDirectionWithRadius2[0] + desiredCenter[0]), + "1,1": me.fromAst(desiredDirectionWithRadius2[1] + desiredCenter[1]) + }; + + + instructions.push({ + setDependency: "verticesAttr", + desiredValue: desiredVertices, + variableIndex: 0, + }) + + } + + return { + success: true, + instructions + } + + } + + } + + + stateVariableDefinitions.vertices = { + public: true, + shadowingInstructions: { + createComponentOfType: "math", + returnWrappingComponents(prefix) { + if (prefix === "vertexX") { + return []; + } else { + // vertex or entire array + // wrap inner dimension by both and + // don't wrap outer dimension (for entire array) + return [["point", { componentType: "mathList", isAttribute: "xs" }]]; + } + }, + }, + isArray: true, + nDimensions: 2, + entryPrefixes: ["vertexX", "vertex"], + getArrayKeysFromVarName({ arrayEntryPrefix, varEnding, arraySize }) { + if (arrayEntryPrefix === "vertexX") { + // vertexX1_2 is the 2nd component of the first vertex + let indices = varEnding.split('_').map(x => Number(x) - 1) + if (indices.length === 2 && indices.every( + (x, i) => Number.isInteger(x) && x >= 0 + )) { + if (arraySize) { + if (indices.every((x, i) => x < arraySize[i])) { + return [String(indices)]; + } else { + return []; + } + } else { + // If not given the array size, + // then return the array keys assuming the array is large enough. + // Must do this as it is used to determine potential array entries. + return [String(indices)]; + } + } else { + return []; + } + } else { + // vertex3 is all components of the third vertex + + let pointInd = Number(varEnding) - 1; + if (!(Number.isInteger(pointInd) && pointInd >= 0)) { + return []; + } + + if (!arraySize) { + // If don't have array size, we just need to determine if it is a potential entry. + // Return the first entry assuming array is large enough + return [pointInd + ",0"]; + } + if (pointInd < arraySize[0]) { + // array of "pointInd,i", where i=0, ..., arraySize[1]-1 + return Array.from(Array(arraySize[1]), (_, i) => pointInd + "," + i) + } else { + return []; + } + } + + }, + arrayVarNameFromPropIndex(propIndex, varName) { + if (varName === "vertices") { + if (propIndex.length === 1) { + return "vertex" + propIndex[0]; + } else { + // if propIndex has additional entries, ignore them + return `vertexX${propIndex[0]}_${propIndex[1]}` + } + } + if (varName.slice(0, 6) === "vertex") { + // could be vertex or vertexX + let vertexNum = Number(varName.slice(6)); + if (Number.isInteger(vertexNum) && vertexNum > 0) { + // if propIndex has additional entries, ignore them + return `vertexX${vertexNum}_${propIndex[0]}` + } + } + return null; + }, + returnArraySizeDependencies: () => ({ + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + } + }), + returnArraySize({ dependencyValues }) { + return [dependencyValues.nVertices, 2]; + }, + returnArrayDependenciesByKey() { + let globalDependencies = { + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + centerComponents: { + dependencyType: "stateVariable", + variableName: "centerComponents" + }, + directionWithRadius: { + dependencyType: "stateVariable", + variableName: "directionWithRadius" + }, + + }; + + return { + globalDependencies + }; + }, + arrayDefinitionByKey({ globalDependencyValues }) { + + // just compute all vertices every time, as they are all mutually dependent + // (rather than just computing for the array keys requested) + + let nVertices = globalDependencyValues.nVertices; + + let center = globalDependencyValues.centerComponents; + let directionWithRadius = globalDependencyValues.directionWithRadius; + + + let vertices = {}; + + if (center.some(x => !Number.isFinite(x)) || directionWithRadius.some(x => !Number.isFinite(x))) { + for (let vertexInd = 0; vertexInd < nVertices; vertexInd++) { + vertices[`${vertexInd},0`] = me.fromAst('\uff3f') + vertices[`${vertexInd},1`] = me.fromAst('\uff3f') + + } + } else { + + for (let vertexInd = 0; vertexInd < nVertices; vertexInd++) { + + let rotation = vertexInd * 2 * Math.PI / nVertices; + + let s = Math.sin(rotation); + let c = Math.cos(rotation); + + vertices[`${vertexInd},0`] = me.fromAst(center[0] + directionWithRadius[0] * c - directionWithRadius[1] * s); + vertices[`${vertexInd},1`] = me.fromAst(center[1] + directionWithRadius[0] * s + directionWithRadius[1] * c); + + } + } + + return { setValue: { vertices } }; + }, + + async inverseArrayDefinitionByKey({ + desiredStateVariableValues, globalDependencyValues, + stateValues, workspace + }) { + + let nVertices = globalDependencyValues.nVertices; + + if (!workspace.desiredVertices) { + workspace.desiredVertices = {}; + } + Object.assign(workspace.desiredVertices, desiredStateVariableValues.vertices); + + + let desiredKeys = Object.keys(workspace.desiredVertices); + let vertexInd1String = desiredKeys[0].split(",")[0]; + let changingJustOneVertex = desiredKeys.every(v => v.split(",")[0] === vertexInd1String); + + let desiredCenter; + + if (changingJustOneVertex) { + // if change one vertex, then make sure that center stays the same + + desiredCenter = (await stateValues.center).map(evaluate_to_constant); + + } else { + + // if change multiple vertices, then calculate center as average of all vertices + + if (!workspace.allVertices) { + workspace.allVertices = {}; + } + + Object.assign(workspace.allVertices, workspace.desiredVertices); + + let center_x = 0, center_y = 0; + + for (let vertexInd = 0; vertexInd < nVertices; vertexInd++) { + + let v_x = workspace.allVertices[vertexInd + ",0"]; + if (!v_x) { + let vertices = await stateValues.vertices + v_x = vertices[vertexInd][0]; + workspace.allVertices[vertexInd + ",0"] = v_x; + } + + + let v_y = workspace.allVertices[vertexInd + ",1"]; + if (!v_y) { + let vertices = await stateValues.vertices + v_y = vertices[vertexInd][1]; + workspace.allVertices[vertexInd + ",1"] = v_y; + } + + center_x += evaluate_to_constant(v_x); + center_y += evaluate_to_constant(v_y); + } + + center_x /= nVertices; + center_y /= nVertices; + + desiredCenter = [center_x, center_y]; + + } + + + // use the first index found in desired indices to determine directionWithRadius + let vertexInd1 = Number(vertexInd1String); + + let desiredVertex_x = workspace.desiredVertices[vertexInd1String + ",0"]; + if (!desiredVertex_x) { + let vertices = await stateValues.vertices; + desiredVertex_x = vertices[vertexInd1][0]; + } + + let desiredVertex_y = workspace.desiredVertices[vertexInd1String + ",1"]; + if (!desiredVertex_y) { + let vertices = await stateValues.vertices; + desiredVertex_y = vertices[vertexInd1][1]; + } + + let desiredVertex = [evaluate_to_constant(desiredVertex_x), evaluate_to_constant(desiredVertex_y)]; + + let centerToVertex = [desiredVertex[0] - desiredCenter[0], desiredVertex[1] - desiredCenter[1]]; + + let angle = -vertexInd1 * 2 * Math.PI / nVertices; + + let c = Math.cos(angle); + let s = Math.sin(angle); + + let desiredDirectionWithRadius = [centerToVertex[0] * c - centerToVertex[1] * s, centerToVertex[0] * s + centerToVertex[1] * c]; + + + + let instructions = [{ + setDependency: "centerComponents", + desiredValue: desiredCenter, + }, { + setDependency: "directionWithRadius", + desiredValue: desiredDirectionWithRadius, + }]; + + + return { + success: true, + instructions + } + } + } + + + + stateVariableDefinitions.center = { + public: true, + isArray: true, + entryPrefixes: ["centerX"], + shadowingInstructions: { + createComponentOfType: "math", + returnWrappingComponents(prefix) { + if (prefix === "centerX") { + return []; + } else { + // entire array + // wrap by both and + return [["point", { componentType: "mathList", isAttribute: "xs" }]]; + } + }, + }, + + returnArraySizeDependencies: () => ({}), + returnArraySize: () => [2], + + returnArrayDependenciesByKey() { + + let globalDependencies = { + centerComponents: { + dependencyType: "stateVariable", + variableName: "centerComponents" + }, + }; + + return { globalDependencies }; + }, + + arrayDefinitionByKey({ globalDependencyValues }) { + + return { setValue: { center: globalDependencyValues.centerComponents.map(x => me.fromAst(x)) } }; + }, + + async inverseArrayDefinitionByKey({ desiredStateVariableValues, stateValues, workspace }) { + + + let desired_center_x = desiredStateVariableValues.center[0]; + if (!desired_center_x) { + desired_center_x = workspace.desired_center_x; + } + if (!desired_center_x) { + desired_center_x = (await stateValues.center)[0] + } + workspace.desired_center_x = desired_center_x; + + let desired_center_y = desiredStateVariableValues.center[1]; + if (!desired_center_y) { + desired_center_y = workspace.desired_center_y; + } + if (!desired_center_y) { + desired_center_y = (await stateValues.center)[1] + } + workspace.desired_center_y = desired_center_y; + + + + let instructions = [{ + setDependency: "centerComponents", + desiredValue: [evaluate_to_constant(desired_center_x), evaluate_to_constant(desired_center_y)], + }]; + + return { + success: true, + instructions + } + } + } + + + stateVariableDefinitions.circumradius = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + returnDependencies: () => ({ + center: { + dependencyType: "stateVariable", + variableName: "center" + }, + vertex1: { + dependencyType: "stateVariable", + variableName: "vertex1" + } + }), + definition({ dependencyValues }) { + let center = dependencyValues.center.map(evaluate_to_constant); + + let vertex1 = dependencyValues.vertex1.map(evaluate_to_constant); + + let circumradius = Math.sqrt( + (vertex1[0] - center[0]) ** 2 + + (vertex1[1] - center[1]) ** 2 + ) + + return { setValue: { circumradius } }; + + }, + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + let center = dependencyValues.center.map(evaluate_to_constant); + + let vertex1 = dependencyValues.vertex1.map(evaluate_to_constant); + + let directionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]]; + + let previousRadius = Math.sqrt(directionWithRadius[0] ** 2 + directionWithRadius[1] ** 2); + + let desiredRadius = desiredStateVariableValues.circumradius; + + let desiredDirectionWithRadius = directionWithRadius.map(x => x / previousRadius * desiredRadius); + + + let desiredVertex1 = [ + me.fromAst(desiredDirectionWithRadius[0] + center[0]), + me.fromAst(desiredDirectionWithRadius[1] + center[1]), + ] + + return { + success: true, + instructions: [{ + setDependency: "vertex1", + desiredValue: desiredVertex1 + }] + } + + } + } + + + + stateVariableDefinitions.radius = { + isAlias: true, + targetVariableName: "circumradius" + }; + + + + stateVariableDefinitions.inradius = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + returnDependencies: () => ({ + circumradius: { + dependencyType: "stateVariable", + variableName: "circumradius" + }, + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + }), + definition({ dependencyValues }) { + let circumradius = dependencyValues.circumradius; + let nVertices = dependencyValues.nVertices; + + let inradius = circumradius * Math.cos(Math.PI / nVertices) + + return { setValue: { inradius } }; + + }, + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + + let desiredInradius = desiredStateVariableValues.inradius; + let nVertices = dependencyValues.nVertices; + + let desiredCircumradius = desiredInradius / Math.cos(Math.PI / nVertices); + + return { + success: true, + instructions: [{ + setDependency: "circumradius", + desiredValue: desiredCircumradius + }] + } + + } + } + + + stateVariableDefinitions.apothem = { + isAlias: true, + targetVariableName: "inradius" + }; + + + stateVariableDefinitions.sideLength = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + returnDependencies: () => ({ + circumradius: { + dependencyType: "stateVariable", + variableName: "circumradius" + }, + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + }), + definition({ dependencyValues }) { + let circumradius = dependencyValues.circumradius; + let nVertices = dependencyValues.nVertices; + + let sideLength = circumradius * (2 * Math.sin(Math.PI / nVertices)) + + return { setValue: { sideLength } }; + + }, + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + + let desiredSideLength = desiredStateVariableValues.sideLength; + let nVertices = dependencyValues.nVertices; + + let desiredCircumradius = desiredSideLength / (2 * Math.sin(Math.PI / nVertices)); + + return { + success: true, + instructions: [{ + setDependency: "circumradius", + desiredValue: desiredCircumradius + }] + } + + } + } + + + stateVariableDefinitions.perimeter = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + returnDependencies: () => ({ + circumradius: { + dependencyType: "stateVariable", + variableName: "circumradius" + }, + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + }), + definition({ dependencyValues }) { + let circumradius = dependencyValues.circumradius; + let nVertices = dependencyValues.nVertices; + + let perimeter = circumradius * (2 * nVertices * Math.sin(Math.PI / nVertices)) + + return { setValue: { perimeter } }; + + }, + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + + let desiredPerimeter = desiredStateVariableValues.perimeter; + let nVertices = dependencyValues.nVertices; + + let desiredCircumradius = desiredPerimeter / (2 * nVertices * Math.sin(Math.PI / nVertices)); + + return { + success: true, + instructions: [{ + setDependency: "circumradius", + desiredValue: desiredCircumradius + }] + } + + } + } + + + stateVariableDefinitions.area = { + public: true, + shadowingInstructions: { + createComponentOfType: "number", + }, + returnDependencies: () => ({ + circumradius: { + dependencyType: "stateVariable", + variableName: "circumradius" + }, + nVertices: { + dependencyType: "stateVariable", + variableName: "nVertices" + }, + }), + definition({ dependencyValues }) { + let circumradius = dependencyValues.circumradius; + let nVertices = dependencyValues.nVertices; + + let area = circumradius ** 2 * (nVertices / 2 * Math.sin(2 * Math.PI / nVertices)) + + return { setValue: { area } }; + + }, + inverseDefinition({ desiredStateVariableValues, dependencyValues }) { + + let desiredArea = desiredStateVariableValues.area; + let nVertices = dependencyValues.nVertices; + + + + let desiredCircumradius = Math.sqrt(desiredArea + / (nVertices / 2 * Math.sin(2 * Math.PI / nVertices))) + + return { + success: true, + instructions: [{ + setDependency: "circumradius", + desiredValue: desiredCircumradius + }] + } + + } + } + + + + + stateVariableDefinitions.nSides = { + isAlias: true, + targetVariableName: "nVertices" + }; + + return stateVariableDefinitions; + } + + +} + +function evaluate_to_constant(x) { + let val = x.evaluate_to_constant(); + if (val === null) { + return NaN; + } else { + return val; + } +} \ No newline at end of file diff --git a/src/Parser/parser.js b/src/Parser/parser.js index e3907bc3da..301c712b5b 100644 --- a/src/Parser/parser.js +++ b/src/Parser/parser.js @@ -150,7 +150,7 @@ export function parseAndCompile(inText) { cursor.parent(); } - let range = { selfCloseBegin: tagBegin, selfCloseEnd: cursor.to + 2 }; + let range = { selfCloseBegin: tagBegin, selfCloseEnd: cursor.to + 3 }; // console.log(">>>toReturn", {componentType : tagName, props : attrs, children : []});