Skip to main content

loam_shape/
projected_edges.rs

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/// Samples in the body frame, then clips before adding the world translation.
21#[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
70// do Carmo, Differential Geometry of Curves and Surfaces, 1976, §1.5.
71fn 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
135/// Restores near-pole magnitude; the exact pole produces a non-finite sample for culling.
136// Coxeter, Introduction to Geometry, 1969, §6.9.
137pub 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/// Blends body-frame chords and arcs with `blend` in `[0, 1]`; arc radii interpolate about `arc_center`.
185#[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}