1use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::ops::Range;
5use std::sync::Arc;
6
7use base64::{Engine as _, prelude::BASE64_STANDARD};
8use bloq_utils::{Basis, Pauli, RGBA, UDirection};
9use glam::{IVec3, Vec3, vec3};
10use gltf_json::{
11 Accessor, Asset, Buffer, Material, Root, Value,
12 accessor::{ComponentType, GenericComponentType, Type},
13 buffer::{Target, View},
14 extensions::material::{Material as MaterialExtensions, Unlit},
15 material::{AlphaMode, PbrBaseColorFactor, PbrMetallicRoughness, StrengthFactor},
16 mesh::{Mode, Primitive, Semantic},
17 validation::Checked::Valid,
18};
19
20use crate::{
21 Block, BlockGraphError, BlockKind, Direction, PatchRotationKind, Pipe, Stabilizer,
22 StabilizerGenerator, StabilizerRowKind, WalkingKind, ZXGraph, graph::BlockGraph,
23};
24
25#[derive(Debug, Clone)]
27pub struct TextLabel {
28 pub position: Vec3,
30 pub text: String,
32 pub color: RGBA,
34}
35
36#[derive(Debug, Clone, Default)]
39pub struct GltfData {
40 pub triangles: BTreeMap<RGBA, Vec<[Vec3; 3]>>,
42 pub lines: BTreeMap<RGBA, Vec<[Vec3; 2]>>,
44 pub texts: Vec<TextLabel>,
46 pub unlit_colors: BTreeSet<RGBA>,
48}
49
50pub(crate) type ModuleFaceRanges = BTreeMap<RGBA, Vec<(Option<usize>, Range<usize>)>>;
52type ModuleMaterial = (usize, usize, [f32; 4], [f32; 4], &'static str);
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum GltfFaceSelector {
57 All(Direction),
59 Block {
61 position: IVec3,
63 face: Direction,
65 },
66 Pipe {
68 u: IVec3,
70 v: IVec3,
72 face: Direction,
74 },
75}
76
77impl GltfFaceSelector {
78 fn for_block(self, position: IVec3) -> Option<Direction> {
79 match self {
80 Self::All(face) => Some(face),
81 Self::Block { position: p, face } if p == position => Some(face),
82 _ => None,
83 }
84 }
85
86 fn for_pipe(self, u: IVec3, v: IVec3) -> Option<Direction> {
87 match self {
88 Self::All(face) => Some(face),
89 Self::Pipe { u: a, v: b, face } if (a == u && b == v) || (a == v && b == u) => {
90 Some(face)
91 }
92 _ => None,
93 }
94 }
95}
96
97const VIEWER_BACKGROUND: &str = "#D8E0E6";
99
100pub const EDITOR_AMBIENT_BRIGHTNESS: f32 = 1065.0;
102pub const EDITOR_DIRECTIONAL_ILLUMINANCE: f32 = 1055.0;
104
105fn editor_face_brightness(triangle: &[Vec3; 3]) -> f32 {
106 let [a, b, c] = *triangle;
107 let normal = (b - a).cross(c - a).normalize_or_zero();
108 let exposure = 2.0_f32.powf(-9.7) / 1.2;
109 exposure
110 * (0.4524 * EDITOR_AMBIENT_BRIGHTNESS
111 + 0.3255 * EDITOR_DIRECTIONAL_ILLUMINANCE * normal.dot(Vec3::ONE.normalize()).max(0.0))
112}
113
114fn linear_palette(color: RGBA) -> [f32; 4] {
115 let mut channels = color.to_f32_array();
116 for channel in &mut channels[..3] {
117 *channel = if *channel <= 0.04045 {
118 *channel / 12.92
119 } else {
120 ((*channel + 0.055) / 1.055).powf(2.4)
121 };
122 }
123 channels
124}
125
126pub(crate) const POPPED_X_FACE_COLOR: RGBA = RGBA::from_hex(0xA61E2D, 255);
127pub(crate) const POPPED_Y_FACE_COLOR: RGBA = RGBA::from_hex(0x188038, 255);
128pub(crate) const POPPED_Z_FACE_COLOR: RGBA = RGBA::from_hex(0x174EA6, 255);
129
130const DEFAULT_METALLIC_FACTOR: f32 = 0.0;
133const DEFAULT_ROUGHNESS_FACTOR: f32 = 1.0;
134
135#[derive(Debug, Clone, Copy)]
136enum FaceStyle {
137 Fill,
138 Outline,
139 FillAndOutline,
140}
141
142impl FaceStyle {
143 fn has_fill(self) -> bool {
144 matches!(self, FaceStyle::Fill | FaceStyle::FillAndOutline)
145 }
146
147 fn has_outline(self) -> bool {
148 matches!(self, FaceStyle::Outline | FaceStyle::FillAndOutline)
149 }
150}
151
152impl GltfData {
153 fn color_module_faces(&mut self, color: RGBA) {
154 let triangles: Vec<_> = std::mem::take(&mut self.triangles)
155 .into_values()
156 .flatten()
157 .collect();
158 if !triangles.is_empty() {
159 self.triangles.insert(color, triangles);
160 self.unlit_colors.insert(color);
161 }
162 }
163
164 pub fn scale_face_opacity(&self, scale: f32) -> GltfData {
167 let apply_scale = |color: RGBA| RGBA {
168 r: color.r,
169 g: color.g,
170 b: color.b,
171 a: ((color.a as f32) * scale) as u8,
172 };
173
174 let mut triangles: BTreeMap<RGBA, Vec<[Vec3; 3]>> = BTreeMap::new();
178 for (color, tris) in &self.triangles {
179 triangles
180 .entry(apply_scale(*color))
181 .or_default()
182 .extend(tris.iter().copied());
183 }
184
185 GltfData {
186 triangles,
187 lines: self.lines.clone(),
188 texts: self.texts.clone(),
189 unlit_colors: self.unlit_colors.iter().copied().map(apply_scale).collect(),
190 }
191 }
192
193 pub fn pop_faces_at_directions(mut self, directions: &[Direction]) -> GltfData {
197 if directions.is_empty() {
198 return self;
199 }
200
201 self.triangles.retain(|_, triangles| {
202 triangles.retain(|[a, b, c]| {
203 let normal = (*b - *a).cross(*c - *a).normalize_or_zero();
204 !directions
205 .iter()
206 .any(|direction| normal.dot(direction.to_vec3()) > 0.999)
207 });
208 !triangles.is_empty()
209 });
210 self
211 }
212
213 pub fn extend(&mut self, other: GltfData) {
215 self.unlit_colors.extend(other.unlit_colors);
216 for (color, triangles) in other.triangles {
217 self.add_triangles(color, triangles);
218 }
219 for (color, lines) in other.lines {
220 self.add_line(color, lines);
221 }
222 self.texts.extend(other.texts);
223 }
224
225 fn add_triangles(&mut self, color: RGBA, triangles: impl IntoIterator<Item = [Vec3; 3]>) {
226 self.triangles.entry(color).or_default().extend(triangles);
227 }
228
229 fn add_line(&mut self, color: RGBA, lines: impl IntoIterator<Item = [Vec3; 2]>) {
230 self.lines.entry(color).or_default().extend(lines);
231 }
232
233 fn add_face(&mut self, center: Vec3, side1: Vec3, side2: Vec3, rgba: RGBA, style: FaceStyle) {
234 let origin = center - (side1 + side2) * 0.5;
235 if style.has_fill() {
236 self.add_triangles(
237 rgba,
238 [
239 [origin, origin + side1, origin + side2],
240 [origin + side1 + side2, origin + side2, origin + side1],
241 ],
242 );
243 }
244 if style.has_outline() {
245 self.add_line(
246 RGBA::LINE_BLACK,
247 [
248 [origin, origin + side1],
249 [origin + side1 + side2, origin + side1],
250 [origin, origin + side2],
251 [origin + side1 + side2, origin + side2],
252 ],
253 );
254 }
255 }
256
257 fn add_cube(&mut self, center: Vec3, sides: Vec3, rgba: RGBA, style: FaceStyle) {
258 let [x, y, z] = center.to_array();
259 let [sx, sy, sz] = sides.to_array();
260 let mut f = |c, s1, s2| {
261 self.add_face(c, s1, s2, rgba, style);
262 };
263 f(
264 vec3(x, y, z - sz * 0.5),
265 vec3(0.0, sy, 0.0),
266 vec3(sx, 0.0, 0.0),
267 );
268 f(
269 vec3(x, y, z + sz * 0.5),
270 vec3(sx, 0.0, 0.0),
271 vec3(0.0, sy, 0.0),
272 );
273 f(
274 vec3(x, y - sy * 0.5, z),
275 vec3(sx, 0.0, 0.0),
276 vec3(0.0, 0.0, sz),
277 );
278 f(
279 vec3(x, y + sy * 0.5, z),
280 vec3(0.0, 0.0, sz),
281 vec3(sx, 0.0, 0.0),
282 );
283 f(
284 vec3(x - sx * 0.5, y, z),
285 vec3(0.0, 0.0, sz),
286 vec3(0.0, sy, 0.0),
287 );
288 f(
289 vec3(x + sx * 0.5, y, z),
290 vec3(0.0, sy, 0.0),
291 vec3(0.0, 0.0, sz),
292 );
293 }
294
295 fn add_prism_face(&mut self, corners: [Vec3; 4], rgba: RGBA, style: FaceStyle) {
296 if style.has_fill() {
297 self.add_triangles(
298 rgba,
299 [
300 [corners[0], corners[1], corners[2]],
301 [corners[2], corners[3], corners[0]],
302 ],
303 );
304 }
305 if style.has_outline() {
306 self.add_line(
307 RGBA::LINE_BLACK,
308 [
309 [corners[0], corners[1]],
310 [corners[1], corners[2]],
311 [corners[2], corners[3]],
312 [corners[3], corners[0]],
313 ],
314 );
315 }
316 }
317
318 fn add_double_color_cube(&mut self, center: Vec3, sides: Vec3, rgba1: RGBA, rgba2: RGBA) {
319 self.add_cube(center, sides, RGBA::LINE_BLACK, FaceStyle::Outline);
320 let [x, y, z] = center.to_array();
321 let [sx, sy, sz] = (0.5 * sides).to_array();
322 self.add_triangles(
323 rgba1,
324 [
325 [
326 vec3(x - sx, y - sy, z - sz),
327 vec3(x - sx, y - sy, z + sz),
328 vec3(x - sx, y + sy, z + sz),
329 ],
330 [
331 vec3(x - sx, y - sy, z - sz),
332 vec3(x + sx, y - sy, z - sz),
333 vec3(x + sx, y - sy, z + sz),
334 ],
335 [
336 vec3(x - sx, y - sy, z + sz),
337 vec3(x + sx, y - sy, z + sz),
338 vec3(x + sx, y + sy, z + sz),
339 ],
340 [
341 vec3(x + sx, y - sy, z - sz),
342 vec3(x + sx, y + sy, z - sz),
343 vec3(x + sx, y + sy, z + sz),
344 ],
345 [
346 vec3(x - sx, y + sy, z - sz),
347 vec3(x - sx, y + sy, z + sz),
348 vec3(x + sx, y + sy, z + sz),
349 ],
350 [
351 vec3(x - sx, y - sy, z - sz),
352 vec3(x - sx, y + sy, z - sz),
353 vec3(x + sx, y + sy, z - sz),
354 ],
355 ],
356 );
357 self.add_triangles(
358 rgba2,
359 [
360 [
361 vec3(x - sx, y + sy, z + sz),
362 vec3(x - sx, y + sy, z - sz),
363 vec3(x - sx, y - sy, z - sz),
364 ],
365 [
366 vec3(x + sx, y - sy, z + sz),
367 vec3(x - sx, y - sy, z + sz),
368 vec3(x - sx, y - sy, z - sz),
369 ],
370 [
371 vec3(x + sx, y + sy, z + sz),
372 vec3(x - sx, y + sy, z + sz),
373 vec3(x - sx, y - sy, z + sz),
374 ],
375 [
376 vec3(x + sx, y + sy, z + sz),
377 vec3(x + sx, y - sy, z + sz),
378 vec3(x + sx, y - sy, z - sz),
379 ],
380 [
381 vec3(x + sx, y + sy, z + sz),
382 vec3(x + sx, y + sy, z - sz),
383 vec3(x - sx, y + sy, z - sz),
384 ],
385 [
386 vec3(x + sx, y + sy, z - sz),
387 vec3(x + sx, y - sy, z - sz),
388 vec3(x - sx, y - sy, z - sz),
389 ],
390 ],
391 );
392 }
393
394 pub fn swap_yz(self) -> GltfData {
399 let mut transformed = GltfData {
400 unlit_colors: self.unlit_colors,
401 ..Default::default()
402 };
403 let swap = |v: Vec3| vec3(v.x, v.z, v.y);
404
405 for (color, triangles) in self.triangles {
406 let transformed_triangles = triangles
407 .into_iter()
408 .map(|[a, b, c]| [swap(a), swap(c), swap(b)])
409 .collect();
410 transformed.triangles.insert(color, transformed_triangles);
411 }
412
413 for (color, lines) in self.lines {
414 let transformed_lines = lines.into_iter().map(|[a, b]| [swap(a), swap(b)]).collect();
415 transformed.lines.insert(color, transformed_lines);
416 }
417 transformed.texts = self
418 .texts
419 .into_iter()
420 .map(|label| TextLabel {
421 position: swap(label.position),
422 ..label
423 })
424 .collect();
425
426 transformed
427 }
428
429 fn to_buffer_bytes(&self) -> Vec<u8> {
430 let positions = self
431 .triangles
432 .values()
433 .flatten()
434 .flatten()
435 .chain(self.lines.values().flatten().flatten())
436 .flat_map(Vec3::to_array);
437 let colors = self.triangles.iter().flat_map(|(color, triangles)| {
442 triangles.iter().flat_map(|triangle| {
443 let brightness = if self.unlit_colors.contains(color) {
444 1.0
445 } else {
446 editor_face_brightness(triangle)
447 };
448 [[brightness, brightness, brightness, 1.0]; 3]
449 .into_iter()
450 .flatten()
451 })
452 });
453 positions.chain(colors).flat_map(f32::to_le_bytes).collect()
454 }
455
456 pub fn map_points(mut self, f: impl Fn(Vec3) -> Vec3) -> GltfData {
462 for triangles in self.triangles.values_mut() {
463 for triangle in triangles {
464 for point in triangle {
465 *point = f(*point);
466 }
467 }
468 }
469
470 for lines in self.lines.values_mut() {
471 for line in lines {
472 for point in line {
473 *point = f(*point);
474 }
475 }
476 }
477
478 for label in &mut self.texts {
479 label.position = f(label.position);
480 }
481
482 self
483 }
484
485 fn to_model(&self) -> Root {
487 let mut root = gltf_json::Root {
488 asset: Asset {
489 version: "2.0".to_string(),
490 copyright: Some("Generated by `bloq_graph` crate".to_string()),
491 ..Default::default()
492 },
493 ..Default::default()
494 };
495 root.extensions_used.push("KHR_materials_unlit".to_string());
496 root.extensions_required
497 .push("KHR_materials_unlit".to_string());
498
499 let mut create_material = |color: RGBA, double_sided: bool| {
500 root.push(Material {
501 pbr_metallic_roughness: PbrMetallicRoughness {
502 base_color_factor: PbrBaseColorFactor(linear_palette(color)),
504 metallic_factor: StrengthFactor(DEFAULT_METALLIC_FACTOR),
505 roughness_factor: StrengthFactor(DEFAULT_ROUGHNESS_FACTOR),
506 ..Default::default()
507 },
508 double_sided,
509 extensions: Some(MaterialExtensions {
510 unlit: Some(Unlit {}),
511 }),
512 alpha_mode: Valid(if color.a < 255 {
513 AlphaMode::Blend
514 } else {
515 AlphaMode::Opaque
516 }),
517 ..Default::default()
518 })
519 };
520
521 let tri_materials: BTreeMap<_, _> = self
522 .triangles
523 .keys()
524 .map(|&c| (c, create_material(c, true)))
525 .collect();
526 let line_materials: BTreeMap<_, _> = self
527 .lines
528 .keys()
529 .map(|&c| (c, create_material(c, false)))
530 .collect();
531
532 let buffer_bytes = self.to_buffer_bytes();
533 let buffer_index = root.push(Buffer {
534 uri: Some(format!(
535 "data:application/octet-stream;base64,{}",
536 BASE64_STANDARD.encode(&buffer_bytes)
537 )),
538 byte_length: buffer_bytes.len().into(),
539 extensions: None,
540 extras: Default::default(),
541 name: None,
542 });
543
544 let mut byte_offset = 0;
545 let mut color_byte_offset = (self.triangles.values().map(Vec::len).sum::<usize>() * 3
546 + self.lines.values().map(Vec::len).sum::<usize>() * 2)
547 * 3
548 * 4;
549 let mut primitives = Vec::new();
550
551 let mut add_primitive = |count: usize,
552 min: Vec3,
553 max: Vec3,
554 material_index: gltf_json::Index<Material>,
555 mode: Mode| {
556 let byte_length = count * 3 * 4;
557 let view_index = root.push(View {
558 buffer: buffer_index,
559 byte_length: byte_length.into(),
560 byte_offset: Some(byte_offset.into()),
561 byte_stride: None,
562 target: Some(Valid(Target::ArrayBuffer)),
563 extensions: None,
564 extras: Default::default(),
565 name: None,
566 });
567
568 let accessor_index = root.push(Accessor {
569 buffer_view: Some(view_index),
570 byte_offset: Some(0usize.into()),
571 count: count.into(),
572 component_type: Valid(GenericComponentType(ComponentType::F32)),
573 type_: Valid(Type::Vec3),
574 normalized: false,
575 min: Some(Value::from(Vec::from(min.to_array()))),
576 max: Some(Value::from(Vec::from(max.to_array()))),
577 sparse: None,
578 extensions: None,
579 extras: Default::default(),
580 name: None,
581 });
582
583 let mut attributes = BTreeMap::new();
584 attributes.insert(Valid(Semantic::Positions), accessor_index);
585 if mode == Mode::Triangles {
586 let color_byte_length = count * 4 * 4;
587 let color_view = root.push(View {
588 buffer: buffer_index,
589 byte_length: color_byte_length.into(),
590 byte_offset: Some(color_byte_offset.into()),
591 target: Some(Valid(Target::ArrayBuffer)),
592 byte_stride: None,
593 extensions: None,
594 extras: Default::default(),
595 name: None,
596 });
597 let color_accessor = root.push(Accessor {
598 buffer_view: Some(color_view),
599 count: count.into(),
600 component_type: Valid(GenericComponentType(ComponentType::F32)),
601 type_: Valid(Type::Vec4),
602 byte_offset: Some(0usize.into()),
603 normalized: false,
604 min: None,
605 max: None,
606 sparse: None,
607 extensions: None,
608 extras: Default::default(),
609 name: None,
610 });
611 attributes.insert(Valid(Semantic::Colors(0)), color_accessor);
612 color_byte_offset += color_byte_length;
613 }
614 primitives.push(Primitive {
615 attributes,
616 indices: None,
617 material: Some(material_index),
618 mode: Valid(mode),
619 targets: None,
620 extensions: None,
621 extras: Default::default(),
622 });
623
624 byte_offset += byte_length;
625 };
626
627 for (color, triangles) in &self.triangles {
628 let count = triangles.len() * 3;
629 let (min, max) = bounds(triangles.iter().flatten());
630 add_primitive(count, min, max, tri_materials[color], Mode::Triangles);
631 }
632
633 for (color, lines) in &self.lines {
634 let count = lines.len() * 2;
635 let (min, max) = bounds(lines.iter().flatten());
636 add_primitive(count, min, max, line_materials[color], Mode::Lines);
637 }
638
639 let mesh_index = root.push(gltf_json::Mesh {
640 primitives,
641 weights: None,
642 extensions: None,
643 extras: Default::default(),
644 name: None,
645 });
646 let node_index = root.push(gltf_json::Node {
647 mesh: Some(mesh_index),
648 ..Default::default()
649 });
650 root.push(gltf_json::Scene {
651 nodes: vec![node_index],
652 extensions: None,
653 extras: Default::default(),
654 name: None,
655 });
656 root
657 }
658
659 fn module_model(&self, ranges: &ModuleFaceRanges) -> (Root, Vec<ModuleMaterial>) {
660 let mut model = self.to_model();
661 let original_primitives = std::mem::take(&mut model.meshes[0].primitives);
662 let mut materials = BTreeMap::new();
663 let mut toggles = Vec::new();
664 for ((color, triangles), primitive) in self.triangles.iter().zip(&original_primitives) {
665 for (owner, range) in &ranges[color] {
666 let mut part = primitive.clone();
667 for (semantic, bytes_per_vertex) in
669 [(Semantic::Positions, 12), (Semantic::Colors(0), 16)]
670 {
671 let mut accessor =
672 model.accessors[part.attributes[&Valid(semantic.clone())].value()].clone();
673 accessor.byte_offset = Some((range.start * 3 * bytes_per_vertex).into());
674 accessor.count = (range.len() * 3).into();
675 if semantic == Semantic::Positions {
676 let (min, max) = bounds(triangles[range.clone()].iter().flatten());
677 accessor.min = Some(Value::from(Vec::from(min.to_array())));
678 accessor.max = Some(Value::from(Vec::from(max.to_array())));
679 }
680 part.attributes
681 .insert(Valid(semantic), model.push(accessor));
682 }
683 if let Some(owner) = owner {
684 let material = *materials.entry((*owner, *color)).or_insert_with(|| {
685 let mut material = model.materials[primitive
686 .material
687 .expect("triangle primitive has a material")
688 .value()]
689 .clone();
690 let original = material.pbr_metallic_roughness.base_color_factor.0;
691 let highlight = linear_palette(crate::module_color(*owner));
692 let alpha = if color.a < 255 { "BLEND" } else { "OPAQUE" };
693 material.pbr_metallic_roughness.base_color_factor =
694 PbrBaseColorFactor(highlight);
695 material.alpha_mode = Valid(AlphaMode::Opaque);
696 let index = model.push(material);
697 toggles.push((*owner, index.value(), original, highlight, alpha));
698 index
699 });
700 part.material = Some(material);
701 }
702 model.meshes[0].primitives.push(part);
703 }
704 }
705 model.meshes[0]
707 .primitives
708 .extend(original_primitives.into_iter().skip(self.triangles.len()));
709 (model, toggles)
710 }
711
712 pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), BlockGraphError> {
722 let path = path.as_ref();
723 let model = self.to_model();
724 let file = std::fs::File::create(path).map_err(|e| BlockGraphError::Io {
725 path: path.to_path_buf(),
726 source: Arc::new(e),
727 })?;
728 gltf_json::serialize::to_writer(file, &model)
729 .map_err(|e| BlockGraphError::GltfSerialize(Arc::new(e)))
730 }
731
732 pub fn to_html_str(&self) -> Result<String, BlockGraphError> {
743 self.to_html_str_with_legend("")
744 }
745
746 pub(crate) fn to_html_str_with_legend(&self, legend: &str) -> Result<String, BlockGraphError> {
747 Self::model_html(&self.to_model(), legend, &[])
748 }
749
750 pub(crate) fn to_module_html_str(
751 &self,
752 legend: &str,
753 ranges: &ModuleFaceRanges,
754 ) -> Result<String, BlockGraphError> {
755 let (model, toggles) = self.module_model(ranges);
756 Self::model_html(&model, legend, &toggles)
757 }
758
759 fn model_html(
760 model: &Root,
761 legend: &str,
762 toggles: &[ModuleMaterial],
763 ) -> Result<String, BlockGraphError> {
764 let bytes = gltf_json::serialize::to_vec(model)
765 .map_err(|error| BlockGraphError::GltfSerialize(Arc::new(error)))?;
766 let toggles = gltf_json::serialize::to_string(toggles)
767 .map_err(|error| BlockGraphError::GltfSerialize(Arc::new(error)))?;
768 let model_data_uri = format!(
769 "data:model/gltf+json;base64,{}",
770 BASE64_STANDARD.encode(&bytes)
771 );
772 Ok(format!(
773 r##"<!DOCTYPE html>
774<html>
775<head>
776 <meta charset="UTF-8" />
777 <meta name="viewport" content="width=device-width, initial-scale=1" />
778 <script id="viewer-runtime" type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/4.2.0/model-viewer.min.js" onerror="window.viewerLoadFailed = true"></script>
779 <style>
780 body {{ margin: 0; font: 14px system-ui, sans-serif; }}
781 model-viewer {{ width: 100vw; height: 100vh; background-color: {VIEWER_BACKGROUND}; }}
782 .controls {{ position: absolute; top: 12px; left: 12px; right: 12px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12px; color: #1f2937; pointer-events: none; }}
783 .controls button {{ pointer-events: auto; }}
784 .module-legend {{ position: absolute; top: 56px; right: 12px; max-width: 45%; max-height: calc(100vh - 150px); overflow: auto; padding: 8px; border-radius: 4px; background: #f8fafcee; color: #1f2937; font-size: 12px; }}
785 .module-legend ul {{ padding: 0; margin: 8px 0 0; list-style: none; }}
786 .module-legend li {{ margin: 6px 0; overflow-wrap: anywhere; }}
787 .module-legend label {{ cursor: pointer; }}
788 .module-legend input {{ vertical-align: middle; margin: 0 5px 0 0; }}
789 .module-legend i {{ display: inline-block; width: 10px; height: 10px; margin-right: 5px; }}
790 #axes {{ position: absolute; bottom: 12px; left: 12px; width: 100px; height: 100px; pointer-events: none; visibility: hidden; }}
791 #axes text {{ font: bold 13px system-ui, sans-serif; text-anchor: middle; dominant-baseline: central; paint-order: stroke; stroke: {VIEWER_BACKGROUND}; stroke-width: 3px; }}
792 #status {{ position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; text-align: center; color: #526070; pointer-events: none; }}
793 button {{ padding: 6px 12px; border: 1px solid #94a3b2; border-radius: 4px; background: #f8fafc; color: #1f2937; cursor: pointer; }}
794 </style>
795</head>
796<body>
797 <model-viewer
798 src="{}"
799 alt="Block Graph 3D Model"
800 camera-controls
801 camera-orbit="45deg 54.7356deg auto"
802 field-of-view="45deg"
803 max-field-of-view="45deg"
804 shadow-intensity="0"
805 tone-mapping="none"
806 exposure="1.0"
807 interaction-prompt="none"
808 touch-action="none">
809 </model-viewer>
810 <div id="status" role="status">Loading 3D graph…</div>
811 <div class="controls">
812 <button type="button" disabled aria-label="Reset graph view">Fit</button>
813 <span>Drag to orbit · Right-drag to pan · Scroll to zoom</span>
814 </div>
815 {legend}
816 <svg id="axes" viewBox="0 0 100 100" role="img" aria-label="Graph axes: X and Y are spatial, Z is time">
817 <circle cx="50" cy="50" r="3" fill="#526070" />
818 <g data-axis="X" stroke="#dc2626" fill="#dc2626"><line x1="50" y1="50" stroke-width="2"/><text stroke-width="0">X</text></g>
819 <g data-axis="Y" stroke="#16a34a" fill="#16a34a"><line x1="50" y1="50" stroke-width="2"/><text stroke-width="0">Y</text></g>
820 <g data-axis="Z" stroke="#2563eb" fill="#2563eb"><line x1="50" y1="50" stroke-width="2"/><text stroke-width="0">Z</text></g>
821 </svg>
822 <script>
823 const viewer = document.querySelector('model-viewer');
824 const fit = document.querySelector('button');
825 const status = document.querySelector('#status');
826 const axes = document.querySelector('#axes');
827 const moduleMaterials = {toggles};
828 const moduleToggles = document.querySelectorAll('.module-legend input');
829 for (const toggle of moduleToggles) {{
830 toggle.addEventListener('change', () => {{
831 for (const [owner, index, original, highlight, alpha] of moduleMaterials) {{
832 if (owner !== Number(toggle.dataset.module)) continue;
833 const material = viewer.model.materials[index];
834 material.pbrMetallicRoughness.setBaseColorFactor(toggle.checked ? highlight : original);
835 material.setAlphaMode(toggle.checked ? 'OPAQUE' : alpha);
836 }}
837 }});
838 }}
839 const updateAxes = () => {{
840 const {{theta, phi}} = viewer.getCameraOrbit();
841 axes.style.visibility = 'visible';
842 // Graph (x,y,z) maps to renderer (x,z,-y), just as in Bloq Editor.
843 const directions = {{
844 X: [Math.cos(theta), Math.sin(theta) * Math.cos(phi)],
845 Y: [Math.sin(theta), -Math.cos(theta) * Math.cos(phi)],
846 Z: [0, -Math.sin(phi)]
847 }};
848 for (const group of axes.querySelectorAll('[data-axis]')) {{
849 const [x, y] = directions[group.dataset.axis];
850 const line = group.querySelector('line');
851 line.setAttribute('x2', 50 + 32 * x);
852 line.setAttribute('y2', 50 + 32 * y);
853 const label = group.querySelector('text');
854 label.setAttribute('x', 50 + 43 * x);
855 label.setAttribute('y', 50 + 43 * y);
856 }}
857 }};
858 viewer.addEventListener('camera-change', updateAxes);
859 const showError = () => {{ status.textContent = 'Unable to load the 3D viewer. Check your network connection.'; }};
860 document.querySelector('#viewer-runtime').addEventListener('error', showError);
861 if (window.viewerLoadFailed) showError();
862 viewer.addEventListener('error', showError);
863 viewer.addEventListener('load', () => {{
864 fit.disabled = false;
865 for (const toggle of moduleToggles) toggle.disabled = false;
866 status.textContent = '';
867 updateAxes();
868 }});
869 fit.addEventListener('click', () => {{
870 viewer.cameraTarget = 'auto auto auto';
871 viewer.cameraOrbit = '45deg 54.7356deg auto';
872 viewer.fieldOfView = '45deg';
873 }});
874 </script>
875</body>
876</html>"##,
877 model_data_uri
878 ))
879 }
880}
881
882const HADAMARD_STRIP_WIDTH: f32 = 0.2;
883
884fn bounds<'a>(points: impl Iterator<Item = &'a Vec3>) -> (Vec3, Vec3) {
885 points.fold((Vec3::MAX, Vec3::MIN), |(min, max), v| {
886 (min.min(*v), max.max(*v))
887 })
888}
889
890fn next_axis(u: UDirection) -> UDirection {
891 match u {
892 UDirection::X => UDirection::Y,
893 UDirection::Y => UDirection::Z,
894 UDirection::Z => UDirection::X,
895 }
896}
897
898fn get_basis(dir: UDirection, kind: crate::CubeKind) -> Basis {
899 match dir {
900 UDirection::X => kind.x(),
901 UDirection::Y => kind.y(),
902 UDirection::Z => kind.z(),
903 }
904}
905
906fn walking_as_gltf_data(kind: WalkingKind, pipe_length: f32) -> GltfData {
907 let mut data = GltfData::default();
908 let stride = pipe_length + 1.0;
909 let movement = kind.movement_3d().as_vec3() * stride;
910 let bottom_center = Vec3::NEG_Z * 0.5;
911 let top_center = movement + Vec3::Z * 0.5;
912 let x = Vec3::X * 0.5;
913 let y = Vec3::Y * 0.5;
914
915 let b00 = bottom_center - x - y;
916 let b10 = bottom_center + x - y;
917 let b11 = bottom_center + x + y;
918 let b01 = bottom_center - x + y;
919 let t00 = top_center - x - y;
920 let t10 = top_center + x - y;
921 let t11 = top_center + x + y;
922 let t01 = top_center - x + y;
923
924 let boundary = kind.boundary();
925 data.add_prism_face(
926 [b00, b01, b11, b10],
927 boundary.z().into(),
928 FaceStyle::FillAndOutline,
929 );
930 data.add_prism_face(
931 [t00, t10, t11, t01],
932 boundary.z().into(),
933 FaceStyle::FillAndOutline,
934 );
935 data.add_prism_face(
936 [b10, b11, t11, t10],
937 boundary.x().into(),
938 FaceStyle::FillAndOutline,
939 );
940 data.add_prism_face(
941 [b00, t00, t01, b01],
942 boundary.x().into(),
943 FaceStyle::FillAndOutline,
944 );
945 data.add_prism_face(
946 [b01, t01, t11, b11],
947 boundary.y().into(),
948 FaceStyle::FillAndOutline,
949 );
950 data.add_prism_face(
951 [b00, b10, t10, t00],
952 boundary.y().into(),
953 FaceStyle::FillAndOutline,
954 );
955 data
956}
957
958fn patch_rotation_as_gltf_data(kind: PatchRotationKind, pipe_length: f32) -> GltfData {
959 let mut data = GltfData::default();
960 let stride = pipe_length + 1.0;
961 let movement = kind.movement_3d();
962 let colors = PatchRotationColors::new(kind);
963
964 let (min, max) = patch_rotation_bounds(movement, stride);
965
966 add_patch_rotation_boundary_faces(&mut data, kind, stride, colors);
967
968 data.add_prism_face(
969 [
970 vec3(min.x, min.y, min.z),
971 vec3(min.x, max.y, min.z),
972 vec3(max.x, max.y, min.z),
973 vec3(max.x, min.y, min.z),
974 ],
975 colors.start_time,
976 FaceStyle::FillAndOutline,
977 );
978 data.add_prism_face(
979 [
980 vec3(min.x, min.y, max.z),
981 vec3(max.x, min.y, max.z),
982 vec3(max.x, max.y, max.z),
983 vec3(min.x, max.y, max.z),
984 ],
985 colors.end_time,
986 FaceStyle::FillAndOutline,
987 );
988 data
989}
990
991#[derive(Debug, Clone, Copy)]
992struct PatchRotationColors {
993 before: RGBA,
994 after: RGBA,
995 start_time: RGBA,
996 end_time: RGBA,
997 mixed_side: PatchRotationMixedSide,
998}
999
1000impl PatchRotationColors {
1001 fn new(kind: PatchRotationKind) -> Self {
1002 let before = RGBA::from(kind.x_axis_boundary_basis());
1003 let after = RGBA::from(kind.x_axis_boundary_basis().flip());
1004 let mixed_side = PatchRotationMixedSide::for_kind(kind);
1005 Self {
1006 before,
1007 after,
1008 start_time: after,
1009 end_time: before,
1010 mixed_side,
1011 }
1012 }
1013}
1014
1015fn add_patch_rotation_boundary_faces(
1016 data: &mut GltfData,
1017 kind: PatchRotationKind,
1018 stride: f32,
1019 colors: PatchRotationColors,
1020) {
1021 let min_u = -0.5;
1022 let max_u = 0.5;
1023 let min_v = -0.5;
1024 let max_v = stride + 0.5;
1025 let min_z = -0.5;
1026 let max_z = stride + 0.5;
1027 let mid_v = f32::midpoint(min_v, max_v);
1028 let mid_z = f32::midpoint(min_z, max_z);
1029
1030 add_patch_rotation_local_face(
1031 data,
1032 kind,
1033 [
1034 vec3(min_u, min_v, min_z),
1035 vec3(max_u, min_v, min_z),
1036 vec3(max_u, min_v, max_z),
1037 vec3(min_u, min_v, max_z),
1038 ],
1039 colors.after,
1040 );
1041 add_patch_rotation_local_face(
1042 data,
1043 kind,
1044 [
1045 vec3(min_u, max_v, min_z),
1046 vec3(min_u, max_v, max_z),
1047 vec3(max_u, max_v, max_z),
1048 vec3(max_u, max_v, min_z),
1049 ],
1050 colors.before,
1051 );
1052
1053 for (side, u) in [
1054 (PatchRotationMixedSide::Left, min_u),
1055 (PatchRotationMixedSide::Right, max_u),
1056 ] {
1057 if side == colors.mixed_side {
1058 add_patch_rotation_local_face(
1059 data,
1060 kind,
1061 [
1062 vec3(u, min_v, min_z),
1063 vec3(u, mid_v, min_z),
1064 vec3(u, mid_v, max_z),
1065 vec3(u, min_v, max_z),
1066 ],
1067 colors.before,
1068 );
1069 add_patch_rotation_local_face(
1070 data,
1071 kind,
1072 [
1073 vec3(u, mid_v, min_z),
1074 vec3(u, max_v, min_z),
1075 vec3(u, max_v, max_z),
1076 vec3(u, mid_v, max_z),
1077 ],
1078 colors.after,
1079 );
1080 } else {
1081 add_patch_rotation_local_face(
1082 data,
1083 kind,
1084 [
1085 vec3(u, min_v, min_z),
1086 vec3(u, max_v, min_z),
1087 vec3(u, max_v, mid_z),
1088 vec3(u, min_v, mid_z),
1089 ],
1090 colors.before,
1091 );
1092 add_patch_rotation_local_face(
1093 data,
1094 kind,
1095 [
1096 vec3(u, min_v, mid_z),
1097 vec3(u, max_v, mid_z),
1098 vec3(u, max_v, max_z),
1099 vec3(u, min_v, max_z),
1100 ],
1101 colors.after,
1102 );
1103 }
1104 }
1105}
1106
1107fn add_patch_rotation_local_face(
1108 data: &mut GltfData,
1109 kind: PatchRotationKind,
1110 mut corners: [Vec3; 4],
1111 color: RGBA,
1112) {
1113 if corners.iter().all(|corner| corner.x < 0.0) ^ (kind.movement().x == 1) {
1115 corners.reverse();
1116 }
1117 data.add_prism_face(
1118 corners.map(|corner| patch_rotation_local_to_global(kind, corner)),
1119 color,
1120 FaceStyle::FillAndOutline,
1121 );
1122}
1123
1124fn patch_rotation_local_to_global(kind: PatchRotationKind, point: Vec3) -> Vec3 {
1125 match kind.movement().to_array() {
1126 [0, 1] => point,
1127 [0, -1] => vec3(-point.x, -point.y, point.z),
1128 [1, 0] => vec3(point.y, point.x, point.z),
1129 [-1, 0] => vec3(-point.y, point.x, point.z),
1130 _ => unreachable!("PatchRotationKind rejects non-cardinal movement"),
1131 }
1132}
1133
1134fn patch_rotation_local_vector_to_global(kind: PatchRotationKind, vector: Vec3) -> Vec3 {
1135 patch_rotation_local_to_global(kind, vector) - patch_rotation_local_to_global(kind, Vec3::ZERO)
1136}
1137
1138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1139enum PatchRotationMixedSide {
1140 Left,
1141 Right,
1142}
1143
1144impl PatchRotationMixedSide {
1145 fn for_kind(kind: PatchRotationKind) -> Self {
1146 match (
1147 kind.x_axis_boundary_basis(),
1148 kind.movement() == glam::IVec2::NEG_X,
1149 ) {
1150 (Basis::X, true) | (Basis::Z, false) => Self::Right,
1151 (Basis::X, false) | (Basis::Z, true) => Self::Left,
1152 }
1153 }
1154}
1155
1156fn patch_rotation_bounds(movement: IVec3, stride: f32) -> (Vec3, Vec3) {
1157 let min = vec3(
1158 (0.min(movement.x) as f32) * stride - 0.5,
1159 (0.min(movement.y) as f32) * stride - 0.5,
1160 -0.5,
1161 );
1162 let max = vec3(
1163 (0.max(movement.x) as f32) * stride + 0.5,
1164 (0.max(movement.y) as f32) * stride + 0.5,
1165 stride + 0.5,
1166 );
1167 (min, max)
1168}
1169
1170fn has_pipe_toward(graph: &BlockGraph, position: IVec3, offset: IVec3) -> bool {
1171 crate::checked_add_position(position, offset)
1172 .is_ok_and(|neighbor| graph.has_pipe_between(position, neighbor))
1173}
1174
1175fn checked_position_delta(lhs: IVec3, rhs: IVec3) -> Option<IVec3> {
1176 Some(IVec3::new(
1177 lhs.x.checked_sub(rhs.x)?,
1178 lhs.y.checked_sub(rhs.y)?,
1179 lhs.z.checked_sub(rhs.z)?,
1180 ))
1181}
1182
1183pub fn block_as_gltf_data(block: &Block, g: &BlockGraph) -> GltfData {
1185 block_as_gltf_data_with_pipe_length(block, g, 1.0)
1186}
1187
1188pub fn block_as_gltf_data_with_pipe_length(
1197 block: &Block,
1198 g: &BlockGraph,
1199 pipe_length: f32,
1200) -> GltfData {
1201 let mut data = GltfData::default();
1202 let p = block.pos;
1203 let center = Vec3::ZERO;
1204
1205 match block.kind {
1206 BlockKind::Port => {
1207 data.add_cube(
1208 center,
1209 Vec3::ONE,
1210 block
1211 .port_color()
1212 .expect("Port blocks have a display color"),
1213 FaceStyle::FillAndOutline,
1214 );
1215 let connected_faces = Direction::iter()
1216 .filter(|direction| has_pipe_toward(g, p, direction.to_ivec3()))
1217 .collect::<Vec<_>>();
1218 data = data.pop_faces_at_directions(&connected_faces);
1219 }
1220 BlockKind::Cube(kind) => {
1221 let height_cells = block.height_cells();
1222 if height_cells == 1 {
1223 for dir in Direction::iter() {
1224 if !has_pipe_toward(g, p, dir.to_ivec3()) {
1225 let d_vec = dir.to_vec3();
1226 let basis = get_basis(dir.as_udirection(), kind);
1227 let color = basis.into();
1228
1229 let u = dir.as_udirection();
1230 let next_u = next_axis(u);
1231 let next_next_u = next_axis(next_u);
1232
1233 let s1_u = match dir {
1234 Direction::XPLUS | Direction::YPLUS | Direction::ZPLUS => next_u,
1235 Direction::XMINUS | Direction::YMINUS | Direction::ZMINUS => {
1236 next_next_u
1237 }
1238 };
1239 let s2_u = match dir {
1240 Direction::XPLUS | Direction::YPLUS | Direction::ZPLUS => next_next_u,
1241 Direction::XMINUS | Direction::YMINUS | Direction::ZMINUS => next_u,
1242 };
1243
1244 let s1 = s1_u.to_vec3();
1245 let s2 = s2_u.to_vec3();
1246
1247 data.add_face(
1248 center + d_vec * 0.5,
1249 s1,
1250 s2,
1251 color,
1252 FaceStyle::FillAndOutline,
1253 );
1254 }
1255 }
1256 data.add_cube(center, Vec3::ONE, RGBA::LINE_BLACK, FaceStyle::Outline);
1257 } else {
1258 add_tall_cube_faces(&mut data, g, p, kind, height_cells, pipe_length);
1259 }
1260 }
1261 BlockKind::Walking(kind) => {
1262 data.extend(walking_as_gltf_data(kind, pipe_length));
1263 }
1264 BlockKind::PatchRotation(kind) => {
1265 data.extend(patch_rotation_as_gltf_data(kind, pipe_length));
1266 }
1267 BlockKind::T => data.add_cube(center, Vec3::ONE, RGBA::T_PURPLE, FaceStyle::FillAndOutline),
1268 BlockKind::Y => {
1269 let center = if has_pipe_toward(g, p, IVec3::Z) {
1270 center + Vec3::Z * 0.25
1271 } else if has_pipe_toward(g, p, IVec3::NEG_Z) {
1272 center + Vec3::NEG_Z * 0.25
1273 } else {
1274 center
1275 };
1276 data.add_cube(
1277 center,
1278 vec3(1.0, 1.0, 0.5),
1279 RGBA::Y_GREEN,
1280 FaceStyle::FillAndOutline,
1281 );
1282 }
1283 BlockKind::Measurement(basis) => data.add_cube(
1284 center + Vec3::NEG_Z * 0.25,
1285 vec3(1.0, 1.0, 0.5),
1286 basis.into(),
1287 FaceStyle::FillAndOutline,
1288 ),
1289 BlockKind::Selective(kind) => {
1290 let rgba1 = kind.pauli_if_true().into();
1291 let rgba2 = kind.pauli_if_false().into();
1292 data.add_double_color_cube(
1293 center + Vec3::NEG_Z * 0.25,
1294 vec3(1.0, 1.0, 0.5),
1295 rgba1,
1296 rgba2,
1297 );
1298 }
1299 }
1300 data
1301}
1302
1303fn add_tall_cube_faces(
1304 data: &mut GltfData,
1305 graph: &BlockGraph,
1306 pos: IVec3,
1307 kind: crate::CubeKind,
1308 height_cells: u32,
1309 pipe_length: f32,
1310) {
1311 let stride = pipe_length + 1.0;
1312 let min = vec3(-0.5, -0.5, -0.5);
1313 let max = vec3(0.5, 0.5, (height_cells - 1) as f32 * stride + 0.5);
1314 let top = crate::checked_add_position(pos, IVec3::new(0, 0, height_cells as i32 - 1)).ok();
1315 let x_color: RGBA = kind.x().into();
1316 let y_color: RGBA = kind.y().into();
1317 let z_color: RGBA = kind.z().into();
1318
1319 if !has_pipe_toward(graph, pos, IVec3::NEG_X) {
1320 data.add_prism_face(
1321 [
1322 vec3(min.x, min.y, min.z),
1323 vec3(min.x, min.y, max.z),
1324 vec3(min.x, max.y, max.z),
1325 vec3(min.x, max.y, min.z),
1326 ],
1327 x_color,
1328 FaceStyle::FillAndOutline,
1329 );
1330 }
1331 if !has_pipe_toward(graph, pos, IVec3::X) {
1332 data.add_prism_face(
1333 [
1334 vec3(max.x, min.y, min.z),
1335 vec3(max.x, max.y, min.z),
1336 vec3(max.x, max.y, max.z),
1337 vec3(max.x, min.y, max.z),
1338 ],
1339 x_color,
1340 FaceStyle::FillAndOutline,
1341 );
1342 }
1343 if !has_pipe_toward(graph, pos, IVec3::NEG_Y) {
1344 data.add_prism_face(
1345 [
1346 vec3(min.x, min.y, min.z),
1347 vec3(max.x, min.y, min.z),
1348 vec3(max.x, min.y, max.z),
1349 vec3(min.x, min.y, max.z),
1350 ],
1351 y_color,
1352 FaceStyle::FillAndOutline,
1353 );
1354 }
1355 if !has_pipe_toward(graph, pos, IVec3::Y) {
1356 data.add_prism_face(
1357 [
1358 vec3(min.x, max.y, min.z),
1359 vec3(min.x, max.y, max.z),
1360 vec3(max.x, max.y, max.z),
1361 vec3(max.x, max.y, min.z),
1362 ],
1363 y_color,
1364 FaceStyle::FillAndOutline,
1365 );
1366 }
1367 if !has_pipe_toward(graph, pos, IVec3::NEG_Z) {
1368 data.add_prism_face(
1369 [
1370 vec3(min.x, min.y, min.z),
1371 vec3(min.x, max.y, min.z),
1372 vec3(max.x, max.y, min.z),
1373 vec3(max.x, min.y, min.z),
1374 ],
1375 z_color,
1376 FaceStyle::FillAndOutline,
1377 );
1378 }
1379 if top.is_none_or(|top| !has_pipe_toward(graph, top, IVec3::Z)) {
1380 data.add_prism_face(
1381 [
1382 vec3(min.x, min.y, max.z),
1383 vec3(max.x, min.y, max.z),
1384 vec3(max.x, max.y, max.z),
1385 vec3(min.x, max.y, max.z),
1386 ],
1387 z_color,
1388 FaceStyle::FillAndOutline,
1389 );
1390 }
1391}
1392
1393pub fn pipe_as_gltf_data(
1395 u: &Block,
1396 v: &Block,
1397 pipe: &Pipe,
1398 g: &BlockGraph,
1399 pipe_length: f32,
1400) -> GltfData {
1401 pipe_between_positions_as_gltf_data(u.pos, v.pos, pipe, g, pipe_length)
1402}
1403
1404pub fn pipe_between_positions_as_gltf_data(
1406 u_pos: IVec3,
1407 v_pos: IVec3,
1408 pipe: &Pipe,
1409 g: &BlockGraph,
1410 pipe_length: f32,
1411) -> GltfData {
1412 let mut data = GltfData::default();
1413 let diff = v_pos.as_vec3() - u_pos.as_vec3();
1414 let pipe_height = scaled_spatial_pipe_height(u_pos, v_pos, pipe, g, pipe_length).unwrap_or(1.0);
1415 let center = Vec3::Z * ((pipe_height - 1.0) * 0.5);
1416
1417 let h2 = HADAMARD_STRIP_WIDTH * pipe_length * 0.25;
1418 let p2 = pipe_length * 0.5;
1419 let face_center_delta = f32::midpoint(p2, h2);
1420
1421 let main_axis = if diff.x != 0.0 {
1422 Vec3::X
1423 } else if diff.y != 0.0 {
1424 Vec3::Y
1425 } else {
1426 Vec3::Z
1427 };
1428
1429 let inferred_bases = g.infer_pipe_basis(pipe);
1430 let pipe_dir_u = pipe.dir.as_udirection();
1431
1432 let size = Vec3::ONE + main_axis.abs() * (pipe_length - 1.0) + Vec3::Z * (pipe_height - 1.0);
1433 data.add_cube(center, size, RGBA::LINE_BLACK, FaceStyle::Outline);
1434
1435 for dir in UDirection::iter() {
1436 if dir == pipe_dir_u {
1437 continue;
1438 }
1439
1440 let (color_start, color_end, strip_color) = match inferred_bases[dir.index()] {
1444 Some(basis) => {
1445 let basis_end = if pipe.hadamard { basis.flip() } else { basis };
1446 let strip = if pipe.hadamard {
1447 RGBA::H_YELLOW
1448 } else {
1449 basis.into()
1450 };
1451 (basis.into(), basis_end.into(), strip)
1452 }
1453 None => (RGBA::PORT_GRAY, RGBA::PORT_GRAY, RGBA::PORT_GRAY),
1454 };
1455
1456 let normal = dir.to_vec3();
1457 let normal_len = if normal.abs().dot(Vec3::Z) > 0.0 {
1458 pipe_height
1459 } else {
1460 1.0
1461 };
1462 let width_dir = main_axis.cross(normal);
1463 let width_len = if width_dir.abs().dot(Vec3::Z) > 0.0 {
1464 pipe_height
1465 } else {
1466 1.0
1467 };
1468 let width_dir = width_dir * width_len;
1469
1470 for polarity in [1.0, -1.0] {
1471 let d = normal * polarity;
1472 let normal_offset = d * (normal_len * 0.5);
1473
1474 let (s1, s2) = if main_axis.cross(width_dir).dot(d) < 0.0 {
1475 (width_dir, main_axis * (p2 - h2))
1476 } else {
1477 (main_axis * (p2 - h2), width_dir)
1478 };
1479
1480 let (s1_strip, s2_strip) = if main_axis.cross(width_dir).dot(d) < 0.0 {
1481 (width_dir, main_axis * h2 * 2.0)
1482 } else {
1483 (main_axis * h2 * 2.0, width_dir)
1484 };
1485
1486 data.add_face(
1487 center - diff * face_center_delta + normal_offset,
1488 s1,
1489 s2,
1490 color_start,
1491 FaceStyle::Fill,
1492 );
1493 data.add_face(
1494 center + diff * face_center_delta + normal_offset,
1495 s1,
1496 s2,
1497 color_end,
1498 FaceStyle::Fill,
1499 );
1500 data.add_face(
1501 center + normal_offset,
1502 s1_strip,
1503 s2_strip,
1504 strip_color,
1505 FaceStyle::Fill,
1506 );
1507 }
1508 }
1509 data
1510}
1511
1512fn scaled_spatial_pipe_height(
1513 u_pos: IVec3,
1514 v_pos: IVec3,
1515 pipe: &Pipe,
1516 g: &BlockGraph,
1517 pipe_length: f32,
1518) -> Option<f32> {
1519 if !pipe.dir.is_spatial() {
1520 return None;
1521 }
1522 let u = g.get_endpoint_block(u_pos)?;
1523 let v = g.get_endpoint_block(v_pos)?;
1524 if !u.kind().is_cube() || !v.kind().is_cube() || u.height_cells() != v.height_cells() {
1525 return None;
1526 }
1527 (u.height_cells() > 1).then_some((u.height_cells() - 1) as f32 * (pipe_length + 1.0) + 1.0)
1528}
1529
1530pub fn block_graph_as_gltf_data(g: &BlockGraph, pipe_length: f32) -> GltfData {
1533 block_graph_as_gltf_data_with_popped_faces(g, pipe_length, &[])
1534}
1535
1536pub fn block_graph_as_gltf_data_with_popped_faces(
1538 g: &BlockGraph,
1539 pipe_length: f32,
1540 popped_faces: &[GltfFaceSelector],
1541) -> GltfData {
1542 block_graph_as_gltf_data_with_modules(g, pipe_length, popped_faces, None)
1543}
1544
1545pub(crate) fn block_graph_as_gltf_data_with_modules(
1546 g: &BlockGraph,
1547 pipe_length: f32,
1548 popped_faces: &[GltfFaceSelector],
1549 modules: Option<&crate::ModuleView>,
1550) -> GltfData {
1551 build_module_geometry(g, pipe_length, popped_faces, modules, true).0
1552}
1553
1554pub(crate) fn module_html_geometry(
1555 g: &BlockGraph,
1556 pipe_length: f32,
1557 popped_faces: &[GltfFaceSelector],
1558 modules: Option<&crate::ModuleView>,
1559) -> (GltfData, ModuleFaceRanges) {
1560 build_module_geometry(g, pipe_length, popped_faces, modules, false)
1561}
1562
1563fn build_module_geometry(
1564 g: &BlockGraph,
1565 pipe_length: f32,
1566 popped_faces: &[GltfFaceSelector],
1567 modules: Option<&crate::ModuleView>,
1568 highlight: bool,
1569) -> (GltfData, ModuleFaceRanges) {
1570 let mut data = GltfData::default();
1571 let mut ranges = ModuleFaceRanges::new();
1572 let mut append = |mut object: GltfData, owner: Option<usize>, center: Vec3| {
1573 if highlight {
1574 if let Some(owner) = owner {
1575 object.color_module_faces(crate::module_color(owner));
1576 }
1577 } else {
1578 for (color, triangles) in &object.triangles {
1579 let start = data.triangles.get(color).map_or(0, Vec::len);
1580 let end = start + triangles.len();
1581 let spans = ranges.entry(*color).or_default();
1582 if let Some((last_owner, last_range)) = spans.last_mut()
1583 && *last_owner == owner
1584 {
1585 last_range.end = end;
1586 } else {
1587 spans.push((owner, start..end));
1588 }
1589 }
1590 }
1591 data.extend(object.map_points(|v| v + center));
1592 };
1593 let stride = pipe_length + 1.0;
1594
1595 for block in g.blocks() {
1596 let p = block.pos;
1597 let center = p.as_vec3() * stride;
1598 let directions = popped_faces
1599 .iter()
1600 .filter_map(|selector| selector.for_block(p))
1601 .collect::<Vec<_>>();
1602 let block_data = block_as_gltf_data_with_pipe_length(block, g, pipe_length)
1603 .pop_faces_at_directions(&directions);
1604 append(
1605 block_data,
1606 modules.and_then(|view| view.module_for_block(p)),
1607 center,
1608 );
1609 }
1610
1611 for (u_pos, v_pos, _, _, pipe) in g.pipe_endpoints_with_blocks() {
1612 let diff = (v_pos - u_pos).as_vec3();
1613 let center = u_pos.as_vec3() * stride + diff * stride * 0.5;
1614 let directions = popped_faces
1615 .iter()
1616 .filter_map(|selector| selector.for_pipe(u_pos, v_pos))
1617 .collect::<Vec<_>>();
1618 let pipe_data = pipe_between_positions_as_gltf_data(u_pos, v_pos, pipe, g, pipe_length)
1619 .pop_faces_at_directions(&directions);
1620 append(
1621 pipe_data,
1622 modules.and_then(|view| view.module_for_pipe(g, u_pos, v_pos)),
1623 center,
1624 );
1625 }
1626 (data, ranges)
1627}
1628
1629pub fn stabilizer_as_gltf_data(
1636 generator: &StabilizerGenerator,
1637 g: &BlockGraph,
1638 pipe_length: f32,
1639) -> Result<GltfData, BlockGraphError> {
1640 let measurement_label = if let StabilizerRowKind::Measurement { name } = &generator.kind {
1641 ZXGraph::try_from(g)?
1642 .measurement_outcome_position(name)
1643 .map(|position| (position, name.clone()))
1644 } else {
1645 None
1646 };
1647 let stabilizer = &generator.stabilizer;
1648 let g = g.fix_shadowed_faces();
1649 let mut data = GltfData::default();
1650 let stride = pipe_length + 1.0;
1651
1652 generate_interior_edge_surfaces(stabilizer, &g, pipe_length, stride, &mut data)?;
1653 let neighbors = build_neighbor_map(stabilizer, &g);
1654 generate_interior_node_surfaces(stabilizer, &g, stride, &neighbors, &mut data)?;
1655
1656 if let Some((pos, text)) = measurement_label {
1657 data.texts.push(TextLabel {
1658 position: (pos + 0.25 * Vec3::Z) * stride,
1659 text,
1660 color: RGBA::H_YELLOW,
1661 });
1662 }
1663 Ok(data)
1664}
1665
1666fn get_surface_normal(g: &BlockGraph, pipe: &Pipe, p: Pauli) -> Vec3 {
1667 let inferred = g.infer_pipe_basis(pipe);
1668 let pipe_dir = pipe.dir.as_udirection();
1669
1670 UDirection::iter()
1671 .filter(|&d| d != pipe_dir)
1672 .find_map(|dir| {
1673 inferred[dir.index()].and_then(|basis| {
1674 if Pauli::from(basis) == p.flip() {
1675 Some(dir.to_vec3())
1676 } else {
1677 None
1678 }
1679 })
1680 })
1681 .unwrap_or_else(|| pipe_dir.to_vec3())
1682}
1683
1684fn generate_interior_edge_surfaces(
1685 stabilizer: &Stabilizer,
1686 g: &BlockGraph,
1687 pipe_length: f32,
1688 stride: f32,
1689 data: &mut GltfData,
1690) -> Result<(), BlockGraphError> {
1691 for (&(u, v), &p) in &stabilizer.interior_edges {
1692 let physical = physical_pipe_for_stabilizer_edge(g, u, v)
1693 .ok_or(BlockGraphError::PipeNotFound(u, v))?;
1694 let pipe = physical.pipe;
1695
1696 let (u, v, p) = if physical.src == pipe.src {
1697 (physical.src, physical.dst, p)
1698 } else {
1699 (
1700 physical.dst,
1701 physical.src,
1702 if pipe.hadamard { p.flip() } else { p },
1703 )
1704 };
1705 let dir_vec = (v - u).as_vec3();
1706 let center = (u.as_vec3() + v.as_vec3()) * 0.5 * stride;
1707
1708 let segment_len = if pipe.hadamard {
1709 pipe_length * 0.5
1710 } else {
1711 pipe_length
1712 };
1713 let pipe_height = scaled_spatial_pipe_height(u, v, pipe, g, pipe_length).unwrap_or(1.0);
1714 let scaled_pipe_center_offset = Vec3::Z * ((pipe_height - 1.0) * 0.5);
1715
1716 for pauli in p.iter_xz() {
1717 let normal = get_surface_normal(g, pipe, pauli);
1718 let offset = if pipe.hadamard {
1719 -0.5 * segment_len * dir_vec
1720 } else {
1721 Vec3::ZERO
1722 };
1723 let seg_center = center + offset + scaled_pipe_center_offset;
1724 let width_vec = scale_z_dimension(dir_vec.cross(normal), pipe_height);
1725
1726 data.add_face(
1727 seg_center,
1728 dir_vec * segment_len,
1729 width_vec,
1730 correlation_surface_color(pauli),
1731 FaceStyle::Fill,
1732 );
1733
1734 if pipe.hadamard {
1735 let p_flipped = pauli.flip();
1736 let color_flipped = correlation_surface_color(p_flipped);
1737 let seg_center = center - offset + scaled_pipe_center_offset;
1738 data.add_face(
1739 seg_center,
1740 dir_vec * segment_len,
1741 width_vec,
1742 color_flipped,
1743 FaceStyle::Fill,
1744 );
1745 }
1746 }
1747 }
1748 Ok(())
1749}
1750
1751fn add_walking_node_surface(
1752 block_pos: IVec3,
1753 kind: WalkingKind,
1754 p: Pauli,
1755 stride: f32,
1756 data: &mut GltfData,
1757) {
1758 let start = walking_attachment_point(block_pos, kind, block_pos, stride);
1759 let end = walking_attachment_point(block_pos, kind, kind.end_position(block_pos), stride);
1760 add_walking_surface_between(kind, p, start, end, data);
1761}
1762
1763fn add_walking_surface_between(
1764 kind: WalkingKind,
1765 p: Pauli,
1766 start: Vec3,
1767 end: Vec3,
1768 data: &mut GltfData,
1769) {
1770 let center = (start + end) * 0.5;
1771 let side = end - start;
1772
1773 for pauli in p.iter_xz() {
1774 let Some(width_vec) = walking_surface_width(kind, pauli) else {
1775 continue;
1776 };
1777 data.add_face(
1778 center,
1779 side,
1780 width_vec,
1781 correlation_surface_color(pauli),
1782 FaceStyle::Fill,
1783 );
1784 }
1785}
1786
1787fn walking_attachment_point(
1788 block_pos: IVec3,
1789 kind: WalkingKind,
1790 endpoint: IVec3,
1791 stride: f32,
1792) -> Vec3 {
1793 if endpoint == block_pos {
1794 endpoint.as_vec3() * stride + Vec3::NEG_Z * 0.5
1795 } else if endpoint == kind.end_position(block_pos) {
1796 endpoint.as_vec3() * stride + Vec3::Z * 0.5
1797 } else {
1798 endpoint.as_vec3() * stride
1799 }
1800}
1801
1802fn walking_surface_width(kind: WalkingKind, pauli: Pauli) -> Option<Vec3> {
1803 let boundary = kind.boundary();
1804 [(boundary.x(), Vec3::X), (boundary.y(), Vec3::Y)]
1805 .into_iter()
1806 .find_map(|(basis, axis)| (Pauli::from(basis) == pauli).then_some(axis))
1807}
1808
1809#[derive(Debug, Clone, Copy)]
1810struct PatchRotationStabilizerQuad {
1811 center: Vec3,
1812 side1: Vec3,
1813 side2: Vec3,
1814}
1815
1816fn add_patch_rotation_node_surface(
1817 block_pos: IVec3,
1818 kind: PatchRotationKind,
1819 pauli: Pauli,
1820 stride: f32,
1821 data: &mut GltfData,
1822) {
1823 let Some(shape) = patch_rotation_stabilizer_shape(kind, pauli) else {
1824 return;
1825 };
1826 let color = correlation_surface_color(pauli);
1827 let translation = block_pos.as_vec3() * stride;
1828
1829 for quad in
1830 patch_rotation_stabilizer_quads(shape, stride, PatchRotationMixedSide::for_kind(kind))
1831 {
1832 let center = patch_rotation_local_to_global(kind, quad.center) + translation;
1833 let side1 = patch_rotation_local_vector_to_global(kind, quad.side1);
1834 let side2 = patch_rotation_local_vector_to_global(kind, quad.side2);
1835 data.add_face(center, side1, side2, color, FaceStyle::Fill);
1836 }
1837}
1838
1839fn patch_rotation_stabilizer_shape(kind: PatchRotationKind, pauli: Pauli) -> Option<Pauli> {
1840 let basis = Pauli::from(kind.x_axis_boundary_basis());
1841 if pauli == basis {
1842 Some(Pauli::X)
1843 } else if pauli == basis.flip() {
1844 Some(Pauli::Z)
1845 } else {
1846 None
1847 }
1848}
1849
1850fn patch_rotation_stabilizer_quads(
1851 shape: Pauli,
1852 stride: f32,
1853 mixed_side: PatchRotationMixedSide,
1854) -> Vec<PatchRotationStabilizerQuad> {
1855 let min_x = -0.5;
1856 let max_x = 0.5;
1857 let mid_x = 0.0;
1858 let min_y = -0.5;
1859 let max_y = stride + 0.5;
1860 let min_z = -0.5;
1861 let max_z = stride + 0.5;
1862 let mid_y = f32::midpoint(min_y, max_y);
1863 let upper_line_y = f32::midpoint(min_y, mid_y);
1864 let upper_input_line_y = upper_line_y - 0.5;
1865 let lower_line_y = min_y + (max_y - min_y) * 0.75;
1866 let sheet_margin_y = (max_y - min_y) * 0.125;
1867 let upper_sheet_y = (upper_line_y - sheet_margin_y).max(min_y);
1868 let lower_sheet_y = (lower_line_y + sheet_margin_y).min(max_y);
1869 let lower_output_line_y = f32::midpoint(lower_line_y, max_y);
1870 let in_quarter_z = min_z + (max_z - min_z) * 0.25;
1871 let out_quarter_z = min_z + (max_z - min_z) * 0.75;
1872 let (mixed_x0, mixed_x1, extended_x0, extended_x1) = match mixed_side {
1873 PatchRotationMixedSide::Left => (min_x, mid_x, mid_x, max_x),
1874 PatchRotationMixedSide::Right => (mid_x, max_x, min_x, mid_x),
1875 };
1876
1877 match shape {
1878 Pauli::Z => vec![
1879 xz_quad(mixed_x0, mixed_x1, mid_y, min_z, out_quarter_z),
1880 yz_quad(mid_x, min_y, mid_y, min_z, out_quarter_z),
1881 xy_quad(extended_x0, extended_x1, min_y, mid_y, out_quarter_z),
1882 xy_quad(min_x, max_x, mid_y, lower_sheet_y, out_quarter_z),
1883 xz_quad(min_x, max_x, lower_output_line_y, out_quarter_z, max_z),
1884 ],
1885 Pauli::X => vec![
1886 xz_quad(min_x, max_x, upper_input_line_y, min_z, in_quarter_z),
1887 xy_quad(min_x, max_x, upper_sheet_y, mid_y, in_quarter_z),
1888 xy_quad(extended_x0, extended_x1, mid_y, max_y, in_quarter_z),
1889 xz_quad(mixed_x0, mixed_x1, mid_y, in_quarter_z, max_z),
1890 yz_quad(mid_x, mid_y, max_y, in_quarter_z, max_z),
1891 ],
1892 _ => Vec::new(),
1893 }
1894}
1895
1896fn xy_quad(x0: f32, x1: f32, y0: f32, y1: f32, z: f32) -> PatchRotationStabilizerQuad {
1897 PatchRotationStabilizerQuad {
1898 center: vec3(f32::midpoint(x0, x1), f32::midpoint(y0, y1), z),
1899 side1: Vec3::X * (x1 - x0),
1900 side2: Vec3::Y * (y1 - y0),
1901 }
1902}
1903
1904fn xz_quad(x0: f32, x1: f32, y: f32, z0: f32, z1: f32) -> PatchRotationStabilizerQuad {
1905 PatchRotationStabilizerQuad {
1906 center: vec3(f32::midpoint(x0, x1), y, f32::midpoint(z0, z1)),
1907 side1: Vec3::X * (x1 - x0),
1908 side2: Vec3::Z * (z1 - z0),
1909 }
1910}
1911
1912fn yz_quad(x: f32, y0: f32, y1: f32, z0: f32, z1: f32) -> PatchRotationStabilizerQuad {
1913 PatchRotationStabilizerQuad {
1914 center: vec3(x, f32::midpoint(y0, y1), f32::midpoint(z0, z1)),
1915 side1: Vec3::Y * (y1 - y0),
1916 side2: Vec3::Z * (z1 - z0),
1917 }
1918}
1919
1920fn cube_stabilizer_height(block: &Block, stride: f32) -> f32 {
1921 if block.kind().is_cube() {
1922 (block.height_cells() - 1) as f32 * stride + 1.0
1923 } else {
1924 1.0
1925 }
1926}
1927
1928fn cube_stabilizer_center(block: &Block, stride: f32) -> Vec3 {
1929 let height = cube_stabilizer_height(block, stride);
1930 block.pos().as_vec3() * stride + Vec3::Z * ((height - 1.0) * 0.5)
1931}
1932
1933fn scale_z_dimension(vector: Vec3, height: f32) -> Vec3 {
1934 if vector.abs().dot(Vec3::Z) > 0.0 {
1935 vector * height
1936 } else {
1937 vector
1938 }
1939}
1940
1941fn cube_half_surface_offset(dvec: Vec3, height: f32) -> f32 {
1942 if dvec.abs().dot(Vec3::Z) > 0.0 {
1943 height * 0.25
1944 } else {
1945 0.25
1946 }
1947}
1948
1949fn cube_half_surface_side(dvec: Vec3, height: f32) -> Vec3 {
1950 if dvec.abs().dot(Vec3::Z) > 0.0 {
1951 dvec * (height * 0.5)
1952 } else {
1953 dvec * 0.5
1954 }
1955}
1956
1957fn pipe_for_node_direction<'a>(
1958 g: &'a BlockGraph,
1959 block: &Block,
1960 dir: Direction,
1961) -> Option<(IVec3, &'a Pipe)> {
1962 block
1963 .connectable_offsets()
1964 .into_iter()
1965 .filter_map(|offset| crate::checked_add_position(block.pos(), offset).ok())
1966 .find_map(|endpoint| {
1967 crate::checked_add_position(endpoint, dir.to_ivec3())
1968 .ok()
1969 .and_then(|neighbor| g.get_pipe(endpoint, neighbor))
1970 .map(|pipe| (endpoint, pipe))
1971 })
1972}
1973
1974fn build_neighbor_map(
1975 stabilizer: &Stabilizer,
1976 g: &BlockGraph,
1977) -> HashMap<IVec3, Vec<(Direction, Pauli)>> {
1978 let mut neighbors: HashMap<IVec3, Vec<(Direction, Pauli)>> =
1979 HashMap::with_capacity(stabilizer.interior_nodes.len());
1980
1981 for (&(u, v), &p) in &stabilizer.interior_edges {
1982 if let Some(dir_uv) =
1983 checked_position_delta(v, u).and_then(|delta| Direction::try_from(delta).ok())
1984 {
1985 neighbors.entry(u).or_default().push((dir_uv, p));
1986 let p_v = g
1987 .get_pipe(u, v)
1988 .map(|pipe| if pipe.hadamard { p.flip() } else { p })
1989 .unwrap_or(p);
1990 if let Some(dir_vu) =
1991 checked_position_delta(u, v).and_then(|delta| Direction::try_from(delta).ok())
1992 {
1993 neighbors.entry(v).or_default().push((dir_vu, p_v));
1994 }
1995 continue;
1996 }
1997
1998 let Some(physical) = physical_pipe_for_stabilizer_edge(g, u, v) else {
1999 continue;
2000 };
2001
2002 let p_v = if physical.pipe.hadamard { p.flip() } else { p };
2003 if let Some(dir_uv) = checked_position_delta(physical.dst, physical.src)
2004 .and_then(|delta| Direction::try_from(delta).ok())
2005 {
2006 neighbors.entry(u).or_default().push((dir_uv, p));
2007 neighbors.entry(v).or_default().push((dir_uv.negate(), p_v));
2008 }
2009 }
2010 neighbors
2011}
2012
2013struct PhysicalStabilizerPipe<'a> {
2014 src: IVec3,
2015 dst: IVec3,
2016 pipe: &'a Pipe,
2017}
2018
2019fn physical_pipe_for_stabilizer_edge<'a>(
2020 g: &'a BlockGraph,
2021 u: IVec3,
2022 v: IVec3,
2023) -> Option<PhysicalStabilizerPipe<'a>> {
2024 let u_block = g.get_endpoint_block(u)?;
2025 let v_block = g.get_endpoint_block(v)?;
2026 for u_endpoint in u_block
2027 .connectable_offsets()
2028 .into_iter()
2029 .filter_map(|offset| crate::checked_add_position(u_block.pos(), offset).ok())
2030 {
2031 for v_endpoint in v_block
2032 .connectable_offsets()
2033 .into_iter()
2034 .filter_map(|offset| crate::checked_add_position(v_block.pos(), offset).ok())
2035 {
2036 let Some(pipe) = g.get_pipe(u_endpoint, v_endpoint) else {
2037 continue;
2038 };
2039 return Some(PhysicalStabilizerPipe {
2040 src: u_endpoint,
2041 dst: v_endpoint,
2042 pipe,
2043 });
2044 }
2045 }
2046 None
2047}
2048
2049fn generate_interior_node_surfaces(
2050 stabilizer: &Stabilizer,
2051 g: &BlockGraph,
2052 stride: f32,
2053 neighbors: &HashMap<IVec3, Vec<(Direction, Pauli)>>,
2054 data: &mut GltfData,
2055) -> Result<(), BlockGraphError> {
2056 let mut nodes = stabilizer.interior_nodes.clone();
2060 for (&pos, incident) in neighbors {
2061 if g.get_block(pos).is_some_and(|block| block.kind().is_cube()) {
2062 let support = incident.iter().fold(Pauli::I, |p, (_, q)| p | *q);
2063 let p = nodes.entry(pos).or_insert(Pauli::I);
2064 *p = *p | support;
2065 }
2066 }
2067 for (&pos, &p) in &nodes {
2068 let block = g
2069 .get_block(pos)
2070 .ok_or(BlockGraphError::BlockNotFound(pos))?;
2071 let center = pos.as_vec3() * stride;
2072
2073 for pauli in p.iter_xz() {
2074 match block.kind {
2075 BlockKind::Walking(kind) => {
2076 add_walking_node_surface(pos, kind, pauli, stride, data);
2077 }
2078 BlockKind::PatchRotation(kind) => {
2079 add_patch_rotation_node_surface(pos, kind, pauli, stride, data);
2080 }
2081 BlockKind::Cube(kind) if Pauli::from(kind.normal_basis()) != pauli => {
2082 let normal = kind.normal_direction().to_vec3();
2083 if let Some(dvec) = Direction::iter()
2084 .find(|d| d.as_udirection() != kind.normal_direction())
2085 .map(Direction::to_vec3)
2086 {
2087 let center = cube_stabilizer_center(block, stride);
2088 let width_vec = scale_z_dimension(
2089 dvec.cross(normal),
2090 cube_stabilizer_height(block, stride),
2091 );
2092 data.add_face(
2093 center,
2094 dvec,
2095 width_vec,
2096 correlation_surface_color(pauli),
2097 FaceStyle::Fill,
2098 );
2099 }
2100 }
2101 BlockKind::Cube(_) | BlockKind::Port => {
2102 let is_cube = block.kind.is_cube();
2103 let height = cube_stabilizer_height(block, stride);
2104 let center = if is_cube {
2105 cube_stabilizer_center(block, stride)
2106 } else {
2107 center
2108 };
2109
2110 for (dir, _) in neighbors
2111 .get(&pos)
2112 .into_iter()
2113 .flatten()
2114 .filter(|(_, neighbor_pauli)| *neighbor_pauli & pauli)
2115 {
2116 let dvec = dir.to_vec3();
2117 if let Some((endpoint, pipe)) = pipe_for_node_direction(g, block, *dir) {
2118 let effective_pauli = if pipe.hadamard && (endpoint != pipe.src) {
2125 pauli.flip()
2126 } else {
2127 pauli
2128 };
2129 let normal = get_surface_normal(g, pipe, effective_pauli);
2130 let (width_vec, side, offset) = if is_cube {
2131 (
2132 scale_z_dimension(dvec.cross(normal), height),
2133 cube_half_surface_side(dvec, height),
2134 cube_half_surface_offset(dvec, height),
2135 )
2136 } else {
2137 (dvec.cross(normal), dvec * 0.5, 0.25)
2138 };
2139
2140 data.add_face(
2141 center + dvec * offset,
2142 side,
2143 width_vec,
2144 correlation_surface_color(pauli),
2145 FaceStyle::Fill,
2146 );
2147 }
2148 }
2149 }
2150 _ => {}
2151 }
2152 }
2153 }
2154 Ok(())
2155}
2156
2157fn correlation_surface_color(pauli: Pauli) -> RGBA {
2158 match pauli {
2159 Pauli::X => RGBA::X_PURE_RED,
2160 Pauli::Y => RGBA::Y_PURE_GREEN,
2161 Pauli::Z => RGBA::Z_PURE_BLUE,
2162 Pauli::I => RGBA::LINE_BLACK,
2163 }
2164}
2165
2166#[cfg(test)]
2167mod tests {
2168 use super::*;
2169 use crate::{GalleryItem, WalkingBoundaryKind, parse_blog_to_graph};
2170 use bloq_utils::PauliString;
2171 use glam::{IVec2, ivec2};
2172
2173 const EPS: f32 = 1e-5;
2174
2175 #[test]
2176 fn gltf_bakes_editor_lighting_without_changing_palette_opacity() {
2177 use gltf_json::validation::Validate;
2178
2179 let mut data = GltfData::default();
2180 data.add_triangles(
2182 RGBA::PORT_GRAY,
2183 [
2184 [Vec3::ZERO, Vec3::Z, Vec3::X],
2185 [Vec3::ZERO, Vec3::X, Vec3::Z],
2186 ],
2187 );
2188 data.add_line(RGBA::LINE_BLACK, [[Vec3::ZERO, Vec3::X]]);
2189 let model = data.to_model();
2190 let bytes = gltf_json::serialize::to_vec(&model).unwrap();
2191 let model: Root = gltf_json::deserialize::from_slice(&bytes).unwrap();
2192 let mut errors = Vec::new();
2193 model.validate(&model, gltf_json::Path::new, &mut |path, error| {
2194 errors.push((path(), error));
2195 });
2196 assert!(errors.is_empty(), "invalid glTF: {errors:?}");
2197 assert!(
2198 model
2199 .extensions_required
2200 .iter()
2201 .any(|name| name == "KHR_materials_unlit")
2202 );
2203 assert!(
2204 model
2205 .materials
2206 .iter()
2207 .all(|material| { material.extensions.as_ref().unwrap().unlit.is_some() })
2208 );
2209
2210 let primitive = &model.meshes[0].primitives[0];
2211 let material = &model.materials[primitive.material.unwrap().value()];
2212 assert_eq!(material.alpha_mode, Valid(AlphaMode::Blend));
2213 let [red, _, _, alpha] = material.pbr_metallic_roughness.base_color_factor.0;
2214 assert!((red - 0.7230551).abs() < EPS); assert!((alpha - 89.0 / 255.0).abs() < EPS);
2216 let positions = &model.accessors[primitive.attributes[&Valid(Semantic::Positions)].value()];
2217 let colors = &model.accessors[primitive.attributes[&Valid(Semantic::Colors(0))].value()];
2218 assert_eq!(colors.type_, Valid(Type::Vec4));
2219 assert_eq!(colors.count, positions.count);
2220 assert_eq!(colors.count.0, 6);
2221 assert!(
2222 !model.meshes[0].primitives[1]
2223 .attributes
2224 .contains_key(&Valid(Semantic::Colors(0)))
2225 );
2226
2227 let uri = model.buffers[0].uri.as_ref().unwrap();
2228 let buffer = BASE64_STANDARD
2229 .decode(uri.split_once(',').unwrap().1)
2230 .unwrap();
2231 let view = &model.buffer_views[colors.buffer_view.unwrap().value()];
2232 let start = view.byte_offset.unwrap().0 as usize;
2233 let end = start + view.byte_length.0 as usize;
2234 assert_eq!(end, buffer.len());
2235 let values: Vec<_> = buffer[start..end]
2236 .as_chunks::<4>()
2237 .0
2238 .iter()
2239 .map(|bytes| f32::from_le_bytes(*bytes))
2240 .collect();
2241 let exposure = 2.0_f32.powf(-9.7) / 1.2;
2242 let ambient = 0.4524 * EDITOR_AMBIENT_BRIGHTNESS * exposure;
2243 let lit = ambient + 0.3255 * EDITOR_DIRECTIONAL_ILLUMINANCE * exposure / 3.0_f32.sqrt();
2244 for (vertex, expected) in values
2245 .as_chunks::<4>()
2246 .0
2247 .iter()
2248 .zip([lit, lit, lit, ambient, ambient, ambient])
2249 {
2250 assert!((vertex[0] - expected).abs() < EPS);
2251 assert_eq!(vertex[0], vertex[1]);
2252 assert_eq!(vertex[1], vertex[2]);
2253 assert_eq!(vertex[3], 1.0); }
2255 assert!(linear_palette(RGBA::from_hex(0x080808, 255))[0] < 0.003);
2256 }
2257
2258 #[test]
2259 fn module_faces_keep_the_shared_opaque_color_and_unlit_brightness() {
2260 let mut data = GltfData::default();
2261 data.add_triangles(RGBA::X_RED, [[Vec3::ZERO, Vec3::X, Vec3::Y]]);
2262 data.add_line(RGBA::LINE_BLACK, [[Vec3::ZERO, Vec3::X]]);
2263 let color = crate::module_color(0);
2264 data.color_module_faces(color);
2265 assert_eq!(data.triangles.keys().copied().collect::<Vec<_>>(), [color]);
2266 assert!(data.lines.contains_key(&RGBA::LINE_BLACK));
2267 let data = data.swap_yz();
2268 let model = data.to_model();
2269 let primitive = &model.meshes[0].primitives[0];
2270 let material = &model.materials[primitive.material.unwrap().value()];
2271 assert_eq!(material.alpha_mode, Valid(AlphaMode::Opaque));
2272 assert_eq!(
2273 material.pbr_metallic_roughness.base_color_factor.0,
2274 linear_palette(color)
2275 );
2276 let accessor = &model.accessors[primitive.attributes[&Valid(Semantic::Colors(0))].value()];
2277 let view = &model.buffer_views[accessor.buffer_view.unwrap().value()];
2278 let buffer = data.to_buffer_bytes();
2279 let start = view.byte_offset.unwrap().0 as usize;
2280 for bytes in buffer[start..].as_chunks::<4>().0 {
2281 assert_eq!(f32::from_le_bytes(*bytes), 1.0);
2282 }
2283 }
2284
2285 #[test]
2286 fn module_toggles_preserve_original_faces_opacity_and_seams() {
2287 let source = crate::GalleryItem::ThreeBitAdder.build();
2288 let graph = source.flatten().unwrap();
2289 let modules = crate::ModuleView::from_graph(&source, &graph).unwrap();
2290 let (data, ranges) = module_html_geometry(&graph, 2.0, &[], Some(&modules));
2291 let ordinary = block_graph_as_gltf_data(&graph, 2.0);
2292 assert_eq!(data.triangles, ordinary.triangles);
2293 assert_eq!(data.lines, ordinary.lines);
2294 assert_eq!(data.to_buffer_bytes(), ordinary.to_buffer_bytes());
2295
2296 let (model, toggles) = data.module_model(&ranges);
2297 assert_eq!(model.buffers[0].uri, ordinary.to_model().buffers[0].uri);
2298 let mut primitive_index = 0;
2299 let mut owners = BTreeSet::new();
2300 let mut seams = 0;
2301 let mut transparent = 0;
2302 for (color, triangles) in &data.triangles {
2303 let mut next_triangle = 0;
2304 for (owner, range) in &ranges[color] {
2305 assert_eq!(range.start, next_triangle);
2306 next_triangle = range.end;
2307 transparent += usize::from(color.a < 255);
2308 let primitive = &model.meshes[0].primitives[primitive_index];
2309 primitive_index += 1;
2310 let positions =
2311 &model.accessors[primitive.attributes[&Valid(Semantic::Positions)].value()];
2312 let colors =
2313 &model.accessors[primitive.attributes[&Valid(Semantic::Colors(0))].value()];
2314 assert_eq!(positions.byte_offset.unwrap().0 as usize, range.start * 36);
2315 assert_eq!(colors.byte_offset.unwrap().0 as usize, range.start * 48);
2316 assert_eq!(positions.count.0 as usize, range.len() * 3);
2317 assert_eq!(positions.count, colors.count);
2318 let index = primitive.material.unwrap().value();
2319 let material = &model.materials[index];
2320 if let Some(owner) = owner {
2321 owners.insert(*owner);
2322 let (_, _, original, highlight, alpha) = toggles
2323 .iter()
2324 .find(|(group, material, ..)| group == owner && *material == index)
2325 .unwrap();
2326 assert_eq!(*original, linear_palette(*color));
2327 assert_eq!(*highlight, linear_palette(crate::module_color(*owner)));
2328 assert_eq!(
2329 material.pbr_metallic_roughness.base_color_factor.0,
2330 *highlight
2331 );
2332 assert_eq!(*alpha, if color.a < 255 { "BLEND" } else { "OPAQUE" });
2333 } else {
2334 seams += 1;
2335 assert_eq!(
2336 material.alpha_mode,
2337 Valid(if color.a < 255 {
2338 AlphaMode::Blend
2339 } else {
2340 AlphaMode::Opaque
2341 })
2342 );
2343 assert_eq!(
2344 material.pbr_metallic_roughness.base_color_factor.0,
2345 linear_palette(*color)
2346 );
2347 }
2348 }
2349 assert_eq!(next_triangle, triangles.len());
2350 }
2351 assert_eq!(owners.len(), modules.modules().len());
2352 assert!(seams > 0);
2353 assert!(transparent > 0);
2354 assert_eq!(
2355 model.meshes[0].primitives.len() - primitive_index,
2356 data.lines.len()
2357 );
2358 }
2359
2360 #[test]
2361 fn html_viewer_uses_editor_projection_and_no_tone_mapping() {
2362 let html = GltfData::default().to_html_str().unwrap();
2363 assert!(html.contains("model-viewer/4.2.0/"));
2364 assert!(html.contains("background-color: #D8E0E6"));
2365 assert!(html.contains("camera-orbit=\"45deg 54.7356deg auto\""));
2366 assert!(html.contains("field-of-view=\"45deg\""));
2367 assert!(html.contains("max-field-of-view=\"45deg\""));
2368 assert!(html.contains("tone-mapping=\"none\""));
2369 assert!(html.contains("aria-label=\"Reset graph view\""));
2370 assert!(html.contains("Graph axes: X and Y are spatial, Z is time"));
2371 for axis in ["X", "Y", "Z"] {
2372 assert!(html.contains(&format!("data-axis=\"{axis}\"")));
2373 }
2374 assert!(html.contains("viewer.addEventListener('camera-change', updateAxes)"));
2375 }
2376
2377 fn lerp(min: f32, max: f32, t: f32) -> f32 {
2378 min + (max - min) * t
2379 }
2380
2381 #[test]
2382 fn product_surface_keeps_cube_between_supported_pipes() {
2383 let graph = crate::GalleryItem::CNOT
2384 .build()
2385 .materialize_root_graph()
2386 .expect("gallery flat projection");
2387 let generators = graph.stabilizers().unwrap().generators;
2388 let mut product = generators[1].clone();
2389 product
2390 .stabilizer
2391 .phase_free_mul_assign(&generators[2].stabilizer);
2392 let data = stabilizer_as_gltf_data(&product, &graph, 2.0).unwrap();
2393 assert_patch_rotation_color_at_named(
2394 &data,
2395 Vec3::Z * 3.0,
2396 correlation_surface_color(Pauli::Z),
2397 "Z control sheet crosses its lower junction",
2398 );
2399 }
2400
2401 #[test]
2402 fn moving_prisms_face_outward_and_pop_the_geometric_top() {
2403 let mut kinds = Vec::new();
2404 for x in -1..=1 {
2405 for y in -1..=1 {
2406 if x == 0 && y == 0 {
2407 continue;
2408 }
2409 let movement = ivec2(x, y);
2410 kinds.push(BlockKind::Walking(
2411 WalkingKind::new(WalkingBoundaryKind::ZXZ, movement).unwrap(),
2412 ));
2413 if x == 0 || y == 0 {
2414 for basis in [Basis::X, Basis::Z] {
2415 kinds.push(BlockKind::PatchRotation(
2416 PatchRotationKind::new(basis, movement).unwrap(),
2417 ));
2418 }
2419 }
2420 }
2421 }
2422 for kind in kinds {
2423 let block = Block::new(IVec3::ZERO, kind);
2424 let data = block_as_gltf_data(&block, &BlockGraph::new());
2425 let (min, max) = triangle_bounds(&data);
2426 let center = (min + max) * 0.5;
2427 for &[a, b, c] in data.triangles.values().flatten() {
2428 assert!(
2429 (b - a).cross(c - a).dot(a - center) > EPS,
2430 "inward face: {kind:?} {a:?} {b:?} {c:?}"
2431 );
2432 }
2433 let popped = data.pop_faces_at_directions(&[Direction::ZPLUS]);
2434 let count_at = |z: f32| {
2435 popped
2436 .triangles
2437 .values()
2438 .flatten()
2439 .filter(|triangle| triangle.iter().all(|vertex| (vertex.z - z).abs() < EPS))
2440 .count()
2441 };
2442 assert_eq!(count_at(max.z), 0, "top of {kind:?}");
2443 assert_eq!(count_at(min.z), 2, "bottom of {kind:?}");
2444 }
2445 }
2446
2447 #[test]
2448 fn popping_face_direction_removes_only_that_filled_face() {
2449 let graph = GalleryItem::XMemory
2450 .build()
2451 .materialize_root_graph()
2452 .expect("gallery flat projection");
2453 let data = block_graph_as_gltf_data(&graph, 2.0);
2454 let triangle_count = data.triangles.values().map(Vec::len).sum::<usize>();
2455 let line_count = data.lines.values().map(Vec::len).sum::<usize>();
2456
2457 let popped = data.pop_faces_at_directions(&[Direction::YMINUS]);
2458
2459 assert!(popped.triangles.values().map(Vec::len).sum::<usize>() < triangle_count);
2460 assert_eq!(
2461 popped.lines.values().map(Vec::len).sum::<usize>(),
2462 line_count
2463 );
2464 assert!(popped.triangles.values().flatten().all(|[a, b, c]| {
2465 let normal = (*b - *a).cross(*c - *a).normalize_or_zero();
2466 normal.dot(Direction::YMINUS.to_vec3()) < 0.999
2467 }));
2468 }
2469
2470 #[test]
2471 fn popping_selected_faces_leaves_other_elements_closed() {
2472 let mut graph = BlockGraph::new();
2473 graph.add_block(Block::new(
2474 IVec3::ZERO,
2475 BlockKind::Cube(crate::CubeKind::ZXZ),
2476 ));
2477 graph.add_block(Block::new(IVec3::X, BlockKind::Cube(crate::CubeKind::ZXZ)));
2478 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
2479
2480 let triangle_count = |data: &GltfData| data.triangles.values().map(Vec::len).sum::<usize>();
2481 let closed = block_graph_as_gltf_data(&graph, 2.0);
2482 let scoped = block_graph_as_gltf_data_with_popped_faces(
2483 &graph,
2484 2.0,
2485 &[
2486 GltfFaceSelector::Block {
2487 position: IVec3::ZERO,
2488 face: Direction::YMINUS,
2489 },
2490 GltfFaceSelector::Pipe {
2491 u: IVec3::ZERO,
2492 v: IVec3::X,
2493 face: Direction::YMINUS,
2494 },
2495 ],
2496 );
2497 let global = block_graph_as_gltf_data_with_popped_faces(
2498 &graph,
2499 2.0,
2500 &[GltfFaceSelector::All(Direction::YMINUS)],
2501 );
2502
2503 assert_eq!(triangle_count(&closed) - triangle_count(&scoped), 8);
2504 assert_eq!(triangle_count(&closed) - triangle_count(&global), 10);
2505 }
2506
2507 #[test]
2508 fn connected_port_omits_its_pipe_face_but_keeps_the_outline() {
2509 let port = Block::new(IVec3::Z, BlockKind::Port);
2510 let mut graph = BlockGraph::new();
2511 graph.add_block(Block::new(
2512 IVec3::ZERO,
2513 BlockKind::Cube(crate::CubeKind::ZXZ),
2514 ));
2515 graph.add_block(port.clone());
2516 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::ZPLUS));
2517
2518 let connected = block_as_gltf_data(&port, &graph);
2519 let mut isolated_graph = BlockGraph::new();
2520 isolated_graph.add_block(port.clone());
2521 let isolated = block_as_gltf_data(&port, &isolated_graph);
2522
2523 assert_eq!(
2524 isolated.triangles.values().map(Vec::len).sum::<usize>()
2525 - connected.triangles.values().map(Vec::len).sum::<usize>(),
2526 2
2527 );
2528 assert_eq!(
2529 connected.lines.values().map(Vec::len).sum::<usize>(),
2530 isolated.lines.values().map(Vec::len).sum::<usize>()
2531 );
2532 assert!(connected.triangles.values().flatten().all(|[a, b, c]| {
2533 let normal = (*b - *a).cross(*c - *a).normalize_or_zero();
2534 normal.dot(Direction::ZMINUS.to_vec3()) < 0.999
2535 }));
2536 }
2537
2538 #[test]
2539 fn custom_port_color_keeps_port_alpha() {
2540 let port = Block::new(IVec3::ZERO, BlockKind::Port)
2541 .with_port_color([0xeb, 0x40, 0x34])
2542 .expect("Port accepts a color");
2543 let data = block_as_gltf_data(&port, &BlockGraph::new());
2544
2545 assert!(
2546 data.triangles
2547 .contains_key(&RGBA::from_hex(0xeb4034, RGBA::PORT_GRAY.a))
2548 );
2549 }
2550
2551 fn point_in_triangle(point: Vec3, triangle: [Vec3; 3]) -> bool {
2552 let [a, b, c] = triangle;
2553 let normal = (b - a).cross(c - a);
2554 if normal.length_squared() < EPS {
2555 return false;
2556 }
2557 if (point - a).dot(normal).abs() > EPS {
2558 return false;
2559 }
2560
2561 let ab = b - a;
2562 let ac = c - a;
2563 let offset = point - a;
2564 let dot_ab_ab = ab.dot(ab);
2565 let dot_ab_ac = ab.dot(ac);
2566 let dot_ab_offset = ab.dot(offset);
2567 let dot_ac_ac = ac.dot(ac);
2568 let dot_ac_offset = ac.dot(offset);
2569 let denom = dot_ab_ab * dot_ac_ac - dot_ab_ac * dot_ab_ac;
2570 if denom.abs() < EPS {
2571 return false;
2572 }
2573 let inv_denom = 1.0 / denom;
2574 let u = (dot_ac_ac * dot_ab_offset - dot_ab_ac * dot_ac_offset) * inv_denom;
2575 let v = (dot_ab_ab * dot_ac_offset - dot_ab_ac * dot_ab_offset) * inv_denom;
2576
2577 u >= -EPS && v >= -EPS && u + v <= 1.0 + EPS
2578 }
2579
2580 fn colors_at(data: &GltfData, point: Vec3) -> Vec<RGBA> {
2581 let mut colors: Vec<_> = data
2582 .triangles
2583 .iter()
2584 .filter_map(|(color, triangles)| {
2585 triangles
2586 .iter()
2587 .any(|&triangle| point_in_triangle(point, triangle))
2588 .then_some(*color)
2589 })
2590 .collect();
2591 colors.sort();
2592 colors.dedup();
2593 colors
2594 }
2595
2596 fn assert_patch_rotation_color_at(data: &GltfData, point: Vec3, expected: RGBA) {
2597 let colors = colors_at(data, point);
2598 assert_eq!(colors, vec![expected], "unexpected color at {point:?}");
2599 }
2600
2601 fn assert_patch_rotation_color_at_named(
2602 data: &GltfData,
2603 point: Vec3,
2604 expected: RGBA,
2605 name: &str,
2606 ) {
2607 let colors = colors_at(data, point);
2608 assert_eq!(
2609 colors,
2610 vec![expected],
2611 "unexpected color for {name} at {point:?}"
2612 );
2613 }
2614
2615 fn assert_patch_rotation_empty_at_named(data: &GltfData, point: Vec3, name: &str) {
2616 let colors = colors_at(data, point);
2617 assert!(
2618 colors.is_empty(),
2619 "expected no surface for {name} at {point:?}, got {colors:?}"
2620 );
2621 }
2622
2623 fn triangle_bounds(data: &GltfData) -> (Vec3, Vec3) {
2624 let mut vertices = data.triangles.values().flatten().flatten().copied();
2625 let first = vertices.next().expect("mesh should contain triangles");
2626 vertices.fold((first, first), |(min, max), vertex| {
2627 (min.min(vertex), max.max(vertex))
2628 })
2629 }
2630
2631 fn assert_patch_rotation_time_colors(
2632 data: &GltfData,
2633 min: Vec3,
2634 max: Vec3,
2635 z_min_color: RGBA,
2636 z_max_color: RGBA,
2637 ) {
2638 assert_patch_rotation_color_at(
2639 data,
2640 vec3(lerp(min.x, max.x, 0.37), lerp(min.y, max.y, 0.23), min.z),
2641 z_min_color,
2642 );
2643 assert_patch_rotation_color_at(
2644 data,
2645 vec3(lerp(min.x, max.x, 0.29), lerp(min.y, max.y, 0.61), max.z),
2646 z_max_color,
2647 );
2648 }
2649
2650 fn assert_patch_rotation_colors(basis: Basis, movement: IVec2) {
2651 let pipe_length = 1.0;
2652 let stride = pipe_length + 1.0;
2653 let kind = PatchRotationKind::new(basis, movement).unwrap();
2654 let data = patch_rotation_as_gltf_data(kind, pipe_length);
2655 let (min, max) = patch_rotation_bounds(IVec3::new(movement.x, movement.y, 1), stride);
2656 let colors = PatchRotationColors::new(kind);
2657 let measurement_basis = kind.x_axis_boundary_basis();
2658
2659 assert_patch_rotation_time_colors(&data, min, max, colors.start_time, colors.end_time);
2660 assert_eq!(colors.start_time, RGBA::from(measurement_basis.flip()));
2661 assert_eq!(colors.end_time, RGBA::from(measurement_basis));
2662
2663 let local = |x: f32, y: f32, z: f32| patch_rotation_local_to_global(kind, vec3(x, y, z));
2664 let mixed_x = match colors.mixed_side {
2665 PatchRotationMixedSide::Left => -0.5,
2666 PatchRotationMixedSide::Right => 0.5,
2667 };
2668 let extended_x = -mixed_x;
2669 let mid_y = stride * 0.5;
2670 let low_z = 0.25;
2671 let high_z = stride - 0.25;
2672
2673 assert_patch_rotation_color_at(&data, local(0.0, -0.5, low_z), colors.after);
2674 assert_patch_rotation_color_at(&data, local(0.0, stride + 0.5, low_z), colors.before);
2675 assert_patch_rotation_color_at(&data, local(mixed_x, mid_y * 0.5, low_z), colors.before);
2676 assert_patch_rotation_color_at(
2677 &data,
2678 local(mixed_x, mid_y + mid_y * 0.5, low_z),
2679 colors.after,
2680 );
2681 assert_patch_rotation_color_at(&data, local(extended_x, mid_y * 0.5, low_z), colors.before);
2682 assert_patch_rotation_color_at(&data, local(extended_x, mid_y * 0.5, high_z), colors.after);
2683 }
2684
2685 #[test]
2686 fn boundary_blocks_render_with_overflowing_neighbor_directions_absent() {
2687 for position in [IVec3::splat(i32::MIN), IVec3::splat(i32::MAX)] {
2688 let block = Block::new(position, BlockKind::Cube(crate::CubeKind::ZXZ));
2689 let mut graph = BlockGraph::new();
2690 graph.try_add_block(block.clone()).unwrap();
2691
2692 let data = block_as_gltf_data(&block, &graph);
2693 assert!(!data.triangles.is_empty());
2694 }
2695
2696 let scaled = Block::new(
2697 IVec3::new(i32::MAX, 0, 0),
2698 BlockKind::Cube(crate::CubeKind::ZXZ),
2699 )
2700 .with_height("2d".parse().expect("valid height"))
2701 .unwrap();
2702 let mut scaled_graph = BlockGraph::new();
2703 scaled_graph.try_add_block(scaled.clone()).unwrap();
2704 assert!(
2705 !block_as_gltf_data(&scaled, &scaled_graph)
2706 .triangles
2707 .is_empty()
2708 );
2709
2710 assert!(pipe_for_node_direction(&scaled_graph, &scaled, Direction::XPLUS).is_none());
2711 }
2712
2713 #[test]
2714 fn boundary_y_block_uses_its_representable_temporal_neighbor() {
2715 let below = IVec3::new(0, 0, i32::MAX - 1);
2716 let position = IVec3::new(0, 0, i32::MAX);
2717 let block = Block::new(position, BlockKind::Y);
2718 let mut graph = BlockGraph::new();
2719 graph
2720 .try_add_block(Block::new(below, BlockKind::Port))
2721 .unwrap();
2722 graph.try_add_block(block.clone()).unwrap();
2723 graph
2724 .try_add_pipe(Pipe::new(below, Direction::ZPLUS))
2725 .unwrap();
2726
2727 let data = block_as_gltf_data(&block, &graph);
2728 let (min, max) = triangle_bounds(&data);
2729 assert!((min.z + 0.5).abs() < EPS, "unexpected min z: {min:?}");
2730 assert!(max.z.abs() < EPS, "unexpected max z: {max:?}");
2731 }
2732
2733 #[test]
2734 fn measurement_block_is_a_past_facing_basis_colored_cap() {
2735 for basis in [Basis::X, Basis::Z] {
2736 let block = Block::new(IVec3::ZERO, BlockKind::Measurement(basis));
2737 let mut graph = BlockGraph::new();
2738 graph.add_block(block.clone());
2739
2740 let data = block_as_gltf_data(&block, &graph);
2741 let (min, max) = triangle_bounds(&data);
2742 assert!((min.z + 0.5).abs() < EPS, "unexpected min z: {min:?}");
2743 assert!(max.z.abs() < EPS, "unexpected max z: {max:?}");
2744 assert!(data.triangles.contains_key(&RGBA::from(basis)));
2745 }
2746 }
2747
2748 #[test]
2749 fn patch_rotation_visualization_colors_all_movements() {
2750 for basis in [Basis::X, Basis::Z] {
2751 for movement in [ivec2(1, 0), ivec2(-1, 0), ivec2(0, 1), ivec2(0, -1)] {
2752 assert_patch_rotation_colors(basis, movement);
2753 }
2754 }
2755 }
2756
2757 #[test]
2758 fn patch_rotation_visualization_mixed_side_matches_construction_table() {
2759 use PatchRotationMixedSide::{Left, Right};
2760
2761 let cases = [
2762 (Basis::X, ivec2(-1, 0), Left),
2763 (Basis::X, ivec2(1, 0), Right),
2764 (Basis::X, ivec2(0, -1), Left),
2765 (Basis::X, ivec2(0, 1), Left),
2766 (Basis::Z, ivec2(-1, 0), Right),
2767 (Basis::Z, ivec2(1, 0), Left),
2768 (Basis::Z, ivec2(0, -1), Right),
2769 (Basis::Z, ivec2(0, 1), Right),
2770 ];
2771
2772 for (basis, movement, expected) in cases {
2773 let kind = PatchRotationKind::new(basis, movement).unwrap();
2774 assert_eq!(PatchRotationMixedSide::for_kind(kind), expected);
2775 }
2776 }
2777
2778 #[test]
2779 fn spatial_pipe_between_scaled_cubes_matches_cube_height() {
2780 let mut graph = BlockGraph::new();
2781 graph.add_block(
2782 Block::new(IVec3::ZERO, BlockKind::Cube(crate::CubeKind::ZXZ))
2783 .with_height("2d".parse().expect("valid height"))
2784 .expect("valid scaled cube"),
2785 );
2786 graph.add_block(
2787 Block::new(IVec3::X, BlockKind::Cube(crate::CubeKind::ZXZ))
2788 .with_height("2d".parse().expect("valid height"))
2789 .expect("valid scaled cube"),
2790 );
2791 let pipe = Pipe::new(IVec3::ZERO, Direction::XPLUS);
2792 graph.add_pipe(pipe.clone());
2793
2794 let data = pipe_between_positions_as_gltf_data(IVec3::ZERO, IVec3::X, &pipe, &graph, 1.0);
2795 let (min, max) = triangle_bounds(&data);
2796
2797 assert!((min.z + 0.5).abs() < EPS, "unexpected min z: {min:?}");
2798 assert!((max.z - 2.5).abs() < EPS, "unexpected max z: {max:?}");
2799 }
2800
2801 #[test]
2802 fn standalone_pipe_rendering_handles_distant_coordinate_extremes() {
2803 let u = Block::new(IVec3::splat(i32::MIN), BlockKind::Port);
2804 let v = Block::new(IVec3::splat(i32::MAX), BlockKind::Port);
2805 let pipe = Pipe::new(u.pos(), Direction::XPLUS);
2806 let graph = BlockGraph::new();
2807
2808 let positioned = pipe_between_positions_as_gltf_data(u.pos(), v.pos(), &pipe, &graph, 1.0);
2809 assert!(!positioned.triangles.is_empty());
2810
2811 let wrapped = pipe_as_gltf_data(&u, &v, &pipe, &graph, 1.0);
2812 assert!(!wrapped.triangles.is_empty());
2813 }
2814
2815 #[test]
2816 fn stabilizer_visualization_scales_cube_node_surface_height() {
2817 let mut graph = BlockGraph::new();
2818 graph.add_block(
2819 Block::new(IVec3::ZERO, BlockKind::Cube(crate::CubeKind::ZXZ))
2820 .with_height("2d".parse().expect("valid height"))
2821 .expect("valid scaled cube"),
2822 );
2823 let stabilizer = Stabilizer {
2824 paulis: PauliString::new(0),
2825 sign: false,
2826 port_stabilizer: Default::default(),
2827 interior_nodes: [(IVec3::ZERO, Pauli::Z)].into_iter().collect(),
2828 interior_edges: Default::default(),
2829 };
2830 let generator = StabilizerGenerator::new(stabilizer, StabilizerRowKind::Logical);
2831
2832 let data = stabilizer_as_gltf_data(&generator, &graph, 1.0)
2833 .expect("scaled cube stabilizer should render");
2834 let (min, max) = triangle_bounds(&data);
2835
2836 assert!((min.z + 0.5).abs() < EPS, "unexpected min z: {min:?}");
2837 assert!((max.z - 2.5).abs() < EPS, "unexpected max z: {max:?}");
2838 }
2839
2840 #[test]
2841 fn measurement_generator_label_is_derived_at_render_time() {
2842 let graph = GalleryItem::T
2843 .build()
2844 .materialize_root_graph()
2845 .expect("gallery flat projection");
2846 let stabilizers = graph.stabilizers().expect("T stabilizers");
2847 let generator = stabilizers
2848 .generators
2849 .iter()
2850 .find(|generator| generator.is_measurement())
2851 .expect("T has a measurement generator");
2852
2853 let data = stabilizer_as_gltf_data(generator, &graph, 2.0)
2854 .expect("measurement generator should render");
2855
2856 assert_eq!(data.texts.len(), 1);
2857 assert_eq!(data.texts[0].text, generator.measurement_name().unwrap());
2858 }
2859
2860 #[test]
2861 fn stabilizer_visualization_scales_spatial_pipe_between_scaled_cubes() {
2862 let mut graph = BlockGraph::new();
2863 graph.add_block(
2864 Block::new(IVec3::ZERO, BlockKind::Cube(crate::CubeKind::ZXZ))
2865 .with_height("2d".parse().expect("valid height"))
2866 .expect("valid scaled cube"),
2867 );
2868 graph.add_block(
2869 Block::new(IVec3::X, BlockKind::Cube(crate::CubeKind::ZXZ))
2870 .with_height("2d".parse().expect("valid height"))
2871 .expect("valid scaled cube"),
2872 );
2873 graph.add_pipe(Pipe::new(IVec3::ZERO, Direction::XPLUS));
2874 let stabilizer = Stabilizer {
2875 paulis: PauliString::new(0),
2876 sign: false,
2877 port_stabilizer: Default::default(),
2878 interior_nodes: Default::default(),
2879 interior_edges: [((IVec3::ZERO, IVec3::X), Pauli::Z)].into_iter().collect(),
2880 };
2881 let generator = StabilizerGenerator::new(stabilizer, StabilizerRowKind::Logical);
2882
2883 let data = stabilizer_as_gltf_data(&generator, &graph, 1.0)
2884 .expect("scaled cube pipe stabilizer should render");
2885 let (min, max) = triangle_bounds(&data);
2886
2887 assert!((min.z + 0.5).abs() < EPS, "unexpected min z: {min:?}");
2888 assert!((max.z - 2.5).abs() < EPS, "unexpected max z: {max:?}");
2889 }
2890
2891 fn assert_patch_rotation_stabilizer_support(
2892 basis: Basis,
2893 movement: IVec2,
2894 pauli: Pauli,
2895 expected_shape: Pauli,
2896 ) {
2897 let kind = PatchRotationKind::new(basis, movement).unwrap();
2898 let stride = 3.0;
2899 let mut data = GltfData::default();
2900 add_patch_rotation_node_surface(IVec3::ZERO, kind, pauli, stride, &mut data);
2901
2902 let color = correlation_surface_color(pauli);
2903 let colors = PatchRotationColors::new(kind);
2904 let (mixed_x0, mixed_x1) = match colors.mixed_side {
2905 PatchRotationMixedSide::Left => (-0.5, 0.0),
2906 PatchRotationMixedSide::Right => (0.0, 0.5),
2907 };
2908 let local = |x: f32, y: f32, z: f32| patch_rotation_local_to_global(kind, vec3(x, y, z));
2909
2910 match expected_shape {
2911 Pauli::Z => {
2912 assert_patch_rotation_color_at_named(
2913 &data,
2914 local(f32::midpoint(mixed_x0, mixed_x1), 1.5, 0.5),
2915 color,
2916 &format!("{basis:?} {movement:?} {pauli:?} side leg"),
2917 );
2918 assert_patch_rotation_color_at_named(
2919 &data,
2920 local(0.0, 1.0, 0.5),
2921 color,
2922 &format!("{basis:?} {movement:?} {pauli:?} vertical leg"),
2923 );
2924 assert_patch_rotation_color_at_named(
2925 &data,
2926 local(0.0, 3.0, 2.5),
2927 color,
2928 &format!("{basis:?} {movement:?} {pauli:?} final line"),
2929 );
2930 }
2931 Pauli::X => {
2932 assert_patch_rotation_color_at_named(
2933 &data,
2934 local(0.0, 0.0, 0.0),
2935 color,
2936 &format!("{basis:?} {movement:?} {pauli:?} initial line"),
2937 );
2938 assert_patch_rotation_color_at_named(
2939 &data,
2940 local(f32::midpoint(mixed_x0, mixed_x1), 1.5, 2.0),
2941 color,
2942 &format!("{basis:?} {movement:?} {pauli:?} side leg"),
2943 );
2944 assert_patch_rotation_color_at_named(
2945 &data,
2946 local(0.0, 2.0, 2.0),
2947 color,
2948 &format!("{basis:?} {movement:?} {pauli:?} vertical leg"),
2949 );
2950 }
2951 Pauli::I | Pauli::Y => unreachable!("patch rotation stabilizer shapes are X/Z"),
2952 }
2953 }
2954
2955 #[test]
2956 fn patch_rotation_stabilizer_visualization_matches_variant_table() {
2957 for basis in [Basis::X, Basis::Z] {
2958 for (movement, shape) in [
2961 (ivec2(1, 0), Pauli::Z),
2962 (ivec2(-1, 0), Pauli::Z),
2963 (ivec2(0, 1), Pauli::X),
2964 (ivec2(0, -1), Pauli::X),
2965 ] {
2966 assert_patch_rotation_stabilizer_support(
2967 basis,
2968 movement,
2969 Pauli::from(basis),
2970 shape,
2971 );
2972 assert_patch_rotation_stabilizer_support(
2973 basis,
2974 movement,
2975 Pauli::from(basis.flip()),
2976 shape.flip(),
2977 );
2978 }
2979 }
2980 }
2981
2982 #[test]
2983 fn x_plus_y_stabilizer_visualization_matches_observable_evolution() {
2984 let kind = PatchRotationKind::new(Basis::X, ivec2(0, 1)).unwrap();
2985 let stride = 3.0;
2986 let mut z_data = GltfData::default();
2987 let mut x_data = GltfData::default();
2988 add_patch_rotation_node_surface(IVec3::ZERO, kind, Pauli::Z, stride, &mut z_data);
2989 add_patch_rotation_node_surface(IVec3::ZERO, kind, Pauli::X, stride, &mut x_data);
2990
2991 assert_patch_rotation_color_at_named(
2992 &z_data,
2993 vec3(0.0, 1.0, 0.5),
2994 correlation_surface_color(Pauli::Z),
2995 "x_plus_y Z input L vertical leg",
2996 );
2997 assert_patch_rotation_empty_at_named(
2998 &z_data,
2999 vec3(0.0, 2.0, 0.5),
3000 "x_plus_y Z excluded lower input leg",
3001 );
3002 assert_patch_rotation_color_at_named(
3003 &z_data,
3004 vec3(0.25, 1.0, 2.5),
3005 correlation_surface_color(Pauli::Z),
3006 "x_plus_y Z 3/4 sheet mixed half",
3007 );
3008 assert_patch_rotation_empty_at_named(
3009 &z_data,
3010 vec3(-0.25, 1.0, 2.5),
3011 "x_plus_y Z 3/4 sheet excluded half",
3012 );
3013 assert_patch_rotation_color_at_named(
3014 &z_data,
3015 vec3(0.0, 3.0, 3.0),
3016 correlation_surface_color(Pauli::Z),
3017 "x_plus_y Z output midpoint line",
3018 );
3019 assert_patch_rotation_empty_at_named(
3020 &z_data,
3021 vec3(0.0, 2.75, 3.0),
3022 "x_plus_y Z excluded output line",
3023 );
3024
3025 assert_patch_rotation_color_at_named(
3026 &x_data,
3027 vec3(0.0, 0.0, 0.0),
3028 correlation_surface_color(Pauli::X),
3029 "x_plus_y X shifted input line",
3030 );
3031 assert_patch_rotation_empty_at_named(
3032 &x_data,
3033 vec3(0.0, 0.5, 0.0),
3034 "x_plus_y X excluded input line",
3035 );
3036 assert_patch_rotation_color_at_named(
3037 &x_data,
3038 vec3(0.0, 0.5, 0.5),
3039 correlation_surface_color(Pauli::X),
3040 "x_plus_y X 1/4 sheet input band",
3041 );
3042 assert_patch_rotation_empty_at_named(
3043 &x_data,
3044 vec3(-0.25, 2.0, 0.5),
3045 "x_plus_y X 1/4 sheet excluded half",
3046 );
3047 assert_patch_rotation_color_at_named(
3048 &x_data,
3049 vec3(-0.25, 1.5, 2.0),
3050 correlation_surface_color(Pauli::X),
3051 "x_plus_y X final L horizontal leg",
3052 );
3053 assert_patch_rotation_color_at_named(
3054 &x_data,
3055 vec3(0.0, 2.0, 2.0),
3056 correlation_surface_color(Pauli::X),
3057 "x_plus_y X final L vertical leg",
3058 );
3059 }
3060
3061 #[test]
3062 fn stabilizer_visualization_draws_patch_rotation_node_support() {
3063 let kind = PatchRotationKind::new(Basis::X, ivec2(1, 0)).unwrap();
3064 let mut graph = BlockGraph::new();
3065 graph.add_block(Block::new(IVec3::ZERO, BlockKind::PatchRotation(kind)));
3066
3067 let interior_nodes = [(IVec3::ZERO, Pauli::Z)].into_iter().collect();
3068 let stabilizer = Stabilizer {
3069 paulis: PauliString::new(0),
3070 sign: false,
3071 port_stabilizer: Default::default(),
3072 interior_nodes,
3073 interior_edges: Default::default(),
3074 };
3075 let generator = StabilizerGenerator::new(stabilizer, StabilizerRowKind::Logical);
3076
3077 let data = stabilizer_as_gltf_data(&generator, &graph, 3.0)
3078 .expect("patch rotation stabilizer should render");
3079 assert_patch_rotation_color_at(&data, vec3(2.0, 0.0, 2.0), RGBA::Z_PURE_BLUE);
3080 }
3081
3082 #[test]
3083 fn stabilizer_visualization_handles_walking_end_pipe() {
3084 let mut graph = BlockGraph::new();
3085 graph.add_block(Block::new(
3086 IVec3::new(0, 0, 0),
3087 BlockKind::Walking(
3088 WalkingKind::new(WalkingBoundaryKind::ZXZ, IVec2::new(1, 1)).unwrap(),
3089 ),
3090 ));
3091 graph.add_block(Block::new(IVec3::new(1, 1, 2), BlockKind::Port));
3092 graph.add_block(Block::new(IVec3::new(0, 0, -1), BlockKind::Port));
3093 graph.add_pipe(Pipe::new(IVec3::new(1, 1, 1), Direction::ZPLUS));
3094 graph.add_pipe(Pipe::new(IVec3::new(0, 0, 0), Direction::ZMINUS));
3095
3096 let stabilizers = graph.stabilizers().expect("walking graph stabilizers");
3097 assert!(!stabilizers.generators.is_empty());
3098 for generator in &stabilizers.generators {
3099 let mesh = stabilizer_as_gltf_data(generator, &graph, 2.0)
3100 .expect("walking stabilizer should convert to mesh data");
3101 assert!(!mesh.triangles.is_empty());
3102 }
3103 }
3104
3105 #[test]
3106 fn stabilizer_visualization_draws_walking_surface_from_walking_node_support() {
3107 let graph = parse_blog_to_graph(
3108 "BLOG 1.0\n\n 0: XZZ [0, 0, 0]\n 1: walk XZZ [0, 0, 1] -> [1, 1, 2]\n [0, 0, 0] -> +Z\n",
3109 )
3110 .expect("walking start-pipe graph should parse");
3111 let stabilizers = graph.stabilizers().expect("walking graph stabilizers");
3112 let stride = 3.0;
3113 let expected_top = IVec3::new(1, 1, 2).as_vec3() * stride + Vec3::Z * 0.5;
3114
3115 assert!(stabilizers.generators.iter().any(|generator| {
3116 if !generator
3117 .stabilizer
3118 .interior_nodes
3119 .contains_key(&IVec3::new(0, 0, 1))
3120 {
3121 return false;
3122 }
3123 let mesh = stabilizer_as_gltf_data(generator, &graph, 2.0)
3124 .expect("walking stabilizer should convert to mesh data");
3125 mesh.triangles.values().flatten().flatten().any(|vertex| {
3126 vertex.abs_diff_eq(expected_top + Vec3::X * 0.5, 1e-5)
3127 || vertex.abs_diff_eq(expected_top - Vec3::X * 0.5, 1e-5)
3128 || vertex.abs_diff_eq(expected_top + Vec3::Y * 0.5, 1e-5)
3129 || vertex.abs_diff_eq(expected_top - Vec3::Y * 0.5, 1e-5)
3130 })
3131 }));
3132 }
3133}