mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 07:42:55 +01:00
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
This commit is contained in:
@@ -74,6 +74,7 @@
|
||||
"algoliasearch": "^5.35.0",
|
||||
"archiver": "^5.3.2",
|
||||
"async": "^3.2.6",
|
||||
"blockly": "^12.3.1",
|
||||
"classnames": "^2.5.1",
|
||||
"dagre": "^0.8.5",
|
||||
"diff3": "0.0.4",
|
||||
|
||||
@@ -232,8 +232,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--popup-layer-tooltip-border-color: var(--theme-color-secondary);
|
||||
--popup-layer-tooltip-background-color: var(--theme-color-secondary);
|
||||
--popup-layer-tooltip-border-color: var(--theme-color-border-default);
|
||||
--popup-layer-tooltip-background-color: var(--theme-color-bg-3);
|
||||
}
|
||||
|
||||
.popup-layer-tooltip {
|
||||
@@ -244,7 +244,7 @@
|
||||
border-color: var(--popup-layer-tooltip-border-color);
|
||||
border-width: 1px;
|
||||
padding: 12px 16px;
|
||||
color: var(--theme-color-fg-highlight);
|
||||
color: var(--theme-color-fg-default);
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.3s;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* BlocklyWorkspace Styles
|
||||
*
|
||||
* Styling for the Blockly visual programming workspace.
|
||||
* Uses theme tokens for consistent integration with Noodl editor.
|
||||
*/
|
||||
|
||||
.Root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--theme-color-bg-1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.BlocklyContainer {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Ensure Blockly SVG fills container */
|
||||
& > .injectionDiv {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Override Blockly default styles to match Noodl theme */
|
||||
:global {
|
||||
/* Toolbox styling */
|
||||
.blocklyToolboxDiv {
|
||||
background-color: var(--theme-color-bg-2) !important;
|
||||
border-right: 1px solid var(--theme-color-border-default) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeLabel {
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.blocklyTreeRow:hover {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
}
|
||||
|
||||
.blocklyTreeSelected {
|
||||
background-color: var(--theme-color-primary) !important;
|
||||
}
|
||||
|
||||
/* Flyout styling */
|
||||
.blocklyFlyoutBackground {
|
||||
fill: var(--theme-color-bg-2) !important;
|
||||
fill-opacity: 0.95 !important;
|
||||
}
|
||||
|
||||
/* Block styling - keep default Blockly colors for now */
|
||||
/* May customize later to match Noodl node colors */
|
||||
|
||||
/* Zoom controls */
|
||||
.blocklyZoom {
|
||||
& image {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Trashcan */
|
||||
.blocklyTrash {
|
||||
& image {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* Context menu */
|
||||
.blocklyContextMenu {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
border: 1px solid var(--theme-color-border-default) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem {
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
font-size: 13px !important;
|
||||
padding: 6px 12px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
}
|
||||
|
||||
&.blocklyMenuItemDisabled {
|
||||
color: var(--theme-color-fg-default-shy) !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
.blocklyScrollbarHandle {
|
||||
fill: var(--theme-color-border-default) !important;
|
||||
}
|
||||
|
||||
/* Field editor backgrounds */
|
||||
.blocklyWidgetDiv {
|
||||
& .goog-menu {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
border: 1px solid var(--theme-color-border-default) !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
& .goog-menuitem {
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* BlocklyWorkspace Component
|
||||
*
|
||||
* React wrapper for Google Blockly visual programming workspace.
|
||||
* Provides integration with Noodl's node system for visual logic building.
|
||||
*
|
||||
* @module BlocklyEditor
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import css from './BlocklyWorkspace.module.scss';
|
||||
|
||||
export interface BlocklyWorkspaceProps {
|
||||
/** Initial workspace JSON (for loading saved state) */
|
||||
initialWorkspace?: string;
|
||||
/** Toolbox configuration */
|
||||
toolbox?: Blockly.utils.toolbox.ToolboxDefinition;
|
||||
/** Callback when workspace changes */
|
||||
onChange?: (workspace: Blockly.WorkspaceSvg, json: string, code: string) => void;
|
||||
/** Read-only mode */
|
||||
readOnly?: boolean;
|
||||
/** Custom theme */
|
||||
theme?: Blockly.Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* BlocklyWorkspace - React component for Blockly integration
|
||||
*
|
||||
* Handles:
|
||||
* - Blockly workspace initialization
|
||||
* - Workspace persistence (save/load)
|
||||
* - Change detection and callbacks
|
||||
* - Cleanup on unmount
|
||||
*/
|
||||
export function BlocklyWorkspace({
|
||||
initialWorkspace,
|
||||
toolbox,
|
||||
onChange,
|
||||
readOnly = false,
|
||||
theme
|
||||
}: BlocklyWorkspaceProps) {
|
||||
const blocklyDiv = useRef<HTMLDivElement>(null);
|
||||
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// Initialize Blockly workspace
|
||||
useEffect(() => {
|
||||
if (!blocklyDiv.current) return;
|
||||
|
||||
console.log('🔧 [Blockly] Initializing workspace');
|
||||
|
||||
// Inject Blockly
|
||||
const workspace = Blockly.inject(blocklyDiv.current, {
|
||||
toolbox: toolbox || getDefaultToolbox(),
|
||||
theme: theme,
|
||||
readOnly: readOnly,
|
||||
trashcan: true,
|
||||
zoom: {
|
||||
controls: true,
|
||||
wheel: true,
|
||||
startScale: 1.0,
|
||||
maxScale: 3,
|
||||
minScale: 0.3,
|
||||
scaleSpeed: 1.2
|
||||
},
|
||||
grid: {
|
||||
spacing: 20,
|
||||
length: 3,
|
||||
colour: '#ccc',
|
||||
snap: true
|
||||
}
|
||||
});
|
||||
|
||||
workspaceRef.current = workspace;
|
||||
|
||||
// Load initial workspace if provided
|
||||
if (initialWorkspace) {
|
||||
try {
|
||||
const json = JSON.parse(initialWorkspace);
|
||||
Blockly.serialization.workspaces.load(json, workspace);
|
||||
console.log('✅ [Blockly] Loaded initial workspace');
|
||||
} catch (error) {
|
||||
console.error('❌ [Blockly] Failed to load initial workspace:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
|
||||
// Listen for changes
|
||||
const changeListener = () => {
|
||||
if (onChange && workspace) {
|
||||
const json = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
const code = Blockly.JavaScript.workspaceToCode(workspace);
|
||||
onChange(workspace, json, code);
|
||||
}
|
||||
};
|
||||
|
||||
workspace.addChangeListener(changeListener);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
console.log('🧹 [Blockly] Disposing workspace');
|
||||
workspace.removeChangeListener(changeListener);
|
||||
workspace.dispose();
|
||||
workspaceRef.current = null;
|
||||
setIsInitialized(false);
|
||||
};
|
||||
}, [toolbox, theme, readOnly]);
|
||||
|
||||
// Handle initial workspace separately to avoid re-initialization
|
||||
useEffect(() => {
|
||||
if (isInitialized && initialWorkspace && workspaceRef.current) {
|
||||
try {
|
||||
const json = JSON.parse(initialWorkspace);
|
||||
Blockly.serialization.workspaces.load(json, workspaceRef.current);
|
||||
} catch (error) {
|
||||
console.error('❌ [Blockly] Failed to update workspace:', error);
|
||||
}
|
||||
}
|
||||
}, [initialWorkspace]);
|
||||
|
||||
return (
|
||||
<div className={css.Root}>
|
||||
<div ref={blocklyDiv} className={css.BlocklyContainer} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default toolbox with standard Blockly blocks
|
||||
* This will be replaced with Noodl-specific toolbox
|
||||
*/
|
||||
function getDefaultToolbox(): Blockly.utils.toolbox.ToolboxDefinition {
|
||||
return {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
{
|
||||
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: 'category',
|
||||
name: 'Math',
|
||||
colour: '230',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'math_number' },
|
||||
{ kind: 'block', type: 'math_arithmetic' },
|
||||
{ kind: 'block', type: 'math_single' }
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Text',
|
||||
colour: '160',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'text' },
|
||||
{ kind: 'block', type: 'text_join' },
|
||||
{ kind: 'block', type: 'text_length' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Noodl Custom Blocks for Blockly
|
||||
*
|
||||
* Defines custom blocks for Noodl-specific functionality:
|
||||
* - Inputs/Outputs (node I/O)
|
||||
* - Variables (Noodl.Variables)
|
||||
* - Objects (Noodl.Objects)
|
||||
* - Arrays (Noodl.Arrays)
|
||||
* - Events/Signals
|
||||
*
|
||||
* @module BlocklyEditor
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
/**
|
||||
* Initialize all Noodl custom blocks
|
||||
*/
|
||||
export function initNoodlBlocks() {
|
||||
console.log('🔧 [Blockly] Initializing Noodl custom blocks');
|
||||
|
||||
// Input/Output blocks
|
||||
defineInputOutputBlocks();
|
||||
|
||||
// Variable blocks
|
||||
defineVariableBlocks();
|
||||
|
||||
// Object blocks (basic - will expand later)
|
||||
defineObjectBlocks();
|
||||
|
||||
// Array blocks (basic - will expand later)
|
||||
defineArrayBlocks();
|
||||
|
||||
console.log('✅ [Blockly] Noodl blocks initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Input/Output Blocks
|
||||
*/
|
||||
function defineInputOutputBlocks() {
|
||||
// Define Input block - declares an input port
|
||||
Blockly.Blocks['noodl_define_input'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('📥 Define input')
|
||||
.appendField(new Blockly.FieldTextInput('myInput'), 'NAME')
|
||||
.appendField('type')
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
['any', '*'],
|
||||
['string', 'string'],
|
||||
['number', 'number'],
|
||||
['boolean', 'boolean'],
|
||||
['object', 'object'],
|
||||
['array', 'array']
|
||||
]),
|
||||
'TYPE'
|
||||
);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Defines an input port that appears on the node');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Get Input block - gets value from an input
|
||||
Blockly.Blocks['noodl_get_input'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField('📥 get input').appendField(new Blockly.FieldTextInput('value'), 'NAME');
|
||||
this.setOutput(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Gets the value from an input port');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Define Output block - declares an output port
|
||||
Blockly.Blocks['noodl_define_output'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('📤 Define output')
|
||||
.appendField(new Blockly.FieldTextInput('result'), 'NAME')
|
||||
.appendField('type')
|
||||
.appendField(
|
||||
new Blockly.FieldDropdown([
|
||||
['any', '*'],
|
||||
['string', 'string'],
|
||||
['number', 'number'],
|
||||
['boolean', 'boolean'],
|
||||
['object', 'object'],
|
||||
['array', 'array']
|
||||
]),
|
||||
'TYPE'
|
||||
);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Defines an output port that appears on the node');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Set Output block - sets value on an output
|
||||
Blockly.Blocks['noodl_set_output'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('VALUE')
|
||||
.setCheck(null)
|
||||
.appendField('📤 set output')
|
||||
.appendField(new Blockly.FieldTextInput('result'), 'NAME')
|
||||
.appendField('to');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
this.setTooltip('Sets the value of an output port');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Define Signal Input block
|
||||
Blockly.Blocks['noodl_define_signal_input'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('⚡ Define signal input')
|
||||
.appendField(new Blockly.FieldTextInput('trigger'), 'NAME');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(180);
|
||||
this.setTooltip('Defines a signal input that can trigger logic');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Define Signal Output block
|
||||
Blockly.Blocks['noodl_define_signal_output'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('⚡ Define signal output')
|
||||
.appendField(new Blockly.FieldTextInput('done'), 'NAME');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(180);
|
||||
this.setTooltip('Defines a signal output that can trigger other nodes');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Send Signal block
|
||||
Blockly.Blocks['noodl_send_signal'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField('⚡ send signal').appendField(new Blockly.FieldTextInput('done'), 'NAME');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(180);
|
||||
this.setTooltip('Sends a signal to connected nodes');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Variable Blocks
|
||||
*/
|
||||
function defineVariableBlocks() {
|
||||
// Get Variable block
|
||||
Blockly.Blocks['noodl_get_variable'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput()
|
||||
.appendField('📖 get variable')
|
||||
.appendField(new Blockly.FieldTextInput('myVariable'), 'NAME');
|
||||
this.setOutput(true, null);
|
||||
this.setColour(330);
|
||||
this.setTooltip('Gets the value of a global Noodl variable');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Set Variable block
|
||||
Blockly.Blocks['noodl_set_variable'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('VALUE')
|
||||
.setCheck(null)
|
||||
.appendField('✏️ set variable')
|
||||
.appendField(new Blockly.FieldTextInput('myVariable'), 'NAME')
|
||||
.appendField('to');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(330);
|
||||
this.setTooltip('Sets the value of a global Noodl variable');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Object Blocks (basic set - will expand in Phase E)
|
||||
*/
|
||||
function defineObjectBlocks() {
|
||||
// Get Object block
|
||||
Blockly.Blocks['noodl_get_object'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('ID').setCheck('String').appendField('📦 get object');
|
||||
this.setOutput(true, 'Object');
|
||||
this.setColour(20);
|
||||
this.setTooltip('Gets a Noodl Object by its ID');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Get Object Property block
|
||||
Blockly.Blocks['noodl_get_object_property'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('OBJECT')
|
||||
.setCheck(null)
|
||||
.appendField('📖 get')
|
||||
.appendField(new Blockly.FieldTextInput('name'), 'PROPERTY')
|
||||
.appendField('from object');
|
||||
this.setOutput(true, null);
|
||||
this.setColour(20);
|
||||
this.setTooltip('Gets a property value from an object');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Set Object Property block
|
||||
Blockly.Blocks['noodl_set_object_property'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('OBJECT')
|
||||
.setCheck(null)
|
||||
.appendField('✏️ set')
|
||||
.appendField(new Blockly.FieldTextInput('name'), 'PROPERTY')
|
||||
.appendField('on object');
|
||||
this.appendValueInput('VALUE').setCheck(null).appendField('to');
|
||||
this.setInputsInline(false);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(20);
|
||||
this.setTooltip('Sets a property value on an object');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Array Blocks (basic set - will expand in Phase E)
|
||||
*/
|
||||
function defineArrayBlocks() {
|
||||
// Get Array block
|
||||
Blockly.Blocks['noodl_get_array'] = {
|
||||
init: function () {
|
||||
this.appendDummyInput().appendField('📋 get array').appendField(new Blockly.FieldTextInput('myArray'), 'NAME');
|
||||
this.setOutput(true, 'Array');
|
||||
this.setColour(260);
|
||||
this.setTooltip('Gets a Noodl Array by name');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Array Length block
|
||||
Blockly.Blocks['noodl_array_length'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('ARRAY').setCheck('Array').appendField('🔢 length of array');
|
||||
this.setOutput(true, 'Number');
|
||||
this.setColour(260);
|
||||
this.setTooltip('Gets the number of items in an array');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
|
||||
// Array Add block
|
||||
Blockly.Blocks['noodl_array_add'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('ITEM').setCheck(null).appendField('➕ add');
|
||||
this.appendValueInput('ARRAY').setCheck('Array').appendField('to array');
|
||||
this.setInputsInline(true);
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(260);
|
||||
this.setTooltip('Adds an item to the end of an array');
|
||||
this.setHelpUrl('');
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Noodl Code Generators for Blockly
|
||||
*
|
||||
* Converts Blockly blocks into executable JavaScript code for the Noodl runtime.
|
||||
* Generated code has access to:
|
||||
* - Inputs: Input values from connections
|
||||
* - Outputs: Output values to connections
|
||||
* - Noodl.Variables: Global variables
|
||||
* - Noodl.Objects: Global objects
|
||||
* - Noodl.Arrays: Global arrays
|
||||
*
|
||||
* @module BlocklyEditor
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
import { javascriptGenerator } from 'blockly/javascript';
|
||||
|
||||
/**
|
||||
* Initialize all Noodl code generators
|
||||
*/
|
||||
export function initNoodlGenerators() {
|
||||
console.log('🔧 [Blockly] Initializing Noodl code generators');
|
||||
|
||||
// Input/Output generators
|
||||
initInputOutputGenerators();
|
||||
|
||||
// Variable generators
|
||||
initVariableGenerators();
|
||||
|
||||
// Object generators
|
||||
initObjectGenerators();
|
||||
|
||||
// Array generators
|
||||
initArrayGenerators();
|
||||
|
||||
console.log('✅ [Blockly] Noodl generators initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Input/Output Generators
|
||||
*/
|
||||
function initInputOutputGenerators() {
|
||||
// Define Input - no runtime code (used for I/O detection only)
|
||||
javascriptGenerator.forBlock['noodl_define_input'] = function () {
|
||||
return '';
|
||||
};
|
||||
|
||||
// Get Input - generates: Inputs["name"]
|
||||
javascriptGenerator.forBlock['noodl_get_input'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Inputs["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Define Output - no runtime code (used for I/O detection only)
|
||||
javascriptGenerator.forBlock['noodl_define_output'] = function () {
|
||||
return '';
|
||||
};
|
||||
|
||||
// Set Output - generates: Outputs["name"] = value;
|
||||
javascriptGenerator.forBlock['noodl_set_output'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
|
||||
return `Outputs["${name}"] = ${value};\n`;
|
||||
};
|
||||
|
||||
// Define Signal Input - no runtime code
|
||||
javascriptGenerator.forBlock['noodl_define_signal_input'] = function () {
|
||||
return '';
|
||||
};
|
||||
|
||||
// Define Signal Output - no runtime code
|
||||
javascriptGenerator.forBlock['noodl_define_signal_output'] = function () {
|
||||
return '';
|
||||
};
|
||||
|
||||
// Send Signal - generates: this.sendSignalOnOutput("name");
|
||||
javascriptGenerator.forBlock['noodl_send_signal'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
return `this.sendSignalOnOutput("${name}");\n`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Variable Generators
|
||||
*/
|
||||
function initVariableGenerators() {
|
||||
// Get Variable - generates: Noodl.Variables["name"]
|
||||
javascriptGenerator.forBlock['noodl_get_variable'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Noodl.Variables["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Set Variable - generates: Noodl.Variables["name"] = value;
|
||||
javascriptGenerator.forBlock['noodl_set_variable'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
|
||||
return `Noodl.Variables["${name}"] = ${value};\n`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Object Generators
|
||||
*/
|
||||
function initObjectGenerators() {
|
||||
// Get Object - generates: Noodl.Objects[id]
|
||||
javascriptGenerator.forBlock['noodl_get_object'] = function (block) {
|
||||
const id = javascriptGenerator.valueToCode(block, 'ID', Blockly.JavaScript.ORDER_NONE) || '""';
|
||||
const code = `Noodl.Objects[${id}]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Get Object Property - generates: object["property"]
|
||||
javascriptGenerator.forBlock['noodl_get_object_property'] = function (block) {
|
||||
const property = block.getFieldValue('PROPERTY');
|
||||
const object = javascriptGenerator.valueToCode(block, 'OBJECT', Blockly.JavaScript.ORDER_MEMBER) || '{}';
|
||||
const code = `${object}["${property}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Set Object Property - generates: object["property"] = value;
|
||||
javascriptGenerator.forBlock['noodl_set_object_property'] = function (block) {
|
||||
const property = block.getFieldValue('PROPERTY');
|
||||
const object = javascriptGenerator.valueToCode(block, 'OBJECT', Blockly.JavaScript.ORDER_MEMBER) || '{}';
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Blockly.JavaScript.ORDER_ASSIGNMENT) || 'null';
|
||||
return `${object}["${property}"] = ${value};\n`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Array Generators
|
||||
*/
|
||||
function initArrayGenerators() {
|
||||
// Get Array - generates: Noodl.Arrays["name"]
|
||||
javascriptGenerator.forBlock['noodl_get_array'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Noodl.Arrays["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Array Length - generates: array.length
|
||||
javascriptGenerator.forBlock['noodl_array_length'] = function (block) {
|
||||
const array = javascriptGenerator.valueToCode(block, 'ARRAY', Blockly.JavaScript.ORDER_MEMBER) || '[]';
|
||||
const code = `${array}.length`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
};
|
||||
|
||||
// Array Add - generates: array.push(item);
|
||||
javascriptGenerator.forBlock['noodl_array_add'] = function (block) {
|
||||
const item = javascriptGenerator.valueToCode(block, 'ITEM', Blockly.JavaScript.ORDER_NONE) || 'null';
|
||||
const array = javascriptGenerator.valueToCode(block, 'ARRAY', Blockly.JavaScript.ORDER_MEMBER) || '[]';
|
||||
return `${array}.push(${item});\n`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complete JavaScript code from workspace
|
||||
*
|
||||
* @param workspace - The Blockly workspace
|
||||
* @returns Generated JavaScript code
|
||||
*/
|
||||
export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
||||
return javascriptGenerator.workspaceToCode(workspace);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* BlocklyEditor Module
|
||||
*
|
||||
* Entry point for Blockly integration in Noodl.
|
||||
* Exports components, blocks, and generators for visual logic building.
|
||||
*
|
||||
* @module BlocklyEditor
|
||||
*/
|
||||
|
||||
import { initNoodlBlocks } from './NoodlBlocks';
|
||||
import { initNoodlGenerators } from './NoodlGenerators';
|
||||
|
||||
// Main component
|
||||
export { BlocklyWorkspace } from './BlocklyWorkspace';
|
||||
export type { BlocklyWorkspaceProps } from './BlocklyWorkspace';
|
||||
|
||||
// Block definitions and generators
|
||||
export { initNoodlBlocks } from './NoodlBlocks';
|
||||
export { initNoodlGenerators, generateCode } from './NoodlGenerators';
|
||||
|
||||
/**
|
||||
* Initialize all Noodl Blockly extensions
|
||||
* Call this once at app startup before using Blockly components
|
||||
*/
|
||||
export function initBlocklyIntegration() {
|
||||
console.log('🔧 [Blockly] Initializing Noodl Blockly integration');
|
||||
|
||||
// Initialize custom blocks
|
||||
initNoodlBlocks();
|
||||
|
||||
// Initialize code generators
|
||||
initNoodlGenerators();
|
||||
|
||||
console.log('✅ [Blockly] Integration initialized');
|
||||
}
|
||||
Reference in New Issue
Block a user