Files
OpenNoodl/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-012-blockly-integration/BLOCKS-SPEC.md
Richard Osborne 554dd9f3b4 feat(blockly): Phase A foundation - Blockly setup, custom blocks, and generators
- Install blockly package (~500KB)
- Create BlocklyWorkspace React component with serialization
- Define custom Noodl blocks (Input/Output, Variables, Objects, Arrays)
- Implement JavaScript code generators for all custom blocks
- Add theme-aware styling for Blockly workspace
- Export initialization functions for easy integration

Part of TASK-012: Blockly Visual Logic Integration
2026-01-11 13:30:13 +01:00

845 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Blockly Blocks Specification
This document defines the custom Blockly blocks for Noodl integration.
---
## Block Categories & Colors
| Category | Color (HSL Hue) | Description |
|----------|-----------------|-------------|
| Inputs/Outputs | 230 (Blue) | Node I/O |
| Variables | 330 (Pink) | Noodl.Variables |
| Objects | 20 (Orange) | Noodl.Objects |
| Arrays | 260 (Purple) | Noodl.Arrays |
| Events | 180 (Cyan) | Signals & triggers |
| Logic | 210 (Standard) | If/else, comparisons |
| Math | 230 (Standard) | Math operations |
| Text | 160 (Standard) | String operations |
---
## Inputs/Outputs Blocks
### noodl_define_input
Declares an input port on the node.
```javascript
// Block Definition
{
type: 'noodl_define_input',
message0: '📥 Define input %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myInput' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an input port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_input'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
```
### noodl_get_input
Gets a value from a node input.
```javascript
// Block Definition
{
type: 'noodl_get_input',
message0: '📥 get input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'value' }
],
output: null, // Can connect to any type
colour: 230,
tooltip: 'Gets the value from an input port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_input'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Inputs["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_define_output
Declares an output port on the node.
```javascript
// Block Definition
{
type: 'noodl_define_output',
message0: '📤 Define output %1 type %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'field_dropdown', name: 'TYPE', options: [
['any', '*'],
['string', 'string'],
['number', 'number'],
['boolean', 'boolean'],
['object', 'object'],
['array', 'array']
]}
],
colour: 230,
tooltip: 'Defines an output port that appears on the node',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_define_output'] = function(block) {
// No runtime code - used for I/O detection only
return '';
};
```
### noodl_set_output
Sets a value on a node output.
```javascript
// Block Definition
{
type: 'noodl_set_output',
message0: '📤 set output %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'result' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 230,
tooltip: 'Sets the value of an output port',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_output'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Outputs["' + name + '"] = ' + value + ';\n';
};
```
### noodl_define_signal_input
Declares a signal input port.
```javascript
// Block Definition
{
type: 'noodl_define_signal_input',
message0: '⚡ Define signal input %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'trigger' }
],
colour: 180,
tooltip: 'Defines a signal input that can trigger logic',
helpUrl: ''
}
```
### noodl_define_signal_output
Declares a signal output port.
```javascript
// Block Definition
{
type: 'noodl_define_signal_output',
message0: '⚡ Define signal output %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
colour: 180,
tooltip: 'Defines a signal output that can trigger other nodes',
helpUrl: ''
}
```
### noodl_send_signal
Sends a signal output.
```javascript
// Block Definition
{
type: 'noodl_send_signal',
message0: '⚡ send signal %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'done' }
],
previousStatement: null,
nextStatement: null,
colour: 180,
tooltip: 'Sends a signal to connected nodes',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_send_signal'] = function(block) {
var name = block.getFieldValue('NAME');
return 'this.sendSignalOnOutput("' + name + '");\n';
};
```
---
## Variables Blocks
### noodl_get_variable
Gets a global variable value.
```javascript
// Block Definition
{
type: 'noodl_get_variable',
message0: '📖 get variable %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
output: null,
colour: 330,
tooltip: 'Gets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Variables["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_set_variable
Sets a global variable value.
```javascript
// Block Definition
{
type: 'noodl_set_variable',
message0: '✏️ set variable %1 to %2',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 330,
tooltip: 'Sets the value of a global Noodl variable',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_variable'] = function(block) {
var name = block.getFieldValue('NAME');
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return 'Noodl.Variables["' + name + '"] = ' + value + ';\n';
};
```
---
## Objects Blocks
### noodl_get_object
Gets an object by ID.
```javascript
// Block Definition
{
type: 'noodl_get_object',
message0: '📦 get object %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Gets a Noodl Object by its ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || '""';
var code = 'Noodl.Objects[' + id + ']';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_get_object_property
Gets a property from an object.
```javascript
// Block Definition
{
type: 'noodl_get_object_property',
message0: '📖 get %1 from object %2',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' }
],
output: null,
colour: 20,
tooltip: 'Gets a property value from an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var code = object + '["' + property + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_set_object_property
Sets a property on an object.
```javascript
// Block Definition
{
type: 'noodl_set_object_property',
message0: '✏️ set %1 on object %2 to %3',
args0: [
{ type: 'field_input', name: 'PROPERTY', text: 'name' },
{ type: 'input_value', name: 'OBJECT' },
{ type: 'input_value', name: 'VALUE' }
],
previousStatement: null,
nextStatement: null,
colour: 20,
tooltip: 'Sets a property value on an object',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_set_object_property'] = function(block) {
var property = block.getFieldValue('PROPERTY');
var object = Blockly.JavaScript.valueToCode(block, 'OBJECT',
Blockly.JavaScript.ORDER_MEMBER) || '{}';
var value = Blockly.JavaScript.valueToCode(block, 'VALUE',
Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
return object + '["' + property + '"] = ' + value + ';\n';
};
```
### noodl_create_object
Creates a new object.
```javascript
// Block Definition
{
type: 'noodl_create_object',
message0: ' create object with ID %1',
args0: [
{ type: 'input_value', name: 'ID', check: 'String' }
],
output: 'Object',
colour: 20,
tooltip: 'Creates a new Noodl Object with the given ID',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_create_object'] = function(block) {
var id = Blockly.JavaScript.valueToCode(block, 'ID',
Blockly.JavaScript.ORDER_NONE) || 'Noodl.Object.guid()';
var code = 'Noodl.Object.create(' + id + ')';
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
};
```
---
## Arrays Blocks
### noodl_get_array
Gets an array by name.
```javascript
// Block Definition
{
type: 'noodl_get_array',
message0: '📋 get array %1',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myArray' }
],
output: 'Array',
colour: 260,
tooltip: 'Gets a Noodl Array by name',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_get_array'] = function(block) {
var name = block.getFieldValue('NAME');
var code = 'Noodl.Arrays["' + name + '"]';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_array_add
Adds an item to an array.
```javascript
// Block Definition
{
type: 'noodl_array_add',
message0: ' add %1 to array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Adds an item to the end of an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_add'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.push(' + item + ');\n';
};
```
### noodl_array_remove
Removes an item from an array.
```javascript
// Block Definition
{
type: 'noodl_array_remove',
message0: ' remove %1 from array %2',
args0: [
{ type: 'input_value', name: 'ITEM' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Removes an item from an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_remove'] = function(block) {
var item = Blockly.JavaScript.valueToCode(block, 'ITEM',
Blockly.JavaScript.ORDER_NONE) || 'null';
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
return array + '.splice(' + array + '.indexOf(' + item + '), 1);\n';
};
```
### noodl_array_length
Gets the length of an array.
```javascript
// Block Definition
{
type: 'noodl_array_length',
message0: '🔢 length of array %1',
args0: [
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
output: 'Number',
colour: 260,
tooltip: 'Gets the number of items in an array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_length'] = function(block) {
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var code = array + '.length';
return [code, Blockly.JavaScript.ORDER_MEMBER];
};
```
### noodl_array_foreach
Loops over array items.
```javascript
// Block Definition
{
type: 'noodl_array_foreach',
message0: '🔄 for each %1 in %2',
args0: [
{ type: 'field_variable', name: 'VAR', variable: 'item' },
{ type: 'input_value', name: 'ARRAY', check: 'Array' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
previousStatement: null,
nextStatement: null,
colour: 260,
tooltip: 'Executes code for each item in the array',
helpUrl: ''
}
// Generator
Blockly.JavaScript['noodl_array_foreach'] = function(block) {
var variable = Blockly.JavaScript.nameDB_.getName(
block.getFieldValue('VAR'), Blockly.VARIABLE_CATEGORY_NAME);
var array = Blockly.JavaScript.valueToCode(block, 'ARRAY',
Blockly.JavaScript.ORDER_MEMBER) || '[]';
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
return 'for (var ' + variable + ' of ' + array + ') {\n' +
statements + '}\n';
};
```
---
## Event Blocks
### noodl_on_signal
Event handler for when a signal input is triggered.
```javascript
// Block Definition
{
type: 'noodl_on_signal',
message0: '⚡ when %1 is triggered',
args0: [
{ type: 'field_input', name: 'SIGNAL', text: 'trigger' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 180,
tooltip: 'Runs code when the signal input is triggered',
helpUrl: ''
}
// Generator - This is a special case, generates a handler function
Blockly.JavaScript['noodl_on_signal'] = function(block) {
var signal = block.getFieldValue('SIGNAL');
var statements = Blockly.JavaScript.statementToCode(block, 'DO');
// This generates a named handler that the runtime will call
return '// Handler for signal: ' + signal + '\n' +
'function _onSignal_' + signal + '() {\n' +
statements +
'}\n';
};
```
### noodl_on_variable_change
Event handler for when a variable changes.
```javascript
// Block Definition
{
type: 'noodl_on_variable_change',
message0: '👁️ when variable %1 changes',
args0: [
{ type: 'field_input', name: 'NAME', text: 'myVariable' }
],
message1: 'do %1',
args1: [
{ type: 'input_statement', name: 'DO' }
],
colour: 330,
tooltip: 'Runs code when the variable value changes',
helpUrl: ''
}
```
---
## I/O Detection Algorithm
```typescript
interface DetectedIO {
inputs: Array<{ name: string; type: string }>;
outputs: Array<{ name: string; type: string }>;
signalInputs: string[];
signalOutputs: string[];
}
function detectIO(workspace: Blockly.Workspace): DetectedIO {
const result: DetectedIO = {
inputs: [],
outputs: [],
signalInputs: [],
signalOutputs: []
};
const blocks = workspace.getAllBlocks(false);
for (const block of blocks) {
switch (block.type) {
case 'noodl_define_input':
result.inputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_get_input':
// Auto-detect from usage if not explicitly defined
const inputName = block.getFieldValue('NAME');
if (!result.inputs.find(i => i.name === inputName)) {
result.inputs.push({ name: inputName, type: '*' });
}
break;
case 'noodl_define_output':
result.outputs.push({
name: block.getFieldValue('NAME'),
type: block.getFieldValue('TYPE')
});
break;
case 'noodl_set_output':
// Auto-detect from usage
const outputName = block.getFieldValue('NAME');
if (!result.outputs.find(o => o.name === outputName)) {
result.outputs.push({ name: outputName, type: '*' });
}
break;
case 'noodl_define_signal_input':
case 'noodl_on_signal':
const sigIn = block.getFieldValue('SIGNAL') || block.getFieldValue('NAME');
if (!result.signalInputs.includes(sigIn)) {
result.signalInputs.push(sigIn);
}
break;
case 'noodl_define_signal_output':
case 'noodl_send_signal':
const sigOut = block.getFieldValue('NAME');
if (!result.signalOutputs.includes(sigOut)) {
result.signalOutputs.push(sigOut);
}
break;
}
}
return result;
}
```
---
## Toolbox Configuration
```javascript
const LOGIC_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs/Outputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' },
{ kind: 'block', type: 'noodl_define_output' },
{ kind: 'block', type: 'noodl_set_output' },
{ kind: 'block', type: 'noodl_define_signal_input' },
{ kind: 'block', type: 'noodl_define_signal_output' },
{ kind: 'block', type: 'noodl_send_signal' }
]
},
{
kind: 'category',
name: 'Events',
colour: 180,
contents: [
{ kind: 'block', type: 'noodl_on_signal' },
{ kind: 'block', type: 'noodl_on_variable_change' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' },
{ kind: 'block', type: 'noodl_set_variable' }
]
},
{
kind: 'category',
name: 'Objects',
colour: 20,
contents: [
{ kind: 'block', type: 'noodl_get_object' },
{ kind: 'block', type: 'noodl_get_object_property' },
{ kind: 'block', type: 'noodl_set_object_property' },
{ kind: 'block', type: 'noodl_create_object' }
]
},
{
kind: 'category',
name: 'Arrays',
colour: 260,
contents: [
{ kind: 'block', type: 'noodl_get_array' },
{ kind: 'block', type: 'noodl_array_add' },
{ kind: 'block', type: 'noodl_array_remove' },
{ kind: 'block', type: 'noodl_array_length' },
{ kind: 'block', type: 'noodl_array_foreach' }
]
},
{ kind: 'sep' },
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'controls_if' },
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Loops',
colour: 120,
contents: [
{ kind: 'block', type: 'controls_repeat_ext' },
{ kind: 'block', type: 'controls_whileUntil' },
{ kind: 'block', type: 'controls_for' },
{ kind: 'block', type: 'controls_flow_statements' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' },
{ kind: 'block', type: 'math_modulo' },
{ kind: 'block', type: 'math_random_int' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' },
{ kind: 'block', type: 'text_isEmpty' },
{ kind: 'block', type: 'text_indexOf' },
{ kind: 'block', type: 'text_charAt' }
]
}
]
};
// Simplified toolbox for Expression Builder
const EXPRESSION_BUILDER_TOOLBOX = {
kind: 'categoryToolbox',
contents: [
{
kind: 'category',
name: 'Inputs',
colour: 230,
contents: [
{ kind: 'block', type: 'noodl_define_input' },
{ kind: 'block', type: 'noodl_get_input' }
]
},
{
kind: 'category',
name: 'Variables',
colour: 330,
contents: [
{ kind: 'block', type: 'noodl_get_variable' }
]
},
{
kind: 'category',
name: 'Logic',
colour: 210,
contents: [
{ kind: 'block', type: 'logic_compare' },
{ kind: 'block', type: 'logic_operation' },
{ kind: 'block', type: 'logic_negate' },
{ kind: 'block', type: 'logic_boolean' },
{ kind: 'block', type: 'logic_ternary' }
]
},
{
kind: 'category',
name: 'Math',
colour: 230,
contents: [
{ kind: 'block', type: 'math_number' },
{ kind: 'block', type: 'math_arithmetic' },
{ kind: 'block', type: 'math_single' },
{ kind: 'block', type: 'math_round' }
]
},
{
kind: 'category',
name: 'Text',
colour: 160,
contents: [
{ kind: 'block', type: 'text' },
{ kind: 'block', type: 'text_join' },
{ kind: 'block', type: 'text_length' }
]
}
]
};
```