The Compositional Target Model
A ringgrid target is described at runtime by a TargetLayout. Before 0.8 the
only target was a hex lattice of 16-sector coded rings, modeled by the flat
BoardLayout type (removed in 0.9). TargetLayout generalizes that into four
orthogonal aspects that compose freely:
TargetLayout = lattice × ring geometry × coding × optional fiducials
- Lattice (
LatticeGeometry) — how marker cells are arranged:HexorRect. - Ring geometry (
RingGeometry) — the outer/inner radii shared by every marker. - Coding (
MarkerCoding) — whether markers carry a decodable identity (Coded16) or are plain annuli (Plain). - Fiducials (
OriginFiducials, optional) — filled dots that anchor origin and orientation for targets whose markers do not encode identity.
Each aspect is a small value type; TargetLayout::new composes and validates
them. Geometry cannot be mutated in place — the derived cell cache (positions,
ID↔coordinate lookups) would silently desync — so construction always goes
through new, a preset, or a JSON loader.
Lattice geometry
| Variant | Fields | Nearest-neighbor spacing |
|---|---|---|
Hex(HexGeometry) | rows, long_row_cols, pitch_mm | pitch_mm × √3 |
Rect(RectGeometry) | rows, cols, pitch_mm | pitch_mm |
The hex lattice uses axial rows that alternate between long and short rows;
long_row_cols sets the long-row width. The first generated cell is normalized
to board position [0, 0] mm, and generation order (top row first, left to
right) is load-bearing — sequential IDs derive from it. Cell coordinates are
axial (q, r) for hex and (col, row) for rect, both carried as
projective_grid::Coord { u, v }.
Ring geometry
RingGeometry { outer_radius_mm, inner_radius_mm } is shared by every marker.
For Coded16 markers these are the centerline radii of the stroked outer and
inner rings; for Plain markers they bound the filled annulus directly. The
outermost drawn radius differs accordingly: a stroked ring overshoots its
centerline by half the stroke width, while a plain annulus does not.
Coding
| Variant | Shape | Identity |
|---|---|---|
Coded16(CodedRingSpec) | two stroked rings with a 16-sector code band between them | codebook ID (decoded per marker) |
Plain | a single filled annulus | none — cells are keyed by lattice coordinate |
CodedRingSpec carries the ring_width_mm stroke and an optional
id_assignment (see ID Assignment Optimization). Coded
targets are capped at the embedded codebook size (893 codewords); a lattice with
more cells than that is rejected for Coded16 but valid as Plain.
Fiducials
OriginFiducials { dot_radius_mm } are dark filled dots printed in the lattice
gaps. They resolve the board origin and orientation for plain targets, whose
markers carry no identity. Coded targets do not need them — decoded IDs already
anchor every marker to a physical cell, and coded-with-fiducials is rejected.
Only the dot size is stored: positions are an L of three lattice gaps around
cell (0, 0), derived from the lattice and read back via
TargetLayout::fiducial_dots_mm(). See Origin Fiducials
for the placement rule, validation, and anchoring.
Composition matrix — how each combination detects
Every built-in lattice × coding combination detects end-to-end, but the identity-bearing stages differ. The coded path decodes IDs and labels markers by codebook lookup; the plain path labels markers by their lattice position.
| Lattice | Coding | Labeling path | ID correction | Output frame |
|---|---|---|---|---|
| Hex | Coded16 | decode → global filter → completion | hex-neighbor BFS consensus | Absolute |
| Rect | Coded16 | decode → global filter → completion | — (global filter + geometric verify only) | Absolute |
| Hex | Plain | detect_grid labeling → completion | — | Absolute if fiducials resolve, else RelativeCanonical |
| Rect | Plain | detect_grid labeling → completion | — | Absolute if fiducials resolve, else RelativeCanonical |
Key points:
- Coded targets run the classic decode-anchored pipeline (see the Detection Pipeline). Every decoded ID maps to a physical board cell, so outputs are always in the absolute board frame.
- ID correction is a hex-neighbor BFS consensus — its algorithmic domain — so it runs only for hex coded targets. Rect coded targets rely on the global RANSAC homography filter plus geometric verification instead.
- Plain targets skip decoding entirely. Fitted ring centers are labeled with
lattice coordinates by
projective_grid::detect_grid(labeling only; the frame homography is refit inf64by ringgrid’s RANSAC), then completion grows the labeled patch. See the plain-target path. - Plain outputs are in a canonical relative frame unless the target carries origin fiducials that resolve the origin; see Origin Fiducials.
Presets
Three presets cover the common cases:
| Preset | Lattice | Coding | Cells | Notes |
|---|---|---|---|---|
TargetLayout::default_hex() | 15-row hex, 8 mm pitch | Coded16 | 203 | the classic 200 mm ringgrid board |
TargetLayout::coded_hex(...) | hex (caller geometry) | Coded16 | — | coded hex from direct geometry arguments |
TargetLayout::rect_24x24() | 24×24 rect, 14 mm pitch | Plain | 576 | 24×24 plain target with three Ø2.8 mm origin dots |
default_hex() is geometry-identical to the classic pre-0.9 hex board, so
existing hex-coded workflows are unchanged.
Construction
#![allow(unused)]
fn main() {
use ringgrid::{
TargetLayout, LatticeGeometry, RectGeometry, RingGeometry,
MarkerCoding, OriginFiducials,
};
// A preset
let hex = TargetLayout::default_hex();
let rect = TargetLayout::rect_24x24();
// A custom plain rect target with origin dots — one call, dots derived
let target = TargetLayout::plain_rect(14.0, 12, 12, 5.6, 2.8, OriginDots::Auto)
.expect("valid target");
// The compositional form, for a dot size you pick yourself
let target = TargetLayout::new(
"my_rect",
LatticeGeometry::Rect(RectGeometry { rows: 12, cols: 12, pitch_mm: 14.0 }),
RingGeometry { outer_radius_mm: 5.6, inner_radius_mm: 2.8 },
MarkerCoding::Plain,
Some(OriginFiducials { dot_radius_mm: 1.4 }),
).expect("valid target");
}
The Detector / DetectConfig constructors take impl Into<TargetLayout>, so
a TargetLayout (or anything convertible into one) can be passed directly.
Validation
TargetLayout::new rejects illegal targets up front:
- non-finite or non-positive pitch, radii, or ring width;
inner_radius_mm >= outer_radius_mm, or a non-positive code-band gap for coded markers;- a drawn marker diameter that reaches or exceeds the minimum center spacing (markers would touch);
- more cells than the codebook can encode, or an out-of-range / duplicate entry
in
id_assignment(coded targets); - coded markers combined with origin fiducials (the excluded matrix cell);
- a fiducial dot radius large enough to overlap a marker’s drawn extent, or a board too small to hold the dot triad inside its marker field (see Origin Fiducials).
Legacy board_spec.json files still load unchanged: TargetLayout::from_json_*
auto-migrates the v4 and v5 schemas to the canonical v6 spec.
Source: crates/ringgrid/src/target/ (layout.rs, lattice.rs, ring.rs,
fiducials.rs)