ranim_core/core_item/
vitem.rs1use color::{AlphaColor, Srgb};
2use glam::{Mat4, Vec3, Vec4};
3
4use crate::{
5 Extract,
6 components::{rgba::Rgba, width::Width},
7 core_item::CoreItem,
8 traits::{FillColor, Interpolatable},
9};
10
11pub const DEFAULT_STROKE_WIDTH: f32 = 0.02;
13
14pub fn vitem_normal_from_points(points: &[Vec4]) -> Vec3 {
22 if points.len() < 3 {
23 return Vec3::Z;
24 }
25
26 let point3 = |point: &Vec4| point.truncate();
27 let origin = point3(&points[0]);
28
29 let mut area_normal = Vec3::ZERO;
32 let mut previous = origin;
33 let mut scale_squared = 0.0_f32;
34 for point in points.iter().step_by(2).skip(1) {
35 let current = point3(point);
36 let previous_offset = previous - origin;
37 let current_offset = current - origin;
38 area_normal += previous_offset.cross(current_offset);
39 scale_squared = scale_squared
40 .max(previous_offset.length_squared())
41 .max(current_offset.length_squared());
42 previous = current;
43 }
44 if area_normal.length_squared() > f32::EPSILON * scale_squared * scale_squared {
45 return area_normal.normalize();
46 }
47
48 let mut direction: Option<Vec3> = None;
52 for point in &points[1..] {
53 let candidate = point3(point) - origin;
54 let candidate_length_squared = candidate.length_squared();
55 if candidate_length_squared <= f32::EPSILON {
56 continue;
57 }
58 if let Some(direction) = direction {
59 let normal = direction.cross(candidate);
60 if normal.length_squared()
61 > f32::EPSILON * direction.length_squared() * candidate_length_squared
62 {
63 return normal.normalize();
64 }
65 } else {
66 direction = Some(candidate);
67 }
68 }
69
70 if let Some(direction) = direction {
71 let direction = direction.normalize();
72 let reference = if direction.dot(Vec3::Z).abs() < 0.99 {
73 Vec3::Z
74 } else {
75 Vec3::X
76 };
77 return (reference - direction * direction.dot(reference)).normalize();
78 }
79
80 Vec3::Z
81}
82
83#[derive(bevy_ecs::component::Component, Debug, Clone, PartialEq)]
84pub struct VItem {
86 pub normal: Option<Vec3>,
90 pub points: Vec<Vec4>,
93 pub transform: Mat4,
95 pub fill_rgbas: Vec<Rgba>,
97 pub stroke_rgbas: Vec<Rgba>,
99 pub stroke_widths: Vec<Width>,
101}
102
103impl Default for VItem {
104 fn default() -> Self {
105 Self {
106 normal: None,
107 points: vec![Vec4::ZERO; 3],
108 transform: Mat4::IDENTITY,
109 stroke_widths: vec![Width::default(); 2],
110 stroke_rgbas: vec![Rgba::default(); 2],
111 fill_rgbas: vec![Rgba::default(); 2],
112 }
113 }
114}
115
116impl Interpolatable for VItem {
117 fn lerp(&self, target: &Self, t: f64) -> Self {
118 Self {
119 normal: if t < 0.5 { self.normal } else { target.normal },
120 points: self.points.lerp(&target.points, t),
121 transform: self.transform.lerp(&target.transform, t),
122 fill_rgbas: self.fill_rgbas.lerp(&target.fill_rgbas, t),
123 stroke_rgbas: self.stroke_rgbas.lerp(&target.stroke_rgbas, t),
124 stroke_widths: self.stroke_widths.lerp(&target.stroke_widths, t),
125 }
126 }
127}
128
129impl Extract for VItem {
130 type Target = CoreItem;
131 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
132 buf.push(CoreItem::VItem(self.clone()));
133 }
134}
135
136impl FillColor for VItem {
137 fn fill_color(&self) -> AlphaColor<Srgb> {
138 let Rgba(rgba) = self.fill_rgbas[0];
139 AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
140 }
141 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
142 self.fill_rgbas.fill(color.into());
143 self
144 }
145 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
146 self.fill_rgbas
147 .iter_mut()
148 .for_each(|rgba| rgba.0.w = opacity);
149 self
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use glam::{Vec3, Vec4};
156
157 use super::vitem_normal_from_points;
158
159 fn point(point: Vec3) -> Vec4 {
160 point.extend(0.0)
161 }
162
163 #[test]
164 fn inferred_normal_matches_the_polygon_plane() {
165 let anchors = [
166 Vec3::new(2.0, -1.0, -1.0),
167 Vec3::new(2.0, 1.0, -1.0),
168 Vec3::new(2.0, 1.0, 1.0),
169 Vec3::new(2.0, -1.0, 1.0),
170 Vec3::new(2.0, -1.0, -1.0),
171 ];
172 let mut points = Vec::new();
173 for edge in anchors.windows(2) {
174 points.push(point(edge[0]));
175 points.push(point((edge[0] + edge[1]) * 0.5));
176 }
177 points.push(point(*anchors.last().unwrap()));
178
179 let normal = vitem_normal_from_points(&points);
180 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
181 }
182
183 #[test]
184 fn normal_inference_falls_back_for_degenerate_inputs() {
185 let curved = [point(Vec3::ZERO), point(Vec3::Y), point(Vec3::X)];
187 assert!(vitem_normal_from_points(&curved).abs_diff_eq(Vec3::NEG_Z, 1e-6));
188
189 let collinear = [point(Vec3::ZERO), point(Vec3::X), point(Vec3::X * 2.0)];
191 assert_eq!(vitem_normal_from_points(&collinear), Vec3::Z);
192
193 let vertical = [point(Vec3::ZERO), point(Vec3::Z), point(Vec3::Z * 2.0)];
195 let normal = vitem_normal_from_points(&vertical);
196 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
197 assert!(normal.dot(Vec3::Z).abs() < 1e-6);
198 }
199}