- Default colour now checks dataset.exitError instead of tagName === 'WC-END', so success (exit 0) END blocks default to blue, error END blocks to red - _selectByCode now also sets the HTML value attribute via setAttribute(), ensuring cloneNode(true) preserves the formatted display value - Add 4 regression tests for colour defaults, attribute sync, and clone value
Visual Programming Framework
A browser-based flowchart-to-C code generator with typed measurement system, procedure libraries, and live MathJax-rendered expressions.
Architecture
The app is built as vanilla HTML/CSS/JS with no framework. All state lives in the DOM (custom elements) and is serialised to localStorage.
File dependency order (bottom to top)
index.html ← entry point, templates, page layout
│
dom.js ← $, $id, svgNamespace, STORAGE_KEY (global constants)
helpers.js ← data-source lookups, string escaping, dropdown refresh
error.js ← modal error overlay
draw.js ← SVG path routing, link interaction, path deletion
│
wcblock.js ← WCBlock base class (drag/drop, link system)
blocks-algo.js ← Algorithm tab blocks (WCStart, WCEnd, WCOutput, WCDecision,
│ WCAssign, WCExpression, WCVarRef)
blocks-data.js ← Data & Measurement tab blocks (WCQuantity, WCUnit, WCBoolean,
│ WCScalar, WCString, WCArray)
blocks-proc.js ← Procedure blocks (WCProcedure, WCProcCall)
wcprogram.js ← WCProgram + block registry + registration calls
│
persist.js ← localStorage save/load/new
app.js ← bootstraps the workspace, creates defaults, wires UI
Module overview
| File | Lines | Exports | Purpose |
|---|---|---|---|
dom.js |
23 | $, $id, svgNamespace, STORAGE_KEY |
Query shortcuts and app-wide constants |
helpers.js |
~240 | 12 functions | Data-source lookup, string escaping, dropdown refresh utilities |
error.js |
32 | showError, closeError |
Error overlay modal |
draw.js |
208 | 6 functions | SVG path routing, shadow link, link creation/deletion |
wcblock.js |
~236 | WCBlock |
Base class: drag/drop, link system, code traversal |
blocks-algo.js |
~460 | 7 classes | Algorithm tab blocks (start, end, output, decision, assign, expression, varref) |
blocks-data.js |
~570 | 6 classes | Data & Measurement tab blocks (quantity, unit, boolean, scalar, string, array) |
blocks-proc.js |
~280 | 2 classes | Procedure blocks (definition, call) |
wcprogram.js |
~370 | WCProgram + registry |
Root container, ID counter, node factory, code generation, block registration |
persist.js |
~245 | saveWorkspace, loadWorkspace, newWorkspace, helpers |
localStorage serialisation |
app.js |
~390 | 7 functions + main() |
Application bootstrap, OpenGL defaults, event wiring |
style.css |
~480 | — | All styles |
index.html |
~380 | — | Page layout + <template> definitions for all block types |
Block types
All live blocks are custom elements registered via customElements.define(). Each block type has a <template id="wc-xxx"> in index.html.
Flowchart blocks (Algorithm tab)
| Tag | Class | Purpose | Hooks |
|---|---|---|---|
wc-start |
WCStart | Program entry point | 1 logic out |
wc-end |
WCEnd | Program termination | 1 logic in |
wc-output |
WCOutput | printf() statement |
1 logic in, 1 logic out |
wc-decision |
WCDecision | if/else branch |
1 logic in, 2 logic out (yes/no), 1 data in (boolean) |
wc-varref |
WCVarRef | Read-only variable reference | 1 data out |
wc-expression |
WCExpression | Math expression with live LaTeX | N data in (dynamic vars), 1 data out |
wc-assign |
WCAssign | Variable assignment | 1 logic in, 1 logic out, 1 data in |
wc-proccall |
WCProcCall | Procedure call | 1 logic in, 1 logic out, N data in/out (dynamic params) |
Measurement blocks (Measurements tab)
| Tag | Class | Purpose |
|---|---|---|
wc-quantity |
WCQuantity | Defines a quantity type (7 SI dimensions + range + default unit) |
wc-unit |
WCUnit | Defines a unit with recursive SI factor resolution |
Variable blocks (Data tab)
| Tag | Class | Purpose | dataType |
|---|---|---|---|
wc-boolean |
WCBoolean | Boolean variable (int in C) |
"boolean" |
wc-scalar |
WCScalar | Typed scalar with unit/SI conversion | "scalar" |
wc-string |
WCString | String variable (char*/length struct) | "string" |
wc-array |
WCArray | Typed C array with comma-separated values | "array" |
Procedure blocks (Procedures tab)
| Tag | Class | Purpose |
|---|---|---|
wc-procedure |
WCProcedure | Procedure definition with typed params |
wc-proccall |
WCProcCall | Call to a defined procedure |
Container block (Algorithm tab)
| Tag | Class | Purpose |
|---|---|---|
wc-program |
WCProgram | Root container: ID counter, node factory, code generation |
Key mechanisms
Class hierarchy
HTMLElement
├── WCBlock (base: drag/drop, link system, code traversal)
│ ├── WCStart, WCEnd, WCOutput
│ ├── WCDecision
│ ├── WCQuantity, WCUnit
│ ├── WCBoolean, WCScalar, WCString, WCArray
│ ├── WCVarRef, WCExpression
│ ├── WCAssign
│ ├── WCProcedure, WCProcCall
└── WCProgram (not a WCBlock — no drag/link)
Two-click linking
- Click an output hook → starts a shadow (dashed) path that follows the cursor
- Click a compatible input hook → creates a persistent SVG path
- Click an existing path → deletes it
- Click the draw-area background → cancels the pending link
Type checking is enforced: logic hooks only connect to logic hooks, data hooks only to data hooks of the same dataType.
Grid-snapped drag
Blocks use CSS --grid-size (25px) for snap-to-grid positioning. All connection paths are redrawn in real-time during drag via drawpath().
45°-constrained path routing
Paths use routeBetween() which produces SVG polylines constrained to 0°, 45°, and 90° angles. Each path extends EXT (25px) from the source hook in its exit direction before routing.
Persistence
All workspace state is serialised to localStorage under key "workspace":
- Block positions + type-specific data
- Connection topology (by block ID + hook ID)
- ID counter for generating unique block IDs
The loadWorkspace() function follows a strict 7-step order:
- Tear down existing state
- Restore measurements (types + units)
- Restore procedures and library tabs
- Restore algorithm blocks (reposition start/end, recreate others)
- Restore data/variable blocks
- Restore SVG connections (hook resolution uses IDs or fallbacks)
- Refresh all dropdowns and SI displays
MathJax expression rendering
WCExpression uses MathJax.typesetPromise() to render LaTeX as rendered HTML. Variable hooks are rebuilt synchronously (before the async MathJax call) so that connection restoration can find them by deterministic ID ("wc-expr-N-vh-varname").
Code generation
WCProgram.generateCode() produces a complete C source file:
// --- WiseCode® ---
#include <library.h> // for referenced procedure libraries
#include <stdlib.h>
#include <string.h> // if string variables exist
int main() {
// Type definitions (typedef struct for used quantities)
// Unit definitions (comments)
// string struct definition (if needed)
// Variable declarations (booleans, scalars, strings, arrays)
// Expression declarations
// Flowchart body (start → end traversal)
// Free string allocations
return 0;
}
// --- END ---
The generateCode(visited) method on each block uses a Set to track already-emitted blocks, handling fan-out convergence (when two paths merge).
Code generation for OpenGL procedures
When the workflow includes wc-proccall blocks referencing a library like gl, glfw, or glad, the generated code includes:
#include <gl.h>
#include <glfw.h>
#include <glad.h>
The include path uses the library name directly (<gl.h>, <glfw.h>, <glad.h>). Real OpenGL headers use different paths (e.g. <GLFW/glfw3.h>). Adjust the includes in the generated output to match your build environment.
Output parameter convention: All procedure output parameters are generated with a & prefix in the C call, e.g.:
glCreateShader(GL_VERTEX_SHADER, &shader);— generated whenshaderis an output paramglGenBuffers(1, &VBO);— generated whenVBOis an output param
For OpenGL functions that natively return values (like glCreateShader which returns GLuint), the generated code uses a pointer-output convention. A thin wrapper function can adapt if needed:
SI unit system
Units form a directed acyclic graph rooted at SI base units:
- Each unit stores an SI definition string (e.g.
"0.0254"or"12 in") getSiFactor()recursively resolves the conversion factor with cycle detection- Scalars auto-convert to SI values for code generation
- The unit dropdown on scalars filters by the selected quantity type
Shared utility functions (helpers.js)
| Function | Purpose | Used by |
|---|---|---|
splitValueAndUnit() |
Parse "5 km" → {numVal, unitSuffix} |
WCScalar, WCUnit |
rebuildSelect() |
Replace <select> options preserving selection |
All refresh functions |
refreshQuantityOptions() |
Sync quantity-name dropdowns | WCQuantity, setup |
refreshUnitOptions() |
Sync unit-suffix dropdowns | WCScalar, setup |
refreshDefaultUnitOptions() |
Sync default-unit dropdowns | WCQuantity, setup |
getDefaultUnitSuffix() |
Lookup default unit for a quantity | WCScalar |
refreshVarRefOptions() |
Sync variable-select dropdowns | WCVarRef, WCAssign, setup |
refreshProcOptions() |
Sync procedure-select dropdowns | WCProcCall, setup |
switchLibTab() |
Activate a library sub-tab | UI |
findDataSource() |
Find block connected to a data hook | WCDecision, WCAssign, WCProcCall |
escapeString() |
Escape chars for C string literals | WCString, WCAssign |
refreshAll() |
Run all refresh passes in order | Various |
CSS architecture
- CSS custom properties for theming (
--grid-size,--bg,--accent, etc.) - Each block type has a distinct background/border color pair
- Hooks use absolute positioning with
data-directionanddata-exitattributes - SVG connection layer sits below blocks (
z-index: 0) - Grid background via CSS
background-imagelinear-gradient
Array data type (wc-array)
The wc-array custom element provides a typed C array declaration in the Data tab with a tabular element editor. It generates type name[] = {values};.
Tabular element editor
Elements are displayed as a table of rows inside the block:
┌───────────────────────────────────────────┐
│ [arr ] [float ▼] [] 9 elements │
├───────────────────────────────────────────┤
│ 0 [ -0.5 ] [×] │
│ 1 [ -0.5 ] [×] │
│ 2 [ 0.0 ] [×] │
│ ... │
│ 8 [ 0.0 ] [×] │
│ 9 [ ] │ ← empty row for new elements
└───────────────────────────────────────────┘
Each element row shows:
- Index badge: zero-based element number (monospace, muted colour)
- Value input: editable text field for the element's literal value
- Delete button (×): removes that element and renumbers all subsequent rows
Adding elements
The bottom row of the table is always an empty input. To add a new element:
- Type the value into the empty input
- Press Enter, or tab/blur away from the input
- The value is appended to the array and a new empty row appears
Removing elements
Click the × button on any row. The element is removed and all remaining rows are re-indexed starting from 0.
Element count label
The header row shows a live count label (e.g. "9 elements", "1 element"). This updates immediately when elements are added or removed.
Template (index.html)
<template id="wc-array">
<wc-array>
<div class="array-row">
<input class="var-name" placeholder="name" value="arr">
<select class="array-type">
<option value="float">float</option>
<option value="double">double</option>
<option value="int">int</option>
<option value="unsigned int">unsigned int</option>
<option value="char">char</option>
</select>
<span class="array-bracket">[]</span>
<span class="array-count">0 elements</span>
</div>
<div class="array-elements">
<!-- Element rows rendered dynamically by WCArray._renderElements() -->
</div>
</wc-array>
</template>
Fields:
- var-name: the C identifier for the array (default:
"arr") - array-type: the C element type selector (float, double, int, unsigned int, char)
- array-count: auto-updating label showing the element count
- array-elements: container for the dynamic element rows (populated by JS)
Class (blocks-data.js)
class WCArray extends WCBlock {
get dataType() { return "array"; }
constructor() { super(); this._values = []; }
connectedCallback() { /* binds DOM refs + renders initial state */ }
// Row rendering
_renderElements() { /* rebuilds all rows from this._values */ }
_makeElementRow(i) { /* creates one populated value row */ }
_makeNewRow() { /* creates the empty add-element row */ }
// Mutation
_addElement(val) { this._values.push(val); this._renderElements(); }
_removeElement(idx) { this._values.splice(idx, 1); this._renderElements(); }
_updateCount() { /* sets the count label text */ }
// Serialisation
saveData() → { name, type, values: [...] }
restoreData(d) → { /* handles both string (legacy) and array format */ }
// Code gen
generateOwnCode() → "type name[] = {val1, val2, ...};"
}
Key behaviours:
_valuesis an internalArrayof strings, one per element_renderElements()is called after every mutation and fromconnectedCallback(viasetTimeout) andrestoreData- Focus position is preserved across
_renderElements()calls so editing feels seamless - Event handlers use
input.closest()/delBtn.closest()to safely identify the row index from the DOM
Code generation
| Input | Generated C |
|---|---|
name="vertices", type="float", values=["-0.5","-0.5","0.0","0.5","-0.5","0.0","0.0","0.5","0.0"] |
float vertices[] = {-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0}; |
name="indices", type="unsigned int", values=["0","1","2","1","2","3"] |
unsigned int indices[] = {0, 1, 2, 1, 2, 3}; |
name="empty", type="float", values=[] |
float empty[1]; |
Values are joined with , directly (no comma-splitting needed — already individual strings in the array).
Persistence
saveData() serialises _values as a proper JSON array:
{ "name": "vertices", "type": "float", "values": ["-0.5", "-0.5", "0.0"] }
restoreData() handles both the current array format and the legacy string format (comma-separated), so old saved workspaces remain compatible.
Variable reference integration
Arrays are scanned by refreshVarRefOptions() alongside booleans, scalars, and strings. A wc-varref block can reference an array from its dropdown, setting the data hook type to "array". The referenced array name can then be typed into procedure call text inputs (e.g. glBufferData's data parameter).
No data hook
Unlike wc-boolean, wc-scalar, and wc-string, the wc-array block does not include a .hook button. Arrays are referenced by name directly in procedure call arguments, not linked via data-flow connections.
Default vertex array
When the app starts fresh (no saved workspace), createDefaultOpenGLProcedures() creates a wc-array in the Data tab with Hello Triangle vertex data:
┌──────────────────────────────────────┐
│ [vertices] [float ▼] [] 9 elements │
├──────────────────────────────────────┤
│ 0 [-0.5] [×] │
│ 1 [-0.5] [×] │
│ 2 [ 0.0] [×] │
│ 3 [ 0.5] [×] │
│ 4 [-0.5] [×] │
│ 5 [ 0.0] [×] │
│ 6 [ 0.0] [×] │
│ 7 [ 0.5] [×] │
│ 8 [ 0.0] [×] │
│ 9 [ ] │
└──────────────────────────────────────┘
Produces:
float vertices[] = {-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0};
Three vertices, each with (x, y, z) = 9 float values total.
OpenGL default procedures
On first load, createDefaultOpenGLProcedures() populates three library tabs in the Procedures pane with everything needed for a Hello Triangle pipeline.
Library: glfw (window management)
| Procedure | Inputs | Outputs | C signature |
|---|---|---|---|
glfwInit |
— | result: int |
int glfwInit(void) |
glfwCreateWindow |
width: int, height: int, title: string |
window: int |
GLFWwindow* glfwCreateWindow(int, int, const char*, ...) |
glfwMakeContextCurrent |
window: int |
— | void glfwMakeContextCurrent(GLFWwindow*) |
glfwSwapBuffers |
window: int |
— | void glfwSwapBuffers(GLFWwindow*) |
glfwPollEvents |
— | — | void glfwPollEvents(void) |
glfwWindowShouldClose |
window: int |
result: int |
int glfwWindowShouldClose(GLFWwindow*) |
glfwTerminate |
— | — | void glfwTerminate(void) |
Generated code uses int for the GLFWwindow* handle (stored as a pointer-sized integer). Cast as needed.
Library: glad (OpenGL loader)
| Procedure | Inputs | Outputs | C signature |
|---|---|---|---|
gladLoadGL |
— | result: int |
int gladLoadGL(void) |
Library: gl (OpenGL core — Hello Triangle pipeline)
| Procedure | Inputs | Outputs |
|---|---|---|
glCreateShader |
type: unsigned int |
shader: unsigned int |
glShaderSource |
shader: unsigned int, count: int, source: string |
— |
glCompileShader |
shader: unsigned int |
— |
glGetShaderiv |
shader: unsigned int, pname: unsigned int |
params: int |
glCreateProgram |
— | program: unsigned int |
glAttachShader |
program: unsigned int, shader: unsigned int |
— |
glLinkProgram |
program: unsigned int |
— |
glGetProgramiv |
program: unsigned int, pname: unsigned int |
params: int |
glGenVertexArrays |
n: unsigned int |
arrays: unsigned int |
glBindVertexArray |
array: unsigned int |
— |
glGenBuffers |
n: unsigned int |
buffers: unsigned int |
glBindBuffer |
target: unsigned int, buffer: unsigned int |
— |
glBufferData |
target: unsigned int, size: int, data: string, usage: unsigned int |
— |
glVertexAttribPointer |
index: unsigned int, size: int, type: unsigned int, normalized: int, stride: int, pointer: int |
— |
glEnableVertexAttribArray |
index: unsigned int |
— |
glUseProgram |
program: unsigned int |
— |
glDrawArrays |
mode: unsigned int, first: int, count: int |
— |
glClear |
mask: unsigned int |
— |
glClearColor |
red: float, green: float, blue: float, alpha: float |
— |
glDeleteShader |
shader: unsigned int |
— |
glDeleteProgram |
program: unsigned int |
— |
glViewport |
x: int, y: int, width: int, height: int |
— |
Using the procedures in a Hello Triangle workflow
A typical flowchart from START to END:
glfw/glfwInit→ outputresultconnected to a decision or ignoredglfw/glfwCreateWindow→ inputwidth: 800,height: 600,title: "Hello Triangle"→ outputwindowglfw/glfwMakeContextCurrent→ inputwindowfrom step 2glad/gladLoadGL→ outputresultgl/glCreateShader→ inputtype: 35633(GL_VERTEX_SHADER) → outputshadergl/glShaderSource→ inputshaderfrom step 5,count: 1,source: "..."(vertex source)gl/glCompileShader→ inputshaderfrom step 5- Repeat steps 5–7 for fragment shader (
type: 35632=GL_FRAGMENT_SHADER) gl/glCreateProgram→ outputprogramgl/glAttachShader→ inputprogram,shader(vertex)gl/glAttachShader→ inputprogram,shader(fragment)gl/glLinkProgram→ inputprogramgl/glGenVertexArrays→ inputn: 1→ outputVAOgl/glBindVertexArray→ inputarray: VAOgl/glGenBuffers→ inputn: 1→ outputVBOgl/glBindBuffer→ inputtarget: 34962(GL_ARRAY_BUFFER),buffer: VBOgl/glBufferData→ inputtarget: 34962,size: 9*4(orsizeof(vertices)),data: vertices,usage: 35044(GL_STATIC_DRAW)gl/glVertexAttribPointer→ inputindex: 0,size: 3,type: 5126(GL_FLOAT),normalized: 0,stride: 0,pointer: 0gl/glEnableVertexAttribArray→ inputindex: 0- Loop:
glClear,glUseProgram,glBindVertexArray,glDrawArrays,glfwSwapBuffers,glfwPollEvents glfw/glfwWindowShouldClose→ inputwindow→ outputresultdrives the loop decision- Cleanup:
glDeleteProgram,glDeleteShader,glfwTerminate
Generated C example (simplified)
#include <gl.h>
#include <glfw.h>
#include <glad.h>
int main() {
float vertices[] = {-0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.0, 0.5, 0.0};
int result;
int window;
unsigned int shader;
unsigned int program;
unsigned int VAO;
unsigned int VBO;
glfwInit(&result);
glfwCreateWindow(800, 600, "Hello Triangle", &window);
glfwMakeContextCurrent(window);
gladLoadGL(&result);
glCreateShader(35633, &shader);
glShaderSource(shader, 1, vertexSource);
glCompileShader(shader);
// ... fragment shader, program linking, VAO/VBO setup ...
glClear(16640);
glUseProgram(program);
glBindVertexArray(VAO);
glDrawArrays(4, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
// ... loop ...
glfwTerminate();
return 0;
}
Note
: Output parameters use
&prefix. For functions likeglCreateShaderthat natively return values, the generated code uses a pointer-output convention. Adjust the function signatures or add thin wrappers to match the real OpenGL API.
Include path mapping
| Library name | Generated include | Real include (adjust manually) |
|---|---|---|
glfw |
#include <glfw.h> |
#include <GLFW/glfw3.h> |
glad |
#include <glad.h> |
#include <glad/glad.h> |
gl |
#include <gl.h> |
Included by glad, or #include <GL/gl.h> |
The generated output uses the library name directly. Adjust the #include paths in the generated C code to match your project's OpenGL setup.
Development notes
- No build step — edit files directly, refresh browser
- Script loading order in
index.htmlmatches the dependency order above - New block types: add a
<template>, create a class extendingWCBlock, register withregisterBlock() - All functions are global (no modules/imports) — naming conventions and JSDoc serve as documentation