adrien 3657a22125 fix: WCEnd editor modal pre-loads correct colour and errno value
- 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
2026-06-23 00:52:56 +02:00

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

  1. Click an output hook → starts a shadow (dashed) path that follows the cursor
  2. Click a compatible input hook → creates a persistent SVG path
  3. Click an existing path → deletes it
  4. 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:

  1. Tear down existing state
  2. Restore measurements (types + units)
  3. Restore procedures and library tabs
  4. Restore algorithm blocks (reposition start/end, recreate others)
  5. Restore data/variable blocks
  6. Restore SVG connections (hook resolution uses IDs or fallbacks)
  7. 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 when shader is an output param
  • glGenBuffers(1, &VBO); — generated when VBO is 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-direction and data-exit attributes
  • SVG connection layer sits below blocks (z-index: 0)
  • Grid background via CSS background-image linear-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:

  1. Type the value into the empty input
  2. Press Enter, or tab/blur away from the input
  3. 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:

  • _values is an internal Array of strings, one per element
  • _renderElements() is called after every mutation and from connectedCallback (via setTimeout) and restoreData
  • 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:

  1. glfw/glfwInit → output result connected to a decision or ignored
  2. glfw/glfwCreateWindow → input width: 800, height: 600, title: "Hello Triangle" → output window
  3. glfw/glfwMakeContextCurrent → input window from step 2
  4. glad/gladLoadGL → output result
  5. gl/glCreateShader → input type: 35633 (GL_VERTEX_SHADER) → output shader
  6. gl/glShaderSource → input shader from step 5, count: 1, source: "..." (vertex source)
  7. gl/glCompileShader → input shader from step 5
  8. Repeat steps 5–7 for fragment shader (type: 35632 = GL_FRAGMENT_SHADER)
  9. gl/glCreateProgram → output program
  10. gl/glAttachShader → input program, shader (vertex)
  11. gl/glAttachShader → input program, shader (fragment)
  12. gl/glLinkProgram → input program
  13. gl/glGenVertexArrays → input n: 1 → output VAO
  14. gl/glBindVertexArray → input array: VAO
  15. gl/glGenBuffers → input n: 1 → output VBO
  16. gl/glBindBuffer → input target: 34962 (GL_ARRAY_BUFFER), buffer: VBO
  17. gl/glBufferData → input target: 34962, size: 9*4 (or sizeof(vertices)), data: vertices, usage: 35044 (GL_STATIC_DRAW)
  18. gl/glVertexAttribPointer → input index: 0, size: 3, type: 5126 (GL_FLOAT), normalized: 0, stride: 0, pointer: 0
  19. gl/glEnableVertexAttribArray → input index: 0
  20. Loop: glClear, glUseProgram, glBindVertexArray, glDrawArrays, glfwSwapBuffers, glfwPollEvents
  21. glfw/glfwWindowShouldClose → input window → output result drives the loop decision
  22. 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 like glCreateShader that 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.html matches the dependency order above
  • New block types: add a <template>, create a class extending WCBlock, register with registerBlock()
  • All functions are global (no modules/imports) — naming conventions and JSDoc serve as documentation
S
Description
Anti-Vibecoding Framework
Readme
898 KiB
Languages
JavaScript 79.9%
CSS 13.4%
HTML 6.2%
Nix 0.4%