Skip to main content

Module optic

Module optic 

Source
Expand description

Turret and reticle geometry: click detents, revolutions, physical travel, current dial state, and reticle hold limits (MBA-1348).

Before this module the crate’s entire optic model was crate::adjustment::ClickValue plus two tracking correction factors (elevation/windage CF), applied ad hoc wherever a dialed number needed to become a true angular one. Real turrets have more structure than that: click detents grouped into revolutions, an optional zero stop, finite mechanical travel, and a current dialed offset from zero. Real reticles have a finite usable hold extent too. OpticProfile gives all of that a home so a later dial/hold/ hybrid engagement planner has somewhere to read it from. This module is pure data and validation — it does not itself decide how to engage a target.

§MIL, always

Every angular field in this module — travel, turret state, hold bounds — is in milliradians. There is no unit-selection knob here; presenting other units (MOA, SMOA, whole clicks) is a front-end concern, the same way crate::adjustment already handles it for the CLI and the WASM terminal.

§DIAL-space vs TRUE-angular — the one distinction this module exists to encode

Two different kinds of “mil” appear in this crate and they are NOT interchangeable:

  • Turret quantitiesclicks_per_revolution, TravelLimits, TurretState, and clicks generally — live in DIAL-space: the number engraved on the turret, the number a shooter actually dials. A scope with a tracking correction factor (CF) below 1.0 delivers LESS true angular motion per dialed unit than the engraving claims, so it takes MORE dial to reach a given true angle. This crate’s established convention for that relationship — see crate::adjustment::zero_banner_dial_values and crate::truing::scale_report_dial_values — is that dial-space OUTPUTS are obtained by DIVIDING a true angular value by the CF. This module’s turret types inherit that convention unchanged: they do not carry or apply a CF themselves, they simply live in the same DIAL-space that convention produces.
  • Reticle holds elsewhere in the crate (crate::reticle::ReticleHold) are TRUE angular and are never CF-scaled — a mil subtension etched in glass does not care how the turret happens to be calibrated.

A hold and a dialed correction for the identical point of impact are therefore numerically different whenever CF != 1.0. Code that mixes the two spaces without an explicit conversion will silently compute nonsense.

§Travel and turret state are measured from the CURRENT ZERO, not the mechanical stop

TravelLimits and TurretState are offsets from wherever the shooter has zeroed the rifle, not from the turret’s mechanical bottom. That is the fact a shooter actually knows and can act on in the field — “I have 28 mil of up travel left from my zero” — not a fact about the turret’s total mechanical range, which also depends on where in that range the zero happens to sit and is not tracked here.

§reticle_hold_bounds is an explicit input, never derived

See HoldBounds for why: crate::reticle’s off_reticle bounding box is a property of which marks someone chose to author, grown by a fixed margin, not a property of the scope’s actual usable optical extent. It must never be reused as a stand-in for a real spec.

§plan_corrections and the CF rule

plan_corrections turns a TRUE angular AngularCorrection into ranked, executable DialPlans: dial the whole correction in whole clicks, hold the whole correction on the reticle, or split it (dial what the turret can reach, hold the TRUE angular remainder). Getting the DIAL-space/TRUE-angular distinction above right in every arm is the entire reason this function exists, so the rule is restated here in full and followed literally everywhere below:

  • Turret travel, turret state, and click counts all live in DIAL space, as established above.
  • A tracking correction factor (CF) maps one space to the other. To EXECUTE a TRUE angular need of corr_true mil on a turret whose CF is cf, the DIAL target is corr_dial = corr_true / cf — the CF DIVIDES going from a true need to a dial target, the same direction as crate::adjustment::zero_banner_dial_values and crate::truing::scale_report_dial_values. That target is quantized onto whole clicks IN DIAL SPACE, via quantize_angle(corr_dial, &ClickValue { size: click_size_mil(click), base: ClickBase::Mil }) — a SYNTHETIC click value in mil so the reconstruction identity (clicks as f64 * size + residual == corr_dial) holds in mil regardless of whether the real click graduation is mil, MOA, or SMOA. Whatever whole click count results, the dial then EXECUTES clicks as f64 * click_size_mil(click) * cf TRUE angular mil — the CF MULTIPLIES going from dial clicks back to true angular, the OPPOSITE direction from how it was applied going in.
  • Reticle holds are TRUE angular and are NEVER CF-scaled, anywhere: a hold component is computed and bounds-checked entirely in true mil, never quantized onto a click.

cf_dial_space_worked_example (this module’s test suite) pins the hand-derived numbers for a CF of 0.98: a 5.0 true-mil correction on 0.1-mil clicks needs a dial target of 5.0 / 0.98 = 5.10204... mil, which quantizes to 51 clicks; those 51 clicks EXECUTE 51 * 0.1 * 0.98 = 4.998 true mil, leaving a 0.002 true-mil hybrid hold. Swapping the multiply/divide directions, or holding the DIAL-space remainder (corr_dial - clicks * size) instead of the TRUE one (corr_true - dial_mil_true), silently produces a different, wrong number here — this module exists specifically to make that mistake impossible to make quietly.

§Honesty: nothing is silently clamped

A plan whose dial component cannot reach its target given the optic’s declared travel is still returned — clamped to the travel it actually has — but its feasible field is false and the clamp is recorded in limits_hit as a LimitViolation. The same is true of a hold that exceeds reticle_hold_bounds or Preferences::max_hold_mil. A plan’s residual_mil is always computed against what it actually executes, never against the original request, so a clamped plan’s residual honestly reflects the resulting miss (see infeasible_is_reported_never_silently_clamped).

Declared travel/hold data is trusted exactly as given; its ABSENCE is a different, weaker claim than a KNOWN, EXCEEDED limit, and gets its own LimitKind (NoTravelData/NoHoldBoundData) rather than being silently treated as “unlimited.” Whether a missing or exceeded limit turns off a strategy’s OWN feasible flag depends on what that strategy actually promises: Strategy::DialAll promises to deliver the WHOLE correction by dialing alone, so it cannot affirm that promise without knowing (and fitting inside) its travel. Strategy::HoldAll and Strategy::Hybrid promise (respectively) the whole correction, or the dial shortfall’s exact remainder, via the reticle, so THEY cannot affirm their promise without knowing (and fitting inside) hold bounds — but a Hybrid plan’s dial component absorbing less than the full correction because travel ran out is not a broken promise (the hold is DEFINED to absorb exactly that shortfall, by construction), so a missing or exceeded TRAVEL limit is recorded on a Hybrid plan purely for disclosure and never by itself makes it infeasible — only its own hold not fitting does.

OpticProfile::zero_stop is never read by plan_corrections at all, at travel-known or travel-None alike: it is descriptive turret metadata (see its own doc comment), and this module’s ONLY source of truth for what the turret can physically reach is elevation_travel/windage_travel being Some or None — a zero-stopped turret with no declared elevation_travel is treated exactly like a non-zero-stopped one with no declared travel: unverifiable, not “safely bounded at zero because it says zero_stop.”

§Correction-space is not reticle-space (the hold-bound mapping)

hold_mil above is this module’s own correction-space: + means “the point of impact needs to move up/right,” matching AngularCorrection and a dial’s own convention (a positive dial adjustment moves point of impact up/right). HoldBounds (up_mil/down_mil/left_mil/right_mil) is crate::reticle’s RETICLE-space instead, pinned at src/reticle.rs:30-42: down_mil is positive BELOW the optical center, right_mil is positive to the shooter’s RIGHT of it, and a holdover mark (compensating a bullet that falls) sits at POSITIVE down_mil — below center. These two spaces are OPPOSITELY signed on BOTH axes, not just one: a positive (UP) correction’s hold mark sits BELOW center (consumes down_mil), exactly like every BDC reticle’s long-range holdover marks, which sit below center, never above it; a positive (RIGHT) correction’s hold mark sits to the LEFT of center (consumes left_mil) by the identical argument, mirrored. AxisComputation names its two fields bound_for_positive_correction / bound_for_negative_correction (rather than e.g. “hold_up”) specifically so this mapping is stated at the type level and cannot silently re-invert; see the hold_bound_mapping_matches_reticle_space_not_correction_space test, which is built to fail if it does.

Structs§

AngularCorrection
A TRUE angular correction to deliver on target – the output of a solve, not yet realized as any turret or reticle instruction. Positive elevation_mil is UP; positive windage_mil is RIGHT. Never CF-scaled – see the module’s “CF rule” doc section.
AxisInstruction
One axis’s executable instruction within a DialPlan (MBA-1348).
DialPlan
One ranked, executable plan for delivering a two-axis AngularCorrection (MBA-1348). instructions is always exactly [elevation, windage].
DialPlanReportV1
plan_corrections’s versioned report (MBA-1348). Carries method and assumptions in the payload itself – this train’s cross-cutting honesty principle (spec §2) – not only in prose documentation.
HoldBounds
A reticle’s usable hold extent in each direction, TRUE angular mil, relative to the reticle’s own center. All four fields are magnitudes — never negative.
LimitViolation
One recorded reason a DialPlan was clamped or could not be verified feasible (MBA-1348). Always DISCLOSED in DialPlan::limits_hit, never silently absorbed.
OpticProfile
A shooter’s complete turret and reticle geometry (MBA-1348).
Preferences
Caller preferences steering plan_corrections’s ranking – never its underlying arithmetic, which is identical regardless of these values. See “Ranking is deterministic” in plan_corrections’s own doc comment.
TravelLimits
Remaining mechanical turret travel in each direction from the current zero setting, DIAL-space mil. Both fields are magnitudes — never negative — measured outward from zero: down_mil is travel available going down (elevation) or left (windage); up_mil is travel available going up (elevation) or right (windage). validate rejects negative and non-finite values.
TurretState
The turret’s current dialed position, as signed offsets from zero — DIAL-space, mil. Positive elevation_mil is dialed up from zero; positive windage_mil is dialed right from zero. Unlike TravelLimits these ARE signed (a turret can be dialed to either side of zero), so validate does not reject a negative value on its own — it only rejects a state whose magnitude on an axis exceeds that axis’s declared TravelLimits, when both are present.

Enums§

Axis
Which of a shot’s two independent angular axes an AxisInstruction or LimitViolation belongs to (MBA-1348). Unrelated to crate::reticle_import’s private, importer-internal Axis of the same name in a different module.
Direction
AxisInstruction::direction: which way the DELTA actually dialed turns the turret (MBA-1348). Wire form is lowercase ("up"/"down"/"left"/"right") via #[serde(rename_all)] – unchanged from this type’s original plain-&str form (MBA-1348 review fix M1), but now a real enum so AxisInstruction/DialPlan/DialPlanReportV1 can derive Deserialize again (a &'static str field cannot: serde’s blanket impl for &'a str ties 'a to the deserializer’s own input lifetime, which is essentially never 'static).
LimitKind
Why a DialPlan component was clamped, or could not be verified feasible (MBA-1348). Paired with the offending Axis in a LimitViolation.
OpticError
Why an OpticProfile failed validate.
Strategy
How a DialPlan proposes to deliver a correction (MBA-1348).

Constants§

DIAL_PLAN_SCHEMA_VERSION_V1
Schema version for DialPlanReportV1’s payload shape (MBA-1348).

Functions§

plan_corrections
Turns a TRUE angular AngularCorrection into ranked, executable dial/hold/hybrid plans for a real optic (MBA-1348) – see the module’s “plan_corrections and the CF rule” and “Honesty” doc sections for the conventions this function follows in every arm.
revolution_annotation
Splits a DIAL-space click count measured from zero into (revolutions, clicks_within_the_revolution), for a turret whose cap marks revolutions every clicks_per_revolution clicks — e.g. rendering “2 revolutions + 7 clicks” instead of a bare click count the shooter has to do the division on in their head under time pressure.