Finished Blockly prototype, updated project template json
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 682 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,36 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = async function (params) {
|
||||
if (process.platform !== 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!process.env.appleIdPassword) {
|
||||
console.log('apple password not set, skipping notarization');
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = 'com.opennoodl.app';
|
||||
|
||||
const appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);
|
||||
if (!fs.existsSync(appPath)) {
|
||||
throw new Error(`Cannot find application at: ${appPath}`);
|
||||
}
|
||||
|
||||
console.log(`Notarizing ${appId} found at ${appPath}`);
|
||||
|
||||
try {
|
||||
const electron_notarize = require('electron-notarize');
|
||||
await electron_notarize.notarize({
|
||||
appBundleId: appId,
|
||||
appPath: appPath,
|
||||
appleId: process.env.appleId,
|
||||
appleIdPassword: process.env.appleIdPassword
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
console.log(`Done notarizing ${appId}`);
|
||||
};
|
||||
@@ -60,6 +60,7 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@blockly/theme-dark": "^8.0.3",
|
||||
"@electron/remote": "^2.1.3",
|
||||
"@jaames/iro": "^5.5.2",
|
||||
"@microlink/react-json-view": "^1.27.0",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
|
||||
|
||||
import { EventDispatcher } from '../../../shared/utils/EventDispatcher';
|
||||
|
||||
/**
|
||||
* Tab types supported by the canvas tab system
|
||||
*/
|
||||
export type TabType = 'canvas' | 'logic-builder';
|
||||
export type TabType = 'logic-builder';
|
||||
|
||||
/**
|
||||
* Tab data structure
|
||||
@@ -62,15 +64,10 @@ interface CanvasTabsProviderProps {
|
||||
* Provider for canvas tabs state
|
||||
*/
|
||||
export function CanvasTabsProvider({ children }: CanvasTabsProviderProps) {
|
||||
// Always start with the canvas tab
|
||||
const [tabs, setTabs] = useState<Tab[]>([
|
||||
{
|
||||
id: 'canvas',
|
||||
type: 'canvas'
|
||||
}
|
||||
]);
|
||||
// Start with no tabs - Logic Builder tabs are opened on demand
|
||||
const [tabs, setTabs] = useState<Tab[]>([]);
|
||||
|
||||
const [activeTabId, setActiveTabId] = useState<string>('canvas');
|
||||
const [activeTabId, setActiveTabId] = useState<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Open a new tab or switch to existing one
|
||||
@@ -93,23 +90,49 @@ export function CanvasTabsProvider({ children }: CanvasTabsProviderProps) {
|
||||
...newTab,
|
||||
id: tabId
|
||||
};
|
||||
return [...prevTabs, tab];
|
||||
|
||||
const newTabs = [...prevTabs, tab];
|
||||
|
||||
// Emit event that a Logic Builder tab was opened (first tab)
|
||||
if (prevTabs.length === 0) {
|
||||
EventDispatcher.instance.emit('LogicBuilder.TabOpened');
|
||||
}
|
||||
|
||||
return newTabs;
|
||||
});
|
||||
|
||||
// Switch to the new/existing tab
|
||||
setActiveTabId(tabId);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Listen for Logic Builder tab open requests from property panel
|
||||
*/
|
||||
useEffect(() => {
|
||||
const context = {};
|
||||
|
||||
const handleOpenTab = (data: { nodeId: string; nodeName: string; workspace: string }) => {
|
||||
console.log('[CanvasTabsContext] Received LogicBuilder.OpenTab event:', data);
|
||||
openTab({
|
||||
type: 'logic-builder',
|
||||
nodeId: data.nodeId,
|
||||
nodeName: data.nodeName,
|
||||
workspace: data.workspace
|
||||
});
|
||||
};
|
||||
|
||||
EventDispatcher.instance.on('LogicBuilder.OpenTab', handleOpenTab, context);
|
||||
|
||||
return () => {
|
||||
EventDispatcher.instance.off(context);
|
||||
};
|
||||
}, [openTab]);
|
||||
|
||||
/**
|
||||
* Close a tab by ID
|
||||
*/
|
||||
const closeTab = useCallback(
|
||||
(tabId: string) => {
|
||||
// Can't close the canvas tab
|
||||
if (tabId === 'canvas') {
|
||||
return;
|
||||
}
|
||||
|
||||
setTabs((prevTabs) => {
|
||||
const tabIndex = prevTabs.findIndex((t) => t.id === tabId);
|
||||
if (tabIndex === -1) {
|
||||
@@ -118,9 +141,15 @@ export function CanvasTabsProvider({ children }: CanvasTabsProviderProps) {
|
||||
|
||||
const newTabs = prevTabs.filter((t) => t.id !== tabId);
|
||||
|
||||
// If closing the active tab, switch to canvas
|
||||
// If closing the active tab, switch to another tab or clear active
|
||||
if (activeTabId === tabId) {
|
||||
setActiveTabId('canvas');
|
||||
if (newTabs.length > 0) {
|
||||
setActiveTabId(newTabs[newTabs.length - 1].id);
|
||||
} else {
|
||||
setActiveTabId(undefined);
|
||||
// Emit event that all Logic Builder tabs are closed
|
||||
EventDispatcher.instance.emit('LogicBuilder.AllTabsClosed');
|
||||
}
|
||||
}
|
||||
|
||||
return newTabs;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="nodegrapgeditor-bg nodegrapheditor-canvas" style="width: 100%; height: 100%">
|
||||
<!-- Canvas Tabs Root (for React component) -->
|
||||
<div id="canvas-tabs-root" style="width: 100%; height: 100%"></div>
|
||||
<div id="canvas-tabs-root" style="position: absolute; width: 100%; height: 100%; z-index: 100; pointer-events: none;"></div>
|
||||
|
||||
<!--
|
||||
wrap in a div to not trigger chromium bug where comments "scrolls" all the siblings
|
||||
|
||||
@@ -22,19 +22,28 @@ declare global {
|
||||
* This makes IODetector and code generation available to runtime nodes
|
||||
*/
|
||||
export function initBlocklyEditorGlobals() {
|
||||
console.log('🔍 [BlocklyGlobals] initBlocklyEditorGlobals called');
|
||||
console.log('🔍 [BlocklyGlobals] window undefined?', typeof window === 'undefined');
|
||||
|
||||
// Create NoodlEditor namespace if it doesn't exist
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('🔍 [BlocklyGlobals] window.NoodlEditor before:', window.NoodlEditor);
|
||||
|
||||
if (!window.NoodlEditor) {
|
||||
window.NoodlEditor = {};
|
||||
console.log('🔍 [BlocklyGlobals] Created new window.NoodlEditor');
|
||||
}
|
||||
|
||||
// Expose IODetector
|
||||
window.NoodlEditor.detectIO = detectIO;
|
||||
console.log('🔍 [BlocklyGlobals] Assigned detectIO:', typeof window.NoodlEditor.detectIO);
|
||||
|
||||
// Expose code generator
|
||||
window.NoodlEditor.generateBlocklyCode = generateCode;
|
||||
console.log('🔍 [BlocklyGlobals] Assigned generateBlocklyCode:', typeof window.NoodlEditor.generateBlocklyCode);
|
||||
|
||||
console.log('✅ [Blockly] Editor globals initialized');
|
||||
console.log('🔍 [BlocklyGlobals] window.NoodlEditor after:', window.NoodlEditor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,14 +80,16 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.blocklyMenuItem {
|
||||
.blocklyContextMenu .blocklyMenuItem {
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
font-size: 13px !important;
|
||||
padding: 6px 12px !important;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:hover * {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
}
|
||||
|
||||
&.blocklyMenuItemDisabled {
|
||||
@@ -101,21 +103,148 @@
|
||||
fill: var(--theme-color-border-default) !important;
|
||||
}
|
||||
|
||||
/* Field editor backgrounds */
|
||||
.blocklyWidgetDiv {
|
||||
& .goog-menu {
|
||||
/* Field editor backgrounds (dropdowns, text inputs, etc.) */
|
||||
/* NOTE: blocklyWidgetDiv and blocklyDropDownDiv are rendered at document root! */
|
||||
.blocklyWidgetDiv,
|
||||
.blocklyDropDownDiv {
|
||||
z-index: 10000 !important; /* Ensure it's above everything */
|
||||
}
|
||||
|
||||
/* Blockly dropdown container - DARK BACKGROUND */
|
||||
.blocklyDropDownDiv,
|
||||
:global(.blocklyDropDownDiv) {
|
||||
background-color: var(--theme-color-bg-3) !important; /* DARK background */
|
||||
max-height: 400px !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
|
||||
/* Inner scrollable container */
|
||||
& > div {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
border: 1px solid var(--theme-color-border-default) !important;
|
||||
border-radius: 4px !important;
|
||||
max-height: 400px !important;
|
||||
overflow-y: auto !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;
|
||||
}
|
||||
/* SVG containers inside dropdown */
|
||||
& svg {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Text input fields */
|
||||
.blocklyWidgetDiv input,
|
||||
.blocklyHtmlInput {
|
||||
background-color: var(--theme-color-bg-3) !important;
|
||||
color: var(--theme-color-fg-default) !important;
|
||||
border: 1px solid var(--theme-color-border-default) !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 4px 8px !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
}
|
||||
|
||||
/* Dropdown menus - DARK BACKGROUND with WHITE TEXT (matches Noodl theme) */
|
||||
/* Target ACTUAL Blockly classes: .blocklyMenuItem not .goog-menuitem */
|
||||
|
||||
.goog-menu,
|
||||
:global(.goog-menu) {
|
||||
background-color: var(--theme-color-bg-3) !important; /* DARK background */
|
||||
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;
|
||||
max-height: 400px !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
/* Target Blockly's ACTUAL menu item class - DROPDOWN MENUS */
|
||||
.blocklyDropDownDiv .blocklyMenuItem,
|
||||
:global(.blocklyDropDownDiv) :global(.blocklyMenuItem) {
|
||||
color: #ffffff !important; /* WHITE text */
|
||||
background-color: transparent !important;
|
||||
padding: 6px 12px !important;
|
||||
cursor: pointer !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
font-size: 13px !important;
|
||||
|
||||
/* ALL children white */
|
||||
& *,
|
||||
& div,
|
||||
& span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* HOVER - Keep white text with lighter background */
|
||||
&:hover,
|
||||
&:hover *,
|
||||
&:hover div,
|
||||
&:hover span {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
color: #ffffff !important; /* WHITE on hover */
|
||||
}
|
||||
|
||||
&[aria-selected='true'],
|
||||
&[aria-selected='true'] * {
|
||||
background-color: var(--theme-color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
&[aria-disabled='true'],
|
||||
&[aria-disabled='true'] * {
|
||||
color: #999999 !important;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Target Blockly's ACTUAL content class */
|
||||
.blocklyMenuItemContent,
|
||||
:global(.blocklyMenuItemContent) {
|
||||
color: #ffffff !important;
|
||||
|
||||
& *,
|
||||
& div,
|
||||
& span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for goog- classes if they exist */
|
||||
.goog-menuitem,
|
||||
.goog-option,
|
||||
:global(.goog-menuitem),
|
||||
:global(.goog-option) {
|
||||
color: #ffffff !important;
|
||||
background-color: transparent !important;
|
||||
padding: 6px 12px !important;
|
||||
font-family: var(--theme-font-family) !important;
|
||||
font-size: 13px !important;
|
||||
|
||||
& *,
|
||||
& div,
|
||||
& span {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover * {
|
||||
background-color: var(--theme-color-bg-4) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goog-menuitem-content,
|
||||
:global(.goog-menuitem-content) {
|
||||
color: #ffffff !important;
|
||||
|
||||
& * {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Blockly dropdown content container */
|
||||
:global(.blocklyDropDownContent) {
|
||||
max-height: 400px !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
* @module BlocklyEditor
|
||||
*/
|
||||
|
||||
import DarkTheme from '@blockly/theme-dark';
|
||||
import * as Blockly from 'blockly';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { javascriptGenerator } from 'blockly/javascript';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import css from './BlocklyWorkspace.module.scss';
|
||||
import { initBlocklyIntegration } from './index';
|
||||
|
||||
export interface BlocklyWorkspaceProps {
|
||||
/** Initial workspace JSON (for loading saved state) */
|
||||
@@ -43,18 +46,21 @@ export function BlocklyWorkspace({
|
||||
}: BlocklyWorkspaceProps) {
|
||||
const blocklyDiv = useRef<HTMLDivElement>(null);
|
||||
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const changeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Initialize Blockly workspace
|
||||
useEffect(() => {
|
||||
if (!blocklyDiv.current) return;
|
||||
|
||||
// Initialize custom Noodl blocks and generators before creating workspace
|
||||
initBlocklyIntegration();
|
||||
|
||||
console.log('🔧 [Blockly] Initializing workspace');
|
||||
|
||||
// Inject Blockly
|
||||
// Inject Blockly with dark theme
|
||||
const workspace = Blockly.inject(blocklyDiv.current, {
|
||||
toolbox: toolbox || getDefaultToolbox(),
|
||||
theme: theme,
|
||||
theme: theme || DarkTheme,
|
||||
readOnly: readOnly,
|
||||
trashcan: true,
|
||||
zoom: {
|
||||
@@ -86,13 +92,43 @@ export function BlocklyWorkspace({
|
||||
}
|
||||
}
|
||||
|
||||
setIsInitialized(true);
|
||||
// Listen for changes - filter to only respond to finished workspace changes,
|
||||
// not UI events like dragging or moving blocks
|
||||
const changeListener = (event: Blockly.Events.Abstract) => {
|
||||
if (!onChange || !workspace) return;
|
||||
|
||||
// Listen for changes
|
||||
const changeListener = () => {
|
||||
if (onChange && workspace) {
|
||||
// Ignore UI events that don't change the workspace structure
|
||||
// These fire constantly during drags and can cause state corruption
|
||||
if (event.type === Blockly.Events.BLOCK_DRAG) return;
|
||||
if (event.type === Blockly.Events.BLOCK_MOVE && !event.isUiEvent) return; // Allow programmatic moves
|
||||
if (event.type === Blockly.Events.SELECTED) return;
|
||||
if (event.type === Blockly.Events.CLICK) return;
|
||||
if (event.type === Blockly.Events.VIEWPORT_CHANGE) return;
|
||||
if (event.type === Blockly.Events.TOOLBOX_ITEM_SELECT) return;
|
||||
if (event.type === Blockly.Events.THEME_CHANGE) return;
|
||||
if (event.type === Blockly.Events.TRASHCAN_OPEN) return;
|
||||
|
||||
// For UI events that DO change the workspace, debounce them
|
||||
const isUiEvent = event.isUiEvent;
|
||||
|
||||
if (isUiEvent) {
|
||||
// Clear any pending timeout for UI events
|
||||
if (changeTimeoutRef.current) {
|
||||
clearTimeout(changeTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Debounce UI-initiated changes (user editing)
|
||||
changeTimeoutRef.current = setTimeout(() => {
|
||||
const json = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
const code = javascriptGenerator.workspaceToCode(workspace);
|
||||
console.log('[Blockly] Generated code:', code);
|
||||
onChange(workspace, json, code);
|
||||
}, 300);
|
||||
} else {
|
||||
// Programmatic changes fire immediately (e.g., undo/redo, loading)
|
||||
const json = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
|
||||
const code = Blockly.JavaScript.workspaceToCode(workspace);
|
||||
const code = javascriptGenerator.workspaceToCode(workspace);
|
||||
console.log('[Blockly] Generated code:', code);
|
||||
onChange(workspace, json, code);
|
||||
}
|
||||
};
|
||||
@@ -102,24 +138,21 @@ export function BlocklyWorkspace({
|
||||
// Cleanup
|
||||
return () => {
|
||||
console.log('🧹 [Blockly] Disposing workspace');
|
||||
|
||||
// Clear any pending debounced calls
|
||||
if (changeTimeoutRef.current) {
|
||||
clearTimeout(changeTimeoutRef.current);
|
||||
}
|
||||
|
||||
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]);
|
||||
// NOTE: Do NOT reload workspace on initialWorkspace changes!
|
||||
// The initialWorkspace prop changes on every save, which would cause corruption.
|
||||
// Workspace is loaded ONCE on mount above, and changes are saved via onChange callback.
|
||||
|
||||
return (
|
||||
<div className={css.Root}>
|
||||
@@ -129,13 +162,68 @@ export function BlocklyWorkspace({
|
||||
}
|
||||
|
||||
/**
|
||||
* Default toolbox with standard Blockly blocks
|
||||
* This will be replaced with Noodl-specific toolbox
|
||||
* Default toolbox with Noodl-specific blocks
|
||||
*/
|
||||
function getDefaultToolbox(): Blockly.utils.toolbox.ToolboxDefinition {
|
||||
return {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
// Noodl I/O Category
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Noodl 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' }
|
||||
]
|
||||
},
|
||||
// Noodl Signals Category
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Noodl Signals',
|
||||
colour: '180',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'noodl_define_signal_input' },
|
||||
{ kind: 'block', type: 'noodl_define_signal_output' },
|
||||
{ kind: 'block', type: 'noodl_send_signal' }
|
||||
]
|
||||
},
|
||||
// Noodl Variables Category
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Noodl Variables',
|
||||
colour: '330',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'noodl_get_variable' },
|
||||
{ kind: 'block', type: 'noodl_set_variable' }
|
||||
]
|
||||
},
|
||||
// Noodl Objects Category
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Noodl Objects',
|
||||
colour: '20',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'noodl_get_object' },
|
||||
{ kind: 'block', type: 'noodl_get_object_property' },
|
||||
{ kind: 'block', type: 'noodl_set_object_property' }
|
||||
]
|
||||
},
|
||||
// Noodl Arrays Category
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Noodl Arrays',
|
||||
colour: '260',
|
||||
contents: [
|
||||
{ kind: 'block', type: 'noodl_get_array' },
|
||||
{ kind: 'block', type: 'noodl_array_length' },
|
||||
{ kind: 'block', type: 'noodl_array_add' }
|
||||
]
|
||||
},
|
||||
// Standard Logic blocks (useful for conditionals)
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Logic',
|
||||
@@ -148,6 +236,7 @@ function getDefaultToolbox(): Blockly.utils.toolbox.ToolboxDefinition {
|
||||
{ kind: 'block', type: 'logic_boolean' }
|
||||
]
|
||||
},
|
||||
// Standard Math blocks
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Math',
|
||||
@@ -158,6 +247,7 @@ function getDefaultToolbox(): Blockly.utils.toolbox.ToolboxDefinition {
|
||||
{ kind: 'block', type: 'math_single' }
|
||||
]
|
||||
},
|
||||
// Standard Text blocks
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'Text',
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
import * as Blockly from 'blockly';
|
||||
import { javascriptGenerator } from 'blockly/javascript';
|
||||
import { javascriptGenerator, Order } from 'blockly/javascript';
|
||||
|
||||
/**
|
||||
* Initialize all Noodl code generators
|
||||
@@ -49,7 +49,7 @@ function initInputOutputGenerators() {
|
||||
javascriptGenerator.forBlock['noodl_get_input'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Inputs["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, Order.MEMBER];
|
||||
};
|
||||
|
||||
// Define Output - no runtime code (used for I/O detection only)
|
||||
@@ -60,7 +60,7 @@ function initInputOutputGenerators() {
|
||||
// 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';
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || 'null';
|
||||
return `Outputs["${name}"] = ${value};\n`;
|
||||
};
|
||||
|
||||
@@ -89,13 +89,13 @@ function initVariableGenerators() {
|
||||
javascriptGenerator.forBlock['noodl_get_variable'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Noodl.Variables["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, 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';
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || 'null';
|
||||
return `Noodl.Variables["${name}"] = ${value};\n`;
|
||||
};
|
||||
}
|
||||
@@ -106,24 +106,24 @@ function initVariableGenerators() {
|
||||
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 id = javascriptGenerator.valueToCode(block, 'ID', Order.NONE) || '""';
|
||||
const code = `Noodl.Objects[${id}]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, 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 object = javascriptGenerator.valueToCode(block, 'OBJECT', Order.MEMBER) || '{}';
|
||||
const code = `${object}["${property}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, 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';
|
||||
const object = javascriptGenerator.valueToCode(block, 'OBJECT', Order.MEMBER) || '{}';
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || 'null';
|
||||
return `${object}["${property}"] = ${value};\n`;
|
||||
};
|
||||
}
|
||||
@@ -136,20 +136,20 @@ function initArrayGenerators() {
|
||||
javascriptGenerator.forBlock['noodl_get_array'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const code = `Noodl.Arrays["${name}"]`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, 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 array = javascriptGenerator.valueToCode(block, 'ARRAY', Order.MEMBER) || '[]';
|
||||
const code = `${array}.length`;
|
||||
return [code, Blockly.JavaScript.ORDER_MEMBER];
|
||||
return [code, 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) || '[]';
|
||||
const item = javascriptGenerator.valueToCode(block, 'ITEM', Order.NONE) || 'null';
|
||||
const array = javascriptGenerator.valueToCode(block, 'ARRAY', Order.MEMBER) || '[]';
|
||||
return `${array}.push(${item});\n`;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,11 +20,20 @@ export type { BlocklyWorkspaceProps } from './BlocklyWorkspace';
|
||||
export { initNoodlBlocks } from './NoodlBlocks';
|
||||
export { initNoodlGenerators, generateCode } from './NoodlGenerators';
|
||||
|
||||
// Track initialization to prevent double-registration
|
||||
let blocklyInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize all Noodl Blockly extensions
|
||||
* Call this once at app startup before using Blockly components
|
||||
* Safe to call multiple times - will only initialize once
|
||||
*/
|
||||
export function initBlocklyIntegration() {
|
||||
if (blocklyInitialized) {
|
||||
console.log('⏭️ [Blockly] Already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔧 [Blockly] Initializing Noodl Blockly integration');
|
||||
|
||||
// Initialize custom blocks
|
||||
@@ -35,5 +44,6 @@ export function initBlocklyIntegration() {
|
||||
|
||||
// Note: BlocklyEditorGlobals auto-initializes via side-effect import above
|
||||
|
||||
blocklyInitialized = true;
|
||||
console.log('✅ [Blockly] Integration initialized');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: all; /* Enable clicks on tabs */
|
||||
}
|
||||
|
||||
.TabBar {
|
||||
|
||||
@@ -6,14 +6,15 @@ import css from './CanvasTabs.module.scss';
|
||||
|
||||
export interface CanvasTabsProps {
|
||||
/** Callback when workspace changes */
|
||||
onWorkspaceChange?: (nodeId: string, workspace: string) => void;
|
||||
onWorkspaceChange?: (nodeId: string, workspace: string, code: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas Tabs Component
|
||||
*
|
||||
* Manages tabs for canvas view and Logic Builder (Blockly) editors.
|
||||
* Renders a tab bar and switches content based on active tab.
|
||||
* Manages tabs for Logic Builder (Blockly) editors.
|
||||
* The canvas itself is NOT managed here - it's always visible in the background
|
||||
* unless a Logic Builder tab is open.
|
||||
*/
|
||||
export function CanvasTabs({ onWorkspaceChange }: CanvasTabsProps) {
|
||||
const { tabs, activeTabId, switchTab, closeTab, updateTab } = useCanvasTabs();
|
||||
@@ -23,17 +24,17 @@ export function CanvasTabs({ onWorkspaceChange }: CanvasTabsProps) {
|
||||
/**
|
||||
* Handle workspace changes from Blockly editor
|
||||
*/
|
||||
const handleWorkspaceChange = (_workspaceSvg: unknown, json: string) => {
|
||||
if (!activeTab || activeTab.type !== 'logic-builder') {
|
||||
const handleWorkspaceChange = (_workspaceSvg: unknown, json: string, code: string) => {
|
||||
if (!activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update tab's workspace with JSON
|
||||
updateTab(activeTab.id, { workspace: json });
|
||||
|
||||
// Notify parent
|
||||
// Notify parent (pass both workspace JSON and generated code)
|
||||
if (onWorkspaceChange && activeTab.nodeId) {
|
||||
onWorkspaceChange(activeTab.nodeId, json);
|
||||
onWorkspaceChange(activeTab.nodeId, json, code);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,13 +53,17 @@ export function CanvasTabs({ onWorkspaceChange }: CanvasTabsProps) {
|
||||
closeTab(tabId);
|
||||
};
|
||||
|
||||
// Don't render anything if no tabs are open
|
||||
if (tabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css['CanvasTabs']}>
|
||||
{/* Tab Bar */}
|
||||
<div className={css['TabBar']}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const canClose = tab.type !== 'canvas';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -69,20 +74,16 @@ export function CanvasTabs({ onWorkspaceChange }: CanvasTabsProps) {
|
||||
aria-selected={isActive}
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className={css['TabLabel']}>
|
||||
{tab.type === 'canvas' ? 'Canvas' : `Logic Builder: ${tab.nodeName || 'Unnamed'}`}
|
||||
</span>
|
||||
<span className={css['TabLabel']}>Logic Builder: {tab.nodeName || 'Unnamed'}</span>
|
||||
|
||||
{canClose && (
|
||||
<button
|
||||
className={css['TabCloseButton']}
|
||||
onClick={(e) => handleTabClose(e, tab.id)}
|
||||
aria-label="Close tab"
|
||||
title="Close tab"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={css['TabCloseButton']}
|
||||
onClick={(e) => handleTabClose(e, tab.id)}
|
||||
aria-label="Close tab"
|
||||
title="Close tab"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -90,13 +91,7 @@ export function CanvasTabs({ onWorkspaceChange }: CanvasTabsProps) {
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className={css['TabContent']}>
|
||||
{activeTab?.type === 'canvas' && (
|
||||
<div className={css['CanvasContainer']} id="nodegraph-canvas-container">
|
||||
{/* Canvas will be rendered here by NodeGraphEditor */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab?.type === 'logic-builder' && (
|
||||
{activeTab && (
|
||||
<div className={css['BlocklyContainer']}>
|
||||
<BlocklyWorkspace initialWorkspace={activeTab.workspace || undefined} onChange={handleWorkspaceChange} />
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,8 @@ import { NodeLibrary } from '../models/nodelibrary';
|
||||
import { ProjectModel } from '../models/projectmodel';
|
||||
import { WarningsModel } from '../models/warningsmodel';
|
||||
import { HighlightManager } from '../services/HighlightManager';
|
||||
// Initialize Blockly globals early (must run before runtime nodes load)
|
||||
import { initBlocklyEditorGlobals } from '../utils/BlocklyEditorGlobals';
|
||||
import DebugInspector from '../utils/debuginspector';
|
||||
import { rectanglesOverlap, guid } from '../utils/utils';
|
||||
import { ViewerConnection } from '../ViewerConnection';
|
||||
@@ -59,6 +61,8 @@ import PopupLayer from './popuplayer';
|
||||
import { showContextMenuInPopup } from './ShowContextMenuInPopup';
|
||||
import { ToastLayer } from './ToastLayer/ToastLayer';
|
||||
|
||||
initBlocklyEditorGlobals();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const NodeGraphEditorTemplate = require('../templates/nodegrapheditor.html');
|
||||
|
||||
@@ -300,13 +304,32 @@ export class NodeGraphEditor extends View {
|
||||
this
|
||||
);
|
||||
|
||||
// Listen for Logic Builder tab open requests
|
||||
// Listen for Logic Builder tab opened - hide canvas
|
||||
EventDispatcher.instance.on(
|
||||
'LogicBuilder.TabOpened',
|
||||
() => {
|
||||
console.log('[NodeGraphEditor] Logic Builder tab opened - hiding canvas');
|
||||
this.setCanvasVisibility(false);
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
// Listen for all Logic Builder tabs closed - show canvas
|
||||
EventDispatcher.instance.on(
|
||||
'LogicBuilder.AllTabsClosed',
|
||||
() => {
|
||||
console.log('[NodeGraphEditor] All Logic Builder tabs closed - showing canvas');
|
||||
this.setCanvasVisibility(true);
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
// Listen for Logic Builder tab open requests (for opening tabs from property panel)
|
||||
EventDispatcher.instance.on(
|
||||
'LogicBuilder.OpenTab',
|
||||
(args: { nodeId: string; nodeName: string; workspace: string }) => {
|
||||
console.log('[NodeGraphEditor] Opening Logic Builder tab for node:', args.nodeId);
|
||||
// The CanvasTabs context will handle the actual tab opening via EventDispatcher
|
||||
// This is just logged for debugging - the actual implementation happens in Phase C Step 6
|
||||
// The CanvasTabs context will handle the actual tab opening
|
||||
},
|
||||
this
|
||||
);
|
||||
@@ -941,7 +964,7 @@ export class NodeGraphEditor extends View {
|
||||
/**
|
||||
* Handle workspace changes from Blockly editor
|
||||
*/
|
||||
handleBlocklyWorkspaceChange(nodeId: string, workspace: string) {
|
||||
handleBlocklyWorkspaceChange(nodeId: string, workspace: string, code: string) {
|
||||
console.log(`[NodeGraphEditor] Workspace changed for node ${nodeId}`);
|
||||
|
||||
const node = this.findNodeWithId(nodeId);
|
||||
@@ -950,11 +973,14 @@ export class NodeGraphEditor extends View {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save workspace to node model
|
||||
// Save workspace JSON to node model
|
||||
node.model.setParameter('workspace', workspace);
|
||||
|
||||
// TODO: Generate code and update ports
|
||||
// This will be implemented in Phase C Step 7
|
||||
// Save generated JavaScript code to node model
|
||||
// This triggers the runtime's parameterUpdated listener which calls updatePorts()
|
||||
node.model.setParameter('generatedCode', code);
|
||||
|
||||
console.log(`[NodeGraphEditor] Saved workspace and generated code for node ${nodeId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1015,6 +1041,35 @@ export class NodeGraphEditor extends View {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set canvas visibility (hide when Logic Builder is open, show when closed)
|
||||
*/
|
||||
setCanvasVisibility(visible: boolean) {
|
||||
const canvasElement = this.el.find('#nodegraphcanvas');
|
||||
const commentLayerBg = this.el.find('#comment-layer-bg');
|
||||
const commentLayerFg = this.el.find('#comment-layer-fg');
|
||||
const highlightOverlay = this.el.find('#highlight-overlay-layer');
|
||||
const componentTrail = this.el.find('.nodegraph-component-trail-root');
|
||||
|
||||
if (visible) {
|
||||
// Show canvas and related elements
|
||||
canvasElement.css('display', 'block');
|
||||
commentLayerBg.css('display', 'block');
|
||||
commentLayerFg.css('display', 'block');
|
||||
highlightOverlay.css('display', 'block');
|
||||
componentTrail.css('display', 'flex');
|
||||
this.domElementContainer.style.display = '';
|
||||
} else {
|
||||
// Hide canvas and related elements
|
||||
canvasElement.css('display', 'none');
|
||||
commentLayerBg.css('display', 'none');
|
||||
commentLayerFg.css('display', 'none');
|
||||
highlightOverlay.css('display', 'none');
|
||||
componentTrail.css('display', 'none');
|
||||
this.domElementContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// This is called by the parent view (frames view) when the size and position
|
||||
// changes
|
||||
resize(layout) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
render() {
|
||||
// Create a simple container with a button
|
||||
const html = `
|
||||
<div class="property-basic-container logic-builder-workspace-editor">
|
||||
<div class="property-basic-container logic-builder-workspace-editor" style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<div class="property-label-container" style="display: flex; align-items: center; gap: 8px;">
|
||||
<div class="property-changed-dot" data-click="resetToDefault" style="display: none;"></div>
|
||||
<div class="property-label">${this.displayName}</div>
|
||||
@@ -77,11 +77,12 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
}
|
||||
|
||||
onEditBlocksClicked() {
|
||||
const nodeId = this.parent.model.id;
|
||||
const nodeName = this.parent.model.label || this.parent.model.getDisplayName() || 'Logic Builder';
|
||||
const workspace = this.parent.model.getParameter('workspace') || '';
|
||||
// ModelProxy wraps the actual node model in a .model property
|
||||
const nodeId = this.parent?.model?.model?.id;
|
||||
const nodeName = this.parent?.model?.model?.label || this.parent?.model?.type?.displayName || 'Logic Builder';
|
||||
const workspace = this.parent?.model?.getParameter('workspace') || '';
|
||||
|
||||
console.log('[LogicBuilderWorkspaceType] Opening tab for node:', nodeId);
|
||||
console.log('[LogicBuilderWorkspaceType] Opening Logic Builder tab for node:', nodeId);
|
||||
|
||||
// Emit event to open Logic Builder tab
|
||||
EventDispatcher.instance.emit('LogicBuilder.OpenTab', {
|
||||
|
||||
@@ -104,8 +104,16 @@ const LogicBuilderNode = {
|
||||
// Create execution context
|
||||
const context = this._createExecutionContext(triggerSignal);
|
||||
|
||||
// Execute generated code
|
||||
internal.compiledFunction.call(context);
|
||||
// Execute generated code, passing context variables as parameters
|
||||
internal.compiledFunction(
|
||||
context.Inputs,
|
||||
context.Outputs,
|
||||
context.Noodl,
|
||||
context.Variables,
|
||||
context.Objects,
|
||||
context.Arrays,
|
||||
context.sendSignalOnOutput
|
||||
);
|
||||
|
||||
// Update outputs
|
||||
for (const outputName in context.Outputs) {
|
||||
@@ -179,9 +187,18 @@ const LogicBuilderNode = {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Wrap code in a function
|
||||
// Code will have access to: Inputs, Outputs, Noodl, Variables, Objects, Arrays, sendSignalOnOutput
|
||||
const fn = new Function(code);
|
||||
// Create function with parameters for context variables
|
||||
// This makes Inputs, Outputs, Noodl, etc. available to the generated code
|
||||
const fn = new Function(
|
||||
'Inputs',
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Variables',
|
||||
'Objects',
|
||||
'Arrays',
|
||||
'sendSignalOnOutput',
|
||||
code
|
||||
);
|
||||
return fn;
|
||||
} catch (error) {
|
||||
console.error('[Logic Builder] Failed to compile function:', error);
|
||||
@@ -200,13 +217,12 @@ const LogicBuilderNode = {
|
||||
|
||||
inputs: {
|
||||
workspace: {
|
||||
group: 'General',
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
editorType: 'logic-builder-workspace'
|
||||
},
|
||||
displayName: 'Workspace',
|
||||
displayName: 'Logic Blocks',
|
||||
set: function (value) {
|
||||
const internal = this._internal;
|
||||
internal.workspace = value;
|
||||
@@ -214,14 +230,23 @@ const LogicBuilderNode = {
|
||||
}
|
||||
},
|
||||
generatedCode: {
|
||||
group: 'General',
|
||||
type: 'string',
|
||||
displayName: 'Generated Code',
|
||||
group: 'Advanced',
|
||||
editorName: 'Hidden', // Hide from property panel
|
||||
set: function (value) {
|
||||
const internal = this._internal;
|
||||
internal.generatedCode = value;
|
||||
internal.compiledFunction = null; // Reset compiled function
|
||||
}
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
group: 'Signals',
|
||||
valueChangedToTrue: function () {
|
||||
this._executeLogic('run');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -243,14 +268,14 @@ const LogicBuilderNode = {
|
||||
*/
|
||||
let updatePortsImpl = null;
|
||||
|
||||
function updatePorts(nodeId, workspace, editorConnection) {
|
||||
function updatePorts(nodeId, workspace, generatedCode, editorConnection) {
|
||||
if (!workspace) {
|
||||
editorConnection.sendDynamicPorts(nodeId, []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatePortsImpl) {
|
||||
updatePortsImpl(nodeId, workspace, editorConnection);
|
||||
updatePortsImpl(nodeId, workspace, generatedCode, editorConnection);
|
||||
} else {
|
||||
console.warn('[Logic Builder] updatePortsImpl not initialized - running in runtime mode?');
|
||||
}
|
||||
@@ -265,17 +290,47 @@ module.exports = {
|
||||
|
||||
// Inject the real updatePorts implementation
|
||||
// This is set by the editor's initialization code
|
||||
updatePortsImpl = function (nodeId, workspace, editorConnection) {
|
||||
updatePortsImpl = function (nodeId, workspace, generatedCode, editorConnection) {
|
||||
console.log('[Logic Builder] updatePortsImpl called for node:', nodeId);
|
||||
console.log('[Logic Builder] Workspace length:', workspace ? workspace.length : 0);
|
||||
console.log('[Logic Builder] Generated code length:', generatedCode ? generatedCode.length : 0);
|
||||
|
||||
try {
|
||||
// The IODetector should be available in the editor context
|
||||
// We'll access it through the global window object (editor environment)
|
||||
if (typeof window !== 'undefined' && window.NoodlEditor && window.NoodlEditor.detectIO) {
|
||||
const detected = window.NoodlEditor.detectIO(workspace);
|
||||
console.log('[Logic Builder] Parsing generated code for outputs...');
|
||||
|
||||
const detected = {
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
signalInputs: [],
|
||||
signalOutputs: []
|
||||
};
|
||||
|
||||
// Detect outputs from code like: Outputs["result"] = ...
|
||||
const outputRegex = /Outputs\["([^"]+)"\]/g;
|
||||
let match;
|
||||
while ((match = outputRegex.exec(generatedCode)) !== null) {
|
||||
const outputName = match[1];
|
||||
if (!detected.outputs.find((o) => o.name === outputName)) {
|
||||
detected.outputs.push({ name: outputName, type: '*' });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Logic Builder] Detected outputs from code:', detected.outputs);
|
||||
|
||||
if (detected.outputs.length > 0) {
|
||||
console.log('[Logic Builder] Detection results:', {
|
||||
inputs: detected.inputs.length,
|
||||
outputs: detected.outputs.length,
|
||||
signalInputs: detected.signalInputs.length,
|
||||
signalOutputs: detected.signalOutputs.length
|
||||
});
|
||||
console.log('[Logic Builder] Detected outputs:', detected.outputs);
|
||||
|
||||
const ports = [];
|
||||
|
||||
// Add detected inputs
|
||||
detected.inputs.forEach((input) => {
|
||||
console.log('[Logic Builder] Adding input port:', input.name);
|
||||
ports.push({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
@@ -287,6 +342,7 @@ module.exports = {
|
||||
|
||||
// Add detected outputs
|
||||
detected.outputs.forEach((output) => {
|
||||
console.log('[Logic Builder] Adding output port:', output.name);
|
||||
ports.push({
|
||||
name: output.name,
|
||||
type: output.type,
|
||||
@@ -298,6 +354,7 @@ module.exports = {
|
||||
|
||||
// Add detected signal inputs
|
||||
detected.signalInputs.forEach((signalName) => {
|
||||
console.log('[Logic Builder] Adding signal input:', signalName);
|
||||
ports.push({
|
||||
name: signalName,
|
||||
type: 'signal',
|
||||
@@ -309,6 +366,7 @@ module.exports = {
|
||||
|
||||
// Add detected signal outputs
|
||||
detected.signalOutputs.forEach((signalName) => {
|
||||
console.log('[Logic Builder] Adding signal output:', signalName);
|
||||
ports.push({
|
||||
name: signalName,
|
||||
type: 'signal',
|
||||
@@ -318,23 +376,33 @@ module.exports = {
|
||||
});
|
||||
});
|
||||
|
||||
console.log('[Logic Builder] Sending', ports.length, 'ports to editor');
|
||||
editorConnection.sendDynamicPorts(nodeId, ports);
|
||||
console.log('[Logic Builder] Ports sent successfully');
|
||||
} else {
|
||||
console.warn('[Logic Builder] IODetector not available in editor context');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Logic Builder] Failed to update ports:', error);
|
||||
console.error('[Logic Builder] Error stack:', error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
graphModel.on('nodeAdded.Logic Builder', function (node) {
|
||||
console.log('[Logic Builder] Node added:', node.id);
|
||||
if (node.parameters.workspace) {
|
||||
updatePorts(node.id, node.parameters.workspace, context.editorConnection);
|
||||
console.log('[Logic Builder] Node has workspace, updating ports...');
|
||||
updatePorts(node.id, node.parameters.workspace, node.parameters.generatedCode, context.editorConnection);
|
||||
}
|
||||
|
||||
node.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'workspace') {
|
||||
updatePorts(node.id, node.parameters.workspace, context.editorConnection);
|
||||
console.log('[Logic Builder] Parameter updated:', event.name, 'for node:', node.id);
|
||||
// Trigger port update when workspace OR generatedCode changes
|
||||
if (event.name === 'workspace' || event.name === 'generatedCode') {
|
||||
console.log('[Logic Builder] Triggering port update for:', event.name);
|
||||
console.log('[Logic Builder] Workspace value:', node.parameters.workspace ? 'exists' : 'empty');
|
||||
console.log('[Logic Builder] Generated code value:', node.parameters.generatedCode ? 'exists' : 'empty');
|
||||
updatePorts(node.id, node.parameters.workspace, node.parameters.generatedCode, context.editorConnection);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||