- 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
322 lines
17 KiB
Markdown
322 lines
17 KiB
Markdown
# 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 lookup
|
|
- `svgNamespace` — `"http://www.w3.org/2000/svg"` — for SVG element creation
|
|
- `STORAGE_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 selection
|
|
- `refreshQuantityOptions()` — syncs all quantity-name dropdowns from `wc-quantity` blocks
|
|
- `refreshUnitOptions()` — syncs unit-suffix dropdowns per datum's selected quantity
|
|
- `refreshDefaultUnitOptions()` — syncs default-unit dropdowns per quantity's matching units
|
|
- `getDefaultUnitSuffix(quantityName)` — looks up a quantity's default unit
|
|
- `refreshAll()` — runs all refresh passes in dependency order
|
|
- `refreshVarRefOptions()` — 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 message
|
|
- `closeError(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 direction
|
|
- `linkageFirst` — tracks the first-clicked output hook during link creation
|
|
- `shadowPath` — the dashed SVG path following the cursor during linking
|
|
- `routeBetween(x1, y1, x2, y2)` — 45°-constrained polyline routing
|
|
- `resolveHook(path, which)` — resolves from/to hook references on a path
|
|
- `getHookExit(hook, svgRect)` — computes the extended control point for a hook
|
|
- `drawpath(path)` — draws/redraws an SVG connection path between two hooks
|
|
- `startShadowLink(hook)` — begins showing a dashed shadow path from a hook to cursor
|
|
- `onShadowMove(ev)` — updates shadow path endpoint on mousemove
|
|
- `endShadowLink()` — removes shadow path and stops tracking
|
|
- `resetLinkage()` — cancels pending link and removes shadow
|
|
- `deleteLink(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-block` attribute 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`, `wcNextFlow` arrays of SVG path elements)
|
|
- Code generation (`generateCode()`, `generateNextCode()`)
|
|
- Validation via `_require(selector)` — throws a descriptive `Error` if a required child element is missing from the template, failing early instead of producing `Cannot read properties of null`
|
|
- Selection on click (`.selected` class, blue box-shadow ring)
|
|
- Deletion via X button (top-right, shown on hover) or <kbd>Delete</kbd> key
|
|
- X button omitted for `WCStart` / `WCEnd`; their `delete()` 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 blocks
|
|
- `makeNode(tag)` — deep-clones a template by registry lookup, assigns unique ID
|
|
- `newNode(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 block
|
|
- `saveWorkspace()` — serialises all blocks + connections to localStorage
|
|
- `restoreBlock(info)` — deep-clones a template and restores data onto it
|
|
- `loadWorkspace()` — deserialises and reconstructs the workspace
|
|
- `newWorkspace()` — clears localStorage and reloads the page
|
|
|
|
### `app.js` — Entry point & orchestration
|
|
|
|
Wires everything together:
|
|
|
|
- `createDefaultMeasurements()` — pre-populates 22 SI quantities and 22 SI units
|
|
- `switchTab(name)` — activates a tab by name
|
|
- `setupUI()` — 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:
|
|
1. Extends `EXT` (25px) from source hook in its `data-exit` direction
|
|
2. Routes with 45°-constrained polyline to target hook's exit point
|
|
3. Approaches target hook from its `data-exit` direction
|
|
|
|
### 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.wcNextFlow` and `toElement.wcForeFlow`
|
|
- Removes from DOM; nulls out custom properties
|
|
- Delegated click handler on `#connections` in 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-varhooks` with `data-link-type="data"`, `data-type="scalar"`, and wired through `_setupHook()`
|
|
- **Fallback**: If MathJax is unavailable, `.expr-rendered` shows 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. Generates `int name = 0/1;`.
|
|
- **WCScalar** (`wc-scalar`): A typed scalar variable. Has a name, numeric value, unit dropdown, and quantity selector with SI conversion display. Generates `quantityName 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. Generates `type name[N] = {v0, v1, ...};`. Data hook type reports as `"scalar"` (varref outputs element values). Index-aware: WCAssign with an index hook assigns to `name[idx]`; without an index hook assigns to `name[0]` or broadcasts via loop.
|
|
- **WCString** (`wc-string`): A string variable. Has a name and a text value. Generates `string name = {"text", length};`. The `string` struct (`{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 `Set` in `getSiFactor()`
|
|
- Datum values auto-convert to SI: `5 km` → `5000` in 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
|
|
|
|
- `_gridSize` fetched once from CSS computed style, then cached per block (`null` sentinel)
|
|
- Bound functions `_dragBound` / `_stopBound` created once in constructor, reused across drags
|
|
- `$id` uses native `getElementById` instead of CSS selector parsing
|
|
- Container-scoped `querySelectorAll` avoids `#id descendant` selectors
|
|
- Child elements cached in `this._el.*` during `connectedCallback`
|
|
- 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 `getBoundingClientRect` returns 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) and `path.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 hook `data-type` attributes 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.
|