nightshade_api/web/
hud.rs1use crate::wire::SelectedEntity;
5use leptos::prelude::*;
6
7#[component]
9pub fn ViewportOverlay(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
10 view! { <div class=format!("nightshade-viewport-overlay {class}")>{children()}</div> }
11}
12
13#[component]
15pub fn HudPanel(#[prop(into, optional)] class: String, children: Children) -> impl IntoView {
16 view! { <div class=format!("nightshade-hud {class}")>{children()}</div> }
17}
18
19fn dot(left: [f32; 3], right: [f32; 3]) -> f64 {
20 (left[0] * right[0] + left[1] * right[1] + left[2] * right[2]) as f64
21}
22
23fn project(basis: [[f32; 3]; 3], world: [f32; 3]) -> (f64, f64, f64) {
24 let [right, up, forward] = basis;
25 (dot(world, right), -dot(world, up), dot(world, forward))
26}
27
28const AXES: [([f32; 3], &str, usize, &str); 6] = [
29 ([1.0, 0.0, 0.0], "X", 0, "x"),
30 ([-1.0, 0.0, 0.0], "", 1, "x"),
31 ([0.0, 1.0, 0.0], "Y", 2, "y"),
32 ([0.0, -1.0, 0.0], "", 3, "y"),
33 ([0.0, 0.0, 1.0], "Z", 4, "z"),
34 ([0.0, 0.0, -1.0], "", 5, "z"),
35];
36
37#[component]
41pub fn NavGizmo(
42 #[prop(into)] basis: Signal<[[f32; 3]; 3]>,
43 #[prop(optional)] on_axis: Option<Callback<usize>>,
44) -> impl IntoView {
45 let size = 76.0_f64;
46 let center = size / 2.0;
47 let radius = center - 12.0;
48 view! {
49 <svg
50 class="nightshade-nav-gizmo"
51 viewBox=format!("0 0 {size} {size}")
52 width=size
53 height=size
54 >
55 {move || {
56 let current = basis.get();
57 let mut projected = AXES
58 .iter()
59 .map(|(vector, label, index, axis)| {
60 let (x, y, depth) = project(current, *vector);
61 (center + x * radius, center + y * radius, depth, *label, *index, *axis, index % 2 == 0)
62 })
63 .collect::<Vec<_>>();
64 projected.sort_by(|left, right| {
65 left.2.partial_cmp(&right.2).unwrap_or(std::cmp::Ordering::Equal)
66 });
67 projected
68 .into_iter()
69 .map(|(px, py, depth, label, index, axis, positive)| {
70 let opacity = 0.35 + 0.65 * ((depth + 1.0) / 2.0);
71 let dot_class = format!(
72 "nightshade-gizmo-axis {axis} {}",
73 if positive { "pos" } else { "neg" },
74 );
75 let handle = move |_: web_sys::MouseEvent| {
76 if let Some(callback) = on_axis {
77 callback.run(index);
78 }
79 };
80 view! {
81 <g style=format!("opacity:{opacity:.3}") on:click=handle>
82 <line
83 class="nightshade-gizmo-line"
84 x1=center
85 y1=center
86 x2=px
87 y2=py
88 />
89 <circle class=dot_class cx=px cy=py r=if positive { 8.0 } else { 5.0 } />
90 <text
91 class="nightshade-gizmo-label"
92 x=px
93 y=py
94 text-anchor="middle"
95 dominant-baseline="central"
96 >
97 {label}
98 </text>
99 </g>
100 }
101 })
102 .collect_view()
103 }}
104 </svg>
105 }
106}
107
108#[component]
111pub fn SelectedCard(#[prop(into)] selected: Signal<Option<SelectedEntity>>) -> impl IntoView {
112 view! {
113 <div class="nightshade-selected-card">
114 {move || match selected.get() {
115 Some(entity) => {
116 view! {
117 <div class="nightshade-selected-row">
118 <span class="key">"Name"</span>
119 <span>{entity.name}</span>
120 </div>
121 <div class="nightshade-selected-row">
122 <span class="key">"Id"</span>
123 <span>{entity.id}</span>
124 </div>
125 }
126 .into_any()
127 }
128 None => {
129 view! {
130 <div class="nightshade-selected-row">
131 <span class="key">"Selection"</span>
132 <span>"None"</span>
133 </div>
134 }
135 .into_any()
136 }
137 }}
138 </div>
139 }
140}