ranim_core/core_item/
vitem.rs1use color::{AlphaColor, Srgb};
2use glam::{Vec3, Vec4};
3
4use crate::{
5 Extract,
6 components::{rgba::Rgba, width::Width},
7 core_item::CoreItem,
8 traits::FillColor,
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>,
89 pub points: Vec<Vec4>,
92 pub fill_rgbas: Vec<Rgba>,
94 pub stroke_rgbas: Vec<Rgba>,
96 pub stroke_widths: Vec<Width>,
98}
99
100impl Default for VItem {
101 fn default() -> Self {
102 Self {
103 normal: None,
104 points: vec![Vec4::ZERO; 3],
105 stroke_widths: vec![Width::default(); 2],
106 stroke_rgbas: vec![Rgba::default(); 2],
107 fill_rgbas: vec![Rgba::default(); 2],
108 }
109 }
110}
111
112impl Extract for VItem {
113 type Target = CoreItem;
114 fn extract_into(&self, buf: &mut Vec<Self::Target>) {
115 buf.push(CoreItem::VItem(self.clone()));
116 }
117}
118
119impl FillColor for VItem {
120 fn fill_color(&self) -> AlphaColor<Srgb> {
121 let Rgba(rgba) = self.fill_rgbas[0];
122 AlphaColor::new([rgba.x, rgba.y, rgba.z, rgba.w])
123 }
124 fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
125 self.fill_rgbas.fill(color.into());
126 self
127 }
128 fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
129 self.fill_rgbas
130 .iter_mut()
131 .for_each(|rgba| rgba.0.w = opacity);
132 self
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use glam::{Vec3, Vec4};
139
140 use super::vitem_normal_from_points;
141
142 fn point(point: Vec3) -> Vec4 {
143 point.extend(0.0)
144 }
145
146 #[test]
147 fn computes_normal_from_interleaved_polygon_anchors() {
148 let anchors = [
149 Vec3::new(2.0, -1.0, -1.0),
150 Vec3::new(2.0, 1.0, -1.0),
151 Vec3::new(2.0, 1.0, 1.0),
152 Vec3::new(2.0, -1.0, 1.0),
153 Vec3::new(2.0, -1.0, -1.0),
154 ];
155 let mut points = Vec::new();
156 for edge in anchors.windows(2) {
157 points.push(point(edge[0]));
158 points.push(point((edge[0] + edge[1]) * 0.5));
159 }
160 points.push(point(*anchors.last().unwrap()));
161
162 let normal = vitem_normal_from_points(&points);
163 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
164 }
165
166 #[test]
167 fn falls_back_to_control_points_for_single_curved_segment() {
168 let points = [point(Vec3::ZERO), point(Vec3::Y), point(Vec3::X)];
169
170 let normal = vitem_normal_from_points(&points);
171 assert!(normal.abs_diff_eq(Vec3::NEG_Z, 1e-6));
172 }
173
174 #[test]
175 fn collinear_points_use_default_normal() {
176 let points = [point(Vec3::ZERO), point(Vec3::X), point(Vec3::X * 2.0)];
177
178 assert_eq!(vitem_normal_from_points(&points), Vec3::Z);
179 }
180
181 #[test]
182 fn vertical_line_uses_a_plane_containing_the_line() {
183 let points = [point(Vec3::ZERO), point(Vec3::Z), point(Vec3::Z * 2.0)];
184
185 let normal = vitem_normal_from_points(&points);
186 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
187 assert!(normal.dot(Vec3::Z).abs() < 1e-6);
188 }
189}