Selection System
Tag expressions, capability tokens, spatial attributes, and circle fitting algorithm
The selection system translates abstract fixture queries into concrete fixture lists with spatial metadata. It is the key mechanism that enables venue-portable patterns.
Tag Expression Parser
File: src-tauri/src/services/groups.rs
Tag expressions support boolean algebra over fixture group tags:
all -- all fixtures in the venue
front -- fixtures in groups tagged "front"
front & has_color -- front fixtures with RGB capability
left | right -- fixtures on either side
circular & ~blinder -- circular fixtures that are not blinders
has_movement > has_color -- prefer movers, fall back to colorOperators
| Operator | Name | Description |
|---|---|---|
& | AND | Both conditions must match |
| | OR | Either condition matches |
^ | XOR | Exactly one condition matches |
~ | NOT | Negate the following condition |
> | Fallback | Use left side; if zero matches, use right side |
( ) | Grouping | Override operator precedence |
The > (fallback) operator is unique: it evaluates the left side first, and only includes the right side's results if the left side matched zero fixtures. This enables graceful degradation across venues with different fixture inventories. For example, has_movement > has_color prefers moving head fixtures but falls back to any color-capable fixture if no movers are patched.
Capability Tokens
These special tokens are resolved by inspecting each fixture's definition at runtime:
| Token | Matches fixtures with... |
|---|---|
has_color | RGB intensity channels or a color wheel channel |
has_movement | Pan/tilt channels |
has_strobe | A shutter/strobe channel |
Capability tokens are evaluated dynamically -- they do not need to be manually assigned. Any fixture whose QLC+ definition includes the relevant channel types will match automatically.
Spatial Attributes
The get_attribute node (in src-tauri/src/node_graph/nodes/selection.rs) extracts per-fixture scalar values from a Selection and outputs them as a Signal with N = fixture count, T = 1, C = 1.
| Attribute | Description |
|---|---|
index | Integer order within the selection (0, 1, 2, ...) |
normalized_index | Order normalized to 0.0-1.0 range |
pos_x | Absolute global X position (meters) |
pos_y | Absolute global Y position (meters) |
pos_z | Absolute global Z position (meters) |
rel_x | X position relative to selection bounding box (0.0-1.0) |
rel_y | Y position relative to selection bounding box (0.0-1.0) |
rel_z | Z position relative to selection bounding box (0.0-1.0) |
rel_major_span | Position along the axis with largest physical range |
rel_major_count | Position along the axis with most distinct fixture positions |
circle_radius | Distance from the selection's centroid |
angular_position | Angle on fitted circle via PCA + RANSAC (0.0-1.0) |
angular_index | Index-based angular position: fixtures sorted by angle, then assigned equal spacing (0/n, 1/n, 2/n, ...) |
rel_major_span vs rel_major_count
The "span" variant picks the axis with the largest physical extent (useful for linear arrangements where fixtures are spread unevenly). The "count" variant picks the axis with the most distinct position values (useful when fixtures are evenly spaced but the physical range is similar across axes).
Circle Fitting Algorithm
File: src-tauri/src/node_graph/circle_fit.rs
For detecting circular fixture arrangements in arbitrary 3D space (used by angular_position and angular_index attributes):
Step 1: Centroid
Compute the mean position of all input points.
Step 2: PCA Plane Fitting
Build the 3x3 covariance matrix of centered points. Extract the two dominant eigenvectors via power iteration with matrix deflation. These two vectors define the best-fit 2D plane containing the points.
Step 3: Project to 2D
Map each 3D point onto the fitted plane using dot products with the two basis vectors. This reduces the problem from 3D circle fitting to 2D.
Step 4: RANSAC Circle Fit
Run 100 iterations of random 3-point sampling:
- For each sample of 3 points, compute the circumcenter via the analytic formula
- Count inliers (points within 2.5 distance units of the circle)
- Keep the fit with the most inliers
- Early exit if 90% of points are inliers
Step 5: Kasa Refinement
Apply the Kasa algebraic circle fit on inliers, solving the normal equations via Cramer's rule (3x3 determinant). Recompute inliers with the refined circle.
Step 6: Angular Positions
Compute atan2(v - center_v, u - center_u) for each point's projected 2D coordinates, normalized from [-pi, pi] to [0, 1].
The algorithm is deterministic (fixed seed for the pseudo-random generator) so results are reproducible across graph executions.