1use std::collections::HashMap;
4
5use bloq_utils::RGBA;
6use glam::IVec3;
7
8use crate::{BlockGraph, ModuleOrientation};
9
10#[derive(Debug, Clone)]
12pub struct ModuleViewModule {
13 pub name: String,
15 pub instance_count: usize,
17 pub block_count: usize,
19}
20
21#[derive(Debug, Clone)]
26pub struct ModuleView {
27 modules: Vec<ModuleViewModule>,
28 block_modules: HashMap<IVec3, usize>,
29}
30
31impl ModuleView {
32 pub fn from_graph(program: &BlockGraph, graph: &BlockGraph) -> Option<Self> {
42 let mut modules = Vec::new();
43 let mut module_indices = HashMap::new();
44 let mut block_modules = HashMap::new();
45 collect_module_blocks(
46 program,
47 program.root(),
48 ModuleOrientation::IDENTITY,
49 IVec3::ZERO,
50 true,
51 graph,
52 &mut modules,
53 &mut module_indices,
54 &mut block_modules,
55 );
56
57 let mut block_counts = vec![0; modules.len()];
58 for index in block_modules.values() {
59 block_counts[*index] += 1;
60 }
61 let mut remap = vec![None; modules.len()];
62 let mut visible = Vec::new();
63 for (old_index, mut module) in modules.into_iter().enumerate() {
64 let block_count = block_counts[old_index];
65 if block_count == 0 {
66 continue;
67 }
68 module.block_count = block_count;
69 remap[old_index] = Some(visible.len());
70 visible.push(module);
71 }
72 block_modules.retain(|_, index| {
73 let Some(next) = remap[*index] else {
74 return false;
75 };
76 *index = next;
77 true
78 });
79
80 (!visible.is_empty()).then_some(Self {
81 modules: visible,
82 block_modules,
83 })
84 }
85
86 pub fn modules(&self) -> &[ModuleViewModule] {
88 &self.modules
89 }
90
91 pub fn module_for_block(&self, position: IVec3) -> Option<usize> {
93 self.block_modules.get(&position).copied()
94 }
95
96 pub fn module_for_pipe(&self, graph: &BlockGraph, a: IVec3, b: IVec3) -> Option<usize> {
100 let a = self.module_for_block(graph.get_endpoint_block(a)?.pos())?;
101 let b = self.module_for_block(graph.get_endpoint_block(b)?.pos())?;
102 (a == b).then_some(a)
103 }
104}
105
106#[allow(
107 clippy::too_many_arguments,
108 reason = "shared editor traversal preserves ownership ordering"
109)]
110fn collect_module_blocks(
111 program: &BlockGraph,
112 module: &BlockGraph,
113 orientation: ModuleOrientation,
114 translation: IVec3,
115 is_root: bool,
116 graph: &BlockGraph,
117 modules: &mut Vec<ModuleViewModule>,
118 module_indices: &mut HashMap<String, usize>,
119 block_modules: &mut HashMap<IVec3, usize>,
120) {
121 if !is_root {
122 let index = *module_indices
123 .entry(module.name.clone())
124 .or_insert_with(|| {
125 modules.push(ModuleViewModule {
126 name: module.name.clone(),
127 instance_count: 0,
128 block_count: 0,
129 });
130 modules.len() - 1
131 });
132 modules[index].instance_count += 1;
133
134 for block in module.local_body().blocks() {
135 if module
136 .interface
137 .quantum_ports
138 .iter()
139 .any(|port| port.position == block.pos())
140 {
141 continue;
142 }
143 let Some(position) = orientation
144 .try_transform_position(block.pos(), translation)
145 .ok()
146 else {
147 continue;
148 };
149 if graph.has_block_at(position) {
150 block_modules.insert(position, index);
151 }
152 }
153 }
154
155 for instance in &module.instances {
156 let Some(translation) = orientation
157 .try_transform_position(instance.translation, translation)
158 .ok()
159 else {
160 continue;
161 };
162 collect_module_blocks(
163 program,
164 program
165 .module(&instance.definition)
166 .expect("validated instance names a module"),
167 orientation.then(instance.rotation),
168 translation,
169 false,
170 graph,
171 modules,
172 module_indices,
173 block_modules,
174 );
175 }
176}
177
178pub fn module_color(index: usize) -> RGBA {
183 const COLORS: [RGBA; 10] = [
184 RGBA::from_hex(0x4477AA, 255),
185 RGBA::from_hex(0xEE6677, 255),
186 RGBA::from_hex(0x228833, 255),
187 RGBA::from_hex(0xCCBB44, 255),
188 RGBA::from_hex(0x66CCEE, 255),
189 RGBA::from_hex(0xAA3377, 255),
190 RGBA::from_hex(0xEE7733, 255),
191 RGBA::from_hex(0x332288, 255),
192 RGBA::from_hex(0x44AA99, 255),
193 RGBA::from_hex(0x999999, 255),
194 ];
195 if let Some(color) = COLORS.get(index) {
196 return *color;
197 }
198 let hue = ((index as f32 * 0.618_034).fract() + 1.0).fract() * 6.0;
200 let fraction = hue - hue.floor();
201 let value = 0.82;
202 let saturation = 0.72;
203 let low = value * (1.0 - saturation);
204 let falling = value * (1.0 - fraction * saturation);
205 let rising = value * (1.0 - (1.0 - fraction) * saturation);
206 let rgb = match hue.floor() as u8 {
207 0 => [value, rising, low],
208 1 => [falling, value, low],
209 2 => [low, value, rising],
210 3 => [low, falling, value],
211 4 => [rising, low, value],
212 _ => [value, low, falling],
213 };
214 let [r, g, b] = rgb.map(|linear: f32| {
215 let gamma = if linear <= 0.003_130_8 {
216 3294.6 * linear
217 } else {
218 269.025 * linear.powf(1.0 / 2.4) - 14.025
219 };
220 (gamma + 0.5) as u8
221 });
222 RGBA { r, g, b, a: 255 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::GalleryItem;
229 use std::collections::HashSet;
230
231 #[test]
232 fn reused_definitions_keep_one_ownership_group_and_seams_uncolored() {
233 let program = GalleryItem::ThreeBitAdder.build();
234 let graph = program.flatten().unwrap();
235 let view = ModuleView::from_graph(&program, &graph).unwrap();
236 assert_eq!(
237 view.modules()
238 .iter()
239 .map(|module| (module.name.as_str(), module.instance_count))
240 .collect::<Vec<_>>(),
241 [
242 ("InjectedAnd", 3),
243 ("HeadMaj", 1),
244 ("HeadUmaParked", 1),
245 ("BulkMaj", 1),
246 ("BulkUmaParked", 1),
247 ("TailParity", 1)
248 ]
249 );
250 let mut counts = vec![0; view.modules().len()];
251 for block in graph.blocks() {
252 if let Some(index) = view.module_for_block(block.pos()) {
253 counts[index] += 1;
254 } else {
255 assert!(program.root().local_body().has_block_at(block.pos()));
256 }
257 }
258 assert_eq!(
259 counts,
260 view.modules()
261 .iter()
262 .map(|module| module.block_count)
263 .collect::<Vec<_>>()
264 );
265 let mut seams = 0;
266 for (a, b, lhs, rhs, _) in graph.pipe_endpoints_with_blocks() {
267 let left = view.module_for_block(lhs.pos());
268 let right = view.module_for_block(rhs.pos());
269 let expected = left.filter(|_| left == right);
270 assert_eq!(view.module_for_pipe(&graph, a, b), expected);
271 assert_eq!(view.module_for_pipe(&graph, b, a), expected);
272 seams += usize::from(expected.is_none());
273 }
274 assert!(seams > 0);
275 for block in program.root().local_body().blocks() {
276 assert_eq!(view.module_for_block(block.pos()), None);
277 }
278 }
279
280 #[test]
281 fn root_only_and_empty_graphs_have_no_module_highlights() {
282 let program = GalleryItem::OneDYoked.build();
283 let graph = program.flatten().unwrap();
284 assert!(ModuleView::from_graph(&program, &graph).is_none());
285 assert!(ModuleView::from_graph(&BlockGraph::new(), &BlockGraph::new()).is_none());
286 }
287
288 #[test]
289 fn first_thirty_two_module_colors_are_distinct() {
290 assert_eq!((0..32).map(module_color).collect::<HashSet<_>>().len(), 32);
291 }
292}