Files
avcf/ARCHITECTURE.md
T
adrien b8e8ef9113 overhaul: modular architecture, tests, doc generation, warning system, and UX improvements
- 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
2026-06-22 18:31:23 +02:00

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.