early version: modular block files, full template set, tab UI, OpenGL defaults
@@ -0,0 +1,318 @@
|
|||||||
|
# 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()`)
|
||||||
|
- 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;` |
|
||||||
|
| `WCString` | String variable (char* + length) | none (inert hook) | `string name = {"text", len};` |
|
||||||
|
| `WCArray` | Typed C array | none | `type name[] = {values};` |
|
||||||
|
| `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`, `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 / WCString)
|
||||||
|
|
||||||
|
Variable blocks come in three 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;`.
|
||||||
|
- **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.
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#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`)
|
||||||
|
|
||||||
|
```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)
|
||||||
|
|
||||||
|
```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:
|
||||||
|
```json
|
||||||
|
{ "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:
|
||||||
|
```c
|
||||||
|
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)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#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
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
// ── Application entry point & orchestration ───────────────────────────────────
|
||||||
|
//
|
||||||
|
// Wires all modules together: builds the initial workspace, creates default
|
||||||
|
// SI measurements, sets up event listeners, and exposes tab switching.
|
||||||
|
//
|
||||||
|
// Depends on: dom.js ($, $id), helpers.js (refresh*), wcblock.js, blocks-algo.js,
|
||||||
|
// blocks-data.js, blocks-proc.js, wcprogram.js, persist.js
|
||||||
|
//
|
||||||
|
// This is loaded last and calls main() at module top level (which runs after
|
||||||
|
// all deferred scripts have executed, i.e. after full HTML parsing).
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-populates the Measurements tab with SI base and coherent derived types/units.
|
||||||
|
* 22 types and 22 units arranged in a 4-column grid.
|
||||||
|
* No prefixed units are created except kilogram (the SI base unit for mass).
|
||||||
|
*/
|
||||||
|
function createDefaultMeasurements() {
|
||||||
|
const prog = $id("main-program");
|
||||||
|
|
||||||
|
const typeDefs = [
|
||||||
|
{ name: "length", dims: [1,0,0,0,0,0,0], unit: "m" },
|
||||||
|
{ name: "mass", dims: [0,1,0,0,0,0,0], unit: "kg" },
|
||||||
|
{ name: "time", dims: [0,0,1,0,0,0,0], unit: "s" },
|
||||||
|
{ name: "current", dims: [0,0,0,1,0,0,0], unit: "A" },
|
||||||
|
{ name: "temperature", dims: [0,0,0,0,1,0,0], unit: "K" },
|
||||||
|
{ name: "amount", dims: [0,0,0,0,0,1,0], unit: "mol" },
|
||||||
|
{ name: "luminous_intensity", dims: [0,0,0,0,0,0,1], unit: "cd" },
|
||||||
|
{ name: "angle", dims: [0,0,0,0,0,0,0], unit: "rad" },
|
||||||
|
{ name: "frequency", dims: [0,0,-1,0,0,0,0], unit: "Hz" },
|
||||||
|
{ name: "force", dims: [1,1,-2,0,0,0,0], unit: "N" },
|
||||||
|
{ name: "energy", dims: [2,1,-2,0,0,0,0], unit: "J" },
|
||||||
|
{ name: "power", dims: [2,1,-3,0,0,0,0], unit: "W" },
|
||||||
|
{ name: "pressure", dims: [-1,1,-2,0,0,0,0], unit: "Pa" },
|
||||||
|
{ name: "charge", dims: [0,0,1,1,0,0,0], unit: "C" },
|
||||||
|
{ name: "voltage", dims: [2,1,-3,-1,0,0,0], unit: "V" },
|
||||||
|
{ name: "capacitance", dims: [-2,-1,4,2,0,0,0], unit: "F" },
|
||||||
|
{ name: "resistance", dims: [2,1,-3,-2,0,0,0], unit: "\u03A9" },
|
||||||
|
{ name: "conductance", dims: [-2,-1,3,2,0,0,0], unit: "S" },
|
||||||
|
{ name: "magnetic_flux", dims: [2,1,-2,-1,0,0,0], unit: "Wb" },
|
||||||
|
{ name: "magnetic_field", dims: [0,1,-2,-1,0,0,0], unit: "T" },
|
||||||
|
{ name: "inductance", dims: [2,1,-2,-2,0,0,0], unit: "H" },
|
||||||
|
{ name: "catalytic_activity", dims: [0,0,-1,0,0,1,0], unit: "kat" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const unitDefs = [
|
||||||
|
{ fullname: "meter", type: "length", suffix: "m", si: "1" },
|
||||||
|
{ fullname: "kilogram", type: "mass", suffix: "kg", si: "1" },
|
||||||
|
{ fullname: "second", type: "time", suffix: "s", si: "1" },
|
||||||
|
{ fullname: "ampere", type: "current", suffix: "A", si: "1" },
|
||||||
|
{ fullname: "kelvin", type: "temperature", suffix: "K", si: "1" },
|
||||||
|
{ fullname: "mole", type: "amount", suffix: "mol", si: "1" },
|
||||||
|
{ fullname: "candela", type: "luminous_intensity", suffix: "cd", si: "1" },
|
||||||
|
{ fullname: "radian", type: "angle", suffix: "rad", si: "1" },
|
||||||
|
{ fullname: "hertz", type: "frequency", suffix: "Hz", si: "1" },
|
||||||
|
{ fullname: "newton", type: "force", suffix: "N", si: "1" },
|
||||||
|
{ fullname: "joule", type: "energy", suffix: "J", si: "1" },
|
||||||
|
{ fullname: "watt", type: "power", suffix: "W", si: "1" },
|
||||||
|
{ fullname: "pascal", type: "pressure", suffix: "Pa", si: "1" },
|
||||||
|
{ fullname: "coulomb", type: "charge", suffix: "C", si: "1" },
|
||||||
|
{ fullname: "volt", type: "voltage", suffix: "V", si: "1" },
|
||||||
|
{ fullname: "farad", type: "capacitance", suffix: "F", si: "1" },
|
||||||
|
{ fullname: "ohm", type: "resistance", suffix: "\u03A9", si: "1" },
|
||||||
|
{ fullname: "siemens", type: "conductance", suffix: "S", si: "1" },
|
||||||
|
{ fullname: "weber", type: "magnetic_flux", suffix: "Wb", si: "1" },
|
||||||
|
{ fullname: "tesla", type: "magnetic_field", suffix: "T", si: "1" },
|
||||||
|
{ fullname: "henry", type: "inductance", suffix: "H", si: "1" },
|
||||||
|
{ fullname: "katal", type: "catalytic_activity", suffix: "kat", si: "1" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const COLS = 4, COL_W = 250, ROW_H = 190, PAD = 10;
|
||||||
|
const typeRows = Math.ceil(typeDefs.length / COLS);
|
||||||
|
|
||||||
|
const typeTags = typeDefs.map((t, i) => {
|
||||||
|
const el = prog.newQuantityNode();
|
||||||
|
el.style.left = PAD + (i % COLS) * COL_W + "px";
|
||||||
|
el.style.top = PAD + Math.floor(i / COLS) * ROW_H + "px";
|
||||||
|
el.querySelector(".quantity-name").value = t.name;
|
||||||
|
for (let j = 0; j < 7; j++) {
|
||||||
|
el.querySelector(`.dim-${j}`).value = t.dims[j];
|
||||||
|
}
|
||||||
|
el.dataset.defaultUnit = t.unit;
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitTags = unitDefs.map((u, i) => {
|
||||||
|
const el = prog.newUnitNode();
|
||||||
|
el.style.left = PAD + (i % COLS) * COL_W + "px";
|
||||||
|
el.style.top = PAD + (typeRows + Math.floor(i / COLS)) * ROW_H + "px";
|
||||||
|
el.querySelector(".unit-fullname").value = u.fullname;
|
||||||
|
el.querySelector(".unit-type").value = u.type;
|
||||||
|
el.querySelector(".unit-suffix").value = u.suffix;
|
||||||
|
el.querySelector(".unit-si").value = u.si;
|
||||||
|
el.updateSiDisplay();
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const el of typeTags) {
|
||||||
|
const sel = el.querySelector(".quantity-default-unit");
|
||||||
|
if ([...sel.options].some(o => o.value === el.dataset.defaultUnit)) {
|
||||||
|
sel.value = el.dataset.defaultUnit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Library tab creation helper ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a library tab and its associated pane in the Procedures tab.
|
||||||
|
* @param {string} libName - library identifier
|
||||||
|
* @param {boolean} [makeActive] - whether to mark the tab/pane as active
|
||||||
|
* @returns {HTMLElement} the #procedures-area element inside the new pane
|
||||||
|
*/
|
||||||
|
function addLibTab(libName, makeActive) {
|
||||||
|
const tab = document.createElement("button");
|
||||||
|
tab.className = "lib-tab" + (makeActive ? " active" : "");
|
||||||
|
tab.dataset.libName = libName;
|
||||||
|
tab.textContent = libName;
|
||||||
|
tab.addEventListener("click", () => switchLibTab(libName));
|
||||||
|
document.querySelector(".lib-tab-bar").appendChild(tab);
|
||||||
|
|
||||||
|
const pane = document.createElement("div");
|
||||||
|
pane.className = "lib-pane" + (makeActive ? " active" : "");
|
||||||
|
pane.dataset.libName = libName;
|
||||||
|
|
||||||
|
const area = document.createElement("div");
|
||||||
|
area.className = "procedures-area";
|
||||||
|
pane.appendChild(area);
|
||||||
|
$id("pane-procedures").appendChild(pane);
|
||||||
|
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Default library creation ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the default "stdio" library with a "puts" procedure
|
||||||
|
* (one string input param, no outputs).
|
||||||
|
*/
|
||||||
|
function createDefaultLibraries() {
|
||||||
|
const prog = $id("main-program");
|
||||||
|
const libName = "stdio";
|
||||||
|
addLibTab(libName, true);
|
||||||
|
const proc = prog.newProcedureNode(libName);
|
||||||
|
if (proc) {
|
||||||
|
proc.querySelector(".proc-name").value = "puts";
|
||||||
|
proc._addParamRow(proc.querySelector(".proc-inputs"), "in");
|
||||||
|
const row = proc.querySelector(".proc-inputs .proc-param-row");
|
||||||
|
if (row) {
|
||||||
|
row.querySelector(".proc-param-name").value = "str";
|
||||||
|
row.querySelector(".proc-param-type").value = "string";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshProcOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── OpenGL default procedures ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates param rows on a procedure block and sets their name/type values.
|
||||||
|
* Follows the same pattern as WCProcedure.restoreData().
|
||||||
|
* @param {HTMLElement} proc - a wc-procedure element
|
||||||
|
* @param {[string, string][]} inputs - array of [name, type] pairs
|
||||||
|
* @param {[string, string][]} outputs - array of [name, type] pairs
|
||||||
|
*/
|
||||||
|
function _addProcParams(proc, inputs, outputs) {
|
||||||
|
for (const [name, type] of inputs) {
|
||||||
|
proc._addParamRow(proc.querySelector(".proc-inputs"), "in");
|
||||||
|
const rows = proc.querySelectorAll(".proc-inputs .proc-param-row");
|
||||||
|
const last = rows[rows.length - 1];
|
||||||
|
last.querySelector(".proc-param-name").value = name;
|
||||||
|
last.querySelector(".proc-param-type").value = type;
|
||||||
|
}
|
||||||
|
for (const [name, type] of outputs) {
|
||||||
|
proc._addParamRow(proc.querySelector(".proc-outputs"), "out");
|
||||||
|
const rows = proc.querySelectorAll(".proc-outputs .proc-param-row");
|
||||||
|
const last = rows[rows.length - 1];
|
||||||
|
last.querySelector(".proc-param-name").value = name;
|
||||||
|
last.querySelector(".proc-param-type").value = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default OpenGL procedure definitions organised into three libraries:
|
||||||
|
*
|
||||||
|
* glfw — GLFW window management (init, create, swap, poll, close, terminate)
|
||||||
|
* glad — OpenGL loader initialisation (gladLoadGL)
|
||||||
|
* gl — OpenGL core functions for a Hello Triangle pipeline
|
||||||
|
* (shader compile/link, buffer/VAO setup, draw, clear, cleanup)
|
||||||
|
*
|
||||||
|
* Also creates a default "vertices" float array with Hello Triangle vertex data
|
||||||
|
* in the Data tab so the user has a starting point.
|
||||||
|
*
|
||||||
|
* Code generation note:
|
||||||
|
* Output parameters are passed with a `&` prefix in the generated C code
|
||||||
|
* (e.g. `glCreateShader(type, &shader);`). For OpenGL functions that
|
||||||
|
* natively return values rather than taking pointer outputs, the generated
|
||||||
|
* code uses this convention and may need a thin wrapper.
|
||||||
|
*/
|
||||||
|
function createDefaultOpenGLProcedures() {
|
||||||
|
const prog = $id("main-program");
|
||||||
|
|
||||||
|
const PROC_DEFS = [
|
||||||
|
// ── glfw: window management ──
|
||||||
|
{ lib: "glfw", name: "glfwInit", inputs: [], outputs: [["result","int"]] },
|
||||||
|
{ lib: "glfw", name: "glfwCreateWindow", inputs: [["width","int"],["height","int"],["title","string"]], outputs: [["window","int"]] },
|
||||||
|
{ lib: "glfw", name: "glfwMakeContextCurrent", inputs: [["window","int"]], outputs: [] },
|
||||||
|
{ lib: "glfw", name: "glfwSwapBuffers", inputs: [["window","int"]], outputs: [] },
|
||||||
|
{ lib: "glfw", name: "glfwPollEvents", inputs: [], outputs: [] },
|
||||||
|
{ lib: "glfw", name: "glfwWindowShouldClose", inputs: [["window","int"]], outputs: [["result","int"]] },
|
||||||
|
{ lib: "glfw", name: "glfwTerminate", inputs: [], outputs: [] },
|
||||||
|
// ── glad: OpenGL loader ──
|
||||||
|
{ lib: "glad", name: "gladLoadGL", inputs: [], outputs: [["result","int"]] },
|
||||||
|
// ── gl: OpenGL core Hello Triangle ──
|
||||||
|
{ lib: "gl", name: "glCreateShader", inputs: [["type","unsigned int"]], outputs: [["shader","unsigned int"]] },
|
||||||
|
{ lib: "gl", name: "glShaderSource", inputs: [["shader","unsigned int"],["count","int"],["source","string"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glCompileShader", inputs: [["shader","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glGetShaderiv", inputs: [["shader","unsigned int"],["pname","unsigned int"]], outputs: [["params","int"]] },
|
||||||
|
{ lib: "gl", name: "glCreateProgram", inputs: [], outputs: [["program","unsigned int"]] },
|
||||||
|
{ lib: "gl", name: "glAttachShader", inputs: [["program","unsigned int"],["shader","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glLinkProgram", inputs: [["program","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glGetProgramiv", inputs: [["program","unsigned int"],["pname","unsigned int"]], outputs: [["params","int"]] },
|
||||||
|
{ lib: "gl", name: "glGenVertexArrays", inputs: [["n","unsigned int"]], outputs: [["arrays","unsigned int"]] },
|
||||||
|
{ lib: "gl", name: "glBindVertexArray", inputs: [["array","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glGenBuffers", inputs: [["n","unsigned int"]], outputs: [["buffers","unsigned int"]] },
|
||||||
|
{ lib: "gl", name: "glBindBuffer", inputs: [["target","unsigned int"],["buffer","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glBufferData", inputs: [["target","unsigned int"],["size","int"],["data","string"],["usage","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glVertexAttribPointer", inputs: [["index","unsigned int"],["size","int"],["type","unsigned int"],["normalized","int"],["stride","int"],["pointer","int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glEnableVertexAttribArray", inputs: [["index","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glUseProgram", inputs: [["program","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glDrawArrays", inputs: [["mode","unsigned int"],["first","int"],["count","int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glClear", inputs: [["mask","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glClearColor", inputs: [["red","float"],["green","float"],["blue","float"],["alpha","float"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glDeleteShader", inputs: [["shader","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glDeleteProgram", inputs: [["program","unsigned int"]], outputs: [] },
|
||||||
|
{ lib: "gl", name: "glViewport", inputs: [["x","int"],["y","int"],["width","int"],["height","int"]], outputs: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create each library tab once, then populate procedures
|
||||||
|
const libsDone = {};
|
||||||
|
for (const pd of PROC_DEFS) {
|
||||||
|
if (!libsDone[pd.lib]) {
|
||||||
|
libsDone[pd.lib] = true;
|
||||||
|
addLibTab(pd.lib, false);
|
||||||
|
}
|
||||||
|
const proc = prog.newProcedureNode(pd.lib);
|
||||||
|
if (proc) {
|
||||||
|
proc.querySelector(".proc-name").value = pd.name;
|
||||||
|
_addProcParams(proc, pd.inputs, pd.outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshProcOptions();
|
||||||
|
|
||||||
|
// ── Default vertex array for Hello Triangle ──
|
||||||
|
const arr = prog.newArrayNode();
|
||||||
|
if (arr) {
|
||||||
|
arr.restoreData({
|
||||||
|
name: "vertices",
|
||||||
|
type: "float",
|
||||||
|
values: ["-0.5", "-0.5", "0.0", "0.5", "-0.5", "0.0", "0.0", "0.5", "0.0"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Tab switching ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the clicked tab and shows its corresponding pane.
|
||||||
|
* @param {string} name - tab identifier (matches data-tab attribute and pane id suffix)
|
||||||
|
*/
|
||||||
|
function switchTab(name) {
|
||||||
|
for (const btn of document.querySelectorAll(".tab-btn")) {
|
||||||
|
btn.classList.toggle("active", btn.dataset.tab === name);
|
||||||
|
}
|
||||||
|
for (const pane of document.querySelectorAll(".tab-pane")) {
|
||||||
|
pane.classList.toggle("active", pane.id === "pane-" + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Event listeners ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Wires all static button click handlers (replaces inline onclick attributes). */
|
||||||
|
function setupUI() {
|
||||||
|
function closeVariableSubmenu() {
|
||||||
|
const sub = document.querySelector(".submenu[data-action='variable-submenu']");
|
||||||
|
if (sub) sub.classList.remove("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar block-creation helpers */
|
||||||
|
const ACTION_REFRESH = {
|
||||||
|
varref: refreshAll,
|
||||||
|
boolean: refreshAll,
|
||||||
|
scalar: refreshAll,
|
||||||
|
string: refreshAll,
|
||||||
|
array: refreshAll,
|
||||||
|
quantity: refreshAll,
|
||||||
|
unit: refreshAll,
|
||||||
|
proccall: refreshAll,
|
||||||
|
assign: refreshAll
|
||||||
|
};
|
||||||
|
function handleBlockAction(btn) {
|
||||||
|
const tag = BLOCK_ACTIONS[btn.dataset.action];
|
||||||
|
if (!tag) return false;
|
||||||
|
const prog = $id("main-program");
|
||||||
|
const el = prog.newNode(tag);
|
||||||
|
if (el && ACTION_REFRESH[btn.dataset.action]) ACTION_REFRESH[btn.dataset.action]();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Algorithm pane toolbar */
|
||||||
|
const algoToolbar = $id("pane-algorithm").querySelector(".pane-toolbar");
|
||||||
|
algoToolbar.addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (btn && handleBlockAction(btn)) return;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Data pane toolbar */
|
||||||
|
const dataToolbar = $id("pane-data").querySelector(".pane-toolbar");
|
||||||
|
dataToolbar.addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
const prog = $id("main-program");
|
||||||
|
if (action === "variable") {
|
||||||
|
const sub = document.querySelector(".submenu[data-action='variable-submenu']");
|
||||||
|
if (sub) sub.classList.toggle("open");
|
||||||
|
} else if (handleBlockAction(btn)) {
|
||||||
|
closeVariableSubmenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Measurements pane toolbar */
|
||||||
|
const measToolbar = $id("pane-measurements").querySelector(".pane-toolbar");
|
||||||
|
measToolbar.addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (btn) handleBlockAction(btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Procedures pane toolbar */
|
||||||
|
const procToolbar = $id("pane-procedures").querySelector(".pane-toolbar");
|
||||||
|
procToolbar.addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
const prog = $id("main-program");
|
||||||
|
|
||||||
|
if (btn.dataset.action === "add-library") {
|
||||||
|
const libName = prompt("Library name:", "mylib");
|
||||||
|
if (!libName) return;
|
||||||
|
addLibTab(libName, false);
|
||||||
|
switchLibTab(libName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn.dataset.action === "add-procedure") {
|
||||||
|
const activeLibPane = document.querySelector(".lib-pane.active");
|
||||||
|
if (!activeLibPane) return;
|
||||||
|
const el = prog.newNode("wc-procedure", activeLibPane.querySelector("#procedures-area"));
|
||||||
|
if (el) refreshProcOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Lib tab bar click (delegated) */
|
||||||
|
document.querySelector(".lib-tab-bar").addEventListener("click", e => {
|
||||||
|
const tab = e.target.closest(".lib-tab");
|
||||||
|
if (tab) switchLibTab(tab.dataset.libName);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Global toolbar */
|
||||||
|
const globalToolbar = document.querySelector(".global-toolbar");
|
||||||
|
globalToolbar.addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
if (btn.dataset.action === "generate") $id("main-program").generateCode();
|
||||||
|
if (btn.dataset.action === "save") saveWorkspace();
|
||||||
|
if (btn.dataset.action === "load") loadWorkspace();
|
||||||
|
if (btn.dataset.action === "new") newWorkspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Tab bar */
|
||||||
|
document.querySelector(".tab-bar").addEventListener("click", e => {
|
||||||
|
const btn = e.target.closest(".tab-btn");
|
||||||
|
if (btn) switchTab(btn.dataset.tab);
|
||||||
|
closeVariableSubmenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Close variable submenu on outside click */
|
||||||
|
document.addEventListener("click", e => {
|
||||||
|
if (!e.target.closest(".btn-group")) closeVariableSubmenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Click anywhere in draw-area (except hooks, which stopPropagation) cancels pending link */
|
||||||
|
$id("draw-area").addEventListener("click", resetLinkage);
|
||||||
|
|
||||||
|
/* Click on an SVG connection path deletes it */
|
||||||
|
$id("connections").addEventListener("click", e => {
|
||||||
|
const path = e.target.closest("path.flow, path.flow-data");
|
||||||
|
if (!path) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
for (const el of document.querySelectorAll(".selected")) el.classList.remove("selected");
|
||||||
|
resetLinkage();
|
||||||
|
deleteLink(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Error modal dismiss */
|
||||||
|
$("#error-overlay").addEventListener("click", closeError);
|
||||||
|
|
||||||
|
/* Keyboard: Delete key removes selected block */
|
||||||
|
document.addEventListener("keydown", ev => {
|
||||||
|
if (ev.key !== "Delete" && ev.key !== "Del") return;
|
||||||
|
if (ev.target.tagName === "INPUT" || ev.target.tagName === "TEXTAREA" || ev.target.tagName === "SELECT") return;
|
||||||
|
const sel = document.querySelector(".selected");
|
||||||
|
if (sel && typeof sel.delete === "function") {
|
||||||
|
ev.preventDefault();
|
||||||
|
sel.delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Click outside any block deselects */
|
||||||
|
document.addEventListener("click", ev => {
|
||||||
|
if (!ev.target.closest("[data-block]")) {
|
||||||
|
for (const el of document.querySelectorAll(".selected")) el.classList.remove("selected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps the application: builds the flowchart canvas with start/end blocks,
|
||||||
|
* creates default SI measurements, optionally loads a saved workspace, then
|
||||||
|
* wires up all UI event listeners.
|
||||||
|
*/
|
||||||
|
function main() {
|
||||||
|
$id("main-program").build();
|
||||||
|
createDefaultMeasurements();
|
||||||
|
createDefaultLibraries();
|
||||||
|
createDefaultOpenGLProcedures();
|
||||||
|
if (localStorage.getItem(STORAGE_KEY)) {
|
||||||
|
loadWorkspace();
|
||||||
|
}
|
||||||
|
setupUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
// ── Algorithm-tab block definitions ────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Blocks that live on the Algorithm (flowchart) canvas:
|
||||||
|
// WCStart, WCEnd, WCOutput, WCDecision, WCAssign, WCExpression, WCVarRef
|
||||||
|
//
|
||||||
|
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
|
||||||
|
//
|
||||||
|
// Load order dependencies:
|
||||||
|
// wcblock.js → WCBlock base class
|
||||||
|
// helpers.js → findDataSource, resolveSourceName, escapeString, toCName,
|
||||||
|
// commentPrefix, refreshVarRefOptions
|
||||||
|
// dom.js → $id
|
||||||
|
// error.js → showError
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCStart — program entry point ──────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCStart extends WCBlock {
|
||||||
|
delete() { showError("Cannot delete the start block."); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCEnd — program exit ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCEnd extends WCBlock {
|
||||||
|
delete() { showError("Cannot delete the end block."); }
|
||||||
|
generateOwnCode() { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCOutput — printf statement ────────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCOutput extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.textarea = this.querySelector("textarea");
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { textarea: this._el.textarea.value }; }
|
||||||
|
restoreData(d) { this.querySelector("textarea").value = d.textarea || ""; }
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const msg = escapeString(this._el.textarea.value || "");
|
||||||
|
return `\nprintf("${msg}");\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCDecision — conditional branch ────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCDecision extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.boolIn = this.querySelector(".decision-data-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBoolName(block) {
|
||||||
|
return resolveSourceName(block) || "flag";
|
||||||
|
}
|
||||||
|
|
||||||
|
_branchDescendants(roots) {
|
||||||
|
const d = new Set(), q = [...roots];
|
||||||
|
for (const el of q) {
|
||||||
|
if (d.has(el)) continue;
|
||||||
|
d.add(el);
|
||||||
|
if (el instanceof WCDecision) continue;
|
||||||
|
for (const p of el.wcNextFlow) {
|
||||||
|
if (!d.has(p.toElement)) q.push(p.toElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitBranch(roots, stop, visited) {
|
||||||
|
const v = new Set(visited);
|
||||||
|
let c = "";
|
||||||
|
const walk = el => {
|
||||||
|
if (v.has(el) || stop.has(el)) return;
|
||||||
|
v.add(el);
|
||||||
|
if (el instanceof WCDecision) {
|
||||||
|
c += el.generateCode(v);
|
||||||
|
} else {
|
||||||
|
c += el.generateOwnCode();
|
||||||
|
for (const p of el.wcNextFlow) walk(p.toElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const el of roots) walk(el);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCode(visited = new Set()) {
|
||||||
|
const yesTargets = [], noTargets = [];
|
||||||
|
for (const path of this.wcNextFlow) {
|
||||||
|
const hook = path.fromHook;
|
||||||
|
if (!hook || hook.id === "hook-out1") {
|
||||||
|
yesTargets.push(path.toElement);
|
||||||
|
} else if (hook.id === "hook-out0") {
|
||||||
|
noTargets.push(path.toElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const yesAll = this._branchDescendants(yesTargets);
|
||||||
|
const noAll = this._branchDescendants(noTargets);
|
||||||
|
const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this));
|
||||||
|
|
||||||
|
const yesCode = this._emitBranch(yesTargets, shared, visited);
|
||||||
|
const noCode = this._emitBranch(noTargets, shared, visited);
|
||||||
|
const sharedCode = this._emitBranch([...shared], new Set(), visited);
|
||||||
|
|
||||||
|
for (const el of yesAll) visited.add(el);
|
||||||
|
for (const el of noAll) visited.add(el);
|
||||||
|
|
||||||
|
const source = findDataSource(this._el.boolIn);
|
||||||
|
const cond = source ? this._getBoolName(source) : "0";
|
||||||
|
let out = "";
|
||||||
|
if (yesCode || noCode) {
|
||||||
|
if (yesCode) out += `if (${cond}) {\n${yesCode}}`;
|
||||||
|
if (noCode) {
|
||||||
|
out += yesCode
|
||||||
|
? ` else {\n${noCode}}`
|
||||||
|
: `if (!(${cond})) {\n${noCode}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += sharedCode;
|
||||||
|
return out ? `\n${out}\n` : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCAssign — variable assignment block ───────────────────────────────────
|
||||||
|
|
||||||
|
class WCAssign extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.target = this.querySelector(".assign-target");
|
||||||
|
this._el.value = this.querySelector(".assign-value");
|
||||||
|
this._el.dataIn = this.querySelector(".assign-row .hook");
|
||||||
|
this._el.indexArea = this.querySelector(".assign-index-area");
|
||||||
|
this._el.indexInput = this.querySelector(".assign-index");
|
||||||
|
this._el.indexHook = this.querySelector(".assign-index-area .hook");
|
||||||
|
|
||||||
|
this._el.target.addEventListener("change", () => this._syncType());
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshVarRefOptions();
|
||||||
|
this._syncType();
|
||||||
|
this._updateValueVisibility();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateValueVisibility() {
|
||||||
|
const hasConn = findDataSource(this._el.dataIn) !== null;
|
||||||
|
this._el.value.classList.toggle("hidden", hasConn);
|
||||||
|
const hasIdxConn = findDataSource(this._el.indexHook) !== null;
|
||||||
|
this._el.indexInput.classList.toggle("hidden", hasIdxConn);
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncType() {
|
||||||
|
const id = this._el.target.value;
|
||||||
|
const target = id ? $id(id) : null;
|
||||||
|
const type = target ? target.dataType : "scalar";
|
||||||
|
this._el.dataIn.dataset.type = type;
|
||||||
|
this._el.dataIn.title = "expects " + type;
|
||||||
|
this._el.indexArea.style.display = type === "array" ? "flex" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSourceVarName(block) {
|
||||||
|
return resolveSourceName(block) || "x";
|
||||||
|
}
|
||||||
|
|
||||||
|
_genStringLiteral(name, raw) {
|
||||||
|
const escaped = escapeString(raw);
|
||||||
|
const len = raw.length;
|
||||||
|
return "\n" + [
|
||||||
|
`free(${name}.data);`,
|
||||||
|
`${name}.length = ${len};`,
|
||||||
|
`${name}.data = malloc(${len} + 1);`,
|
||||||
|
`memcpy(${name}.data, "${escaped}", ${len});`,
|
||||||
|
`${name}.data[${len}] = 0;`,
|
||||||
|
].join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
_genStringCopy(dst, src) {
|
||||||
|
return "\n" + [
|
||||||
|
`free(${dst}.data);`,
|
||||||
|
`${dst}.length = ${src}.length;`,
|
||||||
|
`${dst}.data = malloc(${src}.length + 1);`,
|
||||||
|
`memcpy(${dst}.data, ${src}.data, ${src}.length);`,
|
||||||
|
`${dst}.data[${dst}.length] = 0;`,
|
||||||
|
].join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { target: this._el.target.value, value: this._el.value.value, index: this._el.indexInput.value }; }
|
||||||
|
restoreData(d) {
|
||||||
|
const tgt = this.querySelector(".assign-target");
|
||||||
|
tgt.dataset.restoredValue = d.target || "";
|
||||||
|
tgt.value = d.target || "";
|
||||||
|
this.querySelector(".assign-value").value = d.value || "";
|
||||||
|
this.querySelector(".assign-index").value = d.index || "0";
|
||||||
|
this._syncType();
|
||||||
|
this._updateValueVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const targetId = this._el.target.value;
|
||||||
|
if (!targetId) return "";
|
||||||
|
const target = $id(targetId);
|
||||||
|
if (!target) return "";
|
||||||
|
const targetName = resolveSourceName(target) || "x";
|
||||||
|
const targetType = target.dataType || "scalar";
|
||||||
|
|
||||||
|
const source = findDataSource(this._el.dataIn);
|
||||||
|
|
||||||
|
if (targetType === "array") {
|
||||||
|
const idxSource = findDataSource(this._el.indexHook);
|
||||||
|
const idxExpr = idxSource ? this._getSourceVarName(idxSource) : (this._el.indexInput.value || "0");
|
||||||
|
if (!source) {
|
||||||
|
const raw = this._el.value.value || "0";
|
||||||
|
return `\n${targetName}[${idxExpr}] = ${raw};\n`;
|
||||||
|
}
|
||||||
|
const srcName = this._getSourceVarName(source);
|
||||||
|
return `\n${targetName}[${idxExpr}] = ${srcName};\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
const raw = this._el.value.value || "0";
|
||||||
|
if (targetType === "string") return this._genStringLiteral(targetName, raw);
|
||||||
|
return `\n${targetName} = ${raw};\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcName = this._getSourceVarName(source);
|
||||||
|
if (targetType === "string" && source.dataType === "string") {
|
||||||
|
return this._genStringCopy(targetName, srcName);
|
||||||
|
}
|
||||||
|
return `\n${targetName} = ${srcName};\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCVarRef — read-only variable reference with data output hook ──────────
|
||||||
|
|
||||||
|
class WCVarRef extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.select = this.querySelector(".varref-select");
|
||||||
|
this._el.select.addEventListener("change", () => {
|
||||||
|
this._syncType();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncType() {
|
||||||
|
const id = this._el.select.value;
|
||||||
|
const target = id ? $id(id) : null;
|
||||||
|
const hook = this.querySelector(".hook-out");
|
||||||
|
hook.dataset.type = target ? target.dataType : "scalar";
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { selected: this._el.select.value }; }
|
||||||
|
restoreData(d) {
|
||||||
|
const sel = this.querySelector(".varref-select");
|
||||||
|
sel.dataset.restoredValue = d.selected || "";
|
||||||
|
sel.value = d.selected || "";
|
||||||
|
const id = sel.value;
|
||||||
|
const target = id ? $id(id) : null;
|
||||||
|
const hook = this.querySelector(".hook-out");
|
||||||
|
hook.dataset.type = target ? target.dataType : "scalar";
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCExpression — mathematical expression with MathJax rendering & data hooks ──
|
||||||
|
|
||||||
|
class WCExpression extends WCBlock {
|
||||||
|
get dataType() {
|
||||||
|
const el = this._el?.latex || this.querySelector(".expr-latex");
|
||||||
|
const latex = el?.value || "";
|
||||||
|
return /[<>]|==|!=|\\(?:ge|le|ne|neq|geq|leq|land|lor|wedge|vee|neg|lnot)\b/.test(latex) ? "boolean" : "scalar";
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".expr-name");
|
||||||
|
this._el.latex = this.querySelector(".expr-latex");
|
||||||
|
this._el.rendered = this.querySelector(".expr-rendered");
|
||||||
|
this._el.varhooks = this.querySelector(".expr-varhooks");
|
||||||
|
|
||||||
|
this._el.latex.addEventListener("input", () => this._renderFormula());
|
||||||
|
setTimeout(() => this._renderFormula(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { name: this._el.name.value, latex: this._el.latex.value }; }
|
||||||
|
restoreData(d) {
|
||||||
|
this._el.name.value = d.name || "";
|
||||||
|
this._el.latex.value = d.latex || "";
|
||||||
|
this._renderFormula();
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseVariables(latex) {
|
||||||
|
const tokens = latex
|
||||||
|
.replace(/\\[a-zA-Z]+/g, "")
|
||||||
|
.replace(/[{}\[\]()]/g, " ")
|
||||||
|
.replace(/[\d+*\/^_=,;:!?<>|~`@#\$%&'-]/g, " ")
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(t => t.length > 0 && t.length <= 8)
|
||||||
|
.filter(t => ![
|
||||||
|
"sin","cos","tan","cot","sec","csc","arcsin","arccos","arctan",
|
||||||
|
"log","ln","lg","exp","sqrt","lim","inf","sum","int","prod","det",
|
||||||
|
"gcd","lcm","max","min","arg","mod","Re","Im","deg","Pr","var",
|
||||||
|
"alpha","beta","gamma","delta","epsilon","zeta","eta","theta",
|
||||||
|
"iota","kappa","lambda","mu","nu","xi","omicron","rho","sigma",
|
||||||
|
"tau","upsilon","phi","chi","psi","omega","varepsilon","vartheta",
|
||||||
|
"varpi","varphi","varrho","varsigma","varUpsilon"
|
||||||
|
].includes(t.toLowerCase()));
|
||||||
|
return [...new Set(tokens)];
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebuildVarHooks(vars) {
|
||||||
|
this._el.varhooks.innerHTML = "";
|
||||||
|
for (const v of vars) {
|
||||||
|
const hook = document.createElement("button");
|
||||||
|
hook.className = "hook";
|
||||||
|
hook.id = this.id + "-vh-" + v;
|
||||||
|
hook.title = v;
|
||||||
|
hook.textContent = v;
|
||||||
|
hook.dataset.direction = "in";
|
||||||
|
hook.dataset.linkType = "data";
|
||||||
|
hook.dataset.type = "scalar";
|
||||||
|
hook.dataset.exit = "left";
|
||||||
|
this._setupHook(hook);
|
||||||
|
this._el.varhooks.appendChild(hook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFormula() {
|
||||||
|
const latex = this._el.latex.value;
|
||||||
|
const outHook = this.querySelector(".hook-out");
|
||||||
|
if (outHook) outHook.dataset.type = this.dataType;
|
||||||
|
this._el.rendered.innerHTML = "";
|
||||||
|
const vars = latex.trim() ? this._parseVariables(latex) : [];
|
||||||
|
this._rebuildVarHooks(vars);
|
||||||
|
if (!latex.trim()) return;
|
||||||
|
if (window.MathJax && MathJax.typesetPromise) {
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.textContent = "$$" + latex + "$$";
|
||||||
|
const typeset = () => {
|
||||||
|
MathJax.typesetPromise([wrapper]).then(() => {
|
||||||
|
this._el.rendered.innerHTML = wrapper.innerHTML;
|
||||||
|
}).catch(() => {
|
||||||
|
this._el.rendered.textContent = "\u26A0 error";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (MathJax.startup && MathJax.startup.promise && MathJax.startup.promise.then) {
|
||||||
|
MathJax.startup.promise.then(typeset);
|
||||||
|
} else {
|
||||||
|
typeset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._el.rendered.textContent = latex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCode(visited = new Set()) {
|
||||||
|
if (visited.has(this)) return "";
|
||||||
|
visited.add(this);
|
||||||
|
return this.generateOwnCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const name = this._el.name.value || "res";
|
||||||
|
const formula = this._el.latex.value || "0";
|
||||||
|
const type = this.dataType === "boolean" ? "int" : "double";
|
||||||
|
return `${type} ${name} = ${formula};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
// ── Data & Measurement tab block definitions ───────────────────────────────
|
||||||
|
//
|
||||||
|
// Blocks that live in the Data tab (variable area) and Measurements tab:
|
||||||
|
// WCQuantity, WCUnit, WCBoolean, WCScalar, WCString, WCArray
|
||||||
|
//
|
||||||
|
// Each extends WCBlock (from wcblock.js) and provides generateOwnCode().
|
||||||
|
//
|
||||||
|
// Load order dependencies:
|
||||||
|
// wcblock.js → WCBlock base class
|
||||||
|
// helpers.js → splitValueAndUnit, rebuildSelect, refreshQuantityOptions,
|
||||||
|
// refreshUnitOptions, refreshDefaultUnitOptions, refreshVarRefOptions,
|
||||||
|
// getDefaultUnitSuffix, toCName, commentPrefix, escapeString,
|
||||||
|
// refreshAll
|
||||||
|
// dom.js → $id
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCQuantity — measurement quantity definition ───────────────────────────
|
||||||
|
|
||||||
|
class WCQuantity extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".quantity-name");
|
||||||
|
this._el.dims = Array.from({ length: 7 }, (_, i) => this.querySelector(`.dim-${i}`));
|
||||||
|
this._el.min = this.querySelector(".quantity-min");
|
||||||
|
this._el.max = this.querySelector(".quantity-max");
|
||||||
|
this._el.defaultUnit = this.querySelector(".quantity-default-unit");
|
||||||
|
|
||||||
|
for (const inp of this.querySelectorAll("input")) {
|
||||||
|
inp.addEventListener("input", () => {
|
||||||
|
refreshQuantityOptions();
|
||||||
|
refreshDefaultUnitOptions();
|
||||||
|
refreshUnitOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const sel of this.querySelectorAll("select")) {
|
||||||
|
sel.addEventListener("change", () => {
|
||||||
|
for (const d of $id("variable-area").querySelectorAll("wc-scalar")) {
|
||||||
|
d.updateSiDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => { refreshQuantityOptions(); refreshDefaultUnitOptions(); }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
return {
|
||||||
|
name: this._el.name.value,
|
||||||
|
dims: this._el.dims.map(el => el.value),
|
||||||
|
min: this._el.min.value,
|
||||||
|
max: this._el.max.value,
|
||||||
|
defaultUnit: this._el.defaultUnit.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
this.querySelector(".quantity-name").value = d.name || "";
|
||||||
|
for (let i = 0; i < 7; i++) this.querySelector(`.dim-${i}`).value = d.dims ? d.dims[i] : "0";
|
||||||
|
this.querySelector(".quantity-min").value = d.min || "";
|
||||||
|
this.querySelector(".quantity-max").value = d.max || "";
|
||||||
|
this.querySelector(".quantity-default-unit").value = d.defaultUnit || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCUnit — unit of measurement ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCUnit extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.fullname = this.querySelector(".unit-fullname");
|
||||||
|
this._el.type = this.querySelector(".unit-type");
|
||||||
|
this._el.suffix = this.querySelector(".unit-suffix");
|
||||||
|
this._el.si = this.querySelector(".unit-si");
|
||||||
|
this._el.resolved = this.querySelector(".unit-resolved");
|
||||||
|
|
||||||
|
for (const inp of this.querySelectorAll("input")) {
|
||||||
|
inp.addEventListener("input", () => {
|
||||||
|
refreshUnitOptions();
|
||||||
|
refreshDefaultUnitOptions();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const sel of this.querySelectorAll("select")) {
|
||||||
|
sel.addEventListener("change", () => {
|
||||||
|
refreshUnitOptions();
|
||||||
|
refreshDefaultUnitOptions();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshQuantityOptions();
|
||||||
|
refreshDefaultUnitOptions();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseDefinition() {
|
||||||
|
return splitValueAndUnit(this._el.si.value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSiFactor(visited) {
|
||||||
|
visited = visited || new Set();
|
||||||
|
const { numVal, unitSuffix } = this.parseDefinition();
|
||||||
|
if (!unitSuffix || visited.has(this.id)) return numVal;
|
||||||
|
for (const u of $id("data-area").querySelectorAll("wc-unit")) {
|
||||||
|
if (u.querySelector(".unit-suffix").value === unitSuffix) {
|
||||||
|
visited.add(this.id);
|
||||||
|
return numVal * u.getSiFactor(visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
return {
|
||||||
|
fullname: this._el.fullname.value,
|
||||||
|
type: this._el.type.value,
|
||||||
|
suffix: this._el.suffix.value,
|
||||||
|
si: this._el.si.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
this.querySelector(".unit-fullname").value = d.fullname || "";
|
||||||
|
this.querySelector(".unit-type").value = d.type || "";
|
||||||
|
this.querySelector(".unit-suffix").value = d.suffix || "";
|
||||||
|
this.querySelector(".unit-si").value = d.si || "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSiDisplay() {
|
||||||
|
const { unitSuffix } = this.parseDefinition();
|
||||||
|
this._el.resolved.textContent = unitSuffix
|
||||||
|
? `= ${this.getSiFactor()} SI`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCBoolean — boolean variable ───────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCBoolean extends WCBlock {
|
||||||
|
get dataType() { return "boolean"; }
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".var-name");
|
||||||
|
this._el.value = this.querySelector(".var-value");
|
||||||
|
this._el.comment = this.querySelector(".var-comment");
|
||||||
|
this._el.cname = this.querySelector(".var-cname");
|
||||||
|
this._el.name.addEventListener("input", () => {
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
|
||||||
|
restoreData(d) {
|
||||||
|
this.querySelector(".var-name").value = d.name || "";
|
||||||
|
this.querySelector(".var-value").value = d.value || "false";
|
||||||
|
this.querySelector(".var-comment").value = d.comment || "";
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const name = toCName(this._el.name.value) || "flag";
|
||||||
|
const raw = this._el.value.value;
|
||||||
|
const numeric = raw === "true" ? 1 : 0;
|
||||||
|
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||||
|
return `${prefix}int ${name} = ${numeric};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCScalar — typed scalar variable with quantity, unit, and SI conversion ──
|
||||||
|
|
||||||
|
class WCScalar extends WCBlock {
|
||||||
|
get dataType() { return "scalar"; }
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".var-name");
|
||||||
|
this._el.value = this.querySelector(".var-value");
|
||||||
|
this._el.unit = this.querySelector(".var-unit");
|
||||||
|
this._el.type = this.querySelector(".var-type");
|
||||||
|
this._el.si = this.querySelector(".var-si");
|
||||||
|
this._el.comment = this.querySelector(".var-comment");
|
||||||
|
this._el.cname = this.querySelector(".var-cname");
|
||||||
|
|
||||||
|
this._el.name.addEventListener("input", () => {
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshQuantityOptions();
|
||||||
|
refreshUnitOptions();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
this._el.value.addEventListener("input", () => {
|
||||||
|
this.syncUnitFromValue();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
});
|
||||||
|
this._el.unit.addEventListener("change", () => {
|
||||||
|
this.syncValueFromUnit();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
});
|
||||||
|
this._el.type.addEventListener("change", () => {
|
||||||
|
refreshUnitOptions();
|
||||||
|
this.updateSiDisplay();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parseValue() {
|
||||||
|
return splitValueAndUnit(this._el.value.value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
siValue() {
|
||||||
|
const { numVal, unitSuffix } = this.parseValue();
|
||||||
|
if (!unitSuffix) return numVal;
|
||||||
|
const quantityName = this._el.type.value;
|
||||||
|
for (const u of $id("data-area").querySelectorAll("wc-unit")) {
|
||||||
|
if (
|
||||||
|
u.querySelector(".unit-type").value === quantityName &&
|
||||||
|
u.querySelector(".unit-suffix").value === unitSuffix
|
||||||
|
) {
|
||||||
|
return numVal * u.getSiFactor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncUnitFromValue() {
|
||||||
|
const { unitSuffix } = this.parseValue();
|
||||||
|
if (unitSuffix && [...this._el.unit.options].some(o => o.value === unitSuffix)) {
|
||||||
|
this._el.unit.value = unitSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncValueFromUnit() {
|
||||||
|
const unitSuffix = this._el.unit.value;
|
||||||
|
const { numVal } = this.parseValue();
|
||||||
|
const raw = (this._el.value.value || "0").trim();
|
||||||
|
if (unitSuffix && raw.split(/\s+/).length <= 1 && !isNaN(parseFloat(raw))) {
|
||||||
|
this._el.value.value = numVal + " " + unitSuffix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSiDisplay() {
|
||||||
|
const typeName = this._el.type.value;
|
||||||
|
const { unitSuffix } = this.parseValue();
|
||||||
|
if (unitSuffix && typeName) {
|
||||||
|
const si = this.siValue();
|
||||||
|
const defaultUnit = getDefaultUnitSuffix(typeName);
|
||||||
|
this._el.si.textContent = defaultUnit
|
||||||
|
? `= ${si} ${defaultUnit}`
|
||||||
|
: `= ${si} SI`;
|
||||||
|
} else {
|
||||||
|
this._el.si.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
return {
|
||||||
|
name: this._el.name.value,
|
||||||
|
value: this._el.value.value,
|
||||||
|
type: this._el.type.value,
|
||||||
|
unit: this._el.unit.value,
|
||||||
|
comment: this._el.comment.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
this.querySelector(".var-name").value = d.name || "";
|
||||||
|
this.querySelector(".var-value").value = d.value || "";
|
||||||
|
this.querySelector(".var-type").value = d.type || "";
|
||||||
|
this.querySelector(".var-unit").value = d.unit || "";
|
||||||
|
this.querySelector(".var-comment").value = d.comment || "";
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const name = toCName(this._el.name.value) || "x";
|
||||||
|
const typeName = this._el.type.value;
|
||||||
|
const si = this.siValue();
|
||||||
|
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||||
|
return typeName
|
||||||
|
? `${prefix}${typeName} ${name} = ${si};`
|
||||||
|
: `${prefix}double ${name} = ${si};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCString — string variable ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
class WCString extends WCBlock {
|
||||||
|
get dataType() { return "string"; }
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".var-name");
|
||||||
|
this._el.value = this.querySelector(".var-value");
|
||||||
|
this._el.comment = this.querySelector(".var-comment");
|
||||||
|
this._el.cname = this.querySelector(".var-cname");
|
||||||
|
this._el.name.addEventListener("input", () => {
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() { return { name: this._el.name.value, value: this._el.value.value, comment: this._el.comment.value }; }
|
||||||
|
restoreData(d) {
|
||||||
|
this.querySelector(".var-name").value = d.name || "";
|
||||||
|
this.querySelector(".var-value").value = d.value || "";
|
||||||
|
this.querySelector(".var-comment").value = d.comment || "";
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const name = toCName(this._el.name.value) || "s";
|
||||||
|
const raw = this._el.value.value || "";
|
||||||
|
const escaped = escapeString(raw);
|
||||||
|
const len = raw.length;
|
||||||
|
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||||
|
return `${prefix}string ${name} = {0};\n${name}.length = ${len};\n${name}.data = malloc(${len} + 1);\nmemcpy(${name}.data, "${escaped}", ${len});\n${name}.data[${len}] = 0;\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCArray — typed C array variable ───────────────────────────────────────
|
||||||
|
|
||||||
|
class WCArray extends WCBlock {
|
||||||
|
get dataType() { return "array"; }
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._values = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".var-name");
|
||||||
|
this._el.type = this.querySelector(".array-type");
|
||||||
|
this._el.elements = this.querySelector(".array-elements");
|
||||||
|
this._el.count = this.querySelector(".array-count");
|
||||||
|
this._el.comment = this.querySelector(".var-comment");
|
||||||
|
this._el.cname = this.querySelector(".var-cname");
|
||||||
|
|
||||||
|
this._el.name.addEventListener("input", () => {
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
refreshVarRefOptions();
|
||||||
|
});
|
||||||
|
setTimeout(() => this._renderElements(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderElements() {
|
||||||
|
const focused = this._el.elements.querySelector("input:focus");
|
||||||
|
let focusIdx = -1, focusOff = 0;
|
||||||
|
if (focused) {
|
||||||
|
const row = focused.closest(".array-element-row");
|
||||||
|
if (row && row.dataset.idx) {
|
||||||
|
focusIdx = parseInt(row.dataset.idx);
|
||||||
|
focusOff = focused.selectionStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._el.elements.innerHTML = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < this._values.length; i++) {
|
||||||
|
this._el.elements.appendChild(this._makeElementRow(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._el.elements.appendChild(this._makeNewRow());
|
||||||
|
this._updateCount();
|
||||||
|
|
||||||
|
if (focusIdx >= 0 && focusIdx < this._values.length) {
|
||||||
|
const row = this._el.elements.querySelector(`[data-idx="${focusIdx}"]`);
|
||||||
|
if (row) {
|
||||||
|
const inp = row.querySelector(".array-element-value");
|
||||||
|
if (inp) { inp.focus(); inp.selectionStart = inp.selectionEnd = Math.min(focusOff, inp.value.length); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeElementRow(i) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "array-element-row";
|
||||||
|
row.dataset.idx = i;
|
||||||
|
|
||||||
|
const idxSpan = document.createElement("span");
|
||||||
|
idxSpan.className = "array-element-idx";
|
||||||
|
idxSpan.textContent = i;
|
||||||
|
row.appendChild(idxSpan);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.className = "array-element-value";
|
||||||
|
input.value = this._values[i];
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
const ix = parseInt(input.closest(".array-element-row").dataset.idx);
|
||||||
|
this._values[ix] = input.value;
|
||||||
|
});
|
||||||
|
row.appendChild(input);
|
||||||
|
|
||||||
|
const delBtn = document.createElement("button");
|
||||||
|
delBtn.className = "array-element-del";
|
||||||
|
delBtn.textContent = "\u00D7";
|
||||||
|
delBtn.title = "remove";
|
||||||
|
delBtn.addEventListener("click", () => {
|
||||||
|
const ix = parseInt(delBtn.closest(".array-element-row").dataset.idx);
|
||||||
|
this._removeElement(ix);
|
||||||
|
});
|
||||||
|
row.appendChild(delBtn);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
_makeNewRow() {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "array-element-row";
|
||||||
|
|
||||||
|
const idxSpan = document.createElement("span");
|
||||||
|
idxSpan.className = "array-element-idx";
|
||||||
|
idxSpan.textContent = this._values.length;
|
||||||
|
row.appendChild(idxSpan);
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.className = "array-element-value";
|
||||||
|
input.placeholder = "new element";
|
||||||
|
input.addEventListener("keydown", e => {
|
||||||
|
if (e.key === "Enter" && input.value.trim()) {
|
||||||
|
const val = input.value.trim();
|
||||||
|
input.value = "";
|
||||||
|
this._addElement(val);
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener("blur", () => {
|
||||||
|
if (input.value.trim()) {
|
||||||
|
const val = input.value.trim();
|
||||||
|
input.value = "";
|
||||||
|
this._addElement(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
row.appendChild(input);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
_addElement(value) {
|
||||||
|
this._values.push(value);
|
||||||
|
this._renderElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeElement(idx) {
|
||||||
|
this._values.splice(idx, 1);
|
||||||
|
this._renderElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateCount() {
|
||||||
|
const n = this._values.length;
|
||||||
|
this._el.count.textContent = n + " element" + (n === 1 ? "" : "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
return {
|
||||||
|
name: this._el.name.value,
|
||||||
|
type: this._el.type.value,
|
||||||
|
values: [...this._values],
|
||||||
|
comment: this._el.comment.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
this._el.name.value = d.name || "arr";
|
||||||
|
this._el.type.value = d.type || "float";
|
||||||
|
this._el.comment.value = d.comment || "";
|
||||||
|
this._el.cname.textContent = toCName(this._el.name.value);
|
||||||
|
if (Array.isArray(d.values)) {
|
||||||
|
this._values = [...d.values];
|
||||||
|
} else if (typeof d.values === "string" && d.values) {
|
||||||
|
this._values = d.values.split(",").map(s => s.trim()).filter(s => s);
|
||||||
|
} else {
|
||||||
|
this._values = [];
|
||||||
|
}
|
||||||
|
this._renderElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const name = toCName(this._el.name.value) || "arr";
|
||||||
|
const type = this._el.type.value || "float";
|
||||||
|
const elements = this._values.filter(s => s).join(", ");
|
||||||
|
const prefix = commentPrefix(this._el.name.value, this._el.comment.value);
|
||||||
|
if (elements) {
|
||||||
|
return `${prefix}${type} ${name}[] = {${elements}};\n`;
|
||||||
|
}
|
||||||
|
return `${prefix}${type} ${name}[1];\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// ── Procedure block definitions ────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Blocks that define and call procedures:
|
||||||
|
// WCProcedure — procedure definition (lives in Procedures tab)
|
||||||
|
// WCProcCall — procedure call block (lives in Algorithm tab)
|
||||||
|
//
|
||||||
|
// Each extends WCBlock (from wcblock.js).
|
||||||
|
//
|
||||||
|
// Load order dependencies:
|
||||||
|
// wcblock.js → WCBlock base class
|
||||||
|
// helpers.js → refreshProcOptions, findDataSource, resolveSourceName
|
||||||
|
// dom.js → $id
|
||||||
|
// draw.js → deleteLink
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCProcedure — procedure definition ─────────────────────────────────────
|
||||||
|
|
||||||
|
class WCProcedure extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.name = this.querySelector(".proc-name");
|
||||||
|
this._el.inputs = this.querySelector(".proc-inputs");
|
||||||
|
this._el.outputs = this.querySelector(".proc-outputs");
|
||||||
|
|
||||||
|
this.querySelector(".proc-add-input").addEventListener("click", () => {
|
||||||
|
this._addParamRow(this._el.inputs, "in");
|
||||||
|
});
|
||||||
|
this.querySelector(".proc-add-output").addEventListener("click", () => {
|
||||||
|
this._addParamRow(this._el.outputs, "out");
|
||||||
|
});
|
||||||
|
|
||||||
|
this._el.name.addEventListener("input", () => refreshProcOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
_addParamRow(container, dir) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "proc-param-row";
|
||||||
|
row.innerHTML =
|
||||||
|
`<input class="proc-param-name" placeholder="name" value="">` +
|
||||||
|
`<input class="proc-param-type" placeholder="type" value="scalar">` +
|
||||||
|
`<button class="proc-param-del" title="remove">×</button>`;
|
||||||
|
row.querySelector(".proc-param-del").addEventListener("click", () => {
|
||||||
|
row.remove();
|
||||||
|
refreshProcOptions();
|
||||||
|
});
|
||||||
|
for (const inp of row.querySelectorAll("input")) {
|
||||||
|
inp.addEventListener("input", () => refreshProcOptions());
|
||||||
|
}
|
||||||
|
container.appendChild(row);
|
||||||
|
refreshProcOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLibrary() {
|
||||||
|
const pane = this.closest(".lib-pane");
|
||||||
|
return pane ? pane.dataset.libName : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
getQualifiedName() {
|
||||||
|
return this.getLibrary() + "/" + (this._el.name.value || "unnamed");
|
||||||
|
}
|
||||||
|
|
||||||
|
getInputParams() {
|
||||||
|
return this._getParams(this._el.inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOutputParams() {
|
||||||
|
return this._getParams(this._el.outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getParams(container) {
|
||||||
|
return Array.from(container.querySelectorAll(".proc-param-row")).map(row => ({
|
||||||
|
name: row.querySelector(".proc-param-name").value || "arg",
|
||||||
|
type: row.querySelector(".proc-param-type").value || "scalar"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
return {
|
||||||
|
library: this.getLibrary(),
|
||||||
|
name: this._el.name.value,
|
||||||
|
inputs: this.getInputParams(),
|
||||||
|
outputs: this.getOutputParams()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
this._el.name.value = d.name || "proc";
|
||||||
|
for (const p of (d.inputs || [])) {
|
||||||
|
this._addParamRow(this._el.inputs, "in");
|
||||||
|
const rows = this._el.inputs.querySelectorAll(".proc-param-row");
|
||||||
|
const last = rows[rows.length - 1];
|
||||||
|
last.querySelector(".proc-param-name").value = p.name || "arg";
|
||||||
|
last.querySelector(".proc-param-type").value = p.type || "scalar";
|
||||||
|
}
|
||||||
|
for (const p of (d.outputs || [])) {
|
||||||
|
this._addParamRow(this._el.outputs, "out");
|
||||||
|
const rows = this._el.outputs.querySelectorAll(".proc-param-row");
|
||||||
|
const last = rows[rows.length - 1];
|
||||||
|
last.querySelector(".proc-param-name").value = p.name || "arg";
|
||||||
|
last.querySelector(".proc-param-type").value = p.type || "scalar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── WCProcCall — procedure call block ──────────────────────────────────────
|
||||||
|
|
||||||
|
class WCProcCall extends WCBlock {
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._el.select = this.querySelector(".proccall-select");
|
||||||
|
this._el.nameInput = this.querySelector(".proccall-name");
|
||||||
|
this._el.inputs = this.querySelector(".proccall-inputs");
|
||||||
|
this._el.outputs = this.querySelector(".proccall-outputs");
|
||||||
|
|
||||||
|
this._el.select.addEventListener("change", () => this._rebuildParams());
|
||||||
|
this._initTimer = setTimeout(() => this._rebuildParams(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebuildParams() {
|
||||||
|
const qname = this._el.select.dataset.restoredValue || this._el.select.value;
|
||||||
|
|
||||||
|
for (const container of [this._el.inputs, this._el.outputs]) {
|
||||||
|
for (const hook of container.querySelectorAll(".hook")) {
|
||||||
|
const paths = [...document.querySelectorAll("path.flow, path.flow-data")];
|
||||||
|
for (const path of paths) {
|
||||||
|
if (path.fromHook === hook || path.toHook === hook) {
|
||||||
|
deleteLink(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._el.inputs.innerHTML = "";
|
||||||
|
this._el.outputs.innerHTML = "";
|
||||||
|
|
||||||
|
if (!qname) return;
|
||||||
|
|
||||||
|
for (const proc of document.querySelectorAll("wc-procedure")) {
|
||||||
|
if (proc.getQualifiedName() === qname) {
|
||||||
|
for (const p of proc.getInputParams()) {
|
||||||
|
this._createArgField(this._el.inputs, p, "in");
|
||||||
|
}
|
||||||
|
for (const p of proc.getOutputParams()) {
|
||||||
|
this._createArgField(this._el.outputs, p, "out");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createArgField(container, param, dir) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "proccall-arg-row";
|
||||||
|
|
||||||
|
const label = document.createElement("span");
|
||||||
|
label.className = "proccall-arg-label";
|
||||||
|
label.textContent = param.name + ":";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.className = "proccall-arg-input";
|
||||||
|
input.placeholder = param.name;
|
||||||
|
input.dataset.dir = dir;
|
||||||
|
input.dataset.paramName = param.name;
|
||||||
|
input.dataset.paramType = param.type;
|
||||||
|
|
||||||
|
const hook = document.createElement("button");
|
||||||
|
hook.className = "hook";
|
||||||
|
hook.dataset.direction = dir === "in" ? "in" : "out";
|
||||||
|
hook.dataset.linkType = "data";
|
||||||
|
hook.dataset.type = param.type;
|
||||||
|
hook.dataset.exit = dir === "in" ? "left" : "right";
|
||||||
|
hook.title = (dir === "in" ? "expects " : "") + param.type;
|
||||||
|
|
||||||
|
if (dir === "in") {
|
||||||
|
row.appendChild(hook);
|
||||||
|
row.appendChild(label);
|
||||||
|
row.appendChild(input);
|
||||||
|
} else {
|
||||||
|
row.appendChild(label);
|
||||||
|
row.appendChild(input);
|
||||||
|
row.appendChild(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(row);
|
||||||
|
this._setupHook(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sourceName(block) {
|
||||||
|
const name = resolveSourceName(block);
|
||||||
|
if (!name) return "";
|
||||||
|
return block.dataType === "string" ? name + ".data" : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateOwnCode() {
|
||||||
|
const qname = this._el.select.value;
|
||||||
|
if (!qname) return "";
|
||||||
|
const parts = qname.split("/");
|
||||||
|
const procName = parts[1] || qname;
|
||||||
|
|
||||||
|
const inputs = [];
|
||||||
|
const outputs = [];
|
||||||
|
for (const row of this._el.inputs.querySelectorAll(".proccall-arg-row")) {
|
||||||
|
const hook = row.querySelector(".hook");
|
||||||
|
const inp = row.querySelector(".proccall-arg-input");
|
||||||
|
const source = hook ? findDataSource(hook) : null;
|
||||||
|
inputs.push(source ? this._sourceName(source) : (inp.value || inp.placeholder));
|
||||||
|
}
|
||||||
|
for (const row of this._el.outputs.querySelectorAll(".proccall-arg-row")) {
|
||||||
|
const inp = row.querySelector(".proccall-arg-input");
|
||||||
|
outputs.push("&" + (inp.value || inp.placeholder));
|
||||||
|
}
|
||||||
|
const args = [...inputs, ...outputs].join(", ");
|
||||||
|
return `\n${procName}(${args});\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
const args = {};
|
||||||
|
for (const inp of this.querySelectorAll(".proccall-arg-input")) {
|
||||||
|
args[inp.dataset.dir + ":" + inp.dataset.paramName] = inp.value;
|
||||||
|
}
|
||||||
|
return { selected: this._el.select.value, name: this._el.nameInput.value, args };
|
||||||
|
}
|
||||||
|
restoreData(d) {
|
||||||
|
if (this._initTimer) { clearTimeout(this._initTimer); this._initTimer = null; }
|
||||||
|
this._el.select.dataset.restoredValue = d.selected || "";
|
||||||
|
this._el.select.value = d.selected || "";
|
||||||
|
this._el.nameInput.value = d.name || "";
|
||||||
|
this._rebuildParams();
|
||||||
|
if (d.args) {
|
||||||
|
for (const inp of this.querySelectorAll(".proccall-arg-input")) {
|
||||||
|
const key = inp.dataset.dir + ":" + inp.dataset.paramName;
|
||||||
|
if (d.args[key] !== undefined) inp.value = d.args[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// ── Global constants & DOM shortcuts ──────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Provides app-wide constants and a concise DOM query shortcut.
|
||||||
|
// This module depends on nothing and is loaded first.
|
||||||
|
//
|
||||||
|
// Globals provided:
|
||||||
|
// $ → document.querySelector() bound once
|
||||||
|
// $id → document.getElementById() bound once
|
||||||
|
// svgNamespace → SVG namespace URI for createElementNS
|
||||||
|
// STORAGE_KEY → localStorage key used by persist.js
|
||||||
|
|
||||||
|
/** Shorthand for document.querySelector, bound once. */
|
||||||
|
const $ = document.querySelector.bind(document);
|
||||||
|
|
||||||
|
/** Shorthand for document.getElementById, bound once. */
|
||||||
|
const $id = document.getElementById.bind(document);
|
||||||
|
|
||||||
|
/** SVG namespace URI, required when creating SVG elements via JS. */
|
||||||
|
const svgNamespace = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
/** localStorage key for workspace persistence (save/load/new). */
|
||||||
|
const STORAGE_KEY = "workspace";
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// ── SVG connection drawing, direction routing, shadow link & deletion ──────────
|
||||||
|
//
|
||||||
|
// Manages the two-click link system between block hooks, draws polyline SVG
|
||||||
|
// paths constrained to {0°, 45°, 90°} angles, renders a live shadow path
|
||||||
|
// from the selected output hook to the cursor while linking, and handles
|
||||||
|
// link deletion with full block array cleanup.
|
||||||
|
//
|
||||||
|
// Depends on: dom.js (svgNamespace, $, $id)
|
||||||
|
//
|
||||||
|
|
||||||
|
const EXT = 25; // pixels the path extends from a hook in its exit direction
|
||||||
|
let linkageFirst = null;
|
||||||
|
let shadowPath = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes a 45°-constrained polyline between two points.
|
||||||
|
* Returns the SVG path data (everything after the initial "M") as a string
|
||||||
|
* suitable for appending to an existing moveto.
|
||||||
|
* @param {number} x1 - start x
|
||||||
|
* @param {number} y1 - start y
|
||||||
|
* @param {number} x2 - end x
|
||||||
|
* @param {number} y2 - end y
|
||||||
|
* @returns {string} SVG path data segment (e.g. "L 100 50 L 150 50")
|
||||||
|
*/
|
||||||
|
function routeBetween(x1, y1, x2, y2) {
|
||||||
|
const dx = x2 - x1, dy = y2 - y1;
|
||||||
|
const adx = Math.abs(dx), ady = Math.abs(dy);
|
||||||
|
|
||||||
|
if (adx === 0 && ady === 0) return `L ${x2} ${y2}`;
|
||||||
|
|
||||||
|
const sx = dx >= 0 ? 1 : -1;
|
||||||
|
const sy = dy >= 0 ? 1 : -1;
|
||||||
|
const R45 = Math.PI / 4, R90 = Math.PI / 2, TOL = 5 * Math.PI / 180;
|
||||||
|
const normAngle = (() => {
|
||||||
|
const raw = Math.abs(Math.atan2(dy, dx));
|
||||||
|
return raw > R90 ? Math.PI - raw : raw;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(normAngle) < TOL ||
|
||||||
|
Math.abs(normAngle - R45) < TOL ||
|
||||||
|
Math.abs(normAngle - R90) < TOL
|
||||||
|
) {
|
||||||
|
return `L ${x2} ${y2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adx >= ady) {
|
||||||
|
const x1m = x1 + sx * ((adx - ady) / 2);
|
||||||
|
const x2m = x1m + sx * ady;
|
||||||
|
return `L ${x1m} ${y1} L ${x2m} ${y2} L ${x2} ${y2}`;
|
||||||
|
} else {
|
||||||
|
const y1m = y1 + sy * ((ady - adx) / 2);
|
||||||
|
const y2m = y1m + sy * adx;
|
||||||
|
return `L ${x1} ${y1m} L ${x2} ${y2m} L ${x2} ${y2}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a hook DOM element, preferring the explicitly stored reference
|
||||||
|
* and falling back to the block's first .hook child.
|
||||||
|
* @param {SVGPathElement} path
|
||||||
|
* @param {string} which - "from" or "to"
|
||||||
|
* @returns {HTMLElement} the hook button
|
||||||
|
*/
|
||||||
|
function resolveHook(path, which) {
|
||||||
|
const key = which === "from" ? "fromHook" : "toHook";
|
||||||
|
const elKey = which === "from" ? "fromElement" : "toElement";
|
||||||
|
return path[key] || path[elKey].querySelector(".hook");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the extended control point for a hook: the hook centre pushed
|
||||||
|
* `EXT` pixels in the hook's declared exit direction.
|
||||||
|
* @param {HTMLElement} hook - the hook button element
|
||||||
|
* @param {DOMRect} svgRect - bounding rect of the SVG viewport
|
||||||
|
* @returns {{ x: number, y: number }}
|
||||||
|
*/
|
||||||
|
function getHookExit(hook, svgRect) {
|
||||||
|
const r = hook.getBoundingClientRect();
|
||||||
|
const x = (r.left + r.right) / 2 - svgRect.left;
|
||||||
|
const y = (r.top + r.bottom) / 2 - svgRect.top;
|
||||||
|
const dir = hook.dataset.exit;
|
||||||
|
if (dir === "right") return { x: x + EXT, y };
|
||||||
|
if (dir === "left") return { x: x - EXT, y };
|
||||||
|
if (dir === "down") return { x, y: y + EXT };
|
||||||
|
if (dir === "up") return { x, y: y - EXT };
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws (or redraws) an SVG connection path between two linked block hooks.
|
||||||
|
* Each segment starts by extending EXT pixels in the source hook's exit
|
||||||
|
* direction and ends by approaching from the target hook's exit direction.
|
||||||
|
* The middle portion uses 45°-constrained routing.
|
||||||
|
* @param {SVGPathElement} path - path element with fromHook / toHook references
|
||||||
|
*/
|
||||||
|
function drawpath(path) {
|
||||||
|
const svg = path.closest("svg");
|
||||||
|
const svgRect = svg.getBoundingClientRect();
|
||||||
|
|
||||||
|
const fromHook = resolveHook(path, "from");
|
||||||
|
const toHook = resolveHook(path, "to");
|
||||||
|
const rFrom = fromHook.getBoundingClientRect();
|
||||||
|
const rTo = toHook.getBoundingClientRect();
|
||||||
|
|
||||||
|
const fx = (rFrom.left + rFrom.right) / 2 - svgRect.left;
|
||||||
|
const fy = (rFrom.top + rFrom.bottom) / 2 - svgRect.top;
|
||||||
|
const tx = (rTo.left + rTo.right) / 2 - svgRect.left;
|
||||||
|
const ty = (rTo.top + rTo.bottom) / 2 - svgRect.top;
|
||||||
|
|
||||||
|
const srcExit = getHookExit(fromHook, svgRect);
|
||||||
|
const tgtExit = getHookExit(toHook, svgRect);
|
||||||
|
|
||||||
|
path.setAttribute("d",
|
||||||
|
`M ${fx} ${fy} L ${srcExit.x} ${srcExit.y}` +
|
||||||
|
routeBetween(srcExit.x, srcExit.y, tgtExit.x, tgtExit.y) +
|
||||||
|
` L ${tx} ${ty}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Shadow (preview) link ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins showing a dashed shadow path from the given hook to the cursor.
|
||||||
|
* @param {HTMLElement} hook - the source hook button
|
||||||
|
*/
|
||||||
|
function startShadowLink(hook) {
|
||||||
|
if (shadowPath) endShadowLink();
|
||||||
|
const svg = $id("connections");
|
||||||
|
shadowPath = document.createElementNS(svgNamespace, "path");
|
||||||
|
shadowPath.classList.add(hook.dataset.linkType === "data" ? "flow-shadow-data" : "flow-shadow");
|
||||||
|
shadowPath._hook = hook;
|
||||||
|
svg.appendChild(shadowPath);
|
||||||
|
document.addEventListener("mousemove", onShadowMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the shadow path endpoint to follow the mouse. */
|
||||||
|
function onShadowMove(ev) {
|
||||||
|
if (!shadowPath) return;
|
||||||
|
const svg = shadowPath.closest("svg");
|
||||||
|
const svgRect = svg.getBoundingClientRect();
|
||||||
|
const mx = ev.clientX - svgRect.left;
|
||||||
|
const my = ev.clientY - svgRect.top;
|
||||||
|
|
||||||
|
const hook = shadowPath._hook;
|
||||||
|
const rHook = hook.getBoundingClientRect();
|
||||||
|
const fx = (rHook.left + rHook.right) / 2 - svgRect.left;
|
||||||
|
const fy = (rHook.top + rHook.bottom) / 2 - svgRect.top;
|
||||||
|
|
||||||
|
const exit = getHookExit(hook, svgRect);
|
||||||
|
|
||||||
|
shadowPath.setAttribute("d",
|
||||||
|
`M ${fx} ${fy} L ${exit.x} ${exit.y}` +
|
||||||
|
routeBetween(exit.x, exit.y, mx, my)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes the shadow path and stops tracking the mouse. */
|
||||||
|
function endShadowLink() {
|
||||||
|
if (shadowPath) {
|
||||||
|
shadowPath.remove();
|
||||||
|
shadowPath = null;
|
||||||
|
}
|
||||||
|
document.removeEventListener("mousemove", onShadowMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Cancels any pending link operation and removes the shadow. */
|
||||||
|
function resetLinkage() {
|
||||||
|
linkageFirst = null;
|
||||||
|
endShadowLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Link deletion ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an SVG connection path and cleans up both blocks' flow arrays.
|
||||||
|
* @param {SVGPathElement} path - the flow path element to delete
|
||||||
|
*/
|
||||||
|
function deleteLink(path) {
|
||||||
|
if (!path.classList.contains("flow") && !path.classList.contains("flow-data")) return;
|
||||||
|
|
||||||
|
const src = path.fromElement;
|
||||||
|
const tgt = path.toElement;
|
||||||
|
|
||||||
|
if (src && src.wcNextFlow) {
|
||||||
|
const i = src.wcNextFlow.indexOf(path);
|
||||||
|
if (i !== -1) src.wcNextFlow.splice(i, 1);
|
||||||
|
}
|
||||||
|
if (tgt && tgt.wcForeFlow) {
|
||||||
|
const i = tgt.wcForeFlow.indexOf(path);
|
||||||
|
if (i !== -1) tgt.wcForeFlow.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
path.remove();
|
||||||
|
|
||||||
|
path.fromElement = null;
|
||||||
|
path.toElement = null;
|
||||||
|
path.fromHook = null;
|
||||||
|
path.toHook = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// ── Error modal ───────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// A simple overlay modal that displays -ACK! with a custom message.
|
||||||
|
// Depends on: dom.js ($)
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the error modal with the given message.
|
||||||
|
* The overlay is absolutely positioned and blocks all interaction until dismissed.
|
||||||
|
* @param {string} msg - message to display inside the error box
|
||||||
|
*/
|
||||||
|
function showError(msg) {
|
||||||
|
const el = $("#error-overlay");
|
||||||
|
el.querySelector(".error-msg").textContent = msg;
|
||||||
|
el.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the error modal. Triggered by clicking the × button,
|
||||||
|
* the overlay backdrop, or called programmatically.
|
||||||
|
* @param {MouseEvent} [ev] - event to check target matching
|
||||||
|
*/
|
||||||
|
function closeError(ev) {
|
||||||
|
if (
|
||||||
|
!ev ||
|
||||||
|
ev.target === ev.currentTarget ||
|
||||||
|
ev.target.classList.contains("error-close")
|
||||||
|
) {
|
||||||
|
$("#error-overlay").classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
// ── Utility functions & dropdown refresh ─────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Pure helper functions shared across blocks, persistence, and app modules.
|
||||||
|
// Depends on: dom.js ($, $id)
|
||||||
|
//
|
||||||
|
// Exports:
|
||||||
|
// splitValueAndUnit() → parse "5 km" → {numVal, unitSuffix}
|
||||||
|
// rebuildSelect() → replace <select> options preserving selection
|
||||||
|
// refreshQuantityOptions() → sync quantity-name dropdowns + "boolean"
|
||||||
|
// refreshUnitOptions() → sync unit-suffix dropdowns from unit blocks
|
||||||
|
// getDefaultUnitSuffix() → lookup default unit for a quantity name
|
||||||
|
// refreshDefaultUnitOptions() → sync default-unit dropdowns on quantity blocks
|
||||||
|
// refreshVarRefOptions() → sync variable-select dropdowns
|
||||||
|
// refreshProcOptions() → sync procedure-select dropdowns
|
||||||
|
// switchLibTab() → activate library sub-tab
|
||||||
|
// findDataSource() → find block connected to a data hook
|
||||||
|
// escapeString() → escape special chars for C string literal
|
||||||
|
// refreshAll() → run all refresh passes in order
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string like "5 km" into a numeric value and an optional unit suffix.
|
||||||
|
* Falls back to parsing the entire string as a bare number when no suffix is present.
|
||||||
|
* @param {string} str - e.g. "5 km", "42", ""
|
||||||
|
* @param {number} defaultNum - fallback when parseFloat yields NaN
|
||||||
|
* @returns {{ numVal: number, unitSuffix: string|null }}
|
||||||
|
*/
|
||||||
|
function splitValueAndUnit(str, defaultNum) {
|
||||||
|
const trimmed = (str || "").trim();
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
let numVal, unitSuffix;
|
||||||
|
if (parts.length < 2 || isNaN(parseFloat(parts[0]))) {
|
||||||
|
numVal = parseFloat(trimmed);
|
||||||
|
unitSuffix = null;
|
||||||
|
} else {
|
||||||
|
numVal = parseFloat(parts[0]);
|
||||||
|
unitSuffix = parts.slice(1).join(" ");
|
||||||
|
}
|
||||||
|
return { numVal: isNaN(numVal) ? defaultNum : numVal, unitSuffix };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all options in a <select>, preserving the current selection if it still exists.
|
||||||
|
* @param {HTMLSelectElement} sel - the select element to rebuild
|
||||||
|
* @param {string} placeholder - text for the first (empty-value) option
|
||||||
|
* @param {string[]} values - option value/label pairs to add
|
||||||
|
* @param {string} current - currently selected value to attempt to preserve
|
||||||
|
*/
|
||||||
|
function rebuildSelect(sel, placeholder, values, current) {
|
||||||
|
sel.innerHTML = `<option value="">${placeholder}</option>`;
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
for (const v of values) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = v;
|
||||||
|
opt.textContent = v;
|
||||||
|
frag.appendChild(opt);
|
||||||
|
}
|
||||||
|
sel.appendChild(frag);
|
||||||
|
if (current && [...sel.options].some(o => o.value === current)) {
|
||||||
|
sel.value = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes every quantity-name dropdown (.var-type, .unit-type) by scanning
|
||||||
|
* all wc-quantity blocks in the data area.
|
||||||
|
*/
|
||||||
|
function refreshQuantityOptions() {
|
||||||
|
const qNames = Array.from(
|
||||||
|
$id("data-area").querySelectorAll("wc-quantity")
|
||||||
|
).map(q => (q.querySelector(".quantity-name").value || "unnamed"));
|
||||||
|
|
||||||
|
document.querySelectorAll(
|
||||||
|
"#variable-area .var-type, #data-area .unit-type"
|
||||||
|
).forEach(sel => {
|
||||||
|
const placeholder = sel.classList.contains("var-type")
|
||||||
|
? "(no type)" : "(select quantity)";
|
||||||
|
rebuildSelect(sel, placeholder, qNames, sel.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the unit-suffix dropdown on every datum block to show only units
|
||||||
|
* that match the datum's currently selected quantity.
|
||||||
|
*/
|
||||||
|
function refreshUnitOptions() {
|
||||||
|
$id("variable-area").querySelectorAll("wc-scalar").forEach(d => {
|
||||||
|
const quantityName = d.querySelector(".var-type").value;
|
||||||
|
const sel = d.querySelector(".var-unit");
|
||||||
|
const suffixes = Array.from(
|
||||||
|
$id("data-area").querySelectorAll("wc-unit")
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
u =>
|
||||||
|
u.querySelector(".unit-type").value === quantityName &&
|
||||||
|
u.querySelector(".unit-suffix").value
|
||||||
|
)
|
||||||
|
.map(u => u.querySelector(".unit-suffix").value);
|
||||||
|
rebuildSelect(sel, "(unit)", suffixes, sel.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the default unit suffix for a given quantity name.
|
||||||
|
* @param {string} quantityName
|
||||||
|
* @returns {string} default unit suffix, or "" if not found
|
||||||
|
*/
|
||||||
|
function getDefaultUnitSuffix(quantityName) {
|
||||||
|
for (const q of $id("data-area").querySelectorAll("wc-quantity")) {
|
||||||
|
if ((q.querySelector(".quantity-name").value || "unnamed") === quantityName) {
|
||||||
|
return q.querySelector(".quantity-default-unit").value || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the default-unit dropdown on every quantity block with suffixes
|
||||||
|
* of matching unit blocks.
|
||||||
|
*/
|
||||||
|
function refreshDefaultUnitOptions() {
|
||||||
|
$id("data-area").querySelectorAll("wc-quantity").forEach(q => {
|
||||||
|
const quantityName = q.querySelector(".quantity-name").value || "unnamed";
|
||||||
|
const sel = q.querySelector(".quantity-default-unit");
|
||||||
|
const suffixes = Array.from(
|
||||||
|
$id("data-area").querySelectorAll("wc-unit")
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
u =>
|
||||||
|
u.querySelector(".unit-type").value === quantityName &&
|
||||||
|
u.querySelector(".unit-suffix").value
|
||||||
|
)
|
||||||
|
.map(u => u.querySelector(".unit-suffix").value);
|
||||||
|
rebuildSelect(sel, "(none)", suffixes, sel.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the variable-select dropdown on every wc-varref block in the
|
||||||
|
* algorithm pane. Scans all boolean, scalar, string, and array blocks.
|
||||||
|
*/
|
||||||
|
function refreshVarRefOptions() {
|
||||||
|
const selects = document.querySelectorAll(".varref-select, .assign-target");
|
||||||
|
if (!selects.length) return;
|
||||||
|
const vars = [];
|
||||||
|
for (const b of $id("variable-area").querySelectorAll("wc-boolean, wc-scalar, wc-string, wc-array")) {
|
||||||
|
const name = (b.querySelector(".var-name") || {}).value || "unnamed";
|
||||||
|
const tag = b.tagName.toLowerCase().replace("wc-", "");
|
||||||
|
vars.push({ id: b.id, label: tag + ": " + name });
|
||||||
|
}
|
||||||
|
for (const sel of selects) {
|
||||||
|
const current = sel.dataset.restoredValue || sel.value;
|
||||||
|
delete sel.dataset.restoredValue;
|
||||||
|
sel.innerHTML = '<option value="">(select variable)</option>';
|
||||||
|
for (const v of vars) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = v.id;
|
||||||
|
opt.textContent = v.label;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
if (current && [...sel.options].some(o => o.value === current)) {
|
||||||
|
sel.value = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the procedure-select dropdown on every wc-proccall block.
|
||||||
|
* Scans all wc-procedure blocks and populates options as "library/name".
|
||||||
|
*/
|
||||||
|
function refreshProcOptions() {
|
||||||
|
const proccalls = document.querySelectorAll("wc-proccall");
|
||||||
|
if (!proccalls.length) return;
|
||||||
|
const options = Array.from(document.querySelectorAll("wc-procedure")).map(p => p.getQualifiedName());
|
||||||
|
for (const call of proccalls) {
|
||||||
|
const sel = call.querySelector(".proccall-select");
|
||||||
|
const current = sel.dataset.restoredValue || sel.value;
|
||||||
|
delete sel.dataset.restoredValue;
|
||||||
|
sel.innerHTML = '<option value="">(select procedure)</option>';
|
||||||
|
for (const opt of options) {
|
||||||
|
const o = document.createElement("option");
|
||||||
|
o.value = opt;
|
||||||
|
o.textContent = opt;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
if (current && [...sel.options].some(o => o.value === current)) {
|
||||||
|
sel.value = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the clicked library sub-tab and shows its corresponding pane.
|
||||||
|
* @param {string} name - library name (matches data-lib-name attribute)
|
||||||
|
*/
|
||||||
|
function switchLibTab(name) {
|
||||||
|
for (const tab of document.querySelectorAll(".lib-tab")) {
|
||||||
|
tab.classList.toggle("active", tab.dataset.libName === name);
|
||||||
|
}
|
||||||
|
for (const pane of document.querySelectorAll(".lib-pane")) {
|
||||||
|
pane.classList.toggle("active", pane.dataset.libName === name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the block connected to a given data hook via any .flow-data path.
|
||||||
|
* @param {HTMLElement} hook - the data hook button element
|
||||||
|
* @returns {HTMLElement|null} the source block, or null if nothing is connected
|
||||||
|
*/
|
||||||
|
function findDataSource(hook) {
|
||||||
|
for (const path of document.querySelectorAll("path.flow-data")) {
|
||||||
|
if (path.toHook === hook) return path.fromElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes special characters for embedding in a C string literal.
|
||||||
|
* Handles backslash, double-quote, newline, carriage return, and tab.
|
||||||
|
* @param {string} str - raw string
|
||||||
|
* @returns {string} escaped string safe for C "..." literals
|
||||||
|
*/
|
||||||
|
function escapeString(str) {
|
||||||
|
return str
|
||||||
|
.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
||||||
|
.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a natural-language name into a valid C identifier.
|
||||||
|
* Lowercases, replaces non-alphanumeric chars with underscores,
|
||||||
|
* collapses runs of underscores, strips leading/trailing underscores,
|
||||||
|
* and prepends an underscore if the result starts with a digit.
|
||||||
|
* @param {string} str - natural name like "My Variable 2"
|
||||||
|
* @returns {string} valid C identifier like "my_variable_2"
|
||||||
|
*/
|
||||||
|
function toCName(str) {
|
||||||
|
let s = (str || "").trim().toLowerCase();
|
||||||
|
s = s.replace(/[^a-z0-9_]/g, "_");
|
||||||
|
s = s.replace(/_+/g, "_");
|
||||||
|
s = s.replace(/^_|_$/g, "");
|
||||||
|
if (/^\d/.test(s)) s = "_" + s;
|
||||||
|
return s || "var";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a C comment prefix from a variable's natural name and an optional comment.
|
||||||
|
* @param {string} naturalName - the human-readable variable name
|
||||||
|
* @param {string} comment - the optional comment text
|
||||||
|
* @returns {string} e.g. "// My Flag: indicates status\n" or "" when no comment
|
||||||
|
*/
|
||||||
|
function commentPrefix(naturalName, comment) {
|
||||||
|
naturalName = (naturalName || "").trim();
|
||||||
|
comment = (comment || "").trim();
|
||||||
|
return comment && naturalName ? `// ${naturalName}: ${comment}\n` : comment ? `// ${comment}\n` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the ultimate C variable name from a block by checking for
|
||||||
|
* .var-name, .expr-name, and following .varref-select chains.
|
||||||
|
* Returns the empty string when nothing is found.
|
||||||
|
* @param {HTMLElement} block - any block that may hold a variable reference
|
||||||
|
* @param {Set} [visited] - cycle detection set (internal use)
|
||||||
|
* @returns {string} the C name, or ""
|
||||||
|
*/
|
||||||
|
function resolveSourceName(block, visited) {
|
||||||
|
visited = visited || new Set();
|
||||||
|
if (visited.has(block)) return "";
|
||||||
|
visited.add(block);
|
||||||
|
|
||||||
|
const ni = block.querySelector(".var-name");
|
||||||
|
if (ni) return toCName(ni.value) || "x";
|
||||||
|
const ei = block.querySelector(".expr-name");
|
||||||
|
if (ei) return ei.value || "res";
|
||||||
|
const vr = block.querySelector(".varref-select");
|
||||||
|
if (vr) {
|
||||||
|
const target = vr.value ? $id(vr.value) : null;
|
||||||
|
if (target) return resolveSourceName(target, visited);
|
||||||
|
return vr.value || "var";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runs all refresh passes in dependency order. */
|
||||||
|
function refreshAll() {
|
||||||
|
refreshQuantityOptions();
|
||||||
|
refreshUnitOptions();
|
||||||
|
refreshDefaultUnitOptions();
|
||||||
|
refreshVarRefOptions();
|
||||||
|
refreshProcOptions();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Icon Attribution
|
||||||
|
|
||||||
|
The SVG icons in this directory are from [Lucide](https://lucide.dev/) v0.454.0, licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
## Icons used
|
||||||
|
|
||||||
|
- git-branch.svg (Algorithm tab)
|
||||||
|
- database.svg (Data tab)
|
||||||
|
- ruler.svg (Measurements tab)
|
||||||
|
- file-text.svg (Procedures tab)
|
||||||
|
- diamond.svg (Decision block button)
|
||||||
|
- function-square.svg (Expression block button)
|
||||||
|
- variable.svg (Variable reference / Variable creation buttons)
|
||||||
|
- terminal.svg (Output block button)
|
||||||
|
- book-open.svg (Procedure call button)
|
||||||
|
- arrow-right-circle.svg (Assignment block button)
|
||||||
|
- sliders.svg (Quantity button)
|
||||||
|
- grid.svg (Unit button)
|
||||||
|
- folder-plus.svg (Add library button)
|
||||||
|
- file-plus.svg (Add procedure button)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-arrow-right"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-book-open"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 7v14" />
|
||||||
|
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 459 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-database"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||||
|
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
|
||||||
|
<path d="M3 12A9 3 0 0 0 21 12" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-diamond"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 436 B |
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-plus"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||||
|
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||||
|
<path d="M9 15h6" />
|
||||||
|
<path d="M12 18v-6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 450 B |
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-file-text"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
|
||||||
|
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
|
||||||
|
<path d="M10 9H8" />
|
||||||
|
<path d="M16 13H8" />
|
||||||
|
<path d="M16 17H8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 473 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-folder-plus"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 10v6" />
|
||||||
|
<path d="M9 13h6" />
|
||||||
|
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-sigma"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-git-branch"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="6" x2="6" y1="3" y2="15" />
|
||||||
|
<circle cx="18" cy="6" r="3" />
|
||||||
|
<circle cx="6" cy="18" r="3" />
|
||||||
|
<path d="M18 9a9 9 0 0 1-9 9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 433 B |
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-grid-3x3"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
<path d="M3 9h18" />
|
||||||
|
<path d="M3 15h18" />
|
||||||
|
<path d="M9 3v18" />
|
||||||
|
<path d="M15 3v18" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-ruler"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z" />
|
||||||
|
<path d="m14.5 12.5 2-2" />
|
||||||
|
<path d="m11.5 9.5 2-2" />
|
||||||
|
<path d="m8.5 6.5 2-2" />
|
||||||
|
<path d="m17.5 15.5 2-2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 538 B |
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-sliders-horizontal"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="21" x2="14" y1="4" y2="4" />
|
||||||
|
<line x1="10" x2="3" y1="4" y2="4" />
|
||||||
|
<line x1="21" x2="12" y1="12" y2="12" />
|
||||||
|
<line x1="8" x2="3" y1="12" y2="12" />
|
||||||
|
<line x1="21" x2="16" y1="20" y2="20" />
|
||||||
|
<line x1="12" x2="3" y1="20" y2="20" />
|
||||||
|
<line x1="14" x2="14" y1="2" y2="6" />
|
||||||
|
<line x1="8" x2="8" y1="10" y2="14" />
|
||||||
|
<line x1="16" x2="16" y1="18" y2="22" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 673 B |
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-terminal"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="4 17 10 11 4 5" />
|
||||||
|
<line x1="12" x2="20" y1="19" y2="19" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 370 B |
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- @license lucide-static v0.454.0 - ISC -->
|
||||||
|
<svg
|
||||||
|
class="lucide lucide-variable"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M8 21s-4-3-4-9 4-9 4-9" />
|
||||||
|
<path d="M16 3s4 3 4 9-4 9-4 9" />
|
||||||
|
<line x1="15" x2="9" y1="9" y2="15" />
|
||||||
|
<line x1="9" x2="15" y1="9" y2="15" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 445 B |
@@ -3,63 +3,404 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title></title>
|
<title>Visual Programming Tool</title>
|
||||||
<link href="style.css" rel="stylesheet">
|
<link href="style.css" rel="stylesheet">
|
||||||
<script src="main.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body onload="main()">
|
<body>
|
||||||
|
|
||||||
|
<!-- ─── Flowchart node templates ─── -->
|
||||||
|
|
||||||
|
<!-- Program entry point (top of main()). -->
|
||||||
<template id="wc-start">
|
<template id="wc-start">
|
||||||
<wc-start>
|
<wc-start>
|
||||||
START
|
START
|
||||||
<button class="hook"></button>
|
<button class="hook" data-direction="out" data-exit="down" data-link-type="logic"></button>
|
||||||
</wc-start>
|
</wc-start>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Program exit (exit(0)). -->
|
||||||
<template id="wc-end">
|
<template id="wc-end">
|
||||||
<wc-end>
|
<wc-end>
|
||||||
<button class="hook"></button>
|
<button class="hook" data-direction="in" data-exit="up" data-link-type="logic"></button>
|
||||||
END
|
END
|
||||||
</wc-end>
|
</wc-end>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Output/print block with a textarea for the format string. -->
|
||||||
<template id="wc-output">
|
<template id="wc-output">
|
||||||
<wc-output>
|
<wc-output>
|
||||||
<textarea id="" placeholder="output string"></textarea>
|
<textarea placeholder="output string"></textarea>
|
||||||
<button class="hook" id="hook-fore">in</button>
|
<button class="hook" id="hook-fore" data-direction="in" data-exit="left" data-link-type="logic">in</button>
|
||||||
<button class="hook" id="hook-next">out</button>
|
<button class="hook" id="hook-next" data-direction="out" data-exit="right" data-link-type="logic">out</button>
|
||||||
</wc-output>
|
</wc-output>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Decision/branch block with condition, yes, no, test, and in ports. -->
|
||||||
<template id="wc-decision">
|
<template id="wc-decision">
|
||||||
<wc-decision>
|
<wc-decision>
|
||||||
<p>BRANCH</p>
|
<p>BRANCH</p>
|
||||||
<button class="hook" id="hook-test">test</button>
|
<div class="decision-row">
|
||||||
<button class="hook" id="hook-out1">yes</button>
|
<span class="decision-label">if</span>
|
||||||
<button class="hook" id="hook-out0">no</button>
|
<button class="hook decision-data-in" data-direction="in" data-link-type="data" data-type="boolean" data-exit="left">bool</button>
|
||||||
<button class="hook" id="hook-fore">in</button>
|
</div>
|
||||||
|
<button class="hook" id="hook-out1" data-direction="out" data-exit="right" data-link-type="logic">yes</button>
|
||||||
|
<button class="hook" id="hook-out0" data-direction="out" data-exit="right" data-link-type="logic">no</button>
|
||||||
|
<button class="hook" id="hook-fore" data-direction="in" data-exit="left" data-link-type="logic">in</button>
|
||||||
</wc-decision>
|
</wc-decision>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="wc-datum">
|
<!-- ─── Measurement node templates ─── -->
|
||||||
<wc-datum>
|
|
||||||
DATUM
|
<!-- Quantity definition with 7 SI dimensions and default unit. -->
|
||||||
<button class="hook"></button>
|
<template id="wc-quantity">
|
||||||
</wc-datum>
|
<wc-quantity>
|
||||||
|
<div class="quantity-header">
|
||||||
|
<span class="quantity-label">QUANTITY</span>
|
||||||
|
<input class="quantity-name" placeholder="quantity name" value="quantity">
|
||||||
|
</div>
|
||||||
|
<div class="dims">
|
||||||
|
<label>L<input class="dim-0" type="number" value="0"></label>
|
||||||
|
<label>M<input class="dim-1" type="number" value="0"></label>
|
||||||
|
<label>T<input class="dim-2" type="number" value="0"></label>
|
||||||
|
<label>I<input class="dim-3" type="number" value="0"></label>
|
||||||
|
<label>Θ<input class="dim-4" type="number" value="0"></label>
|
||||||
|
<label>N<input class="dim-5" type="number" value="0"></label>
|
||||||
|
<label>J<input class="dim-6" type="number" value="0"></label>
|
||||||
|
</div>
|
||||||
|
<div class="range">
|
||||||
|
<input class="quantity-min" placeholder="min" value="">
|
||||||
|
<input class="quantity-max" placeholder="max" value="">
|
||||||
|
</div>
|
||||||
|
<div class="quantity-footer">
|
||||||
|
<select class="quantity-default-unit">
|
||||||
|
<option value="">(none)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</wc-quantity>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<h1>The anti-vibecoding framework</h1>
|
<!-- Unit of measurement with type, suffix, and SI factor. -->
|
||||||
|
<template id="wc-unit">
|
||||||
|
<wc-unit>
|
||||||
|
<div class="unit-header">
|
||||||
|
<span class="unit-label">UNIT</span>
|
||||||
|
<input class="unit-fullname" placeholder="full name" value="unit">
|
||||||
|
</div>
|
||||||
|
<div class="unit-body">
|
||||||
|
<label>type <select class="unit-type"><option value="">(select quantity)</option></select></label>
|
||||||
|
<label>suffix <input class="unit-suffix" placeholder="suffix" value=""></label>
|
||||||
|
<label>SI <input class="unit-si" placeholder="1" value="1"></label>
|
||||||
|
<span class="unit-resolved"></span>
|
||||||
|
</div>
|
||||||
|
</wc-unit>
|
||||||
|
</template>
|
||||||
|
|
||||||
<noscript>bro thinks this works without js 💀</noscript>
|
<!-- Boolean variable block: fixed boolean type, no unit/SI. -->
|
||||||
<button onclick="$('#main-program').newDecisionNode()">new decision</button>
|
<template id="wc-boolean">
|
||||||
<button onclick="$('#main-program').newOutputNode()">new output</button>
|
<wc-boolean>
|
||||||
<button onclick="$('#main-program').newDataNode()">new data</button>
|
<div class="datum-row">
|
||||||
<button onclick="$('#main-program').generateCode()">Generate code</button>
|
<input class="var-name" placeholder="name" value="flag">
|
||||||
<textarea id="c-code"></textarea>
|
=
|
||||||
<div id="draw-area" onclick="linkedFirst=null">
|
<select class="var-value">
|
||||||
|
<option value="false">false</option>
|
||||||
|
<option value="true">true</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span class="var-cname">flag</span>
|
||||||
|
<input class="var-comment" placeholder="// comment">
|
||||||
|
</wc-boolean>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Scalar variable block: typed quantity with unit and SI conversion. -->
|
||||||
|
<template id="wc-scalar">
|
||||||
|
<wc-scalar>
|
||||||
|
<div class="datum-row">
|
||||||
|
<input class="var-name" placeholder="name" value="x">
|
||||||
|
=
|
||||||
|
<input class="var-value" placeholder="5 km" value="0">
|
||||||
|
<select class="var-unit">
|
||||||
|
<option value="">(unit)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="datum-row">
|
||||||
|
<select class="var-type">
|
||||||
|
<option value="">(no type)</option>
|
||||||
|
</select>
|
||||||
|
<span class="var-si"></span>
|
||||||
|
</div>
|
||||||
|
<span class="var-cname">x</span>
|
||||||
|
<input class="var-comment" placeholder="// comment">
|
||||||
|
</wc-scalar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- String variable block: C string (char* + length struct). -->
|
||||||
|
<template id="wc-string">
|
||||||
|
<wc-string>
|
||||||
|
<div class="datum-row">
|
||||||
|
<input class="var-name" placeholder="name" value="s">
|
||||||
|
=
|
||||||
|
<input class="var-value" placeholder="string value" value="">
|
||||||
|
</div>
|
||||||
|
<span class="var-cname">s</span>
|
||||||
|
<input class="var-comment" placeholder="// comment">
|
||||||
|
</wc-string>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Array variable block: typed C array with tabular element editor (lives in Data tab). -->
|
||||||
|
<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>
|
||||||
|
<span class="var-cname">arr</span>
|
||||||
|
<input class="var-comment" placeholder="// comment">
|
||||||
|
<div class="array-elements">
|
||||||
|
<!-- Element rows rendered dynamically by WCArray._renderElements() -->
|
||||||
|
</div>
|
||||||
|
</wc-array>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Variable reference block: read-only instance of a Data-tab variable with data output hook. -->
|
||||||
|
<template id="wc-varref">
|
||||||
|
<wc-varref>
|
||||||
|
<span class="varref-label">VAR</span>
|
||||||
|
<select class="varref-select">
|
||||||
|
<option value="">(select variable)</option>
|
||||||
|
</select>
|
||||||
|
<button class="hook hook-out" data-direction="out" data-link-type="data" data-type="scalar" data-exit="down">out</button>
|
||||||
|
</wc-varref>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Expression block: mathematical expression with data hooks. -->
|
||||||
|
<template id="wc-expression">
|
||||||
|
<wc-expression>
|
||||||
|
<div class="expr-varhooks"></div>
|
||||||
|
<div>EXPR</div>
|
||||||
|
<div class="expr-row">
|
||||||
|
<input class="expr-name" placeholder="result" value="res">
|
||||||
|
=
|
||||||
|
</div>
|
||||||
|
<div class="expr-formula-container">
|
||||||
|
<textarea class="expr-latex" placeholder="a + b" rows="2" spellcheck="false"></textarea>
|
||||||
|
<div class="expr-rendered"></div>
|
||||||
|
</div>
|
||||||
|
<button class="hook hook-out" id="hook-out" data-direction="out" data-link-type="data" data-type="scalar" data-exit="down">out</button>
|
||||||
|
</wc-expression>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Assignment block: assigns a value to a variable. -->
|
||||||
|
<template id="wc-assign">
|
||||||
|
<wc-assign>
|
||||||
|
<div class="assign-header">ASSIGN</div>
|
||||||
|
<div class="assign-row">
|
||||||
|
<select class="assign-target">
|
||||||
|
<option value="">(select variable)</option>
|
||||||
|
</select>
|
||||||
|
<span class="assign-eq">=</span>
|
||||||
|
<input class="assign-value" placeholder="value">
|
||||||
|
<button class="hook" data-direction="in" data-link-type="data" data-type="scalar" data-exit="left">val</button>
|
||||||
|
</div>
|
||||||
|
<div class="assign-index-area" style="display:none">
|
||||||
|
<span class="assign-bracket">[</span>
|
||||||
|
<input class="assign-index" placeholder="0" value="0">
|
||||||
|
<button class="hook" data-direction="in" data-link-type="data" data-type="scalar" data-exit="left">idx</button>
|
||||||
|
<span class="assign-bracket">]</span>
|
||||||
|
</div>
|
||||||
|
<button class="hook" id="hook-fore" data-direction="in" data-exit="left" data-link-type="logic">in</button>
|
||||||
|
<button class="hook" id="hook-next" data-direction="out" data-exit="right" data-link-type="logic">out</button>
|
||||||
|
</wc-assign>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ─── Procedure templates ─── -->
|
||||||
|
|
||||||
|
<!-- Procedure definition block (lives in the Procedures tab). -->
|
||||||
|
<template id="wc-procedure">
|
||||||
|
<wc-procedure>
|
||||||
|
<div class="proc-header">
|
||||||
|
<span class="proc-label">PROCEDURE</span>
|
||||||
|
<input class="proc-name" placeholder="proc_name" value="proc">
|
||||||
|
</div>
|
||||||
|
<div class="proc-params">
|
||||||
|
<div class="proc-params-col">
|
||||||
|
<span class="proc-params-heading">inputs</span>
|
||||||
|
<div class="proc-inputs"></div>
|
||||||
|
<button class="proc-add-input">+ input</button>
|
||||||
|
</div>
|
||||||
|
<div class="proc-params-col">
|
||||||
|
<span class="proc-params-heading">outputs</span>
|
||||||
|
<div class="proc-outputs"></div>
|
||||||
|
<button class="proc-add-output">+ output</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="hook"></button>
|
||||||
|
</wc-procedure>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Procedure call block (lives in the Algorithm tab). -->
|
||||||
|
<template id="wc-proccall">
|
||||||
|
<wc-proccall>
|
||||||
|
<div class="proccall-header">
|
||||||
|
<span class="proccall-label">CALL</span>
|
||||||
|
<select class="proccall-select">
|
||||||
|
<option value="">(select procedure)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="proccall-name-row">
|
||||||
|
<input class="proccall-name" placeholder="result var" value="">
|
||||||
|
</div>
|
||||||
|
<div class="proccall-container">
|
||||||
|
<div class="proccall-inputs"></div>
|
||||||
|
<div class="proccall-outputs"></div>
|
||||||
|
</div>
|
||||||
|
<button class="hook hook-in" data-direction="in" data-link-type="logic" data-exit="up">in</button>
|
||||||
|
<button class="hook hook-out" data-direction="out" data-link-type="logic" data-exit="down">out</button>
|
||||||
|
</wc-proccall>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ─── Page layout ─── -->
|
||||||
|
|
||||||
|
<h1>Visual Programming Framework</h1>
|
||||||
|
|
||||||
|
<!-- Tab bar: Algorithm, Data, and Measurements tabs. -->
|
||||||
|
<div class="tab-bar">
|
||||||
|
<button class="tab-btn active" data-tab="algorithm">
|
||||||
|
<img class="tab-icon" src="icons/git-branch.svg" width="16" height="16" alt="">
|
||||||
|
Algorithm
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="data">
|
||||||
|
<img class="tab-icon" src="icons/database.svg" width="16" height="16" alt="">
|
||||||
|
Data
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="measurements">
|
||||||
|
<img class="tab-icon" src="icons/ruler.svg" width="16" height="16" alt="">
|
||||||
|
Measurements
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="procedures">
|
||||||
|
<img class="tab-icon" src="icons/file-text.svg" width="16" height="16" alt="">
|
||||||
|
Procedures
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab content area that fills remaining vertical space. -->
|
||||||
|
<div class="tab-body">
|
||||||
|
<!-- Algorithm pane: flowchart toolbar + drawing surface with SVG connections. -->
|
||||||
|
<div class="tab-pane active" id="pane-algorithm">
|
||||||
|
<div class="pane-toolbar">
|
||||||
|
<button data-action="decision" title="new decision">
|
||||||
|
<img src="icons/diamond.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="expression" title="new expression">
|
||||||
|
<img src="icons/function-square.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="varref" title="new variable">
|
||||||
|
<img src="icons/variable.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="output" title="new output">
|
||||||
|
<img src="icons/terminal.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="proccall" title="new procedure call">
|
||||||
|
<img src="icons/book-open.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="assign" title="new assignment">
|
||||||
|
<img src="icons/arrow-right-circle.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="draw-area">
|
||||||
<svg id="connections" xmlns="http://www.w3.org/2000/svg">
|
<svg id="connections" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs></defs>
|
||||||
</svg>
|
</svg>
|
||||||
<wc-program id="main-program">
|
<wc-program id="main-program">
|
||||||
</wc-program>
|
</wc-program>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data pane: toolbar for creating variables/quantities + variable area. -->
|
||||||
|
<div class="tab-pane" id="pane-data">
|
||||||
|
<div class="pane-toolbar">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button data-action="variable" title="new variable">
|
||||||
|
<img src="icons/variable.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<div class="submenu" data-action="variable-submenu">
|
||||||
|
<button data-action="boolean">boolean</button>
|
||||||
|
<button data-action="scalar">scalar</button>
|
||||||
|
<button data-action="string">string</button>
|
||||||
|
<button data-action="array">array</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="variable-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Measurements pane: toolbar for creating units + node area. -->
|
||||||
|
<div class="tab-pane" id="pane-measurements">
|
||||||
|
<div class="pane-toolbar">
|
||||||
|
<button data-action="quantity" title="new quantity">
|
||||||
|
<img src="icons/sliders.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="unit" title="new unit">
|
||||||
|
<img src="icons/grid.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="data-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Procedures pane: library sub-tabs + procedure definitions. -->
|
||||||
|
<div class="tab-pane" id="pane-procedures">
|
||||||
|
<div class="pane-toolbar">
|
||||||
|
<button data-action="add-library" title="new library">
|
||||||
|
<img src="icons/folder-plus.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
<button data-action="add-procedure" title="new procedure">
|
||||||
|
<img src="icons/file-plus.svg" width="18" height="18" alt="">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="lib-tab-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom toolbar: code generation button and output textarea. -->
|
||||||
|
<div class="global-toolbar">
|
||||||
|
<button data-action="generate">Generate code</button>
|
||||||
|
<button data-action="save">Save</button>
|
||||||
|
<button data-action="load">Load</button>
|
||||||
|
<button data-action="new">New</button>
|
||||||
|
<textarea id="c-code"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="error-overlay" class="error-overlay">
|
||||||
|
<div class="error-box">
|
||||||
|
<span class="error-close">×</span>
|
||||||
|
<div class="error-ack">-ACK!</div>
|
||||||
|
<div class="error-msg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
window.MathJax = {
|
||||||
|
tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$']] },
|
||||||
|
options: { enableMenu: false, ignoreHtmlClass: 'expr-latex' },
|
||||||
|
startup: { elements: [] }
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="lib/mathjax/tex-mml-chtml.js"></script>
|
||||||
|
<script src="dom.js"></script>
|
||||||
|
<script src="helpers.js"></script>
|
||||||
|
<script src="error.js"></script>
|
||||||
|
<script src="draw.js"></script>
|
||||||
|
<script src="wcblock.js"></script>
|
||||||
|
<script src="blocks-algo.js"></script>
|
||||||
|
<script src="blocks-data.js"></script>
|
||||||
|
<script src="blocks-proc.js"></script>
|
||||||
|
<script src="wcprogram.js"></script>
|
||||||
|
<script src="persist.js"></script>
|
||||||
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
let $ = document.querySelector.bind(document);
|
|
||||||
let $$ = document.querySelectorAll.bind(document);
|
|
||||||
const svgNamespace = 'http://www.w3.org/2000/svg';
|
|
||||||
|
|
||||||
let linkageFirst = null
|
|
||||||
|
|
||||||
class WCBlock extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.addEventListener("mousedown", this.startDragging);
|
|
||||||
this.querySelector(".hook").addEventListener("click", this.linkEvent.bind(this));
|
|
||||||
/* List of connections to and from this */
|
|
||||||
this.wcForeFlow = Array.of();
|
|
||||||
this.wcNextFlow = Array.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
startDragging(event) {
|
|
||||||
this.dragStartX = event.clientX;
|
|
||||||
this.dragStartY = event.clientY;
|
|
||||||
document.onmousemove = this.dragAround.bind(this);
|
|
||||||
document.onmouseup = this.stopDragging.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragAround(event) {
|
|
||||||
const {clientX, clientY} = event;
|
|
||||||
const gridSize = parseInt(window.getComputedStyle(this).getPropertyValue('--grid-size'));
|
|
||||||
let newTop = (this.offsetTop + event.clientY - this.dragStartY);
|
|
||||||
let newLeft = (this.offsetLeft + event.clientX - this.dragStartX);
|
|
||||||
newTop = Math.round(newTop / gridSize) * gridSize;
|
|
||||||
newLeft = Math.round(newLeft / gridSize) * gridSize;
|
|
||||||
this.dragStartX = Math.round(event.clientX/gridSize) * gridSize;
|
|
||||||
this.style.top = newTop +"px";
|
|
||||||
this.dragStartY = Math.round(event.clientY/gridSize) * gridSize;
|
|
||||||
this.style.left = newLeft +"px";
|
|
||||||
this.wcNextFlow.forEach(element => {
|
|
||||||
drawpath(element);
|
|
||||||
});
|
|
||||||
this.wcForeFlow.forEach(element => {
|
|
||||||
drawpath(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
linkEvent() {
|
|
||||||
if (! linkageFirst) {
|
|
||||||
linkageFirst = this;
|
|
||||||
} else if (this != linkageFirst) {
|
|
||||||
var pathElement = document.createElementNS(svgNamespace, 'path');
|
|
||||||
pathElement.id = "flow-"+this.id+"2"+linkageFirst.id;
|
|
||||||
pathElement.toElement = this;
|
|
||||||
pathElement.fromElement = linkageFirst;
|
|
||||||
pathElement.classList.add("flow");
|
|
||||||
this.wcForeFlow.push(pathElement);
|
|
||||||
linkageFirst.wcNextFlow.push(pathElement);
|
|
||||||
$("#connections").appendChild(pathElement);
|
|
||||||
drawpath(pathElement);
|
|
||||||
linkageFirst = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateNextCode() {
|
|
||||||
var code = ""
|
|
||||||
this.wcNextFlow.forEach((path) => {
|
|
||||||
code += path.toElement.generateCode();
|
|
||||||
});
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopDragging(event) {
|
|
||||||
document.onmousemove = null;
|
|
||||||
document.onmouseup = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCOutput extends WCBlock {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCode() {
|
|
||||||
return `
|
|
||||||
printf("${this.querySelector("textarea").value}");
|
|
||||||
` + this.generateNextCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCStart extends WCBlock {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCode() {
|
|
||||||
var code = `
|
|
||||||
// --- WiseCode® ---
|
|
||||||
int main() {
|
|
||||||
|
|
||||||
`;
|
|
||||||
return code + this.generateNextCode() + `
|
|
||||||
}
|
|
||||||
// --- END ---
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCEnd extends WCBlock {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCode() {
|
|
||||||
return `
|
|
||||||
// WCEnd
|
|
||||||
exit(0);
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCProgram extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.counter = 0;
|
|
||||||
}
|
|
||||||
build() {
|
|
||||||
this.startNode = document.importNode($("#wc-start").content, true)
|
|
||||||
this.endNode = document.importNode($("#wc-end").content, true)
|
|
||||||
this.startNode.firstElementChild.id = "main-start";
|
|
||||||
this.endNode.firstElementChild.id = "main-end";
|
|
||||||
this.appendChild(this.startNode)
|
|
||||||
this.appendChild(this.endNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
newDecisionNode() {
|
|
||||||
this.counter ++;
|
|
||||||
const decisionNode = document.importNode($("#wc-decision").content, true)
|
|
||||||
decisionNode.firstElementChild.id = "decision"+this.counter;
|
|
||||||
this.appendChild(decisionNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
newOutputNode() {
|
|
||||||
const outputNode = document.importNode($("#wc-output").content, true)
|
|
||||||
outputNode.firstElementChild.id = "output"+this.counter;
|
|
||||||
this.appendChild(outputNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
newDataNode() {
|
|
||||||
const dataNode = document.importNode($("#wc-datum").content, true)
|
|
||||||
dataNode.firstElementChild.id = "data"+this.counter;
|
|
||||||
this.appendChild(dataNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCode() {
|
|
||||||
$("#c-code").value = this.firstElementChild.generateCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCDecision extends WCBlock {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCode() {
|
|
||||||
return `
|
|
||||||
if (condition) goto next; else goto alt;
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WCDatum extends WCBlock {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawpath(path) {
|
|
||||||
const rectFrom = path.fromElement.querySelector(".hook").getBoundingClientRect();
|
|
||||||
const rectTo = path.toElement.querySelector(".hook").getBoundingClientRect();
|
|
||||||
const [fromX, fromY] = [(rectFrom.left + rectFrom.right) / 2, (rectFrom.top + rectFrom.bottom) / 2];
|
|
||||||
const [toX, toY] = [(rectTo.left + rectTo.right) / 2, (rectTo.top + rectTo.bottom) / 2];
|
|
||||||
path.setAttribute("d",
|
|
||||||
`M ${fromX} ${fromY} \
|
|
||||||
C\
|
|
||||||
${toX} ${fromY} \
|
|
||||||
${fromX} ${toY} \
|
|
||||||
${toX} ${toY} \
|
|
||||||
`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("wc-start", WCStart);
|
|
||||||
customElements.define("wc-end", WCEnd);
|
|
||||||
customElements.define("wc-program", WCProgram);
|
|
||||||
customElements.define("wc-output", WCOutput);
|
|
||||||
customElements.define("wc-decision", WCDecision);
|
|
||||||
customElements.define("wc-datum", WCDatum);
|
|
||||||
|
|
||||||
function generateCode() {
|
|
||||||
const text = $("#main-start").generateCode()
|
|
||||||
$("#c-code").innerHTML = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
$("#main-program").build();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
// ── Workspace persistence (localStorage) ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Serialises and deserialises the full workspace state — all blocks in all
|
||||||
|
// three tab panes, their positions and data, plus SVG connection topology.
|
||||||
|
//
|
||||||
|
// Depends on: dom.js ($, $id, svgNamespace, STORAGE_KEY)
|
||||||
|
// helpers.js (refreshAll, splitValueAndUnit)
|
||||||
|
// draw.js (drawpath)
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the position and type-specific data from a single block element.
|
||||||
|
* Delegates to the block's saveData() method for type-specific fields.
|
||||||
|
* @param {HTMLElement} block - a WCBlock or WCProgram child element
|
||||||
|
* @returns {{ x: number, y: number, ... }}
|
||||||
|
*/
|
||||||
|
function saveBlockData(block) {
|
||||||
|
const data = {
|
||||||
|
x: parseInt(block.style.left) || 0,
|
||||||
|
y: parseInt(block.style.top) || 0
|
||||||
|
};
|
||||||
|
Object.assign(data, block.saveData());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialises the entire workspace to localStorage under STORAGE_KEY.
|
||||||
|
* Captures all blocks from algorithm, measurements, and data panes,
|
||||||
|
* plus every SVG connection path.
|
||||||
|
*/
|
||||||
|
function saveWorkspace() {
|
||||||
|
const prog = $id("main-program");
|
||||||
|
const state = {
|
||||||
|
version: 1,
|
||||||
|
counter: prog.counter,
|
||||||
|
blocks: { algorithm: [], measurements: [], data: [], procedures: [] },
|
||||||
|
connections: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const block of prog.children) {
|
||||||
|
state.blocks.algorithm.push({
|
||||||
|
id: block.id, tag: block.tagName.toLowerCase(),
|
||||||
|
data: saveBlockData(block)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const block of $id("data-area").children) {
|
||||||
|
state.blocks.measurements.push({
|
||||||
|
id: block.id, tag: block.tagName.toLowerCase(),
|
||||||
|
data: saveBlockData(block)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const block of $id("variable-area").children) {
|
||||||
|
state.blocks.data.push({
|
||||||
|
id: block.id, tag: block.tagName.toLowerCase(),
|
||||||
|
data: saveBlockData(block)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const block of document.querySelectorAll(".lib-pane .procedures-area > [data-block]")) {
|
||||||
|
state.blocks.procedures.push({
|
||||||
|
id: block.id, tag: block.tagName.toLowerCase(),
|
||||||
|
data: saveBlockData(block)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const path of $id("connections").querySelectorAll("path.flow, path.flow-data")) {
|
||||||
|
state.connections.push({
|
||||||
|
fromBlock: path.fromElement.id,
|
||||||
|
fromHook: path.fromHook ? (path.fromHook.id || null) : null,
|
||||||
|
toBlock: path.toElement.id,
|
||||||
|
toHook: path.toHook ? (path.toHook.id || null) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep-clones a block <template> by tag name and restores position + data onto it.
|
||||||
|
* Delegates to the element's restoreData() method for type-specific fields.
|
||||||
|
* @param {{ id: string, tag: string, data: object }} info - saved block descriptor
|
||||||
|
* @returns {HTMLElement|null} the restored element, or null for unknown tags
|
||||||
|
*/
|
||||||
|
function restoreBlock(info) {
|
||||||
|
const reg = BLOCK_REGISTRY[info.tag];
|
||||||
|
if (!reg) return null;
|
||||||
|
|
||||||
|
const frag = document.importNode(document.querySelector(reg.template).content, true);
|
||||||
|
const el = frag.firstElementChild;
|
||||||
|
el.id = info.id;
|
||||||
|
el.style.left = info.data.x + "px";
|
||||||
|
el.style.top = info.data.y + "px";
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a group of block descriptors into a container in a single pass.
|
||||||
|
* Creates each block, appends it, then calls restoreData on all of them.
|
||||||
|
* @param {{ id: string, tag: string, data: object }[]} infos - saved block descriptors
|
||||||
|
* @param {HTMLElement} container - parent element to append blocks into
|
||||||
|
*/
|
||||||
|
function restoreGroup(infos, container) {
|
||||||
|
const els = [];
|
||||||
|
for (const info of infos) {
|
||||||
|
const el = restoreBlock(info);
|
||||||
|
if (el) { container.appendChild(el); els.push({ el, data: info.data || {} }); }
|
||||||
|
}
|
||||||
|
for (const { el, data } of els) el.restoreData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialises the workspace from localStorage and reconstructs all blocks,
|
||||||
|
* connections, and internal state. Existing user blocks are removed first.
|
||||||
|
*/
|
||||||
|
function loadWorkspace() {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
let state;
|
||||||
|
try { state = JSON.parse(raw); } catch { return; }
|
||||||
|
|
||||||
|
const prog = $id("main-program");
|
||||||
|
const dataArea = $id("data-area");
|
||||||
|
const varArea = $id("variable-area");
|
||||||
|
const connectionsSvg = $id("connections");
|
||||||
|
|
||||||
|
/* ── 1. Tear down existing state ── */
|
||||||
|
for (const p of connectionsSvg.querySelectorAll("path.flow, path.flow-data")) p.remove();
|
||||||
|
for (const block of document.querySelectorAll("[data-block]")) {
|
||||||
|
block.wcForeFlow = [];
|
||||||
|
block.wcNextFlow = [];
|
||||||
|
}
|
||||||
|
for (const block of [...prog.children]) {
|
||||||
|
if (block.id !== "main-start" && block.id !== "main-end") block.remove();
|
||||||
|
}
|
||||||
|
dataArea.innerHTML = "";
|
||||||
|
varArea.innerHTML = "";
|
||||||
|
for (const pane of document.querySelectorAll(".lib-pane")) {
|
||||||
|
const pa = pane.querySelector(".procedures-area");
|
||||||
|
if (pa) pa.innerHTML = "";
|
||||||
|
}
|
||||||
|
document.querySelector(".lib-tab-bar").innerHTML = "";
|
||||||
|
|
||||||
|
prog.counter = state.counter;
|
||||||
|
|
||||||
|
/* ── 2. Restore measurements (types + units) ── */
|
||||||
|
restoreGroup(state.blocks.measurements, dataArea);
|
||||||
|
|
||||||
|
/* ── 3. Restore procedures — BEFORE algorithm (WCProcCall depends on them) ── */
|
||||||
|
const libMap = {};
|
||||||
|
const procEls = [];
|
||||||
|
for (const info of state.blocks.procedures) {
|
||||||
|
const lib = (info.data && info.data.library) || "unknown";
|
||||||
|
if (!libMap[lib]) {
|
||||||
|
const tab = document.createElement("button");
|
||||||
|
tab.className = "lib-tab";
|
||||||
|
tab.dataset.libName = lib;
|
||||||
|
tab.textContent = lib;
|
||||||
|
tab.addEventListener("click", () => switchLibTab(lib));
|
||||||
|
document.querySelector(".lib-tab-bar").appendChild(tab);
|
||||||
|
|
||||||
|
const pane = document.createElement("div");
|
||||||
|
pane.className = "lib-pane";
|
||||||
|
pane.dataset.libName = lib;
|
||||||
|
const pa = document.createElement("div");
|
||||||
|
pa.className = "procedures-area";
|
||||||
|
pane.appendChild(pa);
|
||||||
|
$id("pane-procedures").appendChild(pane);
|
||||||
|
|
||||||
|
libMap[lib] = pa;
|
||||||
|
}
|
||||||
|
const el = restoreBlock(info);
|
||||||
|
if (el) { libMap[lib].appendChild(el); procEls.push({ el, data: info.data || {} }); }
|
||||||
|
}
|
||||||
|
for (const { el, data } of procEls) el.restoreData(data);
|
||||||
|
/* Activate first library tab */
|
||||||
|
const firstTab = document.querySelector(".lib-tab");
|
||||||
|
if (firstTab) switchLibTab(firstTab.dataset.libName);
|
||||||
|
|
||||||
|
/* ── 4. Restore algorithm blocks (reposition start/end, recreate others) ── */
|
||||||
|
const algoEls = [];
|
||||||
|
for (const info of state.blocks.algorithm) {
|
||||||
|
if (info.id === "main-start" || info.id === "main-end") {
|
||||||
|
const el = $id(info.id);
|
||||||
|
if (el) {
|
||||||
|
el.style.left = info.data.x + "px";
|
||||||
|
el.style.top = info.data.y + "px";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const el = restoreBlock(info);
|
||||||
|
if (el) { prog.appendChild(el); algoEls.push({ el, data: info.data || {} }); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const { el, data } of algoEls) el.restoreData(data);
|
||||||
|
|
||||||
|
/* ── 5. Restore data (datum / variable blocks) ── */
|
||||||
|
restoreGroup(state.blocks.data, varArea);
|
||||||
|
|
||||||
|
/* ── 6. Restore SVG connections ── */
|
||||||
|
for (const conn of state.connections) {
|
||||||
|
const fromBlock = $id(conn.fromBlock);
|
||||||
|
const toBlock = $id(conn.toBlock);
|
||||||
|
if (!fromBlock || !toBlock) continue;
|
||||||
|
|
||||||
|
const fromHook = conn.fromHook
|
||||||
|
? fromBlock.querySelector("#" + conn.fromHook)
|
||||||
|
: (fromBlock.querySelector('[data-direction="out"]') || fromBlock.querySelector(".hook"));
|
||||||
|
const toHook = conn.toHook
|
||||||
|
? toBlock.querySelector("#" + conn.toHook)
|
||||||
|
: (toBlock.querySelector('[data-direction="in"]') || toBlock.querySelector(".hook"));
|
||||||
|
if (!fromHook || !toHook) continue;
|
||||||
|
|
||||||
|
const linkType = fromHook.dataset.linkType || "logic";
|
||||||
|
const path = document.createElementNS(svgNamespace, "path");
|
||||||
|
path.dataset.linkType = linkType;
|
||||||
|
path.classList.add(linkType === "data" ? "flow-data" : "flow");
|
||||||
|
path.toElement = toBlock;
|
||||||
|
path.fromElement = fromBlock;
|
||||||
|
path.fromHook = fromHook;
|
||||||
|
path.toHook = toHook;
|
||||||
|
toBlock.wcForeFlow.push(path);
|
||||||
|
fromBlock.wcNextFlow.push(path);
|
||||||
|
connectionsSvg.appendChild(path);
|
||||||
|
drawpath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 7. Refresh dropdowns & SI displays (after all blocks + connections exist) ── */
|
||||||
|
refreshAll();
|
||||||
|
for (const u of dataArea.querySelectorAll("wc-unit")) u.updateSiDisplay();
|
||||||
|
for (const d of varArea.querySelectorAll("wc-scalar")) d.updateSiDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the saved workspace (removes localStorage entry) and reloads the page,
|
||||||
|
* returning the app to its pristine default state.
|
||||||
|
*/
|
||||||
|
function newWorkspace() {
|
||||||
|
if (!confirm("Clear the entire workspace and start fresh?")) return;
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
@@ -1,118 +1,548 @@
|
|||||||
:root {
|
:root {
|
||||||
--grid-size: 25px;
|
--grid-size: 25px;
|
||||||
|
--bg: #fff;
|
||||||
|
--surface: #f5f5f5;
|
||||||
|
--text: #333;
|
||||||
|
--muted: #999;
|
||||||
|
--border: #ccc;
|
||||||
|
--accent: #4a90d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-start, wc-end, wc-output, wc-decision, wc-datum {
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
border: 0.1rem solid black;
|
|
||||||
padding: .5rem;
|
html, body {
|
||||||
/* WARNING: margin adds up every event handle when dragging the element, any margin set should be set outside*/
|
margin: 0; padding: 0; height: 100%;
|
||||||
margin: 0;
|
background: var(--bg); color: var(--text);
|
||||||
position: absolute;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
display: flex;
|
font-size: 14px; line-height: 1.4;
|
||||||
justify-content: center;
|
}
|
||||||
align-items: center;
|
body { display: flex; flex-direction: column; }
|
||||||
flex-direction: column;
|
|
||||||
user-select: none;
|
h1 {
|
||||||
|
font-size: 1rem; padding: 0.5rem 1rem; margin: 0;
|
||||||
|
background: var(--surface); border-bottom: 1px solid var(--border); font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-start, wc-end {
|
.tab-bar {
|
||||||
border-radius: 1rem;
|
display: flex; border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface); flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
display: flex; align-items: center; gap: 0.35rem;
|
||||||
|
padding: 0.5rem 1rem; border: none; background: transparent;
|
||||||
|
cursor: pointer; font: inherit; font-size: 0.9rem;
|
||||||
|
border-bottom: 2px solid transparent; margin-bottom: -1px; color: var(--muted);
|
||||||
|
}
|
||||||
|
.tab-btn:hover { background: #e8e8e8; color: var(--text); }
|
||||||
|
.tab-btn.active { background: #e0e0e0; border-bottom-color: var(--accent); color: var(--text); }
|
||||||
|
.tab-icon { display: block; }
|
||||||
|
|
||||||
|
.tab-body { flex: 1; position: relative; min-height: 0; }
|
||||||
|
.tab-pane { display: none; height: 100%; flex-direction: column; }
|
||||||
|
.tab-pane.active { display: flex; }
|
||||||
|
|
||||||
|
.pane-toolbar {
|
||||||
|
display: flex; gap: 0.25rem; padding: 0.3rem;
|
||||||
|
flex-shrink: 0; flex-wrap: wrap;
|
||||||
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.pane-toolbar button {
|
||||||
|
padding: 0.3rem; font: inherit; font-size: 0.75rem; cursor: pointer;
|
||||||
|
background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
||||||
|
border-radius: 3px; display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.pane-toolbar button:hover { background: #e8e8e8; }
|
||||||
|
.pane-toolbar button svg,
|
||||||
|
.pane-toolbar button img { display: block; }
|
||||||
|
|
||||||
|
.btn-group { position: relative; display: inline-flex; }
|
||||||
|
.submenu {
|
||||||
|
display: none; position: absolute; top: 100%; left: 0; z-index: 100;
|
||||||
|
margin-top: 2px; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||||||
|
min-width: 100px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.submenu.open { display: block; }
|
||||||
|
.submenu button {
|
||||||
|
display: block; width: 100%; padding: 0.35rem 0.75rem; border: none;
|
||||||
|
border-radius: 0; text-align: left; font-size: 0.8rem; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.submenu button:hover { background: #e8e8e8; }
|
||||||
|
.submenu button:not(:last-child) { border-bottom: 1px solid var(--border); }
|
||||||
|
|
||||||
|
/* ── Library sub-tab system ── */
|
||||||
|
.lib-tab-bar {
|
||||||
|
display: flex; border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface); flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.lib-tab {
|
||||||
|
padding: 0.3rem 0.7rem; border: none; background: transparent;
|
||||||
|
cursor: pointer; font: inherit; font-size: 0.8rem;
|
||||||
|
border-bottom: 2px solid transparent; margin-bottom: -1px; color: var(--muted);
|
||||||
|
}
|
||||||
|
.lib-tab:hover { background: #e8e8e8; color: var(--text); }
|
||||||
|
.lib-tab.active { background: #e0e0e0; border-bottom-color: var(--accent); color: var(--text); }
|
||||||
|
|
||||||
|
.lib-pane { display: none; flex: 1; flex-direction: column; min-height: 0; }
|
||||||
|
.lib-pane.active { display: flex; }
|
||||||
|
|
||||||
|
/* ── WCProcedure ── */
|
||||||
|
wc-procedure { gap: 0.35rem; min-width: 200px; }
|
||||||
|
wc-procedure .proc-header { display: flex; align-items: center; gap: 0.3rem; width: 100%; }
|
||||||
|
wc-procedure .proc-label { font-weight: bold; font-size: 0.7rem; color: var(--muted); letter-spacing: 0.05em; }
|
||||||
|
wc-procedure .proc-name {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.8rem; padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-params {
|
||||||
|
display: flex; gap: 0.5rem; width: 100%;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-params-col {
|
||||||
|
flex: 1; display: flex; flex-direction: column; gap: 0.2rem;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-params-heading {
|
||||||
|
font-size: 0.65rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-param-row {
|
||||||
|
display: flex; align-items: center; gap: 0.2rem;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-param-row input {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.7rem; padding: 0.1rem 0.2rem; text-align: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-param-name { width: 4em; }
|
||||||
|
wc-procedure .proc-param-type { width: 5em; }
|
||||||
|
wc-procedure .proc-param-del {
|
||||||
|
width: 16px; height: 16px; border-radius: 50%;
|
||||||
|
border: 1px solid #ccc; background: #fff; color: #999;
|
||||||
|
font-size: 11px; line-height: 1; cursor: pointer; padding: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
wc-procedure .proc-param-del:hover { background: #d32f2f; color: #fff; border-color: #d32f2f; }
|
||||||
|
wc-procedure .proc-add-input, wc-procedure .proc-add-output {
|
||||||
|
font-size: 0.7rem; padding: 0.15rem 0.4rem; cursor: pointer;
|
||||||
|
background: var(--bg); border: 1px dashed var(--border);
|
||||||
|
border-radius: 3px; color: var(--muted);
|
||||||
|
}
|
||||||
|
wc-procedure .proc-add-input:hover, wc-procedure .proc-add-output:hover {
|
||||||
|
background: #e8e8e8; color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-output {
|
/* ── WCProcCall ── */
|
||||||
border-radius: 0;
|
wc-proccall { gap: 0.25rem; min-width: 160px; padding-top: 1.2rem; padding-bottom: 1.2rem; }
|
||||||
background-color: #a0a0f0;
|
wc-proccall .proccall-header { display: flex; align-items: center; gap: 0.3rem; width: 100%; }
|
||||||
transform: skewX(-20deg);
|
wc-proccall .proccall-label { font-weight: bold; font-size: 0.65rem; color: var(--muted); letter-spacing: 0.05em; }
|
||||||
|
wc-proccall .proccall-select {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.7rem; padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-name-row {
|
||||||
|
display: flex; align-items: center; gap: 0.25rem; width: 100%;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-name {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.7rem; padding: 0.1rem 0.2rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-container {
|
||||||
|
display: flex; gap: 0.5rem; width: 100%;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-inputs, wc-proccall .proccall-outputs {
|
||||||
|
flex: 1; display: flex; flex-direction: column; gap: 0.25rem;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-arg-row {
|
||||||
|
display: flex; align-items: center; gap: 0.2rem;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-arg-label {
|
||||||
|
font-size: 0.65rem; color: var(--text); white-space: nowrap;
|
||||||
|
}
|
||||||
|
wc-proccall .proccall-arg-input {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.65rem; padding: 0.1rem 0.2rem; text-align: center;
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
/* Data hooks inside proccall arg rows are inline (position: static by default in .hook) */
|
||||||
|
wc-proccall .proccall-arg-row .hook {
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-output * {
|
/* ── WCAssign ── */
|
||||||
transform: skewX(20deg);
|
wc-assign { gap: 0.25rem; padding-top: 1.2rem; padding-bottom: 1.2rem; }
|
||||||
|
wc-assign .assign-header { font-weight: bold; font-size: 0.65rem; color: var(--muted); letter-spacing: 0.05em; }
|
||||||
|
wc-assign .assign-row { display: flex; align-items: center; gap: 0.25rem; width: 100%; }
|
||||||
|
wc-assign .assign-target {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
wc-assign .assign-eq { font-size: 0.8rem; font-weight: bold; color: var(--text); }
|
||||||
|
wc-assign .assign-value {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
|
wc-assign .assign-value.hidden { display: none; }
|
||||||
|
wc-assign .assign-index-area {
|
||||||
|
display: none; align-items: center; gap: 0.2rem; width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
wc-assign .assign-index-area .hook {
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
wc-assign .assign-bracket { font-size: 0.85rem; font-weight: bold; color: var(--muted); }
|
||||||
|
wc-assign .assign-index {
|
||||||
|
width: 3em; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-assign .assign-index.hidden { display: none; }
|
||||||
|
wc-assign .assign-row .hook {
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
wc-assign #hook-fore[data-direction="in"] {
|
||||||
|
left: -7px; top: 50%; transform: translateY(-50%); bottom: auto;
|
||||||
|
}
|
||||||
|
wc-assign #hook-next[data-direction="out"] {
|
||||||
|
right: -7px; top: 50%; transform: translateY(-50%); bottom: auto; left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-decision {
|
#draw-area, #data-area, #variable-area, .procedures-area {
|
||||||
background-color: #f0f0a0;
|
flex: 1; position: relative; min-height: 0;
|
||||||
border-radius: 0;
|
overflow: auto;
|
||||||
aspect-ratio: 1;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-decision * {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-decision #hook-fore {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-decision #hook-out1 {
|
|
||||||
position: absolute;
|
|
||||||
top:100%;
|
|
||||||
left:100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-decision #hook-out0 {
|
|
||||||
position: absolute;
|
|
||||||
top: 0%;
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-decision #hook-test {
|
|
||||||
align-self: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
wc-datum {
|
|
||||||
background-color: #2020f0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#draw-area {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
background-size: var(--grid-size) var(--grid-size);
|
background-size: var(--grid-size) var(--grid-size);
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(to right, grey 1px, transparent 1px),
|
linear-gradient(to right, #eee 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, grey 1px, transparent 1px);
|
linear-gradient(to bottom, #eee 1px, transparent 1px);
|
||||||
|
background-color: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-toolbar {
|
||||||
|
display: flex; gap: 0.25rem; padding: 0.3rem;
|
||||||
|
align-items: flex-start; flex-shrink: 0;
|
||||||
|
border-top: 1px solid var(--border); background: var(--surface);
|
||||||
|
}
|
||||||
|
.global-toolbar button {
|
||||||
|
padding: 0.4rem 0.8rem; font: inherit; font-size: 0.8rem; cursor: pointer;
|
||||||
|
background: var(--accent); color: #fff; border: none; border-radius: 3px;
|
||||||
|
}
|
||||||
|
.global-toolbar button:hover { background: #357abd; }
|
||||||
|
.global-toolbar textarea {
|
||||||
|
flex: 1; height: 80px; resize: vertical; padding: 0.3rem;
|
||||||
|
background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
||||||
|
font-family: 'Courier New', monospace; font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-block] {
|
||||||
|
border: 1px solid var(--border); padding: 0.5rem; margin: 0;
|
||||||
|
position: absolute; display: flex; justify-content: center;
|
||||||
|
align-items: center; flex-direction: column; user-select: none;
|
||||||
|
background: var(--bg); color: var(--text); min-width: 60px; min-height: 30px;
|
||||||
|
overflow: visible; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); z-index: 1;
|
||||||
|
}
|
||||||
|
[data-block]:hover .resize-handle { opacity: 0.5; }
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute; right: 0; bottom: 0; width: 14px; height: 14px;
|
||||||
|
cursor: nwse-resize; opacity: 0; z-index: 10;
|
||||||
|
background: linear-gradient(135deg, transparent 50%, var(--muted) 50%, var(--muted) 60%, transparent 60%);
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
wc-start { background: #e8f5e9; border-color: #66bb6a; }
|
||||||
|
wc-end { background: #fbe9e7; border-color: #ef5350; }
|
||||||
|
wc-output { background: #e3f2fd; border-color: #42a5f5; }
|
||||||
|
wc-decision { background: #fff3e0; border-color: #ffa726; }
|
||||||
|
wc-boolean { background: #f3e5f5; border-color: #ab47bc; }
|
||||||
|
wc-scalar { background: #e8eaf6; border-color: #5c6bc0; }
|
||||||
|
wc-string { background: #fff8e1; border-color: #f9a825; }
|
||||||
|
wc-varref { background: #f5f5f5; border-color: #9e9e9e; }
|
||||||
|
wc-expression { background: #e8f5e9; border-color: #43a047; }
|
||||||
|
wc-quantity { background: #fce4ec; border-color: #ec407a; }
|
||||||
|
wc-unit { background: #e0f7fa; border-color: #26c6da; }
|
||||||
|
wc-procedure { background: #ede7f6; border-color: #7e57c2; }
|
||||||
|
wc-proccall { background: #fff8e1; border-color: #f9a825; }
|
||||||
|
wc-assign { background: #fce4ec; border-color: #e91e63; }
|
||||||
|
|
||||||
|
wc-output textarea {
|
||||||
|
background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.2rem; width: 10em; resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-decision p { margin: 0.2rem 0; font-weight: 600; }
|
||||||
|
wc-decision .decision-row { display: flex; align-items: center; gap: 0.3rem; }
|
||||||
|
wc-decision .decision-label { font-size: 0.8rem; font-weight: bold; color: var(--text); }
|
||||||
|
wc-decision .decision-row .hook {
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-boolean, wc-scalar, wc-string { gap: 0.25rem; }
|
||||||
|
wc-boolean .datum-row, wc-scalar .datum-row, wc-string .datum-row { display: flex; align-items: center; gap: 0.25rem; width: 100%; }
|
||||||
|
wc-boolean input, wc-scalar input, wc-string input {
|
||||||
|
background: var(--bg); border: 1px solid var(--border); font: inherit;
|
||||||
|
padding: 0.15rem 0.3rem; width: 4em; text-align: center;
|
||||||
|
}
|
||||||
|
wc-boolean .var-name, wc-scalar .var-name, wc-string .var-name, wc-array .var-name {
|
||||||
|
border: none; background: transparent; cursor: pointer;
|
||||||
|
font-weight: bold; font-size: 0.85rem;
|
||||||
|
width: auto; min-width: 2em; padding: 0.1rem;
|
||||||
|
}
|
||||||
|
wc-boolean .var-name:focus, wc-scalar .var-name:focus, wc-string .var-name:focus, wc-array .var-name:focus {
|
||||||
|
border: 1px solid var(--border); background: var(--bg); cursor: text;
|
||||||
|
outline: none; width: 5em;
|
||||||
|
}
|
||||||
|
wc-boolean .var-comment, wc-scalar .var-comment, wc-string .var-comment, wc-array .var-comment {
|
||||||
|
border: none; background: transparent;
|
||||||
|
font-style: italic; font-size: 0.7rem; color: var(--muted);
|
||||||
|
width: 100%; padding: 0.1rem 0.3rem; cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
wc-boolean .var-comment:focus, wc-scalar .var-comment:focus, wc-string .var-comment:focus, wc-array .var-comment:focus {
|
||||||
|
border: 1px solid var(--border); background: var(--bg);
|
||||||
|
font-style: italic; color: var(--text); cursor: text;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
wc-boolean .var-cname, wc-scalar .var-cname, wc-string .var-cname, wc-array .var-cname {
|
||||||
|
font-size: 0.65rem; color: var(--muted); font-family: 'Courier New', monospace;
|
||||||
|
width: 100%; padding: 0 0.3rem; line-height: 1.2;
|
||||||
|
}
|
||||||
|
wc-boolean .var-value, wc-scalar .var-value { width: 5em; }
|
||||||
|
wc-string .var-value { width: 8em; }
|
||||||
|
wc-array { gap: 0.25rem; background: #e0f2f1; border-color: #26a69a; }
|
||||||
|
wc-array .array-row { display: flex; align-items: center; gap: 0.25rem; width: 100%; }
|
||||||
|
|
||||||
|
wc-array .array-type { width: 7em; }
|
||||||
|
wc-array .array-bracket { font-size: 0.85rem; font-weight: bold; }
|
||||||
|
wc-array .array-count { font-size: 0.7rem; color: var(--muted); margin-left: auto; white-space: nowrap; }
|
||||||
|
wc-array .array-elements {
|
||||||
|
display: flex; flex-direction: column; gap: 0.15rem; width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
wc-array .array-element-row {
|
||||||
|
display: flex; align-items: center; gap: 0.25rem;
|
||||||
|
}
|
||||||
|
wc-array .array-element-idx {
|
||||||
|
font-size: 0.7rem; color: var(--muted); width: 1.5em; text-align: right; flex-shrink: 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
wc-array .array-element-value {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.1rem 0.3rem; text-align: center;
|
||||||
|
min-width: 3em;
|
||||||
|
}
|
||||||
|
wc-array .array-element-del {
|
||||||
|
width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0;
|
||||||
|
border: 1px solid #ccc; background: #fff; color: #999;
|
||||||
|
font-size: 11px; line-height: 1; cursor: pointer; padding: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
wc-array .array-element-del:hover { background: #d32f2f; color: #fff; border-color: #d32f2f; }
|
||||||
|
wc-scalar .var-si { font-size: 0.75rem; color: var(--muted); white-space: nowrap; }
|
||||||
|
wc-scalar .var-type, wc-scalar .var-unit {
|
||||||
|
background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-expression { gap: 0.25rem; }
|
||||||
|
wc-expression .expr-row { display: flex; align-items: center; gap: 0.25rem; width: 100%; }
|
||||||
|
wc-expression input {
|
||||||
|
background: var(--bg); border: 1px solid var(--border); font: inherit;
|
||||||
|
padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-expression .expr-name { width: 3em; }
|
||||||
|
wc-expression .expr-formula-container {
|
||||||
|
display: flex; gap: 0.25rem; width: 100%;
|
||||||
|
}
|
||||||
|
wc-expression .expr-latex {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font-family: 'Courier New', monospace; font-size: 0.75rem;
|
||||||
|
padding: 0.2rem; resize: vertical; min-width: 4em;
|
||||||
|
}
|
||||||
|
wc-expression .expr-rendered {
|
||||||
|
min-width: 3em; min-height: 2em; display: flex; align-items: center;
|
||||||
|
justify-content: center; background: #fafafa; border: 1px solid #eee;
|
||||||
|
border-radius: 3px; padding: 0.2rem 0.4rem; font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
wc-expression .expr-varhooks {
|
||||||
|
position: absolute; top: -7px; left: 0; right: 0; z-index: 3;
|
||||||
|
display: flex; justify-content: center; gap: 6px;
|
||||||
|
pointer-events: none; height: 14px;
|
||||||
|
}
|
||||||
|
wc-expression .expr-varhooks .hook {
|
||||||
|
position: static; pointer-events: auto; flex-shrink: 0;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-varref { gap: 0.25rem; padding-bottom: 1rem; }
|
||||||
|
wc-varref .varref-label { font-weight: bold; font-size: 0.65rem; color: var(--muted); letter-spacing: 0.05em; }
|
||||||
|
wc-varref .varref-select {
|
||||||
|
background: var(--bg); border: 1px solid var(--border); font: inherit;
|
||||||
|
font-size: 0.75rem; padding: 0.15rem 0.2rem; width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-quantity { gap: 0.25rem; }
|
||||||
|
wc-quantity .quantity-header { display: flex; align-items: center; gap: 0.3rem; width: 100%; }
|
||||||
|
wc-quantity .quantity-label { font-weight: bold; font-size: 0.7rem; }
|
||||||
|
wc-quantity .quantity-name {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-quantity .dims { display: flex; flex-wrap: wrap; gap: 0.2rem; justify-content: center; }
|
||||||
|
wc-quantity .dims label { display: flex; align-items: center; gap: 0.1rem; font-size: 0.75rem; font-weight: bold; }
|
||||||
|
wc-quantity .dims input {
|
||||||
|
width: 2.5em; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
padding: 0.1rem; font: inherit; text-align: center;
|
||||||
|
}
|
||||||
|
wc-quantity .range { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
||||||
|
wc-quantity .range input {
|
||||||
|
width: 4em; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
padding: 0.1rem 0.2rem; font: inherit; text-align: center;
|
||||||
|
}
|
||||||
|
wc-quantity .quantity-footer { width: 100%; font-size: 0.75rem; }
|
||||||
|
wc-quantity .quantity-footer select {
|
||||||
|
width: 100%; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-unit { gap: 0.25rem; }
|
||||||
|
wc-unit .unit-header { display: flex; align-items: center; gap: 0.3rem; width: 100%; }
|
||||||
|
wc-unit .unit-label { font-weight: bold; font-size: 0.7rem; }
|
||||||
|
wc-unit .unit-fullname {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; padding: 0.15rem 0.3rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-unit .unit-body { display: flex; flex-direction: column; gap: 0.2rem; width: 100%; }
|
||||||
|
wc-unit .unit-body label { display: flex; align-items: center; gap: 0.3rem; font-size: 0.8rem; }
|
||||||
|
wc-unit .unit-body select, wc-unit .unit-body input {
|
||||||
|
flex: 1; background: var(--bg); border: 1px solid var(--border);
|
||||||
|
font: inherit; padding: 0.1rem 0.2rem; text-align: center;
|
||||||
|
}
|
||||||
|
wc-unit .unit-si { width: 7em; }
|
||||||
|
wc-unit .unit-resolved { font-size: 0.75rem; color: var(--muted); align-self: flex-end; }
|
||||||
|
|
||||||
|
.error-overlay {
|
||||||
|
display: none; position: fixed; z-index: 99999;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.45);
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.error-overlay.active { display: flex; }
|
||||||
|
.error-box {
|
||||||
|
background: #d32f2f; color: #fff; padding: 2rem;
|
||||||
|
border-radius: 8px; max-width: 400px; width: 90%;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.35);
|
||||||
|
text-align: center; position: relative;
|
||||||
|
}
|
||||||
|
.error-ack {
|
||||||
|
font-size: 2.2rem; font-weight: 900;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
letter-spacing: 0.1em; margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.error-close {
|
||||||
|
position: absolute; top: 0.3rem; right: 0.75rem;
|
||||||
|
cursor: pointer; font-size: 1.8rem; line-height: 1;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.error-close:hover { opacity: 1; }
|
||||||
|
.error-msg { font-size: 1rem; line-height: 1.5; }
|
||||||
|
|
||||||
svg#connections {
|
svg#connections {
|
||||||
z-index: -1;
|
z-index: 0; background: transparent; position: absolute;
|
||||||
background: transparent;
|
top: 0; left: 0; width: 100%; height: 100%;
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.flow {
|
|
||||||
fill: none;
|
|
||||||
stroke: red;
|
|
||||||
stroke-width: 5;
|
|
||||||
}
|
}
|
||||||
|
path.flow { fill: none; stroke: var(--accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; transition: stroke 0.1s, stroke-width 0.1s; }
|
||||||
|
path.flow:hover { stroke: #d32f2f; stroke-width: 3; cursor: pointer; }
|
||||||
|
path.flow-shadow { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 5 3; pointer-events: none; opacity: 0.6; }
|
||||||
|
path.flow-data { fill: none; stroke: #43a047; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; transition: stroke 0.1s, stroke-width 0.1s; }
|
||||||
|
path.flow-data:hover { stroke: #d32f2f; stroke-width: 3; cursor: pointer; }
|
||||||
|
path.flow-shadow-data { fill: none; stroke: #43a047; stroke-width: 1.5; stroke-dasharray: 5 3; pointer-events: none; opacity: 0.6; }
|
||||||
|
|
||||||
button.hook {
|
button.hook {
|
||||||
display: block;
|
display: block; width: 14px; height: 14px; border-radius: 50%;
|
||||||
width: 2em;
|
cursor: pointer; padding: 0; z-index: 2;
|
||||||
height: 2em;
|
transition: transform 0.1s, box-shadow 0.1s;
|
||||||
border-radius: 1em;
|
|
||||||
contain: layout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-end {
|
button.hook[data-direction="out"] {
|
||||||
background-color: #a0ffa0;
|
background: var(--accent); border: 2px solid var(--accent);
|
||||||
|
position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
button.hook[data-direction="out"]:hover {
|
||||||
|
transform: translateX(-50%) scale(1.3);
|
||||||
|
box-shadow: 0 0 6px rgba(74,144,217,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
wc-start {
|
button.hook[data-direction="in"] {
|
||||||
background-color: #ffa0a0;
|
background: var(--bg); border: 2px dashed var(--muted);
|
||||||
|
position: absolute; top: -7px; left: 50%; transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
button.hook[data-direction="in"]:hover {
|
||||||
|
border-color: var(--accent); border-style: solid; background: #e8f0fe;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
button.hook:not([data-direction]) {
|
||||||
white-space: pre-wrap;
|
opacity: 0.2; pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.hook[data-link-type="data"][data-direction="out"] {
|
||||||
|
background: #43a047; border-color: #43a047;
|
||||||
|
}
|
||||||
|
button.hook[data-link-type="data"][data-direction="out"]:hover {
|
||||||
|
transform: translateX(-50%) scale(1.3);
|
||||||
|
box-shadow: 0 0 6px rgba(67,160,71,0.5);
|
||||||
|
}
|
||||||
|
button.hook[data-link-type="data"][data-direction="in"] {
|
||||||
|
border-color: #43a047; background: #e8f5e9;
|
||||||
|
}
|
||||||
|
button.hook[data-link-type="data"][data-direction="in"]:hover {
|
||||||
|
background: #c8e6c9; border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-output #hook-fore[data-direction="in"] {
|
||||||
|
left: -7px; top: 50%; transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
wc-output #hook-next[data-direction="out"] {
|
||||||
|
right: -7px; top: 50%; transform: translateY(-50%); bottom: auto; left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
wc-decision #hook-fore[data-direction="in"] {
|
||||||
|
left: -7px; top: 50%; transform: translateY(-50%); bottom: auto;
|
||||||
|
}
|
||||||
|
wc-decision #hook-out1[data-direction="out"] {
|
||||||
|
right: -7px; top: -7px; bottom: auto; left: auto; transform: none;
|
||||||
|
}
|
||||||
|
wc-decision #hook-out0[data-direction="out"] {
|
||||||
|
right: -7px; bottom: -7px; top: auto; left: auto; transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
box-shadow: 0 0 0 2px var(--accent), 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute; top: -8px; right: -8px;
|
||||||
|
width: 18px; height: 18px; border-radius: 50%;
|
||||||
|
border: 1px solid #ccc; background: #fff; color: #999;
|
||||||
|
font-size: 13px; line-height: 1; cursor: pointer; padding: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
z-index: 20; opacity: 0; transition: opacity 0.12s;
|
||||||
|
}
|
||||||
|
.delete-btn:hover {
|
||||||
|
opacity: 1 !important; background: #d32f2f; color: #fff; border-color: #d32f2f;
|
||||||
|
}
|
||||||
|
[data-block]:hover .delete-btn, .selected .delete-btn {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea { white-space: pre-wrap; }
|
||||||
|
select {
|
||||||
|
background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
||||||
|
font: inherit; font-size: 0.75rem; padding: 0.1rem 0.2rem;
|
||||||
|
}
|
||||||
|
select option { background: var(--bg); color: var(--text); }
|
||||||
|
input { color-scheme: light; }
|
||||||
|
* { scrollbar-color: var(--border) var(--bg); scrollbar-width: thin; }
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
// ── WCBlock — draggable/linkable block base class ──────────────────────────
|
||||||
|
//
|
||||||
|
// Base class for all draggable, linkable flowchart and measurement blocks.
|
||||||
|
// Provides grid-snapped drag-and-drop and a two-click link system between .hook buttons.
|
||||||
|
// Each block tracks incoming (wcForeFlow) and outgoing (wcNextFlow) SVG path references.
|
||||||
|
//
|
||||||
|
// Subclasses override generateCode() / generateOwnCode() to emit C code fragments.
|
||||||
|
//
|
||||||
|
// Load order dependencies:
|
||||||
|
// dom.js → svgNamespace, $id
|
||||||
|
// error.js → showError
|
||||||
|
// draw.js → linkageFirst, resetLinkage, startShadowLink, drawpath, deleteLink
|
||||||
|
//
|
||||||
|
// This file is loaded first among the block files.
|
||||||
|
// → blocks-algo.js, blocks-data.js, blocks-proc.js, wcprogram.js
|
||||||
|
|
||||||
|
class WCBlock extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.dataset.block = "";
|
||||||
|
this.addEventListener("mousedown", this.startDragging);
|
||||||
|
this.wcForeFlow = [];
|
||||||
|
this.wcNextFlow = [];
|
||||||
|
this._gridSize = null;
|
||||||
|
this._el = {};
|
||||||
|
this._dragBound = ev => this.dragAround(ev);
|
||||||
|
this._stopBound = () => this.stopDragging();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (this._hooksSetup) return;
|
||||||
|
this._hooksSetup = true;
|
||||||
|
for (const hook of this.querySelectorAll(".hook")) {
|
||||||
|
this._setupHook(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addEventListener("mousedown", () => { this._dragMoved = false; });
|
||||||
|
this.addEventListener("click", ev => {
|
||||||
|
if (ev.target.closest(".hook") || ev.target.closest(".delete-btn") || ev.target.closest(".resize-handle")) return;
|
||||||
|
if (this._dragMoved) return;
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._select();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(this instanceof WCStart) && !(this instanceof WCEnd)) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "delete-btn";
|
||||||
|
btn.textContent = "\u00D7";
|
||||||
|
btn.title = "delete";
|
||||||
|
btn.addEventListener("click", ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.delete();
|
||||||
|
});
|
||||||
|
this.appendChild(btn);
|
||||||
|
const rh = document.createElement("div");
|
||||||
|
rh.className = "resize-handle";
|
||||||
|
rh.addEventListener("mousedown", ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
const startX = ev.clientX, startY = ev.clientY;
|
||||||
|
const startW = this.offsetWidth, startH = this.offsetHeight;
|
||||||
|
const move = e => {
|
||||||
|
this.style.width = Math.max(60, startW + e.clientX - startX) + "px";
|
||||||
|
this.style.height = Math.max(30, startH + e.clientY - startY) + "px";
|
||||||
|
for (const p of this.wcNextFlow) drawpath(p);
|
||||||
|
for (const p of this.wcForeFlow) drawpath(p);
|
||||||
|
};
|
||||||
|
const up = () => {
|
||||||
|
document.removeEventListener("mousemove", move);
|
||||||
|
document.removeEventListener("mouseup", up);
|
||||||
|
};
|
||||||
|
document.addEventListener("mousemove", move);
|
||||||
|
document.addEventListener("mouseup", up);
|
||||||
|
});
|
||||||
|
this.appendChild(rh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_select() {
|
||||||
|
for (const el of document.querySelectorAll(".selected")) {
|
||||||
|
el.classList.remove("selected");
|
||||||
|
}
|
||||||
|
this.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
resetLinkage();
|
||||||
|
for (const path of [...this.wcForeFlow]) deleteLink(path);
|
||||||
|
for (const path of [...this.wcNextFlow]) deleteLink(path);
|
||||||
|
this.wcForeFlow = [];
|
||||||
|
this.wcNextFlow = [];
|
||||||
|
this.remove();
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupHook(hook) {
|
||||||
|
hook.addEventListener("click", ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const linkType = hook.dataset.linkType;
|
||||||
|
|
||||||
|
if (!linkageFirst) {
|
||||||
|
if (hook.dataset.direction !== "out") return;
|
||||||
|
linkageFirst = { block: this, hook };
|
||||||
|
startShadowLink(hook);
|
||||||
|
} else if (this === linkageFirst.block) {
|
||||||
|
showError("Cannot link a block to itself.");
|
||||||
|
resetLinkage();
|
||||||
|
} else {
|
||||||
|
if (hook.dataset.direction !== "in") {
|
||||||
|
showError("Must link to an input hook.");
|
||||||
|
resetLinkage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstType = linkageFirst.hook.dataset.linkType;
|
||||||
|
if (firstType !== linkType) {
|
||||||
|
showError("Cannot link " + (firstType || "logic") + " to " + (linkType || "logic") + " hook.");
|
||||||
|
resetLinkage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (linkType === "data") {
|
||||||
|
if (linkageFirst.hook.dataset.type !== hook.dataset.type) {
|
||||||
|
showError("Data type mismatch: " + (linkageFirst.hook.dataset.type || "?") + " \u2260 " + (hook.dataset.type || "?"));
|
||||||
|
resetLinkage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const path = document.createElementNS(svgNamespace, "path");
|
||||||
|
path.toElement = this;
|
||||||
|
path.fromElement = linkageFirst.block;
|
||||||
|
path.fromHook = linkageFirst.hook;
|
||||||
|
path.toHook = hook;
|
||||||
|
path.dataset.linkType = linkType || "logic";
|
||||||
|
path.classList.add(linkType === "data" ? "flow-data" : "flow");
|
||||||
|
this.wcForeFlow.push(path);
|
||||||
|
linkageFirst.block.wcNextFlow.push(path);
|
||||||
|
$id("connections").appendChild(path);
|
||||||
|
drawpath(path);
|
||||||
|
resetLinkage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get dataType() { return null; }
|
||||||
|
|
||||||
|
saveData() { return {}; }
|
||||||
|
|
||||||
|
restoreData(data) {}
|
||||||
|
|
||||||
|
generateOwnCode() { return ""; }
|
||||||
|
|
||||||
|
generateCode(visited = new Set()) {
|
||||||
|
if (visited.has(this)) return "";
|
||||||
|
visited.add(this);
|
||||||
|
return this.generateOwnCode() + this.generateNextCode(visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNextCode(visited) {
|
||||||
|
let code = "";
|
||||||
|
for (const path of this.wcNextFlow) {
|
||||||
|
code += path.toElement.generateCode(visited);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getGridSize() {
|
||||||
|
if (this._gridSize === null) {
|
||||||
|
this._gridSize = parseInt(
|
||||||
|
window.getComputedStyle(this).getPropertyValue("--grid-size")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._gridSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
startDragging(event) {
|
||||||
|
if (event.target.closest(".resize-handle")) return;
|
||||||
|
this._dragMoved = false;
|
||||||
|
this._getGridSize();
|
||||||
|
this.dragStartX = event.clientX;
|
||||||
|
this.dragStartY = event.clientY;
|
||||||
|
document.onmousemove = this._dragBound;
|
||||||
|
document.onmouseup = this._stopBound;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragAround(event) {
|
||||||
|
this._dragMoved = true;
|
||||||
|
const snap = v => Math.round(v / this._gridSize) * this._gridSize;
|
||||||
|
this.style.top = snap(this.offsetTop + event.clientY - this.dragStartY) + "px";
|
||||||
|
this.style.left = snap(this.offsetLeft + event.clientX - this.dragStartX) + "px";
|
||||||
|
this.dragStartX = snap(event.clientX);
|
||||||
|
this.dragStartY = snap(event.clientY);
|
||||||
|
for (const p of this.wcNextFlow) drawpath(p);
|
||||||
|
for (const p of this.wcForeFlow) drawpath(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopDragging() {
|
||||||
|
document.onmousemove = null;
|
||||||
|
document.onmouseup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// ── WCProgram — root container + block registration ──────────────────────────
|
||||||
|
//
|
||||||
|
// WCProgram is the root custom element that owns the flowchart workspace.
|
||||||
|
// Unlike other blocks, WCProgram extends HTMLElement directly — not WCBlock —
|
||||||
|
// because the program container itself is not draggable or linkable.
|
||||||
|
//
|
||||||
|
// This file also contains the block registry and registration calls.
|
||||||
|
// Must be loaded AFTER all block class definitions exist in the page.
|
||||||
|
//
|
||||||
|
// Load order dependencies:
|
||||||
|
// All block class files must be loaded first:
|
||||||
|
// wcblock.js → blocks-algo.js → blocks-data.js → blocks-proc.js
|
||||||
|
// helpers.js → toCName
|
||||||
|
// dom.js → $id
|
||||||
|
//
|
||||||
|
// This file is loaded last among the block group, before persist.js and app.js.
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// WCProgram — root container
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
class WCProgram extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
const startFrag = document.importNode(
|
||||||
|
$id("wc-start").content, true
|
||||||
|
);
|
||||||
|
const endFrag = document.importNode(
|
||||||
|
$id("wc-end").content, true
|
||||||
|
);
|
||||||
|
startFrag.firstElementChild.id = "main-start";
|
||||||
|
endFrag.firstElementChild.id = "main-end";
|
||||||
|
this.appendChild(startFrag);
|
||||||
|
this.appendChild(endFrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeNode(tag) {
|
||||||
|
const info = BLOCK_REGISTRY[tag];
|
||||||
|
if (!info) return null;
|
||||||
|
this.counter++;
|
||||||
|
const frag = document.importNode(document.querySelector(info.template).content, true);
|
||||||
|
const el = frag.firstElementChild;
|
||||||
|
el.id = info.prefix + this.counter;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode(tag, parent) {
|
||||||
|
const info = BLOCK_REGISTRY[tag];
|
||||||
|
if (!info) return null;
|
||||||
|
const el = this.makeNode(tag);
|
||||||
|
if (!el) return null;
|
||||||
|
(parent || (info.getParent ? info.getParent() : this)).appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
newDecisionNode() { return this.newNode("wc-decision"); }
|
||||||
|
newExpressionNode() { return this.newNode("wc-expression"); }
|
||||||
|
newVarRefNode() { return this.newNode("wc-varref"); }
|
||||||
|
newOutputNode() { return this.newNode("wc-output"); }
|
||||||
|
newProcCallNode() { const el = this.newNode("wc-proccall"); if (el) refreshProcOptions(); return el; }
|
||||||
|
|
||||||
|
newUnitNode() {
|
||||||
|
const el = this.newNode("wc-unit", $id("data-area"));
|
||||||
|
if (el) refreshAll();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newQuantityNode() {
|
||||||
|
const el = this.newNode("wc-quantity", $id("data-area"));
|
||||||
|
if (el) { refreshQuantityOptions(); refreshDefaultUnitOptions(); }
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newBooleanNode() {
|
||||||
|
const el = this.newNode("wc-boolean", $id("variable-area"));
|
||||||
|
if (el) refreshVarRefOptions();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newScalarNode() {
|
||||||
|
const el = this.newNode("wc-scalar", $id("variable-area"));
|
||||||
|
if (el) { refreshUnitOptions(); refreshVarRefOptions(); }
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newStringNode() {
|
||||||
|
const el = this.newNode("wc-string", $id("variable-area"));
|
||||||
|
if (el) refreshVarRefOptions();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newArrayNode() {
|
||||||
|
const el = this.newNode("wc-array", $id("variable-area"));
|
||||||
|
if (el) refreshVarRefOptions();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
newProcedureNode(libName) {
|
||||||
|
const libPane = libName
|
||||||
|
? document.querySelector(`.lib-pane[data-lib-name="${libName}"]`)
|
||||||
|
: document.querySelector(".lib-pane");
|
||||||
|
if (!libPane) return null;
|
||||||
|
const el = this.newNode("wc-procedure", libPane.querySelector(".procedures-area"));
|
||||||
|
if (el) refreshProcOptions();
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gatherUsedTypes() {
|
||||||
|
const usedQuantities = new Set(), usedUnits = new Set();
|
||||||
|
for (const d of $id("variable-area").querySelectorAll("wc-scalar")) {
|
||||||
|
const t = d.querySelector(".var-type").value;
|
||||||
|
if (t) usedQuantities.add(t);
|
||||||
|
const u = d.querySelector(".var-unit").value;
|
||||||
|
if (u) usedUnits.add(u);
|
||||||
|
}
|
||||||
|
return { usedQuantities, usedUnits };
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateTypeDefs(usedQuantities) {
|
||||||
|
return Array.from(
|
||||||
|
$id("data-area").querySelectorAll("wc-quantity")
|
||||||
|
).filter(q => usedQuantities.has(q.querySelector(".quantity-name").value || "unnamed"))
|
||||||
|
.map(q => {
|
||||||
|
const name = q.querySelector(".quantity-name").value || "unnamed";
|
||||||
|
return `typedef double ${name};`;
|
||||||
|
}).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateUnitDefs(usedUnits) {
|
||||||
|
return Array.from(
|
||||||
|
$id("data-area").querySelectorAll("wc-unit")
|
||||||
|
).filter(u => usedUnits.has(u.querySelector(".unit-suffix").value))
|
||||||
|
.map(u => {
|
||||||
|
const type = u.querySelector(".unit-type").value || "unknown";
|
||||||
|
const suffix = u.querySelector(".unit-suffix").value || "";
|
||||||
|
const fullname = u.querySelector(".unit-fullname").value || "";
|
||||||
|
const si = u.querySelector(".unit-si").value || "1";
|
||||||
|
return `// unit: ${fullname} (${suffix}) = 1 ${type} * ${si}`;
|
||||||
|
}).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateVariableDeclarations() {
|
||||||
|
const boolDefs = Array.from($id("variable-area").querySelectorAll("wc-boolean")).map(d => d.generateCode()).join("\n");
|
||||||
|
const scalarDefs = Array.from($id("variable-area").querySelectorAll("wc-scalar")).map(d => d.generateCode()).join("\n");
|
||||||
|
const stringDefs = Array.from($id("variable-area").querySelectorAll("wc-string")).map(d => d.generateCode()).join("\n");
|
||||||
|
const arrayDefs = Array.from($id("variable-area").querySelectorAll("wc-array")).map(d => d.generateCode()).join("\n");
|
||||||
|
const exprDefs = Array.from(this.querySelectorAll("wc-expression")).map(d => d.generateCode()).join("\n");
|
||||||
|
return { boolDefs, scalarDefs, stringDefs, arrayDefs, exprDefs };
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateIncludes(proccalls, hasStrings) {
|
||||||
|
const libNames = new Set();
|
||||||
|
for (const p of proccalls) {
|
||||||
|
const qname = p.querySelector(".proccall-select").value;
|
||||||
|
if (qname) {
|
||||||
|
const lib = qname.split("/")[0];
|
||||||
|
if (lib) libNames.add(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let includes = Array.from(libNames).map(l => `#include <${l}.h>`).join("\n");
|
||||||
|
if (hasStrings) {
|
||||||
|
const hdr = "#include <stdlib.h>\n#include <string.h>";
|
||||||
|
includes = includes ? includes + "\n" + hdr : hdr;
|
||||||
|
}
|
||||||
|
return includes;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCode() {
|
||||||
|
const { usedQuantities, usedUnits } = this._gatherUsedTypes();
|
||||||
|
|
||||||
|
const typeDefs = this._generateTypeDefs(usedQuantities);
|
||||||
|
const unitDefs = this._generateUnitDefs(usedUnits);
|
||||||
|
const { boolDefs, scalarDefs, stringDefs, arrayDefs, exprDefs } = this._generateVariableDeclarations();
|
||||||
|
|
||||||
|
const hasStrings = $id("variable-area").querySelectorAll("wc-string").length > 0;
|
||||||
|
const stringStruct = hasStrings
|
||||||
|
? "typedef struct { char *data; long int length; } string;\n\n"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const body = this.firstElementChild.generateCode(new Set());
|
||||||
|
const proccalls = this.querySelectorAll("wc-proccall");
|
||||||
|
const includes = this._generateIncludes(proccalls, hasStrings);
|
||||||
|
|
||||||
|
const stringNames = Array.from($id("variable-area").querySelectorAll("wc-string")).map(
|
||||||
|
d => toCName(d.querySelector(".var-name").value) || "s"
|
||||||
|
);
|
||||||
|
const freeSection = hasStrings
|
||||||
|
? "\n" + stringNames.map(n => `free(${n}.data);`).join("\n") + "\n"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const dataSection = (boolDefs || scalarDefs || stringDefs || arrayDefs || exprDefs)
|
||||||
|
? [boolDefs, scalarDefs, stringDefs, arrayDefs, exprDefs]
|
||||||
|
.filter(Boolean).map(s => s + "\n").join("")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
$id("c-code").value =
|
||||||
|
"// --- WiseCode\u00ae ---\n" +
|
||||||
|
(includes ? includes + "\n\n" : "") +
|
||||||
|
"int main() {\n" +
|
||||||
|
(typeDefs ? typeDefs + "\n\n" : "") +
|
||||||
|
(unitDefs ? unitDefs + "\n\n" : "") +
|
||||||
|
stringStruct + dataSection + body + freeSection +
|
||||||
|
"return 0;\n" +
|
||||||
|
"}\n// --- END ---";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// Block registry
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const BLOCK_REGISTRY = {};
|
||||||
|
const BLOCK_ACTIONS = {};
|
||||||
|
|
||||||
|
function registerBlock(tag, cls, prefix, creatable, getParent) {
|
||||||
|
customElements.define(tag, cls);
|
||||||
|
BLOCK_REGISTRY[tag] = { cls, template: "#" + tag, prefix: prefix || tag.replace("wc-", ""), getParent };
|
||||||
|
if (creatable) BLOCK_ACTIONS[creatable] = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerBlock("wc-start", WCStart, "start", null, null);
|
||||||
|
registerBlock("wc-end", WCEnd, "end", null, null);
|
||||||
|
registerBlock("wc-program", WCProgram, "program", null, null);
|
||||||
|
registerBlock("wc-output", WCOutput, "output", "output", null);
|
||||||
|
registerBlock("wc-decision", WCDecision, "decision", "decision", null);
|
||||||
|
registerBlock("wc-boolean", WCBoolean, "bool", "boolean", () => $id("variable-area"));
|
||||||
|
registerBlock("wc-scalar", WCScalar, "scalar", "scalar", () => $id("variable-area"));
|
||||||
|
registerBlock("wc-string", WCString, "str", "string", () => $id("variable-area"));
|
||||||
|
registerBlock("wc-array", WCArray, "arr", "array", () => $id("variable-area"));
|
||||||
|
registerBlock("wc-varref", WCVarRef, "varref", "varref", null);
|
||||||
|
registerBlock("wc-expression", WCExpression, "expr", "expression", null);
|
||||||
|
registerBlock("wc-quantity", WCQuantity, "quantity", "quantity", () => $id("data-area"));
|
||||||
|
registerBlock("wc-unit", WCUnit, "unit", "unit", () => $id("data-area"));
|
||||||
|
registerBlock("wc-procedure", WCProcedure, "proc", null, null);
|
||||||
|
registerBlock("wc-proccall", WCProcCall, "proccall", "proccall", null);
|
||||||
|
registerBlock("wc-assign", WCAssign, "assign", "assign", null);
|
||||||
|
|
||||||
|
const _connObserver = new MutationObserver(() => {
|
||||||
|
for (const el of document.querySelectorAll("wc-assign")) el._updateValueVisibility();
|
||||||
|
});
|
||||||
|
const _connSvg = document.getElementById("connections");
|
||||||
|
if (_connSvg) _connObserver.observe(_connSvg, { childList: true, subtree: true });
|
||||||