Skip to content

Architecture ​

Layers ​

mermaid
flowchart TB
    subgraph UI["React UI (src/pages · src/components)"]
        A1["Welcome · Workbench<br/>(TopBar/Sidebar/Central/Right/Status)"]
        A2["Settings · Share ·<br/>Plugin dialogs · Example-data dialogs"]
        A3["Flow mode canvas<br/>Palette · Canvas · Node · Param editor<br/>Toolbar · Result preview"]
        A4["Block mode<br/>Blockly workspace · Toolbar<br/>Preview / Variables / Console cards"]
    end

    subgraph State["State & Core Services"]
        B1["Zustand stores<br/>app / project / plugin / settings / block / editor"]
        B2["Core services<br/>storage (IndexedDB) · events (bus)<br/>i18n · theming · perf<br/>fileFormat · wasm · gpu<br/>scene3d · sandbox<br/>stats · io · plot · repro"]
    end

    subgraph Blocks["Block system (Flow mode, src/blocks)"]
        D1["Catalog<br/>data_source · transform · filter<br/>math · statistics · plot · visualize"]
        D2["Compiler<br/>pure · validates ports/types<br/>topological sort · diagnostics"]
        D3["Executor<br/>incremental cache<br/>dirty propagation · run()"]
        D4["Render bridge<br/>viz.* RenderedView → plugin.loadData"]
    end

    subgraph Editor["Block mode (src/editor)"]
        E1["IR<br/>shared node union · validate · hash<br/>(also powers Code mode)"]
        E2["Blockly engine + block definitions<br/>workspace JSON ⇄ IR convert"]
        E3["Interpreter<br/>walks IR · calls studio.*"]
        E4["Codegen<br/>IR → JS · IR → Python"]
        E5["StudioApi<br/>load / transform / stats / plot / print<br/>(reuses src/blocks/ops)"]
    end

    subgraph Runtime["Runtime Layer"]
        C1["Plugin runtime<br/>builtin/* (49 core + 10 fun)<br/>marketplace catalog<br/>cspkg loader (sandbox)<br/>registry & lifecycle"]
        C2["Native core (Rust→WASM)<br/>device mgmt · compute<br/>kernel scheduling<br/>file-kind detection"]
    end

    UI --> B1
    UI --> B2
    B1 <--> B2
    B1 --> C1
    B2 --> C1
    B2 --> C2
    A3 --> B1
    B1 --> D2
    B1 --> D3
    D1 --> D2
    D3 --> D4
    D4 --> C1
    A4 --> E2
    E2 --> E1
    E1 --> E3
    E1 --> E4
    E3 --> E5
    E5 --> D4

One plugin bridge, two callers. D4 (Flow's RenderedView → plugin.loadData) and E5 → D4 (Block mode's studio.plot → RenderedView) collapse onto the same code path. Adding a visualisation in Flow mode lights up a studio.plot(...) variant in Block mode for free. See Block Mode for the editor layer in detail, and Flow Mode for the block system.

Scientific computing subsystems ​

Four pure-TypeScript, DOM-light, node-testable subsystems live under src/core/ and are consumed by the block system, the Studio API, and the workbench UI:

SubsystemPathSurface
Statistics kernelsrc/core/stats/descriptive stats, special functions, hypothesis tests, effect sizes, multiple-comparison corrections, power analysis
Scientific I/Osrc/core/io/one dispatcher (loadScientificData) routing to HDF5 / NetCDF / FITS / Zarr / Parquet loaders, returning RawVariable[]
Plot enginesrc/core/plot/pure-TS SVG rendering with linear/log/temporal scales, plus SVG/PDF export
Reproducibilitysrc/core/repro/seeded RNG (mulberry32), stable hashing, run manifests, DAG-to-Python export

State management ​

Five Zustand stores hold all application state:

StoreResponsibility
appStorehost status, banners, notifications, perf metrics, panel toggles
projectStorecurrent project, recent list, save/autosave, share, param persistence
pluginStoreregistry, load/activate lifecycle, file dispatch, host containers
settingsStoreGPU mode, autosave interval, and other preferences
editorStoreBlock / Code mode sessions (IR, code, variables, console)

Cross-cutting UI communication uses a small typed event bus (src/core/events.ts) — e.g. plugin:<id>:params, host:params:changed, host:file:choose-plugin.

Rendering pipeline ​

The central viewport owns three surfaces:

  1. central-canvas — the shared 2D canvas every 2D plugin draws into.

  2. central-dom-host — a DOM container for plugins that need elements (also where the sandboxed canvas surfaces are mounted).

  3. Three.js scene — created lazily by scene3d.ts only when a plugin declares renderToScene. The plugin store decides visibility centrally on every activation:

    • plugin declares 3D → show the scene, clear any stale 2D frame;
    • anything else → hide the scene immediately.

    This guarantees a 3D coordinate system can never appear over a 2D viewport (and vice versa).

Native core ​

native/ergalics-core (Rust) compiles to wasm32-unknown-unknown and is bound with wasm-bindgen into src/native/. It requires the unstable web-sys WebGPU bindings, enabled via rustflags = ["--cfg=web_sys_unstable_apis"] in native/.cargo/config.toml. See Native Core & WebGPU for the exact API surface and the calling conventions.

Dependency rules ​

  • pages → stores → core — lower layers never import higher ones.
  • core must stay DOM-light: pure services (i18n, fileFormat, storage adapters) are unit-testable in a node environment.
  • The plugin contract (src/types/plugin.ts) is the only shared vocabulary between the host and third-party code.

Research module layering ​

The research features (experiment tracking, uncertainty, units, lineage, chunked I/O, figure studio, supplementary bundles, notebook) all follow one three-layer convention — they grow into the existing structure:

  1. Data/logic layer — src/core/<feature>/: pure TypeScript, no React, no store imports. May only depend on sibling core modules (repro, plot, io, gpu) and fflate/apache-arrow style leaf libraries. Every module ships unit tests under tests/ mirroring the path.
  2. Business layer — src/stores/<feature>Store.ts: orchestration only — calls core, persists through projectStore / storage.ts, subscribes to host event channels. No UI logic, no direct DOM.
  3. Presentation layer — src/pages/ and src/components/: consumes stores exclusively; styling via the global.css design tokens (no hard-coded colors/spacing); every user-facing string goes through i18n (zh-CN + en-US in the same change).

Cross-module communication prefers the typed host channels at the bottom of src/core/events.ts (run:completed, data:ingested, lineage:changed, figure:exported, notebook:executed) over store→store calls; direct calls are reserved for genuine parent/child relationships (e.g. projectStore coordinating reset on project open).

Persistence split: bulky, per-project-but-not-portable data (run records) lives in dedicated IndexedDB stores (storage.ts) and is cascade-deleted with the project; portable document state (notebook cells, figure sheets) lives on ProjectState and travels inside the .clproj.

Research data flow (who writes which state, who hears which event):

text
                         ┌────────────────────────────────────────────┐
                         │                event bus                   │
                         │  run:completed / data:ingested /           │
                         │  lineage:changed / figure:exported /       │
                         │  notebook:executed                         │
                         └──────▲───────────────▲─────────────▲───────┘
                                │               │             │
  Flow/Block/Code/Notebook ─────┘               │             └───── figureStore
  execution points                              │
  (recordRun / chunkStore)                      │
        │                                       │
        ▼                                       │
  IndexedDB `runs` store ──► experimentStore ───┘
        │                       ▲
        │ listRuns()            │ writes through
        ▼                       │
  lineageStore ──► LineageCanvas│
        ▲                       │
        │                       │
  chunkStore ───► data:ingested │
                                │
        project.state (portable, inside .clproj)
        ├── figureSheets ◄────── figureStore (write-through + dirty)
        ├── notebook      ◄────── notebookStore (write-through + dirty)
        └── blockGraph / editorSessions ◄── apply*() at save time

Every arrow into the bus is a store emitting after a state transition; every arrow out is a long-lived subscription registered in the store's init* function (called once from App.tsx).

Released under the MIT License.