Selection System
Tag expressions, 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.
Selection Expression Parser
File: src-tauri/src/services/groups.rs
Selection expressions support boolean algebra over fixture group names. Each token in the expression is matched against the snake_case names of groups in the current venue. A fixture matches a token if it belongs to a group with that name.
all -- all fixtures in the venue
front_wash -- fixtures in the "front_wash" group
front_wash & left_truss -- fixtures in both groups
left_truss | right_truss -- fixtures in either group
floor_ring & ~dj_booth -- floor ring fixtures not in the DJ booth group
strobes > front_wash -- prefer strobes, fall back to front washOperators
| 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 group inventories. For example, strobes > front_wash prefers strobes but falls back to front wash if no strobes group exists in the venue.
How Selection Reaches the Graph
Fixture selection is not a node in the pattern graph. Instead, every pattern has a Selection argument -- a selection expression that is set when the pattern is placed on a timeline as an annotation. The resolved fixtures are passed into the graph's execution context and automatically available to Apply nodes and Get Attribute nodes.
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 | Integer index around fitted circle: fixtures sorted by angle, then assigned 0, 1, 2, ... |
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.