1use crate::container::Command;
47use crate::draw_context::{control_text_position_with_font, intersect_clip_rect, DrawCtx};
48use crate::*;
49use std::rc::Rc;
50
51const GEOM_EPS: f32 = 1.0e-5;
52const GEOM_EPS_SQ: f32 = GEOM_EPS * GEOM_EPS;
53
54fn translate_rect(rect: Recti, offset: Vec2i) -> Recti {
57 Recti::new(rect.x + offset.x, rect.y + offset.y, rect.width, rect.height)
58}
59
60fn translate_vertex(vertex: Vertex, offset: Vec2f) -> Vertex {
63 Vertex::new(vertex.position() + offset, vertex.tex_coord(), vertex.color())
64}
65
66#[cfg(test)]
69fn rect_from_points(points: &[Vec2f]) -> Recti {
70 let mut min_x = f32::INFINITY;
71 let mut min_y = f32::INFINITY;
72 let mut max_x = f32::NEG_INFINITY;
73 let mut max_y = f32::NEG_INFINITY;
74
75 for point in points {
76 min_x = min_x.min(point.x);
77 min_y = min_y.min(point.y);
78 max_x = max_x.max(point.x);
79 max_y = max_y.max(point.y);
80 }
81
82 let x0 = min_x.floor() as i32;
83 let y0 = min_y.floor() as i32;
84 let x1 = max_x.ceil() as i32;
85 let y1 = max_y.ceil() as i32;
86 Recti::new(x0, y0, (x1 - x0).max(0), (y1 - y0).max(0))
87}
88
89fn cross2(a: Vec2f, b: Vec2f) -> f32 {
92 a.x * b.y - a.y * b.x
93}
94
95fn distance_sq(a: Vec2f, b: Vec2f) -> f32 {
98 let dx = a.x - b.x;
99 let dy = a.y - b.y;
100 dx * dx + dy * dy
101}
102
103fn signed_area(points: &[Vec2f]) -> f32 {
106 if points.len() < 3 {
107 return 0.0;
108 }
109
110 let mut area = 0.0;
111 for idx in 0..points.len() {
112 let curr = points[idx];
113 let next = points[(idx + 1) % points.len()];
114 area += curr.x * next.y - next.x * curr.y;
115 }
116 area * 0.5
117}
118
119fn dedupe_and_simplify_polygon(points: &[Vec2f]) -> Vec<Vec2f> {
123 let mut deduped = Vec::with_capacity(points.len());
124 for point in points {
125 if deduped.last().map(|prev| distance_sq(*prev, *point) > GEOM_EPS_SQ).unwrap_or(true) {
126 deduped.push(*point);
127 }
128 }
129
130 if deduped.len() > 1 && distance_sq(deduped[0], *deduped.last().unwrap()) <= GEOM_EPS_SQ {
131 deduped.pop();
132 }
133
134 if deduped.len() < 3 {
135 return Vec::new();
136 }
137
138 let mut simplified = Vec::with_capacity(deduped.len());
139 for idx in 0..deduped.len() {
140 let prev = deduped[(idx + deduped.len() - 1) % deduped.len()];
141 let curr = deduped[idx];
142 let next = deduped[(idx + 1) % deduped.len()];
143
144 if distance_sq(prev, curr) <= GEOM_EPS_SQ || distance_sq(curr, next) <= GEOM_EPS_SQ {
145 continue;
146 }
147
148 if cross2(curr - prev, next - curr).abs() <= GEOM_EPS {
149 continue;
150 }
151
152 simplified.push(curr);
153 }
154
155 simplified
156}
157
158fn is_convex_ccw(prev: Vec2f, curr: Vec2f, next: Vec2f) -> bool {
160 cross2(curr - prev, next - curr) > GEOM_EPS
161}
162
163fn is_convex_polygon_ccw(points: &[Vec2f]) -> bool {
166 if points.len() < 3 {
167 return false;
168 }
169
170 for idx in 0..points.len() {
171 let prev = points[(idx + points.len() - 1) % points.len()];
172 let curr = points[idx];
173 let next = points[(idx + 1) % points.len()];
174 if !is_convex_ccw(prev, curr, next) {
175 return false;
176 }
177 }
178 true
179}
180
181fn point_in_triangle_ccw(point: Vec2f, a: Vec2f, b: Vec2f, c: Vec2f) -> bool {
184 let ab = cross2(b - a, point - a);
185 let bc = cross2(c - b, point - b);
186 let ca = cross2(a - c, point - c);
187 ab >= -GEOM_EPS && bc >= -GEOM_EPS && ca >= -GEOM_EPS
188}
189
190#[derive(Copy, Clone)]
191enum RectClipEdge {
192 Left,
193 Right,
194 Top,
195 Bottom,
196}
197
198fn lerp_vec2(a: Vec2f, b: Vec2f, t: f32) -> Vec2f {
202 let omt = 1.0 - t;
203 Vec2f::new(a.x * omt + b.x * t, a.y * omt + b.y * t)
204}
205
206fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
209 ((a as f32) + (b as f32 - a as f32) * t).round().clamp(0.0, 255.0) as u8
210}
211
212fn lerp_color4b(a: Color4b, b: Color4b, t: f32) -> Color4b {
215 color4b(lerp_u8(a.x, b.x, t), lerp_u8(a.y, b.y, t), lerp_u8(a.z, b.z, t), lerp_u8(a.w, b.w, t))
216}
217
218fn lerp_vertex(a: Vertex, b: Vertex, t: f32) -> Vertex {
222 Vertex::new(
223 lerp_vec2(a.position(), b.position(), t),
224 lerp_vec2(a.tex_coord(), b.tex_coord(), t),
225 lerp_color4b(a.color(), b.color(), t),
226 )
227}
228
229fn point_inside_clip_edge(point: Vec2f, edge: RectClipEdge, clip: Recti) -> bool {
233 let left = clip.x as f32;
234 let right = (clip.x + clip.width) as f32;
235 let top = clip.y as f32;
236 let bottom = (clip.y + clip.height) as f32;
237 match edge {
238 RectClipEdge::Left => point.x >= left - GEOM_EPS,
239 RectClipEdge::Right => point.x <= right + GEOM_EPS,
240 RectClipEdge::Top => point.y >= top - GEOM_EPS,
241 RectClipEdge::Bottom => point.y <= bottom + GEOM_EPS,
242 }
243}
244
245fn intersection_t_for_edge(a: Vec2f, b: Vec2f, edge: RectClipEdge, clip: Recti) -> f32 {
249 let (start, delta, boundary) = match edge {
250 RectClipEdge::Left => (a.x, b.x - a.x, clip.x as f32),
251 RectClipEdge::Right => (a.x, b.x - a.x, (clip.x + clip.width) as f32),
252 RectClipEdge::Top => (a.y, b.y - a.y, clip.y as f32),
253 RectClipEdge::Bottom => (a.y, b.y - a.y, (clip.y + clip.height) as f32),
254 };
255
256 if delta.abs() <= GEOM_EPS {
257 0.0
258 } else {
259 ((boundary - start) / delta).clamp(0.0, 1.0)
260 }
261}
262
263fn intersect_vertex_edge(a: Vertex, b: Vertex, edge: RectClipEdge, clip: Recti) -> Vertex {
266 let t = intersection_t_for_edge(a.position(), b.position(), edge, clip);
267 lerp_vertex(a, b, t)
268}
269
270fn push_unique_vertex(dst: &mut [Vertex; 8], count: &mut usize, vertex: Vertex) {
274 if *count > 0 && distance_sq(dst[*count - 1].position(), vertex.position()) <= GEOM_EPS_SQ {
275 dst[*count - 1] = vertex;
276 return;
277 }
278
279 debug_assert!(*count < dst.len(), "rect-clipped triangle exceeded fixed vertex budget");
280 dst[*count] = vertex;
281 *count += 1;
282}
283
284fn clip_polygon_against_edge(input: &[Vertex; 8], input_count: usize, edge: RectClipEdge, clip: Recti, output: &mut [Vertex; 8]) -> usize {
287 if input_count == 0 {
288 return 0;
289 }
290
291 let mut out_count = 0;
292 let mut prev = input[input_count - 1];
293 let mut prev_inside = point_inside_clip_edge(prev.position(), edge, clip);
294
295 for curr in input.iter().copied().take(input_count) {
296 let curr_inside = point_inside_clip_edge(curr.position(), edge, clip);
297
298 if curr_inside != prev_inside {
299 let intersection = intersect_vertex_edge(prev, curr, edge, clip);
300 push_unique_vertex(output, &mut out_count, intersection);
301 }
302 if curr_inside {
303 push_unique_vertex(output, &mut out_count, curr);
304 }
305
306 prev = curr;
307 prev_inside = curr_inside;
308 }
309
310 if out_count > 1 && distance_sq(output[0].position(), output[out_count - 1].position()) <= GEOM_EPS_SQ {
311 out_count -= 1;
312 }
313
314 out_count
315}
316
317fn signed_area_vertices(points: &[Vertex]) -> f32 {
320 if points.len() < 3 {
321 return 0.0;
322 }
323
324 let mut area = 0.0;
325 for idx in 0..points.len() {
326 let curr = points[idx].position();
327 let next = points[(idx + 1) % points.len()].position();
328 area += curr.x * next.y - next.x * curr.y;
329 }
330 area * 0.5
331}
332
333pub(crate) fn clip_triangle_vertices_to_rect<F>(v0: Vertex, v1: Vertex, v2: Vertex, clip: Recti, mut emit: F)
339where
340 F: FnMut(Vertex, Vertex, Vertex),
341{
342 if clip.width <= 0 || clip.height <= 0 {
343 return;
344 }
345
346 let mut input = [Vertex::default(); 8];
347 let mut output = [Vertex::default(); 8];
348 input[0] = v0;
349 input[1] = v1;
350 input[2] = v2;
351 let mut input_count = 3usize;
352
353 for edge in [RectClipEdge::Left, RectClipEdge::Right, RectClipEdge::Top, RectClipEdge::Bottom] {
354 let output_count = clip_polygon_against_edge(&input, input_count, edge, clip, &mut output);
355 if output_count < 3 {
356 return;
357 }
358 input_count = output_count;
359 std::mem::swap(&mut input, &mut output);
360 }
361
362 if signed_area_vertices(&input[..input_count]).abs() <= GEOM_EPS {
363 return;
364 }
365
366 for idx in 1..input_count - 1 {
367 let a = input[0];
368 let b = input[idx];
369 let c = input[idx + 1];
370 let tri_area = cross2(b.position() - a.position(), c.position() - a.position());
371 if tri_area.abs() > GEOM_EPS {
372 emit(a, b, c);
373 }
374 }
375}
376
377pub struct Graphics<'a, 'b> {
390 draw: &'a mut DrawCtx<'b>,
391 widget_rect: Recti,
392 widget_origin: Vec2f,
393 white_uv: Vec2f,
394 clip_base_depth: usize,
395 triangle_batch_start: usize,
396 triangle_batch_count: usize,
397}
398
399impl<'a, 'b> Graphics<'a, 'b> {
400 pub(crate) fn new(draw: &'a mut DrawCtx<'b>, widget_rect: Recti) -> Self {
401 Self::new_with_clip_root(draw, widget_rect, widget_rect)
402 }
403
404 pub(crate) fn new_with_clip_root(draw: &'a mut DrawCtx<'b>, widget_rect: Recti, clip_root: Recti) -> Self {
408 let clip_base_depth = draw.clip_depth();
412 draw.push_clip_rect(clip_root);
413 let white_rect = draw.atlas().get_icon_rect(WHITE_ICON);
414 let atlas_dim = draw.atlas().get_texture_dimension();
415 let white_uv = Vec2f::new(
416 (white_rect.x as f32 + white_rect.width as f32 * 0.5) / atlas_dim.width as f32,
417 (white_rect.y as f32 + white_rect.height as f32 * 0.5) / atlas_dim.height as f32,
418 );
419 let triangle_batch_start = draw.triangle_vertex_count();
420
421 Self {
422 draw,
423 widget_rect,
424 widget_origin: Vec2f::new(widget_rect.x as f32, widget_rect.y as f32),
425 white_uv,
426 clip_base_depth,
427 triangle_batch_start,
428 triangle_batch_count: 0,
429 }
430 }
431
432 pub fn local_rect(&self) -> Recti {
437 Recti::new(0, 0, self.widget_rect.width, self.widget_rect.height)
438 }
439
440 pub fn current_clip_rect(&self) -> Recti {
445 self.screen_to_local_rect(self.draw.current_clip_rect())
446 }
447
448 pub fn push_clip_rect(&mut self, rect: Recti) {
454 self.draw.push_clip_rect(self.local_to_screen_rect(rect));
455 }
456
457 pub fn set_clip_rect(&mut self, rect: Recti) {
462 let clip = intersect_clip_rect(self.draw.current_clip_rect(), self.local_to_screen_rect(rect));
463 self.draw.replace_current_clip_rect(clip);
464 }
465
466 pub fn pop_clip_rect(&mut self) {
468 if self.draw.clip_depth() > self.clip_base_depth + 1 {
469 self.draw.pop_clip_rect();
470 }
471 }
472
473 pub fn with_clip<F: FnOnce(&mut Self)>(&mut self, rect: Recti, f: F) {
475 self.push_clip_rect(rect);
476 f(self);
477 self.pop_clip_rect();
478 }
479
480 pub fn draw_rect(&mut self, rect: Recti, color: Color) {
485 if rect.width <= 0 || rect.height <= 0 || color.a == 0 {
486 return;
487 }
488
489 let x0 = rect.x as f32;
490 let y0 = rect.y as f32;
491 let x1 = (rect.x + rect.width) as f32;
492 let y1 = (rect.y + rect.height) as f32;
493 self.push_quad_local(Vec2f::new(x0, y0), Vec2f::new(x1, y0), Vec2f::new(x1, y1), Vec2f::new(x0, y1), color);
494 }
495
496 pub fn draw_box(&mut self, rect: Recti, color: Color) {
501 self.draw_rect(Recti::new(rect.x + 1, rect.y, rect.width - 2, 1), color);
502 self.draw_rect(Recti::new(rect.x + 1, rect.y + rect.height - 1, rect.width - 2, 1), color);
503 self.draw_rect(Recti::new(rect.x, rect.y, 1, rect.height), color);
504 self.draw_rect(Recti::new(rect.x + rect.width - 1, rect.y, 1, rect.height), color);
505 }
506
507 pub fn draw_text(&mut self, font: FontId, text: &str, pos: Vec2i, color: Color) {
513 if text.is_empty() || color.a == 0 {
514 return;
515 }
516
517 let size = self.draw.atlas().get_text_size(font, text);
518 let bounds = Recti::new(pos.x, pos.y, size.width, size.height);
519 let screen_pos = self.local_to_screen_pos(pos);
520 let text = text.to_string();
521 self.emit_clipped_command(bounds, |draw| {
522 draw.push_command(Command::Text { text, pos: screen_pos, color, font });
523 });
524 }
525
526 pub fn draw_icon(&mut self, id: IconId, rect: Recti, color: Color) {
528 let screen_rect = self.local_to_screen_rect(rect);
529 self.emit_clipped_command(rect, |draw| {
530 draw.push_command(Command::Icon { id, rect: screen_rect, color });
531 });
532 }
533
534 pub fn draw_image(&mut self, image: Image, rect: Recti, color: Color) {
536 let screen_rect = self.local_to_screen_rect(rect);
537 self.emit_clipped_command(rect, |draw| {
538 draw.push_command(Command::Image { image, rect: screen_rect, color });
539 });
540 }
541
542 pub fn draw_slot_with_function(&mut self, id: SlotId, rect: Recti, color: Color, payload: Rc<dyn Fn(usize, usize) -> Color4b>) {
544 let screen_rect = self.local_to_screen_rect(rect);
545 self.emit_clipped_command(rect, |draw| {
546 draw.push_command(Command::SlotRedraw { id, rect: screen_rect, color, payload });
547 });
548 }
549
550 pub fn draw_frame(&mut self, rect: Recti, colorid: ControlColor) {
552 let color = self.draw.style().colors[colorid as usize];
553 self.draw_rect(rect, color);
554 if colorid == ControlColor::ScrollBase || colorid == ControlColor::ScrollThumb || colorid == ControlColor::TitleBG {
555 return;
556 }
557
558 let border = self.draw.style().colors[ControlColor::Border as usize];
559 if border.a != 0 {
560 self.draw_box(expand_rect(rect, 1), border);
561 }
562 }
563
564 pub fn draw_widget_frame(&mut self, focused: bool, hovered: bool, rect: Recti, mut colorid: ControlColor, opt: WidgetOption) {
567 if opt.has_no_frame() {
568 return;
569 }
570 if focused {
571 colorid.focus();
572 } else if hovered {
573 colorid.hover();
574 }
575 self.draw_frame(rect, colorid);
576 }
577
578 pub fn draw_control_text(&mut self, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
583 self.draw_control_text_with_font(self.draw.style().font, text, rect, colorid, opt);
584 }
585
586 pub fn draw_control_text_with_font(&mut self, font: FontId, text: &str, rect: Recti, colorid: ControlColor, opt: WidgetOption) {
588 let (font, color, pos) = {
589 let style = self.draw.style();
590 let atlas = self.draw.atlas();
591 (
592 font,
593 style.colors[colorid as usize],
594 control_text_position_with_font(style, atlas, font, text, rect, opt),
595 )
596 };
597 self.push_clip_rect(rect);
598 self.draw_text(font, text, pos, color);
599 self.pop_clip_rect();
600 }
601
602 pub fn stroke_line(&mut self, a: Vec2f, b: Vec2f, width: f32, color: Color) {
608 if width <= 0.0 || color.a == 0 {
609 return;
610 }
611
612 let delta = b - a;
613 let len_sq = delta.x * delta.x + delta.y * delta.y;
614 if len_sq <= GEOM_EPS_SQ {
615 let half = width * 0.5;
616 self.push_quad_local(
617 Vec2f::new(a.x - half, a.y - half),
618 Vec2f::new(a.x + half, a.y - half),
619 Vec2f::new(a.x + half, a.y + half),
620 Vec2f::new(a.x - half, a.y + half),
621 color,
622 );
623 return;
624 }
625
626 let inv_len = len_sq.sqrt().recip();
627 let normal = Vec2f::new(-delta.y * inv_len, delta.x * inv_len) * (width * 0.5);
628
629 let p0 = a + normal;
630 let p1 = b + normal;
631 let p2 = b - normal;
632 let p3 = a - normal;
633 self.push_quad_local(p0, p1, p2, p3, color);
634 }
635
636 pub fn fill_polygon(&mut self, points: &[Vec2f], color: Color) {
642 if points.len() < 3 || color.a == 0 {
643 return;
644 }
645
646 let mut points = dedupe_and_simplify_polygon(points);
647 if points.len() < 3 {
648 return;
649 }
650
651 let area = signed_area(points.as_slice());
652 if area.abs() <= GEOM_EPS {
653 return;
654 }
655 if area < 0.0 {
656 points.reverse();
657 }
658
659 let rgba = color4b(color.r, color.g, color.b, color.a);
660
661 if is_convex_polygon_ccw(points.as_slice()) {
662 self.push_triangle_fan(points.as_slice(), rgba);
663 return;
664 }
665
666 let mut indices: Vec<usize> = (0..points.len()).collect();
667 while indices.len() > 3 {
668 let mut ear_found = false;
669
670 for idx in 0..indices.len() {
671 let prev = indices[(idx + indices.len() - 1) % indices.len()];
672 let curr = indices[idx];
673 let next = indices[(idx + 1) % indices.len()];
674 let a = points[prev];
675 let b = points[curr];
676 let c = points[next];
677
678 if !is_convex_ccw(a, b, c) {
679 continue;
680 }
681
682 let mut contains_other = false;
683 for probe in &indices {
684 if *probe == prev || *probe == curr || *probe == next {
685 continue;
686 }
687 if point_in_triangle_ccw(points[*probe], a, b, c) {
688 contains_other = true;
689 break;
690 }
691 }
692 if contains_other {
693 continue;
694 }
695
696 self.push_triangle_local(a, b, c, rgba);
697 indices.remove(idx);
698 ear_found = true;
699 break;
700 }
701
702 if !ear_found {
703 return;
704 }
705 }
706
707 if indices.len() == 3 {
708 self.push_triangle_local(points[indices[0]], points[indices[1]], points[indices[2]], rgba);
709 }
710 }
711
712 fn current_screen_clip_rect(&self) -> Recti {
715 self.draw.current_clip_rect()
716 }
717
718 fn local_to_screen_pos(&self, pos: Vec2i) -> Vec2i {
721 pos + Vec2i::new(self.widget_rect.x, self.widget_rect.y)
722 }
723
724 fn local_to_screen_rect(&self, rect: Recti) -> Recti {
727 translate_rect(rect, Vec2i::new(self.widget_rect.x, self.widget_rect.y))
728 }
729
730 fn screen_to_local_rect(&self, rect: Recti) -> Recti {
733 translate_rect(rect, Vec2i::new(-self.widget_rect.x, -self.widget_rect.y))
734 }
735
736 fn emit_clipped_command<F>(&mut self, bounds_local: Recti, emit: F)
740 where
741 F: FnOnce(&mut DrawCtx<'b>),
742 {
743 self.flush_batch();
744 let clip = self.current_screen_clip_rect();
745 let bounds = self.local_to_screen_rect(bounds_local);
746 self.draw.emit_clipped(bounds, clip, emit);
747 }
748
749 fn push_triangle_fan(&mut self, points: &[Vec2f], color: Color4b) {
752 for idx in 1..points.len() - 1 {
753 self.push_triangle_local(points[0], points[idx], points[idx + 1], color);
754 }
755 }
756
757 fn push_quad_local(&mut self, p0: Vec2f, p1: Vec2f, p2: Vec2f, p3: Vec2f, color: Color) {
760 let rgba = color4b(color.r, color.g, color.b, color.a);
761 self.push_triangle_local(p0, p1, p2, rgba);
762 self.push_triangle_local(p0, p2, p3, rgba);
763 }
764
765 fn push_triangle_local(&mut self, a: Vec2f, b: Vec2f, c: Vec2f, color: Color4b) {
769 let clip = self.current_clip_rect();
770 let widget_origin = self.widget_origin;
771 clip_triangle_vertices_to_rect(
772 Vertex::new(a, self.white_uv, color),
773 Vertex::new(b, self.white_uv, color),
774 Vertex::new(c, self.white_uv, color),
775 clip,
776 |va, vb, vc| {
777 self.draw.push_triangle_vertices(
778 translate_vertex(va, widget_origin),
779 translate_vertex(vb, widget_origin),
780 translate_vertex(vc, widget_origin),
781 );
782 self.triangle_batch_count += 3;
783 },
784 );
785 }
786
787 fn flush_batch(&mut self) {
791 if self.triangle_batch_count == 0 {
792 return;
793 }
794
795 self.draw.push_command(Command::Triangle {
796 vertex_start: self.triangle_batch_start,
797 vertex_count: self.triangle_batch_count,
798 });
799 self.triangle_batch_start = self.draw.triangle_vertex_count();
800 self.triangle_batch_count = 0;
801 }
802}
803
804impl<'a, 'b> Drop for Graphics<'a, 'b> {
805 fn drop(&mut self) {
806 self.flush_batch();
807 self.draw.pop_clip_rect_to(self.clip_base_depth);
808 }
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::container::Command;
815 use crate::draw_context::clip_relation;
816
817 fn assert_rect_eq(actual: Recti, expected: Recti) {
818 assert_eq!(
819 (actual.x, actual.y, actual.width, actual.height),
820 (expected.x, expected.y, expected.width, expected.height)
821 );
822 }
823
824 fn assert_vec2_eq(actual: Vec2f, expected: Vec2f) {
825 assert!((actual.x - expected.x).abs() <= GEOM_EPS);
826 assert!((actual.y - expected.y).abs() <= GEOM_EPS);
827 }
828
829 fn make_vertex(pos: (f32, f32)) -> Vertex {
830 Vertex::new(Vec2f::new(pos.0, pos.1), Vec2f::default(), color4b(255, 255, 255, 255))
831 }
832
833 #[test]
834 fn clip_relation_reports_partial_overlap() {
835 let clip = rect(10, 10, 10, 10);
836 let bounds = rect(5, 5, 10, 10);
837 assert_eq!(clip_relation(bounds, clip) as u32, Clip::Part as u32);
838 }
839
840 #[test]
841 fn triangle_bounds_are_conservative() {
842 let bounds = rect_from_points(&[Vec2f::new(1.2, 2.6), Vec2f::new(4.8, 3.1), Vec2f::new(3.0, 9.9)]);
843 assert_rect_eq(bounds, rect(1, 2, 4, 8));
844 }
845
846 #[test]
847 fn polygon_cleanup_removes_duplicate_closing_point() {
848 let points = dedupe_and_simplify_polygon(&[
849 Vec2f::new(0.0, 0.0),
850 Vec2f::new(10.0, 0.0),
851 Vec2f::new(10.0, 10.0),
852 Vec2f::new(0.0, 10.0),
853 Vec2f::new(0.0, 0.0),
854 ]);
855 assert_eq!(points.len(), 4);
856 }
857
858 #[test]
859 fn local_rect_translation_is_preserved_in_emitted_vertices() {
860 let atlas = AtlasHandle::from(&AtlasSource {
861 width: 1,
862 height: 1,
863 pixels: &[255, 255, 255, 255],
864 icons: &[("white", Recti::new(0, 0, 1, 1))],
865 fonts: &[],
866 format: SourceFormat::Raw,
867 slots: &[],
868 });
869 let style = Style::default();
870 let mut commands = Vec::new();
871 let mut triangle_vertices = Vec::new();
872 let mut clip_stack = vec![rect(0, 0, 200, 200)];
873 let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
874 {
875 let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
876 graphics.push_triangle_local(Vec2f::new(0.0, 0.0), Vec2f::new(10.0, 0.0), Vec2f::new(0.0, 10.0), color4b(255, 255, 255, 255));
877 }
878
879 match &commands[0] {
880 Command::Triangle { vertex_start, vertex_count } => {
881 let vertices = &triangle_vertices[*vertex_start..*vertex_start + *vertex_count];
882 let a = vertices[0].position();
883 let b = vertices[1].position();
884 let c = vertices[2].position();
885 assert_vec2_eq(a, Vec2f::new(20.0, 30.0));
886 assert_vec2_eq(b, Vec2f::new(30.0, 30.0));
887 assert_vec2_eq(c, Vec2f::new(20.0, 40.0));
888 }
889 _ => panic!("expected triangle command"),
890 }
891 }
892
893 #[test]
894 fn local_clip_changes_stay_in_one_triangle_batch() {
895 let atlas = AtlasHandle::from(&AtlasSource {
896 width: 1,
897 height: 1,
898 pixels: &[255, 255, 255, 255],
899 icons: &[("white", Recti::new(0, 0, 1, 1))],
900 fonts: &[],
901 format: SourceFormat::Raw,
902 slots: &[],
903 });
904 let style = Style::default();
905 let mut commands = Vec::new();
906 let mut triangle_vertices = Vec::new();
907 let mut clip_stack = vec![rect(0, 0, 200, 200)];
908 let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
909 {
910 let mut graphics = Graphics::new(&mut draw, rect(0, 0, 50, 50));
911 graphics.stroke_line(Vec2f::new(0.0, 0.0), Vec2f::new(10.0, 0.0), 2.0, color(255, 0, 0, 255));
912 graphics.push_clip_rect(rect(0, 0, 5, 5));
913 graphics.stroke_line(Vec2f::new(0.0, 2.0), Vec2f::new(10.0, 2.0), 2.0, color(255, 0, 0, 255));
914 }
915
916 let triangle_count = commands.iter().filter(|cmd| matches!(cmd, Command::Triangle { .. })).count();
917 let clip_count = commands.iter().filter(|cmd| matches!(cmd, Command::Clip { .. })).count();
918 assert_eq!(triangle_count, 1);
919 assert_eq!(clip_count, 0);
920 }
921
922 #[test]
923 fn graphics_restores_shared_clip_stack_on_drop() {
924 let atlas = AtlasHandle::from(&AtlasSource {
925 width: 1,
926 height: 1,
927 pixels: &[255, 255, 255, 255],
928 icons: &[("white", Recti::new(0, 0, 1, 1))],
929 fonts: &[],
930 format: SourceFormat::Raw,
931 slots: &[],
932 });
933 let style = Style::default();
934 let mut commands = Vec::new();
935 let mut triangle_vertices = Vec::new();
936 let mut clip_stack = vec![rect(0, 0, 200, 200)];
937 let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
938 {
939 let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
940 graphics.push_clip_rect(rect(0, 0, 5, 5));
941 assert_rect_eq(graphics.current_clip_rect(), rect(0, 0, 5, 5));
942 }
943
944 assert_rect_eq(draw.current_clip_rect(), rect(0, 0, 200, 200));
945 }
946
947 #[test]
948 fn local_triangles_are_software_clipped_before_emission() {
949 let atlas = AtlasHandle::from(&AtlasSource {
950 width: 1,
951 height: 1,
952 pixels: &[255, 255, 255, 255],
953 icons: &[("white", Recti::new(0, 0, 1, 1))],
954 fonts: &[],
955 format: SourceFormat::Raw,
956 slots: &[],
957 });
958 let style = Style::default();
959 let mut commands = Vec::new();
960 let mut triangle_vertices = Vec::new();
961 let mut clip_stack = vec![rect(0, 0, 200, 200)];
962 let mut draw = DrawCtx::new(&mut commands, &mut triangle_vertices, &mut clip_stack, &style, &atlas);
963 {
964 let mut graphics = Graphics::new(&mut draw, rect(20, 30, 50, 50));
965 graphics.push_clip_rect(rect(0, 0, 5, 5));
966 graphics.stroke_line(Vec2f::new(-10.0, 2.0), Vec2f::new(20.0, 2.0), 2.0, color(255, 0, 0, 255));
967 }
968
969 match &commands[0] {
970 Command::Triangle { vertex_start, vertex_count } => {
971 let vertices = &triangle_vertices[*vertex_start..*vertex_start + *vertex_count];
972 assert!(!vertices.is_empty());
973 for vertex in vertices {
974 let pos = vertex.position();
975 assert!(pos.x >= 20.0 - GEOM_EPS && pos.x <= 25.0 + GEOM_EPS);
976 assert!(pos.y >= 30.0 - GEOM_EPS && pos.y <= 35.0 + GEOM_EPS);
977 }
978 }
979 _ => panic!("expected triangle command"),
980 }
981 }
982
983 #[test]
984 fn point_in_triangle_accepts_boundary_points() {
985 let a = Vec2f::new(0.0, 0.0);
986 let b = Vec2f::new(10.0, 0.0);
987 let c = Vec2f::new(0.0, 10.0);
988 assert!(point_in_triangle_ccw(Vec2f::new(5.0, 0.0), a, b, c));
989 }
990
991 #[test]
992 fn helper_vertices_are_constructible() {
993 let vertex = make_vertex((1.0, 2.0));
994 assert_vec2_eq(vertex.position(), Vec2f::new(1.0, 2.0));
995 }
996}