From 87229c2e1784055df461edd607cd2605dee2fa91 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 20:22:19 +0000 Subject: [PATCH 1/2] feat(propagation): complete Phase 3 roadmap items + COST 231 Closes P3-1, P3-3, P3-4 and P4-2 ahead of the next production build. P3-1 -- Client-side Hata/FSPL: Port Okumura-Hata to JavaScript (src/utils/math/hata.js), mirroring rf_physics.calculate_hata_loss term-for-term including input clamping; both engines agree to 4 decimal places on the reference cases. Add a dispatcher (src/utils/math/pathLoss.js) and wire LinkLayer to resolve `fspl` and `hata` locally, so neither needs /api/calculate-link and both keep working offline/PWA. `bullington` and server `itm` stay on the backend, which applies clutter_height and k_factor to the profile in ways the current JS Bullington does not. P4-2 -- COST 231-Hata: Extend Hata coverage from 150-1500 MHz to 150-2000 MHz in both engines. Selecting "Hata" auto-dispatches to COST 231 at/above 1500 MHz; `cost231` is also accepted as an explicit backend model. Per the standard, COST 231 defines only the 3 dB metropolitan correction and has no suburban/rural term, so those environments now warn above the crossover rather than silently reusing the Okumura-Hata corrections. Link Analysis validity warnings now come from a single getHataValidity helper. P3-3 -- WASM ITM for batch reports: Add a propagation-model selector to Batch Processing. ITM runs the same WASM engine as Link Analysis over a 100-point profile (vs 20 for Bullington), honouring Ground Type and Climate Zone, so batch numbers match single-link analysis. The module loads lazily on selection (useWasmITM gained an `enabled` flag, keeping it off the startup path), and a link that fails ITM falls back to Bullington instead of failing the whole report. P3-4 -- Per-node configs in batch CSV: Parse optional Antenna_Height, Antenna_Gain, TX_Power, Device and Antenna columns, with aliases and loose preset-name matching. nodeConfig.js merges them over the global A/B config field-by-field; blank cells and existing three-column files behave exactly as before. Also fixed: Bullington diffraction was never displayed for Hata in LinkLayer -- the check compared against 'Hata' while the model value is lowercase 'hata'. Mesh report export now writes via a Blob and escapes CSV fields, so node names containing commas no longer corrupt the report, and it gained Model/PathLoss_dB/per-node parameter columns plus a progress bar. P4-6 (variability modes) is documented as blocked: it needs new LinkParameters fields and an emscripten rebuild of meshrf.wasm. Tests: 44 frontend (28 new), 23 backend (14 new). Lint and build clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FU7LVUXdVx5YfXt4eJJSPs --- CHANGELOG.md | 10 + Documentation/batch-processing.md | 62 +++- Documentation/link-analyzer.md | 2 +- README.md | 16 +- ROADMAP.md | 42 +-- rf-engine/rf_physics.py | 58 +++- rf-engine/routers/analysis.py | 2 +- rf-engine/tests/test_rf_physics.py | 110 +++++++ src/components/Map/BatchProcessing.jsx | 375 +++++++++++++++++------ src/components/Map/LinkAnalysisPanel.jsx | 62 ++-- src/components/Map/LinkLayer.jsx | 29 +- src/hooks/useWasmITM.js | 9 +- src/utils/__tests__/csvParser.test.js | 163 ++++++++++ src/utils/__tests__/hata.test.js | 209 +++++++++++++ src/utils/csvImportExport.js | 50 +++ src/utils/csvParser.js | 99 +++++- src/utils/math/hata.js | 221 +++++++++++++ src/utils/math/pathLoss.js | 59 ++++ src/utils/nodeConfig.js | 27 ++ src/utils/rfConstants.js | 35 ++- src/utils/rfMath.js | 2 + 21 files changed, 1462 insertions(+), 180 deletions(-) create mode 100644 rf-engine/tests/test_rf_physics.py create mode 100644 src/utils/__tests__/csvParser.test.js create mode 100644 src/utils/__tests__/hata.test.js create mode 100644 src/utils/math/hata.js create mode 100644 src/utils/math/pathLoss.js create mode 100644 src/utils/nodeConfig.js diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ed6df..ce3caa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Per-Node Coverage Visualization (P6-1)**: Multi-Site Analysis composite overlay now renders each selected node's coverage as a distinct color instead of one flat cyan mask, so overlapping/redundant sites are visually distinguishable. Simulation node markers and the Sites results table are color-matched to the same coverage patches. +- **Client-Side Hata & FSPL (P3-1)**: Okumura-Hata is now implemented in JavaScript (`src/utils/math/hata.js`), mirroring the Python engine term-for-term. The Link Analysis tool resolves the `fspl` and `hata` models locally instead of calling `/api/calculate-link`, so both remain usable with no backend -- including offline/PWA use. `bullington` and server-side `itm` still use the backend, which applies clutter height and k-factor to the terrain profile. +- **COST 231-Hata Extension (P4-2)**: Hata coverage extended from 150-1500 MHz to 150-2000 MHz. Selecting "Hata" automatically dispatches to COST 231 at or above 1500 MHz in both the frontend and `rf-engine`; `cost231` is also accepted as an explicit backend model. COST 231 defines only the 3 dB metropolitan correction, so choosing a suburban or rural environment above the crossover now raises an explicit warning rather than silently reusing the Okumura-Hata corrections. +- **WASM ITM for Batch Reports (P3-3)**: Batch Processing gained a propagation-model selector. Choosing Longley-Rice ITM runs the same WASM engine as Link Analysis over a 100-point terrain profile (vs 20 for Bullington), honouring Ground Type and Climate Zone, so batch numbers agree with single-link analysis. The engine loads lazily on selection, and a link that fails ITM falls back to Bullington instead of failing the report. +- **Per-Node Configs in Batch CSV (P3-4)**: Batch CSV import now accepts optional `Antenna_Height`, `Antenna_Gain`, `TX_Power`, `Device` and `Antenna` columns (with aliases such as `Height`/`AGL`/`Gain`/`Power`, and loose preset-name matching so `HELTEC_V3`, `heltec-v3` and `Heltec V3` all resolve). Values are merged over the global A/B config field-by-field; blank cells and existing three-column files behave exactly as before. The sidebar reports how many loaded nodes carry an override. +- Mesh report CSV now includes `Model`, `PathLoss_dB`, and the per-node TX/RX heights, gains and TX power, plus a progress bar while the report runs. ### Changed - **Backend**: Refactored `rf-engine/core/viewshed_proc.py` to share a single pixel-projection helper instead of three near-identical copies of the same coordinate-mapping logic. - Added `eslint-plugin-react`'s `jsx-uses-vars` rule to `eslint.config.js` -- the previous config had no way to recognize JSX component usage, producing ~100 false-positive "unused import" warnings that were masking real ones. - Added a `ci.yml` GitHub Actions workflow that runs frontend lint/test/build and the `rf-engine` pytest suite on every push and PR to `dev`/`main`. +- `useWasmITM` accepts an `enabled` flag so always-mounted panels can defer loading the WASM module until ITM is actually selected, keeping it off the startup path. +- Hata validity warnings in the Link Analysis panel are driven by a single `getHataValidity` helper instead of inline thresholds, so they track the active model variant. +- Mesh report export writes via a Blob instead of a `data:` URI and escapes CSV fields, so node names containing commas or quotes no longer corrupt the report. ### Fixed +- Bullington diffraction loss was never shown for the Hata model in `LinkLayer`: the check compared against `'Hata'` while the model value is lowercase `'hata'`, so the condition could never be true. + - Dead `elevation` variable, unused caught SSE-parse error, and stale `eslint-disable` directives now flagged/cleaned up now that lint output is trustworthy again. - `ROADMAP.md` had a duplicated P5-6/P5-7 section from a copy-paste error. - `README.md` linked to `Documentation/pwa-guide.md`, which never existed; the guide has been written. diff --git a/Documentation/batch-processing.md b/Documentation/batch-processing.md index c8dc365..540a3f6 100644 --- a/Documentation/batch-processing.md +++ b/Documentation/batch-processing.md @@ -20,6 +20,36 @@ The first line can be a header or data. The tool uses a heuristic to identify co > [!TIP] > Use the **"Download Template"** link in the sidebar to get a perfectly formatted CSV file. +### Per-Node Hardware Overrides + +When your CSV has a header row, you can add optional columns to give individual +nodes their own hardware instead of applying the global A/B configuration to +every site. Any column you leave out — or leave blank on a given row — falls +back to the global config, so existing three-column files keep working unchanged. + +| Column | Aliases | Meaning | +| --- | --- | --- | +| `Antenna_Height` | `Height`, `AGL` | Antenna height above ground, in meters | +| `Antenna_Gain` | `Gain` | Antenna gain in dBi | +| `TX_Power` | `Power` | Transmit power in dBm | +| `Device` | — | Device preset, e.g. `HELTEC_V3`, `RAK_4631`, `Station G2` | +| `Antenna` | `Antenna_Type` | Antenna preset, e.g. `YAGI`, `OMNI_HIGH`, `Standard Dipole` | + +Preset names are matched loosely — `HELTEC_V3`, `heltec-v3` and `Heltec V3` all +resolve to the same device. Selecting an `Antenna` preset supplies its nominal +gain automatically; an explicit `Antenna_Gain` on the same row wins. + +```csv +Name,Lat,Lon,Antenna_Height,Antenna_Gain,TX_Power,Device,Antenna +Site Alpha,45.5152,-122.6784,30,8,22,HELTEC_V3,OMNI_HIGH +Site Bravo,45.5252,-122.6684,12,,,,DIPOLE +Site Delta,45.5100,-122.6500,,,,, +``` + +Above, Alpha is fully specified, Bravo overrides only its height and antenna +(taking 3.0 dBi from the dipole preset), and Delta inherits everything from the +global config. The sidebar shows how many loaded nodes carry an override. + ## 2. Integration with Link Analyzer The Batch Nodes panel provides a seamless transition into detailed point-to-point analysis: @@ -32,7 +62,25 @@ By using these buttons, you can quickly verify a link's profile and Fresnel clea > [!IMPORTANT] > Selecting a different tool from the toolbar (e.g., switching from Link Analysis to Viewshed) will clear the current analysis markers and results from the map. However, your **Batch Nodes** will remain visible until you click **"Clear All Nodes"**. -## 3. Exporting Mesh Reports +## 3. Choosing a Propagation Model + +Once more than one node is loaded, a **Propagation Model** selector appears above +the export button: + +- **Bullington (Fast)** — Free-space path loss plus knife-edge diffraction, + computed over a 20-point terrain profile per link. This is the default and is + well suited to sweeping a large candidate list quickly. +- **Longley-Rice ITM (Accurate, Slower)** — The same WASM ITM engine the Link + Analysis tool uses, run over a denser 100-point profile and using your + **Ground Type** and **Climate Zone** settings. Batch numbers then agree with + what you see when you open the same link in the Link Analyzer. + +The ITM engine is loaded on demand the first time you select it; the export +button stays disabled for the moment it takes to initialize. If ITM fails on an +individual link, that link falls back to Bullington rather than failing the +whole report — the `Model` column records which model actually produced each row. + +## 4. Exporting Mesh Reports The most powerful feature of Batch Processing is the **"Export Mesh Report"** button. @@ -40,15 +88,23 @@ The most powerful feature of Batch Processing is the **"Export Mesh Report"** bu 1. It iterates through every possible pair of nodes in your imported list. 2. It performs a terrain profile analysis for every link (Source to Target). -3. It calculates the Link Budget based on your current **Hardware Settings**. +3. It calculates the Link Budget from each node's own configuration, falling + back to your global **Hardware Settings** where no override is given. 4. It generates a CSV report containing: - **Distance**: In kilometers. - **Status**: GOOD, MARGINAL, or OBSTRUCTED. + - **Quality**: Fresnel-based link quality rating. - **Clearance**: Minimum Fresnel zone clearance in meters. - **Margin**: Signal margin in dB. + - **Model**: Which propagation model produced the row (`ITM` or `Bullington`). + - **PathLoss_dB**: Total path loss used for the budget. + - **Per-node params**: TX/RX antenna height, TX/RX gain, and TX power, so the + report is self-documenting when nodes differ. + +A progress bar tracks completion while the report runs. > [!WARNING] -> Processing many links (e.g., 20+ nodes result in 190+ links) can take several minutes as the tool fetches terrain data for each path. +> Processing many links (e.g., 20+ nodes result in 190+ links) can take several minutes as the tool fetches terrain data for each path. The ITM model is noticeably slower than Bullington, since it fetches a 5x denser terrain profile per link. ## Workflow Example diff --git a/Documentation/link-analyzer.md b/Documentation/link-analyzer.md index 91990d0..04152f2 100644 --- a/Documentation/link-analyzer.md +++ b/Documentation/link-analyzer.md @@ -7,7 +7,7 @@ The **Link Analyzer** is designed for detailed point-to-point analysis between t - **Profile Verification**: Real-time terrain profile between Site A and Site B. - **Fresnel Zone Analysis**: Visualizes the first Fresnel zone and calculates minimum clearance. - **Link Budget Calculation**: Predicts RSSI and link margin based on hardware and environment settings. -- **Propagation Models**: Choose between Free Space Path Loss (Ideal) and Okumura-Hata (Realistic). +- **Propagation Models**: Choose between Free Space Path Loss (Ideal), Okumura-Hata / COST 231 (Realistic, 150-2000 MHz), Bullington, and ITM. FSPL and the Hata family are computed in the browser and need no backend connection. ## How to Use diff --git a/README.md b/README.md index 32002a7..7102039 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ meshRF is designed for **mission-critical availability**. It operates with **zer - **Advanced Models**: - **ITM (Longley-Rice)**: High-precision WASM physical modeling. - **Bullington**: Terrain-aware diffraction (Backend). - - **Okumura-Hata**: Empirical model for urban zones. + - **Okumura-Hata / COST 231**: Empirical model for urban zones, covering 150-2000 MHz. - **Free Space**: Baseline physics comparison. - **Model Selector**: Comparison tool to instantly switch between models for A/B testing. - **Asymmetric Links**: Configure unique hardware (power, gain, height) for **Node A** and **Node B** independently. @@ -36,7 +36,9 @@ meshRF is designed for **mission-critical availability**. It operates with **zer ### 3. ⚡ Batch Operations & reporting - **Bulk Link Matrix**: Import CSVs (`Name, Lat, Lon`) to instantly compute link budgets for entire networks. -- **Automated Reporting**: Export detailed CSV reports containing RSSI, Signal Margin, and Clearance values. +- **Per-Node Hardware**: Optional CSV columns (antenna height, gain, TX power, device, antenna) let individual sites override the global config for realistic mixed-device meshes. +- **Selectable Batch Model**: Run mesh reports with fast Bullington diffraction or full WASM ITM for terrain-accurate results that match Link Analysis. +- **Automated Reporting**: Export detailed CSV reports containing RSSI, Signal Margin, Clearance, path loss, and the per-node parameters used. - **Context-Aware Guidance**: Every tool features built-in, interactive help banners that update based on your current mode, guiding you through workflows step-by-step. ### 4. 📚 Documentation @@ -67,11 +69,17 @@ meshRF supports multiple propagation models to suit different environments: | Model | Best For | Characteristics | | :--------------------- | :---------------- | :----------------------------------------------------------------------------- | -| **Free Space (FSPL)** | Ideal LOS, Orbit | Baseline physics, no terrain or environment effects. | -| **Okumura-Hata** | Flat/Suburban | Empirical model for urban/suburban. Assumes flat terrain. | +| **Free Space (FSPL)** | Ideal LOS, Orbit | Baseline physics, no terrain or environment effects. Runs client-side. | +| **Okumura-Hata** | Flat/Suburban | Empirical model for urban/suburban, 150-1500 MHz. Assumes flat terrain. Runs client-side. | +| **COST 231-Hata** | 1.5-2 GHz ISM | Hata extended to 1500-2000 MHz. Selected automatically above 1500 MHz. | | **Bullington** | Terrain/Mesh | Efficient terrain-aware diffraction. Fast & reliable for terrestrial links. | | **ITM (Longley-Rice)** | Irregular Terrain | **Gold Standard**. High-fidelity WASM-powered physical modeling. Ground-aware. | +> [!NOTE] +> FSPL and the Hata family are computed in the browser, so they remain available +> when the Python backend is unreachable (including offline/PWA use). Bullington +> and server-side ITM require the RF Engine. + > [!TIP] > Use **ITM (Longley-Rice)** for mission-critical link analysis. It accounts for irregular terrain, diffraction, troposcatter, and specific ground/climate parameters. Use **Bullington** for rapid terrain-aware estimates. diff --git a/ROADMAP.md b/ROADMAP.md index 560afb2..7d7862d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # MeshRF Propagation Engine Roadmap -**Last Updated:** 2026-07-24 +**Last Updated:** 2026-07-27 --- @@ -26,34 +26,22 @@ - [x] **P5-3** Adaptive Grid & Heatmap: Replaced fixed grid with dynamic density scan and added Heatmap visualization overlay in `OptimizationLayer`. - [x] **P5-7** Export Capabilities: Added CSV and KML export for optimized site candidates via `/export-results` endpoint. ---- - -## Phase 3: Full Model Switching (Medium-term) +### Phase 3: Full Model Switching (Jul 2026) -### P3-1: Client-Side Hata/FSPL Models +- [x] **P3-1** Client-Side Hata/FSPL Models. Ported Okumura-Hata to JavaScript (`src/utils/math/hata.js`), mirroring `rf_physics.calculate_hata_loss` term-for-term so client and server agree to 4 decimal places. Added a dispatcher (`src/utils/math/pathLoss.js`) and wired `LinkLayer.jsx` to resolve `fspl` and `hata` locally -- these models now work with no backend, including offline/PWA. `bullington` and server `itm` deliberately stay on the backend, which applies `clutter_height` and `k_factor` to the terrain profile in ways the current JS Bullington does not. +- [x] **P3-3** WASM ITM for Batch Processing. `BatchProcessing.jsx` gained a propagation-model selector; choosing ITM runs the same WASM engine as Link Analysis over a denser 100-point profile (vs 20 for Bullington), honouring Ground Type and Climate Zone. The module loads lazily on selection, and an individual link that fails ITM falls back to Bullington rather than failing the report. The `Model` column records which model produced each row. +- [x] **P3-4** Per-Node Configs in Batch CSV. `csvParser.js` now reads optional `Antenna_Height`, `Antenna_Gain`, `TX_Power`, `Device` and `Antenna` columns (with aliases and loose preset-name matching); `nodeConfig.js` merges them over the global A/B config field-by-field. Blank cells and three-column files behave exactly as before. Mesh reports also export the per-node params used. -Move Okumura-Hata and explicit FSPL calculations to JavaScript so model switching works without the Python backend. The Bullington diffraction model is already in `rfMath.js`. Adding Hata eliminates the backend dependency for non-ITM models. +--- -**Files:** `src/utils/rfMath.js`, `src/components/Map/LinkLayer.jsx` +## Phase 3: Remaining ### P3-2: Model Selection for RF Coverage -Currently the RF coverage tool is hardwired to WASM ITM. Add a model dispatch in `useRFCoverageTool.js` that supports FSPL-only or Hata for faster coverage maps when full ITM precision isn't needed. ITM remains the default. +Currently the RF coverage tool is hardwired to WASM ITM. Add a model dispatch in `useRFCoverageTool.js` that supports FSPL-only or Hata for faster coverage maps when full ITM precision isn't needed. ITM remains the default. Now that `calculateClientPathLoss` exists (P3-1), the remaining work is a JS raster path over the stitched elevation grid. **Files:** `src/hooks/useRFCoverageTool.js`, `src/components/Map/Controls/CoverageClickHandler.jsx` -### P3-3: WASM ITM for Batch Processing - -Batch mesh reports currently use FSPL + Bullington (frontend-only). Integrate the WASM ITM path (same as link analysis) for full terrain-aware batch reports. Requires fetching elevation profiles for each node pair. - -**Files:** `src/components/Map/BatchProcessing.jsx`, `src/hooks/useWasmITM.js` - -### P3-4: Per-Node Configs in Batch CSV - -Allow CSV import to include optional per-node columns: antenna height, device type, antenna type. Currently all batch nodes use the global A/B config. Per-node overrides would enable realistic multi-device mesh planning. - -**Files:** `src/components/Map/BatchProcessing.jsx` - --- ## Phase 4: Advanced Integration (Long-term) @@ -64,12 +52,6 @@ Allow CSV import to include optional per-node columns: antenna height, device ty **Files:** `rf-engine/rf_physics.py`, `rf-engine/tasks/` -### P4-2: COST 231 Hata Extension - -Current Hata model covers 150-1500 MHz. The COST 231 extension covers 1500-2000 MHz for future higher-frequency deployments (e.g., 2.4 GHz ISM). Straightforward formula addition. - -**Files:** `rf-engine/rf_physics.py`, `src/utils/rfMath.js` (if client-side Hata is added in P3-1) - ### P4-3: Clutter / Land-Use Integration Current clutter model applies a uniform height everywhere. Integrating land cover data (NLCD for US, Corine for EU) would enable per-pixel clutter classification: forest canopy height, urban building density, open field. This would significantly improve coverage accuracy in mixed environments. @@ -105,6 +87,8 @@ The ITM supports time/location/situation variability percentages (currently fixe - Best-case estimation (10/10/10 for maximum range) - Statistical coverage contours showing probability of reception +**Blocked on a native rebuild.** `time_pct` is hardcoded to 50.0 in `libmeshrf/src/meshrf_itm.cpp`, and `LinkParameters` (`libmeshrf/include/meshrf_itm.h`, bound in `src/bindings.cpp`) exposes no variability fields. This needs three new struct members, updated Embind bindings, and a fresh emscripten build of `public/meshrf.wasm` -- it cannot ship as a frontend-only change. + --- ### High Priority Bugs (Immediate) @@ -155,6 +139,12 @@ The ITM supports time/location/situation variability percentages (currently fixe ## Recently Completed +### P4-2: COST 231-Hata Extension (Completed) + +**Status:** ✅ Implemented 2026-07-27. +**Problem:** The Hata model only covered 150-1500 MHz, leaving the 1.5-2 GHz range unsupported. +**Solution:** Added COST 231-Hata to both engines (`calculate_cost231_loss` in `rf-engine/rf_physics.py`, `calculateCost231Loss` in `src/utils/math/hata.js`). Selecting "Hata" now auto-dispatches to COST 231 at or above 1500 MHz, and `cost231` is also accepted as an explicit backend model. Per the standard, COST 231 defines only the 3 dB metropolitan correction -- it has no suburban or rural term -- so `getHataValidity` warns when those environments are selected above the crossover instead of silently reusing the Okumura-Hata corrections. Validity warnings in the Link Analysis panel are now driven from that single helper rather than inline thresholds. + ### P6-1: Per-Node Coverage Visualization (Completed) **Status:** ✅ Implemented 2026-07-24. diff --git a/rf-engine/rf_physics.py b/rf-engine/rf_physics.py index d9c9f14..439ed07 100644 --- a/rf-engine/rf_physics.py +++ b/rf-engine/rf_physics.py @@ -127,6 +127,54 @@ def calculate_hata_loss(dist_m, freq_mhz, tx_h, rx_h, environment='urban_small') return max(0.0, loss) +COST231_CROSSOVER_MHZ = 1500.0 + + +def calculate_cost231_loss(dist_m, freq_mhz, tx_h, rx_h, environment='urban_small'): + """ + Calculate COST 231-Hata Path Loss (ROADMAP P4-2). + Valid for 1500-2000 MHz, 1-20km -- extends Okumura-Hata into the upper ISM + bands (e.g. 1.8/2.0 GHz). + + COST 231 defines only a metropolitan correction factor C (3 dB); it has no + separate suburban or rural correction, so those environments evaluate the + same as 'urban_small'. Mirrored by calculateCost231Loss in + src/utils/math/hata.js. + """ + dist_km = dist_m / 1000.0 + + # Clamp identically to calculate_hata_loss + d = max(0.1, dist_km) + f = freq_mhz + hb = max(1, tx_h) + hm = max(1, rx_h) + + logF = math.log10(f) + logHb = math.log10(hb) + logD = math.log10(d) + + # COST 231 always uses the small/medium-city mobile height correction + a_hm = (1.1 * logF - 0.7) * hm - (1.56 * logF - 0.8) + + # C = 3 dB metropolitan centres, 0 dB medium cities / suburban + metro = 3.0 if environment == 'urban_large' else 0.0 + + # L = 46.3 + 33.9*log(f) - 13.82*log(hb) - a(hm) + (44.9 - 6.55*log(hb))*log(d) + C + loss = 46.3 + 33.9 * logF - 13.82 * logHb - a_hm + (44.9 - 6.55 * logHb) * logD + metro + + return max(0.0, loss) + + +def calculate_hata_family_loss(dist_m, freq_mhz, tx_h, rx_h, environment='urban_small'): + """ + Dispatch between Okumura-Hata (<1500 MHz) and COST 231 (>=1500 MHz) so a + single 'hata' model selection covers 150-2000 MHz. + """ + if freq_mhz >= COST231_CROSSOVER_MHZ: + return calculate_cost231_loss(dist_m, freq_mhz, tx_h, rx_h, environment) + return calculate_hata_loss(dist_m, freq_mhz, tx_h, rx_h, environment) + + def calculate_path_loss(dist_m, elevs, freq_mhz, tx_h, rx_h, model='bullington', environment='suburban', k_factor=1.333, clutter_height=0.0): """ Generic Path Loss Calculator. @@ -135,10 +183,14 @@ def calculate_path_loss(dist_m, elevs, freq_mhz, tx_h, rx_h, model='bullington', dist_km = dist_m / 1000.0 if dist_km < 0.001: return 0.0 - # 1. Okumura-Hata + # 1. Okumura-Hata (auto-extends to COST 231 above 1500 MHz) if model == 'hata': - return calculate_hata_loss(dist_m, freq_mhz, tx_h, rx_h, environment) - + return calculate_hata_family_loss(dist_m, freq_mhz, tx_h, rx_h, environment) + + # 1b. Explicit COST 231 selection + if model == 'cost231': + return calculate_cost231_loss(dist_m, freq_mhz, tx_h, rx_h, environment) + # 2. Free Space (FSPL) - implicitly used as base for others or explicit fspl = 20 * math.log10(dist_km) + 20 * math.log10(freq_mhz) + 32.45 diff --git a/rf-engine/routers/analysis.py b/rf-engine/routers/analysis.py index ff28830..e3e9685 100644 --- a/rf-engine/routers/analysis.py +++ b/rf-engine/routers/analysis.py @@ -13,7 +13,7 @@ class LinkRequest(BaseModel): frequency_mhz: float tx_height: float rx_height: float - model: str = "bullington" # bullington, fspl + model: str = "bullington" # bullington, fspl, hata, cost231, itm environment: str = "suburban" k_factor: float = 1.333 clutter_height: float = 0.0 diff --git a/rf-engine/tests/test_rf_physics.py b/rf-engine/tests/test_rf_physics.py new file mode 100644 index 0000000..bf8d3c0 --- /dev/null +++ b/rf-engine/tests/test_rf_physics.py @@ -0,0 +1,110 @@ +import math +import sys +import os + +import pytest + +# Add parent directory to path to import modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import rf_physics + + +class TestHataLoss: + """Okumura-Hata (ROADMAP P3-1 -- must stay in step with src/utils/math/hata.js).""" + + def test_urban_small_reference(self): + # f=900MHz, hb=50m, hm=1.5m, d=5km. Hand-computed: 146.9428 dB + loss = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'urban_small') + assert loss == pytest.approx(146.9428, abs=1e-3) + + def test_suburban_is_reduction_from_urban(self): + urban = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'urban_small') + suburban = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'suburban') + expected_delta = 2 * (math.log10(900 / 28) ** 2) + 5.4 + assert urban - suburban == pytest.approx(expected_delta, abs=1e-6) + + def test_environment_ordering(self): + urban = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'urban_small') + suburban = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'suburban') + rural = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'rural') + assert urban > suburban > rural + + def test_clamps_degenerate_input(self): + loss = rf_physics.calculate_hata_loss(0, 915, 0, 0, 'suburban') + assert math.isfinite(loss) + assert loss >= 0.0 + + +class TestCost231Loss: + """COST 231-Hata extension (ROADMAP P4-2).""" + + def test_reference_1800mhz(self): + # f=1800MHz, hb=50m, hm=1.5m, d=5km, C=0. Hand-computed: 156.7364 dB + loss = rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + assert loss == pytest.approx(156.7364, abs=1e-3) + + def test_metropolitan_correction_is_3db(self): + medium = rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + metro = rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_large') + assert metro - medium == pytest.approx(3.0, abs=1e-9) + + def test_no_suburban_or_rural_term(self): + medium = rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + assert rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'suburban') == pytest.approx(medium) + assert rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'rural') == pytest.approx(medium) + + def test_higher_loss_than_hata_at_900(self): + hata_900 = rf_physics.calculate_hata_loss(5000, 900, 50, 1.5, 'urban_small') + cost_1800 = rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + assert cost_1800 > hata_900 + + +class TestHataFamilyDispatch: + def test_below_crossover_uses_hata(self): + assert rf_physics.calculate_hata_family_loss(5000, 915, 50, 1.5, 'suburban') == pytest.approx( + rf_physics.calculate_hata_loss(5000, 915, 50, 1.5, 'suburban') + ) + + def test_at_and_above_crossover_uses_cost231(self): + for freq in (1500, 1800): + assert rf_physics.calculate_hata_family_loss(5000, freq, 50, 1.5, 'urban_small') == pytest.approx( + rf_physics.calculate_cost231_loss(5000, freq, 50, 1.5, 'urban_small') + ) + + +class TestPathLossDispatcher: + ELEVS = [100.0] * 20 + + def test_hata_model_auto_extends_above_1500mhz(self): + loss = rf_physics.calculate_path_loss( + 5000, self.ELEVS, 1800, 50, 1.5, model='hata', environment='urban_small' + ) + assert loss == pytest.approx( + rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + ) + + def test_explicit_cost231_model(self): + loss = rf_physics.calculate_path_loss( + 5000, self.ELEVS, 1800, 50, 1.5, model='cost231', environment='urban_small' + ) + assert loss == pytest.approx( + rf_physics.calculate_cost231_loss(5000, 1800, 50, 1.5, 'urban_small') + ) + + def test_fspl_model_unchanged(self): + loss = rf_physics.calculate_path_loss( + 5000, self.ELEVS, 915, 50, 1.5, model='fspl' + ) + expected = 20 * math.log10(5.0) + 20 * math.log10(915) + 32.45 + assert loss == pytest.approx(expected, abs=1e-9) + + def test_bullington_adds_diffraction_to_fspl(self): + # Flat terrain -> no diffraction, so bullington collapses to FSPL + loss = rf_physics.calculate_path_loss( + 5000, self.ELEVS, 915, 50, 1.5, model='bullington' + ) + fspl = rf_physics.calculate_path_loss( + 5000, self.ELEVS, 915, 50, 1.5, model='fspl' + ) + assert loss >= fspl diff --git a/src/components/Map/BatchProcessing.jsx b/src/components/Map/BatchProcessing.jsx index dcf923e..b44c587 100644 --- a/src/components/Map/BatchProcessing.jsx +++ b/src/components/Map/BatchProcessing.jsx @@ -1,9 +1,21 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { useRF } from '../../context/RFContext'; +import React, { useRef, useState, useEffect, useCallback } from 'react'; +import { useRF, GROUND_TYPES } from '../../context/RFContext'; import { fetchElevationPath } from '../../utils/elevation'; import { analyzeLinkProfile, calculateLinkBudget, calculateBullingtonDiffraction } from '../../utils/rfMath'; -import { DEVICE_PRESETS } from '../../data/presets'; import { parseBatchNodesCSV } from '../../utils/csvParser'; +import { resolveNodeConfig } from '../../utils/nodeConfig'; +import { csvEscape, downloadTextFile, downloadBatchNodesTemplate } from '../../utils/csvImportExport'; +import { useWasmITM } from '../../hooks/useWasmITM'; + +/** + * Elevation samples per link. ITM needs a denser profile to resolve terrain + * (the backend uses 100 for the same reason); Bullington stays coarse so large + * meshes remain quick. + */ +const PROFILE_SAMPLES = { + bullington: 20, + itm_wasm: 100, +}; const BatchProcessing = () => { const { @@ -12,13 +24,22 @@ const BatchProcessing = () => { freq, nodeConfigs, kFactor, clutterHeight, sf, bw, fadeMargin, + groundType, climate, isMobile, sidebarIsOpen } = useRF(); const [batchNotification, setBatchNotification] = useState(null); // { message, type } const [showHelp, setShowHelp] = useState(false); + const [batchModel, setBatchModel] = useState('bullington'); + const [progress, setProgress] = useState(null); // { done, total } const fileInputRef = useRef(null); + // WASM ITM for terrain-aware batch reports (ROADMAP P3-3). + // Loaded lazily -- this panel is always mounted, so the module is only + // instantiated once the user actually selects the ITM model. + const itmSelected = batchModel === 'itm_wasm'; + const { calculatePathLoss: calculateITM, isReady: itmReady } = useWasmITM(itmSelected); + useEffect(() => { if (batchNotification) { const timer = setTimeout(() => { @@ -28,6 +49,158 @@ const BatchProcessing = () => { } }, [batchNotification]); + /** + * Analyse a single node pair and return its CSV row fields. + * Falls back from ITM to Bullington per-link if the WASM call fails, so one + * bad profile doesn't sink the whole report. + */ + const analyzePair = useCallback(async (n1, n2, useITM) => { + const samples = useITM ? PROFILE_SAMPLES.itm_wasm : PROFILE_SAMPLES.bullington; + const profile = await fetchElevationPath( + { lat: n1.lat, lng: n1.lng }, + { lat: n2.lat, lng: n2.lng }, + samples + ); + + // Need at least two points to derive a step size / distance + if (!profile || profile.length < 2) return null; + + const cfgA = resolveNodeConfig(n1, nodeConfigs.A); + const cfgB = resolveNodeConfig(n2, nodeConfigs.B); + + const analysis = analyzeLinkProfile( + profile, + freq, + cfgA.antennaHeight, + cfgB.antennaHeight, + kFactor, + clutterHeight + ); + + const distKm = profile[profile.length - 1].distance; + + let pathLossOverride = null; + let excessLoss = 0; + let modelUsed = 'Bullington'; + + if (useITM) { + try { + const ground = GROUND_TYPES[groundType] || GROUND_TYPES['Average Ground']; + const totalDistMeters = distKm * 1000; + const loss = await calculateITM({ + elevationProfile: new Float32Array(profile.map((p) => p.elevation)), + stepSizeMeters: totalDistMeters / (profile.length - 1), + frequencyMHz: freq, + txHeightM: cfgA.antennaHeight, + rxHeightM: cfgB.antennaHeight, + groundEpsilon: ground.epsilon, + groundSigma: ground.sigma, + climate, + }); + + if (Number.isFinite(loss)) { + pathLossOverride = loss; + modelUsed = 'ITM'; + } + } catch (e) { + console.error('Batch ITM failed, falling back to Bullington', e); + } + } + + if (pathLossOverride === null) { + // FSPL + Bullington diffraction (terrain-aware, no WASM required) + excessLoss = analysis.profileWithStats + ? calculateBullingtonDiffraction( + analysis.profileWithStats, + freq, + cfgA.antennaHeight, + cfgB.antennaHeight + ) + : 0; + } + + const budget = calculateLinkBudget({ + txPower: cfgA.txPower, + txGain: cfgA.antennaGain, + txLoss: cfgA.loss, + rxGain: cfgB.antennaGain, + rxLoss: cfgB.loss, + distanceKm: distKm, + freqMHz: freq, + sf, bw, + pathLossOverride, + excessLoss, + fadeMargin, + }); + + const status = analysis.isObstructed + ? 'OBSTRUCTED' + : (budget.margin > 10 ? 'GOOD' : 'MARGINAL'); + + return [ + n1.name, + n2.name, + distKm.toFixed(3), + status, + analysis.linkQuality, + budget.margin, + analysis.minClearance, + modelUsed, + // budget.fspl carries the override when one was supplied, otherwise + // plain FSPL -- diffraction is billed separately as excessLoss. + (pathLossOverride !== null ? pathLossOverride : budget.fspl + excessLoss).toFixed(2), + cfgA.antennaHeight, + cfgB.antennaHeight, + cfgA.antennaGain, + cfgB.antennaGain, + cfgA.txPower, + ]; + }, [nodeConfigs, freq, kFactor, clutterHeight, sf, bw, fadeMargin, groundType, climate, calculateITM]); + + /** Run the all-pairs mesh report and download it as CSV. */ + const runMeshReport = useCallback(async () => { + const useITM = batchModel === 'itm_wasm' && itmReady; + const total = batchNodes.length * (batchNodes.length - 1) / 2; + + const header = [ + 'Source', 'Target', 'Distance_km', 'Status', 'Quality', 'Margin_dB', + 'Clearance_m', 'Model', 'PathLoss_dB', 'TxHeight_m', 'RxHeight_m', + 'TxGain_dBi', 'RxGain_dBi', 'TxPower_dBm', + ]; + const rows = [header.join(',')]; + + setProgress({ done: 0, total }); + let done = 0; + + for (let i = 0; i < batchNodes.length; i++) { + for (let j = i + 1; j < batchNodes.length; j++) { + const n1 = batchNodes[i]; + const n2 = batchNodes[j]; + + try { + const fields = await analyzePair(n1, n2, useITM); + if (fields) rows.push(fields.map(csvEscape).join(',')); + } catch (e) { + console.error('Batch Error', e); + rows.push([n1.name, n2.name, ...Array(header.length - 2).fill('ERR')] + .map(csvEscape).join(',')); + } + + done += 1; + setProgress({ done, total }); + + // Small delay to prevent browser freeze & rate limit + await new Promise((r) => setTimeout(r, 200)); + } + } + + downloadTextFile( + rows.join('\n'), + `mesh_rf_analysis_${new Date().toISOString().slice(0, 10)}.csv` + ); + setProgress(null); + }, [batchNodes, batchModel, itmReady, analyzePair]); + const sectionStyle = { marginBottom: 'var(--spacing-lg)', borderBottom: '1px solid var(--color-border)', @@ -96,8 +269,15 @@ const BatchProcessing = () => { + {/* Propagation model for the mesh report (ROADMAP P3-3) */} + {batchNodes.length > 1 && ( +
+ + +
+ {!itmSelected && 'FSPL + knife-edge diffraction on 20-point profiles.'} + {itmSelected && itmReady && + 'Full terrain-aware ITM, matching Link Analysis. Uses 100-point profiles.'} + {itmSelected && !itmReady && ( + Loading ITM engine... + )} +
+
+ )} + {/* Export Report */} {batchNodes.length > 1 && ( - )} + + {/* Mesh report progress */} + {progress && progress.total > 0 && ( +
+
+
+
+
+ )} + {batchNodes.length > 0 && ( -
{batchNodes.length} Nodes Loaded
+
+ {batchNodes.length} Nodes Loaded + {batchNodes.some((n) => n.config) && ( + + {' '}({batchNodes.filter((n) => n.config).length} with per-node config) + + )} +
)} {/* Batch Import Notification Overlay */} diff --git a/src/components/Map/LinkAnalysisPanel.jsx b/src/components/Map/LinkAnalysisPanel.jsx index e21b3c8..5177a63 100644 --- a/src/components/Map/LinkAnalysisPanel.jsx +++ b/src/components/Map/LinkAnalysisPanel.jsx @@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from "react"; import PropTypes from "prop-types"; import L from "leaflet"; import LinkProfileChart from "./LinkProfileChart"; -import { calculateBullingtonDiffraction } from "../../utils/rfMath"; +import { calculateBullingtonDiffraction, getHataValidity } from "../../utils/rfMath"; import { useRF } from "../../context/RFContext"; import { useDraggablePanel } from "../../hooks/useDraggablePanel"; @@ -95,13 +95,22 @@ const LinkAnalysisPanel = ({ statusText = "Diffraction Limited"; } + // Hata-family validity (single source of truth in src/utils/math/hata.js -- + // covers both Okumura-Hata and the COST 231 extension above 1500 MHz) + const isHataModel = propagationSettings?.model?.toLowerCase() === "hata"; + const hataValidity = isHataModel + ? getHataValidity({ + distanceKm: distance, + freqMHz: freq, + txHeightM: h1, + rxHeightM: h2, + environment: propagationSettings?.environment, + }) + : null; + // Calculate Dimensions directly (Derived State) let layoutOffset = 380; - if ( - propagationSettings && - propagationSettings.model === "hata" && - (h1 < 30 || distance < 1 || distance > 20 || freq < 150 || freq > 1500) - ) { + if (hataValidity && hataValidity.warnings.length > 0) { layoutOffset += 60; } if (diffractionLoss > 0) { @@ -264,7 +273,9 @@ const LinkAnalysisPanel = ({ - +
@@ -379,8 +390,8 @@ const LinkAnalysisPanel = ({ - {/* Hata Validity Warnings */} - {propagationSettings.model === "hata" && ( + {/* Hata / COST 231 Validity Warnings */} + {hataValidity && hataValidity.warnings.length > 0 && (
- {(distance < 1 || distance > 20) && ( -
- - Dist {distance.toFixed(1)}km (Limit 1-20km) -
- )} - {h1 < 30 && ( -
- - TX {h1}m < 30m (Hata Min) -
- )} - {(freq < 150 || freq > 1500) && ( + {hataValidity.warnings.map((warning) => (
- Freq {freq}MHz (Limit 150-1500) + {warning}
- )} + ))}
)} diff --git a/src/components/Map/LinkLayer.jsx b/src/components/Map/LinkLayer.jsx index cca6cc3..5f4dec1 100644 --- a/src/components/Map/LinkLayer.jsx +++ b/src/components/Map/LinkLayer.jsx @@ -4,7 +4,7 @@ import { useMapEvents, Marker, Popup } from 'react-leaflet'; import L from 'leaflet'; import { useRF, GROUND_TYPES } from '../../context/RFContext'; import { DEVICE_PRESETS } from '../../data/presets'; -import { calculateLinkBudget, calculateFresnelPolygon, analyzeLinkProfile, calculateBullingtonDiffraction } from '../../utils/rfMath'; +import { calculateLinkBudget, calculateFresnelPolygon, analyzeLinkProfile, calculateBullingtonDiffraction, calculateClientPathLoss, isClientSideModel } from '../../utils/rfMath'; import { fetchElevationPath } from '../../utils/elevation'; import { calculateLink } from '../../utils/rfService'; import { useWasmITM } from '../../hooks/useWasmITM'; @@ -64,14 +64,35 @@ const LinkLayer = ({ nodes, setNodes, linkStats, setLinkStats, active = true, lo const currentModel = propagationSettings?.model?.toLowerCase() || 'itm_wasm'; const currentEnv = propagationSettings?.environment || 'suburban'; - // Parallel fetch: Elevation for profile/chart, and Path Loss from Backend + // Parallel fetch: Elevation for profile/chart, and Path Loss from Backend. + // FSPL and Hata/COST 231 resolve client-side (ROADMAP P3-1), so only the + // terrain-profile models still need the backend. + const needsBackend = (currentModel === 'bullington' || currentModel === 'itm'); + Promise.all([ fetchElevationPath(p1, p2), - (currentModel === 'hata' || currentModel === 'bullington' || currentModel === 'itm') + needsBackend ? calculateLink(p1, p2, currentFreq, h1, h2, currentModel, currentEnv, currentConfig.kFactor, currentConfig.clutterHeight) : Promise.resolve(null) ]) .then(async ([profile, backendResult]) => { + // Client-side model dispatch (no backend round trip required) + if (isClientSideModel(currentModel) && profile && profile.length > 0) { + const distanceKm = profile[profile.length - 1].distance; + const clientLoss = calculateClientPathLoss({ + model: currentModel, + distanceKm, + freqMHz: currentFreq, + txHeightM: h1, + rxHeightM: h2, + environment: currentEnv + }); + + if (clientLoss !== null && Number.isFinite(clientLoss)) { + backendResult = { path_loss_db: clientLoss }; + } + } + // WASM ITM Calculation override if (currentModel === 'itm_wasm' && itmReady && profile) { try { @@ -268,7 +289,7 @@ const LinkLayer = ({ nodes, setNodes, linkStats, setLinkStats, active = true, lo // Calculate Diffraction Loss (Bullington) for visualization let diffractionLoss = 0; - if (propagationSettings?.model === 'Hata' && linkStats.profileWithStats) { + if (propagationSettings?.model?.toLowerCase() === 'hata' && linkStats.profileWithStats) { diffractionLoss = calculateBullingtonDiffraction( linkStats.profileWithStats, freq, diff --git a/src/hooks/useWasmITM.js b/src/hooks/useWasmITM.js index f95c598..6ebb3c7 100644 --- a/src/hooks/useWasmITM.js +++ b/src/hooks/useWasmITM.js @@ -3,14 +3,19 @@ import createMeshRF from '../../libmeshrf/js/meshrf.js'; /** * Hook for Point-to-Point ITM Path Loss using Wasm + * @param {boolean} [enabled=true] - When false, the module is not loaded. Lets + * always-mounted panels defer the ~MB instantiation until ITM is actually + * selected; flipping it to true starts the load. * @returns {object} - { calculatePathLoss, isReady } */ -export const useWasmITM = () => { +export const useWasmITM = (enabled = true) => { const [isReady, setIsReady] = useState(false); const wasmModuleRef = useRef(null); // Initialize Wasm Module useEffect(() => { + if (!enabled || wasmModuleRef.current) return; + let mounted = true; const loadWasm = async () => { @@ -36,7 +41,7 @@ export const useWasmITM = () => { loadWasm(); return () => { mounted = false; }; - }, []); + }, [enabled]); /** * Calculate Point-to-Point ITM Path Loss diff --git a/src/utils/__tests__/csvParser.test.js b/src/utils/__tests__/csvParser.test.js new file mode 100644 index 0000000..0d156de --- /dev/null +++ b/src/utils/__tests__/csvParser.test.js @@ -0,0 +1,163 @@ +import { describe, it, expect } from "vitest"; +import { + parseBatchNodesCSV, + parseNodeConfigOverrides, + resolveDevicePreset, + resolveAntennaPreset, +} from "../csvParser"; +import { resolveNodeConfig } from "../nodeConfig"; +import { ANTENNA_PRESETS, DEVICE_PRESETS } from "../../data/presets"; + +describe("preset resolution (P3-4)", () => { + it("resolves devices by key, name and loose casing/spacing", () => { + expect(resolveDevicePreset("HELTEC_V3")).toBe("HELTEC_V3"); + expect(resolveDevicePreset("heltec_v3")).toBe("HELTEC_V3"); + expect(resolveDevicePreset("Heltec V3")).toBe("HELTEC_V3"); + expect(resolveDevicePreset("heltec-v3")).toBe("HELTEC_V3"); + }); + + it("resolves antennas by key and name", () => { + expect(resolveAntennaPreset("YAGI")).toBe("YAGI"); + expect(resolveAntennaPreset("Standard Dipole")).toBe("DIPOLE"); + }); + + it("returns undefined for unknown or empty values", () => { + expect(resolveDevicePreset("NOT_A_DEVICE")).toBeUndefined(); + expect(resolveDevicePreset("")).toBeUndefined(); + expect(resolveAntennaPreset(undefined)).toBeUndefined(); + }); +}); + +describe("parseNodeConfigOverrides (P3-4)", () => { + it("returns undefined when a row carries no overrides", () => { + expect(parseNodeConfigOverrides({ name: "A", lat: "1", lon: "2" })).toBeUndefined(); + }); + + it("reads each optional column", () => { + const config = parseNodeConfigOverrides({ + antenna_height: "30", + antenna_gain: "8", + tx_power: "22", + device: "RAK_4631", + }); + expect(config).toEqual({ + antennaHeight: 30, + antennaGain: 8, + txPower: 22, + device: "RAK_4631", + }); + }); + + it("accepts the height/gain/power aliases", () => { + expect(parseNodeConfigOverrides({ height: "12" }).antennaHeight).toBe(12); + expect(parseNodeConfigOverrides({ agl: "9" }).antennaHeight).toBe(9); + expect(parseNodeConfigOverrides({ gain: "5.8" }).antennaGain).toBe(5.8); + expect(parseNodeConfigOverrides({ power: "27" }).txPower).toBe(27); + }); + + it("derives gain from the antenna preset when no explicit gain is given", () => { + const config = parseNodeConfigOverrides({ antenna: "YAGI" }); + expect(config.antenna).toBe("YAGI"); + expect(config.antennaGain).toBe(ANTENNA_PRESETS.YAGI.gain); + }); + + it("lets an explicit gain win over the antenna preset", () => { + const config = parseNodeConfigOverrides({ antenna: "YAGI", antenna_gain: "14" }); + expect(config.antennaGain).toBe(14); + }); + + it("ignores blank and unparseable cells", () => { + expect(parseNodeConfigOverrides({ antenna_height: "", antenna_gain: " " })).toBeUndefined(); + expect(parseNodeConfigOverrides({ antenna_height: "tall" })).toBeUndefined(); + }); +}); + +describe("parseBatchNodesCSV (P3-4)", () => { + it("still parses a plain Name,Lat,Lon file with no config attached", () => { + const csv = "Name,Lat,Lon\nSite Alpha,45.5152,-122.6784\nSite Bravo,45.5252,-122.6684"; + const nodes = parseBatchNodesCSV(csv); + + expect(nodes).toHaveLength(2); + expect(nodes[0]).toMatchObject({ name: "Site Alpha", lat: 45.5152, lng: -122.6784 }); + expect(nodes[0].config).toBeUndefined(); + }); + + it("attaches per-node config only to rows that specify it", () => { + const csv = [ + "Name,Lat,Lon,Antenna_Height,Antenna_Gain,TX_Power,Device,Antenna", + "Site Alpha,45.5152,-122.6784,30,8,22,HELTEC_V3,OMNI_HIGH", + "Site Bravo,45.5252,-122.6684,,,,,", + "Site Charlie,45.5052,-122.6884,12,,,,DIPOLE", + ].join("\n"); + + const nodes = parseBatchNodesCSV(csv); + expect(nodes).toHaveLength(3); + + expect(nodes[0].config).toEqual({ + antennaHeight: 30, + antennaGain: 8, + txPower: 22, + device: "HELTEC_V3", + antenna: "OMNI_HIGH", + }); + + // No overrides -> inherits globals + expect(nodes[1].config).toBeUndefined(); + + // Partial: height plus antenna-derived gain + expect(nodes[2].config).toEqual({ + antennaHeight: 12, + antenna: "DIPOLE", + antennaGain: ANTENNA_PRESETS.DIPOLE.gain, + }); + }); + + it("skips rows with an unparseable lat/lon", () => { + const csv = "Name,Lat,Lon\nGood,45.5,-122.6\nBad,not-a-lat,-122.6"; + const nodes = parseBatchNodesCSV(csv); + expect(nodes).toHaveLength(1); + expect(nodes[0].name).toBe("Good"); + }); +}); + +describe("resolveNodeConfig (P3-4)", () => { + const globalConfig = { + antennaHeight: 10, + antennaGain: 2.15, + txPower: 20, + device: "HELTEC_V3", + }; + + it("falls back entirely to the global config when a node has no overrides", () => { + const resolved = resolveNodeConfig({ name: "n" }, globalConfig); + expect(resolved).toEqual({ + antennaHeight: 10, + antennaGain: 2.15, + txPower: 20, + device: "HELTEC_V3", + loss: DEVICE_PRESETS.HELTEC_V3.loss, + }); + }); + + it("applies overrides field-by-field, keeping globals for the rest", () => { + const node = { config: { antennaHeight: 45, device: "STATION_G2" } }; + const resolved = resolveNodeConfig(node, globalConfig); + + expect(resolved.antennaHeight).toBe(45); + expect(resolved.device).toBe("STATION_G2"); + expect(resolved.loss).toBe(DEVICE_PRESETS.STATION_G2.loss); + // Untouched fields still come from the global config + expect(resolved.antennaGain).toBe(2.15); + expect(resolved.txPower).toBe(20); + }); + + it("does not let a zero override fall through to the global value", () => { + const resolved = resolveNodeConfig({ config: { antennaGain: 0 } }, globalConfig); + expect(resolved.antennaGain).toBe(0); + }); + + it("yields zero cable loss for an unknown device", () => { + const resolved = resolveNodeConfig({ config: { device: "MYSTERY" } }, globalConfig); + expect(resolved.loss).toBe(0); + }); +}); diff --git a/src/utils/__tests__/hata.test.js b/src/utils/__tests__/hata.test.js new file mode 100644 index 0000000..cd4fc58 --- /dev/null +++ b/src/utils/__tests__/hata.test.js @@ -0,0 +1,209 @@ +import { describe, it, expect } from "vitest"; +import { + calculateHataLoss, + calculateCost231Loss, + calculateHataFamilyLoss, + getHataVariant, + getHataValidity, + calculateClientPathLoss, + isClientSideModel, + calculateFSPL, +} from "../rfMath"; + +describe("Okumura-Hata (P3-1)", () => { + // Reference case: f=900MHz, hb=50m, hm=1.5m, d=5km, urban small/medium. + // a(hm) = (1.1*log10(900) - 0.7)*1.5 - (1.56*log10(900) - 0.8) + // = (1.1*2.9542 - 0.7)*1.5 - (1.56*2.9542 - 0.8) = 3.8244 - 3.8086 = 0.0158 + // L = 69.55 + 26.16*2.9542 - 13.82*1.69897 - 0.0158 + // + (44.9 - 6.55*1.69897)*log10(5) + // = 69.55 + 77.2825 - 23.4798 - 0.0158 + 33.7717*0.69897 = 146.94 + it("matches the hand-computed urban_small reference", () => { + const loss = calculateHataLoss(5, 900, 50, 1.5, "urban_small"); + expect(loss).toBeCloseTo(146.94, 1); + }); + + it("applies the suburban correction as a reduction from urban", () => { + const urban = calculateHataLoss(5, 900, 50, 1.5, "urban_small"); + const suburban = calculateHataLoss(5, 900, 50, 1.5, "suburban"); + // Lsub = Lu - 2*(log10(900/28))^2 - 5.4 => reduction of ~14.5 dB + expect(suburban).toBeLessThan(urban); + expect(urban - suburban).toBeCloseTo(2 * Math.log10(900 / 28) ** 2 + 5.4, 3); + }); + + it("orders environments urban_small > suburban > rural", () => { + const urban = calculateHataLoss(5, 900, 50, 1.5, "urban_small"); + const suburban = calculateHataLoss(5, 900, 50, 1.5, "suburban"); + const rural = calculateHataLoss(5, 900, 50, 1.5, "rural"); + expect(urban).toBeGreaterThan(suburban); + expect(suburban).toBeGreaterThan(rural); + }); + + it("uses the large-city mobile correction above 400 MHz", () => { + const small = calculateHataLoss(5, 900, 50, 1.5, "urban_small"); + const large = calculateHataLoss(5, 900, 50, 1.5, "urban_large"); + // a(hm) differs, so the two must not collapse to the same value + expect(large).not.toBeCloseTo(small, 2); + }); + + it("increases monotonically with distance", () => { + const near = calculateHataLoss(1, 915, 50, 2, "suburban"); + const far = calculateHataLoss(15, 915, 50, 2, "suburban"); + expect(far).toBeGreaterThan(near); + }); + + it("clamps degenerate inputs instead of returning NaN/-Infinity", () => { + expect(Number.isFinite(calculateHataLoss(0, 915, 0, 0, "suburban"))).toBe(true); + expect(calculateHataLoss(0, 915, 0, 0, "suburban")).toBeGreaterThanOrEqual(0); + }); +}); + +describe("COST 231-Hata extension (P4-2)", () => { + // f=1800MHz, hb=50m, hm=1.5m, d=5km, medium city (C=0) + // a(hm) = (1.1*log10(1800) - 0.7)*1.5 - (1.56*log10(1800) - 0.8) + // = (1.1*3.25527 - 0.7)*1.5 - (1.56*3.25527 - 0.8) = 4.32120 - 4.27823 = 0.04297 + // L = 46.3 + 33.9*3.25527 - 13.82*1.69897 - 0.04297 + (44.9 - 6.55*1.69897)*0.69897 + // = 46.3 + 110.35375 - 23.47977 - 0.04297 + 33.77175*0.69897 = 156.74 + it("matches the hand-computed 1800 MHz reference", () => { + const loss = calculateCost231Loss(5, 1800, 50, 1.5, "urban_small"); + expect(loss).toBeCloseTo(156.74, 1); + }); + + it("adds exactly 3 dB for metropolitan centres", () => { + const medium = calculateCost231Loss(5, 1800, 50, 1.5, "urban_small"); + const metro = calculateCost231Loss(5, 1800, 50, 1.5, "urban_large"); + expect(metro - medium).toBeCloseTo(3.0, 6); + }); + + it("has no suburban/rural term of its own", () => { + const medium = calculateCost231Loss(5, 1800, 50, 1.5, "urban_small"); + expect(calculateCost231Loss(5, 1800, 50, 1.5, "suburban")).toBeCloseTo(medium, 6); + expect(calculateCost231Loss(5, 1800, 50, 1.5, "rural")).toBeCloseTo(medium, 6); + }); + + it("predicts higher loss at 1800 MHz than Hata does at 900 MHz", () => { + const hata900 = calculateHataLoss(5, 900, 50, 1.5, "urban_small"); + const cost1800 = calculateCost231Loss(5, 1800, 50, 1.5, "urban_small"); + expect(cost1800).toBeGreaterThan(hata900); + }); +}); + +describe("Hata family dispatch", () => { + it("selects Okumura-Hata below the 1500 MHz crossover", () => { + expect(getHataVariant(915)).toBe("hata"); + expect(calculateHataFamilyLoss(5, 915, 50, 1.5, "suburban")).toBeCloseTo( + calculateHataLoss(5, 915, 50, 1.5, "suburban"), + 6, + ); + }); + + it("selects COST 231 at and above the crossover", () => { + expect(getHataVariant(1500)).toBe("cost231"); + expect(getHataVariant(1800)).toBe("cost231"); + expect(calculateHataFamilyLoss(5, 1800, 50, 1.5, "urban_small")).toBeCloseTo( + calculateCost231Loss(5, 1800, 50, 1.5, "urban_small"), + 6, + ); + }); +}); + +describe("getHataValidity", () => { + it("reports no warnings inside the published envelope", () => { + const { warnings, variant } = getHataValidity({ + distanceKm: 5, + freqMHz: 915, + txHeightM: 50, + rxHeightM: 2, + environment: "suburban", + }); + expect(variant).toBe("hata"); + expect(warnings).toEqual([]); + }); + + it("flags distance, TX height and frequency excursions", () => { + const { warnings } = getHataValidity({ + distanceKm: 40, + freqMHz: 60, + txHeightM: 10, + rxHeightM: 2, + }); + expect(warnings.some((w) => w.includes("Dist"))).toBe(true); + expect(warnings.some((w) => w.includes("TX"))).toBe(true); + expect(warnings.some((w) => w.includes("Freq"))).toBe(true); + }); + + it("accepts 1800 MHz (COST 231 range) without a frequency warning", () => { + const { variant, warnings } = getHataValidity({ + distanceKm: 5, + freqMHz: 1800, + txHeightM: 50, + rxHeightM: 2, + environment: "urban_small", + }); + expect(variant).toBe("cost231"); + expect(warnings.some((w) => w.includes("Freq"))).toBe(false); + }); + + it("warns that COST 231 lacks a suburban term", () => { + const { warnings } = getHataValidity({ + distanceKm: 5, + freqMHz: 1800, + txHeightM: 50, + rxHeightM: 2, + environment: "suburban", + }); + expect(warnings.some((w) => w.includes("no suburban term"))).toBe(true); + }); +}); + +describe("client-side path loss dispatch (P3-1)", () => { + it("identifies which models need no backend", () => { + expect(isClientSideModel("fspl")).toBe(true); + expect(isClientSideModel("hata")).toBe(true); + expect(isClientSideModel("HATA")).toBe(true); + expect(isClientSideModel("bullington")).toBe(false); + expect(isClientSideModel("itm")).toBe(false); + expect(isClientSideModel("itm_wasm")).toBe(false); + expect(isClientSideModel(undefined)).toBe(false); + }); + + it("returns FSPL for the fspl model", () => { + const loss = calculateClientPathLoss({ + model: "fspl", + distanceKm: 10, + freqMHz: 915, + txHeightM: 10, + rxHeightM: 2, + }); + expect(loss).toBeCloseTo(calculateFSPL(10, 915), 6); + }); + + it("returns Hata for the hata model, honouring environment", () => { + const loss = calculateClientPathLoss({ + model: "hata", + distanceKm: 5, + freqMHz: 915, + txHeightM: 50, + rxHeightM: 1.5, + environment: "rural", + }); + expect(loss).toBeCloseTo(calculateHataLoss(5, 915, 50, 1.5, "rural"), 6); + }); + + it("returns null for models that require the backend or WASM", () => { + const base = { distanceKm: 5, freqMHz: 915, txHeightM: 50, rxHeightM: 2 }; + expect(calculateClientPathLoss({ ...base, model: "bullington" })).toBeNull(); + expect(calculateClientPathLoss({ ...base, model: "itm_wasm" })).toBeNull(); + }); + + it("returns null for a non-positive distance", () => { + expect( + calculateClientPathLoss({ + model: "fspl", + distanceKm: 0, + freqMHz: 915, + txHeightM: 10, + rxHeightM: 2, + }), + ).toBeNull(); + }); +}); diff --git a/src/utils/csvImportExport.js b/src/utils/csvImportExport.js index d4c4b90..3c6cc9f 100644 --- a/src/utils/csvImportExport.js +++ b/src/utils/csvImportExport.js @@ -39,6 +39,56 @@ export const parseNodeCSV = (csvText) => { return importedNodes; }; +/** + * Escape a value for CSV output: quote it when it contains a delimiter, + * quote or newline, doubling any embedded quotes. + * @param {*} value + * @returns {string} + */ +export const csvEscape = (value) => { + const str = value === undefined || value === null ? '' : String(value); + return /[",\n\r]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str; +}; + +/** + * Trigger a browser download for generated text content. + * @param {string} content + * @param {string} filename + * @param {string} [mimeType='text/csv;charset=utf-8'] + */ +export const downloadTextFile = (content, filename, mimeType = 'text/csv;charset=utf-8') => { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}; + +/** + * Trigger a download of the batch-nodes CSV template (ROADMAP P3-4). + * Name/Lat/Lon are required; the remaining columns are optional per-node + * overrides that fall back to the global A/B config when blank. + */ +export const downloadBatchNodesTemplate = () => { + const rows = [ + 'Name,Lat,Lon,Antenna_Height,Antenna_Gain,TX_Power,Device,Antenna', + // Fully specified site + 'Site Alpha,45.5152,-122.6784,30,8,22,HELTEC_V3,OMNI_HIGH', + // Height + antenna preset only (gain comes from the preset) + 'Site Bravo,45.5252,-122.6684,12,,,,DIPOLE', + // Height override only + 'Site Charlie,45.5052,-122.6884,6,,,,', + // No overrides at all -- inherits the global config + 'Site Delta,45.5100,-122.6500,,,,,', + 'Site Echo,45.5300,-122.6900,45,11,27,STATION_G2,YAGI', + ]; + downloadTextFile(rows.join('\n'), 'meshrf_template.csv'); +}; + /** * Trigger a download of the CSV template for nodes. */ diff --git a/src/utils/csvParser.js b/src/utils/csvParser.js index a642d7e..bb8d6a2 100644 --- a/src/utils/csvParser.js +++ b/src/utils/csvParser.js @@ -1,3 +1,4 @@ +import { DEVICE_PRESETS, ANTENNA_PRESETS } from "../data/presets"; /** * Parses a generic CSV content into an array of objects. @@ -32,11 +33,100 @@ export const parseCSV = (text) => { return results; }; +/** + * Normalize a free-text preset reference to a preset key. + * "Heltec V3" / "heltec-v3" / "HELTEC_V3" all resolve to "HELTEC_V3". + * @param {string} value + * @returns {string} + */ +const normalizePresetKey = (value) => + String(value || '').trim().toUpperCase().replace(/[\s-]+/g, '_'); + +/** + * Resolve a device column value against DEVICE_PRESETS, by key or display name. + * @param {string} value + * @returns {string|undefined} Preset id, or undefined if unrecognized + */ +export const resolveDevicePreset = (value) => { + if (!value) return undefined; + const key = normalizePresetKey(value); + if (DEVICE_PRESETS[key]) return key; + + const match = Object.values(DEVICE_PRESETS).find( + (preset) => normalizePresetKey(preset.name) === key + ); + return match ? match.id : undefined; +}; + +/** + * Resolve an antenna column value against ANTENNA_PRESETS, by key or display name. + * @param {string} value + * @returns {string|undefined} Preset id, or undefined if unrecognized + */ +export const resolveAntennaPreset = (value) => { + if (!value) return undefined; + const key = normalizePresetKey(value); + if (ANTENNA_PRESETS[key]) return key; + + const match = Object.values(ANTENNA_PRESETS).find( + (preset) => normalizePresetKey(preset.name) === key + ); + return match ? match.id : undefined; +}; + +/** Parse a numeric cell, returning undefined (not NaN) when absent or unparseable. */ +const parseOptionalFloat = (value) => { + if (value === undefined || value === null || String(value).trim() === '') return undefined; + const num = parseFloat(value); + return Number.isFinite(num) ? num : undefined; +}; + +/** + * Extract optional per-node hardware overrides from a parsed CSV row (ROADMAP P3-4). + * + * Every column is optional; anything absent is left undefined so the caller can + * fall back to the global A/B config. Recognized headers: + * antenna_height | height | agl -> antennaHeight (m) + * antenna_gain | gain -> antennaGain (dBi) + * tx_power | power -> txPower (dBm) + * device -> DEVICE_PRESETS id + * antenna | antenna_type -> ANTENNA_PRESETS id (supplies gain if not given) + * + * @param {Object} row - Row keyed by lowercased header + * @returns {Object|undefined} Override object, or undefined when the row carries none + */ +export const parseNodeConfigOverrides = (row) => { + const config = {}; + + const height = parseOptionalFloat(row.antenna_height ?? row.height ?? row.agl); + if (height !== undefined) config.antennaHeight = height; + + const txPower = parseOptionalFloat(row.tx_power ?? row.power); + if (txPower !== undefined) config.txPower = txPower; + + const device = resolveDevicePreset(row.device); + if (device) config.device = device; + + const antenna = resolveAntennaPreset(row.antenna ?? row.antenna_type); + if (antenna) config.antenna = antenna; + + // Explicit gain wins over the antenna preset's nominal gain. + const gain = parseOptionalFloat(row.antenna_gain ?? row.gain); + if (gain !== undefined) { + config.antennaGain = gain; + } else if (antenna && ANTENNA_PRESETS[antenna]) { + config.antennaGain = ANTENNA_PRESETS[antenna].gain; + } + + return Object.keys(config).length > 0 ? config : undefined; +}; + /** * Parses node data specifically from CSV rows. - * Attempts to intelligently find lat/lon/name fields. + * Attempts to intelligently find lat/lon/name fields, and picks up the optional + * per-node hardware columns described in parseNodeConfigOverrides (P3-4). * @param {string} text - CSV content - * @returns {Array} Array of node objects {id, name, lat, lng} + * @returns {Array} Array of node objects {id, name, lat, lng, config?} */ export const parseBatchNodesCSV = (text) => { const rows = parseCSV(text); @@ -75,7 +165,10 @@ export const parseBatchNodesCSV = (text) => { if (!name) name = `Node ${idx + 1}`; if (!isNaN(lat) && !isNaN(lng)) { - nodes.push({ id: idx, name, lat, lng }); + const node = { id: idx, name, lat, lng }; + const config = parseNodeConfigOverrides(row); + if (config) node.config = config; + nodes.push(node); } }); diff --git a/src/utils/math/hata.js b/src/utils/math/hata.js new file mode 100644 index 0000000..6b3a38d --- /dev/null +++ b/src/utils/math/hata.js @@ -0,0 +1,221 @@ +import { RF_CONSTANTS } from "../rfConstants"; + +/** + * Okumura-Hata and COST 231-Hata path loss, client-side (ROADMAP P3-1 / P4-2). + * + * These mirror `calculate_hata_loss` / `calculate_cost231_loss` in + * `rf-engine/rf_physics.py` term-for-term, including the input clamping, so a + * link analysed in the browser and the same link analysed server-side agree. + * Keeping them in JS lets the Hata models run with no backend -- which matters + * for the offline/PWA path, where `/api/calculate-link` is unreachable. + */ + +/** Environment keys accepted by the Hata family. */ +export const HATA_ENVIRONMENTS = ["urban_large", "urban_small", "suburban", "rural"]; + +/** + * Mobile (RX) antenna height correction a(hm). + * Small/medium-city form is the default; large cities use a frequency-split form. + * @param {number} freqMHz + * @param {number} rxHeightM - clamped RX height + * @param {string} environment + * @returns {number} correction in dB + */ +const mobileHeightCorrection = (freqMHz, rxHeightM, environment) => { + const logF = Math.log10(freqMHz); + + if (environment === "urban_large") { + return freqMHz >= 400 + ? 3.2 * Math.pow(Math.log10(11.75 * rxHeightM), 2) - 4.97 + : 8.29 * Math.pow(Math.log10(1.54 * rxHeightM), 2) - 1.1; + } + + // Urban small/medium (also used verbatim by COST 231) + return (1.1 * logF - 0.7) * rxHeightM - (1.56 * logF - 0.8); +}; + +/** Clamp inputs the way the reference implementation does, to avoid log(0)/negatives. */ +const clampInputs = (distanceKm, txHeightM, rxHeightM) => ({ + d: Math.max(RF_CONSTANTS.HATA.LIMITS.MIN_DIST_KM, distanceKm), + hb: Math.max(RF_CONSTANTS.HATA.LIMITS.MIN_HEIGHT, txHeightM), + hm: Math.max(RF_CONSTANTS.HATA.LIMITS.MIN_HEIGHT, rxHeightM), +}); + +/** + * Okumura-Hata path loss. Valid for 150-1500 MHz, 1-20 km. + * @param {number} distanceKm + * @param {number} freqMHz + * @param {number} txHeightM - base station height AGL + * @param {number} rxHeightM - mobile height AGL + * @param {string} [environment='urban_small'] - one of HATA_ENVIRONMENTS + * @returns {number} Path loss in dB (never negative) + */ +export const calculateHataLoss = ( + distanceKm, + freqMHz, + txHeightM, + rxHeightM, + environment = "urban_small", +) => { + const C = RF_CONSTANTS.HATA; + const { d, hb, hm } = clampInputs(distanceKm, txHeightM, rxHeightM); + + const logF = Math.log10(freqMHz); + const logHb = Math.log10(hb); + const logD = Math.log10(d); + + const aHm = mobileHeightCorrection(freqMHz, hm, environment); + + // Lu = 69.55 + 26.16*log(f) - 13.82*log(hb) - a(hm) + (44.9 - 6.55*log(hb))*log(d) + let loss = + C.URBAN_BASE + + C.FREQ_SCALE * logF - + C.HB_SCALE * logHb - + aHm + + (C.DISTANCE_BASE - C.DISTANCE_HB_SCALE * logHb) * logD; + + if (environment === "suburban") { + // Lsub = Lu - 2*(log(f/28))^2 - 5.4 + const val = Math.log10(freqMHz / 28); + loss = loss - 2 * val * val - 5.4; + } else if (environment === "rural") { + // Lrural = Lu - 4.78*(log(f))^2 + 18.33*log(f) - 40.94 + loss = loss - 4.78 * logF * logF + 18.33 * logF - 40.94; + } + + return Math.max(0, loss); +}; + +/** + * COST 231-Hata path loss. Valid for 1500-2000 MHz, 1-20 km (ROADMAP P4-2). + * + * COST 231 defines only a metropolitan correction factor C (3 dB) on top of the + * medium-city/suburban baseline -- it has no separate suburban or rural + * correction term, so those environments evaluate identically to `urban_small`. + * `getHataValidity` surfaces that to the user rather than silently reusing the + * Okumura-Hata corrections, which are not defined at these frequencies. + * + * @param {number} distanceKm + * @param {number} freqMHz + * @param {number} txHeightM + * @param {number} rxHeightM + * @param {string} [environment='urban_small'] + * @returns {number} Path loss in dB (never negative) + */ +export const calculateCost231Loss = ( + distanceKm, + freqMHz, + txHeightM, + rxHeightM, + environment = "urban_small", +) => { + const C = RF_CONSTANTS.COST231; + const { d, hb, hm } = clampInputs(distanceKm, txHeightM, rxHeightM); + + const logF = Math.log10(freqMHz); + const logHb = Math.log10(hb); + const logD = Math.log10(d); + + // COST 231 always uses the small/medium-city a(hm) form. + const aHm = mobileHeightCorrection(freqMHz, hm, "urban_small"); + const metro = environment === "urban_large" ? C.METRO_CORRECTION_DB : 0; + + // L = 46.3 + 33.9*log(f) - 13.82*log(hb) - a(hm) + (44.9 - 6.55*log(hb))*log(d) + C + const loss = + C.URBAN_BASE + + C.FREQ_SCALE * logF - + C.HB_SCALE * logHb - + aHm + + (C.DISTANCE_BASE - C.DISTANCE_HB_SCALE * logHb) * logD + + metro; + + return Math.max(0, loss); +}; + +/** + * Which Hata variant applies at a given frequency. + * @param {number} freqMHz + * @returns {'hata'|'cost231'} + */ +export const getHataVariant = (freqMHz) => + freqMHz >= RF_CONSTANTS.HATA_COST231_CROSSOVER_MHZ ? "cost231" : "hata"; + +/** + * Hata-family dispatcher: picks Okumura-Hata below 1500 MHz and COST 231 at or + * above it, so a single "Hata" model selection covers 150-2000 MHz. + * @param {number} distanceKm + * @param {number} freqMHz + * @param {number} txHeightM + * @param {number} rxHeightM + * @param {string} [environment='urban_small'] + * @returns {number} Path loss in dB + */ +export const calculateHataFamilyLoss = ( + distanceKm, + freqMHz, + txHeightM, + rxHeightM, + environment = "urban_small", +) => + getHataVariant(freqMHz) === "cost231" + ? calculateCost231Loss(distanceKm, freqMHz, txHeightM, rxHeightM, environment) + : calculateHataLoss(distanceKm, freqMHz, txHeightM, rxHeightM, environment); + +/** + * Validity check for the Hata family, so the UI has one source of truth for + * "you are extrapolating" warnings instead of inline magic numbers. + * @param {object} params + * @param {number} params.distanceKm + * @param {number} params.freqMHz + * @param {number} params.txHeightM + * @param {number} [params.rxHeightM] + * @param {string} [params.environment] + * @returns {{variant: string, label: string, warnings: string[]}} + */ +export const getHataValidity = ({ + distanceKm, + freqMHz, + txHeightM, + rxHeightM, + environment = "urban_small", +}) => { + const variant = getHataVariant(freqMHz); + const limits = variant === "cost231" ? RF_CONSTANTS.COST231.VALID : RF_CONSTANTS.HATA.VALID; + const label = variant === "cost231" ? "COST 231" : "Okumura-Hata"; + const warnings = []; + + // Frequency outside the whole family's envelope (150-2000 MHz). + const [freqMin] = RF_CONSTANTS.HATA.VALID.FREQ_MHZ; + const [, freqMax] = RF_CONSTANTS.COST231.VALID.FREQ_MHZ; + if (freqMHz < freqMin || freqMHz > freqMax) { + warnings.push(`Freq ${freqMHz}MHz (Limit ${freqMin}-${freqMax})`); + } + + if (Number.isFinite(distanceKm)) { + const [dMin, dMax] = limits.DIST_KM; + if (distanceKm < dMin || distanceKm > dMax) { + warnings.push(`Dist ${distanceKm.toFixed(1)}km (Limit ${dMin}-${dMax}km)`); + } + } + + if (Number.isFinite(txHeightM)) { + const [hMin] = limits.TX_HEIGHT_M; + if (txHeightM < hMin) { + warnings.push(`TX ${txHeightM}m < ${hMin}m (${label} Min)`); + } + } + + if (Number.isFinite(rxHeightM)) { + const [, rMax] = limits.RX_HEIGHT_M; + if (rxHeightM > rMax) { + warnings.push(`RX ${rxHeightM}m > ${rMax}m (${label} Max)`); + } + } + + // COST 231 has no suburban/rural correction terms of its own. + if (variant === "cost231" && (environment === "suburban" || environment === "rural")) { + warnings.push(`COST 231 has no ${environment} term -- using medium-city baseline`); + } + + return { variant, label, warnings }; +}; diff --git a/src/utils/math/pathLoss.js b/src/utils/math/pathLoss.js new file mode 100644 index 0000000..06f9f22 --- /dev/null +++ b/src/utils/math/pathLoss.js @@ -0,0 +1,59 @@ +import { calculateFSPL } from "./fspl"; +import { calculateHataFamilyLoss } from "./hata"; + +/** + * Client-side path loss dispatch (ROADMAP P3-1). + * + * Mirrors the `model` switch in `rf-engine/rf_physics.py::calculate_path_loss` + * for the models that need no terrain profile, so the Link tool can resolve + * them without a `/api/calculate-link` round trip. + * + * `bullington` and `itm` stay server-side on purpose: the backend applies + * `clutter_height` and `k_factor` to the terrain profile, and the current JS + * `calculateBullingtonDiffraction` does not, so moving them would silently + * change results. `itm_wasm` is handled separately by the WASM ITM path. + */ + +/** Models resolvable entirely in the browser. */ +export const CLIENT_SIDE_MODELS = ["fspl", "hata", "cost231"]; + +/** + * @param {string} model + * @returns {boolean} true when `calculateClientPathLoss` can resolve this model + */ +export const isClientSideModel = (model) => + CLIENT_SIDE_MODELS.includes(String(model || "").toLowerCase()); + +/** + * Resolve path loss for a client-side model. + * @param {object} params + * @param {string} params.model - 'fspl' | 'hata' | 'cost231' + * @param {number} params.distanceKm + * @param {number} params.freqMHz + * @param {number} params.txHeightM + * @param {number} params.rxHeightM + * @param {string} [params.environment='suburban'] + * @returns {number|null} Path loss in dB, or null if the model is not client-side + */ +export const calculateClientPathLoss = ({ + model, + distanceKm, + freqMHz, + txHeightM, + rxHeightM, + environment = "suburban", +}) => { + const normalized = String(model || "").toLowerCase(); + + if (!Number.isFinite(distanceKm) || distanceKm <= 0) return null; + + if (normalized === "fspl") { + return calculateFSPL(distanceKm, freqMHz); + } + + if (normalized === "hata" || normalized === "cost231") { + return calculateHataFamilyLoss(distanceKm, freqMHz, txHeightM, rxHeightM, environment); + } + + return null; +}; diff --git a/src/utils/nodeConfig.js b/src/utils/nodeConfig.js new file mode 100644 index 0000000..8a2d6ca --- /dev/null +++ b/src/utils/nodeConfig.js @@ -0,0 +1,27 @@ +import { DEVICE_PRESETS } from '../data/presets'; + +/** + * Merge a node's optional per-node overrides over a global A/B hardware config + * (ROADMAP P3-4). + * + * Overrides come from the optional CSV columns parsed by + * `parseNodeConfigOverrides`; any field the node does not specify falls back to + * the global config, so an import with no extra columns behaves exactly as + * before. + * + * @param {Object} node - Batch node, may carry a `config` override object + * @param {Object} globalConfig - nodeConfigs.A or nodeConfigs.B + * @returns {{antennaHeight: number, antennaGain: number, txPower: number, device: string, loss: number}} + */ +export const resolveNodeConfig = (node, globalConfig) => { + const override = (node && node.config) || {}; + const device = override.device ?? globalConfig.device; + + return { + antennaHeight: parseFloat(override.antennaHeight ?? globalConfig.antennaHeight), + antennaGain: parseFloat(override.antennaGain ?? globalConfig.antennaGain), + txPower: parseFloat(override.txPower ?? globalConfig.txPower), + device, + loss: DEVICE_PRESETS[device]?.loss || 0, + }; +}; diff --git a/src/utils/rfConstants.js b/src/utils/rfConstants.js index b1daa69..996c3ef 100644 --- a/src/utils/rfConstants.js +++ b/src/utils/rfConstants.js @@ -23,13 +23,44 @@ export const RF_CONSTANTS = { HB_SCALE: 13.82, DISTANCE_BASE: 44.9, DISTANCE_HB_SCALE: 6.55, - + LIMITS: { MIN_DIST_KM: 0.1, MIN_HEIGHT: 1.0, - } + }, + + // Published validity envelope (Okumura-Hata, 150-1500 MHz). + // Outside these the formula still evaluates but is extrapolation. + VALID: { + FREQ_MHZ: [150, 1500], + DIST_KM: [1, 20], + TX_HEIGHT_M: [30, 200], + RX_HEIGHT_M: [1, 10], + }, }, + // COST 231-Hata extension (1500-2000 MHz). Same structure as Hata with a + // different base/frequency slope plus a metropolitan correction factor C. + COST231: { + URBAN_BASE: 46.3, + FREQ_SCALE: 33.9, + HB_SCALE: 13.82, + DISTANCE_BASE: 44.9, + DISTANCE_HB_SCALE: 6.55, + // C: 3 dB for metropolitan centres, 0 dB for medium cities / suburban. + METRO_CORRECTION_DB: 3.0, + + VALID: { + FREQ_MHZ: [1500, 2000], + DIST_KM: [1, 20], + TX_HEIGHT_M: [30, 200], + RX_HEIGHT_M: [1, 10], + }, + }, + + // Frequency at or above which the Hata family switches to COST 231. + HATA_COST231_CROSSOVER_MHZ: 1500, + // LoRa / Hardware Constants (Semtech SX1262 datasheet, power-saving RX mode) LORA: { // Per-SF sensitivity at 125kHz BW (dBm) - SX1262 datasheet Table 3-2 diff --git a/src/utils/rfMath.js b/src/utils/rfMath.js index b31d7b6..626d0ba 100644 --- a/src/utils/rfMath.js +++ b/src/utils/rfMath.js @@ -8,3 +8,5 @@ export * from './math/linkBudget'; export * from './math/earth'; export * from './math/profile'; export * from './math/bullington'; +export * from './math/hata'; +export * from './math/pathLoss'; From 561126dcbf5ad746b1c640edd877d8b83b3c33dd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 21:09:06 +0000 Subject: [PATCH 2/2] feat(itm): expose statistical variability modes (P4-6) ITM's time/location/situation variability was hardcoded to 50/50/50 in meshrf_itm.cpp, so every prediction was the median forecast with no way to plan for worst-case conditions. It is now user-selectable. C++ / WASM: - Add time_pct, loc_pct, sit_pct and mdvar to LinkParameters, defaulted to the values calculate_radial_loss previously hardcoded (50/50/50, mdvar=12). - Expose them through Embind. - Thread variability into the coverage path; calculate_rf_coverage gained three trailing arguments, so useRFCoverageTool now passes 20 (Embind matches on arity). - Regenerate public/meshrf.wasm, libmeshrf/js/meshrf.wasm and libmeshrf/js/meshrf.js. ROADMAP P4-6 documents the rebuild command. Frontend: - RELIABILITY_MODES presets in EnvironmentContext: Best Case (10/10/10), Typical (50/50/50, default), Reliable (90/90/90). - Reliability selector in the Environment sidebar. - Wired through Link Analysis, RF Coverage (click, recalc and drag paths) and batch ITM reports; batch rows record the mode that produced them. Backward compatibility is the important property here, and it is verified against the built module rather than assumed: a call that never sets the new fields returns a bit-identical result to an explicit 50/50/50. Struct defaults, all four Embind bindings, the 20-arg coverage call and the untouched viewshed binding were each exercised against the installed artifacts. Measured on a synthetic ridge (915 MHz, 10 m TX / 2 m RX): 10/10/10 = 193.49 dB, 50/50/50 = 203.76 dB, 90/90/90 = 213.66 dB. Note the WASM was rebuilt with emcc 6.0.4, which also regenerates the JS glue every WASM hook imports. Unit tests cannot cover a glue regression, so link analysis, coverage and viewshed want a manual pass before this ships. Tests: 52 frontend (8 new), 23 backend. Lint and build clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FU7LVUXdVx5YfXt4eJJSPs --- CHANGELOG.md | 3 + Documentation/batch-processing.md | 11 ++- README.md | 1 + ROADMAP.md | 30 +++++-- libmeshrf/include/meshrf_coverage.h | 11 ++- libmeshrf/include/meshrf_itm.h | 15 +++- libmeshrf/js/meshrf.js | 4 +- libmeshrf/js/meshrf.wasm | Bin 111380 -> 112262 bytes libmeshrf/src/bindings.cpp | 14 ++- libmeshrf/src/meshrf_coverage.cpp | 9 +- libmeshrf/src/meshrf_itm.cpp | 23 +++-- public/meshrf.wasm | Bin 111380 -> 112262 bytes .../Layout/sections/EnvironmentSection.jsx | 29 ++++++- src/components/Map/BatchProcessing.jsx | 10 ++- .../Map/Controls/CoverageClickHandler.jsx | 7 +- src/components/Map/LinkLayer.jsx | 9 +- .../Map/hooks/useMapEventHandlers.js | 6 +- .../Map/layers/CoverageLayerManager.jsx | 7 +- src/context/EnvironmentContext.jsx | 66 ++++++++++++++- src/context/RFContext.jsx | 4 +- src/hooks/useRFCoverageTool.js | 7 +- src/hooks/useWasmITM.js | 21 ++++- src/utils/__tests__/variability.test.js | 80 ++++++++++++++++++ 23 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 src/utils/__tests__/variability.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3caa7..db92f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **WASM ITM for Batch Reports (P3-3)**: Batch Processing gained a propagation-model selector. Choosing Longley-Rice ITM runs the same WASM engine as Link Analysis over a 100-point terrain profile (vs 20 for Bullington), honouring Ground Type and Climate Zone, so batch numbers agree with single-link analysis. The engine loads lazily on selection, and a link that fails ITM falls back to Bullington instead of failing the report. - **Per-Node Configs in Batch CSV (P3-4)**: Batch CSV import now accepts optional `Antenna_Height`, `Antenna_Gain`, `TX_Power`, `Device` and `Antenna` columns (with aliases such as `Height`/`AGL`/`Gain`/`Power`, and loose preset-name matching so `HELTEC_V3`, `heltec-v3` and `Heltec V3` all resolve). Values are merged over the global A/B config field-by-field; blank cells and existing three-column files behave exactly as before. The sidebar reports how many loaded nodes carry an override. - Mesh report CSV now includes `Model`, `PathLoss_dB`, and the per-node TX/RX heights, gains and TX power, plus a progress bar while the report runs. +- **Probabilistic / Variability Modes (P4-6)**: ITM's time/location/situation variability was hardcoded to 50/50/50; it is now user-selectable via a **Reliability** control in the Environment sidebar -- Best Case (10/10/10), Typical (50/50/50, default) and Reliable (90/90/90). The mode applies to Link Analysis, RF Coverage and batch ITM reports, and batch rows record which mode produced them. On a synthetic ridge profile at 915 MHz this spans roughly 20 dB (193.5 / 203.8 / 213.7 dB). + - Required C++ changes: `time_pct`, `loc_pct`, `sit_pct` and `mdvar` added to `LinkParameters`, exposed through Embind, and threaded into `calculate_rf_coverage` (which gained three trailing arguments). All default to the previously hardcoded values, so callers that ignore them are unchanged -- a call that never sets the new fields returns a bit-identical result to an explicit 50/50/50. + - `public/meshrf.wasm`, `libmeshrf/js/meshrf.wasm` and `libmeshrf/js/meshrf.js` are regenerated. See ROADMAP.md P4-6 for the rebuild command. ### Changed diff --git a/Documentation/batch-processing.md b/Documentation/batch-processing.md index 540a3f6..3440d4d 100644 --- a/Documentation/batch-processing.md +++ b/Documentation/batch-processing.md @@ -72,8 +72,15 @@ the export button: well suited to sweeping a large candidate list quickly. - **Longley-Rice ITM (Accurate, Slower)** — The same WASM ITM engine the Link Analysis tool uses, run over a denser 100-point profile and using your - **Ground Type** and **Climate Zone** settings. Batch numbers then agree with - what you see when you open the same link in the Link Analyzer. + **Ground Type**, **Climate Zone** and **Reliability** settings. Batch numbers + then agree with what you see when you open the same link in the Link Analyzer. + +> [!TIP] +> The **Reliability** setting in the Environment sidebar applies here too. Running +> a mesh report at *Reliable (90%)* tells you which links survive poor conditions, +> not just which ones work on a median day. The `Model` column records the mode +> used for each row (e.g. `ITM 90/90/90`), so reports run at different confidence +> levels stay distinguishable after export. The ITM engine is loaded on demand the first time you select it; the export button stays disabled for the moment it takes to initialize. If ITM fails on an diff --git a/README.md b/README.md index 7102039..e1ff440 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ meshRF is designed for **mission-critical availability**. It operates with **zer - **RF Coverage Simulator**: Optimized Wasm-powered ITM propagation modeling for wide-area coverage visualization. - **Viewshed Analysis**: Desktop-grade viewshed calculations with "Shadow Mode" visualization. - **Environment Tuning**: Fine-tune simulations with **Ground Type** ($\epsilon$, $\sigma$) and **Climate Zone** parameters for regional accuracy. Supports Sea Water, City/Industrial, Farmland, and more. +- **Reliability Modes**: Select the ITM statistical confidence level — Best Case (10%), Typical (50%, default) or Reliable (90%) — to plan against median or worst-case conditions instead of a single fixed forecast. ### 3. ⚡ Batch Operations & reporting diff --git a/ROADMAP.md b/ROADMAP.md index 7d7862d..6921994 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -79,15 +79,9 @@ Current tools analyze point-to-point links only. A mesh planner would: Could build on the batch processing infrastructure with graph analysis (Dijkstra/Floyd-Warshall for optimal paths). -### P4-6: Probabilistic / Variability Modes +### P4-6: Statistical Coverage Contours (Remaining) -The ITM supports time/location/situation variability percentages (currently fixed at 50/50/50). Exposing these as user controls would enable: - -- Worst-case planning (90/90/90 for reliability) -- Best-case estimation (10/10/10 for maximum range) -- Statistical coverage contours showing probability of reception - -**Blocked on a native rebuild.** `time_pct` is hardcoded to 50.0 in `libmeshrf/src/meshrf_itm.cpp`, and `LinkParameters` (`libmeshrf/include/meshrf_itm.h`, bound in `src/bindings.cpp`) exposes no variability fields. This needs three new struct members, updated Embind bindings, and a fresh emscripten build of `public/meshrf.wasm` -- it cannot ship as a frontend-only change. +Variability percentages are now user-selectable (see Recently Completed below). The remaining piece is *visualizing* the spread rather than picking a single operating point: rendering probability-of-reception contours by running the coverage grid at several percentages and shading the delta between them. --- @@ -139,6 +133,26 @@ The ITM supports time/location/situation variability percentages (currently fixe ## Recently Completed +### P4-6: Probabilistic / Variability Modes (Completed) + +**Status:** ✅ Implemented 2026-07-27. +**Problem:** ITM's time/location/situation variability was hardcoded to 50/50/50 in `libmeshrf/src/meshrf_itm.cpp`, so every prediction was the median forecast with no way to plan for worst case. +**Solution:** Added `time_pct`, `loc_pct`, `sit_pct` and `mdvar` to `LinkParameters` (`libmeshrf/include/meshrf_itm.h`), exposed them through Embind (`src/bindings.cpp`), and threaded them into the coverage path (`calculate_rf_coverage` gained three trailing arguments). A **Reliability** selector in the Environment sidebar offers Best Case (10/10/10), Typical (50/50/50, default) and Reliable (90/90/90); the mode flows into Link Analysis, RF Coverage and batch ITM reports, and batch rows record which mode produced them. + +All new fields are defaulted to the previous hardcoded values, so any caller that ignores them is unchanged -- verified against the built module: a call that never sets the new fields returns a bit-identical result to an explicit 50/50/50. + +**Requires a WASM rebuild.** `public/meshrf.wasm`, `libmeshrf/js/meshrf.wasm` and `libmeshrf/js/meshrf.js` are regenerated artifacts and must be rebuilt whenever `libmeshrf/` C++ changes: + +```sh +docker run --rm -v "$PWD/libmeshrf":/app -w /app emscripten/emsdk:latest \ + bash -c "mkdir -p build_wasm && cd build_wasm && emcmake cmake .. -DEMSCRIPTEN=1 && emmake make" +cp libmeshrf/build_wasm/meshrf.js libmeshrf/js/meshrf.js +cp libmeshrf/build_wasm/meshrf.wasm libmeshrf/js/meshrf.wasm +cp libmeshrf/build_wasm/meshrf.wasm public/meshrf.wasm +``` + +Measured effect on a synthetic ridge profile (915 MHz, 10 m TX / 2 m RX): 10/10/10 = 193.49 dB, 50/50/50 = 203.76 dB, 90/90/90 = 213.66 dB. + ### P4-2: COST 231-Hata Extension (Completed) **Status:** ✅ Implemented 2026-07-27. diff --git a/libmeshrf/include/meshrf_coverage.h b/libmeshrf/include/meshrf_coverage.h index 9c650c4..fa90a79 100644 --- a/libmeshrf/include/meshrf_coverage.h +++ b/libmeshrf/include/meshrf_coverage.h @@ -19,6 +19,12 @@ * @param rx_sensitivity Receiver sensitivity in dBm * @param max_dist_pixels Maximum distance to calculate (pixels) * @param gsd_meters Ground Sample Distance (meters per pixel) + * @param epsilon Ground permittivity + * @param sigma Ground conductivity + * @param climate ITM climate zone + * @param time_pct % of time the predicted loss is not exceeded (ROADMAP P4-6) + * @param loc_pct % of locations the predicted loss is not exceeded + * @param sit_pct % of situations the predicted loss is not exceeded * @return Vector of float values representing received signal strength (dBm) * at each pixel. Values below rx_sensitivity indicate no coverage. */ @@ -39,7 +45,10 @@ std::vector calculate_rf_coverage( float gsd_meters, float epsilon = 15.0f, float sigma = 0.005f, - int climate = 5 + int climate = 5, + float time_pct = 50.0f, + float loc_pct = 50.0f, + float sit_pct = 50.0f ); #endif // MESHRF_COVERAGE_H diff --git a/libmeshrf/include/meshrf_itm.h b/libmeshrf/include/meshrf_itm.h index c933040..983a234 100644 --- a/libmeshrf/include/meshrf_itm.h +++ b/libmeshrf/include/meshrf_itm.h @@ -10,12 +10,25 @@ struct LinkParameters { double rx_height_m; int polarization; // 0=horiz, 1=vert double step_size_m; // Added: Required to calculate distance from profile index - + // ITM Defaults for Environment double N_0 = 301.0; double epsilon = 15.0; double sigma = 0.005; int climate = 5; // Continental Temperate + + // Statistical variability (ROADMAP P4-6). + // ITM predicts the loss NOT exceeded for the given percentage of time, + // locations and situations. 50/50/50 is the median prediction and remains + // the default, so callers that don't set these behave exactly as before. + // Higher percentages => more conservative (higher) predicted loss. + double time_pct = 50.0; + double loc_pct = 50.0; + double sit_pct = 50.0; + + // Mode of variability. 12 = broadcast mode with location variability, + // matching the value previously hardcoded in calculate_radial_loss. + int mdvar = 12; }; /** diff --git a/libmeshrf/js/meshrf.js b/libmeshrf/js/meshrf.js index 2df3185..ab4ecea 100644 --- a/libmeshrf/js/meshrf.js +++ b/libmeshrf/js/meshrf.js @@ -1,2 +1,2 @@ -async function createMeshRF(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["y"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("meshrf.wasm")}return new URL("meshrf.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};var heap32VectorToArray=(count,firstElement)=>{var array=[];for(var i=0;i>2])}return array};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function usesDestructorStack(argTypes){for(var i=1;i{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var captures={toValue:Emval.toValue};var args=argFromPtr.map((argFromPtr,i)=>{var captureName=`argFromPtr${i}`;captures[captureName]=argFromPtr;return`${captureName}(args${i?"+"+i*GenericWireTypeSize:""})`});var functionBody;switch(kind){case 0:functionBody="toValue(handle)";break;case 2:functionBody="new (toValue(handle))";break;case 3:functionBody="";break;case 1:captures["getStringOrSymbol"]=getStringOrSymbol;functionBody="toValue(handle)[getStringOrSymbol(methodName)]";break}functionBody+=`(${args})`;if(!retType.isVoid){captures["toReturnWire"]=toReturnWire;captures["emval_returnValue"]=emval_returnValue;functionBody=`return emval_returnValue(toReturnWire, destructorsRef, ${functionBody})`}functionBody=`return function (handle, methodName, destructorsRef, args) {\n${functionBody}\n}`;var invokerFunction=new Function(Object.keys(captures),functionBody)(...Object.values(captures));var functionName=`methodCaller<(${argTypes.map(t=>t.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var ___getTypeName,_malloc,_free,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["z"];_malloc=Module["_malloc"]=wasmExports["B"];_free=Module["_free"]=wasmExports["C"];memory=wasmMemory=wasmExports["x"];__indirect_function_table=wasmTable=wasmExports["A"]}var wasmImports={o:___cxa_throw,t:__abort_js,n:__embind_register_bigint,p:__embind_register_bool,f:__embind_register_class,e:__embind_register_class_constructor,b:__embind_register_class_function,d:__embind_register_class_property,v:__embind_register_emval,m:__embind_register_float,j:__embind_register_function,c:__embind_register_integer,k:__embind_register_iterable,a:__embind_register_memory_view,i:__embind_register_optional,w:__embind_register_std_string,l:__embind_register_std_wstring,q:__embind_register_void,h:__emval_create_invoker,s:__emval_decref,g:__emval_invoke,r:__emval_run_destructors,u:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} -;return moduleRtn}export default createMeshRF; +async function createMeshRF(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["y"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("meshrf.wasm")}return new URL("meshrf.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var noExitRuntime=true;var HEAPU32;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var HEAPU8;var AsciiToString=ptr=>{var str="";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var awaitingDependencies={};var registeredTypes={};var typeDependencies={};class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}}var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var HEAP16;var HEAPU16;var HEAP32;var HEAP64;var HEAPU64;var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];case 8:return signed?pointer=>HEAP64[pointer>>3]:pointer=>HEAPU64[pointer>>3];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupported sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}}var throwInternalError=message=>{throw new InternalError(message)};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){var rtn=getWasmTableEntry(rawFunction);return rtn}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_register_class=(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor)=>{name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})};var heap32VectorToArray=(count,firstElement)=>{var array=[];for(var i=0;i>2])}return array};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function usesDestructorStack(argTypes){for(var i=1;i{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);invoker=embind__requireFunction(invokerSignature,invoker);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`constructor ${classType.name}`;if(undefined===classType.registeredClass.constructor_body){classType.registeredClass.constructor_body=[]}if(undefined!==classType.registeredClass.constructor_body[argCount-1]){throw new BindingError(`Cannot register multiple constructors with identical number of parameters (${argCount-1}) for class '${classType.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`)}classType.registeredClass.constructor_body[argCount-1]=()=>{throwUnboundTypeError(`Cannot construct ${classType.name} due to unbound types`,rawArgTypes)};whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{argTypes.splice(1,0,null);classType.registeredClass.constructor_body[argCount-1]=craftInvokerFunction(humanName,argTypes,null,invoker,rawConstructor);return[]});return[]})};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_function=(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn)=>{var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext)=>{fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){var value=emval_handles[handle];emval_handles[handle]=undefined;emval_freelist.push(handle)}};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var HEAPF32;var HEAPF64;var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var __embind_register_function=(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn)=>{var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var installIndexedIterator=(proto,sizeMethodName,getMethodName)=>{const makeIterator=(size,getValue)=>{let index=0;return{next(){if(index>=size){return{done:true}}const current=index;index++;const value=getValue(current);return{value,done:false}},[Symbol.iterator](){return this}}};if(!proto[Symbol.iterator]){proto[Symbol.iterator]=function(){const size=this[sizeMethodName]();return makeIterator(size,i=>this[getMethodName](i))}}};var __embind_register_iterable=(rawClassType,rawElementType,sizeMethodName,getMethodName)=>{sizeMethodName=AsciiToString(sizeMethodName);getMethodName=AsciiToString(getMethodName);whenDependentTypesAreResolved([],[rawClassType,rawElementType],types=>{const classType=types[0];installIndexedIterator(classType.registeredClass.instancePrototype,sizeMethodName,getMethodName);return[]})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var EmValOptionalType=Object.assign({optional:true},EmValType);var __embind_register_optional=(rawOptionalType,rawType)=>{registerType(rawOptionalType,EmValOptionalType)};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx));var str="";for(var i=idx;i{if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite=2147483647)=>{if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>2],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var captures={toValue:Emval.toValue};var args=argFromPtr.map((argFromPtr,i)=>{var captureName=`argFromPtr${i}`;captures[captureName]=argFromPtr;return`${captureName}(args${i?"+"+i*GenericWireTypeSize:""})`});var functionBody;switch(kind){case 0:functionBody="toValue(handle)";break;case 2:functionBody="new (toValue(handle))";break;case 3:functionBody="";break;case 1:captures["getStringOrSymbol"]=getStringOrSymbol;functionBody="toValue(handle)[getStringOrSymbol(methodName)]";break}functionBody+=`(${args})`;if(!retType.isVoid){captures["toReturnWire"]=toReturnWire;captures["emval_returnValue"]=emval_returnValue;functionBody=`return emval_returnValue(toReturnWire, destructorsRef, ${functionBody})`}functionBody=`return function (handle, methodName, destructorsRef, args) {\n${functionBody}\n}`;var invokerFunction=new Function(Object.keys(captures),functionBody)(...Object.values(captures));var functionName=`methodCaller<(${argTypes.map(t=>t.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}var ___getTypeName,_malloc,_free,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){___getTypeName=wasmExports["z"];_malloc=Module["_malloc"]=wasmExports["B"];_free=Module["_free"]=wasmExports["C"];memory=wasmMemory=wasmExports["x"];__indirect_function_table=wasmTable=wasmExports["A"]}var wasmImports={o:___cxa_throw,t:__abort_js,n:__embind_register_bigint,p:__embind_register_bool,f:__embind_register_class,e:__embind_register_class_constructor,b:__embind_register_class_function,c:__embind_register_class_property,v:__embind_register_emval,m:__embind_register_float,j:__embind_register_function,d:__embind_register_integer,k:__embind_register_iterable,a:__embind_register_memory_view,i:__embind_register_optional,w:__embind_register_std_string,l:__embind_register_std_wstring,q:__embind_register_void,h:__emval_create_invoker,s:__emval_decref,g:__emval_invoke,r:__emval_run_destructors,u:_emscripten_resize_heap};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +;return Module}export default createMeshRF; diff --git a/libmeshrf/js/meshrf.wasm b/libmeshrf/js/meshrf.wasm index 5f41fe898b84a636b76a29fb2242a5c463e7fdae..96c848e5f9c1e35c5b163d9455da099f0994a8ac 100644 GIT binary patch delta 21756 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 xv7Spej2^Rk*_!J&tTQgVL{{cPm57qzx diff --git a/libmeshrf/src/bindings.cpp b/libmeshrf/src/bindings.cpp index 36ee1ca..a5260af 100644 --- a/libmeshrf/src/bindings.cpp +++ b/libmeshrf/src/bindings.cpp @@ -25,6 +25,12 @@ EMSCRIPTEN_BINDINGS(meshrf_module) { .property("epsilon", &LinkParameters::epsilon) .property("sigma", &LinkParameters::sigma) .property("climate", &LinkParameters::climate) + // Statistical variability (ROADMAP P4-6). Optional on the JS side -- + // omitting them keeps the median 50/50/50 prediction. + .property("time_pct", &LinkParameters::time_pct) + .property("loc_pct", &LinkParameters::loc_pct) + .property("sit_pct", &LinkParameters::sit_pct) + .property("mdvar", &LinkParameters::mdvar) ; // Register Vector types @@ -62,14 +68,18 @@ EMSCRIPTEN_BINDINGS(meshrf_module) { float gsd_meters, float epsilon, float sigma, - int climate + int climate, + float time_pct, + float loc_pct, + float sit_pct ) { float* elev = reinterpret_cast(elev_ptr); return calculate_rf_coverage( elev, width, height, tx_x, tx_y, tx_h, rx_h, freq_mhz, tx_power_dbm, tx_gain_dbi, rx_gain_dbi, rx_sensitivity, max_dist, gsd_meters, - epsilon, sigma, climate + epsilon, sigma, climate, + time_pct, loc_pct, sit_pct ); })); diff --git a/libmeshrf/src/meshrf_coverage.cpp b/libmeshrf/src/meshrf_coverage.cpp index 26ef295..bf12603 100644 --- a/libmeshrf/src/meshrf_coverage.cpp +++ b/libmeshrf/src/meshrf_coverage.cpp @@ -21,7 +21,10 @@ std::vector calculate_rf_coverage( float gsd_meters, float epsilon, float sigma, - int climate + int climate, + float time_pct, + float loc_pct, + float sit_pct ) { // Initialize result buffer with "no signal" value std::vector signal_strength(width * height, -999.0f); @@ -88,6 +91,10 @@ std::vector calculate_rf_coverage( params.epsilon = epsilon; // Ground permittivity params.sigma = sigma; // Ground conductivity params.climate = climate; // Climate zone + // Statistical variability (ROADMAP P4-6); 50/50/50 = median forecast + params.time_pct = time_pct; + params.loc_pct = loc_pct; + params.sit_pct = sit_pct; // Calculate path loss using ITM std::vector losses = calculate_radial_loss(profile.data(), profile.size(), params); diff --git a/libmeshrf/src/meshrf_itm.cpp b/libmeshrf/src/meshrf_itm.cpp index af35cbd..7089f08 100644 --- a/libmeshrf/src/meshrf_itm.cpp +++ b/libmeshrf/src/meshrf_itm.cpp @@ -33,22 +33,21 @@ std::vector calculate_radial_loss(float* terrain_profile, int profile_len // we effectively use the first i+3 elements. std::vector pfl_buffer(profile_length + 2); - // Default Environmental Parameters (if not in struct, strictly follow pure ITM args) - // Using values from params or defaults - int mdvar = 12; // Mode of variability (Accidental/General?) - 12 is common default? - // Let's look at ITM docs or legacy code. Using standard defaults. - // mdvar: + // Variability parameters come from the caller (ROADMAP P4-6); LinkParameters + // defaults them to the median 50/50/50 prediction with mdvar=12, which is + // what this function previously hardcoded. + // mdvar: // 0 - Single message // 1 - Accidental // 2 - Mobile // 3 - Broadcast - // ... - // Using 1 (spot) or 12? defaults usually passed in. - - // Time/Loc/Sit - double time_pct = 50.0; - double loc_pct = 50.0; - double sit_pct = 50.0; + int mdvar = params.mdvar; + + // Time/Loc/Sit: percentage of time/locations/situations for which the + // predicted loss is NOT exceeded. + double time_pct = params.time_pct; + double loc_pct = params.loc_pct; + double sit_pct = params.sit_pct; // Result at index 0 (TX) is 0 loss results[0] = 0.0f; diff --git a/public/meshrf.wasm b/public/meshrf.wasm index 5f41fe898b84a636b76a29fb2242a5c463e7fdae..96c848e5f9c1e35c5b163d9455da099f0994a8ac 100644 GIT binary patch delta 21756 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 xv7Spej2^Rk*_!J&tTQgVL{{cPm57qzx diff --git a/src/components/Layout/sections/EnvironmentSection.jsx b/src/components/Layout/sections/EnvironmentSection.jsx index b5ad4fa..715fdda 100644 --- a/src/components/Layout/sections/EnvironmentSection.jsx +++ b/src/components/Layout/sections/EnvironmentSection.jsx @@ -1,6 +1,6 @@ import React from 'react'; import CollapsibleSection from './CollapsibleSection'; -import { useEnvironment, GROUND_TYPES, CLIMATE_ZONES } from '../../../context/EnvironmentContext'; +import { useEnvironment, GROUND_TYPES, CLIMATE_ZONES, RELIABILITY_MODES } from '../../../context/EnvironmentContext'; const EnvironmentSection = ({ isOpen, onToggle }) => { const { @@ -8,7 +8,8 @@ const EnvironmentSection = ({ isOpen, onToggle }) => { clutterHeight, setClutterHeight, groundType, setGroundType, climate, setClimate, - fadeMargin, setFadeMargin + fadeMargin, setFadeMargin, + reliabilityMode, setReliabilityMode, variability } = useEnvironment(); const inputStyle = { @@ -101,6 +102,30 @@ const EnvironmentSection = ({ isOpen, onToggle }) => { + {/* Reliability / ITM Variability (ROADMAP P4-6) */} +

+ + +
+ {variability.description} +
+ time/loc/situation: {variability.time}/{variability.loc}/{variability.sit} +
+
+
+ {/* Fade Margin */}