1use serde::{Deserialize, Serialize};
2
3use crate::error::{Error, Result};
4
5use super::{IlluminationSource, KVector, Optics};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct LEDSphere {
14 pub angles: Vec<(f64, f64)>,
15 pub radius: f64,
16 pub center_offset: (f64, f64, f64),
18 pub orientation_radians: (f64, f64, f64),
20 pub angular_corrections: Option<Vec<(f64, f64)>>,
22 pub wavelength_override: Option<f64>,
23 pub illumination_order: Option<Vec<usize>>,
24 pub intensity_weights: Option<Vec<f64>>,
25}
26
27impl LEDSphere {
28 pub fn new(angles: Vec<(f64, f64)>, radius: f64) -> Self {
29 Self {
30 angles,
31 radius,
32 center_offset: (0.0, 0.0, 0.0),
33 orientation_radians: (0.0, 0.0, 0.0),
34 angular_corrections: None,
35 wavelength_override: None,
36 illumination_order: None,
37 intensity_weights: None,
38 }
39 }
40
41 pub fn center_offset(mut self, offset: (f64, f64, f64)) -> Self {
42 self.center_offset = offset;
43 self
44 }
45
46 pub fn orientation_deg(mut self, degrees: (f64, f64, f64)) -> Self {
47 self.orientation_radians = (
48 degrees.0.to_radians(),
49 degrees.1.to_radians(),
50 degrees.2.to_radians(),
51 );
52 self
53 }
54
55 pub fn angular_corrections(mut self, corrections: Vec<(f64, f64)>) -> Self {
56 self.angular_corrections = Some(corrections);
57 self
58 }
59
60 pub fn wavelength_override(mut self, wavelength: f64) -> Self {
61 self.wavelength_override = Some(wavelength);
62 self
63 }
64
65 pub fn illumination_order(mut self, order: Vec<usize>) -> Self {
66 self.illumination_order = Some(order);
67 self
68 }
69
70 pub fn intensity_weights(mut self, weights: Vec<f64>) -> Self {
71 self.intensity_weights = Some(weights);
72 self
73 }
74
75 pub fn validate(&self) -> Result<()> {
76 validate_angle_list(&self.angles, "LED sphere angles")?;
77 validate_length(self.radius, "radius")?;
78 validate_pose(self.center_offset, self.orientation_radians)?;
79 if let Some(corrections) = &self.angular_corrections
80 && (corrections.len() != self.angles.len()
81 || corrections
82 .iter()
83 .any(|&(theta, phi)| !theta.is_finite() || !phi.is_finite()))
84 {
85 return Err(Error::InvalidParameter {
86 name: "angular_corrections",
87 reason: format!(
88 "must contain {} finite (delta_theta, delta_phi) pairs",
89 self.angles.len()
90 ),
91 });
92 }
93 validate_wavelength(self.wavelength_override)?;
94 validate_order(self.illumination_order.as_deref(), self.angles.len())?;
95 validate_weights(self.intensity_weights.as_deref(), self.angles.len())
96 }
97
98 fn natural_positions(&self) -> Result<Vec<[f64; 3]>> {
99 self.angles
100 .iter()
101 .enumerate()
102 .map(|(index, &(theta, phi))| {
103 let (delta_theta, delta_phi) = self
104 .angular_corrections
105 .as_ref()
106 .map_or((0.0, 0.0), |values| values[index]);
107 let direction = spherical_direction(theta + delta_theta, phi + delta_phi);
108 source_position(
109 scale(direction, self.radius),
110 self.center_offset,
111 self.orientation_radians,
112 "LED sphere",
113 )
114 })
115 .collect()
116 }
117}
118
119impl IlluminationSource for LEDSphere {
120 fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
121 self.validate()?;
122 optics.validate()?;
123 let k = wavenumber(self.wavelength_override, optics);
124 let natural = self
125 .natural_positions()?
126 .into_iter()
127 .map(|position| position_to_k_vector(position, k))
128 .collect::<Vec<_>>();
129 Ok(reorder(natural, self.illumination_order.as_deref()))
130 }
131
132 fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
133 self.validate()?;
134 Ok(self
135 .intensity_weights
136 .as_ref()
137 .map(|weights| reorder(weights.clone(), self.illumination_order.as_deref())))
138 }
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
149pub struct SphericalLEDArm {
150 pub commanded_angles: Vec<(f64, f64)>,
151 pub arm_length: f64,
152 pub pivot_offset: (f64, f64, f64),
154 pub orientation_radians: (f64, f64, f64),
156 pub theta_zero_radians: f64,
157 pub phi_zero_radians: f64,
158 pub theta_scale: f64,
159 pub phi_scale: f64,
160 pub elevation_axis_tilt_radians: f64,
162 pub theta_backlash_radians: f64,
164 pub phi_backlash_radians: f64,
166 pub wavelength_override: Option<f64>,
167 pub intensity_weights: Option<Vec<f64>>,
168}
169
170impl SphericalLEDArm {
171 pub fn new(commanded_angles: Vec<(f64, f64)>, arm_length: f64) -> Self {
172 Self {
173 commanded_angles,
174 arm_length,
175 pivot_offset: (0.0, 0.0, 0.0),
176 orientation_radians: (0.0, 0.0, 0.0),
177 theta_zero_radians: 0.0,
178 phi_zero_radians: 0.0,
179 theta_scale: 1.0,
180 phi_scale: 1.0,
181 elevation_axis_tilt_radians: 0.0,
182 theta_backlash_radians: 0.0,
183 phi_backlash_radians: 0.0,
184 wavelength_override: None,
185 intensity_weights: None,
186 }
187 }
188
189 pub fn pivot_offset(mut self, offset: (f64, f64, f64)) -> Self {
190 self.pivot_offset = offset;
191 self
192 }
193
194 pub fn orientation_deg(mut self, degrees: (f64, f64, f64)) -> Self {
195 self.orientation_radians = (
196 degrees.0.to_radians(),
197 degrees.1.to_radians(),
198 degrees.2.to_radians(),
199 );
200 self
201 }
202
203 pub fn encoder_zero_deg(mut self, theta: f64, phi: f64) -> Self {
204 self.theta_zero_radians = theta.to_radians();
205 self.phi_zero_radians = phi.to_radians();
206 self
207 }
208
209 pub fn encoder_scale(mut self, theta: f64, phi: f64) -> Self {
210 self.theta_scale = theta;
211 self.phi_scale = phi;
212 self
213 }
214
215 pub fn elevation_axis_tilt_deg(mut self, degrees: f64) -> Self {
216 self.elevation_axis_tilt_radians = degrees.to_radians();
217 self
218 }
219
220 pub fn backlash_deg(mut self, theta: f64, phi: f64) -> Self {
221 self.theta_backlash_radians = theta.to_radians();
222 self.phi_backlash_radians = phi.to_radians();
223 self
224 }
225
226 pub fn wavelength_override(mut self, wavelength: f64) -> Self {
227 self.wavelength_override = Some(wavelength);
228 self
229 }
230
231 pub fn intensity_weights(mut self, weights: Vec<f64>) -> Self {
232 self.intensity_weights = Some(weights);
233 self
234 }
235
236 pub fn validate(&self) -> Result<()> {
237 validate_angle_list(&self.commanded_angles, "arm commanded_angles")?;
238 validate_length(self.arm_length, "arm_length")?;
239 validate_pose(self.pivot_offset, self.orientation_radians)?;
240 if !self.theta_zero_radians.is_finite()
241 || !self.phi_zero_radians.is_finite()
242 || !self.theta_scale.is_finite()
243 || self.theta_scale <= 0.0
244 || !self.phi_scale.is_finite()
245 || self.phi_scale <= 0.0
246 {
247 return Err(Error::InvalidParameter {
248 name: "arm encoder calibration",
249 reason: "zero offsets must be finite and scales must be finite and positive".into(),
250 });
251 }
252 if !self.elevation_axis_tilt_radians.is_finite()
253 || self.elevation_axis_tilt_radians.abs() >= std::f64::consts::FRAC_PI_2
254 {
255 return Err(Error::InvalidParameter {
256 name: "elevation_axis_tilt_radians",
257 reason: "must be finite with magnitude less than pi/2".into(),
258 });
259 }
260 if !self.theta_backlash_radians.is_finite()
261 || self.theta_backlash_radians < 0.0
262 || !self.phi_backlash_radians.is_finite()
263 || self.phi_backlash_radians < 0.0
264 {
265 return Err(Error::InvalidParameter {
266 name: "arm backlash",
267 reason: "backlash widths must be finite and non-negative".into(),
268 });
269 }
270 validate_wavelength(self.wavelength_override)?;
271 validate_weights(
272 self.intensity_weights.as_deref(),
273 self.commanded_angles.len(),
274 )
275 }
276
277 fn positions(&self) -> Result<Vec<[f64; 3]>> {
278 let theta_branches = backlash_branches(&self.commanded_angles, 0);
279 let phi_branches = backlash_branches(&self.commanded_angles, 1);
280 let (axis_sin, axis_cos) = self.elevation_axis_tilt_radians.sin_cos();
281 let elevation_axis = [0.0, axis_cos, axis_sin];
282
283 self.commanded_angles
284 .iter()
285 .enumerate()
286 .map(|(index, &(theta_command, phi_command))| {
287 let theta = self.theta_scale * theta_command
288 + self.theta_zero_radians
289 + 0.5 * self.theta_backlash_radians * theta_branches[index];
290 let phi = self.phi_scale * phi_command
291 + self.phi_zero_radians
292 + 0.5 * self.phi_backlash_radians * phi_branches[index];
293 let home = [0.0, 0.0, self.arm_length];
294 let elevated = rotate_axis_angle(home, elevation_axis, theta);
295 let arm_position = rotate_z(elevated, phi);
296 source_position(
297 arm_position,
298 self.pivot_offset,
299 self.orientation_radians,
300 "spherical LED arm",
301 )
302 })
303 .collect()
304 }
305}
306
307impl IlluminationSource for SphericalLEDArm {
308 fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
309 self.validate()?;
310 optics.validate()?;
311 let k = wavenumber(self.wavelength_override, optics);
312 Ok(self
313 .positions()?
314 .into_iter()
315 .map(|position| position_to_k_vector(position, k))
316 .collect())
317 }
318
319 fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
320 self.validate()?;
321 Ok(self.intensity_weights.clone())
322 }
323}
324
325#[derive(Clone, Debug, Serialize, Deserialize)]
332pub struct RotatingLEDArc {
333 pub led_thetas: Vec<f64>,
335 pub rotation_angles: Vec<f64>,
337 pub radius: f64,
338 pub axis_origin_offset: (f64, f64, f64),
340 pub axis_tilt_radians: (f64, f64),
342 pub led_angular_corrections: Option<Vec<(f64, f64)>>,
344 pub led_radial_offsets: Option<Vec<f64>>,
346 pub rotation_zero_radians: f64,
347 pub rotation_scale: f64,
348 pub rotation_backlash_radians: f64,
350 pub wavelength_override: Option<f64>,
351 pub led_intensity_weights: Option<Vec<f64>>,
353}
354
355impl RotatingLEDArc {
356 pub fn new(led_thetas: Vec<f64>, rotation_angles: Vec<f64>, radius: f64) -> Self {
357 Self {
358 led_thetas,
359 rotation_angles,
360 radius,
361 axis_origin_offset: (0.0, 0.0, 0.0),
362 axis_tilt_radians: (0.0, 0.0),
363 led_angular_corrections: None,
364 led_radial_offsets: None,
365 rotation_zero_radians: 0.0,
366 rotation_scale: 1.0,
367 rotation_backlash_radians: 0.0,
368 wavelength_override: None,
369 led_intensity_weights: None,
370 }
371 }
372
373 pub fn axis_origin_offset(mut self, offset: (f64, f64, f64)) -> Self {
374 self.axis_origin_offset = offset;
375 self
376 }
377
378 pub fn axis_tilt_deg(mut self, x: f64, y: f64) -> Self {
379 self.axis_tilt_radians = (x.to_radians(), y.to_radians());
380 self
381 }
382
383 pub fn led_angular_corrections(mut self, corrections: Vec<(f64, f64)>) -> Self {
384 self.led_angular_corrections = Some(corrections);
385 self
386 }
387
388 pub fn led_radial_offsets(mut self, offsets: Vec<f64>) -> Self {
389 self.led_radial_offsets = Some(offsets);
390 self
391 }
392
393 pub fn rotation_encoder_zero_deg(mut self, degrees: f64) -> Self {
394 self.rotation_zero_radians = degrees.to_radians();
395 self
396 }
397
398 pub fn rotation_encoder_scale(mut self, scale: f64) -> Self {
399 self.rotation_scale = scale;
400 self
401 }
402
403 pub fn rotation_backlash_deg(mut self, degrees: f64) -> Self {
404 self.rotation_backlash_radians = degrees.to_radians();
405 self
406 }
407
408 pub fn wavelength_override(mut self, wavelength: f64) -> Self {
409 self.wavelength_override = Some(wavelength);
410 self
411 }
412
413 pub fn led_intensity_weights(mut self, weights: Vec<f64>) -> Self {
414 self.led_intensity_weights = Some(weights);
415 self
416 }
417
418 pub fn source_count(&self) -> Result<usize> {
419 self.led_thetas
420 .len()
421 .checked_mul(self.rotation_angles.len())
422 .ok_or_else(|| Error::InvalidParameter {
423 name: "rotating LED arc",
424 reason: "source count overflows".into(),
425 })
426 }
427
428 pub fn validate(&self) -> Result<()> {
429 if self.led_thetas.is_empty()
430 || self.led_thetas.iter().any(|&theta| {
431 !theta.is_finite() || !(0.0..std::f64::consts::FRAC_PI_2).contains(&theta)
432 })
433 {
434 return Err(Error::InvalidParameter {
435 name: "led_thetas",
436 reason: "must contain at least one finite angle with 0 <= theta < pi/2".into(),
437 });
438 }
439 if self.rotation_angles.is_empty()
440 || self.rotation_angles.iter().any(|value| !value.is_finite())
441 {
442 return Err(Error::InvalidParameter {
443 name: "rotation_angles",
444 reason: "must contain at least one finite commanded azimuth".into(),
445 });
446 }
447 self.source_count()?;
448 validate_length(self.radius, "radius")?;
449 validate_pose(
450 self.axis_origin_offset,
451 (self.axis_tilt_radians.0, self.axis_tilt_radians.1, 0.0),
452 )?;
453 if self.axis_tilt_radians.0.abs() >= std::f64::consts::FRAC_PI_2
454 || self.axis_tilt_radians.1.abs() >= std::f64::consts::FRAC_PI_2
455 {
456 return Err(Error::InvalidParameter {
457 name: "axis_tilt_radians",
458 reason: "components must have magnitude less than pi/2".into(),
459 });
460 }
461 if let Some(corrections) = &self.led_angular_corrections
462 && (corrections.len() != self.led_thetas.len()
463 || corrections
464 .iter()
465 .enumerate()
466 .any(|(index, &(theta, phi))| {
467 !theta.is_finite()
468 || !phi.is_finite()
469 || !(0.0..std::f64::consts::FRAC_PI_2)
470 .contains(&(self.led_thetas[index] + theta))
471 }))
472 {
473 return Err(Error::InvalidParameter {
474 name: "led_angular_corrections",
475 reason: format!(
476 "must contain {} finite corrections that keep each LED on the quarter-circle cap",
477 self.led_thetas.len()
478 ),
479 });
480 }
481 if let Some(offsets) = &self.led_radial_offsets
482 && (offsets.len() != self.led_thetas.len()
483 || offsets
484 .iter()
485 .any(|offset| !offset.is_finite() || self.radius + offset <= 0.0))
486 {
487 return Err(Error::InvalidParameter {
488 name: "led_radial_offsets",
489 reason: format!(
490 "must contain {} finite offsets with positive corrected radii",
491 self.led_thetas.len()
492 ),
493 });
494 }
495 if !self.rotation_zero_radians.is_finite()
496 || !self.rotation_scale.is_finite()
497 || self.rotation_scale <= 0.0
498 {
499 return Err(Error::InvalidParameter {
500 name: "rotation encoder calibration",
501 reason: "zero must be finite and scale must be finite and positive".into(),
502 });
503 }
504 if !self.rotation_backlash_radians.is_finite() || self.rotation_backlash_radians < 0.0 {
505 return Err(Error::InvalidParameter {
506 name: "rotation_backlash_radians",
507 reason: "must be finite and non-negative".into(),
508 });
509 }
510 validate_wavelength(self.wavelength_override)?;
511 validate_weights(self.led_intensity_weights.as_deref(), self.led_thetas.len())
512 }
513
514 fn positions(&self) -> Result<Vec<[f64; 3]>> {
515 let branches = scalar_backlash_branches(&self.rotation_angles);
516 let mut positions = Vec::with_capacity(self.source_count()?);
517 for (rotation_index, &command) in self.rotation_angles.iter().enumerate() {
518 let rotation = self.rotation_scale * command
519 + self.rotation_zero_radians
520 + 0.5 * self.rotation_backlash_radians * branches[rotation_index];
521 for (led_index, &nominal_theta) in self.led_thetas.iter().enumerate() {
522 let (delta_theta, delta_phi) = self
523 .led_angular_corrections
524 .as_ref()
525 .map_or((0.0, 0.0), |values| values[led_index]);
526 let radius = self.radius
527 + self
528 .led_radial_offsets
529 .as_ref()
530 .map_or(0.0, |values| values[led_index]);
531 let local = scale(
532 spherical_direction(nominal_theta + delta_theta, delta_phi),
533 radius,
534 );
535 let rotated = rotate_z(local, rotation);
536 positions.push(source_position(
537 rotated,
538 self.axis_origin_offset,
539 (self.axis_tilt_radians.0, self.axis_tilt_radians.1, 0.0),
540 "rotating LED arc",
541 )?);
542 }
543 }
544 Ok(positions)
545 }
546}
547
548impl IlluminationSource for RotatingLEDArc {
549 fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
550 self.validate()?;
551 optics.validate()?;
552 let k = wavenumber(self.wavelength_override, optics);
553 Ok(self
554 .positions()?
555 .into_iter()
556 .map(|position| position_to_k_vector(position, k))
557 .collect())
558 }
559
560 fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
561 self.validate()?;
562 Ok(self.led_intensity_weights.as_ref().map(|weights| {
563 self.rotation_angles
564 .iter()
565 .flat_map(|_| weights.iter().copied())
566 .collect()
567 }))
568 }
569}
570
571fn validate_angle_list(angles: &[(f64, f64)], name: &'static str) -> Result<()> {
572 if angles.is_empty()
573 || angles.iter().any(|&(theta, phi)| {
574 !theta.is_finite()
575 || !(0.0..std::f64::consts::FRAC_PI_2).contains(&theta)
576 || !phi.is_finite()
577 })
578 {
579 return Err(Error::InvalidParameter {
580 name,
581 reason: "must contain at least one finite (theta, phi) pair with 0 <= theta < pi/2"
582 .into(),
583 });
584 }
585 Ok(())
586}
587
588fn validate_length(value: f64, name: &'static str) -> Result<()> {
589 if !value.is_finite() || value <= 0.0 {
590 return Err(Error::InvalidParameter {
591 name,
592 reason: "must be finite and positive".into(),
593 });
594 }
595 Ok(())
596}
597
598fn validate_pose(offset: (f64, f64, f64), orientation: (f64, f64, f64)) -> Result<()> {
599 if [
600 offset.0,
601 offset.1,
602 offset.2,
603 orientation.0,
604 orientation.1,
605 orientation.2,
606 ]
607 .into_iter()
608 .any(|value| !value.is_finite())
609 {
610 return Err(Error::InvalidParameter {
611 name: "spherical geometry pose",
612 reason: "offset and orientation components must be finite".into(),
613 });
614 }
615 Ok(())
616}
617
618fn validate_wavelength(wavelength: Option<f64>) -> Result<()> {
619 if wavelength.is_some_and(|value| !value.is_finite() || value <= 0.0) {
620 return Err(Error::InvalidParameter {
621 name: "wavelength_override",
622 reason: "must be finite and positive".into(),
623 });
624 }
625 Ok(())
626}
627
628fn validate_order(order: Option<&[usize]>, count: usize) -> Result<()> {
629 if let Some(order) = order {
630 let mut sorted = order.to_vec();
631 sorted.sort_unstable();
632 sorted.dedup();
633 if order.len() != count
634 || order.iter().any(|&index| index >= count)
635 || sorted.len() != count
636 {
637 return Err(Error::InvalidParameter {
638 name: "illumination_order",
639 reason: format!("must be a permutation of 0..{count}"),
640 });
641 }
642 }
643 Ok(())
644}
645
646fn validate_weights(weights: Option<&[f64]>, count: usize) -> Result<()> {
647 if weights.is_some_and(|values| {
648 values.len() != count
649 || values
650 .iter()
651 .any(|value| !value.is_finite() || *value <= 0.0)
652 }) {
653 return Err(Error::InvalidParameter {
654 name: "intensity_weights",
655 reason: format!("must contain {count} finite positive values"),
656 });
657 }
658 Ok(())
659}
660
661fn spherical_direction(theta: f64, phi: f64) -> [f64; 3] {
662 let (sin_theta, cos_theta) = theta.sin_cos();
663 let (sin_phi, cos_phi) = phi.sin_cos();
664 [sin_theta * cos_phi, sin_theta * sin_phi, cos_theta]
665}
666
667fn source_position(
668 local: [f64; 3],
669 offset: (f64, f64, f64),
670 orientation: (f64, f64, f64),
671 geometry: &'static str,
672) -> Result<[f64; 3]> {
673 let rotated = rotate_pose(local, orientation);
674 let position = [
675 rotated[0] + offset.0,
676 rotated[1] + offset.1,
677 rotated[2] + offset.2,
678 ];
679 let norm =
680 (position[0] * position[0] + position[1] * position[1] + position[2] * position[2]).sqrt();
681 if !norm.is_finite() || norm <= 0.0 || position[2] <= 0.0 {
682 return Err(Error::InvalidParameter {
683 name: geometry,
684 reason: "every transformed source must be finite, distinct from the sample, and on the positive-z illumination hemisphere"
685 .into(),
686 });
687 }
688 Ok(position)
689}
690
691fn position_to_k_vector(position: [f64; 3], k: f64) -> KVector {
692 let distance =
693 (position[0] * position[0] + position[1] * position[1] + position[2] * position[2]).sqrt();
694 KVector::new(k * position[0] / distance, k * position[1] / distance)
695}
696
697fn wavenumber(wavelength_override: Option<f64>, optics: &Optics) -> f64 {
698 std::f64::consts::TAU * optics.medium_index / wavelength_override.unwrap_or(optics.wavelength)
699}
700
701fn reorder<T: Copy>(values: Vec<T>, order: Option<&[usize]>) -> Vec<T> {
702 match order {
703 Some(indices) => indices.iter().map(|&index| values[index]).collect(),
704 None => values,
705 }
706}
707
708fn backlash_branches(commands: &[(f64, f64)], component: usize) -> Vec<f64> {
709 let mut branches = Vec::with_capacity(commands.len());
710 let mut branch = 0.0;
711 branches.push(branch);
712 for pair in commands.windows(2) {
713 let previous = if component == 0 { pair[0].0 } else { pair[0].1 };
714 let current = if component == 0 { pair[1].0 } else { pair[1].1 };
715 let delta = current - previous;
716 if delta != 0.0 {
717 branch = delta.signum();
718 }
719 branches.push(branch);
720 }
721 branches
722}
723
724fn scalar_backlash_branches(commands: &[f64]) -> Vec<f64> {
725 let mut branches = Vec::with_capacity(commands.len());
726 let mut branch = 0.0;
727 branches.push(branch);
728 for pair in commands.windows(2) {
729 let delta = pair[1] - pair[0];
730 if delta != 0.0 {
731 branch = delta.signum();
732 }
733 branches.push(branch);
734 }
735 branches
736}
737
738fn scale(vector: [f64; 3], scalar: f64) -> [f64; 3] {
739 [vector[0] * scalar, vector[1] * scalar, vector[2] * scalar]
740}
741
742fn rotate_pose(vector: [f64; 3], radians: (f64, f64, f64)) -> [f64; 3] {
743 let (sin_x, cos_x) = radians.0.sin_cos();
744 let after_x = [
745 vector[0],
746 cos_x * vector[1] - sin_x * vector[2],
747 sin_x * vector[1] + cos_x * vector[2],
748 ];
749 let (sin_y, cos_y) = radians.1.sin_cos();
750 let after_y = [
751 cos_y * after_x[0] + sin_y * after_x[2],
752 after_x[1],
753 -sin_y * after_x[0] + cos_y * after_x[2],
754 ];
755 rotate_z(after_y, radians.2)
756}
757
758fn rotate_z(vector: [f64; 3], radians: f64) -> [f64; 3] {
759 let (sin, cos) = radians.sin_cos();
760 [
761 cos * vector[0] - sin * vector[1],
762 sin * vector[0] + cos * vector[1],
763 vector[2],
764 ]
765}
766
767fn rotate_axis_angle(vector: [f64; 3], axis: [f64; 3], radians: f64) -> [f64; 3] {
768 let (sin, cos) = radians.sin_cos();
769 let dot = axis[0] * vector[0] + axis[1] * vector[1] + axis[2] * vector[2];
770 let cross = [
771 axis[1] * vector[2] - axis[2] * vector[1],
772 axis[2] * vector[0] - axis[0] * vector[2],
773 axis[0] * vector[1] - axis[1] * vector[0],
774 ];
775 [
776 vector[0] * cos + cross[0] * sin + axis[0] * dot * (1.0 - cos),
777 vector[1] * cos + cross[1] * sin + axis[1] * dot * (1.0 - cos),
778 vector[2] * cos + cross[2] * sin + axis[2] * dot * (1.0 - cos),
779 ]
780}