diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7697eea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +# CI — her push/PR'da frontend + Rust kalite kapısı. +# Yerel pre-commit hook'unun (husky: lint-staged + typecheck + vitest) uzak aynası; +# hook atlanırsa (--no-verify) veya farklı ortamda bozulan bir şey varsa burada yakalanır. +name: CI + +on: + push: + branches: [main, 'refactor/**', 'fix/**'] + pull_request: + branches: [main] + +jobs: + frontend: + name: Frontend (lint + typecheck + test + build) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm test + - run: npx vite build + + rust: + name: Rust (clippy + test) + runs-on: ubuntu-latest + defaults: + run: + working-directory: src-tauri + steps: + - uses: actions/checkout@v4 + - name: Install Tauri Linux system deps + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \ + libayatana-appindicator3-dev librsvg2-dev libssl-dev + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - run: cargo clippy --all-targets -- -D warnings + - run: cargo test --lib diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9dc5a..77a4ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,3 +162,18 @@ Pure structural/readability pass, locked by a Vitest safety net so behaviour is - **Undo/redo off-by-one fixed** (BUGS.md #1): replaced the single history array + index with an undo/redo **two-stack** model. The first action is now undoable; each undo/redo moves exactly one step; redo restores the latest state. Preserves the 50-step cap (`MAX_HISTORY`) and "compound action = single entry" (multi-move / paste / script batch). Characterization tests intentionally updated to the correct spec + 6 new tests → **72** frontend tests. - **Monaco offline**: the Lua editor now loads from the local `monaco-editor` bundle (`loader.config({ monaco })`) with the editor web worker bundled via Vite `?worker`; a `strip-monaco-cdn` Vite plugin removes the jsDelivr default URL from `dist/`. Lazy boundary preserved (Monaco stays in its own chunk; initial app chunk unchanged ≈ 191 kB). Zero CDN references in the build output. + +--- + +## Phase 10 — Repository Structure & Hygiene + +Professional repo-layout pass; behavior-neutral except one flagged fix. + +- **Lint debt**: fixed all 22 `no-unused-vars` warnings (dead constants, unused destructures, vestigial `rawText` state, an entire unused icon import block); eslint warnings 45 → 23 (remaining `any`/hooks/refresh debt documented as future work) +- **Grouping**: loose root components → `components/overlays/` (EditorLoading, KeyboardHelp, Notifications, ProgressOverlay); scattered sweep slice → `features/sweep/` (SweepDialog + useSweepStore + sweepRunner + sweepHelpers + test), matching the experiment/export pattern +- **Google Fonts CDN removed** (flagged fix): index.html loaded Inter + JetBrains Mono from jsDelivr-adjacent Google hosts and the CSP allowed it — contradicting the zero-network guarantee. Links removed, CSP tightened, Monaco/Ruler fonts switched to the self-hosted IBM Plex Mono (consistent online/offline) +- **Template assets dropped**: vite.svg/tauri.svg/react.svg deleted; favicon is now the real app icon (`public/icon.png` from `src-tauri/icons`) +- **Broken examples removed**: three root-level `.mflow` files used the pre-1.0 legacy schema (`mflow_version: 1`) and could never load; the valid examples live in `examples/projects/` +- **Rust deps pruned**: unused `uuid`, `ndarray`, `rayon` removed from Cargo.toml +- **CI added**: GitHub Actions workflow (frontend: lint/typecheck/vitest/build; rust: clippy + cargo test with webkit deps and cache) +- **Docs trued up**: ARCHITECTURE paths (utils → features), TESTING counts (29 → 36 Rust, phantom `test_clear` removed, 72-test frontend section added), README (CI badge, real examples tree, FORMULAS row, screenshots placeholder), stale CODE_QUALITY_REPORT deleted diff --git a/CONVENTIONS.md b/CONVENTIONS.md index eb64ad4..4b3bf94 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -10,12 +10,12 @@ gezinebilmeli. Uçtan uca akışlar için bkz. [`docs/CODE_MAP.md`](docs/CODE_MA | Klasör | Sorumluluk | Örnek | |---|---|---| -| `components//` | Görsel React bileşenleri, alana göre gruplu | `Canvas/`, `ResultsPanel/`, `Toolbar/`, `Sidebar/`, `PropertiesPanel/`, `StatusBar/`, `ScriptEditor/` | -| `features/<özellik>/` | Kendine yeten, dikey bir özelliğin tüm parçaları (UI + store + util) | `features/experiment/`, `features/export/` | +| `components//` | Görsel React bileşenleri, alana göre gruplu | `Canvas/`, `ResultsPanel/`, `Toolbar/`, `Sidebar/`, `PropertiesPanel/`, `StatusBar/`, `ScriptEditor/`, `overlays/` (toast/progress/yardım) | +| `features/<özellik>/` | Kendine yeten, dikey bir özelliğin tüm parçaları (UI + store + util) | `features/experiment/`, `features/export/`, `features/sweep/` | | `hooks/` | Yeniden kullanılabilir React hook'ları; App orkestrasyonu hook'lara taşınır | `useProjectIO`, `useSimulationRun`, `useExportFlow`, `useScriptRun`, `useKeyboardShortcuts` | -| `stores/` | Zustand global state; her store tek bir alan | `useDesignStore` (tasarım+canvas), `useSimulationStore`, `useSweepStore`, `useUiStore` | +| `stores/` | Zustand global state; her store tek bir alan (feature'a özel store ilgili `features/` klasöründe) | `useDesignStore` (tasarım+canvas), `useSimulationStore`, `useUiStore`, `useProjectStore` | | `types/` | Domain'e bölünmüş TypeScript tipleri + re-export hub | `component.ts`, `canvas.ts`, `simulation.ts`, `project.ts`, `experiment.ts`, `ui.ts`, `index.ts` | -| `utils/` | Saf, framework'süz yardımcılar (paylaşılan) | `portUtils`, `componentBbox`, `colormaps`, `sweepRunner` | +| `utils/` | Saf, framework'süz yardımcılar (paylaşılan) | `portUtils`, `componentBbox`, `colormaps`, `componentDefaults` | | `theme/` | Tasarım token'ları ve renk kaynakları (JS tarafı kanonik) | `tokens.ts`, `componentColors.ts` | | `templates/` | "Şablondan başla" başlangıç tasarımları | `starters.ts` | diff --git a/README.md b/README.md index 827911f..6daf714 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mikroakışkan çip tasarımı ve simülasyonu için profesyonel masaüstü uygulaması. [Tauri v2](https://tauri.app/) (Rust backend + React frontend), AutoCAD/Fusion 360 tarzı bir CAD ergonomisiyle inşa edildi. -![Version](https://img.shields.io/badge/version-1.0.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Tauri](https://img.shields.io/badge/tauri-v2-orange) ![Tests](https://img.shields.io/badge/tests-36%20rust%20%2B%2072%20frontend-brightgreen) +[![CI](https://github.com/hccancan4/microflow-studio/actions/workflows/ci.yml/badge.svg)](https://github.com/hccancan4/microflow-studio/actions/workflows/ci.yml) ![Version](https://img.shields.io/badge/version-1.0.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Tauri](https://img.shields.io/badge/tauri-v2-orange) ![Tests](https://img.shields.io/badge/tests-36%20rust%20%2B%2072%20frontend-brightgreen) --- @@ -12,19 +12,7 @@ Sürükle-bırak canvas editöründe 10 tip parametrik bileşenden mikroakışka --- -## Ekran Görüntüleri - -> Görseller `docs/images/` altında. (Henüz eklenmediyse `npm run tauri dev` ile çalıştırıp çekilebilir.) - -| Canvas editörü | CFD ısı haritası | -|---|---| -| ![Canvas](docs/images/canvas.png) | ![CFD](docs/images/cfd.png) | - -| Sonuç panelleri | Parametre taraması | -|---|---| -| ![Sonuçlar](docs/images/results.png) | ![Tarama](docs/images/sweep.png) | - -![Lua script editörü](docs/images/script.png) +> **Ekran görüntüleri:** henüz eklenmedi — `npm run tauri dev` ile çekilip `docs/images/` altına konacak. --- @@ -217,6 +205,7 @@ MicroFlow Studio tamamen **offline** çalışan bir desktop uygulamasıdır: | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Frontend/backend katman, IPC komut listesi, veri akışı | | [`docs/COMPONENTS.md`](docs/COMPONENTS.md) | 10 bileşen tipi, parametreler, port topoloji, direnç formülleri | | [`docs/SIMULATION.md`](docs/SIMULATION.md) | Analitik ağ çözücü + CFD algoritmaları | +| [`docs/FORMULAS.md`](docs/FORMULAS.md) | Tüm fizik formülleri (direnç, Re, Dean, karıştırma) türetimleriyle | | [`docs/SCRIPTING.md`](docs/SCRIPTING.md) | Lua API referansı + örnekler | | [`docs/FILE_FORMAT.md`](docs/FILE_FORMAT.md) | `.mflow` JSON schema, GDS-II layer stratejisi | | [`docs/SHORTCUTS.md`](docs/SHORTCUTS.md) | Tam klavye kısayolu tablosu | @@ -230,17 +219,20 @@ MicroFlow Studio tamamen **offline** çalışan bir desktop uygulamasıdır: ``` examples/ data/ - velocity_profile.csv — 200 μm kanalda parabolik hız ölçümü - pressure_drop.csv — Basınç düşümü vs debi, 5 kanal uzunluğu - mixing_efficiency.json — 4 Re'de karıştırma verimi + velocity_profile.csv — 200 μm kanalda parabolik hız ölçümü + velocity_profile_experiment.csv — Deney import diyaloğu için örnek ölçüm + pressure_drop.csv — Basınç düşümü vs debi, 5 kanal uzunluğu + pressure_drop_experiment.csv — Deney import diyaloğu için örnek ölçüm + mixing_efficiency.json — 4 Re'de karıştırma verimi (JSON formatı) scripts/ - basic_t_junction.lua — Chip API ile basit T-bağlantı - parametric_sweep.lua — Kanal genişliği taraması - gradient_generator.lua — Konsantrasyon gradiyent ağı + basic_t_junction.lua — Chip API ile basit T-bağlantı + parametric_sweep.lua — Kanal genişliği taraması + gradient_generator.lua — Konsantrasyon gradiyent ağı + droplet_generator.lua — Damla üreteci tasarımı (script ile) + parametric_mixer.lua — Parametrik serpantin mikser projects/ - t_junction_droplet.mflow — Damla üreteci tasarımı - serpentine_mixer.mflow — 8-dönüş serpantin mikser - parallel_channels.mflow — Paralel kanal ağı + t_junction_basic.mflow — Basit T-bağlantı tasarımı (uygulamada aç) + serpentine_mixer.mflow — Serpantin mikser tasarımı ``` --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e0d9d28..f061a0a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -124,19 +124,24 @@ Stage coordinate system: 1 Konva unit = 1 μm, scaled by `canvas.zoom` with offs - **`useScriptDispatcher`** — listens for `script-action` and `script-completed` Tauri events; batches `DesignAction` payloads and dispatches them to `useDesignStore` methods; returns `ScriptRunStatus`. -### Utilities +### Feature modules & utilities + +Vertical feature slices live under `src/features//` (UI + store + utils +co-located); shared, framework-free helpers stay in `src/utils/`. | File | Purpose | |---|---| -| `utils/gdsGeometry.ts` | Generate `GdsPolygon[]` from `ChipComponent[]` (265 lines) | -| `utils/svgExporter.ts` | Build SVG string from design; optional scale bar (249 lines) | -| `utils/experimentMetrics.ts` | R², RMSE, per-point error map computation (110 lines) | -| `utils/sweepRunner.ts` | Batch sweep execution loop (129 lines) | -| `utils/csvParser.ts` | CSV text → `ExperimentDataSet` (157 lines) | +| `features/export/gdsGeometry.ts` | Generate `GdsPolygon[]` from `ChipComponent[]` | +| `features/export/svgExporter.ts` | Build SVG string from design; optional scale bar | +| `features/export/exportRenderer.tsx` | Off-screen Konva `` → base64 PNG for export | +| `features/experiment/experimentMetrics.ts` | R², RMSE, per-point error map computation | +| `features/experiment/csvParser.ts` | CSV/JSON text → `ExperimentDataSet` | +| `features/sweep/sweepRunner.ts` | Batch sweep execution loop | +| `features/sweep/sweepHelpers.ts` | Sweepable parameter catalog per component type | | `utils/colormaps.ts` | viridis / jet / plasma / coolwarm lookup tables | | `utils/componentDefaults.ts` | Default parameter values per component type | | `utils/portUtils.ts` | Port position calculation in component-local coordinates | -| `utils/exportRenderer.tsx` | Off-screen Konva `` → base64 PNG for export | +| `utils/componentBbox.ts` | Rotation-aware bounding boxes (selection, fit-all) | --- diff --git a/docs/CODE_MAP.md b/docs/CODE_MAP.md index 471cf5a..6c9c80a 100644 --- a/docs/CODE_MAP.md +++ b/docs/CODE_MAP.md @@ -40,6 +40,8 @@ Store güncellenir ──► ilgili panel render olur | Direnç modeli + kanal default geometrileri | `src-tauri/src/simulation/analytic.rs` (`ASPECT_RATIO_CORRECTION`, `DEFAULT_CHANNEL_*_UM`) | | `.mflow` dosya şeması | `types/project.ts` + `src-tauri/src/project/mod.rs` | | Tipler (domain'e bölünmüş) | `types/.ts` (`component`, `canvas`, `simulation`, `project`, `experiment`, `ui`) | +| Sweep feature (dialog + store + runner + helpers) | `features/sweep/` | +| Overlay/feedback bileşenleri (toast, progress, kısayol yardımı, lazy spinner) | `components/overlays/` | | Monaco editör offline kurulumu (yerel bundle + worker, CDN yok) | `components/ScriptEditor/monacoSetup.ts` + `vite.config.ts` (`strip-monaco-cdn`) | | Bilinen latent bug'lar | [`../BUGS.md`](../BUGS.md) | @@ -63,7 +65,7 @@ Store güncellenir ──► ilgili panel render olur → `CfdField` → `useSimulationStore` → `components/ResultsPanel/tabs/CfdView.tsx` ### Parametre Taraması (sweep) -`SweepDialog` → `stores/useSweepStore` + `utils/sweepRunner.ts` (her adımda analitik koşu) +`features/sweep/SweepDialog.tsx` → `features/sweep/useSweepStore.ts` + `features/sweep/sweepRunner.ts` (her adımda analitik koşu) → `components/ResultsPanel/tabs/SweepTab.tsx` (canlı grafik + CSV dışa aktar) ### Dışa Aktarma (PNG / SVG / GDS-II) diff --git a/docs/CODE_QUALITY_REPORT.md b/docs/CODE_QUALITY_REPORT.md deleted file mode 100644 index 0d4708d..0000000 --- a/docs/CODE_QUALITY_REPORT.md +++ /dev/null @@ -1,137 +0,0 @@ -# MicroFlow Studio — Code Quality Report - -> Generated from the completed 6-phase codebase. -> Rust: 3 026 lines across 17 files. TypeScript/TSX: 8 411 lines across 35 files. - ---- - -## TypeScript Errors - -**Result: 0 errors** (`npx tsc --noEmit` exits with code 0). - ---- - -## Issues Found - -### 1. Heavy use of `any` in App.tsx and ResultsPanel - -**Severity: Medium** - -`App.tsx` uses `any` for all Tauri `invoke` return values (lines 174, 310, 324, 342, 397). `ResultsPanel.tsx` passes `result: any` into sub-components (lines 187, 220). This bypasses TypeScript's safety for the most critical data paths — simulation results and project loading. - -**Fix:** Create typed wrappers for `invoke` return values matching the Rust `AnalyticDesignResult` and `CfdField` structs. These types already exist in `types/index.ts`; the invoke calls just need to use them: - -```typescript -const res = await invoke('run_analytic_network', { req }); -``` - -Also type `StatusBar`'s props explicitly instead of `any[]`. - ---- - -### 2. Stale comment in `analytic.rs` — "Faz 4B/5'te lineer sistem çözücüyle yapılacak" - -**Severity: Low** - -Line 325 in `simulation/analytic.rs` still references a future Kirchhoff solver ("Faz 4B/5"). The project is complete, but this comment wasn't updated. The current DFS-based series/parallel approach is the final implementation. - -**Fix:** Update comment to reflect that the current approach is the production algorithm and Kirchhoff linear system is a future enhancement if needed. - ---- - -### 3. Version string in StatusBar is `"v0.1.0-faz1"` - -**Severity: Low** - -`App.tsx` line 581 shows `v0.1.0-faz1` in the status bar — a development-phase label left from Phase 1. The project is now at the Polish phase. - -**Fix:** - -```tsx -v0.1.0 -``` - -Or better, read version from `package.json` / `Cargo.toml` at build time via a Vite `define` constant. - ---- - -### 4. `_comp_by_id` built but never used in `analytic.rs` - -**Severity: Low** - -Line 334–335 in `analytic.rs` builds a `HashMap<&str, &DesignComponent>` with a `_` prefix to silence the compiler warning. The data structure is described as "ileride grafta bileşen lookup'ı gerektiğinde burada hazır" (ready for future graph lookup). - -**Fix:** Remove it entirely until it's actually needed. This avoids allocating an unnecessary HashMap on every analytic solve call. - ---- - -### 5. `geometry/mod.rs` is mostly unused in production - -**Severity: Low** - -`src-tauri/src/geometry/mod.rs` declares `Point`, `Rect`, `bounding_box`, and `arc_points`. These utilities are tested but not imported anywhere in the production simulation/export code (polygon geometry is done in `gdsGeometry.ts` on the frontend; CFD uses raw array indexing). The module comment says "ileride kullanılmak üzere" (for future use). - -**Fix:** Either start using these helpers in `gds.rs` / `cfd.rs`, or keep them as foundation code with a clear docstring explaining their intended future role. - ---- - -### 6. Konva event handlers typed as `(e: any)` in ComponentShapes - -**Severity: Low** - -`ComponentShapes.tsx` types all Konva event callbacks as `(e: any) => void`. Konva's TypeScript types provide `KonvaEventObject` etc. - -**Fix:** -```typescript -import type { KonvaEventObject } from 'konva/lib/Node'; -onClick: (e: KonvaEventObject) => void; -``` - ---- - -### 7. `unwrap()` in test code (acceptable) and `expect` in `lib.rs` (production) - -**Severity: Low** - -All `unwrap()` calls are in `#[cfg(test)]` blocks — acceptable there. The single `expect(...)` in `lib.rs` line 45 is the Tauri app builder entry point — this is the standard Tauri pattern and correct for a fatal startup failure. No changes needed. - ---- - -### 8. No TypeScript tests - -**Severity: Medium** (for a production/publication-ready codebase) - -There are zero frontend tests (Jest, Vitest, or similar). All 29 automated tests are Rust unit tests. Critical frontend logic — store reducers, `buildProjectPayload`, CSV parsing, GDS polygon generation, SVG export, experiment metrics — is untested. - -**Fix:** Add Vitest. Highest-priority test targets: -- `utils/experimentMetrics.ts` — R², RMSE math (pure functions, trivial to unit test) -- `utils/csvParser.ts` — CSV parsing edge cases (empty lines, different delimiters) -- `stores/useDesignStore.ts` — undo/redo stack behaviour -- `utils/gdsGeometry.ts` — polygon coordinate correctness for each component type - ---- - -## Overall Assessment - -**Code quality: Good for an academic research tool at version 0.1.** The architecture is clean and the separation of concerns is well maintained: simulation logic lives entirely in Rust, rendering in Konva, state in Zustand, and script execution in mlua. The Chorin projection CFD implementation is real and not a placeholder. - -**Strengths:** -- Rust code is idiomatic; anyhow/thiserror used correctly throughout -- 29 unit tests covering all critical Rust modules; tests are specific and meaningful -- IPC layer is well-structured; Tauri command signatures are clean -- TypeScript types in `types/index.ts` are comprehensive and match Rust structs -- Zero TypeScript compiler errors after all 6 phases - -**Primary weaknesses:** -- Frontend completely lacks automated tests -- Heavy `any` typing in `App.tsx` and `ResultsPanel.tsx` — partially defeats TypeScript's purpose in the most important data paths -- No integration or end-to-end tests (expected for a research prototype, but worth noting before wider distribution) - -**Improvement priorities (in order):** - -1. Add Vitest + testing-library and write unit tests for `experimentMetrics.ts`, `csvParser.ts`, and the Zustand stores -2. Replace `any` in Tauri `invoke` calls with proper typed generics (`invoke(...)`) -3. Fix version string (`v0.1.0-faz1` → `v0.1.0`) -4. Remove unused `_comp_by_id` allocation from `analyze_design` -5. Replace `(e: any)` event handler types in `ComponentShapes.tsx` with Konva types -6. Optionally: expand CFD test coverage to production-scale grids (slow — mark as `#[ignore]` by default) diff --git a/docs/TESTING.md b/docs/TESTING.md index 4b2e6d8..b8fb140 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -1,12 +1,16 @@ # MicroFlow Studio — Test Coverage -All tests are Rust unit tests (`#[test]`). Run with: +Two suites: ```bash -cargo test --manifest-path src-tauri/Cargo.toml +# Rust unit tests (36) +cargo test --manifest-path src-tauri/Cargo.toml --lib + +# Frontend Vitest tests (72) +npm test ``` -Total: **29 tests** across 6 files. +## Rust — **36 tests** across 7 files --- @@ -33,7 +37,7 @@ Total: **29 tests** across 6 files. | `test_stokes_profile_shape` | Chorin solver produces parabolic shape (centre > near-wall), zero at wall, residual history recorded | | `test_stokes_residual_decreases` | Solver reduces max-divergence residual by ≥ 70% over 500 iterations | -## `scripting/mod.rs` — 10 tests +## `scripting/mod.rs` — 9 tests | Test name | What it verifies | |---|---| @@ -46,7 +50,6 @@ Total: **29 tests** across 6 files. | `test_parametric_loop` | Lua `for` loop adding 5 channels emits 5 actions | | `test_connect` | `chip:connect(a, b)` emits one `Connect` action | | `test_sweep_run` | `Sweep.run({values={1,2,3}, callback=...})` executes 3 times | -| `test_clear` | `chip:clear()` emits `ClearDesign` action | ## `export/mod.rs` — 4 tests @@ -57,7 +60,7 @@ Total: **29 tests** across 6 files. | `test_save_png_composite_white` | White background composite: semi-transparent red over white → opaque mixed pixel | | `test_save_svg_writes_file` | SVG string written to disk contains ` 100.", - "author": "MicroFlow Studio Examples", - "created_at": "2025-09-01T10:00:00Z", - "updated_at": "2025-09-01T10:00:00Z", - "tags": ["mixer", "serpentine", "passive-mixing", "laminar"], - "units": "um" - }, - "design": { - "canvas": { - "width": 14000.0, - "height": 10000.0, - "grid_size": 50.0, - "background_color": "#1a1a2e" - }, - "components": [ - { - "id": "comp-inlet-a", - "kind": "inlet_port", - "label": "Stream A (5 μL/min)", - "position": { "x": 500.0, "y": 3500.0 }, - "rotation": 0.0, - "params": { - "diameter": 400.0, - "boundary_type": "flow_rate", - "value": 5.0, - "fluid": "water" - }, - "ports": [ - { - "id": "port-a-out", - "name": "outlet", - "position": { "x": 200.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-inlet-a" - } - ] - }, - { - "id": "comp-inlet-b", - "kind": "inlet_port", - "label": "Stream B (5 μL/min)", - "position": { "x": 500.0, "y": 4500.0 }, - "rotation": 0.0, - "params": { - "diameter": 400.0, - "boundary_type": "flow_rate", - "value": 5.0, - "fluid": "pbs_1x" - }, - "ports": [ - { - "id": "port-b-out", - "name": "outlet", - "position": { "x": 200.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-inlet-b" - } - ] - }, - { - "id": "comp-y-junction", - "kind": "y_junction", - "label": "Y-Junction (merge)", - "position": { "x": 1500.0, "y": 4000.0 }, - "rotation": 0.0, - "params": { - "arm_width": 150.0, - "stem_width": 150.0, - "depth": 50.0, - "half_angle": 15.0 - }, - "ports": [ - { - "id": "port-yj-a", - "name": "arm_a", - "position": { "x": -300.0, "y": -250.0 }, - "direction": "in", - "connected_to": "ch-inlet-a" - }, - { - "id": "port-yj-b", - "name": "arm_b", - "position": { "x": -300.0, "y": 250.0 }, - "direction": "in", - "connected_to": "ch-inlet-b" - }, - { - "id": "port-yj-stem", - "name": "stem", - "position": { "x": 300.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-serpentine" - } - ] - }, - { - "id": "comp-serpentine", - "kind": "serpentine_mixer", - "label": "Serpentine Mixer (8 turns)", - "position": { "x": 4000.0, "y": 4000.0 }, - "rotation": 0.0, - "params": { - "channel_width": 150.0, - "depth": 50.0, - "turn_radius": 300.0, - "straight_length": 2000.0, - "n_turns": 8, - "pitch": 800.0 - }, - "ports": [ - { - "id": "port-ser-in", - "name": "inlet", - "position": { "x": -2000.0, "y": 0.0 }, - "direction": "in", - "connected_to": "ch-serpentine" - }, - { - "id": "port-ser-out", - "name": "outlet", - "position": { "x": 2000.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-outlet-main" - } - ] - }, - { - "id": "comp-outlet", - "kind": "outlet_port", - "label": "Mixed Output", - "position": { "x": 12000.0, "y": 4000.0 }, - "rotation": 0.0, - "params": { - "diameter": 400.0, - "pressure": 0.0 - }, - "ports": [ - { - "id": "port-outlet-in", - "name": "inlet", - "position": { "x": -200.0, "y": 0.0 }, - "direction": "in", - "connected_to": "ch-outlet-main" - } - ] - } - ], - "channels": [ - { - "id": "ch-inlet-a", - "label": "Stream A inlet", - "kind": "straight", - "width": 150.0, - "depth": 50.0, - "points": [ - { "x": 700.0, "y": 3500.0 }, - { "x": 1200.0, "y": 3750.0 } - ], - "start_port": "port-a-out", - "end_port": "port-yj-a", - "style": { "color": "#4fc3f7", "line_width": 2 } - }, - { - "id": "ch-inlet-b", - "label": "Stream B inlet", - "kind": "straight", - "width": 150.0, - "depth": 50.0, - "points": [ - { "x": 700.0, "y": 4500.0 }, - { "x": 1200.0, "y": 4250.0 } - ], - "start_port": "port-b-out", - "end_port": "port-yj-b", - "style": { "color": "#f97316", "line_width": 2 } - }, - { - "id": "ch-serpentine", - "label": "Pre-mixer straight", - "kind": "straight", - "width": 150.0, - "depth": 50.0, - "points": [ - { "x": 1800.0, "y": 4000.0 }, - { "x": 2000.0, "y": 4000.0 } - ], - "start_port": "port-yj-stem", - "end_port": "port-ser-in", - "style": { "color": "#a78bfa", "line_width": 2 } - }, - { - "id": "ch-outlet-main", - "label": "Mixed stream outlet", - "kind": "straight", - "width": 150.0, - "depth": 50.0, - "points": [ - { "x": 6000.0, "y": 4000.0 }, - { "x": 11800.0, "y": 4000.0 } - ], - "start_port": "port-ser-out", - "end_port": "port-outlet-in", - "style": { "color": "#34d399", "line_width": 2 } - } - ] - }, - "simulation": { - "level": "analytical", - "fluid": { - "name": "water", - "density": 997.0, - "viscosity": 0.000890 - }, - "boundary_conditions": [ - { - "port_id": "port-a-out", - "type": "flow_rate", - "value": 5.0, - "unit": "ul_min" - }, - { - "port_id": "port-b-out", - "type": "flow_rate", - "value": 5.0, - "unit": "ul_min" - }, - { - "port_id": "port-outlet-in", - "type": "pressure", - "value": 0.0, - "unit": "pa" - } - ], - "notes": "Combined flow: 10 μL/min in 150×50 μm channel. U_mean = 22.2 mm/s. Re ≈ 1.9 (laminar). D_mol (fluorescent dye) ≈ 4×10⁻¹⁰ m²/s. Pe ≈ 53. Total serpentine length ≈ 23.5 mm. Characteristic mixing length ≈ 63 mm → partial mixing after 8 turns. For complete mixing add more turns or reduce flow rate." - }, - "script": { - "language": "lua", - "content": "-- Serpentine Mixer Analysis\n-- Estimates mixing efficiency after N turns\n\nlocal Q_total = 10 -- μL/min\nlocal w = 150 -- μm\nlocal h = 50 -- μm\nlocal R_bend = 300 -- μm\nlocal L_straight = 2000 -- μm\nlocal n_turns = 8\n\n-- Fluid properties (water)\nlocal rho = 997\nlocal mu = 0.000890\nlocal D_mol = 4e-10 -- m²/s, fluorescent dye\n\n-- SI conversions\nlocal Q_SI = Q_total * 1.667e-11\nlocal w_SI = w * 1e-6\nlocal h_SI = h * 1e-6\nlocal A = w_SI * h_SI\nlocal U = Q_SI / A\nlocal Dh = 2 * w_SI * h_SI / (w_SI + h_SI)\n\n-- Dimensionless numbers\nlocal Re = rho * U * Dh / mu\nlocal Pe = U * Dh / D_mol\n\n-- Total serpentine length\nlocal L_arc = math.pi * R_bend * 1e-6 -- one U-turn arc (180°)\nlocal L_total_m = n_turns * (L_straight * 1e-6 + L_arc)\n\nmf.log(string.format(\"Mean velocity: %.2f mm/s\", U * 1000))\nmf.log(string.format(\"Re = %.2f, Pe = %.0f\", Re, Pe))\nmf.log(string.format(\"D_h = %.1f μm\", Dh * 1e6))\nmf.log(string.format(\"Total channel length: %.1f mm\", L_total_m * 1000))\n\n-- Approximate mixing index per turn (empirical, laminar serpentine)\n-- From Stroock et al. 2002 concept\nlocal n_mix = Pe / (2 * math.pi^2) -- characteristic turns for full mixing\nlocal MI = 1 - math.exp(-n_turns / n_mix)\n\nmf.log(string.format(\"Characteristic mixing turns n_mix = %.1f\", n_mix))\nmf.log(string.format(\"Estimated mixing index after %d turns: MI = %.3f\", n_turns, MI))\n\nif MI > 0.95 then\n mf.log(\"Status: Excellent mixing (MI > 0.95)\")\nelseif MI > 0.8 then\n mf.log(\"Status: Good mixing (MI > 0.80)\")\nelse\n mf.log(string.format(\"Status: Partial mixing — need ~%.0f turns for MI > 0.95\", -n_mix * math.log(0.05)))\nend\n\nlocal result = mf.simulate({ fluid = \"water\" })\nmf.log(string.format(\"Total pressure drop: %.1f Pa\", result.total_pressure_drop_pa))\n", - "last_run_at": null, - "last_run_output": null - } -} diff --git a/examples/t_junction_droplet.mflow b/examples/t_junction_droplet.mflow deleted file mode 100644 index 67345d2..0000000 --- a/examples/t_junction_droplet.mflow +++ /dev/null @@ -1,217 +0,0 @@ -{ - "mflow_version": 1, - "metadata": { - "name": "T-Junction Droplet Generator", - "description": "Water-in-oil droplet generation using a T-junction. Continuous phase: mineral oil at 15 μL/min. Dispersed phase: water at 5 μL/min. Channel width 200 μm, depth 50 μm. Expected droplet regime: squeezing (Ca ≈ 0.003).", - "author": "MicroFlow Studio Examples", - "created_at": "2025-09-01T10:00:00Z", - "updated_at": "2025-09-01T10:00:00Z", - "tags": ["droplet", "water-in-oil", "t-junction", "squeezing-regime"], - "units": "um" - }, - "design": { - "canvas": { - "width": 8000.0, - "height": 5000.0, - "grid_size": 50.0, - "background_color": "#1a1a2e" - }, - "components": [ - { - "id": "comp-oil-inlet", - "kind": "inlet_port", - "label": "Oil Inlet (15 μL/min)", - "position": { "x": 500.0, "y": 2000.0 }, - "rotation": 0.0, - "params": { - "diameter": 500.0, - "boundary_type": "flow_rate", - "value": 15.0, - "fluid": "mineral_oil" - }, - "ports": [ - { - "id": "port-oil-out", - "name": "outlet", - "position": { "x": 250.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-oil-main" - } - ] - }, - { - "id": "comp-water-inlet", - "kind": "inlet_port", - "label": "Water Inlet (5 μL/min)", - "position": { "x": 2500.0, "y": 500.0 }, - "rotation": 0.0, - "params": { - "diameter": 500.0, - "boundary_type": "flow_rate", - "value": 5.0, - "fluid": "water" - }, - "ports": [ - { - "id": "port-water-out", - "name": "outlet", - "position": { "x": 0.0, "y": 250.0 }, - "direction": "out", - "connected_to": "ch-water-side" - } - ] - }, - { - "id": "comp-tj-1", - "kind": "t_junction", - "label": "T-Junction", - "position": { "x": 2500.0, "y": 2000.0 }, - "rotation": 0.0, - "params": { - "main_width": 200.0, - "side_width": 100.0, - "depth": 50.0, - "junction_angle": 90.0 - }, - "ports": [ - { - "id": "port-tj-main-in", - "name": "main_inlet", - "position": { "x": -400.0, "y": 0.0 }, - "direction": "in", - "connected_to": "ch-oil-main" - }, - { - "id": "port-tj-side", - "name": "side_inlet", - "position": { "x": 0.0, "y": -300.0 }, - "direction": "in", - "connected_to": "ch-water-side" - }, - { - "id": "port-tj-out", - "name": "outlet", - "position": { "x": 400.0, "y": 0.0 }, - "direction": "out", - "connected_to": "ch-droplet-collection" - } - ] - }, - { - "id": "comp-outlet-1", - "kind": "outlet_port", - "label": "Droplet Collection", - "position": { "x": 7000.0, "y": 2000.0 }, - "rotation": 0.0, - "params": { - "diameter": 500.0, - "pressure": 0.0 - }, - "ports": [ - { - "id": "port-outlet-in", - "name": "inlet", - "position": { "x": -250.0, "y": 0.0 }, - "direction": "in", - "connected_to": "ch-droplet-collection" - } - ] - } - ], - "channels": [ - { - "id": "ch-oil-main", - "label": "Oil main channel", - "kind": "straight", - "width": 200.0, - "depth": 50.0, - "points": [ - { "x": 750.0, "y": 2000.0 }, - { "x": 2100.0, "y": 2000.0 } - ], - "start_port": "port-oil-out", - "end_port": "port-tj-main-in", - "style": { - "color": "#ffb347", - "line_width": 2 - } - }, - { - "id": "ch-water-side", - "label": "Water side channel", - "kind": "straight", - "width": 100.0, - "depth": 50.0, - "points": [ - { "x": 2500.0, "y": 750.0 }, - { "x": 2500.0, "y": 1700.0 } - ], - "start_port": "port-water-out", - "end_port": "port-tj-side", - "style": { - "color": "#4fc3f7", - "line_width": 2 - } - }, - { - "id": "ch-droplet-collection", - "label": "Droplet collection channel", - "kind": "straight", - "width": 200.0, - "depth": 50.0, - "points": [ - { "x": 2900.0, "y": 2000.0 }, - { "x": 6750.0, "y": 2000.0 } - ], - "start_port": "port-tj-out", - "end_port": "port-outlet-in", - "style": { - "color": "#a78bfa", - "line_width": 2 - } - } - ] - }, - "simulation": { - "level": "analytical", - "fluid": { - "name": "water", - "density": 997.0, - "viscosity": 0.000890 - }, - "boundary_conditions": [ - { - "port_id": "port-oil-out", - "type": "flow_rate", - "value": 15.0, - "unit": "ul_min" - }, - { - "port_id": "port-water-out", - "type": "flow_rate", - "value": 5.0, - "unit": "ul_min" - }, - { - "port_id": "port-outlet-in", - "type": "pressure", - "value": 0.0, - "unit": "pa" - } - ], - "cfd_settings": { - "mesh_resolution": "medium", - "max_iterations": 2000, - "tolerance": 1e-5, - "relaxation_u": 0.7, - "relaxation_p": 0.3 - }, - "notes": "Water-in-oil system. Oil: mineral oil (μ=0.025 Pa·s). Water: μ=0.000890 Pa·s. Interfacial tension: ~0.035 N/m (without surfactant). Ca_oil ≈ 0.003 → squeezing regime. Expected droplet length: ~250 μm. Flow rate ratio Q_w/Q_oil = 1/3." - }, - "script": { - "language": "lua", - "content": "-- T-Junction Droplet Generator\n-- Analytical estimates for droplet size\n\nlocal Q_oil = 15 -- μL/min\nlocal Q_water = 5 -- μL/min\nlocal w_main = 200 -- μm\nlocal w_side = 100 -- μm\nlocal depth = 50 -- μm\n\n-- Interfacial tension water-oil (Pa·m = N/m)\nlocal gamma = 0.035\n\n-- Oil velocity in main channel\nlocal A_main = w_main * depth * 1e-12 -- m²\nlocal Q_oil_SI = Q_oil * 1.667e-11 -- m³/s\nlocal U_oil = Q_oil_SI / A_main -- m/s\n\n-- Capillary number\nlocal mu_oil = 0.025\nlocal Ca = mu_oil * U_oil / gamma\n\nmf.log(string.format(\"Oil velocity: %.4f m/s\", U_oil))\nmf.log(string.format(\"Capillary number Ca = %.4f\", Ca))\n\nif Ca < 0.01 then\n mf.log(\"Regime: SQUEEZING (Ca < 0.01)\")\n -- Garstecki 2006 scaling law: L_drop/w_main ≈ 1 + Q_d/Q_c\n local L_drop = w_main * (1 + Q_water / Q_oil)\n mf.log(string.format(\"Predicted droplet length: %.1f μm\", L_drop))\nelseif Ca < 0.3 then\n mf.log(\"Regime: DRIPPING (0.01 < Ca < 0.3)\")\nelse\n mf.log(\"Regime: JETTING (Ca > 0.3) — not recommended\")\nend\n\nlocal result = mf.simulate({ fluid = \"water\" })\nmf.log(string.format(\"Total pressure drop: %.1f Pa\", result.total_pressure_drop_pa))\n", - "last_run_at": null, - "last_run_output": null - } -} diff --git a/index.html b/index.html index 6515f2d..be3b42b 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,10 @@ - + MicroFlow Studio - - - +
diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..a437dd5 Binary files /dev/null and b/public/icon.png differ diff --git a/public/tauri.svg b/public/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7805907..11fa639 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2528,16 +2528,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matrixmultiply" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -2574,9 +2564,7 @@ dependencies = [ "image", "log", "mlua", - "ndarray", "parking_lot", - "rayon", "serde", "serde_json", "tauri", @@ -2585,7 +2573,6 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-opener", "tokio", - "uuid", ] [[package]] @@ -2674,21 +2661,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "ndarray" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2765,15 +2737,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -3706,12 +3669,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "rayon" version = "1.12.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 050b0b8..905f857 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,13 +20,10 @@ tauri-plugin-fs = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } -uuid = { version = "1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } log = "0.4" env_logger = "0.11" anyhow = "1" -ndarray = "0.16" -rayon = "1" image = "0.25" mlua = { version = "0.10", features = ["lua54", "vendored", "send"] } parking_lot = "0.12" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index fc26289..e2eeabe 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -24,7 +24,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost; worker-src 'self' blob:" + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost; worker-src 'self' blob:" } }, "bundle": { diff --git a/src/App.tsx b/src/App.tsx index 3e1807b..e880562 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,20 +13,20 @@ import CanvasEditor from './components/Canvas/CanvasEditor'; const ScriptEditor = lazy(() => import('./components/ScriptEditor/ScriptEditor')); import ResultsPanel from './components/ResultsPanel/ResultsPanel'; import PropertiesPanel from './components/PropertiesPanel/PropertiesPanel'; -import ProgressOverlay from './components/ProgressOverlay'; +import ProgressOverlay from './components/overlays/ProgressOverlay'; import ExportDialog from './features/export/ExportDialog'; import ExperimentImportDialog from './features/experiment/ExperimentImportDialog'; -import SweepDialog from './components/SweepDialog'; -import KeyboardHelp from './components/KeyboardHelp'; +import SweepDialog from './features/sweep/SweepDialog'; +import KeyboardHelp from './components/overlays/KeyboardHelp'; import StatusBar from './components/StatusBar/StatusBar'; -import EditorLoading from './components/EditorLoading'; -import Notifications from './components/Notifications'; +import EditorLoading from './components/overlays/EditorLoading'; +import Notifications from './components/overlays/Notifications'; import ExportRenderer from './features/export/exportRenderer'; import { useProjectStore } from './stores/useProjectStore'; import { useDesignStore } from './stores/useDesignStore'; import { useSimulationStore } from './stores/useSimulationStore'; -import { useSweepStore } from './stores/useSweepStore'; +import { useSweepStore } from './features/sweep/useSweepStore'; import { useExperimentStore } from './features/experiment/useExperimentStore'; import { useElementSize } from './hooks/useElementSize'; diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/Canvas/CanvasEditor.tsx b/src/components/Canvas/CanvasEditor.tsx index 8b0d200..fa2181b 100644 --- a/src/components/Canvas/CanvasEditor.tsx +++ b/src/components/Canvas/CanvasEditor.tsx @@ -81,12 +81,10 @@ const CanvasEditor: React.FC = ({ width, height }) => { toggleSelection, clearSelection, selectAll, - updateComponent, copySelected, pasteClipboard, duplicateSelected, pendingConnection, - startConnection, updateConnectionMouse, cancelConnection, updateCanvas, @@ -461,7 +459,7 @@ const CanvasEditor: React.FC = ({ width, height }) => { // ── Stage mouse move — pan + pending bağlantı + rubber-band ────────────── const handleMouseMove = useCallback( - (e: Konva.KonvaEventObject) => { + (_e: Konva.KonvaEventObject) => { const stage = stageRef.current; if (!stage) return; const ptr = stage.getPointerPosition()!; diff --git a/src/components/Canvas/CfdOverlay.tsx b/src/components/Canvas/CfdOverlay.tsx index 1b29c5d..3fd2c55 100644 --- a/src/components/Canvas/CfdOverlay.tsx +++ b/src/components/Canvas/CfdOverlay.tsx @@ -83,7 +83,7 @@ const CfdOverlay: React.FC = ({ opacity = 0.75, }) => { // İlgili saha dizisini seç + min/max hesapla - const { values, min, max, unit, label } = useMemo(() => { + const { values, min, max } = useMemo(() => { const src = fieldType === 'magnitude' ? field.magnitude diff --git a/src/components/Canvas/ContextMenu.tsx b/src/components/Canvas/ContextMenu.tsx index a575d9c..207ce08 100644 --- a/src/components/Canvas/ContextMenu.tsx +++ b/src/components/Canvas/ContextMenu.tsx @@ -4,10 +4,6 @@ * Seçili bileşenler için: sil, kopyala, yapıştır, döndür, özellikler. */ import React, { useEffect, useRef } from 'react'; -import { - FiTrash2, FiCopy, FiClipboard, FiRotateCw, FiRotateCcw, - FiLayers, FiMove, FiLink, FiLink2, -} from 'react-icons/fi'; export interface ContextMenuItem { label: string; @@ -19,7 +15,7 @@ export interface ContextMenuItem { } interface ContextMenuProps { - x: number; // screen px + x: number; // screen px y: number; items: ContextMenuItem[]; onClose: () => void; @@ -50,7 +46,7 @@ const ContextMenu: React.FC = ({ x, y, items, onClose }) => { const style: React.CSSProperties = { position: 'fixed', left: Math.min(x, window.innerWidth - 200), - top: Math.min(y, window.innerHeight - items.length * 32 - 16), + top: Math.min(y, window.innerHeight - items.length * 32 - 16), zIndex: 9999, }; @@ -63,25 +59,28 @@ const ContextMenu: React.FC = ({ x, y, items, onClose }) => { > {items.map((item, idx) => ( - {item.divider && idx > 0 && ( -
- )} + {item.divider && idx > 0 &&
} diff --git a/src/components/Canvas/PortOverlay.tsx b/src/components/Canvas/PortOverlay.tsx index c0f42b0..5736d57 100644 --- a/src/components/Canvas/PortOverlay.tsx +++ b/src/components/Canvas/PortOverlay.tsx @@ -22,7 +22,6 @@ const PREFERS_REDUCED_MOTION = const PORT_R_SCREEN_PX = 9; // port overlay dairesi ekran boyutu (piksel) const PORT_R_MAX_UM = 120; // çok uzak zoom'da port overlay dünya-uzayı üst sınırı (μm) const PORT_R_MIN_UM = 4; // çok yakın zoom'da port overlay dünya-uzayı alt sınırı (μm) -const SNAP_DISTANCE = 400; // μm — bu mesafe içindeyse snap interface PortOverlayProps { components: ChipComponent[]; @@ -48,8 +47,7 @@ const PortOverlay: React.FC = ({ onConnectionContextMenu, layer = 'above', }) => { - const { startConnection, cancelConnection, addConnection, updateConnectionMouse } = - useDesignStore(); + const { startConnection, cancelConnection, addConnection } = useDesignStore(); const [hoveredPort, setHoveredPort] = useState(null); const [compatWarning, setCompatWarning] = useState(null); @@ -69,8 +67,8 @@ const PortOverlay: React.FC = ({ const fromComp = components.find((c) => c.id === pendingConnection.fromComponentId); if (!fromComp || compId === pendingConnection.fromComponentId) return; - // Uyumluluk kontrolü - const { compatible, warning } = checkPortCompatibility( + // Uyumluluk kontrolü (yalnız uyarı metni kullanılır; engelleme handlePortClick'te) + const { warning } = checkPortCompatibility( pendingConnection ? getPortDiameter( pendingConnection.fromComponentId, diff --git a/src/components/Canvas/Ruler.tsx b/src/components/Canvas/Ruler.tsx index 700e973..dd71808 100644 --- a/src/components/Canvas/Ruler.tsx +++ b/src/components/Canvas/Ruler.tsx @@ -71,7 +71,7 @@ const Ruler: React.FC = ({ orientation, length, zoom, pan, mousePos const startUm = Math.floor(-pan / zoom / tickUm) * tickUm - tickUm; const endUm = startUm + length / zoom + tickUm * 2; - ctx.font = `9px JetBrains Mono, monospace`; + ctx.font = `9px "IBM Plex Mono", monospace`; ctx.fillStyle = TEXT_COLOR; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; diff --git a/src/components/Canvas/shapes/ComponentShapes.tsx b/src/components/Canvas/shapes/ComponentShapes.tsx index 8b062db..9751e62 100644 --- a/src/components/Canvas/shapes/ComponentShapes.tsx +++ b/src/components/Canvas/shapes/ComponentShapes.tsx @@ -259,7 +259,6 @@ const TJunction: React.FC = ({ comp, sw, selected }) => { const YJunction: React.FC = ({ comp, sw, selected }) => { const { mainWidth = 300, branchWidth = 200, branchAngle = 45 } = comp.params as YJunctionParams; const c = selected ? COLORS.selected : COLORS.junction; - const rad = (branchAngle * Math.PI) / 180; const armLen = mainWidth * 5; // Basit 3 dikdörtgen kollu Y şekli @@ -530,7 +529,7 @@ const ReservoirShape: React.FC = ({ comp, sw, selected }) => { ); }; -const FallbackShape: React.FC<{ comp: ChipComponent; sw: number }> = ({ comp, sw }) => ( +const FallbackShape: React.FC<{ comp: ChipComponent; sw: number }> = ({ sw }) => ( = ({ theme: 'mf-dark', minimap: { enabled: false }, fontSize: 13, - fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", + fontFamily: "'IBM Plex Mono', Consolas, monospace", lineNumbers: 'on' as const, scrollBeyondLastLine: false, wordWrap: 'on' as const, diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 694678d..73db628 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -5,7 +5,6 @@ import React, { useState } from 'react'; import { FiChevronDown, FiChevronRight } from 'react-icons/fi'; import type { ComponentType } from '../../types'; -import clsx from 'clsx'; import { COMPONENT_COLORS, PORT_COLORS } from '../../theme/componentColors'; // Bileşen renkleri — canvas ile birebir aynı (componentColors.ts tek kaynak) diff --git a/src/components/StatusBar/StatusBar.tsx b/src/components/StatusBar/StatusBar.tsx index 4a5a87c..858e24b 100644 --- a/src/components/StatusBar/StatusBar.tsx +++ b/src/components/StatusBar/StatusBar.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { useDesignStore } from '../../stores/useDesignStore'; import { useSimulationStore } from '../../stores/useSimulationStore'; -import { useSweepStore } from '../../stores/useSweepStore'; +import { useSweepStore } from '../../features/sweep/useSweepStore'; import { useCursorStore } from '../../stores/useCursorStore'; import type { ChipComponent, Connection, ProjectMetadata } from '../../types'; diff --git a/src/components/EditorLoading.tsx b/src/components/overlays/EditorLoading.tsx similarity index 100% rename from src/components/EditorLoading.tsx rename to src/components/overlays/EditorLoading.tsx diff --git a/src/components/KeyboardHelp.tsx b/src/components/overlays/KeyboardHelp.tsx similarity index 98% rename from src/components/KeyboardHelp.tsx rename to src/components/overlays/KeyboardHelp.tsx index c016acf..eca80e9 100644 --- a/src/components/KeyboardHelp.tsx +++ b/src/components/overlays/KeyboardHelp.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; import { FiX } from 'react-icons/fi'; -import { useEscapeClose } from '../hooks/useEscapeClose'; +import { useEscapeClose } from '../../hooks/useEscapeClose'; interface Props { open: boolean; diff --git a/src/components/Notifications.tsx b/src/components/overlays/Notifications.tsx similarity index 97% rename from src/components/Notifications.tsx rename to src/components/overlays/Notifications.tsx index 25017f5..75d5599 100644 --- a/src/components/Notifications.tsx +++ b/src/components/overlays/Notifications.tsx @@ -8,8 +8,8 @@ import React, { useEffect } from 'react'; import { createPortal } from 'react-dom'; import { FiCheckCircle, FiAlertTriangle, FiXCircle, FiInfo, FiX } from 'react-icons/fi'; import clsx from 'clsx'; -import { useUiStore, type Toast, type ToastKind } from '../stores/useUiStore'; -import { useEscapeClose } from '../hooks/useEscapeClose'; +import { useUiStore, type Toast, type ToastKind } from '../../stores/useUiStore'; +import { useEscapeClose } from '../../hooks/useEscapeClose'; // ── Toast ───────────────────────────────────────────────────────────────── const KIND: Record = { diff --git a/src/components/ProgressOverlay.tsx b/src/components/overlays/ProgressOverlay.tsx similarity index 98% rename from src/components/ProgressOverlay.tsx rename to src/components/overlays/ProgressOverlay.tsx index 25072d8..586c965 100644 --- a/src/components/ProgressOverlay.tsx +++ b/src/components/overlays/ProgressOverlay.tsx @@ -9,8 +9,8 @@ */ import React, { useEffect, useRef, useState } from 'react'; import { FiLoader, FiCheckCircle, FiAlertTriangle } from 'react-icons/fi'; -import { useSimulationStore } from '../stores/useSimulationStore'; -import { CHART_SERIES } from '../theme/tokens'; +import { useSimulationStore } from '../../stores/useSimulationStore'; +import { CHART_SERIES } from '../../theme/tokens'; import clsx from 'clsx'; /** "İterasyon 120/800 — residual=1.2e-4" mesajından residual + iterasyon ayıkla. */ diff --git a/src/features/experiment/ExperimentImportDialog.tsx b/src/features/experiment/ExperimentImportDialog.tsx index 418df94..55937d7 100644 --- a/src/features/experiment/ExperimentImportDialog.tsx +++ b/src/features/experiment/ExperimentImportDialog.tsx @@ -32,7 +32,6 @@ const DATA_TYPE_LABELS: Record = { const ExperimentImportDialog: React.FC = ({ open, onCancel, onConfirm, suggestedColor }) => { const [filename, setFilename] = useState(''); - const [rawText, setRawText] = useState(null); const [table, setTable] = useState(null); const [err, setErr] = useState(null); @@ -48,7 +47,6 @@ const ExperimentImportDialog: React.FC = ({ open, onCancel, onConfirm, su useEffect(() => { if (open) { setFilename(''); - setRawText(null); setTable(null); setErr(null); setXCol(''); @@ -113,7 +111,6 @@ const ExperimentImportDialog: React.FC = ({ open, onCancel, onConfirm, su if (!selected || typeof selected !== 'string') return; const text = await readTextFile(selected); setFilename(selected.split(/[\\/]/).pop() ?? selected); - setRawText(text); setErr(null); try { const parsed = parseTable(text, selected); diff --git a/src/components/SweepDialog.tsx b/src/features/sweep/SweepDialog.tsx similarity index 96% rename from src/components/SweepDialog.tsx rename to src/features/sweep/SweepDialog.tsx index 542314b..b2d220f 100644 --- a/src/components/SweepDialog.tsx +++ b/src/features/sweep/SweepDialog.tsx @@ -9,13 +9,13 @@ */ import React, { useMemo, useState, useEffect } from 'react'; import { FiX, FiPlay } from 'react-icons/fi'; -import { useDesignStore } from '../stores/useDesignStore'; -import { useSimulationStore } from '../stores/useSimulationStore'; -import { useSweepStore, buildSweepValues } from '../stores/useSweepStore'; -import { componentDisplayLabel, getParamOption, getSweepableParams } from '../utils/sweepHelpers'; -import { runSweep } from '../utils/sweepRunner'; -import { useEscapeClose } from '../hooks/useEscapeClose'; -import { toast } from '../stores/useUiStore'; +import { useDesignStore } from '../../stores/useDesignStore'; +import { useSimulationStore } from '../../stores/useSimulationStore'; +import { useSweepStore, buildSweepValues } from './useSweepStore'; +import { componentDisplayLabel, getParamOption, getSweepableParams } from './sweepHelpers'; +import { runSweep } from './sweepRunner'; +import { useEscapeClose } from '../../hooks/useEscapeClose'; +import { toast } from '../../stores/useUiStore'; interface Props { open: boolean; diff --git a/src/utils/sweepHelpers.test.ts b/src/features/sweep/sweepHelpers.test.ts similarity index 95% rename from src/utils/sweepHelpers.test.ts rename to src/features/sweep/sweepHelpers.test.ts index f4299eb..98b397d 100644 --- a/src/utils/sweepHelpers.test.ts +++ b/src/features/sweep/sweepHelpers.test.ts @@ -5,8 +5,8 @@ import { componentDisplayLabel, withOverriddenParam, } from './sweepHelpers'; -import { buildSweepValues } from '../stores/useSweepStore'; -import type { ChipComponent } from '../types'; +import { buildSweepValues } from './useSweepStore'; +import type { ChipComponent } from '../../types'; describe('buildSweepValues', () => { it('start ve end dahil, eşit aralıklı', () => { diff --git a/src/features/sweep/sweepHelpers.ts b/src/features/sweep/sweepHelpers.ts new file mode 100644 index 0000000..b4952ba --- /dev/null +++ b/src/features/sweep/sweepHelpers.ts @@ -0,0 +1,119 @@ +/** + * sweepHelpers — Parametre tarama yardımcıları (Faz 6B). + * + * Her bileşen tipi için süpürülebilir numerik parametrelerin anahtarları, + * kullanıcıya gösterilecek Türkçe etiketleri ve birimleri burada tanımlıdır. + * Seçenek dropdown'u ve SweepDialog preview için bu listeden beslenir. + */ +import type { ChipComponent, ComponentType } from '../../types'; + +export interface ParamOption { + key: string; + label: string; + unit: string; + /** Güvenli minimum değer (pozitiflik vb.). */ + min: number; + /** Önerilen maksimum (slider sınırı; kullanıcı elle geçebilir). */ + maxHint: number; +} + +const PARAM_OPTIONS: Record = { + straight_channel: [ + { key: 'width', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'length', label: 'Kanal uzunluğu', unit: 'μm', min: 1, maxHint: 20000 }, + { key: 'depth', label: 'Kanal derinliği', unit: 'μm', min: 1, maxHint: 500 }, + ], + curved_channel: [ + { key: 'radius', label: 'Yarıçap', unit: 'μm', min: 10, maxHint: 5000 }, + { key: 'angle', label: 'Açı', unit: '°', min: 1, maxHint: 360 }, + { key: 'width', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'depth', label: 'Kanal derinliği', unit: 'μm', min: 1, maxHint: 500 }, + ], + t_junction: [ + { key: 'mainWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'branchWidth', label: 'Dal kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'angle', label: 'Açı', unit: '°', min: 1, maxHint: 180 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + y_junction: [ + { key: 'mainWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'branchWidth', label: 'Dal kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'branchAngle', label: 'Dal açısı', unit: '°', min: 1, maxHint: 180 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + serpentine_mixer: [ + { key: 'channelWidth', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'turns', label: 'Dönüş sayısı', unit: '', min: 1, maxHint: 30 }, + { key: 'pitch', label: 'Adım', unit: 'μm', min: 10, maxHint: 5000 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + port: [{ key: 'diameter', label: 'Çap', unit: 'μm', min: 50, maxHint: 3000 }], + droplet_generator: [ + { key: 'orificeWidth', label: 'Orifis genişliği', unit: 'μm', min: 1, maxHint: 500 }, + { key: 'mainChannelWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { + key: 'dispersedChannelWidth', + label: 'Dispers kanal genişliği', + unit: 'μm', + min: 1, + maxHint: 2000, + }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + filter_array: [ + { key: 'pillarDiameter', label: 'Pilar çapı', unit: 'μm', min: 1, maxHint: 500 }, + { key: 'spacing', label: 'Aralık', unit: 'μm', min: 1, maxHint: 500 }, + { key: 'rows', label: 'Sıra sayısı', unit: '', min: 1, maxHint: 50 }, + { key: 'columns', label: 'Sütun sayısı', unit: '', min: 1, maxHint: 50 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + expansion: [ + { key: 'inletWidth', label: 'Giriş genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'outletWidth', label: 'Çıkış genişliği', unit: 'μm', min: 1, maxHint: 2000 }, + { key: 'length', label: 'Uzunluk', unit: 'μm', min: 1, maxHint: 20000 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], + reservoir: [ + { key: 'width', label: 'Genişlik', unit: 'μm', min: 10, maxHint: 10000 }, + { key: 'height', label: 'Yükseklik', unit: 'μm', min: 10, maxHint: 10000 }, + { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, + ], +}; + +export function getSweepableParams(type: ComponentType): ParamOption[] { + return PARAM_OPTIONS[type] ?? []; +} + +export function getParamOption(type: ComponentType, key: string): ParamOption | undefined { + return getSweepableParams(type).find((p) => p.key === key); +} + +/** Türkçe bileşen etiketi — dialog'da dropdown label olarak. */ +const TYPE_LABEL: Record = { + straight_channel: 'Düz Kanal', + curved_channel: 'Eğri Kanal', + t_junction: 'T-Birleşim', + y_junction: 'Y-Birleşim', + serpentine_mixer: 'Serpentin Mikser', + port: 'Port', + droplet_generator: 'Damlacık Üretici', + filter_array: 'Filtre Dizisi', + expansion: 'Genleşme', + reservoir: 'Rezervuar', +}; + +export function componentDisplayLabel(c: ChipComponent, index: number): string { + const base = c.label?.trim() || TYPE_LABEL[c.type]; + return `${base} #${index + 1}`; +} + +/** Bileşen kopyasında bir param anahtarını yeni değerle üzerine yaz. */ +export function withOverriddenParam(c: ChipComponent, key: string, value: number): ChipComponent { + return { + ...c, + params: { + ...(c.params as unknown as Record), + [key]: value, + } as unknown as ChipComponent['params'], + }; +} diff --git a/src/utils/sweepRunner.ts b/src/features/sweep/sweepRunner.ts similarity index 97% rename from src/utils/sweepRunner.ts rename to src/features/sweep/sweepRunner.ts index 8bac96f..875ad84 100644 --- a/src/utils/sweepRunner.ts +++ b/src/features/sweep/sweepRunner.ts @@ -6,13 +6,8 @@ * eklenir; UI reaktif olarak ilerlemeyi gösterir. */ import { invoke } from '@tauri-apps/api/core'; -import type { ChipComponent, Connection, FluidProperties } from '../types'; -import { - useSweepStore, - buildSweepValues, - type SweepConfig, - type SweepRun, -} from '../stores/useSweepStore'; +import type { ChipComponent, Connection, FluidProperties } from '../../types'; +import { useSweepStore, buildSweepValues, type SweepConfig, type SweepRun } from './useSweepStore'; import { withOverriddenParam } from './sweepHelpers'; export interface SweepRunnerArgs { diff --git a/src/stores/useSweepStore.ts b/src/features/sweep/useSweepStore.ts similarity index 91% rename from src/stores/useSweepStore.ts rename to src/features/sweep/useSweepStore.ts index a669d6d..2ad53b4 100644 --- a/src/stores/useSweepStore.ts +++ b/src/features/sweep/useSweepStore.ts @@ -24,11 +24,11 @@ export interface SweepConfig { export interface SweepRun { paramValue: number; - totalFlowRate: number; // μL/min + totalFlowRate: number; // μL/min totalResistance: number; - maxPressure: number; // Pa - minPressure: number; // Pa - maxVelocity: number; // m/s + maxPressure: number; // Pa + minPressure: number; // Pa + maxVelocity: number; // m/s maxReynolds: number; /** Hedeflenen bileşenin kendi metrikleri (varsa). */ componentFlowRate?: number; @@ -42,7 +42,7 @@ interface SweepState { config: SweepConfig | null; runs: SweepRun[]; running: boolean; - progress: number; // 0..1 + progress: number; // 0..1 currentIndex: number; cancelRequested: boolean; @@ -72,8 +72,7 @@ export const useSweepStore = create()((set) => ({ cancelRequested: false, }), - appendRun: (run) => - set((s) => ({ runs: [...s.runs, run] })), + appendRun: (run) => set((s) => ({ runs: [...s.runs, run] })), setProgress: (idx, total) => set({ diff --git a/src/hooks/useProjectIO.ts b/src/hooks/useProjectIO.ts index 162294c..c755b9b 100644 --- a/src/hooks/useProjectIO.ts +++ b/src/hooks/useProjectIO.ts @@ -9,7 +9,7 @@ import { useProjectStore } from '../stores/useProjectStore'; import { useDesignStore } from '../stores/useDesignStore'; import { useSimulationStore } from '../stores/useSimulationStore'; import { useExperimentStore } from '../features/experiment/useExperimentStore'; -import { useSweepStore } from '../stores/useSweepStore'; +import { useSweepStore } from '../features/sweep/useSweepStore'; import { toast, confirmAsync } from '../stores/useUiStore'; import type { RawMFlowProject } from '../types'; diff --git a/src/hooks/useSimulationRun.ts b/src/hooks/useSimulationRun.ts index acc3c74..a9f3298 100644 --- a/src/hooks/useSimulationRun.ts +++ b/src/hooks/useSimulationRun.ts @@ -8,7 +8,7 @@ import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { useSimulationStore } from '../stores/useSimulationStore'; import { useDesignStore } from '../stores/useDesignStore'; -import { useSweepStore } from '../stores/useSweepStore'; +import { useSweepStore } from '../features/sweep/useSweepStore'; import { toast } from '../stores/useUiStore'; import type { ChipComponent, Connection, RawAnalyticDesignResult, CfdField } from '../types'; diff --git a/src/styles/design-system.css b/src/styles/design-system.css index 95be281..c8c6d1b 100644 --- a/src/styles/design-system.css +++ b/src/styles/design-system.css @@ -129,7 +129,7 @@ /* ── Tipografi ────────────────────────────────────────────────────────── */ --font-sans: 'IBM Plex Sans', system-ui, sans-serif; - --font-mono: 'IBM Plex Mono', 'JetBrains Mono', Consolas, monospace; + --font-mono: 'IBM Plex Mono', Consolas, monospace; } /* ════════════════════════════════════════════════════════════════════════ diff --git a/src/utils/portUtils.ts b/src/utils/portUtils.ts index 456fe18..d8c96e4 100644 --- a/src/utils/portUtils.ts +++ b/src/utils/portUtils.ts @@ -66,7 +66,6 @@ export function getPortInfos(comp: ChipComponent): PortInfo[] { case 't_junction': { const { mainWidth = 300, branchWidth = 200 } = p as TJunctionParams; - const halfBranch = branchWidth / 2; return [ { index: 0, diff --git a/src/utils/sweepHelpers.ts b/src/utils/sweepHelpers.ts deleted file mode 100644 index e5d34e2..0000000 --- a/src/utils/sweepHelpers.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * sweepHelpers — Parametre tarama yardımcıları (Faz 6B). - * - * Her bileşen tipi için süpürülebilir numerik parametrelerin anahtarları, - * kullanıcıya gösterilecek Türkçe etiketleri ve birimleri burada tanımlıdır. - * Seçenek dropdown'u ve SweepDialog preview için bu listeden beslenir. - */ -import type { ChipComponent, ComponentType } from '../types'; - -export interface ParamOption { - key: string; - label: string; - unit: string; - /** Güvenli minimum değer (pozitiflik vb.). */ - min: number; - /** Önerilen maksimum (slider sınırı; kullanıcı elle geçebilir). */ - maxHint: number; -} - -const PARAM_OPTIONS: Record = { - straight_channel: [ - { key: 'width', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'length', label: 'Kanal uzunluğu', unit: 'μm', min: 1, maxHint: 20000 }, - { key: 'depth', label: 'Kanal derinliği', unit: 'μm', min: 1, maxHint: 500 }, - ], - curved_channel: [ - { key: 'radius', label: 'Yarıçap', unit: 'μm', min: 10, maxHint: 5000 }, - { key: 'angle', label: 'Açı', unit: '°', min: 1, maxHint: 360 }, - { key: 'width', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'depth', label: 'Kanal derinliği', unit: 'μm', min: 1, maxHint: 500 }, - ], - t_junction: [ - { key: 'mainWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'branchWidth', label: 'Dal kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'angle', label: 'Açı', unit: '°', min: 1, maxHint: 180 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - y_junction: [ - { key: 'mainWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'branchWidth', label: 'Dal kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'branchAngle', label: 'Dal açısı', unit: '°', min: 1, maxHint: 180 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - serpentine_mixer: [ - { key: 'channelWidth', label: 'Kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'turns', label: 'Dönüş sayısı', unit: '', min: 1, maxHint: 30 }, - { key: 'pitch', label: 'Adım', unit: 'μm', min: 10, maxHint: 5000 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - port: [ - { key: 'diameter', label: 'Çap', unit: 'μm', min: 50, maxHint: 3000 }, - ], - droplet_generator: [ - { key: 'orificeWidth', label: 'Orifis genişliği', unit: 'μm', min: 1, maxHint: 500 }, - { key: 'mainChannelWidth', label: 'Ana kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'dispersedChannelWidth', label: 'Dispers kanal genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - filter_array: [ - { key: 'pillarDiameter', label: 'Pilar çapı', unit: 'μm', min: 1, maxHint: 500 }, - { key: 'spacing', label: 'Aralık', unit: 'μm', min: 1, maxHint: 500 }, - { key: 'rows', label: 'Sıra sayısı', unit: '', min: 1, maxHint: 50 }, - { key: 'columns', label: 'Sütun sayısı',unit: '', min: 1, maxHint: 50 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - expansion: [ - { key: 'inletWidth', label: 'Giriş genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'outletWidth', label: 'Çıkış genişliği', unit: 'μm', min: 1, maxHint: 2000 }, - { key: 'length', label: 'Uzunluk', unit: 'μm', min: 1, maxHint: 20000 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], - reservoir: [ - { key: 'width', label: 'Genişlik', unit: 'μm', min: 10, maxHint: 10000 }, - { key: 'height', label: 'Yükseklik', unit: 'μm', min: 10, maxHint: 10000 }, - { key: 'depth', label: 'Derinlik', unit: 'μm', min: 1, maxHint: 500 }, - ], -}; - -export function getSweepableParams(type: ComponentType): ParamOption[] { - return PARAM_OPTIONS[type] ?? []; -} - -export function getParamOption(type: ComponentType, key: string): ParamOption | undefined { - return getSweepableParams(type).find((p) => p.key === key); -} - -/** Türkçe bileşen etiketi — dialog'da dropdown label olarak. */ -const TYPE_LABEL: Record = { - straight_channel: 'Düz Kanal', - curved_channel: 'Eğri Kanal', - t_junction: 'T-Birleşim', - y_junction: 'Y-Birleşim', - serpentine_mixer: 'Serpentin Mikser', - port: 'Port', - droplet_generator: 'Damlacık Üretici', - filter_array: 'Filtre Dizisi', - expansion: 'Genleşme', - reservoir: 'Rezervuar', -}; - -export function componentDisplayLabel(c: ChipComponent, index: number): string { - const base = c.label?.trim() || TYPE_LABEL[c.type]; - return `${base} #${index + 1}`; -} - -/** Bileşen kopyasında bir param anahtarını yeni değerle üzerine yaz. */ -export function withOverriddenParam(c: ChipComponent, key: string, value: number): ChipComponent { - return { - ...c, - params: { ...(c.params as unknown as Record), [key]: value } as unknown as ChipComponent['params'], - }; -} diff --git a/tailwind.config.js b/tailwind.config.js index 29f48cb..37f5770 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -54,7 +54,7 @@ export default { }, fontFamily: { sans: ['"IBM Plex Sans"', 'system-ui', 'sans-serif'], - mono: ['"IBM Plex Mono"', 'JetBrains Mono', 'Consolas', 'monospace'], + mono: ['"IBM Plex Mono"', 'Consolas', 'monospace'], }, fontSize: { // CAD-friendly skala. base'den önce daha fazla küçük adım.