Skip to main content

Module reticle_document_import

Module reticle_document_import 

Source
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 a ReticleMark.
  • Everything under <elements> is the picture, and is dropped. reticle-line, reticle-circle, reticle-rectangle, reticle-path (and its reticle-path-move-to / reticle-path-line-to vertices), 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 the text-offset / text-height hints 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 has label: 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-y

and 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§

ReticleImportReport
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 .reticle document may use before it is rejected.

Functions§

import_reticle_document
Import a .reticle XML document into a ReticleDescription.
import_reticle_document_with_report
Import a .reticle document, also returning a ReticleImportReport of what the hold/decoration rule left behind.