feat: implement export/import functionality and UI enhancements for Blockly workspace
parent
09e67ccbbc
commit
c82e30b9b1
|
|
@ -234,7 +234,9 @@ amr-ros-k4/ # ROS2 Workspace root
|
|||
│ ├── breakpoints.js # Debug breakpoint management
|
||||
│ ├── bridge.js # executeAction — pywebview bridge
|
||||
│ ├── debug-engine.js # Run, debug, step, stop logic
|
||||
│ └── ui-controls.js # Button states and callbacks
|
||||
│ ├── ui-controls.js # Button states and callbacks
|
||||
│ ├── ui-tabs.js # switchTab(), refreshCodePanel()
|
||||
│ └── workspace-io.js # exportWorkspace(), importWorkspace()
|
||||
├── blocks/ # One file per block (auto-discovered)
|
||||
│ ├── manifest.js # BLOCK_FILES array
|
||||
│ ├── led_on.js
|
||||
|
|
@ -357,10 +359,13 @@ If the executor is not running, tests are **skipped** with an informative messag
|
|||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| [`_wait_for_future(future, timeout_sec)`](src/blockly_app/blockly_app/app.py:26) | Polls `future.done()` without calling `rclpy.spin()`. Used because the node is already being spun by a background thread. |
|
||||
| [`BlocklyAPI`](src/blockly_app/blockly_app/app.py:46) | Python class exposed to JavaScript via pywebview. Its methods are callable as `window.pywebview.api.<method>()`. |
|
||||
| [`BlocklyAPI.execute_action()`](src/blockly_app/blockly_app/app.py:61) | Sends a ROS2 Action Goal and blocks until the result arrives. Returns `{success: bool, message: str}` to JavaScript. |
|
||||
| [`main()`](src/blockly_app/blockly_app/app.py:145) | Initializes rclpy, creates the Action Client, starts the background spin thread with `MultiThreadedExecutor`, creates the pywebview window, and handles cleanup on exit. |
|
||||
| [`_native_save_dialog()` / `_native_open_dialog()`](src/blockly_app/blockly_app/app.py:29) | Opens native OS file dialogs via `tkinter.filedialog`. Uses Tcl/Tk interpreter separate from Qt — no thread conflict. Available in pixi environment without extra dependencies. |
|
||||
| [`_wait_for_future(future, timeout_sec)`](src/blockly_app/blockly_app/app.py:63) | Polls `future.done()` without calling `rclpy.spin()`. Used because the node is already being spun by a background thread. |
|
||||
| [`BlocklyAPI`](src/blockly_app/blockly_app/app.py:80) | Python class exposed to JavaScript via pywebview. Its methods are callable as `window.pywebview.api.<method>()`. |
|
||||
| [`BlocklyAPI.execute_action()`](src/blockly_app/blockly_app/app.py:95) | Sends a ROS2 Action Goal and blocks until the result arrives. Returns `{success: bool, message: str}` to JavaScript. |
|
||||
| [`BlocklyAPI.save_workspace(json_string)`](src/blockly_app/blockly_app/app.py:145) | Opens native "Save As" dialog via tkinter, writes workspace JSON to chosen file. Returns `{success, path}` directly to JS. |
|
||||
| [`BlocklyAPI.load_workspace()`](src/blockly_app/blockly_app/app.py:168) | Opens native "Open" dialog via tkinter, reads and validates JSON, returns `{success, data, path}` to JS. |
|
||||
| [`main()`](src/blockly_app/blockly_app/app.py:190) | Initializes rclpy, creates the Action Client, starts the background spin thread with `MultiThreadedExecutor`, creates the pywebview window, and handles cleanup on exit. |
|
||||
|
||||
**Threading design:** The `main()` function starts `MultiThreadedExecutor.spin()` in a daemon thread. When JavaScript calls `execute_action()`, the method uses `_wait_for_future()` to poll for completion — it never calls `rclpy.spin_until_future_complete()`, which would conflict with the background spin.
|
||||
|
||||
|
|
@ -376,7 +381,7 @@ If the executor is not running, tests are **skipped** with an informative messag
|
|||
|
||||
**Script loading order** (fixed — never needs changing when adding blocks):
|
||||
1. `vendor/blockly.min.js` + other Blockly libs
|
||||
2. `blockly/core/registry.js` → `breakpoints.js` → `bridge.js` → `debug-engine.js` → `ui-controls.js`
|
||||
2. `blockly/core/registry.js` → `breakpoints.js` → `bridge.js` → `debug-engine.js` → `ui-controls.js` → `ui-tabs.js` → `workspace-io.js`
|
||||
3. `blockly/blocks/manifest.js` + `blockly/loader.js`
|
||||
4. `blockly/workspace-init.js`
|
||||
5. Inline script: `loadAllBlocks().then(() => initWorkspace())`
|
||||
|
|
@ -391,6 +396,28 @@ If the executor is not running, tests are **skipped** with an informative messag
|
|||
| `BlockRegistry.getBlocks()` | Get all registered blocks |
|
||||
| `BlockRegistry.getToolboxJSON()` | Build Blockly toolbox JSON from registered blocks + built-in categories |
|
||||
|
||||
#### [`blockly_app/ui/blockly/core/ui-tabs.js`](src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js) — Tab Switching
|
||||
|
||||
**Purpose:** Manages switching between the **Blocks** editor tab and the **Code** preview tab.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `switchTab(tab)` | Shows/hides `#blockly-area` and `#code-panel`. Calls `Blockly.svgResize()` when returning to Blocks tab (canvas dimensions are zeroed while hidden). |
|
||||
| `refreshCodePanel()` | Regenerates JS code from workspace via `javascript.javascriptGenerator.workspaceToCode()` and displays in `#code-output`. |
|
||||
|
||||
The Code tab updates automatically on every workspace change (via a change listener in `workspace-init.js`).
|
||||
|
||||
#### [`blockly_app/ui/blockly/core/workspace-io.js`](src/blockly_app/blockly_app/ui/blockly/core/workspace-io.js) — Workspace Export/Import
|
||||
|
||||
**Purpose:** Exports workspace to JSON (Save As dialog) and imports from JSON (Open dialog) via the Python bridge.
|
||||
|
||||
| Function | Description |
|
||||
|---|---|
|
||||
| `exportWorkspace()` | Serializes workspace with `Blockly.serialization.workspaces.save()`, calls `window.pywebview.api.save_workspace(json)`, logs result path. |
|
||||
| `importWorkspace()` | Calls `window.pywebview.api.load_workspace()`, clears workspace, loads returned JSON with `Blockly.serialization.workspaces.load()`. |
|
||||
|
||||
Both functions use `async/await` — they return after the file dialog closes and the file has been read/written.
|
||||
|
||||
#### [`blockly_app/ui/blockly/core/bridge.js`](src/blockly_app/blockly_app/ui/blockly/core/bridge.js) — pywebview Bridge
|
||||
|
||||
**Purpose:** Provides `executeAction(command, params)` which calls Python via the pywebview JS-to-Python bridge. Falls back to a mock when running outside pywebview (browser dev).
|
||||
|
|
@ -1164,6 +1191,17 @@ pixi run executor
|
|||
pixi run test
|
||||
```
|
||||
|
||||
### Export/Import button has no effect (force close or nothing happens)
|
||||
|
||||
**Symptom:** Clicking Export or Import either force-closes the app or does nothing.
|
||||
|
||||
**Cause:** Qt file dialogs (`QFileDialog`, `pywebview.create_file_dialog`) must run on the Qt main thread. pywebview calls Python API methods from a background thread. Attempting to open a Qt dialog from there causes:
|
||||
- `pywebview.create_file_dialog` → deadlock via `BlockingQueuedConnection` → force close
|
||||
- `QFileDialog` via `QTimer.singleShot` → no effect, because non-QThread background threads have no Qt event loop
|
||||
|
||||
**Solution:** Use `tkinter.filedialog` — tkinter uses its own Tcl/Tk interpreter, completely separate from Qt. `filedialog.asksaveasfilename()` blocks the calling background thread until the user responds. Already available in the pixi environment (no extra dependency needed).
|
||||
See [`_native_save_dialog()`](src/blockly_app/blockly_app/app.py:29) in `app.py`.
|
||||
|
||||
### pywebview shows "GTK cannot be loaded"
|
||||
|
||||
**Symptom:** Warning about `ModuleNotFoundError: No module named 'gi'` followed by "Using Qt 5.15".
|
||||
|
|
|
|||
28
readme.md
28
readme.md
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
> **Project**: Blockly ROS2 Robot Controller (Kiwi Wheel AMR)
|
||||
> **ROS2 Distro**: Jazzy
|
||||
> **Last Updated**: 2026-03-08
|
||||
> **Last Updated**: 2026-03-09
|
||||
> **Current Focus**: `kiwi_controller` — Adaptive control for Kiwi Wheel drive
|
||||
|
||||
Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
|
||||
|
||||
|
|
@ -11,13 +12,12 @@ Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
|
|||
bab pada dokumen merepresentasikan alur rencana pengembangan.
|
||||
Potential Enhancements : Feasibility Study
|
||||
Planned Feature : Backlog
|
||||
Feature Task : Penjabaran Pekerjaan yang ready untuk dikerjakan
|
||||
Feature Task : Penjabaran Pekerjaan yang ready untuk dikerjakan. Task harus dijelaskan apa yang akan dikerjakan dan terdapat definition of done nya
|
||||
|
||||
---
|
||||
|
||||
# Potential Enhancements
|
||||
this list is short by priority
|
||||
- **Enhance UI**: add tab to get generate version of block blocky, export/import from/to block blocky generated file, fixing visibelity text color in left colom view (curently it greyed with white text).
|
||||
- **Launch files**: `blockly_bringup` package with ROS2 launch files to start all nodes with one command
|
||||
- **Sensor integration**: Subscriber nodes for sensor data feeding back into Blockly visual feedback
|
||||
- **RealHardware implementation**: Fill in ROS2 publishers/service clients for actual Pi hardware nodes (topics TBD)
|
||||
|
|
@ -33,8 +33,26 @@ this list is short by priority
|
|||
| `blockly_executor` | Action Server — command handler registry | ✅ Done |
|
||||
| `blockly_interfaces` | Custom ROS2 action definitions | ✅ Done |
|
||||
| `kiwi_controller` | Adaptive control for Kiwi Wheel drive | 📋 Planned |
|
||||
| **Enhance UI** | Tab Code preview, Export/Import, dark toolbox | ✅ Done |
|
||||
|
||||
|
||||
# Future Tasks
|
||||
# Feature Task
|
||||
|
||||
<!-- Add new phases / features here -->
|
||||
## Enhance UI
|
||||
|
||||
| # | Task | Status |
|
||||
|---|------|--------|
|
||||
| 1 | Fix toolbox text color (dark theme via `Blockly.Theme.defineTheme`) | ✅ Done |
|
||||
| 2 | Add "Code" tab — realtime generated JS preview | ✅ Done |
|
||||
| 3 | Export/Import workspace (.json) via toolbar buttons | ✅ Done |
|
||||
|
||||
**Files changed:**
|
||||
- `src/blockly_app/blockly_app/ui/blockly/workspace-init.js` — dark theme definition + code panel change listener
|
||||
- `src/blockly_app/blockly_app/ui/blockly/core/ui-tabs.js` *(new)* — `switchTab()`, `refreshCodePanel()`
|
||||
- `src/blockly_app/blockly_app/ui/blockly/core/workspace-io.js` *(new)* — `exportWorkspace()`, `importWorkspace()` via `await window.pywebview.api`
|
||||
- `src/blockly_app/blockly_app/ui/index.html` — CSS, HTML tab bar, code panel, toolbar buttons
|
||||
- `src/blockly_app/blockly_app/app.py` — `save_workspace()`, `load_workspace()` via zenity subprocess
|
||||
|
||||
**Note — Export/Import implementation:**
|
||||
`pywebview.create_file_dialog` dan `QFileDialog` via `QTimer.singleShot` keduanya gagal dari background thread (deadlock / no event loop).
|
||||
Solusi final: `tkinter.filedialog` — interpreter Tcl/Tk terpisah dari Qt, tidak ada konflik thread, sudah tersedia di pixi environment tanpa dependency tambahan.
|
||||
|
|
|
|||
|
|
@ -6,12 +6,16 @@ in a background thread. The BlocklyAPI class exposes execute_action()
|
|||
to JavaScript, bridging Blockly's runtime with the ROS2 Executor Node.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
|
||||
import tkinter as _tk
|
||||
from tkinter import filedialog as _filedialog
|
||||
|
||||
import webview
|
||||
import rclpy
|
||||
from rclpy.action import ActionClient
|
||||
|
|
@ -23,6 +27,43 @@ from blockly_interfaces.action import BlocklyAction
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _native_save_dialog(suggested: str = "workspace.json") -> str:
|
||||
"""
|
||||
Open a native "Save As" file dialog using tkinter.
|
||||
|
||||
tkinter uses its own Tcl/Tk interpreter — no Qt thread conflict.
|
||||
filedialog.asksaveasfilename() blocks the calling thread until the
|
||||
user picks a file or cancels. Returns "" on cancel.
|
||||
"""
|
||||
root = _tk.Tk()
|
||||
root.withdraw()
|
||||
root.wm_attributes("-topmost", True)
|
||||
path = _filedialog.asksaveasfilename(
|
||||
title="Save Workspace",
|
||||
defaultextension=".json",
|
||||
initialfile=suggested,
|
||||
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
|
||||
)
|
||||
root.destroy()
|
||||
return path or ""
|
||||
|
||||
|
||||
def _native_open_dialog() -> str:
|
||||
"""
|
||||
Open a native "Open" file dialog using tkinter.
|
||||
Returns "" on cancel.
|
||||
"""
|
||||
root = _tk.Tk()
|
||||
root.withdraw()
|
||||
root.wm_attributes("-topmost", True)
|
||||
path = _filedialog.askopenfilename(
|
||||
title="Load Workspace",
|
||||
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
|
||||
)
|
||||
root.destroy()
|
||||
return path or ""
|
||||
|
||||
|
||||
def _wait_for_future(future, timeout_sec: float = 30.0):
|
||||
"""
|
||||
Wait for an rclpy Future to complete without calling spin.
|
||||
|
|
@ -141,6 +182,50 @@ class BlocklyAPI:
|
|||
"message": f"Error: {str(e)}",
|
||||
}
|
||||
|
||||
def save_workspace(self, json_string: str) -> dict:
|
||||
"""
|
||||
Open a native OS "Save As" dialog via zenity/kdialog subprocess and
|
||||
write workspace JSON to the chosen file.
|
||||
|
||||
Called from JS: await window.pywebview.api.save_workspace(jsonString)
|
||||
|
||||
Using subprocess avoids all Qt thread constraints — zenity/kdialog run
|
||||
in their own process, blocking the pywebview bridge thread until the
|
||||
user confirms or cancels.
|
||||
"""
|
||||
try:
|
||||
path = _native_save_dialog("workspace.json")
|
||||
if not path:
|
||||
return {"success": False, "error": "Cancelled"}
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(json_string)
|
||||
logger.info(f"Workspace saved to: {path}")
|
||||
return {"success": True, "path": path}
|
||||
except Exception as e:
|
||||
logger.error(f"save_workspace error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def load_workspace(self) -> dict:
|
||||
"""
|
||||
Open a native OS "Open" dialog via zenity/kdialog and read the file.
|
||||
|
||||
Called from JS: await window.pywebview.api.load_workspace()
|
||||
"""
|
||||
try:
|
||||
path = _native_open_dialog()
|
||||
if not path:
|
||||
return {"success": False, "error": "Cancelled"}
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
json.loads(data) # validate before sending to JS
|
||||
logger.info(f"Workspace loaded from: {path}")
|
||||
return {"success": True, "data": data, "path": path}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": False, "error": "File is not valid JSON"}
|
||||
except Exception as e:
|
||||
logger.error(f"load_workspace error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
"""Application entry point."""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* UI Tabs — Tab switching between Blocks editor and Code preview.
|
||||
*
|
||||
* Depends on: workspace (global, index.html), refreshCodePanel (this file),
|
||||
* Blockly (vendor), javascript (vendor)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Switch between "blocks" and "code" tabs.
|
||||
* Calls Blockly.svgResize when returning to blocks tab (canvas was hidden).
|
||||
*
|
||||
* @param {string} tab - "blocks" or "code"
|
||||
*/
|
||||
function switchTab(tab) {
|
||||
const blocksArea = document.getElementById('blockly-area');
|
||||
const codePanel = document.getElementById('code-panel');
|
||||
const tabBlocks = document.getElementById('tab-blocks');
|
||||
const tabCode = document.getElementById('tab-code');
|
||||
|
||||
if (tab === 'blocks') {
|
||||
blocksArea.style.display = 'block';
|
||||
codePanel.style.display = 'none';
|
||||
tabBlocks.classList.add('active');
|
||||
tabCode.classList.remove('active');
|
||||
// Restore Blockly canvas dimensions — they're zeroed while hidden
|
||||
if (typeof workspace !== 'undefined' && workspace) {
|
||||
Blockly.svgResize(workspace);
|
||||
}
|
||||
} else {
|
||||
blocksArea.style.display = 'none';
|
||||
codePanel.style.display = 'block';
|
||||
tabBlocks.classList.remove('active');
|
||||
tabCode.classList.add('active');
|
||||
refreshCodePanel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate JS code from workspace and display in code panel.
|
||||
*/
|
||||
function refreshCodePanel() {
|
||||
if (typeof workspace === 'undefined' || !workspace) return;
|
||||
const code = javascript.javascriptGenerator.workspaceToCode(workspace);
|
||||
document.getElementById('code-output').textContent = code || '// (no blocks)';
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Workspace I/O — Export and import workspace as JSON via native OS dialogs.
|
||||
*
|
||||
* Calls Python save_workspace() / load_workspace() via pywebview bridge.
|
||||
* Both methods are synchronous on the Python side (blocking file dialog),
|
||||
* and return their result directly — no evaluate_js callback needed.
|
||||
*
|
||||
* Depends on: workspace (global, index.html), consoleLog (index.html),
|
||||
* Blockly (vendor), refreshCodePanel (ui-tabs.js)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialize workspace and open native OS "Save As" dialog.
|
||||
*/
|
||||
async function exportWorkspace() {
|
||||
if (typeof workspace === 'undefined' || !workspace) {
|
||||
consoleLog('No workspace to export', 'error');
|
||||
return;
|
||||
}
|
||||
const state = Blockly.serialization.workspaces.save(workspace);
|
||||
const json = JSON.stringify(state, null, 2);
|
||||
try {
|
||||
const res = await window.pywebview.api.save_workspace(json);
|
||||
if (res.success) {
|
||||
consoleLog('Workspace saved to: ' + res.path, 'success');
|
||||
} else if (res.error !== 'Cancelled') {
|
||||
consoleLog('Export failed: ' + res.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
consoleLog('Export error: ' + err, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open native OS file picker and load selected JSON into workspace.
|
||||
*/
|
||||
async function importWorkspace() {
|
||||
try {
|
||||
const res = await window.pywebview.api.load_workspace();
|
||||
if (!res.success) {
|
||||
if (res.error !== 'Cancelled') {
|
||||
consoleLog('Import failed: ' + res.error, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const state = JSON.parse(res.data);
|
||||
workspace.clear();
|
||||
Blockly.serialization.workspaces.load(state, workspace);
|
||||
consoleLog('Workspace loaded from: ' + res.path, 'success');
|
||||
const codePanel = document.getElementById('code-panel');
|
||||
if (codePanel && codePanel.style.display !== 'none') {
|
||||
refreshCodePanel();
|
||||
}
|
||||
} catch (err) {
|
||||
consoleLog('Import error: ' + err, 'error');
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,17 @@ function initWorkspace() {
|
|||
// Build toolbox from registry
|
||||
const toolbox = BlockRegistry.getToolboxJSON();
|
||||
|
||||
// Dark theme — fixes toolbox text visibility against dark UI background
|
||||
const DarkTheme = Blockly.Theme.defineTheme('dark-ros2', {
|
||||
base: Blockly.Themes.Classic,
|
||||
componentStyles: {
|
||||
toolboxBackgroundColour: '#252526',
|
||||
toolboxForegroundColour: '#cccccc',
|
||||
flyoutBackgroundColour: '#2d2d2d',
|
||||
flyoutOpacity: 0.95,
|
||||
},
|
||||
});
|
||||
|
||||
// Inject Blockly workspace
|
||||
const ws = Blockly.inject('blockly-div', {
|
||||
toolbox: toolbox,
|
||||
|
|
@ -38,7 +49,7 @@ function initWorkspace() {
|
|||
scaleSpeed: 1.2,
|
||||
},
|
||||
trashcan: true,
|
||||
theme: Blockly.Themes.Classic,
|
||||
theme: DarkTheme,
|
||||
});
|
||||
|
||||
// Set up breakpoint click listener
|
||||
|
|
@ -51,6 +62,14 @@ function initWorkspace() {
|
|||
}
|
||||
});
|
||||
|
||||
// Refresh Code tab when blocks change
|
||||
ws.addChangeListener(function () {
|
||||
const codePanel = document.getElementById('code-panel');
|
||||
if (codePanel && codePanel.style.display !== 'none') {
|
||||
refreshCodePanel();
|
||||
}
|
||||
});
|
||||
|
||||
// Resize handler
|
||||
function onResize() {
|
||||
const area = document.getElementById('blockly-area');
|
||||
|
|
|
|||
|
|
@ -85,12 +85,77 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
#workspace-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tab-bar {
|
||||
display: flex;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #404040;
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
padding: 0 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #d4d4d4;
|
||||
border-bottom-color: #4caf50;
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
color: #bbb;
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
#blockly-area {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#code-panel {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#code-output {
|
||||
margin: 0;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#btn-export {
|
||||
background: #607d8b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#btn-import {
|
||||
background: #455a64;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* SVG flyout label text — not controlled by Blockly theme componentStyles */
|
||||
.blocklyFlyoutLabelText { fill: #cccccc !important; }
|
||||
|
||||
#blockly-div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -129,15 +194,27 @@
|
|||
<button id="btn-step-into" onclick="stepInto()" disabled>Step Into</button>
|
||||
<div class="separator"></div>
|
||||
<button id="btn-stop" onclick="stopExecution()" disabled>Stop</button>
|
||||
<div class="separator"></div>
|
||||
<button id="btn-export" onclick="exportWorkspace()">Export</button>
|
||||
<button id="btn-import" onclick="importWorkspace()">Import</button>
|
||||
<div id="debug-toggle">
|
||||
<input type="checkbox" id="chk-debug" onchange="onDebugToggle(this.checked)">
|
||||
<label for="chk-debug">Debug Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blockly Workspace -->
|
||||
<div id="blockly-area">
|
||||
<div id="blockly-div"></div>
|
||||
<!-- Blockly Workspace + Code Tab -->
|
||||
<div id="workspace-area">
|
||||
<div id="tab-bar">
|
||||
<button class="tab-btn active" id="tab-blocks" onclick="switchTab('blocks')">Blocks</button>
|
||||
<button class="tab-btn" id="tab-code" onclick="switchTab('code')">Code</button>
|
||||
</div>
|
||||
<div id="blockly-area">
|
||||
<div id="blockly-div"></div>
|
||||
</div>
|
||||
<div id="code-panel" style="display:none">
|
||||
<pre id="code-output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console Output -->
|
||||
|
|
@ -169,6 +246,8 @@
|
|||
<script src="blockly/core/bridge.js"></script>
|
||||
<script src="blockly/core/debug-engine.js"></script>
|
||||
<script src="blockly/core/ui-controls.js"></script>
|
||||
<script src="blockly/core/ui-tabs.js"></script>
|
||||
<script src="blockly/core/workspace-io.js"></script>
|
||||
|
||||
<!-- Block manifest and loader -->
|
||||
<script src="blockly/blocks/manifest.js"></script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"blocks": {
|
||||
"languageVersion": 0,
|
||||
"blocks": [
|
||||
{
|
||||
"type": "controls_if",
|
||||
"id": "WLmpkq!^,rcoKGH+bQgN",
|
||||
"x": 210,
|
||||
"y": 70,
|
||||
"inputs": {
|
||||
"DO0": {
|
||||
"block": {
|
||||
"type": "led_on",
|
||||
"id": "._%j-2DZ*BZJlyW:1#`Q",
|
||||
"fields": {
|
||||
"PIN": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue