ranim_items/vitem/render/
mod.rs1mod buffer;
4mod pipeline;
5
6use ranim_core::{
7 components::{rgba::Rgba, width::Width},
8 glam::{Mat4, Vec3, Vec4},
9};
10
11pub(super) fn install(app: &mut bevy_app::App) {
12 use bevy_ecs::schedule::IntoScheduleConfigs;
13 use ranim_render::{
14 extract::ExtractPlugin,
15 schedule::{RenderSchedule, ViewPhase},
16 utils::WgpuContext,
17 };
18
19 use super::{VItem, geometry::*, svg::SvgItem};
20
21 let buffer = buffer::VItemsBuffer::new(app.world().resource::<WgpuContext>());
22 app.insert_resource(buffer).add_plugins((
23 ExtractPlugin::<VItem>::default(),
24 ExtractPlugin::<Vec<VItem>>::default(),
25 ExtractPlugin::<Arc>::default(),
26 ExtractPlugin::<ArcBetweenPoints>::default(),
27 ExtractPlugin::<Circle>::default(),
28 ExtractPlugin::<Ellipse>::default(),
29 ExtractPlugin::<EllipticArc>::default(),
30 ExtractPlugin::<Line>::default(),
31 ExtractPlugin::<Parallelogram>::default(),
32 ExtractPlugin::<Polygon>::default(),
33 ExtractPlugin::<Rectangle>::default(),
34 ExtractPlugin::<RegularPolygon>::default(),
35 ExtractPlugin::<Square>::default(),
36 ExtractPlugin::<SvgItem>::default(),
37 ));
38 #[cfg(feature = "typst")]
39 app.add_plugins((
40 ExtractPlugin::<super::text::TextItem>::default(),
41 ExtractPlugin::<super::typst::TypstText>::default(),
42 ));
43
44 app.add_systems(RenderSchedule::Prepare, pipeline::prepare_vitems)
45 .add_systems(
46 RenderSchedule::View,
47 (
48 pipeline::compute.in_set(ViewPhase::Compute),
49 pipeline::depth.in_set(ViewPhase::Depth),
50 pipeline::color.in_set(ViewPhase::Color),
51 ),
52 );
53}
54
55pub fn vitem_normal_from_points(points: &[Vec4]) -> Vec3 {
63 if points.len() < 3 {
64 return Vec3::Z;
65 }
66
67 let point3 = |point: &Vec4| point.truncate();
68 let origin = point3(&points[0]);
69
70 let mut area_normal = Vec3::ZERO;
73 let mut previous = origin;
74 let mut scale_squared = 0.0_f32;
75 for point in points.iter().step_by(2).skip(1) {
76 let current = point3(point);
77 let previous_offset = previous - origin;
78 let current_offset = current - origin;
79 area_normal += previous_offset.cross(current_offset);
80 scale_squared = scale_squared
81 .max(previous_offset.length_squared())
82 .max(current_offset.length_squared());
83 previous = current;
84 }
85 if area_normal.length_squared() > f32::EPSILON * scale_squared * scale_squared {
86 return area_normal.normalize();
87 }
88
89 let mut direction: Option<Vec3> = None;
93 for point in &points[1..] {
94 let candidate = point3(point) - origin;
95 let candidate_length_squared = candidate.length_squared();
96 if candidate_length_squared <= f32::EPSILON {
97 continue;
98 }
99 if let Some(direction) = direction {
100 let normal = direction.cross(candidate);
101 if normal.length_squared()
102 > f32::EPSILON * direction.length_squared() * candidate_length_squared
103 {
104 return normal.normalize();
105 }
106 } else {
107 direction = Some(candidate);
108 }
109 }
110
111 if let Some(direction) = direction {
112 let direction = direction.normalize();
113 let reference = if direction.dot(Vec3::Z).abs() < 0.99 {
114 Vec3::Z
115 } else {
116 Vec3::X
117 };
118 return (reference - direction * direction.dot(reference)).normalize();
119 }
120
121 Vec3::Z
122}
123
124#[derive(bevy_ecs::component::Component, Debug, Clone, PartialEq)]
125pub struct RenderVItem {
127 pub normal: Option<Vec3>,
131 pub points: Vec<Vec4>,
134 pub transform: Mat4,
136 pub fill_rgbas: Vec<Rgba>,
138 pub stroke_rgbas: Vec<Rgba>,
140 pub stroke_widths: Vec<Width>,
142}
143
144impl Default for RenderVItem {
145 fn default() -> Self {
146 Self {
147 normal: None,
148 points: vec![Vec4::ZERO; 3],
149 transform: Mat4::IDENTITY,
150 stroke_widths: vec![Width::default(); 2],
151 stroke_rgbas: vec![Rgba::default(); 2],
152 fill_rgbas: vec![Rgba::default(); 2],
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use ranim_core::glam::{Vec3, Vec4};
160
161 use super::vitem_normal_from_points;
162
163 fn point(point: Vec3) -> Vec4 {
164 point.extend(0.0)
165 }
166
167 #[test]
168 fn inferred_normal_matches_the_polygon_plane() {
169 let anchors = [
170 Vec3::new(2.0, -1.0, -1.0),
171 Vec3::new(2.0, 1.0, -1.0),
172 Vec3::new(2.0, 1.0, 1.0),
173 Vec3::new(2.0, -1.0, 1.0),
174 Vec3::new(2.0, -1.0, -1.0),
175 ];
176 let mut points = Vec::new();
177 for edge in anchors.windows(2) {
178 points.push(point(edge[0]));
179 points.push(point((edge[0] + edge[1]) * 0.5));
180 }
181 points.push(point(*anchors.last().unwrap()));
182
183 let normal = vitem_normal_from_points(&points);
184 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
185 }
186
187 #[test]
188 fn normal_inference_falls_back_for_degenerate_inputs() {
189 let curved = [point(Vec3::ZERO), point(Vec3::Y), point(Vec3::X)];
191 assert!(vitem_normal_from_points(&curved).abs_diff_eq(Vec3::NEG_Z, 1e-6));
192
193 let collinear = [point(Vec3::ZERO), point(Vec3::X), point(Vec3::X * 2.0)];
195 assert_eq!(vitem_normal_from_points(&collinear), Vec3::Z);
196
197 let vertical = [point(Vec3::ZERO), point(Vec3::Z), point(Vec3::Z * 2.0)];
199 let normal = vitem_normal_from_points(&vertical);
200 assert!(normal.abs_diff_eq(Vec3::X, 1e-6));
201 assert!(normal.dot(Vec3::Z).abs() < 1e-6);
202 }
203}