Expand description
Cleanroom importer for the .reticle XML document format.
A .reticle file is a third-party drawing format for scope reticles: a single
<reticle> root carrying an <elements> list of shapes (lines, circles, rectangles,
filled paths) plus a <bdc> section. This module is the one-way transform from that
authoring format into the engine’s own ReticleDescription, so a reticle drawn in
that format can be hold-solved by the existing
hold_point_in_reticle without any of the
CLI/FFI/WASM surfaces having to learn a second schema. It is a converter, not new
physics — every coordinate is carried straight through, only unit-normalized to
milliradians and sign-corrected for the engine’s axis convention.
It is a sibling of crate::reticle_import (Bero’s “Ventum” JSON), and meets the same
hold-vs-decoration problem from the opposite direction: Ventum is an aiming-mark format
that happens to contain some decoration, while .reticle is a picture that happens to
contain some aiming marks.
Everything here was derived from a sample .reticle document and the SVG it renders
to — the observable file structure, and the observable output that pins down what its
coordinates mean. No third-party implementation of this format was consulted, and
none is vendored. A file format is not itself copyrightable; this crate stays MIT OR
Apache-2.0. Every fixture in this module’s tests and in
tests/fixtures/reticle_document/ is invented content.
Like crate::reticle_import, this module is fs-free (it transforms an in-memory
&str), carries no feature gate, and compiles for wasm32-unknown-unknown.
§What carries a hold, and what does not
This is the whole design decision, and the format answers it itself rather than leaving it to be inferred:
<bdc>entries are the holds. A<bdc position-x="…" position-y="…">element is a declared aiming point. Each one becomes aReticleMark.- Everything under
<elements>is the picture, and is dropped.reticle-line,reticle-circle,reticle-rectangle,reticle-path(and itsreticle-path-move-to/reticle-path-line-tovertices), and any unknown future tag carry no hold. Their geometry attributes are never read as angles at all — a shape this module drops cannot fail its import.
The evidence that this split is the format’s own and not our invention: in a real
document the <bdc> positions are duplicates of shapes already drawn under
<elements>. A BDC reticle draws its three holdover marks as three short
reticle-line ticks, and then lists those same three coordinates again under <bdc>.
The drawing says how it looks; the <bdc> section says what it means. Importing the
drawing as well would double every holdover.
§The cost of that rule, stated plainly
A .reticle document’s windage marks are drawn as reticle-line ticks and are not
listed under <bdc>. They are therefore dropped, and an imported reticle usually has
marks on the elevation axis only. That has a visible consequence downstream: with every
mark at right_mil == 0, the windage axis of the mark bounding box is degenerate, so
ReticleHold::off_reticle reads true for
any nonzero wind hold — see that field’s documentation. For a reticle that really does
have windage hashes etched on it, that answer comes from this import, not from the optic.
Recovering those marks would mean classifying reticle-line elements as hash marks by
their geometry (short, axis-perpendicular, off-center) — inventing holds out of
decoration, and doing it with a heuristic that cannot distinguish a windage hash from a
crosshair arm. This module will not guess. Instead the drop is made countable rather
than silent: import_reticle_document_with_report returns a
ReticleImportReport tallying every drawing tag it walked past, so a caller can tell
the shooter how many lines and paths this document drew that are not holds, instead of
leaving them to infer it from a suspiciously empty reticle.
Two related non-inventions, for the reader who wonders:
- No synthetic center mark. The zero is the coordinate origin every imported mark
is measured from, and a hold is reported in those same coordinates, so it survives as
the frame rather than as a mark. If a document wants the center to be a selectable
aiming point it can say so with a
<bdc position-x="0…" position-y="0…">entry. - No labels. A
<bdc>entry carries no range and no text — only a position and thetext-offset/text-heighthints for where a renderer should draw a label. The range that belongs to a holdover is a property of the firing solution, not of the document, so every imported mark haslabel: None.
§Coordinate and unit conventions
The document’s +y is UP; the engine’s is DOWN. This is the one sign trap of the
format, and it is the opposite of crate::reticle_import, where no flip was needed.
Confirmed from rendered output: an element at position-y="8moa" renders above the
zero, and a holdover BDC entry sits at a negative position-y. So
right_mil = position-x down_mil = -position-yand a holdover’s negative position-y becomes the positive down_mil the engine wants.
Every value names its own unit inline as a suffix — "96moa", "-0.55moa",
"1.5mil". There is no document-level unit declaration, so each attribute is converted
on its own: moa scales by MOA_TO_MIL (shared with crate::reticle_import, so
the two importers cannot drift apart), mil / mils / mrad pass through. A value
with no suffix, or with a suffix this module does not recognize, is a hard error rather
than a guessed scale — silently mis-scaling a reticle by 3.44x is worse than refusing it.
§size-x / size-y / zero-x / zero-y are canvas placement, not a mark offset
The root declares a canvas extent (size-x, size-y) and the position of the zero
within it (zero-x, zero-y, measured from the canvas top-left). They exist so a
renderer can map a reticle coordinate onto its output surface:
canvas_x = (zero-x + position-x) / size-x * width
canvas_y = (zero-y - position-y) / size-y * height (note the minus: +y is up)The origin of the element coordinate system is the zero — that is what the attributes
are named after — so element coordinates are already zero-relative and zero-x/zero-y
do not enter this transform. Changing them re-frames the picture on its canvas; it
does not move a hold relative to the point of aim. They are reported (as
ReticleImportReport::zero_in_canvas_mil) rather than applied, and there is a test
that two documents differing only in their zero attributes import to identical marks.
§What the format does not carry
.reticle has no focal-plane and no magnification attribute. A document’s own name
string may hint at one in prose ("… (4-12x40, at 12x)"), but that is text for a human,
not a field, and this module does not mine it.
An imported description is therefore always FocalPlane::First with
reference_magnification = 1.0 — the identity, the only choice that scales nothing.
If the reticle is actually SFP, the caller must set
ReticleDescription::focal_plane and
ReticleDescription::reference_magnification from the optic before solving, or
every hold will be computed as though the subtensions were magnification-independent.
Both fields are public precisely so that is a one-line fix at the call site.
§Safety
crate::reticle::MAX_RETICLE_MARKS is enforced while <bdc> entries are collected,
so a hostile document with a million entries returns ReticleError::TooManyMarks
promptly instead of allocating. Element nesting is bounded by
MAX_DOCUMENT_DEPTH, and the scanner is iterative — no recursion, so a deeply nested
document cannot overflow the stack. This is the same defense as the 0.31.0 reticle
generator size guards and the Ventum importer’s repeat-expansion cap.
Structs§
- Reticle
Import Report - What an import left behind, so the hold/decoration rule is countable rather than implicit. See the module documentation for why the drawing is dropped.
Constants§
- MAX_
DOCUMENT_ DEPTH - Deepest element nesting a
.reticledocument may use before it is rejected.
Functions§
- import_
reticle_ document - Import a
.reticleXML document into aReticleDescription. - import_
reticle_ document_ with_ report - Import a
.reticledocument, also returning aReticleImportReportof what the hold/decoration rule left behind.