1use std::f32::consts::{FRAC_PI_2, PI};
11
12#[repr(C)]
20#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
21pub(crate) struct AxesVertex {
22 pub(crate) position: [f32; 2],
24 pub(crate) color: [f32; 4],
26}
27
28impl AxesVertex {
29 pub(crate) fn buffer_layout() -> wgpu::VertexBufferLayout<'static> {
31 wgpu::VertexBufferLayout {
32 array_stride: std::mem::size_of::<AxesVertex>() as wgpu::BufferAddress,
33 step_mode: wgpu::VertexStepMode::Vertex,
34 attributes: &[
35 wgpu::VertexAttribute {
36 offset: 0,
37 shader_location: 0,
38 format: wgpu::VertexFormat::Float32x2,
39 },
40 wgpu::VertexAttribute {
41 offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
42 shader_location: 1,
43 format: wgpu::VertexFormat::Float32x4,
44 },
45 ],
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
56pub struct AxisView {
57 pub orientation: glam::Quat,
59 pub axis_index: usize,
61}
62
63const X_COLOR: [f32; 4] = [0.878, 0.322, 0.322, 1.0]; const Y_COLOR: [f32; 4] = [0.361, 0.722, 0.361, 1.0]; const Z_COLOR: [f32; 4] = [0.290, 0.620, 1.000, 1.0]; const ORIGIN_OFFSET: f32 = 40.0;
70const LINE_LENGTH: f32 = 30.0;
71const LINE_HALF_WIDTH: f32 = 1.0;
72const CIRCLE_RADIUS: f32 = 9.0;
73const CIRCLE_SEGMENTS: usize = 24;
74
75pub(crate) fn build_axes_geometry(
86 viewport_w: f32,
87 viewport_h: f32,
88 orientation: glam::Quat,
89) -> Vec<AxesVertex> {
90 let mut verts = Vec::with_capacity(1024);
91
92 let px_to_ndc_x = |px: f32| -> f32 { px * 2.0 / viewport_w };
94 let px_to_ndc_y = |py: f32| -> f32 { py * 2.0 / viewport_h };
95
96 let ox = -1.0 + px_to_ndc_x(ORIGIN_OFFSET);
98 let oy = -1.0 + px_to_ndc_y(ORIGIN_OFFSET);
99
100 let view_right = orientation * glam::Vec3::X;
102 let view_up = orientation * glam::Vec3::Y;
103 let view_fwd = orientation * glam::Vec3::Z; let project = |world_axis: glam::Vec3| -> (f32, f32) {
106 let sx = world_axis.dot(view_right);
107 let sy = world_axis.dot(view_up); (sx * px_to_ndc_x(LINE_LENGTH), sy * px_to_ndc_y(LINE_LENGTH))
109 };
110
111 let axes_world = [glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z];
112 let colors = [X_COLOR, Y_COLOR, Z_COLOR];
113 let offsets: [(f32, f32); 3] = [
114 project(glam::Vec3::X),
115 project(glam::Vec3::Y),
116 project(glam::Vec3::Z),
117 ];
118
119 let mut order: [usize; 3] = [0, 1, 2];
121 order.sort_by(|&a, &b| {
122 let da = axes_world[a].dot(view_fwd);
123 let db = axes_world[b].dot(view_fwd);
124 da.partial_cmp(&db).unwrap()
125 });
126
127 for &i in &order {
128 let (dx, dy) = offsets[i];
129 let color = colors[i];
130 let tip_x = ox + dx;
131 let tip_y = oy + dy;
132
133 let lw_x = px_to_ndc_x(LINE_HALF_WIDTH);
135 let lw_y = px_to_ndc_y(LINE_HALF_WIDTH);
136 let len = (dx * dx + dy * dy).sqrt().max(0.001);
138 let perp_x = -dy / len;
139 let perp_y = dx / len;
140 let px_ = perp_x * lw_x;
141 let py_ = perp_y * lw_y;
142
143 push_quad(
144 &mut verts,
145 [ox + px_, oy + py_],
146 [ox - px_, oy - py_],
147 [tip_x - px_, tip_y - py_],
148 [tip_x + px_, tip_y + py_],
149 color,
150 );
151
152 let bg_color = [color[0] * 0.33, color[1] * 0.33, color[2] * 0.33, 0.7];
154 let cr_x = px_to_ndc_x(CIRCLE_RADIUS);
155 let cr_y = px_to_ndc_y(CIRCLE_RADIUS);
156 push_circle_filled(&mut verts, tip_x, tip_y, cr_x, cr_y, bg_color);
157
158 let ring_inner = 0.82; push_circle_ring(&mut verts, tip_x, tip_y, cr_x, cr_y, ring_inner, color);
161
162 let glyph_hw = px_to_ndc_x(4.5); let glyph_hh = px_to_ndc_y(4.5); let glw_x = px_to_ndc_x(0.8); let glw_y = px_to_ndc_y(0.8);
167 match i {
168 0 => push_letter_x(
169 &mut verts, tip_x, tip_y, glyph_hw, glyph_hh, glw_x, glw_y, color,
170 ),
171 1 => push_letter_y(
172 &mut verts, tip_x, tip_y, glyph_hw, glyph_hh, glw_x, glw_y, color,
173 ),
174 2 => push_letter_z(
175 &mut verts, tip_x, tip_y, glyph_hw, glyph_hh, glw_x, glw_y, color,
176 ),
177 _ => {}
178 }
179 }
180
181 verts
182}
183
184pub fn hit_test(
193 screen_pos: [f32; 2],
194 viewport_rect: [f32; 4],
195 orientation: glam::Quat,
196) -> Option<AxisView> {
197 let vp_x = viewport_rect[0];
198 let vp_y = viewport_rect[1];
199 let vp_h = viewport_rect[3];
200
201 let rel_x = screen_pos[0] - vp_x;
203 let rel_y = vp_h - (screen_pos[1] - vp_y); let ox = ORIGIN_OFFSET;
207 let oy = ORIGIN_OFFSET;
208
209 let view_right = orientation * glam::Vec3::X;
211 let view_up = orientation * glam::Vec3::Y;
212 let view_fwd = orientation * glam::Vec3::Z; let project = |world_axis: glam::Vec3| -> (f32, f32) {
215 let sx = world_axis.dot(view_right);
216 let sy = world_axis.dot(view_up);
217 (ox + sx * LINE_LENGTH, oy + sy * LINE_LENGTH)
218 };
219
220 let axes = [glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z];
221 let targets = [
226 AxisView {
227 orientation: glam::Quat::from_rotation_y(FRAC_PI_2),
228 axis_index: 0,
229 }, AxisView {
231 orientation: glam::Quat::from_rotation_x(FRAC_PI_2),
232 axis_index: 1,
233 }, AxisView {
235 orientation: glam::Quat::IDENTITY,
236 axis_index: 2,
237 }, ];
239
240 let mut order: [usize; 3] = [0, 1, 2];
242 order.sort_by(|&a, &b| {
243 let da = axes[a].dot(view_fwd);
244 let db = axes[b].dot(view_fwd);
245 db.partial_cmp(&da).unwrap() });
247
248 for &i in &order {
249 let (tx, ty) = project(axes[i]);
250 let dx = rel_x - tx;
251 let dy = rel_y - ty;
252 if dx * dx + dy * dy <= CIRCLE_RADIUS * CIRCLE_RADIUS {
253 return Some(targets[i]);
254 }
255 }
256
257 None
258}
259
260fn push_quad(
265 verts: &mut Vec<AxesVertex>,
266 a: [f32; 2],
267 b: [f32; 2],
268 c: [f32; 2],
269 d: [f32; 2],
270 color: [f32; 4],
271) {
272 for &pos in &[a, b, c, a, c, d] {
274 verts.push(AxesVertex {
275 position: pos,
276 color,
277 });
278 }
279}
280
281fn push_circle_filled(
282 verts: &mut Vec<AxesVertex>,
283 cx: f32,
284 cy: f32,
285 rx: f32,
286 ry: f32,
287 color: [f32; 4],
288) {
289 let step = 2.0 * PI / CIRCLE_SEGMENTS as f32;
290 for i in 0..CIRCLE_SEGMENTS {
291 let a0 = step * i as f32;
292 let a1 = step * (i + 1) as f32;
293 verts.push(AxesVertex {
294 position: [cx, cy],
295 color,
296 });
297 verts.push(AxesVertex {
298 position: [cx + rx * a0.cos(), cy + ry * a0.sin()],
299 color,
300 });
301 verts.push(AxesVertex {
302 position: [cx + rx * a1.cos(), cy + ry * a1.sin()],
303 color,
304 });
305 }
306}
307
308fn push_circle_ring(
309 verts: &mut Vec<AxesVertex>,
310 cx: f32,
311 cy: f32,
312 rx: f32,
313 ry: f32,
314 inner_frac: f32,
315 color: [f32; 4],
316) {
317 let step = 2.0 * PI / CIRCLE_SEGMENTS as f32;
318 let irx = rx * inner_frac;
319 let iry = ry * inner_frac;
320 for i in 0..CIRCLE_SEGMENTS {
321 let a0 = step * i as f32;
322 let a1 = step * (i + 1) as f32;
323 let (c0, s0) = (a0.cos(), a0.sin());
324 let (c1, s1) = (a1.cos(), a1.sin());
325 let o0 = [cx + rx * c0, cy + ry * s0];
326 let o1 = [cx + rx * c1, cy + ry * s1];
327 let i0 = [cx + irx * c0, cy + iry * s0];
328 let i1 = [cx + irx * c1, cy + iry * s1];
329 for &pos in &[o0, i0, o1, o1, i0, i1] {
331 verts.push(AxesVertex {
332 position: pos,
333 color,
334 });
335 }
336 }
337}
338
339fn push_letter_x(
341 verts: &mut Vec<AxesVertex>,
342 cx: f32,
343 cy: f32,
344 hw: f32,
345 hh: f32,
346 lw_x: f32,
347 lw_y: f32,
348 color: [f32; 4],
349) {
350 push_line_segment(verts, cx - hw, cy + hh, cx + hw, cy - hh, lw_x, lw_y, color);
352 push_line_segment(verts, cx - hw, cy - hh, cx + hw, cy + hh, lw_x, lw_y, color);
354}
355
356fn push_letter_y(
358 verts: &mut Vec<AxesVertex>,
359 cx: f32,
360 cy: f32,
361 hw: f32,
362 hh: f32,
363 lw_x: f32,
364 lw_y: f32,
365 color: [f32; 4],
366) {
367 push_line_segment(verts, cx - hw, cy + hh, cx, cy, lw_x, lw_y, color);
369 push_line_segment(verts, cx + hw, cy + hh, cx, cy, lw_x, lw_y, color);
371 push_line_segment(verts, cx, cy, cx, cy - hh, lw_x, lw_y, color);
373}
374
375fn push_letter_z(
377 verts: &mut Vec<AxesVertex>,
378 cx: f32,
379 cy: f32,
380 hw: f32,
381 hh: f32,
382 lw_x: f32,
383 lw_y: f32,
384 color: [f32; 4],
385) {
386 push_line_segment(verts, cx - hw, cy + hh, cx + hw, cy + hh, lw_x, lw_y, color);
388 push_line_segment(verts, cx + hw, cy + hh, cx - hw, cy - hh, lw_x, lw_y, color);
390 push_line_segment(verts, cx - hw, cy - hh, cx + hw, cy - hh, lw_x, lw_y, color);
392}
393
394fn push_line_segment(
396 verts: &mut Vec<AxesVertex>,
397 x0: f32,
398 y0: f32,
399 x1: f32,
400 y1: f32,
401 lw_x: f32,
402 lw_y: f32,
403 color: [f32; 4],
404) {
405 let dx = x1 - x0;
406 let dy = y1 - y0;
407 let len = (dx * dx + dy * dy).sqrt().max(0.0001);
408 let px = -(dy / len) * lw_x;
410 let py = (dx / len) * lw_y;
411
412 push_quad(
413 verts,
414 [x0 + px, y0 + py],
415 [x0 - px, y0 - py],
416 [x1 - px, y1 - py],
417 [x1 + px, y1 + py],
418 color,
419 );
420}