1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! The inspector of a chain's network output: where in the universes the
//! chain starts.
use crate::fixture_editor::FixtureWindow;
use crate::fixture_editor::state::PatchEdit;
use egui::{Color32, Grid, Ui};
use gled_model::patching::fixture::{
CHANNELS_PER_LAMP, FixtureId, LAMPS_PER_UNIVERSE, MAX_UNIVERSE,
};
use gled_model::patching::topology::Port;
/// What a chain's network output reads as: the universe it starts on, and —
/// when the chain is pinned somewhere other than the start of that universe —
/// the DMX address too.
pub(crate) fn port_label(port: Port) -> String {
format!("{}.{:03}", port.universe, port.channel + 1)
}
impl FixtureWindow {
/// The chain's network output: which universe it starts on, and — when the
/// chain does not start at the beginning of that universe — where in it.
pub(super) fn show_port_inspector(
&mut self,
ui: &mut Ui,
edit: &mut PatchEdit<'_>,
head: FixtureId,
) {
let name = edit
.value
.fixture(head)
.map(|fixture| fixture.name.clone())
.unwrap_or_default();
let Some(port) = edit.value.topology.ports.get(&head).copied() else {
ui.label("No network output");
return;
};
ui.heading("Network output");
ui.label(
egui::RichText::new(format!("Chain starting at {name}")).color(Color32::from_gray(160)),
);
Grid::new("port inspector")
.num_columns(2)
.spacing([8.0, 4.0])
.show(ui, |ui| {
ui.label("Universe");
let mut universe = port.universe;
// a chain past the highest universe would get no address
let response = ui
.add(egui::DragValue::new(&mut universe).range(0..=MAX_UNIVERSE))
.on_hover_text("Up to the highest Art-Net universe");
edit.tracked(&response, |patch| {
if let Some(port) = patch.topology.ports.get_mut(&head) {
port.universe = universe;
}
});
ui.end_row();
// The field steps in lamps, shown as the DMX address each
// starts on: 1, 4, 7, …
ui.label("DMX start");
let mut lamp = port.channel / CHANNELS_PER_LAMP;
let channel = port.channel;
let response = ui
.horizontal(|ui| {
let response = ui
.add(
egui::DragValue::new(&mut lamp)
.range(0..=LAMPS_PER_UNIVERSE - 1)
.speed(0.25)
.custom_formatter(move |lamp, _| {
shown_dmx_start(channel, lamp as u16)
})
.custom_parser(typed_dmx_start),
)
.on_hover_text(
"First channel of the chain, inside its universe. Pixels \
are RGB triples, so a chain starts on 1, 4, 7, …",
);
if !channel.is_multiple_of(CHANNELS_PER_LAMP) {
ui.colored_label(super::WARNING, "⚠").on_hover_text(
"Not the start of an RGB triple: each pixel mixes its \
colour with its neighbour's. Any change snaps it to one.",
);
}
response
})
.inner;
edit.tracked(&response, |patch| {
if let Some(port) = patch.topology.ports.get_mut(&head) {
port.channel = lamp * CHANNELS_PER_LAMP;
}
});
ui.end_row();
});
ui.label(
egui::RichText::new("A chain that runs past the universe continues in the next one.")
.small()
.color(Color32::from_gray(140)),
);
}
}
/// The DMX address lamp `lamp` starts on, counted from one. A chain whose
/// start `channel` lies inside that lamp — from a file written before starts
/// were held to whole lamps — shows where it really starts, until it is
/// changed.
fn shown_dmx_start(channel: u16, lamp: u16) -> String {
let start = if channel / CHANNELS_PER_LAMP == lamp {
channel
} else {
lamp * CHANNELS_PER_LAMP
};
(start + 1).to_string()
}
/// The lamp a typed DMX address falls in: the triple holding it.
fn typed_dmx_start(text: &str) -> Option<f64> {
let address = text.trim().parse::<f64>().ok()?;
Some(
((address - 1.0) / f64::from(CHANNELS_PER_LAMP))
.floor()
.max(0.0),
)
}
#[cfg(test)]
mod tests {
use super::*;
/// The network output carries the addressing, so it reads as the full
/// address the chain starts at, universe and DMX start together.
#[test]
fn a_port_reads_as_its_universe_and_start() {
assert_eq!(
port_label(Port {
universe: 3,
channel: 0
}),
"3.001"
);
assert_eq!(
port_label(Port {
universe: 3,
channel: 49
}),
"3.050"
);
assert_eq!(
port_label(Port {
universe: 0,
channel: 1
}),
"0.002"
);
}
/// A chain starts on an RGB triple: 1, 4, 7, … A typed address inside a
/// triple lands on its start.
#[test]
fn a_dmx_start_snaps_to_whole_lamps() {
let typed = |text| typed_dmx_start(text).map(|lamp| lamp as u16);
assert_eq!(typed("1"), Some(0));
assert_eq!(typed("6"), Some(1));
assert_eq!(typed(" 508"), Some(169));
assert_eq!(typed("lots"), None);
assert_eq!(shown_dmx_start(3, 1), "4");
assert_eq!(shown_dmx_start(3, 2), "7");
}
/// A start from an older file that lies inside a triple is shown as it
/// is, not as the triple it would snap to.
#[test]
fn a_start_inside_a_triple_shows_where_it_is() {
assert_eq!(shown_dmx_start(5, 1), "6");
assert_eq!(shown_dmx_start(5, 2), "7");
}
}