// The shape a mixture form field starts from, how a cylinder's role is written, and the // breathing-gas maths that turns an O₂/He pair into something a diver recognizes - a gas // name, a maximum operating depth, an equivalent narcotic or air depth. Pure data and // pure functions with no React in it, and imported by `lib/dive-import.ts` - which is // why it lives here rather than in `mixture-fields.tsx`: a `lib` module reaching into a // `"use client"` component to pull react-hook-form, lucide-react and three shadcn // components along with one constant is the wrong direction. // // `DEFAULT_MIXTURE` is re-exported from `mixture-fields.tsx` so existing call sites are // unchanged. // // **Why the maths is client-side.** `services/dive_gas.py` draws the line at values // derivable purely from stored columns; `gear_service.py` has a web twin because the // browser holds an input the server lacks. That is this case exactly. The decisive // input is *live form state* - the O₂ the diver is halfway through typing, which has // not been saved and may never be - so an API round trip could not answer the // question being asked, and a second server-side implementation would exist only to // duplicate this one. The iOS app does carry a second implementation: `GasMath` in // opendiving-ios's OpenDivingKit mirrors the gas name, the MOD (`mod(oxygen:ppO2:)`), // the ppO₂ limits, the MOD warnings and the parallel-set rule, so a change to any of // those here is owed there too. Lifting them to the API, for both to read, waits on a // consumer that cannot carry its own copy (an export renderer, a notification job). import type { GasRole, TankUsage } from "@/lib/api/dives"; import type { TankGas } from "@/components/icons/tank-icon"; import { formatComparableDepth, formatDepth, type UnitSystem, } from "@/lib/units"; // Every depth this module *computes* is metres, and every depth it *prints* goes // through `formatDepth`, or `formatComparableDepth` where a limit is stated against a // depth. The maths below is unit-blind on purpose - `METERS_PER_BAR` // is a fact about water, not a display choice - so `units` reaches only the string // builders, and only ever as the last step. // Default values pre-filled when a new mixture (tank) is added. Start/end // pressure are deliberately left blank ("") rather than defaulted, since // they vary per tank/fill and shouldn't be guessed. // // `po2_limit` is blank for the same reason and one more: a MOD is the number this // whole module exists to get right, and seeding 1.4 would make every hand-added // cylinder claim a limit the diver never chose. Absent, `mod()` applies // `PPO2_WORKING` itself - the same 1.4, but as a documented fallback rather than as // a value pretending to be a decision. `role` carries no *value* either, for a third // reason: it is a fact about the dive plan that only the diver knows. // // `role` is still spelled `""` rather than left off, because `""` is this form's word // for cleared - `mergeMixture` and `toDiveMixtureInput` both write it, and // `normalizeMixtures` converts it away at the edge. Omitting it here made a // hand-added cylinder the one row where the field arrived `undefined`, which is the // value react-hook-form treats as "show the default" (see `diveMixtureSchema`). // `gas_number` is the genuine exception: no input writes it, so it has no cleared // state to spell - it is a file's own identifier and a hand-added cylinder has none. export const DEFAULT_MIXTURE = { volume: 11.1, start_pressure: "" as const, end_pressure: "" as const, oxygen: 21.0, helium: 0, po2_limit: "" as const, role: "" as const, // `""` for the same reason as `role`, and it carries no value for the same third // reason too: how the cylinders were breathed is a fact about the dive only the // diver knows, and no import can ever supply it. usage: "" as const, // No vessel: a hand-added tank links one only when the diver picks it. gear_item_uuid: "", }; // How each `GasRole` is written for a diver. Separate from the wire values, which are // the API's vocabulary (`GasRole` in `schemas/dive_mixture.py`) - capitalized, and // free to diverge if a role is ever better named than its enum member. Lives in this // module, beside the maths, so the form's picker and the detail badge name a role // identically rather than each keeping their own copy of the mapping. // // One word each, and the "gas" that "Bottom gas"/"Deco gas" would naturally carry is // deliberately dropped: every place these appear supplies that word already - beside // the gas name on a tank card, in the consumption table's column headed **Gas**, and // under the form's **Role** label. export const GAS_ROLE_LABELS: Record = { bottom: "Bottom", deco: "Deco", diluent: "Diluent", oxygen: "Oxygen", }; // How each `TankUsage` is written for a diver, on the same terms as `GAS_ROLE_LABELS` // above: separate from the wire vocabulary (`TankUsage` in `schemas/dive_mixture.py`), // capitalized, and shared by the form's picker and the dive page so the two cannot // name the same flag differently. // // One word each because it is a *name*, not a description: it is the word the diver // picked in the form, quoted back to them on the tank's card. The meaning rides in // the form's own longer options ("Parallel (sidemount / independent)"). export const TANK_USAGE_LABELS: Record = { parallel: "Parallel", staged: "Staged", }; // Just the flag, so a saved `DiveMixture` and a half-filled form row both satisfy the // two predicates below - the same looseness `OxygenFractions` is built on, and the // `""` is there for the same reason: it is how a cleared `