@@ -12042,4 +12042,194 @@ describe("Line Tag Tests", function () {
"rgb(0, 0, 255)",
);
});
+
+ it("line through two points, one constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+ (-4,-1)
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/_text1")).should("have.text", "a"); // to wait for page to load
+
+ let x1 = 4,
+ y1 = 6;
+ let x2 = -4,
+ y2 = -1;
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+
+ cy.log(
+ "move line down 4 and right 0.5 actually moves it down 3 and right none",
+ );
+ cy.window().then(async (win) => {
+ let dx = 0.5,
+ dy = -4;
+
+ let x1Desired = x1 + dx;
+ let y1Desired = y1 + dy;
+ let x2Desired = x2 + dx;
+ let y2Desired = y2 + dy;
+
+ dx = 0;
+ dy = -3;
+ x1 += dx;
+ y1 += dy;
+ x2 += dx;
+ y2 += dy;
+
+ win.callAction1({
+ actionName: "moveLine",
+ componentName: "/_line1",
+ args: {
+ point1coords: [x1Desired, y1Desired],
+ point2coords: [x2Desired, y2Desired],
+ },
+ });
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+ });
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+ });
+
+ it("line through two points, one constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+ (-4,-1)
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/_text1")).should("have.text", "a"); // to wait for page to load
+
+ let x1 = 4,
+ y1 = 6;
+ let x2 = -4,
+ y2 = -1;
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+
+ cy.log("move line down 4 and right 0.5");
+ cy.window().then(async (win) => {
+ let dx = 0.5,
+ dy = -4;
+
+ let x1Desired = x1 + dx;
+ let y1Desired = y1 + dy;
+ let x2Desired = x2 + dx;
+ let y2Desired = y2 + dy;
+
+ dx = 0;
+ dy = -3;
+ x1 += dx;
+ y1 += dy;
+ x2 = x2Desired;
+ y2 = y2Desired;
+
+ win.callAction1({
+ actionName: "moveLine",
+ componentName: "/_line1",
+ args: {
+ point1coords: [x1Desired, y1Desired],
+ point2coords: [x2Desired, y2Desired],
+ },
+ });
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+ });
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/linesegment.cy.js b/cypress/e2e/DoenetML/tagSpecific/linesegment.cy.js
index 5f25286975..5ce132d004 100644
--- a/cypress/e2e/DoenetML/tagSpecific/linesegment.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/linesegment.cy.js
@@ -4650,4 +4650,196 @@ describe("LineSegment Tag Tests", function () {
"C is a thin white line segment.",
);
});
+
+ it("line segment based on two endpoints, one constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+ (-4,-1)
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/_text1")).should("have.text", "a"); // to wait for page to load
+
+ let x1 = 4,
+ y1 = 6;
+ let x2 = -4,
+ y2 = -1;
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+
+ cy.log(
+ "move line down 4 and right 0.5 actually moves it down 3 and right none",
+ );
+ cy.window().then(async (win) => {
+ let dx = 0.5,
+ dy = -4;
+
+ let x1Desired = x1 + dx;
+ let y1Desired = y1 + dy;
+ let x2Desired = x2 + dx;
+ let y2Desired = y2 + dy;
+
+ dx = 0;
+ dy = -3;
+ x1 += dx;
+ y1 += dy;
+ x2 += dx;
+ y2 += dy;
+
+ win.callAction1({
+ actionName: "moveLineSegment",
+ componentName: "/_linesegment1",
+ args: {
+ point1coords: [x1Desired, y1Desired],
+ point2coords: [x2Desired, y2Desired],
+ },
+ });
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+ });
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+ });
+
+ it("line segment based on two endpoints, one constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+ (-4,-1)
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/_text1")).should("have.text", "a"); // to wait for page to load
+
+ let x1 = 4,
+ y1 = 6;
+ let x2 = -4,
+ y2 = -1;
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+
+ cy.log(
+ "move line down 4 and right 0.5 actually moves it down 3 and right none",
+ );
+ cy.window().then(async (win) => {
+ let dx = 0.5,
+ dy = -4;
+
+ let x1Desired = x1 + dx;
+ let y1Desired = y1 + dy;
+ let x2Desired = x2 + dx;
+ let y2Desired = y2 + dy;
+
+ dx = 0;
+ dy = -3;
+ x1 += dx;
+ y1 += dy;
+ x2 = x2Desired;
+ y2 = y2Desired;
+
+ win.callAction1({
+ actionName: "moveLineSegment",
+ componentName: "/_linesegment1",
+ args: {
+ point1coords: [x1Desired, y1Desired],
+ point2coords: [x2Desired, y2Desired],
+ },
+ });
+
+ cy.get(cesc2("#/Pa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x1)},${nInDOM(y1)})`,
+ );
+ cy.get(cesc2("#/Qa") + " .mjx-mrow").should(
+ "contain.text",
+ `(${nInDOM(x2)},${nInDOM(y2)})`,
+ );
+ });
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/P"].stateValues.xs[0]).eq(x1);
+ expect(stateVariables["/P"].stateValues.xs[1]).eq(y1);
+ expect(stateVariables["/P"].stateValues.coords).eqls(["vector", x1, y1]);
+ expect(stateVariables["/Q"].stateValues.xs[0]).eq(x2);
+ expect(stateVariables["/Q"].stateValues.xs[1]).eq(y2);
+ expect(stateVariables["/Q"].stateValues.coords).eqls(["vector", x2, y2]);
+ });
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/polygon.cy.js b/cypress/e2e/DoenetML/tagSpecific/polygon.cy.js
index f60cb6657a..c4f290ca2c 100644
--- a/cypress/e2e/DoenetML/tagSpecific/polygon.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/polygon.cy.js
@@ -4419,4 +4419,708 @@ describe("Polygon Tag Tests", function () {
);
cy.get(cesc("#\\/Cfilldescrip")).should("have.text", "C has a white fill.");
});
+
+ it("One vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 5],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
+
+ it("One vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 5],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ vertices[2][0] = vertices[2][0] + moveX;
+ vertices[2][1] = vertices[2][1] + moveY;
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ vertices[2][0] = vertices[2][0] + moveX;
+ vertices[2][1] = vertices[2][1] + moveY;
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
+
+ it("Two vertices constrained to same grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
+
+ it("Two vertices constrained to same grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ if ([0, 2].includes(i)) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ if ([0, 2].includes(i)) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
+
+ it("Three vertices constrained to same grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+
+
+
+
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-3, 0],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ // adjust for constraint
+ vertices[1] = [3, 8];
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
+
+ it("Three vertices constrained to same grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+
+
+
+
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-3, 0],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolygonCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ // adjust for constraint
+ vertices[1] = [3, 8];
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polygon up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ // all but last vertex
+ for (let i = 0; i < vertices.length - 1; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polygon down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ // all but last vertex
+ for (let i = 0; i < vertices.length - 1; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolygonCopiedTwice({ vertices });
+ });
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/polyline.cy.js b/cypress/e2e/DoenetML/tagSpecific/polyline.cy.js
index 141d934228..c38d8b7631 100644
--- a/cypress/e2e/DoenetML/tagSpecific/polyline.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/polyline.cy.js
@@ -4256,4 +4256,708 @@ describe("Polyline Tag Tests", function () {
"C is a thin white polyline.",
);
});
+
+ it("One vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 5],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
+
+ it("One vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 5],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ vertices[2][0] = vertices[2][0] + moveX;
+ vertices[2][1] = vertices[2][1] + moveY;
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ vertices[2][0] = vertices[2][0] + moveX;
+ vertices[2][1] = vertices[2][1] + moveY;
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
+
+ it("Two vertices constrained to same grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
+
+ it("Two vertices constrained to same grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-4, -1],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ if ([0, 2].includes(i)) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ if ([0, 2].includes(i)) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
+
+ it("Three vertices constrained to same grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+
+
+
+
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-3, 0],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ // adjust for constraint
+ vertices[1] = [3, 8];
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
+
+ it("Three vertices constrained to same grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (3,5)
+
+
+
+
+ (-4,-1)
+
+
+
+
+ (5,2)
+
+
+
+
+ (-3,4)
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+ cy.get(cesc("#\\/_text1")).should("have.text", "a"); //wait for page to load
+
+ let vertices = [
+ [3, 4],
+ [-3, 0],
+ [6, 4],
+ [-3, 4],
+ ];
+
+ testPolylineCopiedTwice({ vertices });
+
+ cy.log("move individual vertex");
+ cy.window().then(async (win) => {
+ vertices[1] = [4, 7];
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g1/pg",
+ args: {
+ pointCoords: { 1: vertices[1] },
+ },
+ });
+
+ // adjust for constraint
+ vertices[1] = [3, 8];
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("move copied polyline up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 4;
+ let moveY = 3;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g2/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = 1;
+ // all but last vertex
+ for (let i = 0; i < vertices.length - 1; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+
+ cy.log("try to move double copied polyline down and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 1;
+ let moveY = -7;
+
+ for (let i = 0; i < vertices.length; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ win.callAction1({
+ actionName: "movePolyline",
+ componentName: "/g3/pg",
+ args: {
+ pointCoords: vertices,
+ },
+ });
+
+ // adjustment due to constraint
+ moveX = -1;
+ moveY = -1;
+ // all but last vertex
+ for (let i = 0; i < vertices.length - 1; i++) {
+ vertices[i][0] = vertices[i][0] + moveX;
+ vertices[i][1] = vertices[i][1] + moveY;
+ }
+
+ testPolylineCopiedTwice({ vertices });
+ });
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/ray.cy.js b/cypress/e2e/DoenetML/tagSpecific/ray.cy.js
index ea64fadc4b..3da559f10b 100644
--- a/cypress/e2e/DoenetML/tagSpecific/ray.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/ray.cy.js
@@ -10362,4 +10362,686 @@ describe("Ray Tag Tests", function () {
cy.get(cesc("#\\/Bdescrip")).should("have.text", "B is a light red ray.");
cy.get(cesc("#\\/Cdescrip")).should("have.text", "C is a thin white ray.");
});
+
+ it("ray with through and endpoint, endpoint constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+
+
+
+
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let endpointx = 5;
+ let endpointy = 0;
+ let throughx = -4;
+ let throughy = 2;
+ let directionEndpointShiftx = 0;
+ let directionEndpointShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move ray up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ win.callAction1({
+ actionName: "moveRay",
+ componentName: "/_ray1",
+ args: {
+ endpointcoords: [endpointx, endpointy],
+ throughcoords: [throughx, throughy],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied through");
+ cy.window().then(async (win) => {
+ throughx = -5;
+ throughy = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/through",
+ args: { x: throughx, y: throughy },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied endpoint");
+ cy.window().then(async (win) => {
+ endpointx = -3;
+ endpointy = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/endpoint",
+ args: { x: endpointx, y: endpointy },
+ });
+
+ // adjust for constraints
+ endpointx = -5;
+ endpointy = -9;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied direction");
+ cy.window().then(async (win) => {
+ let directionEndpointShiftx = -4;
+ let directionEndpointShifty = -5;
+
+ let directionx = 2;
+ let directiony = -3;
+
+ throughx = endpointx + directionx;
+ throughy = endpointy + directiony;
+
+ let directionthroughx = directionEndpointShiftx + directionx;
+ let directionthroughy = directionEndpointShifty + directiony;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/direction",
+ args: {
+ tailcoords: [directionEndpointShiftx, directionEndpointShifty],
+ headcoords: [directionthroughx, directionthroughy],
+ },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+ });
+
+ it("ray with through and endpoint, endpoint constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+
+
+
+
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let endpointx = 5;
+ let endpointy = 0;
+ let throughx = -4;
+ let throughy = 2;
+ let directionEndpointShiftx = 0;
+ let directionEndpointShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move ray up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ win.callAction1({
+ actionName: "moveRay",
+ componentName: "/_ray1",
+ args: {
+ endpointcoords: [endpointx, endpointy],
+ throughcoords: [throughx, throughy],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ endpointx += moveX;
+ endpointy += moveY;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied through");
+ cy.window().then(async (win) => {
+ throughx = -5;
+ throughy = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/through",
+ args: { x: throughx, y: throughy },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied endpoint");
+ cy.window().then(async (win) => {
+ endpointx = -3;
+ endpointy = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/endpoint",
+ args: { x: endpointx, y: endpointy },
+ });
+
+ // adjust for constraints
+ endpointx = -5;
+ endpointy = -9;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied direction");
+ cy.window().then(async (win) => {
+ let directionEndpointShiftx = -4;
+ let directionEndpointShifty = -5;
+
+ let directionx = 2;
+ let directiony = -3;
+
+ throughx = endpointx + directionx;
+ throughy = endpointy + directiony;
+
+ let directionthroughx = directionEndpointShiftx + directionx;
+ let directionthroughy = directionEndpointShifty + directiony;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/direction",
+ args: {
+ tailcoords: [directionEndpointShiftx, directionEndpointShifty],
+ headcoords: [directionthroughx, directionthroughy],
+ },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+ });
+
+ it("ray with through and endpoint, through point constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let endpointx = 4;
+ let endpointy = 1;
+ let throughx = -5;
+ let throughy = 3;
+ let directionEndpointShiftx = 0;
+ let directionEndpointShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move ray up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ win.callAction1({
+ actionName: "moveRay",
+ componentName: "/_ray1",
+ args: {
+ endpointcoords: [endpointx, endpointy],
+ throughcoords: [throughx, throughy],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied through");
+ cy.window().then(async (win) => {
+ throughx = -5;
+ throughy = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/through",
+ args: { x: throughx, y: throughy },
+ });
+
+ // adjust for constraints
+ throughx = -5;
+ throughy = 6;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied endpoint");
+ cy.window().then(async (win) => {
+ endpointx = -3;
+ endpointy = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/endpoint",
+ args: { x: endpointx, y: endpointy },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied direction");
+ cy.window().then(async (win) => {
+ let directionEndpointShiftx = -4;
+ let directionEndpointShifty = -5;
+
+ let directionx = 2;
+ let directiony = -3;
+
+ throughx = endpointx + directionx;
+ throughy = endpointy + directiony;
+
+ let directionthroughx = directionEndpointShiftx + directionx;
+ let directionthroughy = directionEndpointShifty + directiony;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/direction",
+ args: {
+ tailcoords: [directionEndpointShiftx, directionEndpointShifty],
+ headcoords: [directionthroughx, directionthroughy],
+ },
+ });
+
+ // adjust for constraints
+ throughx = Math.round(throughx / 5) * 5;
+ throughy = Math.round(throughy / 3) * 3;
+ throughx = throughx === 0 ? 0 : throughx; // change -0 to 0
+ directionx = throughx - endpointx;
+ directiony = throughy - endpointy;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+ });
+
+ it("ray with through and endpoint, through point constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let endpointx = 4;
+ let endpointy = 1;
+ let throughx = -5;
+ let throughy = 3;
+ let directionEndpointShiftx = 0;
+ let directionEndpointShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move ray up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ endpointx += moveX;
+ throughx += moveX;
+ endpointy += moveY;
+ throughy += moveY;
+
+ win.callAction1({
+ actionName: "moveRay",
+ componentName: "/_ray1",
+ args: {
+ endpointcoords: [endpointx, endpointy],
+ throughcoords: [throughx, throughy],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ throughx += moveX;
+ throughy += moveY;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied through");
+ cy.window().then(async (win) => {
+ throughx = -5;
+ throughy = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/through",
+ args: { x: throughx, y: throughy },
+ });
+
+ // adjust for constraints
+ throughx = -5;
+ throughy = 6;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied endpoint");
+ cy.window().then(async (win) => {
+ endpointx = -3;
+ endpointy = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/endpoint",
+ args: { x: endpointx, y: endpointy },
+ });
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+
+ cy.log("move copied direction");
+ cy.window().then(async (win) => {
+ let directionEndpointShiftx = -4;
+ let directionEndpointShifty = -5;
+
+ let directionx = 2;
+ let directiony = -3;
+
+ throughx = endpointx + directionx;
+ throughy = endpointy + directiony;
+
+ let directionthroughx = directionEndpointShiftx + directionx;
+ let directionthroughy = directionEndpointShifty + directiony;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/direction",
+ args: {
+ tailcoords: [directionEndpointShiftx, directionEndpointShifty],
+ headcoords: [directionthroughx, directionthroughy],
+ },
+ });
+
+ // adjust for constraints
+ throughx = Math.round(throughx / 5) * 5;
+ throughy = Math.round(throughy / 3) * 3;
+ throughx = throughx === 0 ? 0 : throughx; // change -0 to 0
+ directionx = throughx - endpointx;
+ directiony = throughy - endpointy;
+
+ await testRayCopiedHTD({
+ throughx,
+ throughy,
+ endpointx,
+ endpointy,
+ directionEndpointShiftx,
+ directionEndpointShifty,
+ });
+ });
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js b/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js
index 247e5b3e64..21c9f5cb82 100644
--- a/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/rectangle.cy.js
@@ -1,5 +1,5 @@
import me from "math-expressions";
-import { cesc } from "../../../../src/_utils/url";
+import { cesc, cesc2 } from "../../../../src/_utils/url";
function nInDOM(n) {
if (n < 0) {
@@ -1215,6 +1215,383 @@ describe("Rectangle Tag Tests", function () {
expect(stateVariables["/p"].stateValues.verticesDraggable).eq(false);
});
});
+
+ it("single vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(1,5)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [8, 9],
+ [9, 9],
+ [9, 10],
+ [8, 10],
+ ],
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(9,10)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(9,10)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(10,11)");
+ });
+
+ it("two vertices, first vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-8, -9],
+ [-2, -9],
+ [-2, -8],
+ [-8, -8],
+ ],
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−9,−8)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−9,−8)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−3,−7)");
+ });
+
+ it("two vertices, first vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-8, -9],
+ [-2, -9],
+ [-2, -8],
+ [-8, -8],
+ ],
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−9,−8)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−9,−8)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−2,−8)");
+ });
+
+ it("center and vertex, vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(12,6)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-8, -9],
+ [4, -9],
+ [4, -7],
+ [-8, -7],
+ ],
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−9,−8)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−9,−8)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(3,−6)");
+ });
+
+ it("center and vertex, vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(12,6)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-8, -9],
+ [4, -9],
+ [4, -7],
+ [-8, -7],
+ ],
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−9,−8)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−9,−8)");
+
+ // keep center at (-2, -8)
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(5,−8)");
+ });
+
+ it("center and vertex, center constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+ (6,5)
+
+
+
+
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(1,3)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(11,9)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-9, -8],
+ [1, -8],
+ [1, -2],
+ [-9, -2],
+ ],
+ },
+ });
+ });
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−8,−7)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−8,−7)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(2,−1)");
+ });
+
+ it("center and vertex, center constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+ (6,5)
+
+
+
+
+
+
+ corner vertices: $p.vertex1{assignNames="v1"} $p.vertex3{assignNames="v3"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(1,3)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(11,9)");
+
+ cy.log("move rectangle");
+ cy.window().then(async (win) => {
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords: [
+ [-9, -8],
+ [1, -8],
+ [1, -2],
+ [-9, -2],
+ ],
+ },
+ });
+ });
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−9,−8)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−9,−8)");
+ cy.get(cesc2("#/v3") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(3,0)");
+ });
});
function setupScene({ rectangleProperties, rectangleChildren }) {
diff --git a/cypress/e2e/DoenetML/tagSpecific/regularPolygon2.cy.js b/cypress/e2e/DoenetML/tagSpecific/regularPolygon2.cy.js
index 7798c18776..e31060c2cd 100644
--- a/cypress/e2e/DoenetML/tagSpecific/regularPolygon2.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/regularPolygon2.cy.js
@@ -1,4 +1,4 @@
-import { cesc } from "../../../../src/_utils/url";
+import { cesc, cesc2 } from "../../../../src/_utils/url";
import { runTests, setupScene } from "./regularPolygonUtils";
describe("Regular Polygon Tag Tests", function () {
@@ -678,4 +678,368 @@ describe("Regular Polygon Tag Tests", function () {
expect(stateVariables["/p"].stateValues.verticesDraggable).eq(false);
});
});
+
+ it("two vertices, first vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ First two vertices: $p.vertex1{assignNames="v1"} $p.vertex2{assignNames="v2" displaySmallAsZero}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v2") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−6,0)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−6,0)");
+ cy.get(cesc2("#/v2") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,1)");
+ });
+
+ it("two vertices, first vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ First two vertices: $p.vertex1{assignNames="v1"} $p.vertex2{assignNames="v2" displaySmallAsZero}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/v2") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−6,0)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−6,0)");
+ cy.get(cesc2("#/v2") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−1,0)");
+ });
+
+ it("center and vertex, vertex constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ First two vertex: $p.vertex1{assignNames="v1"}
+ Center: $p.center{assignNames="c"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−6,0)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−6,0)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,1)");
+ });
+
+ it("center and vertex, vertex constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+
+
+
+
+ (6,5)
+
+
+ First vertex: $p.vertex1{assignNames="v1"}
+ Center: $p.center{assignNames="c" displaySmallAsZero}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,4)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,5)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−6,0)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−6,0)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−1,0)");
+ });
+
+ it("center and vertex, center constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+ (6,5)
+
+
+
+
+
+
+ First vertex: $p.vertex1{assignNames="v1"}
+ Center: $p.center{assignNames="c"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(1,3)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,6)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−5,−1)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−5,−1)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,2)");
+ });
+
+ it("center and vertex, center constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+
+ (1,3)
+ (6,5)
+
+
+
+
+
+
+ First vertex: $p.vertex1{assignNames="v1"}
+ Center: $p.center{assignNames="c"}
+ `,
+ },
+ "*",
+ );
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(1,3)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(6,6)");
+
+ cy.log("move pentagon");
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+
+ let numericalVertices =
+ stateVariables["/p"].stateValues.numericalVertices;
+
+ let dx = -7;
+ let dy = -5;
+
+ let pointCoords = numericalVertices.map((v) => [v[0] + dx, v[1] + dy]);
+
+ await win.callAction1({
+ actionName: "movePolygon",
+ componentName: "/p",
+ args: {
+ pointCoords,
+ },
+ });
+ });
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow").should("contain.text", "(−6,−2)");
+
+ cy.get(cesc2("#/v1") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(−6,−2)");
+ cy.get(cesc2("#/c") + " .mjx-mrow")
+ .eq(0)
+ .should("have.text", "(0,2)");
+ });
});
diff --git a/cypress/e2e/DoenetML/tagSpecific/vector.cy.js b/cypress/e2e/DoenetML/tagSpecific/vector.cy.js
index 2a6d9965c4..99ae6bbc6b 100644
--- a/cypress/e2e/DoenetML/tagSpecific/vector.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/vector.cy.js
@@ -19694,4 +19694,686 @@ describe("Vector Tag Tests", function () {
"rgb(0, 0, 255)",
);
});
+
+ it("vector with head and tail, tail constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+
+
+
+
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let tailx = 5;
+ let taily = 0;
+ let headx = -4;
+ let heady = 2;
+ let displacementTailShiftx = 0;
+ let displacementTailShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move vector up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/_vector1",
+ args: {
+ tailcoords: [tailx, taily],
+ headcoords: [headx, heady],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied head");
+ cy.window().then(async (win) => {
+ headx = -5;
+ heady = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/head",
+ args: { x: headx, y: heady },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied tail");
+ cy.window().then(async (win) => {
+ tailx = -3;
+ taily = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/tail",
+ args: { x: tailx, y: taily },
+ });
+
+ // adjust for constraints
+ tailx = -5;
+ taily = -9;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied displacement");
+ cy.window().then(async (win) => {
+ let displacementTailShiftx = -4;
+ let displacementTailShifty = -5;
+
+ let displacementx = 2;
+ let displacementy = -3;
+
+ headx = tailx + displacementx;
+ heady = taily + displacementy;
+
+ let displacementheadx = displacementTailShiftx + displacementx;
+ let displacementheady = displacementTailShifty + displacementy;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/displacement",
+ args: {
+ tailcoords: [displacementTailShiftx, displacementTailShifty],
+ headcoords: [displacementheadx, displacementheady],
+ },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+ });
+
+ it("vector with head and tail, tail constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+
+
+
+
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let tailx = 5;
+ let taily = 0;
+ let headx = -4;
+ let heady = 2;
+ let displacementTailShiftx = 0;
+ let displacementTailShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move vector up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/_vector1",
+ args: {
+ tailcoords: [tailx, taily],
+ headcoords: [headx, heady],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ tailx += moveX;
+ taily += moveY;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied head");
+ cy.window().then(async (win) => {
+ headx = -5;
+ heady = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/head",
+ args: { x: headx, y: heady },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied tail");
+ cy.window().then(async (win) => {
+ tailx = -3;
+ taily = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/tail",
+ args: { x: tailx, y: taily },
+ });
+
+ // adjust for constraints
+ tailx = -5;
+ taily = -9;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied displacement");
+ cy.window().then(async (win) => {
+ let displacementTailShiftx = -4;
+ let displacementTailShifty = -5;
+
+ let displacementx = 2;
+ let displacementy = -3;
+
+ headx = tailx + displacementx;
+ heady = taily + displacementy;
+
+ let displacementheadx = displacementTailShiftx + displacementx;
+ let displacementheady = displacementTailShifty + displacementy;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/displacement",
+ args: {
+ tailcoords: [displacementTailShiftx, displacementTailShifty],
+ headcoords: [displacementheadx, displacementheady],
+ },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+ });
+
+ it("vector with head and tail, head constrained to grid", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let tailx = 4;
+ let taily = 1;
+ let headx = -5;
+ let heady = 3;
+ let displacementTailShiftx = 0;
+ let displacementTailShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move vector up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/_vector1",
+ args: {
+ tailcoords: [tailx, taily],
+ headcoords: [headx, heady],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied head");
+ cy.window().then(async (win) => {
+ headx = -5;
+ heady = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/head",
+ args: { x: headx, y: heady },
+ });
+
+ // adjust for constraints
+ headx = -5;
+ heady = 6;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied tail");
+ cy.window().then(async (win) => {
+ tailx = -3;
+ taily = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/tail",
+ args: { x: tailx, y: taily },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied displacement");
+ cy.window().then(async (win) => {
+ let displacementTailShiftx = -4;
+ let displacementTailShifty = -5;
+
+ let displacementx = 2;
+ let displacementy = -3;
+
+ headx = tailx + displacementx;
+ heady = taily + displacementy;
+
+ let displacementheadx = displacementTailShiftx + displacementx;
+ let displacementheady = displacementTailShifty + displacementy;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/displacement",
+ args: {
+ tailcoords: [displacementTailShiftx, displacementTailShifty],
+ headcoords: [displacementheadx, displacementheady],
+ },
+ });
+
+ // adjust for constraints
+ headx = Math.round(headx / 5) * 5;
+ heady = Math.round(heady / 3) * 3;
+ headx = headx === 0 ? 0 : headx; // change -0 to 0
+ displacementx = headx - tailx;
+ displacementy = heady - taily;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+ });
+
+ it("vector with head and tail, head constrained to grid, allow flexible motion", () => {
+ cy.window().then(async (win) => {
+ win.postMessage(
+ {
+ doenetML: `
+ a
+
+ (4,1)
+ (-4,2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ "*",
+ );
+ });
+
+ // to wait for page to load
+ cy.get(cesc("#\\/_text1")).should("have.text", "a");
+
+ let tailx = 4;
+ let taily = 1;
+ let headx = -5;
+ let heady = 3;
+ let displacementTailShiftx = 0;
+ let displacementTailShifty = 0;
+
+ cy.window().then(async (win) => {
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move vector up and to the right");
+ cy.window().then(async (win) => {
+ let moveX = 3;
+ let moveY = 2;
+ tailx += moveX;
+ headx += moveX;
+ taily += moveY;
+ heady += moveY;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/_vector1",
+ args: {
+ tailcoords: [tailx, taily],
+ headcoords: [headx, heady],
+ },
+ });
+
+ // adjust for constraints
+ moveX = 2;
+ moveY = 1;
+ headx += moveX;
+ heady += moveY;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied head");
+ cy.window().then(async (win) => {
+ headx = -5;
+ heady = 7;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/head",
+ args: { x: headx, y: heady },
+ });
+
+ // adjust for constraints
+ headx = -5;
+ heady = 6;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied tail");
+ cy.window().then(async (win) => {
+ tailx = -3;
+ taily = -9;
+
+ win.callAction1({
+ actionName: "movePoint",
+ componentName: "/tail",
+ args: { x: tailx, y: taily },
+ });
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+
+ cy.log("move copied displacement");
+ cy.window().then(async (win) => {
+ let displacementTailShiftx = -4;
+ let displacementTailShifty = -5;
+
+ let displacementx = 2;
+ let displacementy = -3;
+
+ headx = tailx + displacementx;
+ heady = taily + displacementy;
+
+ let displacementheadx = displacementTailShiftx + displacementx;
+ let displacementheady = displacementTailShifty + displacementy;
+
+ win.callAction1({
+ actionName: "moveVector",
+ componentName: "/displacement",
+ args: {
+ tailcoords: [displacementTailShiftx, displacementTailShifty],
+ headcoords: [displacementheadx, displacementheady],
+ },
+ });
+
+ // adjust for constraints
+ headx = Math.round(headx / 5) * 5;
+ heady = Math.round(heady / 3) * 3;
+ headx = headx === 0 ? 0 : headx; // change -0 to 0
+ displacementx = headx - tailx;
+ displacementy = heady - taily;
+
+ await testVectorCopiedHTD({
+ headx,
+ heady,
+ tailx,
+ taily,
+ displacementTailShiftx,
+ displacementTailShifty,
+ });
+ });
+ });
});
diff --git a/src/Core/Core.js b/src/Core/Core.js
index 861e95e4b2..36d57a6cc7 100644
--- a/src/Core/Core.js
+++ b/src/Core/Core.js
@@ -83,6 +83,7 @@ export default class Core {
performAction: this.performAction.bind(this),
resolveAction: this.resolveAction.bind(this),
triggerChainedActions: this.triggerChainedActions.bind(this),
+ updateRenderers: this.updateRenderers.bind(this),
requestRecordEvent: this.requestRecordEvent.bind(this),
requestAnimationFrame: this.requestAnimationFrame.bind(this),
cancelAnimationFrame: this.cancelAnimationFrame.bind(this),
@@ -9260,6 +9261,16 @@ export default class Core {
}
}
+ async updateRenderers({
+ actionId,
+ sourceInformation = {},
+ skipRendererUpdate = false,
+ }) {
+ if (!skipRendererUpdate) {
+ await this.updateAllChangedRenderers(sourceInformation, actionId);
+ }
+ }
+
async requestUpdate({
updateInstructions,
transient = false,
@@ -9346,6 +9357,7 @@ export default class Core {
canSkipUpdatingRenderer = false,
skipRendererUpdate = false,
sourceInformation = {},
+ largerMoveOptions,
suppressToast = false, // temporary
}) {
if (this.flags.readOnly && !overrideReadOnly) {
@@ -9454,6 +9466,15 @@ export default class Core {
newStateVariableValuesProcessed.push(newStateVariableValues);
+ if (largerMoveOptions) {
+ await this.attemptLargerMoveIfUnchanged(
+ largerMoveOptions,
+ workspace,
+ newStateVariableValuesProcessed,
+ sourceInformation,
+ );
+ }
+
// always update the renderers from the update instructions themselves,
// as even if changes were prevented, the renderers need to be given that information
// so they can revert if the showed the changes before hearing back from core
@@ -9609,6 +9630,184 @@ export default class Core {
}
}
+ // Check to see if state variable specified is unchanged.
+ // If so, attempt larger move
+ async attemptLargerMoveIfUnchanged(
+ largerMoveOptions,
+ workspace,
+ newStateVariableValuesProcessed,
+ sourceInformation,
+ ) {
+ let componentName = largerMoveOptions.componentName;
+ let stateVariable = largerMoveOptions.stateVariable;
+ let originalValue = largerMoveOptions.originalValue;
+ let limits = largerMoveOptions.limits;
+
+ // unchangedTolerance specifies what is considered unchanged
+ let unchangedTolerance = 1e-2;
+ if (limits) {
+ let scale =
+ (Math.abs(limits[0][1] - limits[0][0]) +
+ Math.abs(limits[1][1] - limits[1][0])) /
+ 2;
+ if (Number.isFinite(scale)) {
+ unchangedTolerance = scale * 1e-3;
+ }
+ }
+
+ let newValue = await this._components[componentName].state[stateVariable]
+ .value;
+
+ let haveArray = false;
+
+ // the function to determine if we have a large enough change in the state variable
+ let checkIfUnchanged;
+
+ if (newValue instanceof me.class && originalValue instanceof me.class) {
+ checkIfUnchanged = (expr1, expr2, tol) =>
+ expr1.equals(expr2, {
+ allowed_error_in_numbers: tol,
+ allowed_error_is_absolute: true,
+ });
+ } else if (
+ Array.isArray(newValue) &&
+ newValue.every((x) => x instanceof me.class) &&
+ Array.isArray(originalValue) &&
+ originalValue.every((x) => x instanceof me.class)
+ ) {
+ checkIfUnchanged = (expr1, expr2, tol) =>
+ expr1.length === expr2.length &&
+ expr1.every((v, i) => {
+ return v.equals(expr2[i], {
+ allowed_error_in_numbers: tol,
+ allowed_error_is_absolute: true,
+ });
+ });
+
+ haveArray = true;
+ } else {
+ // only implemented for math-expressions or arrray of math-expressions so far
+ return;
+ }
+
+ if (!checkIfUnchanged(newValue, originalValue, unchangedTolerance)) {
+ // if the value changed by more than unchangedTolerance, we're done
+ return;
+ }
+
+ // Note: since we typically will have found a change and won't continue past here,
+ // we want to keep the above code as minimal as possible
+
+ let requestedValue = largerMoveOptions.requestedValue;
+
+ // so far, we've implemented this algorithm only for vectors of numbers
+ // and arrays of numerical math-expressions
+ let originalNums, requestedNums;
+
+ if (haveArray) {
+ originalNums = originalValue.map((x) => x.tree);
+ requestedNums = requestedValue.map((x) => x.tree);
+ } else {
+ if (
+ originalValue.tree[0] === "vector" &&
+ requestedValue.tree[0] === "vector"
+ ) {
+ originalNums = originalValue.tree.slice(1);
+ requestedNums = requestedValue.tree.slice(1);
+ } else {
+ // if have single math-expression, only implemented for vectors
+ // (could implement for all vectorOperators if we find a case where it is needed)
+ return;
+ }
+ }
+
+ if (
+ !(
+ originalNums.every(Number.isFinite) &&
+ requestedNums.every(Number.isFinite)
+ )
+ ) {
+ // only implemented for numerical components
+ return;
+ }
+
+ let previousDesiredNums = requestedNums;
+
+ // try steps up to about 100 times larger, doubling each time so takes at most 8 tries
+ // (it takes about 100 unconstrained steps to cross width of full size graph)
+ for (let stepMult = 2; stepMult <= 128; stepMult *= 2) {
+ //////////////////////////////////////////////////
+ // calculate a value of stateVariables that is further away from originalValue
+ //////////////////////////////////////////////////
+
+ let newDesiredNums = originalNums.map(
+ (v, i) => v + stepMult * (requestedNums[i] - v),
+ );
+ if (limits) {
+ newDesiredNums = newDesiredNums.map((v, i) =>
+ Math.min(limits[i][1], Math.max(limits[i][0], v)),
+ );
+ }
+
+ if (newDesiredNums.every((v, i) => previousDesiredNums[i] === v)) {
+ // the desired position isn't changing at all,
+ // so either we ran into a limit
+ // or the requestedValue was the same as the originalValue
+ return;
+ }
+
+ previousDesiredNums = newDesiredNums;
+
+ let value = haveArray
+ ? newDesiredNums.map((x) => me.fromAst(x))
+ : me.fromAst(["vector", ...newDesiredNums]);
+
+ //////////////////////////////////////////////////
+ // repeat the base procedure from performUpdate
+ // to try to change stateVariable to this new value
+ //////////////////////////////////////////////////
+
+ let instruction = {
+ updateType: "updateValue",
+ componentName: componentName,
+ stateVariable: stateVariable,
+ value,
+ };
+
+ let newStateVariableValues = {};
+ await this.requestComponentChanges({
+ instruction,
+ workspace,
+ newStateVariableValues,
+ });
+
+ await this.executeUpdateStateVariables(newStateVariableValues);
+
+ newStateVariableValuesProcessed.push(newStateVariableValues);
+
+ //////////////////////////////////////////////////
+ // check if we've succeeded to obtain a value sufficiently far away from originalValue
+ //////////////////////////////////////////////////
+
+ let newValue = await this._components[componentName].stateValues[
+ stateVariable
+ ];
+
+ if (!checkIfUnchanged(newValue, originalValue, unchangedTolerance)) {
+ // found a large enough movement, so we are unstuck
+
+ let componentSourceInformation = sourceInformation[componentName];
+ if (!componentSourceInformation) {
+ componentSourceInformation = sourceInformation[componentName] = {};
+ }
+
+ componentSourceInformation.largeMoveModification = true;
+
+ return;
+ }
+ }
+ }
+
async updateAllChangedRenderers(sourceInformation = {}, actionId) {
let componentNamesToUpdate = [
...this.updateInfo.componentsToUpdateRenderers,
diff --git a/src/Core/components/BooleanInput.js b/src/Core/components/BooleanInput.js
index 0e5dab84d3..52bbeeb92d 100644
--- a/src/Core/components/BooleanInput.js
+++ b/src/Core/components/BooleanInput.js
@@ -244,6 +244,9 @@ export default class BooleanInput extends Input {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -253,6 +256,9 @@ export default class BooleanInput extends Input {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/CallAction.js b/src/Core/components/CallAction.js
index 92e2497425..3cf2c04404 100644
--- a/src/Core/components/CallAction.js
+++ b/src/Core/components/CallAction.js
@@ -241,6 +241,9 @@ export default class CallAction extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -250,6 +253,9 @@ export default class CallAction extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Circle.js b/src/Core/components/Circle.js
index 1da9beca2a..f2e11e3270 100644
--- a/src/Core/components/Circle.js
+++ b/src/Core/components/Circle.js
@@ -38,6 +38,13 @@ export default class Circle extends Curve {
forRenderer: true,
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
delete attributes.parMin;
delete attributes.parMax;
delete attributes.variable;
@@ -1237,7 +1244,7 @@ export default class Circle extends Curve {
if (pt === undefined) {
// if dependencies haven't been updated,
// it is possible to temporarility have fewer numericalThroughPoints
- // than nThroughPOints
+ // than nThroughPoints
return { setValue: { numericalRadius: NaN } };
}
let numericalRadius = Math.sqrt(
@@ -1586,7 +1593,7 @@ export default class Circle extends Curve {
if (globalDependencyValues.numericalThroughPoints.length < 1) {
// if dependencies haven't been updated,
// it is possible to temporarility have fewer numericalThroughPoints
- // than nThroughPOints
+ // than nThroughPoints
return { setValue: { numericalCenter: [NaN, NaN] } };
}
@@ -1601,7 +1608,7 @@ export default class Circle extends Curve {
if (globalDependencyValues.numericalThroughPoints.length < 2) {
// if dependencies haven't been updated,
// it is possible to temporarility have fewer numericalThroughPoints
- // than nThroughPOints
+ // than nThroughPoints
return { setValue: { numericalCenter: [NaN, NaN] } };
}
@@ -2528,7 +2535,7 @@ export default class Circle extends Curve {
let numericalPrescribedCenter = await this.stateValues
.numericalPrescribedCenter;
- if (nThroughPoints <= 1 || numericalPrescribedCenter !== null) {
+ if (nThroughPoints <= 1 || numericalPrescribedCenter.length > 0) {
instructions.push({
updateType: "updateValue",
componentName: this.componentName,
@@ -2537,16 +2544,16 @@ export default class Circle extends Curve {
});
}
+ let numericalThroughPoints = [];
+
if (nThroughPoints >= 1) {
// set numerical through points for two reasons
- // 1. if have cricle prescribed by center and one point
+ // 1. if have circle prescribed by center and one point
// need to move the point to preserve the radius
// 2. if have through points that are constrained/attracted
// to objects, set through points to attempt to keep their relative
// positions constant even as they get adjusted by the constraints
- let numericalThroughPoints = [];
-
if (throughAngles === undefined) {
throughAngles = await this.stateValues.throughAngles;
}
@@ -2571,20 +2578,22 @@ export default class Circle extends Curve {
});
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: instructions,
transient,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: instructions,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -2597,6 +2606,175 @@ export default class Circle extends Curve {
},
});
}
+
+ // unless allowFlexibleMotion is set
+ // we attempt to keep the radius of the circle fixed
+ // even if one of the points defining it is constrained
+
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ // if circle is based on more than 1 point (center or throughpoints)
+ // and a subset of those points appear to be constrained while preserving their relationship
+ // then move the other points in attempt to preserve their relationship with the constrained points,
+ // which will keep the radius of the circle fixed
+
+ let resultingCenter = await this.stateValues.numericalCenter;
+ let resultingNumericalThroughPoints = await this.stateValues
+ .numericalThroughPoints;
+
+ if (numericalPrescribedCenter.length > 0 && nThroughPoints === 1) {
+ // center and one through point
+
+ let throughPointUnchanged = numericalThroughPoints[0].every(
+ (v, i) => v === resultingNumericalThroughPoints[0][i],
+ );
+
+ let centerUnchanged = center.every((v, i) => v === resultingCenter[i]);
+
+ if (throughPointUnchanged && !centerUnchanged) {
+ let theta = throughAngles[0];
+ let newNumericalThroughPoints = [
+ [
+ resultingCenter[0] + radius * Math.cos(theta),
+ resultingCenter[1] + radius * Math.sin(theta),
+ ],
+ ];
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "numericalThroughPoints",
+ value: newNumericalThroughPoints,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ } else if (centerUnchanged && !throughPointUnchanged) {
+ let theta = throughAngles[0];
+ let newCenter = [
+ resultingNumericalThroughPoints[0][0] - radius * Math.cos(theta),
+ resultingNumericalThroughPoints[0][1] - radius * Math.sin(theta),
+ ];
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "numericalCenter",
+ value: newCenter,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ } else if (nThroughPoints >= 2) {
+ let throughPointsChanged = [];
+ let nThroughPointsChanged = 0;
+
+ for (let [ind, pt] of numericalThroughPoints.entries()) {
+ if (
+ !pt.every((v, i) => v === resultingNumericalThroughPoints[ind][i])
+ ) {
+ throughPointsChanged.push(ind);
+ nThroughPointsChanged++;
+ }
+ }
+
+ if (
+ nThroughPointsChanged > 0 &&
+ nThroughPointsChanged < nThroughPoints
+ ) {
+ // A subset of points were altered from the requested location.
+ // Check to see if the relationship among them is preserved
+
+ let changedInd1 = throughPointsChanged[0];
+ let relationshipPreserved = true;
+
+ if (nThroughPointsChanged > 1) {
+ let orig1 = numericalThroughPoints[changedInd1];
+ let changed1 = resultingNumericalThroughPoints[changedInd1];
+ let tol = 1e-6;
+
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ for (let ind of throughPointsChanged.slice(1)) {
+ let orig2 = numericalThroughPoints[ind];
+ let changed2 = resultingNumericalThroughPoints[ind];
+ let changevec2 = orig2.map((v, i) => v - changed2[i]);
+
+ if (
+ !changevec1.every((v, i) => Math.abs(v - changevec2[i]) < tol)
+ ) {
+ relationshipPreserved = false;
+ break;
+ }
+ }
+ }
+
+ if (relationshipPreserved) {
+ let thetaOfChanged = throughAngles[changedInd1];
+
+ let newCenter = [
+ resultingNumericalThroughPoints[changedInd1][0] -
+ radius * Math.cos(thetaOfChanged),
+ resultingNumericalThroughPoints[changedInd1][1] -
+ radius * Math.sin(thetaOfChanged),
+ ];
+
+ let newNumericalThroughPoints = [];
+
+ for (let i = 0; i < nThroughPoints; i++) {
+ if (throughPointsChanged.includes(i)) {
+ newNumericalThroughPoints.push(
+ resultingNumericalThroughPoints[i],
+ );
+ } else {
+ let theta = throughAngles[i];
+ let pt = [
+ newCenter[0] + radius * Math.cos(theta),
+ newCenter[1] + radius * Math.sin(theta),
+ ];
+ newNumericalThroughPoints.push(pt);
+ }
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "numericalThroughPoints",
+ value: newNumericalThroughPoints,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
async circleClicked({
diff --git a/src/Core/components/Image.js b/src/Core/components/Image.js
index 9b3d94e0da..dcf09ddc89 100644
--- a/src/Core/components/Image.js
+++ b/src/Core/components/Image.js
@@ -454,6 +454,9 @@ export default class Image extends BlockComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -463,6 +466,9 @@ export default class Image extends BlockComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Label.js b/src/Core/components/Label.js
index 8b5eb72ff6..8df830da6c 100644
--- a/src/Core/components/Label.js
+++ b/src/Core/components/Label.js
@@ -434,6 +434,9 @@ export default class Label extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -443,6 +446,9 @@ export default class Label extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Line.js b/src/Core/components/Line.js
index 5b6e023e20..702f45606c 100644
--- a/src/Core/components/Line.js
+++ b/src/Core/components/Line.js
@@ -81,6 +81,13 @@ export default class Line extends GraphicalComponent {
validValues: ["upperright", "upperleft", "lowerright", "lowerleft"],
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
return attributes;
}
@@ -1794,8 +1801,10 @@ export default class Line extends GraphicalComponent {
desiredPoints["1,1"] = me.fromAst(point2coords[1]);
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
updateType: "updateValue",
@@ -1807,10 +1816,10 @@ export default class Line extends GraphicalComponent {
transient: true,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
updateType: "updateValue",
@@ -1821,7 +1830,7 @@ export default class Line extends GraphicalComponent {
],
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -1834,6 +1843,87 @@ export default class Line extends GraphicalComponent {
},
});
}
+
+ // unless allowFlexibleMotion is set,
+ // we will attempt to keep the slope of the line fixed
+ // even if one of the points is constrained
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ if (!(await this.stateValues.basedOnSlope)) {
+ // based on two points
+
+ let numericalPoints = [point1coords, point2coords];
+ let resultingNumericalPoints = await this.stateValues.numericalPoints;
+
+ let pointsChanged = [];
+ let nPointsChanged = 0;
+
+ for (let [ind, pt] of numericalPoints.entries()) {
+ if (!pt.every((v, i) => v === resultingNumericalPoints[ind][i])) {
+ pointsChanged.push(ind);
+ nPointsChanged++;
+ }
+ }
+
+ if (nPointsChanged === 1) {
+ // One point was altered from the requested location
+ // while the other point stayed at the requested location.
+ // We interpret this as one point being constrained and the second one being free
+ // and we move the second point to keep their relative position fixed.
+
+ let changedInd = pointsChanged[0];
+
+ let orig1 = numericalPoints[changedInd];
+ let changed1 = resultingNumericalPoints[changedInd];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ let newNumericalPoints = [];
+
+ for (let i = 0; i < 2; i++) {
+ if (i === changedInd) {
+ newNumericalPoints.push(resultingNumericalPoints[i]);
+ } else {
+ newNumericalPoints.push(
+ numericalPoints[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+ }
+
+ let newPointComponents = {};
+ for (let ind in newNumericalPoints) {
+ newPointComponents[ind + ",0"] = me.fromAst(
+ newNumericalPoints[ind][0],
+ );
+ newPointComponents[ind + ",1"] = me.fromAst(
+ newNumericalPoints[ind][1],
+ );
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "points",
+ value: newPointComponents,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
switchLine() {}
diff --git a/src/Core/components/LineSegment.js b/src/Core/components/LineSegment.js
index b609fbfb69..190739ca62 100644
--- a/src/Core/components/LineSegment.js
+++ b/src/Core/components/LineSegment.js
@@ -51,6 +51,13 @@ export default class LineSegment extends GraphicalComponent {
validValues: ["upperright", "upperleft", "lowerright", "lowerleft"],
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
return attributes;
}
@@ -696,8 +703,10 @@ export default class LineSegment extends GraphicalComponent {
newComponents["1,1"] = me.fromAst(point2coords[1]);
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
componentName: this.componentName,
@@ -710,10 +719,10 @@ export default class LineSegment extends GraphicalComponent {
transient: true,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
componentName: this.componentName,
@@ -725,7 +734,7 @@ export default class LineSegment extends GraphicalComponent {
],
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -739,6 +748,90 @@ export default class LineSegment extends GraphicalComponent {
},
});
}
+
+ // Unless allowFlexibleMotion is set
+ // we will attempt to keep the relationship between the two endpoints fixed
+ // when the whole line segment is moved,
+ // even if one of the endpoints is constrained.
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ // if dragged the whole line segment,
+ // address case where only one endpoint is constrained
+ // to make line segment just translate in this case
+ if (point1coords !== undefined && point2coords !== undefined) {
+ let numericalPoints = [point1coords, point2coords];
+ let resultingNumericalPoints = await this.stateValues
+ .numericalEndpoints;
+
+ let pointsChanged = [];
+ let nPointsChanged = 0;
+
+ for (let [ind, pt] of numericalPoints.entries()) {
+ if (!pt.every((v, i) => v === resultingNumericalPoints[ind][i])) {
+ pointsChanged.push(ind);
+ nPointsChanged++;
+ }
+ }
+
+ if (nPointsChanged === 1) {
+ // One endpoint was altered from the requested location
+ // while the other endpoint stayed at the requested location.
+ // We interpret this as one endpoint being constrained and the second one being free
+ // and we move the second endpoint to keep their relative position fixed.
+
+ let changedInd = pointsChanged[0];
+
+ let orig1 = numericalPoints[changedInd];
+ let changed1 = resultingNumericalPoints[changedInd];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ let newNumericalPoints = [];
+
+ for (let i = 0; i < 2; i++) {
+ if (i === changedInd) {
+ newNumericalPoints.push(resultingNumericalPoints[i]);
+ } else {
+ newNumericalPoints.push(
+ numericalPoints[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+ }
+
+ let newPointComponents = {};
+ for (let ind in newNumericalPoints) {
+ newPointComponents[ind + ",0"] = me.fromAst(
+ newNumericalPoints[ind][0],
+ );
+ newPointComponents[ind + ",1"] = me.fromAst(
+ newNumericalPoints[ind][1],
+ );
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "endpoints",
+ value: newPointComponents,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
async lineSegmentClicked({
diff --git a/src/Core/components/MMeMen.js b/src/Core/components/MMeMen.js
index 1b9df49abf..28178da016 100644
--- a/src/Core/components/MMeMen.js
+++ b/src/Core/components/MMeMen.js
@@ -272,6 +272,9 @@ export class M extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -281,6 +284,9 @@ export class M extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Math.js b/src/Core/components/Math.js
index 56fffa160e..fd2e504ca0 100644
--- a/src/Core/components/Math.js
+++ b/src/Core/components/Math.js
@@ -2153,6 +2153,9 @@ export default class MathComponent extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -2162,6 +2165,9 @@ export default class MathComponent extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/MdMdnMrow.js b/src/Core/components/MdMdnMrow.js
index 0d5843127c..93d8744b84 100644
--- a/src/Core/components/MdMdnMrow.js
+++ b/src/Core/components/MdMdnMrow.js
@@ -241,6 +241,9 @@ export class Md extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -250,6 +253,9 @@ export class Md extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Number.js b/src/Core/components/Number.js
index db261a3677..9f31a86108 100644
--- a/src/Core/components/Number.js
+++ b/src/Core/components/Number.js
@@ -1518,6 +1518,9 @@ export default class NumberComponent extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -1527,6 +1530,9 @@ export default class NumberComponent extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Point.js b/src/Core/components/Point.js
index f24f27a70d..c7864c4523 100644
--- a/src/Core/components/Point.js
+++ b/src/Core/components/Point.js
@@ -1275,7 +1275,11 @@ export default class Point extends GraphicalComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
+ sourceDetails,
sourceInformation = {},
skipRendererUpdate = false,
}) {
@@ -1290,6 +1294,28 @@ export default class Point extends GraphicalComponent {
components[2] = me.fromAst(z);
}
if (transient) {
+ let largerMoveOptions;
+ if (viaKeyboard) {
+ let foundChange =
+ (x !== undefined && x != lastPosition[0]) ||
+ (y !== undefined && y != lastPosition[1]) ||
+ (z !== undefined && z != lastPosition[2]);
+
+ if (foundChange) {
+ // for largerMove algorithm, expect requestedValue to be an array
+ let requestedValue = [];
+ for (let ind in components) {
+ requestedValue[ind] = components[ind];
+ }
+ largerMoveOptions = {
+ componentName: this.componentName,
+ stateVariable: "xs",
+ originalValue: lastPosition.map((x) => me.fromAst(x)),
+ requestedValue,
+ limits,
+ };
+ }
+ }
return await this.coreFunctions.performUpdate({
updateInstructions: [
{
@@ -1297,12 +1323,14 @@ export default class Point extends GraphicalComponent {
componentName: this.componentName,
stateVariable: "xs",
value: components,
+ sourceDetails,
},
],
transient,
actionId,
sourceInformation,
skipRendererUpdate,
+ largerMoveOptions,
});
} else {
return await this.coreFunctions.performUpdate({
@@ -1312,6 +1340,7 @@ export default class Point extends GraphicalComponent {
componentName: this.componentName,
stateVariable: "xs",
value: components,
+ sourceDetails,
},
],
actionId,
diff --git a/src/Core/components/Polyline.js b/src/Core/components/Polyline.js
index 4598cc65c3..3085343659 100644
--- a/src/Core/components/Polyline.js
+++ b/src/Core/components/Polyline.js
@@ -33,6 +33,13 @@ export default class Polyline extends GraphicalComponent {
createComponentOfType: "_pointListComponent",
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
return attributes;
}
@@ -615,8 +622,10 @@ export default class Polyline extends GraphicalComponent {
vertexComponents[ind + ",1"] = me.fromAst(pointCoords[ind][1]);
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
updateType: "updateValue",
@@ -629,10 +638,10 @@ export default class Polyline extends GraphicalComponent {
transient,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions: [
{
updateType: "updateValue",
@@ -644,7 +653,7 @@ export default class Polyline extends GraphicalComponent {
],
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -657,6 +666,114 @@ export default class Polyline extends GraphicalComponent {
},
});
}
+
+ // unless allowFlexibleMotion is set,
+ // we will attempt to preserve the relationship among all the vertices
+ // so that we have a rigid translation
+ // when the whole polyline is moved.
+ // This procedure may preserve the rigid translation
+ // even if a subset of the vertices are constrained.
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ if (nVerticesMoved > 1) {
+ // whole polyline dragged
+
+ let numericalVertices = pointCoords;
+ let resultingNumericalVertices = await this.stateValues
+ .numericalVertices;
+ let nVertices = await this.stateValues.nVertices;
+
+ let verticesChanged = [];
+ let nVerticesChanged = 0;
+
+ for (let [ind, vrtx] of numericalVertices.entries()) {
+ if (!vrtx.every((v, i) => v === resultingNumericalVertices[ind][i])) {
+ verticesChanged.push(ind);
+ nVerticesChanged++;
+ }
+ }
+
+ if (nVerticesChanged > 0 && nVerticesChanged < nVertices) {
+ // A subset of points were altered from the requested location.
+ // Check to see if the relationship among them is preserved
+
+ let changedInd1 = verticesChanged[0];
+ let relationshipPreserved = true;
+
+ let orig1 = numericalVertices[changedInd1];
+ let changed1 = resultingNumericalVertices[changedInd1];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ if (nVerticesChanged > 1) {
+ let tol = 1e-6;
+
+ for (let ind of verticesChanged.slice(1)) {
+ let orig2 = numericalVertices[ind];
+ let changed2 = resultingNumericalVertices[ind];
+ let changevec2 = orig2.map((v, i) => v - changed2[i]);
+
+ if (
+ !changevec1.every((v, i) => Math.abs(v - changevec2[i]) < tol)
+ ) {
+ relationshipPreserved = false;
+ break;
+ }
+ }
+ }
+
+ if (relationshipPreserved) {
+ // All the vertices that were altered from their requested location
+ // were altered in a way consistent with a rigid translation.
+ // Attempt to move the remaining vertices to achieve a rigid translation
+ // of the whole polyline.
+ let newNumericalVertices = [];
+
+ for (let i = 0; i < nVertices; i++) {
+ if (verticesChanged.includes(i)) {
+ newNumericalVertices.push(resultingNumericalVertices[i]);
+ } else {
+ newNumericalVertices.push(
+ numericalVertices[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+ }
+
+ let newVertexComponents = {};
+ for (let ind in newNumericalVertices) {
+ newVertexComponents[ind + ",0"] = me.fromAst(
+ newNumericalVertices[ind][0],
+ );
+ newVertexComponents[ind + ",1"] = me.fromAst(
+ newNumericalVertices[ind][1],
+ );
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "vertices",
+ value: newVertexComponents,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
async finalizePolylinePosition({
diff --git a/src/Core/components/Ray.js b/src/Core/components/Ray.js
index c0f7e417cb..e446177612 100644
--- a/src/Core/components/Ray.js
+++ b/src/Core/components/Ray.js
@@ -35,6 +35,13 @@ export default class Ray extends GraphicalComponent {
createComponentOfType: "vector",
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
return attributes;
}
@@ -1556,21 +1563,23 @@ export default class Ray extends GraphicalComponent {
}
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
transient,
skippable,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -1584,6 +1593,94 @@ export default class Ray extends GraphicalComponent {
},
});
}
+
+ // unless allowFlexibleMotion is set,
+ // we will attempt to keep the slope of the ray fixed
+ // even if one of the points is constrained
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ // if dragged the whole ray that is based on endpoint and through point,
+ // address case where only one point is constrained
+ // to make ray just translate in this case
+
+ if (
+ endpointcoords !== undefined &&
+ throughcoords !== undefined &&
+ (await this.stateValues.basedOnEndpoint) &&
+ (await this.stateValues.basedOnThrough)
+ ) {
+ let numericalPoints = [endpointcoords, throughcoords];
+ let resultingNumericalPoints = [
+ await this.stateValues.numericalEndpoint,
+ await this.stateValues.numericalThroughpoint,
+ ];
+
+ let pointsChanged = [];
+ let nPointsChanged = 0;
+
+ for (let [ind, pt] of numericalPoints.entries()) {
+ if (!pt.every((v, i) => v === resultingNumericalPoints[ind][i])) {
+ pointsChanged.push(ind);
+ nPointsChanged++;
+ }
+ }
+
+ if (nPointsChanged === 1) {
+ // One point was altered from the requested location
+ // while the other point stayed at the requested location.
+ // We interpret this as one point being constrained and the second one being free
+ // and we move the second point to keep their relative position fixed.
+
+ let changedInd = pointsChanged[0];
+
+ let orig1 = numericalPoints[changedInd];
+ let changed1 = resultingNumericalPoints[changedInd];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ let newNumericalPoints = [];
+
+ for (let i = 0; i < 2; i++) {
+ if (i === changedInd) {
+ newNumericalPoints.push(resultingNumericalPoints[i]);
+ } else {
+ newNumericalPoints.push(
+ numericalPoints[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "endpoint",
+ value: newNumericalPoints[0].map((x) => me.fromAst(x)),
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "through",
+ value: newNumericalPoints[1].map((x) => me.fromAst(x)),
+ },
+ ];
+
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
async rayClicked({
diff --git a/src/Core/components/Rectangle.js b/src/Core/components/Rectangle.js
index 9bc05d49b0..f7c14f6071 100644
--- a/src/Core/components/Rectangle.js
+++ b/src/Core/components/Rectangle.js
@@ -1294,7 +1294,7 @@ export default class Rectangle extends Polygon {
sourceDetails,
});
- if (Object.keys(pointCoords).length === 1) {
+ if (nVerticesMoved === 1) {
// When dragging a rectangle corner, add additional instructions
// if they are needed to ensure the opposite corner doesn't move
@@ -1370,22 +1370,22 @@ export default class Rectangle extends Polygon {
}
}
- // console.log("movePolygon updateInstructions", updateInstructions);
-
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
transient,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -1398,5 +1398,132 @@ export default class Rectangle extends Polygon {
},
});
}
+
+ // unless allowFlexibleMotion is set,
+ // we will attempt to preserve the relationship among all the vertices
+ // so that we have a rigid translation
+ // when the whole rectangle is moved.
+ // This procedure may preserve the rigid translation
+ // even if a subset of the vertices are constrained.
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ // if dragged the whole rectangle that is based on two points (vertices and/or center),
+ // address case where only one point is constrained
+ // to make rectangle just translate in this case
+
+ if (nVerticesMoved > 1) {
+ let nVerticesSpecified = await this.stateValues.nVerticesSpecified;
+
+ if (
+ nVerticesSpecified > 1 ||
+ (nVerticesSpecified === 1 &&
+ (await this.stateValues.haveSpecifiedCenter))
+ ) {
+ let resultingNumericalVertices = await this.stateValues
+ .numericalVertices;
+
+ let numericalPoints, resultingNumericalPoints;
+
+ if (nVerticesSpecified > 1) {
+ // just look at first and third vertex
+ numericalPoints = [pointCoords[0], pointCoords[2]];
+ resultingNumericalPoints = [
+ resultingNumericalVertices[0],
+ resultingNumericalVertices[2],
+ ];
+ } else {
+ // just look at center and first vertex
+ // (calculate center from first and third vertex)
+
+ let numericalCenter = [
+ (pointCoords[0][0] + pointCoords[2][0]) / 2,
+ (pointCoords[0][1] + pointCoords[2][1]) / 2,
+ ];
+
+ let resultingNumericalCenter = [
+ (resultingNumericalVertices[0][0] +
+ resultingNumericalVertices[2][0]) /
+ 2,
+ (resultingNumericalVertices[0][1] +
+ resultingNumericalVertices[2][1]) /
+ 2,
+ ];
+
+ numericalPoints = [numericalCenter, pointCoords[0]];
+ resultingNumericalPoints = [
+ resultingNumericalCenter,
+ resultingNumericalVertices[0],
+ ];
+ }
+
+ let pointsChanged = [];
+ let nPointsChanged = 0;
+
+ for (let [ind, pt] of numericalPoints.entries()) {
+ if (!pt.every((v, i) => v === resultingNumericalPoints[ind][i])) {
+ pointsChanged.push(ind);
+ nPointsChanged++;
+ }
+ }
+
+ if (nPointsChanged === 1) {
+ // One of the defining points (center or vertex)
+ // was altered from the requested location
+ // while the other point stayed at the requested location.
+ // We interpret this as one point being constrained and the second one being free
+ // and we move the second point to keep their relative position fixed.
+
+ let changedInd = pointsChanged[0];
+
+ let orig1 = numericalPoints[changedInd];
+ let changed1 = resultingNumericalPoints[changedInd];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ let newNumericalVertices = [];
+
+ for (let i = 0; i < 4; i++) {
+ newNumericalVertices.push(
+ pointCoords[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+
+ let newVertexComponents = {};
+
+ for (let ind in newNumericalVertices) {
+ newVertexComponents[ind + ",0"] = me.fromAst(
+ newNumericalVertices[ind][0],
+ );
+ newVertexComponents[ind + ",1"] = me.fromAst(
+ newNumericalVertices[ind][1],
+ );
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "vertices",
+ value: newVertexComponents,
+ },
+ ];
+
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
}
diff --git a/src/Core/components/RegularPolygon.js b/src/Core/components/RegularPolygon.js
index 59d6417e2f..13966b6df2 100644
--- a/src/Core/components/RegularPolygon.js
+++ b/src/Core/components/RegularPolygon.js
@@ -1742,4 +1742,361 @@ export default class RegularPolygon extends Polygon {
return stateVariableDefinitions;
}
+
+ 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 });
+ }
+
+ // Since the case where drag the entire regular polygon is complicated,
+ // we perform the entire move for a single vertex here and return.
+
+ // If a single vertex is dragged, we perform update on the vertex,
+ // as one typically does for a polygon
+ let vertexComponents = {};
+ for (let ind in pointCoords) {
+ vertexComponents[ind + ",0"] = me.fromAst(pointCoords[ind][0]);
+ vertexComponents[ind + ",1"] = me.fromAst(pointCoords[ind][1]);
+ }
+
+ if (transient) {
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "vertices",
+ value: vertexComponents,
+ sourceDetails,
+ },
+ ],
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ } else {
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "vertices",
+ value: vertexComponents,
+ sourceDetails,
+ },
+ ],
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ event: {
+ verb: "interacted",
+ object: {
+ componentName: this.componentName,
+ componentType: this.componentType,
+ },
+ result: {
+ pointCoordinates: pointCoords,
+ },
+ },
+ });
+ }
+ }
+
+ // whole polyline dragged
+ if (!(await this.stateValues.draggable)) {
+ return await this.coreFunctions.resolveAction({ actionId });
+ }
+
+ // In order to detect if one of the points used to defined the regular polygon is constrained
+ // (so that we can adjust to keep the shape in that case)
+ // we perform that calculations of the inverseDefinition of vertices here
+ // and perform that update directly on centerComponents and directionWithRadius.
+ // (The inverse definition of vertices will still be used if other components
+ // are connected to the vertices.)
+ // We just want this special behavior for the case when the entire polygon
+ // is moved by the renderer.
+
+ let nVertices = await this.stateValues.nVertices;
+
+ // First calculate the desired centers as the average of all points
+
+ let center_x = 0,
+ center_y = 0;
+
+ for (let vertexInd = 0; vertexInd < nVertices; vertexInd++) {
+ center_x += pointCoords[vertexInd][0];
+ center_y += pointCoords[vertexInd][1];
+ }
+
+ center_x /= nVertices;
+ center_y /= nVertices;
+
+ let center = [center_x, center_y];
+
+ // use the first index determine directionWithRadius
+ let vertex1 = pointCoords[0];
+
+ let directionWithRadius = [vertex1[0] - center[0], vertex1[1] - center[1]];
+
+ let updateInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "centerComponents",
+ value: center,
+ sourceDetails,
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "directionWithRadius",
+ value: directionWithRadius,
+ sourceDetails,
+ },
+ ];
+
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
+ if (transient) {
+ await this.coreFunctions.performUpdate({
+ updateInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate: true,
+ });
+ } else {
+ await this.coreFunctions.performUpdate({
+ updateInstructions,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate: true,
+ event: {
+ verb: "interacted",
+ object: {
+ componentName: this.componentName,
+ componentType: this.componentType,
+ },
+ result: {
+ pointCoordinates: pointCoords,
+ },
+ },
+ });
+ }
+
+ // unless allowFlexibleMotion is set,
+ // we will attempt to create a rigid translation of the polygon
+ // even if a subset of the vertices are constrained.
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ let nVerticesSpecified = await this.stateValues.nVerticesSpecified;
+ let haveSpecifiedCenter = await this.stateValues.haveSpecifiedCenter;
+
+ if (haveSpecifiedCenter) {
+ if (nVerticesSpecified >= 1) {
+ // polygon was determined by center and 1 vertex
+
+ let resultingCenter = await this.stateValues.centerComponents;
+
+ let resultingDirectionWithRadius = await this.stateValues
+ .directionWithRadius;
+ let resultingVertex1 = [
+ resultingCenter[0] + resultingDirectionWithRadius[0],
+ resultingCenter[1] + resultingDirectionWithRadius[1],
+ ];
+
+ let tol = 1e-6;
+
+ let vertex1Changed = !vertex1.every(
+ (v, i) => Math.abs(v - resultingVertex1[i]) < tol,
+ );
+ let centerChanged = !center.every(
+ (v, i) => Math.abs(v - resultingCenter[i]) < tol,
+ );
+
+ if (centerChanged) {
+ if (!vertex1Changed) {
+ // only center changed
+ // keep directionWithRadius the same
+ // and use new center position
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "centerComponents",
+ value: resultingCenter,
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "directionWithRadius",
+ value: directionWithRadius,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ } else if (vertex1Changed) {
+ // only vertex 1 changed
+ // keep directionWithRadius the same
+ // adjust center to put vertex 1 at its new location
+
+ let newCenter = [
+ resultingVertex1[0] - directionWithRadius[0],
+ resultingVertex1[1] - directionWithRadius[1],
+ ];
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "centerComponents",
+ value: newCenter,
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "directionWithRadius",
+ value: directionWithRadius,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ } else if (nVerticesSpecified >= 2) {
+ //polygon was determined by two vertices
+
+ // calculate the value of vertex2 calculated from center and vertex1
+ let angle = (2 * Math.PI) / nVertices;
+
+ let c = Math.cos(angle);
+ let s = Math.sin(angle);
+
+ let directionWithRadius2 = [
+ directionWithRadius[0] * c - directionWithRadius[1] * s,
+ directionWithRadius[0] * s + directionWithRadius[1] * c,
+ ];
+
+ let vertex2 = [
+ directionWithRadius2[0] + center[0],
+ directionWithRadius2[1] + center[1],
+ ];
+
+ let resultingVertices = await this.stateValues.vertices;
+ let resultingVertex1 = resultingVertices[0].map((x) =>
+ x.evaluate_to_constant(),
+ );
+ let resultingVertex2 = resultingVertices[1].map((x) =>
+ x.evaluate_to_constant(),
+ );
+
+ let tol = 1e-6;
+
+ let vertex1Changed = !vertex1.every(
+ (v, i) => Math.abs(v - resultingVertex1[i]) < tol,
+ );
+ let vertex2Changed = !vertex2.every(
+ (v, i) => Math.abs(v - resultingVertex2[i]) < tol,
+ );
+
+ if (vertex1Changed) {
+ if (!vertex2Changed) {
+ // only vertex 1 changed
+ // keep directionWithRadius the same
+ // adjust center to put vertex 1 at its new location
+
+ let newCenter = [
+ resultingVertex1[0] - directionWithRadius[0],
+ resultingVertex1[1] - directionWithRadius[1],
+ ];
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "centerComponents",
+ value: newCenter,
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "directionWithRadius",
+ value: directionWithRadius,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ } else if (vertex2Changed) {
+ // only vertex 2 changed
+ // keep directionWithRadius the same
+ // adjust center to put vertex 2 at its new location
+
+ let newCenter = [
+ resultingVertex2[0] - directionWithRadius2[0],
+ resultingVertex2[1] - directionWithRadius2[1],
+ ];
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "centerComponents",
+ value: newCenter,
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "directionWithRadius",
+ value: directionWithRadius,
+ },
+ ];
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
}
diff --git a/src/Core/components/Text.js b/src/Core/components/Text.js
index 07d3acd998..e7360c6213 100644
--- a/src/Core/components/Text.js
+++ b/src/Core/components/Text.js
@@ -283,6 +283,9 @@ export default class Text extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -292,6 +295,9 @@ export default class Text extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/TextInput.js b/src/Core/components/TextInput.js
index e86a21a178..4745309ee2 100644
--- a/src/Core/components/TextInput.js
+++ b/src/Core/components/TextInput.js
@@ -434,6 +434,9 @@ export default class Textinput extends Input {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -443,6 +446,9 @@ export default class Textinput extends Input {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Triangle.js b/src/Core/components/Triangle.js
index 0a8371cd62..91366bbd64 100644
--- a/src/Core/components/Triangle.js
+++ b/src/Core/components/Triangle.js
@@ -278,23 +278,3 @@ export default class Triangle extends Polygon {
return stateVariableDefinitions;
}
}
-
-function mergeVertex({ workspaceVertices, currentVertexValue, desiredVertex }) {
- // If have any empty values in desired value,
- // merge with current values, or value from workspace
-
- let vertexAst;
- if (workspaceVertices) {
- // if have desired expresson from workspace, use that instead of currentValue
- vertexAst = workspaceVertices.slice(0);
- } else {
- vertexAst = currentVertexValue.tree.slice(0);
- }
- for (let [ind, value] of desiredVertex.tree.entries()) {
- if (value !== undefined) {
- vertexAst[ind] = value;
- }
- }
-
- return me.fromAst(vertexAst);
-}
diff --git a/src/Core/components/TriggerSet.js b/src/Core/components/TriggerSet.js
index d23a90be22..6635037089 100644
--- a/src/Core/components/TriggerSet.js
+++ b/src/Core/components/TriggerSet.js
@@ -169,6 +169,9 @@ export default class triggerSet extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -178,6 +181,9 @@ export default class triggerSet extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/UpdateValue.js b/src/Core/components/UpdateValue.js
index 5d68c17c77..40a86a44ba 100644
--- a/src/Core/components/UpdateValue.js
+++ b/src/Core/components/UpdateValue.js
@@ -415,6 +415,9 @@ export default class UpdateValue extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -424,6 +427,9 @@ export default class UpdateValue extends InlineComponent {
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation,
skipRendererUpdate,
diff --git a/src/Core/components/Vector.js b/src/Core/components/Vector.js
index 0022f2fae4..7ba58f1a4c 100644
--- a/src/Core/components/Vector.js
+++ b/src/Core/components/Vector.js
@@ -98,6 +98,13 @@ export default class Vector extends GraphicalComponent {
public: true,
};
+ attributes.allowFlexibleMotion = {
+ createComponentOfType: "boolean",
+ createStateVariable: "allowFlexibleMotion",
+ defaultValue: false,
+ public: true,
+ };
+
return attributes;
}
@@ -2326,21 +2333,23 @@ export default class Vector extends GraphicalComponent {
}
}
+ // Note: we set skipRendererUpdate to true
+ // so that we can make further adjustments before the renderers are updated
if (transient) {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
transient,
skippable,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
});
} else {
- return await this.coreFunctions.performUpdate({
+ await this.coreFunctions.performUpdate({
updateInstructions,
actionId,
sourceInformation,
- skipRendererUpdate,
+ skipRendererUpdate: true,
event: {
verb: "interacted",
object: {
@@ -2354,6 +2363,89 @@ export default class Vector extends GraphicalComponent {
},
});
}
+
+ // unless allowFlexibleMotion is set
+ // we attempt to keep the vector displacement fixed
+ // even if one of the points defining it is constrained
+ if (!(await this.stateValues.allowFlexibleMotion)) {
+ // if dragged the whole vector that is based on tail and head,
+ // address case where only one point is constrained
+ // to make vector just translate in this case
+
+ if (
+ tailcoords !== undefined &&
+ headcoords !== undefined &&
+ (await this.stateValues.basedOnTail) &&
+ (await this.stateValues.basedOnHead)
+ ) {
+ let numericalPoints = [tailcoords, headcoords];
+ let resultingNumericalPoints = await this.stateValues
+ .numericalEndpoints;
+
+ let pointsChanged = [];
+ let nPointsChanged = 0;
+
+ for (let [ind, pt] of numericalPoints.entries()) {
+ if (!pt.every((v, i) => v === resultingNumericalPoints[ind][i])) {
+ pointsChanged.push(ind);
+ nPointsChanged++;
+ }
+ }
+
+ if (nPointsChanged === 1) {
+ // one point was altered from the requested location.
+
+ let changedInd = pointsChanged[0];
+
+ let orig1 = numericalPoints[changedInd];
+ let changed1 = resultingNumericalPoints[changedInd];
+ let changevec1 = orig1.map((v, i) => v - changed1[i]);
+
+ let newNumericalPoints = [];
+
+ for (let i = 0; i < 2; i++) {
+ if (i === changedInd) {
+ newNumericalPoints.push(resultingNumericalPoints[i]);
+ } else {
+ newNumericalPoints.push(
+ numericalPoints[i].map((v, j) => v - changevec1[j]),
+ );
+ }
+ }
+
+ let newInstructions = [
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "tail",
+ value: newNumericalPoints[0].map((x) => me.fromAst(x)),
+ },
+ {
+ updateType: "updateValue",
+ componentName: this.componentName,
+ stateVariable: "head",
+ value: newNumericalPoints[1].map((x) => me.fromAst(x)),
+ },
+ ];
+
+ return await this.coreFunctions.performUpdate({
+ updateInstructions: newInstructions,
+ transient,
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
+ }
+ }
+ }
+
+ // if no modifications were made, still need to update renderers
+ // as original update was performed with skipping renderer update
+ return await this.coreFunctions.updateRenderers({
+ actionId,
+ sourceInformation,
+ skipRendererUpdate,
+ });
}
async vectorClicked({
diff --git a/src/Core/utils/graphical.js b/src/Core/utils/graphical.js
index b4feda4ade..44f1ec0f17 100644
--- a/src/Core/utils/graphical.js
+++ b/src/Core/utils/graphical.js
@@ -134,6 +134,9 @@ export async function moveGraphicalObjectWithAnchorAction({
y,
z,
transient,
+ viaKeyboard,
+ lastPosition,
+ limits,
actionId,
sourceInformation = {},
skipRendererUpdate = false,
@@ -152,6 +155,23 @@ export async function moveGraphicalObjectWithAnchorAction({
components[3] = z;
}
if (transient) {
+ let largerMoveOptions;
+ if (viaKeyboard) {
+ let foundChange =
+ (x !== undefined && x != lastPosition[0]) ||
+ (y !== undefined && y != lastPosition[1]) ||
+ (z !== undefined && z != lastPosition[2]);
+
+ if (foundChange) {
+ largerMoveOptions = {
+ componentName,
+ stateVariable: "anchor",
+ originalValue: me.fromAst(["vector", ...lastPosition]),
+ requestedValue: me.fromAst(components),
+ limits,
+ };
+ }
+ }
return await coreFunctions.performUpdate({
updateInstructions: [
{
@@ -165,6 +185,7 @@ export async function moveGraphicalObjectWithAnchorAction({
actionId,
sourceInformation,
skipRendererUpdate,
+ largerMoveOptions,
});
} else {
return await coreFunctions.performUpdate({
diff --git a/src/Viewer/renderers/booleanInput.jsx b/src/Viewer/renderers/booleanInput.jsx
index 227d8f23fa..0162f5141b 100644
--- a/src/Viewer/renderers/booleanInput.jsx
+++ b/src/Viewer/renderers/booleanInput.jsx
@@ -284,14 +284,25 @@ export default React.memo(function BooleanInput(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveInput,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newInputJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/button.jsx b/src/Viewer/renderers/button.jsx
index 86ed767fd9..df2a1a6a5c 100644
--- a/src/Viewer/renderers/button.jsx
+++ b/src/Viewer/renderers/button.jsx
@@ -206,14 +206,25 @@ export default React.memo(function ButtonComponent(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveButton,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newButtonJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/image.jsx b/src/Viewer/renderers/image.jsx
index 4e4a156b2e..01fad9e07e 100644
--- a/src/Viewer/renderers/image.jsx
+++ b/src/Viewer/renderers/image.jsx
@@ -324,14 +324,25 @@ export default React.memo(function Image(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveImage,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newImageJXG.relativeCoords.setCoordinates(
diff --git a/src/Viewer/renderers/label.jsx b/src/Viewer/renderers/label.jsx
index 95bf7f6f85..8066fa6e04 100644
--- a/src/Viewer/renderers/label.jsx
+++ b/src/Viewer/renderers/label.jsx
@@ -258,14 +258,25 @@ export default React.memo(function Label(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveLabel,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newLabelJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/math.jsx b/src/Viewer/renderers/math.jsx
index 3c33734494..7bb78ac54c 100644
--- a/src/Viewer/renderers/math.jsx
+++ b/src/Viewer/renderers/math.jsx
@@ -278,14 +278,25 @@ export default React.memo(function MathComponent(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveMath,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newMathJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/number.jsx b/src/Viewer/renderers/number.jsx
index 71b0c8e863..edb907586e 100644
--- a/src/Viewer/renderers/number.jsx
+++ b/src/Viewer/renderers/number.jsx
@@ -258,14 +258,25 @@ export default React.memo(function NumberComponent(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveNumber,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newNumberJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/point.jsx b/src/Viewer/renderers/point.jsx
index bffca059a7..a982b50772 100644
--- a/src/Viewer/renderers/point.jsx
+++ b/src/Viewer/renderers/point.jsx
@@ -320,22 +320,42 @@ export default React.memo(function Point(props) {
calculatedX.current = Math.min(xmax, Math.max(xmin, calculatedX.current));
calculatedY.current = Math.min(ymax, Math.max(ymin, calculatedY.current));
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xmin, xmax],
+ [ymin, ymax],
+ ];
+ args.sourceDetails = { viaKeyboard: true };
+ }
+
callAction({
action: actions.movePoint,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
- let shadowX = Math.min(xmax, Math.max(xmin, newShadowPointJXG.X()));
- let shadowY = Math.min(ymax, Math.max(ymin, newShadowPointJXG.Y()));
- newShadowPointJXG.coords.setCoordinates(JXG.COORDS_BY_USER, [
- shadowX,
- shadowY,
- ]);
+ if (viaPointer) {
+ let shadowX = Math.min(xmax, Math.max(xmin, newShadowPointJXG.X()));
+ let shadowY = Math.min(ymax, Math.max(ymin, newShadowPointJXG.Y()));
+ newShadowPointJXG.coords.setCoordinates(JXG.COORDS_BY_USER, [
+ shadowX,
+ shadowY,
+ ]);
+ } else {
+ // don't use shadow point with keyboard
+ newShadowPointJXG.coords.setCoordinates(
+ JXG.COORDS_BY_USER,
+ lastPositionFromCore.current,
+ );
+ }
newPointJXG.coords.setCoordinates(
JXG.COORDS_BY_USER,
@@ -421,7 +441,12 @@ export default React.memo(function Point(props) {
let y = SVs.numericalXs?.[1];
pointJXG.current.coords.setCoordinates(JXG.COORDS_BY_USER, [x, y]);
- if (!dragged.current) {
+
+ // don't use shadow point with keyboard
+ if (
+ !dragged.current ||
+ sourceOfUpdate.sourceInformation?.[name]?.viaKeyboard
+ ) {
shadowPointJXG.current.coords.setCoordinates(JXG.COORDS_BY_USER, [
x,
y,
diff --git a/src/Viewer/renderers/text.jsx b/src/Viewer/renderers/text.jsx
index b7c052372e..b5201dadb3 100644
--- a/src/Viewer/renderers/text.jsx
+++ b/src/Viewer/renderers/text.jsx
@@ -253,14 +253,25 @@ export default React.memo(function Text(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveText,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newTextJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);
diff --git a/src/Viewer/renderers/textInput.jsx b/src/Viewer/renderers/textInput.jsx
index 043ebfe272..7f4fc78d53 100644
--- a/src/Viewer/renderers/textInput.jsx
+++ b/src/Viewer/renderers/textInput.jsx
@@ -409,14 +409,25 @@ export default function TextInput(props) {
Math.max(yminAdjusted, calculatedY.current),
);
+ let args = {
+ x: calculatedX.current,
+ y: calculatedY.current,
+ transient: true,
+ skippable: true,
+ };
+
+ if (!viaPointer) {
+ args.viaKeyboard = true;
+ args.lastPosition = lastPositionFromCore.current;
+ args.limits = [
+ [xminAdjusted, xmaxAdjusted],
+ [yminAdjusted, ymaxAdjusted],
+ ];
+ }
+
callAction({
action: actions.moveInput,
- args: {
- x: calculatedX.current,
- y: calculatedY.current,
- transient: true,
- skippable: true,
- },
+ args,
});
newInputJXG.relativeCoords.setCoordinates(JXG.COORDS_BY_USER, [0, 0]);