1use crate::LineMesh;
2use glam::{Vec3, Vec4};
3
4const MIN_EDGE_RADIUS: f32 = 1e-6;
5const STEREOGRAPHIC_POLE_FAR_CAP: f32 = 1.0e4;
6
7pub fn stereographic_clip_radius(
8 projection: &loam_math::Projection<4>,
9 view_radius: f32,
10) -> Option<f32> {
11 match *projection {
12 loam_math::Projection::Stereographic { .. } => Some(view_radius),
13 loam_math::Projection::Identity
14 | loam_math::Projection::Orthographic { .. }
15 | loam_math::Projection::Perspective4D { .. }
16 | loam_math::Projection::Schlegel { .. } => None,
17 }
18}
19
20#[allow(clippy::too_many_arguments)]
22pub fn push_projected_chord(
23 mesh: &mut LineMesh<3>,
24 a: Vec4,
25 b: Vec4,
26 color_a: [f32; 4],
27 color_b: [f32; 4],
28 width: f32,
29 projection: &loam_math::Projection<4>,
30 body_pos_r3: Vec3,
31 view_radius: f32,
32 samples: usize,
33) {
34 let samples = samples.max(1);
35 let clip_radius = stereographic_clip_radius(projection, view_radius);
36 let sample_at = |p4: Vec4| {
37 let projected = stereographic_view_point(p4, projection);
38 (projected, (projected + body_pos_r3).to_array())
39 };
40 let (proj0, world0) = sample_at(a);
41 let mut prev_world = world0;
42 let mut prev_c = color_a;
43 let mut prev_in = sample_in_radius(proj0, clip_radius);
44 for k in 1..=samples {
45 let s = k as f32 / samples as f32;
46 let (proj, world) = sample_at(a.lerp(b, s));
47 let c = [
48 color_a[0] + (color_b[0] - color_a[0]) * s,
49 color_a[1] + (color_b[1] - color_a[1]) * s,
50 color_a[2] + (color_b[2] - color_a[2]) * s,
51 color_a[3] + (color_b[3] - color_a[3]) * s,
52 ];
53 let cur_in = sample_in_radius(proj, clip_radius);
54 if prev_in && cur_in {
55 mesh.segments.push((prev_world, world));
56 mesh.colors.push((prev_c, c));
57 mesh.widths.push(width);
58 }
59 prev_world = world;
60 prev_c = c;
61 prev_in = cur_in;
62 }
63}
64
65#[inline]
66pub fn sample_in_radius(projected: Vec3, radius: Option<f32>) -> bool {
67 projected.is_finite() && radius.is_none_or(|r| projected.length_squared() <= r * r)
68}
69
70fn radius_crossing_t(p_in: Vec3, p_out: Vec3, r: f32) -> f32 {
72 let d = p_out - p_in;
73 let a = d.length_squared();
74 if a <= f32::MIN_POSITIVE {
75 return 1.0;
76 }
77 let b = p_in.dot(d);
78 let c = p_in.length_squared() - r * r;
79 let disc = (b * b - a * c).max(0.0);
80 ((-b + disc.sqrt()) / a).clamp(0.0, 1.0)
81}
82
83fn clip_point(
84 p_in: Vec3,
85 p_out: Vec3,
86 c_in: [f32; 4],
87 c_out: [f32; 4],
88 t: f32,
89 body_pos: Vec3,
90) -> ([f32; 3], [f32; 4]) {
91 let boundary = (p_in.lerp(p_out, t) + body_pos).to_array();
92 let color = [
93 c_in[0] + (c_out[0] - c_in[0]) * t,
94 c_in[1] + (c_out[1] - c_in[1]) * t,
95 c_in[2] + (c_out[2] - c_in[2]) * t,
96 c_in[3] + (c_out[3] - c_in[3]) * t,
97 ];
98 (boundary, color)
99}
100
101fn push_clipped_subsegment(
102 mesh: &mut LineMesh<3>,
103 clip_radius: Option<f32>,
104 width: f32,
105 body_pos_r3: Vec3,
106 prev: (Vec3, [f32; 3], [f32; 4], bool),
107 cur: (Vec3, [f32; 3], [f32; 4], bool),
108) {
109 let (prev_proj, prev_world, prev_c, prev_in) = prev;
110 let (cur_proj, cur_world, cur_c, cur_in) = cur;
111 if !prev_proj.is_finite() || !cur_proj.is_finite() {
112 return;
113 }
114 let mut push = |a_world, b_world, a_c, b_c| {
115 mesh.segments.push((a_world, b_world));
116 mesh.colors.push((a_c, b_c));
117 mesh.widths.push(width);
118 };
119 match (clip_radius, prev_in, cur_in) {
120 (None, _, _) | (Some(_), true, true) => push(prev_world, cur_world, prev_c, cur_c),
121 (Some(_), false, false) => {}
122 (Some(r), true, false) => {
123 let t = radius_crossing_t(prev_proj, cur_proj, r);
124 let (bw, bc) = clip_point(prev_proj, cur_proj, prev_c, cur_c, t, body_pos_r3);
125 push(prev_world, bw, prev_c, bc);
126 }
127 (Some(r), false, true) => {
128 let t = radius_crossing_t(cur_proj, prev_proj, r);
129 let (bw, bc) = clip_point(cur_proj, prev_proj, cur_c, prev_c, t, body_pos_r3);
130 push(bw, cur_world, bc, cur_c);
131 }
132 }
133}
134
135pub fn stereographic_view_point(p: Vec4, projection: &loam_math::Projection<4>) -> Vec3 {
138 let proj =
139 <loam_math::EuclideanR4 as loam_math::RasterizableSpace<4>>::project_point(p, projection)
140 .unwrap_or(Vec3::NAN);
141 let loam_math::Projection::Stereographic { pole } = projection else {
142 return proj;
143 };
144 let dot = p.normalize().dot(*pole).clamp(-1.0, 1.0);
145 let raw = 1.0 - dot;
146 if raw < loam_math::STEREOGRAPHIC_POLE_EPSILON && proj.length() <= MIN_EDGE_RADIUS {
147 return Vec3::NAN;
148 }
149 if raw < loam_math::STEREOGRAPHIC_POLE_EPSILON && proj.length() > MIN_EDGE_RADIUS {
150 let true_mag = ((1.0 + dot) / raw.max(f32::MIN_POSITIVE))
151 .sqrt()
152 .min(STEREOGRAPHIC_POLE_FAR_CAP);
153 proj.normalize() * true_mag
154 } else {
155 proj
156 }
157}
158
159pub fn retain_in_radius_triangles(
160 indices: &mut Vec<[u32; 3]>,
161 start_i: usize,
162 start_v: usize,
163 projected: &[Vec3],
164 radius: Option<f32>,
165) {
166 if radius.is_none() {
167 return;
168 }
169 let appended = &mut indices[start_i..];
170 let mut write = 0usize;
171 for read in 0..appended.len() {
172 let tri = appended[read];
173 let in_radius = tri
174 .iter()
175 .all(|&i| sample_in_radius(projected[i as usize - start_v], radius));
176 if in_radius {
177 appended[write] = tri;
178 write += 1;
179 }
180 }
181 indices.truncate(start_i + write);
182}
183
184#[allow(clippy::too_many_arguments)]
186pub fn push_blended_edge(
187 mesh: &mut LineMesh<3>,
188 a: Vec4,
189 b: Vec4,
190 arc_center: Vec4,
191 color_a: [f32; 4],
192 color_b: [f32; 4],
193 width: f32,
194 blend: f32,
195 projection: &loam_math::Projection<4>,
196 body_pos_r3: Vec3,
197 samples: usize,
198 view_radius: f32,
199) {
200 let offset_a = a - arc_center;
201 let offset_b = b - arc_center;
202 let (radius_a, radius_b) = if blend <= 0.0 {
203 (0.0, 0.0)
204 } else {
205 (offset_a.length(), offset_b.length())
206 };
207 if blend <= 0.0 || radius_a < MIN_EDGE_RADIUS || radius_b < MIN_EDGE_RADIUS {
208 let clip_radius = stereographic_clip_radius(projection, view_radius);
209 let a3 = stereographic_view_point(a, projection);
210 let b3 = stereographic_view_point(b, projection);
211 if sample_in_radius(a3, clip_radius) && sample_in_radius(b3, clip_radius) {
212 mesh.segments
213 .push(((a3 + body_pos_r3).to_array(), (b3 + body_pos_r3).to_array()));
214 mesh.colors.push((color_a, color_b));
215 mesh.widths.push(width);
216 }
217 return;
218 }
219
220 let samples = samples.max(1);
221 let clip_radius = stereographic_clip_radius(projection, view_radius);
222 let p0u = offset_a / radius_a;
223 let p1u = offset_b / radius_b;
224 let proj0 = stereographic_view_point(a, projection);
225 let mut prev_proj = proj0;
226 let mut prev_world = (proj0 + body_pos_r3).to_array();
227 let mut prev_c = color_a;
228 let mut prev_in = sample_in_radius(proj0, clip_radius);
229 let mut k = 0;
230 <loam_math::SphericalS3Embedded as loam_math::RasterizableSpace<4>>::tessellate_segment(
231 p0u,
232 p1u,
233 samples,
234 |arc_pt| {
235 let index = k;
236 k += 1;
237 if index == 0 {
238 return;
239 }
240 let k = index;
241 let s = k as f32 / samples as f32;
242 let flat = a.lerp(b, s);
243 let radius = radius_a + (radius_b - radius_a) * s;
244 let sphere = arc_center + radius * arc_pt;
245 let proj = stereographic_view_point(flat.lerp(sphere, blend), projection);
246 let world = (proj + body_pos_r3).to_array();
247 let c = [
248 color_a[0] + (color_b[0] - color_a[0]) * s,
249 color_a[1] + (color_b[1] - color_a[1]) * s,
250 color_a[2] + (color_b[2] - color_a[2]) * s,
251 color_a[3] + (color_b[3] - color_a[3]) * s,
252 ];
253 let cur_in = sample_in_radius(proj, clip_radius);
254 push_clipped_subsegment(
255 mesh,
256 clip_radius,
257 width,
258 body_pos_r3,
259 (prev_proj, prev_world, prev_c, prev_in),
260 (proj, world, c, cur_in),
261 );
262 prev_proj = proj;
263 prev_world = world;
264 prev_c = c;
265 prev_in = cur_in;
266 },
267 );
268}