Module card_service

Module card_service 

Source
Expand description

Transport-free card-generation services (come-ups, range table, wind card).

These replicate the CLI’s card commands exactly — same zero solve, same sampled trajectory, same nearest-sample row selection, same adjustment/bias/CF/click ordering via crate::adjustment — as versioned request/response types the bridge can expose to embedded consumers. The CLI remains the reference implementation; tests/card_bridge_golden.rs asserts row-for-row agreement.

§Unit convention

Unlike solve-json (explicit SI), card requests are denominated in the shooter’s own units, exactly like the CLI flags they mirror: imperial = fps, grains, inches, yards, °F, inHg, mph; metric = m/s, grams, mm, meters, °C, hPa, m/s. A DOPE card is a display artifact; its inputs and outputs share the shooter’s unit world, and this keeps the request shape identical to the documented CLI surface.

§Per-dimension units (MBA-1519)

Nobody shoots in one system. A Finnish shooter ranges in metres, dials MIL, and buys bullets labelled in grains, because that is how the industry sells them; European makers dual-print grams / grains rather than replacing one with the other, and mix within a single table. So the denominating state on this request is a unit PER DIMENSION, and CardUnits is a one-shot preset that fills in every dimension the request does not state for itself — not a mode the card is “in”.

Concretely: an explicit per-dimension field wins; an absent one falls back to what units implies. A request written before those fields existed states none of them, so every dimension falls back and the card is byte-for-byte the card it always was (tests/card_units_per_dimension.rs pins that against a stored document).

The response needed no new FIELD for any of this: CardUnitsBlockV1 has always carried seven independent label fields, so a card whose distance is metres and whose velocity is fps already had somewhere to say so. It did need the two angular labels to become requestable: elevation_adjustment/windage_adjustment print "MIL"/"MOA"/"SMOA"/ "IPHY"/"CLICKS" and AdjustmentUnit deserialized lowercase only, so those two were the only fields of the block a caller could not echo back. They take the uppercase spellings as aliases now, which makes “echo the units block into the next request” true of all seven fields rather than five of them.

Three candidates do NOT get a new field, and each for its own reason:

  • Scope adjustment is already per-dimension, and was first: adjustment_unit and windage_unit are separate fields with separate click graduations, because a shooter with a MIL rifle and an MOA rifle has two scopes rather than a preference. SMOA (and its iphy spelling) is already one of the values they take.
  • Clicks is not a units dimension. A click is meaningless without a graduation, and this module already enforces that: adjustment_unit == Clicks without an elevation_click_value is a refusal. The graduation belongs beside the turret’s MIL/MOA/SMOA marking, not in a units picker.
  • Twist rate is a dimension of the design but not of this request — a card request carries no twist, so there is nothing here for a twist unit to denominate.

And the linear-at-distance spellings are NOT units: “inches@100yd” IS SMOA — the same 1/3600 rad, the same printed number, no conversion — and “cm/100m” is 0.1 mrad, i.e. MIL with a factor-of-ten display. They are alternate HEADER TEXT for units this request already has, and adding them as values would give a caller two spellings that produce identical numbers.

§What the response does NOT tell a caller

Like every other default on this request — sight_height, temperature, pressure, humidity, windage_unit — a per-dimension unit taken from the preset is applied silently, and this surface has no notice channel at all (there is no assumptions array on CardResponseV1, unlike solve-json’s SolveSuccessV1).

Be precise about what that costs, because a paragraph of prose can argue it away. The units block names the unit of the SEVEN OUTPUT COLUMNS and nothing else. It says nothing about the six dimensions that are input-only — mass, diameter, sight_height, temperature, pressure, altitude — so for those a caller cannot read back off the card which unit the engine settled on.

altitude is the sharp one, because it is the dimension whose fallback is deliberately NOT the preset’s: an imperial card.range_table carrying altitude: 5000 and no altitude_unit — which a US caller writing an imperial card means as feet — is solved at 5000 METRES, and no field of the response says so. On a .308 175 gr that is ~1.9 MIL of elevation at 1000 yd against the same card with altitude_unit: "feet". The BEHAVIOUR predates the per-dimension fields (altitude has always reached AtmosphericConditions unconverted); what is new is that the request can now name the dimension, so the gap is describable instead of merely present.

No notice was added here, and the reason is narrow rather than principled: any notice — an assumptions array, or extra label fields on the units block — is a change to the RESPONSE bytes of every card, including the stored documents the reprint path compares label-for-label and the six goldens that are the whole evidence for this change being additive on the wire. A response-schema change belongs to a ticket that can move those goldens honestly. Until then the remedy is on the request: a caller that cares which altitude it got states altitude_unit, and a card built by an app should state every dimension rather than lean on a preset.

Structs§

CardBcSegmentV1
One velocity band of an explicit velocity-keyed BC schedule (CardRequestV1::bc_segments). velocity_min/velocity_max are in the request’s velocity_unit and must satisfy velocity_min < velocity_max; bc is the BC (for the request’s drag_model) that applies inside the band.
CardRequestV1
Everything the three card surfaces share: load, zero, atmosphere, display axes.
CardResponseV1
CardRowV1
CardTruncationV1
The wire form of CardTruncation: the rows a card could not print and why. Distances are in the response’s own units.distance.
CardUnitsBlockV1
PdfCardOptionsV1
Presentation knobs for the PDF dope card: the header/footer labels the printed card carries and the table’s font size. The ballistics axes it prints (elevation unit, windage unit, click graduations, tracking CFs, zero-set biases) are NOT here — they are already fields on CardRequestV1, shared with the on-screen card, precisely so the two cannot disagree about what a row means.
PdfCardV1
A rendered PDF dope card: the document plus the facts a transport needs to describe it without re-parsing the bytes.
StoredCardResponseV1
A stored CardResponseV1, as a deserializable mirror.
StoredCardRowV1
One stored row (see CardRowV1, which this mirrors). Only range is required; a column the stored card did not carry stays None and prints as an em-dash rather than a fabricated zero. Unknown fields are ignored, for the reason StoredCardResponseV1 gives.
StoredCardTruncationV1
The truncation block of a stored response (see CardTruncationV1, which this mirrors).
StoredCardUnitsBlockV1
The units block of a stored response (see CardUnitsBlockV1, which this mirrors). Unknown fields are ignored, for the reason StoredCardResponseV1 gives.
StoredCardV1
A card whose rows already exist: print THESE numbers instead of solving.

Enums§

CardAltitudeUnit
Site elevation. See CardRequestV1::altitude_unit for why this one dimension’s default does not come from the preset.
CardDiameterUnit
Bullet diameter: inches or millimetres, the two units a calibre is ever quoted in.
CardDistanceUnit
Range axis: what zero_distance, start/end/step and every row’s range are in.
CardEnergyUnit
Terminal energy column.
CardLinearUnit
A small linear length. Shared by the request’s two TARGET-SIDE linear dimensions (sight-to-bore geometry and the linear drop column) because they take the same values, never because they are the same setting: they are separate fields and resolve independently.
CardMassUnit
Bullet weight. Grains and grams differ by 15.43x, and on the BC path mass never enters the drag denominator — it touches energy and stability only — so a grams-for-grains slip yields a trajectory that looks entirely normal. Which is exactly why the unit has to be stated per-dimension rather than inferred: the dual-printed g / gr box a European shooter is holding is the normal case, not an edge one.
CardPressureUnit
Station pressure. hpa and mbar are the same number, which is why mbar is an alias rather than a value of its own.
CardServiceError
CardTemperatureUnit
Air temperature.
CardUnits
The unit PRESET for a card request: a one-shot bulk action that fills in every per-dimension unit the request does not state for itself, not a mode the card is in.
CardVelocityUnit
Speed of the bullet: muzzle_velocity, bc_segments band edges, the velocity column.
CardWindSpeedUnit
Wind and crossing-target speed. Four values, not two: a European card reads km/h and a coastal one reads knots, and neither is expressible by picking a system.
DragModelV1
Drag model selector mirroring the CLI’s --drag-model strings.
PdfRowSource
Where the printed rows came from — reported so a caller can verify it got a reprint rather than a re-solve, which is otherwise indistinguishable from the document.

Constants§

CARD_SCHEMA_VERSION_V1
Schema version of the card request/response contract.
MAX_PDF_PAGES
Pages a printable card may run to. Bounds the one row set the row cap above cannot: few enough rows, but at a font scale that fits very few of them per page.
MAX_PDF_ROWS
Rows a printable card may carry.
PDF_CARD_KIND
The one card kind card.pdf can print, in the kind spelling CardResponseV1::kind uses.

Functions§

come_ups_v1
Come-ups: elevation dial per range plus the incremental come-up between rows. Mirrors the CLI’s come-ups command row-for-row.
pdf_card_v1
Render the printable PDF dope card for a card request.
range_table_v1
Range table: drop and wind on possibly different display axes. Mirrors the CLI’s range-table command row-for-row (two solves: with wind for drift, without wind for the pure elevation axis).
wind_card_v1
Wind card: drift matrix, ranges x wind speeds, per wind-FROM angle. Mirrors the CLI’s wind-card command cell-for-cell. The windage axis uses adjustment_unit (the CLI’s wind-card has a single unit for the matrix).