use crate::frame::cull::{Camera, TierLevel};
pub struct T1Pass;
impl T1Pass {
pub fn new() -> Self { Self }
pub fn draw_labels(
&self,
visible: &[(u32, TierLevel)],
positions: &[f32],
titles: &[String],
camera: &Camera,
) {
let t1_particles: Vec<u32> = visible
.iter()
.filter(|(_, tier)| *tier == TierLevel::T1)
.map(|(idx, _)| *idx)
.collect();
if t1_particles.is_empty() {
return;
}
let _ = (positions, titles, camera, t1_particles.len());
}
}
impl Default for T1Pass {
fn default() -> Self { Self::new() }
}
#[derive(Debug, Clone)]
pub struct EdgeLabel {
pub screen_pos: [f32; 2],
pub text: String,
pub depth: f32,
}
pub fn compute_edge_labels(
edge_list: &[(u32, u32)],
positions: &[f32],
titles: &[String],
view_proj: &[[f32; 4]; 4],
viewport: [f32; 2],
) -> Vec<EdgeLabel> {
let [w, h] = viewport;
edge_list.iter().filter_map(&(from, to) {
let fb = from as usize * 3;
let tb = to as usize * 3;
if fb + 2 >= positions.len() tb + 2 >= positions.len() { return None; }
let mid = [
(positions[fb] + positions[tb]) * 0.5,
(positions[fb+1] + positions[tb+1]) * 0.5,
(positions[fb+2] + positions[tb+2]) * 0.5,
];
let m = view_proj;
let cx = m[0][0]*mid[0] + m[1][0]*mid[1] + m[2][0]*mid[2] + m[3][0];
let cy = m[0][1]*mid[0] + m[1][1]*mid[1] + m[2][1]*mid[2] + m[3][1];
let cz = m[0][2]*mid[0] + m[1][2]*mid[1] + m[2][2]*mid[2] + m[3][2];
let cw = m[0][3]*mid[0] + m[1][3]*mid[1] + m[2][3]*mid[2] + m[3][3];
if cw <= 0.0 { return None; }
let ndcx = cx / cw;
let ndcy = cy / cw;
if ndcx < -1.0 ndcx > 1.0 ndcy < -1.0 ndcy > 1.0 { return None; }
let sx = (ndcx * 0.5 + 0.5) * w;
let sy = (1.0 - (ndcy * 0.5 + 0.5)) * h;
let truncate = s: &str s.chars().take(8).collect::<String>();
let from_name = titles.get(from as usize).map(s truncate(s)).unwrap_or_else( "?".into());
let to_name = titles.get(to as usize).map(s truncate(s)).unwrap_or_else( "?".into());
let text = format!("{}→{}", from_name, to_name);
Some(EdgeLabel { screen_pos: [sx, sy], text, depth: cz / cw })
}).collect()
}
#[cfg(test)]
mod tests_t1 {
use super::*;
#[test]
fn edge_label_projects_midpoint() {
let edges = vec![(0u32, 1u32)];
let positions = vec![
-100.0f32, 0.0, 0.0, 100.0f32, 0.0, 0.0, ];
let titles = vec!["alpha".to_string(), "beta".to_string()];
let vp: [[f32; 4]; 4] = [
[0.001, 0.0, 0.0, 0.0],
[0.0, 0.001, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let labels = compute_edge_labels(&edges, &positions, &titles, &vp, [1280.0, 720.0]);
assert_eq!(labels.len(), 1, "should produce one label");
assert!((labels[0].screen_pos[0] - 640.0).abs() < 1.0, "x={}", labels[0].screen_pos[0]);
assert!((labels[0].screen_pos[1] - 360.0).abs() < 1.0, "y={}", labels[0].screen_pos[1]);
assert!(labels[0].text.contains("alpha"), "text={}", labels[0].text);
}
#[test]
fn edge_label_behind_camera_excluded() {
let edges = vec![(0u32, 1u32)];
let positions = vec![0.0f32, 0.0, 0.0, 0.0, 0.0, 10.0];
let titles = vec!["a".to_string(), "b".to_string()];
let vp: [[f32; 4]; 4] = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, -1.0, 0.0],
[0.0, 0.0, -1.0, -1.0], ];
let labels = compute_edge_labels(&edges, &positions, &titles, &vp, [1280.0, 720.0]);
assert_eq!(labels.len(), 0, "behind-camera edge should produce no label");
}
}
pub const T1_IMPOSTOR_MSL: &str = r#"
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
float4 position position;
float2 uv;
float3 center_world;
float radius;
float focus; // φ* luminance driver
};
fragment float4 t1_impostor_frag(
VertexOut in stage_in,
constant float4x4 &view_proj buffer(0))
{
// Ray-sphere SDF in clip-quad space.
float2 d = in.uv * 2.0 - 1.0;
float r2 = dot(d, d);
if (r2 > 1.0) discard_fragment();
// Normal from SDF.
float3 normal = float3(d, sqrt(1.0 - r2));
// Diffuse + focus luminance.
float diffuse = max(0.0, dot(normal, float3(0.577, 0.577, 0.577)));
float luma = mix(0.2, 1.0, in.focus) * diffuse;
return float4(luma, luma, luma, 1.0);
}
"#;