Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
451 changes: 451 additions & 0 deletions generators/generators-animate.js

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions generators/generators-condition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export function registerConditionGenerators(javascriptGenerator) {
// -------------------------------
// CONDITION
// -------------------------------
const MODE = { IF: "IF", ELSEIF: "ELSEIF", ELSE: "ELSE" };

// If block ----------------------------------------------------
javascriptGenerator.forBlock["if_clause"] = function (block, generator) {
const isClause = (b) => b && b.type === "if_clause";

const mode = block.getFieldValue("MODE");
const prev = block.getPreviousBlock();

// A new IF always starts a new chain, even if it follows another if_clause.
const isChainTop = !isClause(prev) || mode === MODE.IF;

// Non-top clauses do not emit code independently.
if (!isChainTop) return "";

// Collect this IF plus any following ELSEIF/ELSE clauses,
// but stop before the next IF (that starts a new chain).
const chain = [];
let cur = block;

while (cur && isClause(cur)) {
chain.push(cur);

const next = cur.getNextBlock();
if (next && isClause(next) && next.getFieldValue("MODE") === MODE.IF)
break;

cur = next;
}

let code = "";

const first = chain[0];
const firstCond =
generator.valueToCode(first, "COND", generator.ORDER_NONE) || "false";
const firstBody = generator.statementToCode(first, "DO");

code += `if (${firstCond}) {\n${firstBody}}`;

for (let i = 1; i < chain.length; i++) {
const clause = chain[i];
const clauseMode = clause.getFieldValue("MODE");

if (clauseMode === MODE.ELSEIF) {
const cond =
generator.valueToCode(clause, "COND", generator.ORDER_NONE) ||
"false";
const body = generator.statementToCode(clause, "DO");
code += ` else if (${cond}) {\n${body}}`;
continue;
}

if (clauseMode === MODE.ELSE) {
const body = generator.statementToCode(clause, "DO");
code += ` else {\n${body}}`;
break;
}

// Defensive: if something weird slips through, stop.
if (clauseMode === MODE.IF) break;
}

return code + "\n";
};

// The following blocks use default blockly generators
// ---------------------------------------------------
// Logical comparison
// Not
// Boolean true/false
// Null
// Ternary operator
}
217 changes: 217 additions & 0 deletions generators/generators-control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import * as Blockly from "blockly";

export function registerControlGenerators(javascriptGenerator) {
// -------------------------------
// CONTROL
// -------------------------------
// Wait for x seconds
javascriptGenerator.forBlock["wait_seconds"] = function (block) {
const duration =
javascriptGenerator.valueToCode(
block,
"DURATION",
javascriptGenerator.ORDER_ATOMIC,
) || "1";

return `await wait(${duration});\n`;
};

// Wait until condition is true
javascriptGenerator.forBlock["wait_until"] = function (block) {
const condition =
javascriptGenerator.valueToCode(
block,
"CONDITION",
javascriptGenerator.ORDER_ATOMIC,
) || "false"; // Default to false if no condition is connected

return `await waitUntil(() => ${condition});\n`;
};

// Repeat x times
javascriptGenerator.forBlock["controls_repeat_ext"] = function (
block,
generator,
) {
let repeats;
if (block.getField("TIMES")) {
repeats = String(Number(block.getFieldValue("TIMES")));
} else {
repeats =
generator.valueToCode(block, "TIMES", generator.ORDER_ASSIGNMENT) ||
"0";
}

let branch = generator.statementToCode(block, "DO");

let code = "";
const loopVar = generator.nameDB_.getDistinctName(
"count",
Blockly.Names.NameType.VARIABLE,
);
let endVar = repeats;

if (!/^\w+$/.test(repeats) && isNaN(repeats)) {
endVar = generator.nameDB_.getDistinctName(
"repeat_end",
Blockly.Names.NameType.VARIABLE,
);
code += "let " + endVar + " = " + repeats + ";\n";
}

code +=
"for (let " +
loopVar +
" = 0; " +
loopVar +
" < " +
endVar +
"; " +
loopVar +
"++) {\n" +
branch +
"await wait(0);\n" +
"}\n";

return code;
};

// Repeat while/until condition
javascriptGenerator.forBlock["controls_whileUntil"] = function (block) {
const until = block.getFieldValue("MODE") === "UNTIL";
let argument0 =
javascriptGenerator.valueToCode(
block,
"BOOL",
until
? javascriptGenerator.ORDER_LOGICAL_NOT
: javascriptGenerator.ORDER_NONE,
) || "false";
let branch = javascriptGenerator.statementToCode(block, "DO");
if (until) {
argument0 = "!" + argument0;
}
return (
"while (" + argument0 + ") {\n" + branch + `\nawait wait(0);\n` + "}\n"
);
};

// For each loop with iterator variable
javascriptGenerator.forBlock["controls_for"] = function (block, generator) {
const variable0 = generator.getVariableName(block.getFieldValue("VAR"));

const argument0 =
generator.valueToCode(block, "FROM", generator.ORDER_ASSIGNMENT) || "0";
const argument1 =
generator.valueToCode(block, "TO", generator.ORDER_ASSIGNMENT) || "0";
const increment =
generator.valueToCode(block, "BY", generator.ORDER_ASSIGNMENT) || "1";

const branch = generator.statementToCode(block, "DO");

// Timing and iteration counter variables
const timingVar = generator.nameDB_.getDistinctName(
`${variable0}_timing`,
Blockly.Names.DEVELOPER_VARIABLE_TYPE,
);

const counterVar = generator.nameDB_.getDistinctName(
`${variable0}_counter`,
Blockly.Names.DEVELOPER_VARIABLE_TYPE,
);

return `
let ${timingVar} = performance.now();
let ${counterVar} = 0;
for (let ${variable0} = ${argument0}; (${increment} > 0 ? ${variable0} <= ${argument1} : ${variable0} >= ${argument1}); ${variable0} += ${increment}) {
${branch}
${counterVar}++;
if (${counterVar} % 10 === 0 && performance.now() - ${timingVar} > 16) {
await new Promise(resolve => requestAnimationFrame(resolve));
${timingVar} = performance.now();
}
}
`;
};

// For each loop iterating over list
javascriptGenerator.forBlock["controls_forEach"] = function (
block,
generator,
) {
// For each loop.
const variable0 = generator.getVariableName(block.getFieldValue("VAR"));

// Use correct ORDER constant from the generator
const argument0 =
generator.valueToCode(block, "LIST", generator.ORDER_ASSIGNMENT) || "[]";

let branch = generator.statementToCode(block, "DO");
let code = "";
let listVar = argument0;

if (!/^\w+$/.test(argument0)) {
listVar = generator.nameDB_.getDistinctName(
variable0 + "_list",
Blockly.Names.NameType.VARIABLE,
);
code += "var " + listVar + " = " + argument0 + ";\n";
}

const indexVar = generator.nameDB_.getDistinctName(
variable0 + "_index",
Blockly.Names.NameType.VARIABLE,
);

// Construct the loop body
branch =
generator.INDENT +
variable0 +
" = " +
listVar +
"[" +
indexVar +
"];\n" +
branch;

code +=
"for (var " +
indexVar +
" in " +
listVar +
") {\n" +
branch +
"\n await wait(0);\n" +
"}\n";
Comment on lines +177 to +185
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

for...in is problematic for array iteration.

for...in iterates over enumerable property keys (including inherited ones) as strings, not array indices. For arrays, this can produce unexpected results if the array has additional properties or if the prototype is modified. Consider using a traditional indexed for loop or for...of.

Proposed fix using indexed for loop
-    code +=
-      "for (var " +
-      indexVar +
-      " in " +
-      listVar +
-      ") {\n" +
-      branch +
-      "\n  await wait(0);\n" +
-      "}\n";
+    code +=
+      "for (let " +
+      indexVar +
+      " = 0; " +
+      indexVar +
+      " < " +
+      listVar +
+      ".length; " +
+      indexVar +
+      "++) {\n" +
+      branch +
+      "\n  await wait(0);\n" +
+      "}\n";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
code +=
"for (var " +
indexVar +
" in " +
listVar +
") {\n" +
branch +
"\n await wait(0);\n" +
"}\n";
code +=
"for (let " +
indexVar +
" = 0; " +
indexVar +
" < " +
listVar +
".length; " +
indexVar +
"++) {\n" +
branch +
"\n await wait(0);\n" +
"}\n";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generators/generators-control.js` around lines 177 - 185, The generated loop
uses "for (... in ...)" which can iterate non-numeric keys; change the string
construction in generators-control.js that builds the loop (the block using
variables code, indexVar, listVar and branch) to emit a traditional indexed loop
or a for...of pattern instead—e.g., produce "for (var {indexVar} = 0; {indexVar}
< {listVar}.length; {indexVar}++) { ... await wait(0); }" or emit "for (const
{indexVar} of {listVar}) { ... await wait(0); }" so iteration is over actual
array elements/indices and not enumerable property keys; keep branch and the
await wait(0) inside the loop.


return code;
};

// Break out of loop
// ?? Uses blockly standard

// Local
javascriptGenerator.forBlock["local_variable"] = function (block, generator) {
// Retrieve the variable selected by the user
const variable = generator.nameDB_.getName(
block.getFieldValue("VAR"),
Blockly.VARIABLE_CATEGORY_NAME,
);

// Generate a local 'let' declaration for the selected variable
const code = `let ${variable};\n`;
return code;
};

// Wait x milliseconds
javascriptGenerator.forBlock["wait"] = function (block) {
const duration =
javascriptGenerator.valueToCode(
block,
"DURATION",
javascriptGenerator.ORDER_ATOMIC,
) || "1";

return `await wait(${duration} / 1000);\n`;
};
}
Loading
Loading