diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 000000000..51277e5c3 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,160 @@ +# RapidRAW — Developer Guide + +--- + +## Development Environment Setup + +### Prerequisites + +| Tool | Version | Notes | +|------|---------|-------| +| **Rust** | 1.94.0 (pinned via `../src-tauri/rust-toolchain.toml`) | Install via [rustup](https://rustup.rs/) | +| **Node.js** | ≥ 18 LTS | For the React frontend | + +### Platform Dependencies + +**macOS:** `xcode-select --install` + +**Linux:** +```bash +sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev \ + libayatana-appindicator3-dev librsvg2-dev +``` + +**Windows:** [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++" + +### First Run + +```bash +git clone https://github.com/CyberTimon/RapidRAW.git && cd RapidRAW +npm install +npm start # runs `tauri dev` — launches Vite on :1420 + compiles Rust backend +``` + +> **Note:** First build takes **5–10 minutes** (large Rust dependency tree). The build script also auto-downloads the ONNX Runtime binary for your platform via `../src-tauri/build.rs`. Incremental rebuilds are fast. + +--- + +## Architecture Overview + +![RapidRAW Architecture](img/architecture.excalidraw.png) + +All mutable backend state lives in a single `AppState` struct (in `../src-tauri/src/lib.rs`), accessed via `tauri::State`. Key fields include caches for the loaded image, GPU textures, masks, LUTs, and AI models — all behind `Mutex` or `Arc`. + +--- + +## Project Structure + +``` +../src/ # FRONTEND +├── App.tsx # Root component (~202KB — manages nearly all state) +├── components/ +│ ├── adjustments/ # Slider UIs: Basic, Color, Curves, Details, Effects +│ ├── modals/ # Dialogs: export, panorama, HDR, denoise, etc. +│ ├── panel/ +│ │ ├── Editor.tsx # Main editor layout +│ │ ├── MainLibrary.tsx # Library grid view +│ │ ├── editor/ +│ │ │ ├── ImageCanvas.tsx # Core image display + zoom/pan +│ │ │ └── Waveform.tsx # Histogram / waveform / vectorscope +│ │ └── right/ # Right panel tabs: Controls, Masks, AI, Presets, Export, Crop, Metadata +│ └── ui/ # Reusable primitives: Slider, Button, ColorWheel, etc. +├── hooks/ # useHistoryState, useKeyboardShortcuts, usePresets, etc. +├── utils/ +│ ├── adjustments.tsx # ⭐ Adjustment types, interfaces & defaults +│ ├── frontendLogBridge.ts # Forwards console.* → Rust log file +│ └── themes.tsx, ImageLRUCache.ts, etc. +└── context/ # Right-click context menus + +../src-tauri/src/ # BACKEND +├── lib.rs # ⭐ Core: AppState, all Tauri commands, app builder +├── gpu_processing.rs # wgpu device init, compute pipeline dispatch +├── image_processing.rs # CPU transforms, AllAdjustments repr(C) struct +├── file_management.rs # File I/O, sidecars, thumbnails, presets +├── ai_processing.rs # ONNX inference (SAM2, U2Net, CLIP, LaMa, DepthAnything) +├── mask_generation.rs # Mask bitmap generation from definitions +├── raw_processing.rs # rawler RAW → DynamicImage +├── exif_processing.rs # EXIF metadata extraction +├── lens_correction.rs # Lensfun database & correction +├── denoising.rs, tagging.rs, culling.rs, etc. +└── shaders/ + ├── shader.wgsl # ⭐ Main image processing shader (61KB) + ├── blur.wgsl # Gaussian blur compute shader + └── flare.wgsl # Lens flare effect +``` + +--- + +## How an Edit Works (Data Flow) + +1. User drags a slider → React updates adjustment state in `../src/App.tsx` +2. `invoke('apply_adjustments', { adjustments })` sends JSON to Rust +3. Rust checks transform hash — **reuses cached transformed image** if crop/rotate/flip unchanged +4. Adjustment JSON is deserialized into `AllAdjustments` (a `repr(C)` struct) and uploaded as a GPU uniform buffer +5. wgpu dispatches the compute shader, which processes all adjustments in one pass +6. Resulting RGBA8 pixels are read back and JPEG-encoded for the frontend + +The adjustment model exists in **three synced layers**: + +| Layer | File | Format | +|-------|------|--------| +| Frontend | `../src/utils/adjustments.tsx` | TypeScript `Adjustments` interface | +| Backend | `../src-tauri/src/image_processing.rs` | `AllAdjustments` `#[repr(C)]` struct | +| GPU | `../src-tauri/src/shaders/shader.wgsl` | WGSL uniform struct | + +> **Important:** Adding a new adjustment requires changes in **all three layers**, with correct `repr(C)` alignment in the Rust struct. + +--- + +## Caching + +Performance comes from aggressive caching — when only visual params change, the expensive transform step is skipped: + +| Cache | Key | Stores | +|-------|-----|--------| +| `full_transformed_cache` | transform hash | Full-res image post-crop/rotate/flip/warp | +| `gpu_image_cache` | — | GPU texture of current source image | +| `mask_cache` | mask definition hash | Generated mask bitmaps | +| `patch_cache` | patch ID | AI inpainting data (avoids re-sending large blobs) | +| `lut_cache` | file path | Parsed LUT data | +| `ImageLRUCache` (frontend) | image path | Recently viewed preview images | + +--- + +## Linting & CI + +```bash +# Frontend +npm run lint # ESLint +npm run format:check # Prettier + +# Backend (from src-tauri/) +cargo fmt --check +cargo clippy --all-targets --all-features -- -D warnings +``` + +CI runs on every push to `main` and every PR — full build matrix across Windows (x64/ARM), macOS (x64/ARM), Ubuntu (22.04/24.04, x64/ARM), and Android. + +--- + +## Common Recipes + +### Add a New Slider + +1. Add property + default to `Adjustments` / `INITIAL_ADJUSTMENTS` in `../src/utils/adjustments.tsx` +2. Add `` in the appropriate section component (e.g., `../src/components/adjustments/Basic.tsx`) +3. Add field to `AllAdjustments` in `../src-tauri/src/image_processing.rs` (watch alignment!) +4. Add uniform field + processing logic in `../src-tauri/src/shaders/shader.wgsl` +5. Handle backwards compat via `??` in `normalizeLoadedAdjustments` + +### Add a New Tauri Command + +1. Write `#[tauri::command]` function in the appropriate `.rs` module +2. Register in the `invoke_handler` at the bottom of `../src-tauri/src/lib.rs` +3. Call from frontend: `invoke('command_name', { ...args })` + +### Debugging + +- **Frontend DevTools:** `Cmd+Shift+I` (macOS) / `Ctrl+Shift+I` (Windows/Linux) inside the Tauri window +- **Backend logs:** `~/.config/RapidRAW/logs/` — all frontend `console.*` calls are also forwarded here via `../src/utils/frontendLogBridge.ts` +- **GPU backend issues:** Set `WGPU_BACKEND=metal|vulkan|dx12` env var before running diff --git a/docs/img/architecture.excalidraw b/docs/img/architecture.excalidraw new file mode 100644 index 000000000..e97568fad --- /dev/null +++ b/docs/img/architecture.excalidraw @@ -0,0 +1,797 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "text", + "id": "t1", + "x": 217.76351567498386, + "y": 20.364466703988057, + "width": 375.22711898859035, + "height": 40, + "text": "RapidRAW Architecture", + "fontSize": 32, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": null, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 68, + "versionNonce": 83428562, + "index": "a0", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223337880, + "link": null, + "locked": false, + "containerId": null, + "originalText": "RapidRAW Architecture", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "ui_bg", + "x": 40, + "y": 80, + "width": 720, + "height": 160, + "strokeColor": "#4a9eed", + "backgroundColor": "#dbe4ff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 30, + "roundness": { + "type": 3 + }, + "version": 2, + "versionNonce": 74575054, + "index": "a1", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t2", + "x": 60, + "y": 90, + "width": 322.9966577361911, + "height": 25, + "text": "Frontend (React / TypeScript)", + "fontSize": 20, + "strokeColor": "#2563eb", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 39, + "versionNonce": 648276302, + "index": "a2", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337880, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Frontend (React / TypeScript)", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b1", + "x": 80, + "y": 139.03780790147152, + "width": 222.7479289960546, + "height": 61.924384197056966, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 64, + "versionNonce": 284568210, + "index": "a2V", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "g_pFQcSP1PR2V_JWD4F7l" + ], + "frameId": null, + "boundElements": [], + "updated": 1776223296178, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t3", + "x": 91.71146899658777, + "y": 159.67926930049052, + "width": 207.13499180262315, + "height": 23.221644073896364, + "text": "App & UI Components", + "fontSize": 18.577315259117093, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 124, + "versionNonce": 1961138834, + "index": "a3", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "g_pFQcSP1PR2V_JWD4F7l" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337880, + "link": null, + "locked": false, + "containerId": null, + "originalText": "App & UI Components", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b2", + "x": 320.0040153817446, + "y": 140, + "width": 180, + "height": 60, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 21, + "versionNonce": 72961230, + "index": "a4V", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "TWbD9_BkkcKvCrowUihTj" + ], + "frameId": null, + "boundElements": [], + "updated": 1776223282194, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t4", + "x": 335.0040153817446, + "y": 160, + "width": 150.4078826904297, + "height": 22.5, + "text": "Hooks & Context", + "fontSize": 18, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 25, + "versionNonce": 1608630674, + "index": "a5", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "TWbD9_BkkcKvCrowUihTj" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337882, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Hooks & Context", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b3", + "x": 525.7157292999573, + "y": 140, + "width": 200, + "height": 60, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 64, + "versionNonce": 24319118, + "index": "a6V", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "YZVXbAnRM8DZv8nvWigEO" + ], + "frameId": null, + "boundElements": [], + "updated": 1776223284994, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t5", + "x": 551.824749413521, + "y": 158.85453321603757, + "width": 156.47389221191406, + "height": 22.5, + "text": "Frontend Utilities", + "fontSize": 18, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 103, + "versionNonce": 353877842, + "index": "a7", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "YZVXbAnRM8DZv8nvWigEO" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337882, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Frontend Utilities", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "bridge_bg", + "x": 200, + "y": 260, + "width": 400, + "height": 60, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e5dbff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 4, + "versionNonce": 1880937426, + "index": "a7V", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "23O1NXngzWsHq4Fw0Bycv" + ], + "frameId": null, + "boundElements": [], + "updated": 1776223304979, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t6", + "x": 298.96149065632335, + "y": 280.69769340477717, + "width": 184.11984252929688, + "height": 25, + "text": "Tauri IPC (Bridge)", + "fontSize": 20, + "strokeColor": "#7c3aed", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 39, + "versionNonce": 910689554, + "index": "a8", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [ + "23O1NXngzWsHq4Fw0Bycv" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337883, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Tauri IPC (Bridge)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "arrow", + "id": "a1", + "x": 350, + "y": 240, + "width": 0, + "height": 20, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 20 + ] + ], + "endArrowhead": "arrow", + "version": 2, + "versionNonce": 1952328718, + "index": "aB", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "startArrowhead": null + }, + { + "type": "arrow", + "id": "a2", + "x": 450, + "y": 260, + "width": 0, + "height": 20, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -20 + ] + ], + "endArrowhead": "arrow", + "version": 2, + "versionNonce": 687137746, + "index": "aC", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "startArrowhead": null + }, + { + "type": "rectangle", + "id": "be_bg", + "x": 40, + "y": 340, + "width": 720, + "height": 220, + "strokeColor": "#8b5cf6", + "backgroundColor": "#e5dbff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 30, + "roundness": { + "type": 3 + }, + "version": 2, + "versionNonce": 941193806, + "index": "aD", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t7", + "x": 60, + "y": 350, + "width": 231.65980529785156, + "height": 25, + "text": "Backend (Rust / Tauri)", + "fontSize": 20, + "strokeColor": "#7c3aed", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 6, + "versionNonce": 1558811346, + "index": "aE", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337883, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Backend (Rust / Tauri)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b4", + "x": 80, + "y": 400, + "width": 180, + "height": 80, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 2, + "versionNonce": 2077294734, + "index": "aF", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t8", + "x": 100, + "y": 420, + "width": 123.42391967773438, + "height": 40, + "text": "Image Engine\n(RAW, GPU, AI)", + "fontSize": 16, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 6, + "versionNonce": 1536387218, + "index": "aG", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337883, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Image Engine\n(RAW, GPU, AI)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b5", + "x": 280, + "y": 400, + "width": 240, + "height": 80, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 2, + "versionNonce": 1664990926, + "index": "aH", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t9", + "x": 300, + "y": 415, + "width": 200.33584594726562, + "height": 40, + "text": "Processing Modules\n(LUT, Denoise, Panorama)", + "fontSize": 16, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 6, + "versionNonce": 435979858, + "index": "aI", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337883, + "link": null, + "locked": false, + "containerId": null, + "originalText": "Processing Modules\n(LUT, Denoise, Panorama)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "id": "b6", + "x": 540, + "y": 400, + "width": 200, + "height": 80, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "roundness": { + "type": 3 + }, + "version": 2, + "versionNonce": 855160078, + "index": "aJ", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false + }, + { + "type": "text", + "id": "t10", + "x": 560, + "y": 420, + "width": 147.95187377929688, + "height": 40, + "text": "IO & Management\n(FS, Exif, Tagging)", + "fontSize": 16, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "version": 6, + "versionNonce": 1895558162, + "index": "aK", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223337883, + "link": null, + "locked": false, + "containerId": null, + "originalText": "IO & Management\n(FS, Exif, Tagging)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "arrow", + "id": "a3", + "x": 400, + "y": 320, + "width": 0, + "height": 20, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 20 + ] + ], + "endArrowhead": "arrow", + "version": 2, + "versionNonce": 880468814, + "index": "aL", + "isDeleted": false, + "strokeStyle": "solid", + "angle": 0, + "seed": 1, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1776223195256, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "startArrowhead": null + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} + }, + "files": {} +} \ No newline at end of file diff --git a/docs/img/architecture.excalidraw.png b/docs/img/architecture.excalidraw.png new file mode 100644 index 000000000..434ad9d0a Binary files /dev/null and b/docs/img/architecture.excalidraw.png differ