- Modular block files: split into blocks-algo, blocks-data, blocks-proc - New modules: sections.js (labelled canvas areas), proc-browser.js (procedure tree modal), lib-loader.js (lazy library loading), templates.js (project templates), errno-data.js (errno table) - Test infrastructure: browser test suite (28 tests) and Node pure-function tests (75 tests) - Warning system: unified .block-warn for disconnected inputs, unconnected error hooks, and zero-exit-in-error-path - Error path highlighting: red flow paths and block borders for error-only subgraphs - Input hooks: hidden by default, shown during linking; outputs always at bottom - Block editor modal: pencil icon, colour picker for accent, compact field labels - Doc generation: standalone node script extracts JSDoc -> docs/API.md (124 declarations) - Persistence: save/restore sections, errno selection, block colours - Fixes: _segmentsIntersect bug, WCEnd crash from closest(this), drawpath guard on detached elements
17 KiB
Architecture
Stack
Pure vanilla HTML/CSS/JS — no frameworks, no build tools. MathJax v3 for LaTeX rendering is the only runtime dependency (loaded before app scripts). Eight JS modules loaded in dependency order at the end of <body>.
Load Order
index.html
└─ style.css
└─ lib/mathjax/tex-mml-chtml.js → MathJax v3 (LaTeX→HTML rendering)
└─ dom.js → $, $id, svgNamespace, STORAGE_KEY
└─ helpers.js → splitValueAndUnit, rebuildSelect, refresh* functions
└─ error.js → showError, closeError
└─ draw.js → routeBetween, drawpath, shadow link system
└─ wcblock.js → WCBlock base (drag/drop, link system, code traversal)
└─ 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 → saveWorkspace, loadWorkspace, newWorkspace
└─ app.js → entry point, setupUI, main()
File Roles
dom.js — Global constants & DOM shortcuts
Provides app-wide constants that every module depends on:
$—document.querySelector.bind(document)— concise single-element selector$id—document.getElementById.bind(document)— concise ID lookupsvgNamespace—"http://www.w3.org/2000/svg"— for SVG element creationSTORAGE_KEY—"workspace"— localStorage key for persistence
helpers.js — Utility functions & dropdown refresh
Pure helper functions (no module dependencies beyond DOM):
splitValueAndUnit(str, defaultNum)— parses"5 km"→{numVal: 5, unitSuffix: "km"}rebuildSelect(sel, placeholder, values, current)— replaces<select>options preserving selectionrefreshQuantityOptions()— syncs all quantity-name dropdowns fromwc-quantityblocksrefreshUnitOptions()— syncs unit-suffix dropdowns per datum's selected quantityrefreshDefaultUnitOptions()— syncs default-unit dropdowns per quantity's matching unitsgetDefaultUnitSuffix(quantityName)— looks up a quantity's default unitrefreshAll()— runs all refresh passes in dependency orderrefreshVarRefOptions()— syncs varref dropdowns with Data-tab variables
error.js — Error modal overlay
A self-contained error modal:
showError(msg)— shows the-ACK!overlay with a messagecloseError(ev)— dismisses the overlay (click backdrop, X button, or programmatic)
draw.js — SVG connection drawing & shadow link
Manages the two-click link system and polyline SVG paths:
EXT(25px) — distance a path extends from a hook in its exit directionlinkageFirst— tracks the first-clicked output hook during link creationshadowPath— the dashed SVG path following the cursor during linkingrouteBetween(x1, y1, x2, y2)— 45°-constrained polyline routingresolveHook(path, which)— resolves from/to hook references on a pathgetHookExit(hook, svgRect)— computes the extended control point for a hookdrawpath(path)— draws/redraws an SVG connection path between two hooksstartShadowLink(hook)— begins showing a dashed shadow path from a hook to cursoronShadowMove(ev)— updates shadow path endpoint on mousemoveendShadowLink()— removes shadow path and stops trackingresetLinkage()— cancels pending link and removes shadowdeleteLink(path)— removes a connection path and cleans up block flow arrays
Block files — Custom element classes
Five files define the custom element hierarchy (loaded in order):
wcblock.js — WCBlock base class:
- Drag-and-drop with grid snapping
- Two-click hook link system
- Flow tracking (wcForeFlow, wcNextFlow)
- Code generation traversal (generateCode, generateNextCode, generateOwnCode)
- Selection, deletion, resize handle
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)
Collectively they define two class hierarchies:
WCBlock (extends HTMLElement) — base for all draggable/linkable blocks:
- Sets
data-blockattribute in constructor (used as CSS selector hook instead of listing all tag names) - Grid-snapped drag-and-drop (
startDragging,dragAround,stopDragging; uses arrow functions for bound handlers) - Two-click hook link system (
connectedCallback→_setupHook(hook)per hook button) - Flow tracking (
wcForeFlow,wcNextFlowarrays of SVG path elements) - Code generation (
generateCode(),generateNextCode()) - Validation via
_require(selector)— throws a descriptiveErrorif a required child element is missing from the template, failing early instead of producingCannot read properties of null - Selection on click (
.selectedclass, blue box-shadow ring) - Deletion via X button (top-right, shown on hover) or Delete key
- X button omitted for
WCStart/WCEnd; theirdelete()shows an error
| Subclass | Purpose | Hooks | Code Output |
|---|---|---|---|
WCStart |
Entry point | 1 output | "" |
WCEnd |
Program exit | 1 input | "" |
WCOutput |
Print statement | 1 input, 1 output | printf("..."); |
WCDecision |
Conditional branch | 1 input, 2 output (yes/no), 1 data in (bool) | if(...) {...} else {...} |
WCAssign |
Variable assignment | 1 input, 1 output, 1 data in (scalar), optional data in (index) | name = value; |
WCQuantity |
SI quantity definition | none (inert hook) | typedef comment |
WCUnit |
SI unit definition | none (inert hook) | unit comment |
WCBoolean |
Boolean variable | none (inert hook) | int name = 0/1; |
WCScalar |
Scalar variable with quantity + unit | none (inert hook) | type name = value; |
WCVector |
N-dimensional vector with quantity + unit | none (inert hook) | type name[N] = {v0, v1, ...}; |
WCString |
String variable (char* + length) | none (inert hook) | string name = {"text", len}; |
WCArray |
Typed C array (supports vector stride) | none | type name[size]; |
WCVarRef |
Read-only variable reference in algorithm view | 1 data out | "" (visual only) |
WCExpression |
MathJax-rendered expression | dynamic data-in (per variable), 1 data out | double name = formula; |
WCProcedure |
Procedure definition with typed params | none (inert hook) | "" (comments only) |
WCProcCall |
Procedure call | 1 input, 1 output, dynamic data in/out | proc(args); |
WCProgram (extends HTMLElement directly) — root container:
build()— creates mandatory START/END blocksmakeNode(tag)— deep-clones a template by registry lookup, assigns unique IDnewNode(tag, parent)— creates a node and appends to the given parent (or registry default)- Convenience methods:
newDecisionNode,newExpressionNode,newVarRefNode,newOutputNode,newProcCallNode,newUnitNode,newQuantityNode,newBooleanNode,newScalarNode,newVectorNode,newStringNode,newArrayNode,newProcedureNode generateCode()— traverses all blocks and produces complete C source
persist.js — Workspace persistence (localStorage)
Serialises and deserialises the full workspace:
saveBlockData(block)— reads position + type-specific data from a blocksaveWorkspace()— serialises all blocks + connections to localStoragerestoreBlock(info)— deep-clones a template and restores data onto itloadWorkspace()— deserialises and reconstructs the workspacenewWorkspace()— clears localStorage and reloads the page
app.js — Entry point & orchestration
Wires everything together:
createDefaultMeasurements()— pre-populates 22 SI quantities and 22 SI unitsswitchTab(name)— activates a tab by namesetupUI()— registers all event listeners (delegation pattern, no inline onclick)main()— bootstrap: builds start/end, creates defaults, loads workspace, sets up UI
Data Flow
Link System (Two-Click)
User clicks output hook
→ linkageFirst = { block, hook }
→ startShadowLink(hook) — dashed path follows cursor
User clicks input hook (different block)
→ checks link-type match (logic↔logic, data↔data)
→ checks data-type match for data links
→ creates SVG <path class="flow" or "flow-data">
→ pushes to wcForeFlow / wcNextFlow arrays
→ calls drawpath(path)
→ calls resetLinkage()
User clicks empty space or same block
→ resetLinkage() cancels pending link
Drag-and-Drop
User mousedown on block
→ startDragging records initial position
→ sets document.onmousemove = this._dragBound
→ sets document.onmouseup = this._stopBound
Mouse moves
→ dragAround snaps position to --grid-size increments
→ redraws all wcNextFlow + wcForeFlow paths
Mouse up
→ stopDragging clears document handlers
Code Generation
WCProgram.generateCode()
→ Gathers all wc-boolean + wc-scalar blocks → usedQuantities, usedUnits sets
→ Filters wc-quantity blocks to only used quantities → typedefs
→ Filters wc-unit blocks to only used units → unit comments
→ Generates variable declarations from datums
→ Walks flow graph from START via generateNextCode()
→ Writes to #c-code textarea
Decision blocks handle convergence: BFS per branch + intersection to detect shared blocks. Branch-exclusive code in if/else, shared code after.
Persistence
Save: saveWorkspace() → JSON → localStorage["workspace"]
Load: loadWorkspace() → localStorage["workspace"] → JSON → DOM reconstruction
New: newWorkspace() → confirm() → localStorage.removeItem → location.reload()
Key Patterns
Event Delegation (setupUI in app.js)
All toolbar and tab-bar events use delegated listeners on static parent elements, matching e.target.closest("button"). This avoids per-button listeners.
Custom Element Lifecycle
Each WCBlock subclass calls super.connectedCallback() first, which installs hook click handlers (guarded by _hooksSetup flag). Subclasses then cache their child DOM elements in this._el.* to avoid repeated querySelector calls.
SVG Path Routing
Paths are constrained to {0°, 45°, 90°} angles using routeBetween(). Each path segment:
- Extends
EXT(25px) from source hook in itsdata-exitdirection - Routes with 45°-constrained polyline to target hook's exit point
- Approaches target hook from its
data-exitdirection
Hook Visual System
- Logic output hooks (
data-direction="out",data-link-type="logic"): solid blue circles, scale on hover - Logic input hooks (
data-direction="in",data-link-type="logic"): dashed gray outlines, fill blue on hover - Data output hooks (
data-direction="out",data-link-type="data"): solid green circles, scale on hover - Data input hooks (
data-direction="in",data-link-type="data"): light green dashed outlines, fill green on hover - Side hooks (left/right): force horizontal path exits
- Top/bottom hooks: force vertical path exits
- Inert hooks (no
data-direction): 20% opacity, non-interactive
Link Deletion
Clicking an SVG connection path (class flow) deletes it:
- Removes path from
fromElement.wcNextFlowandtoElement.wcForeFlow - Removes from DOM; nulls out custom properties
- Delegated click handler on
#connectionsin app.js
Tab System
| Tab | Pane ID | Contains | Toolbar Buttons |
|---|---|---|---|
| Algorithm | pane-algorithm |
Flowchart blocks (start, end, output, decision, expression, varref) | new decision, new expression, new variable, new output |
| Data | pane-data |
Boolean/scalar/string variable blocks | new variable |
| Measurements | pane-measurements |
Quantity definitions + unit definitions | new quantity, new unit |
MathJax Integration (WCExpression)
WCExpression uses MathJax v3 (tex-mml-chtml.js) for client-side LaTeX rendering:
- Config: inlineMath (
$...$), displayMath ($$...$$),enableMenu: false,elements: [](manual typeset only) - Rendering:
MathJax.tex2chtmlPromise(latex)returns an HTML DOM node appended to.expr-rendered - Variable parsing: LaTeX source is stripped of
\commands, braces, operators, and numbers; remaining tokens are deduplicated and filtered against known function/Greek names to produce a list of variable names - Dynamic hooks: For each parsed variable, a
<button class="hook">is created in.expr-varhookswithdata-link-type="data",data-type="scalar", and wired through_setupHook() - Fallback: If MathJax is unavailable,
.expr-renderedshows the raw LaTeX text - Persistence: Only the LaTeX string is saved/restored; variable hooks are regenerated from it on load
Data Type System (WCBoolean / WCScalar / WCVector / WCString)
Variable blocks come in four distinct types, not interchangeable:
- WCBoolean (
wc-boolean): A boolean variable. Has a name and a true/false value selector. No unit, no SI conversion, no quantity dropdown. Generatesint name = 0/1;. - WCScalar (
wc-scalar): A typed scalar variable. Has a name, numeric value, unit dropdown, and quantity selector with SI conversion display. GeneratesquantityName name = siValue;. - WCVector (
wc-vector): An N-dimensional vector of scalars. Has a name, dimension N, space-separated values, unit dropdown, and quantity selector with SI conversion display. Generatestype name[N] = {v0, v1, ...};. Data hook type reports as"scalar"(varref outputs element values). Index-aware: WCAssign with an index hook assigns toname[idx]; without an index hook assigns toname[0]or broadcasts via loop. - WCString (
wc-string): A string variable. Has a name and a text value. Generatesstring name = {"text", length};. Thestringstruct ({char *data; long int length;}) is emitted once before variable declarations when any string blocks exist.
Unlike the previous unified WCDatum block, there is no way to switch between types — they are separate custom elements with distinct templates and classes.
WCExpression (wc-expression) is a flowchart block (not a variable) with dynamic data-input hooks that match "scalar" data type for data-flow linking.
SI Measurement System
- 7 base dimensions indexed dim-0..dim-6: L, M, T, I, Θ, N, J
- 22 default quantities (length, mass, time, etc.) and 22 default units (m, kg, s, etc.)
- Units support recursive factor resolution:
1 in = 0.0254 m→getSiFactor()follows references - Cycle detection via visited
SetingetSiFactor() - Datum values auto-convert to SI:
5 km→5000in generated code - Quantity blocks live in the Data tab; Unit blocks live in the Measurements tab
- Only quantities and units actually referenced by datum blocks appear in generated code
Performance Notes
_gridSizefetched once from CSS computed style, then cached per block (nullsentinel)- Bound functions
_dragBound/_stopBoundcreated once in constructor, reused across drags $iduses nativegetElementByIdinstead of CSS selector parsing- Container-scoped
querySelectorAllavoids#id descendantselectors - Child elements cached in
this._el.*duringconnectedCallback - Deferred
setTimeout(..., 0)for initial dropdown population avoids layout thrash
Known Limitations
- No duplicate link detection (same source→target hook pair creates duplicate paths)
- Data hooks only support
"scalar"type for now; no mixed-type data flow (expression varhooks hard-coded to scalar) - No delete functionality for individual blocks
- Datum value parsing only handles
"number suffix"format (no expressions) - Code generation walks linear flow; only decision blocks handle branching
- Tab-hidden SVG paths render at wrong positions (hidden tab
getBoundingClientRectreturns zeros)
Optimization & Simplification Notes
data-block CSS attribute
All WCBlock subclasses set data-block in the constructor. This replaces tedious tag-name selector lists (e.g. wc-start, wc-end, wc-output, ...) throughout style.css, app.js, and persist.js with a single [data-block] selector.
Dead code removed
WCBlock.collectDescendants()— was never called (BFS for downstream blocks; the decision-block codegen handles convergence inline)- Duplicate CSS rule
wc-expression .expr-varhooks .hook[data-direction="in"]— covered by.expr-varhooks .hook
Persistence fixes
- Data connections: save/load now handles both
path.flow(logic) andpath.flow-data(data) SVG paths. Previously, data links were silently lost. - Refresh order:
refreshAll()runs after all blocks and connections are restored, ensuring varref dropdowns and hookdata-typeattributes reflect the full state.
Render deduplication
WCExpression._renderFormula() uses a shared done() helper to call _parseVariables + _rebuildVarHooks after both the MathJax and the fallback rendering path, instead of duplicating the calls.
Refresh consistency
newBooleanNode(), newScalarNode(), and newStringNode() now call refreshVarRefOptions() so existing varref dropdowns update immediately when new variables are created. The explicit refreshVarRefOptions() calls in app.js toolbar handlers were removed as redundant.