1use serde::{Deserialize, Serialize};
203use std::collections::VecDeque;
204use wasm_bindgen::prelude::*;
205use web_sys::ImageData;
206
207use crate::error::{CanvasError, WasmError, WasmResult};
208use crate::tile::TileCoord;
209
210#[allow(dead_code)]
212pub const DEFAULT_CANVAS_BUFFER_SIZE_MB: usize = 50;
213
214pub const MAX_VIEWPORT_HISTORY: usize = 50;
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219pub enum RenderQuality {
220 Low,
222 Medium,
224 High,
226 Ultra,
228}
229
230impl RenderQuality {
231 pub const fn resolution_multiplier(&self) -> f64 {
233 match self {
234 Self::Low => 0.5,
235 Self::Medium => 1.0,
236 Self::High => 1.5,
237 Self::Ultra => 2.0,
238 }
239 }
240
241 pub const fn interpolation_quality(&self) -> &'static str {
243 match self {
244 Self::Low => "low",
245 Self::Medium => "medium",
246 Self::High => "high",
247 Self::Ultra => "high",
248 }
249 }
250}
251
252#[derive(Debug, Clone, Copy, PartialEq)]
254pub struct ViewportTransform {
255 pub tx: f64,
257 pub ty: f64,
259 pub sx: f64,
261 pub sy: f64,
263 pub rotation: f64,
265}
266
267impl ViewportTransform {
268 pub const fn new(sx: f64, _shy: f64, _shx: f64, sy: f64, tx: f64, ty: f64) -> Self {
272 Self {
274 tx,
275 ty,
276 sx,
277 sy,
278 rotation: 0.0,
279 }
280 }
281
282 pub const fn identity() -> Self {
284 Self {
285 tx: 0.0,
286 ty: 0.0,
287 sx: 1.0,
288 sy: 1.0,
289 rotation: 0.0,
290 }
291 }
292
293 pub const fn translate(tx: f64, ty: f64) -> Self {
295 Self {
296 tx,
297 ty,
298 sx: 1.0,
299 sy: 1.0,
300 rotation: 0.0,
301 }
302 }
303
304 pub const fn scale(sx: f64, sy: f64) -> Self {
306 Self {
307 tx: 0.0,
308 ty: 0.0,
309 sx,
310 sy,
311 rotation: 0.0,
312 }
313 }
314
315 pub const fn uniform_scale(s: f64) -> Self {
317 Self::scale(s, s)
318 }
319
320 pub const fn rotate(rotation: f64) -> Self {
322 Self {
323 tx: 0.0,
324 ty: 0.0,
325 sx: 1.0,
326 sy: 1.0,
327 rotation,
328 }
329 }
330
331 pub fn transform_point(&self, x: f64, y: f64) -> (f64, f64) {
333 let cos_r = self.rotation.cos();
334 let sin_r = self.rotation.sin();
335
336 let x_scaled = x * self.sx;
337 let y_scaled = y * self.sy;
338
339 let x_rotated = x_scaled * cos_r - y_scaled * sin_r;
340 let y_rotated = x_scaled * sin_r + y_scaled * cos_r;
341
342 (x_rotated + self.tx, y_rotated + self.ty)
343 }
344
345 pub fn inverse_transform_point(&self, x: f64, y: f64) -> (f64, f64) {
347 let cos_r = self.rotation.cos();
348 let sin_r = self.rotation.sin();
349
350 let x_translated = x - self.tx;
351 let y_translated = y - self.ty;
352
353 let x_rotated = x_translated * cos_r + y_translated * sin_r;
354 let y_rotated = -x_translated * sin_r + y_translated * cos_r;
355
356 (x_rotated / self.sx, y_rotated / self.sy)
357 }
358
359 pub fn compose(&self, other: &Self) -> Self {
361 Self {
362 tx: self.tx + other.tx * self.sx,
363 ty: self.ty + other.ty * self.sy,
364 sx: self.sx * other.sx,
365 sy: self.sy * other.sy,
366 rotation: self.rotation + other.rotation,
367 }
368 }
369}
370
371impl Default for ViewportTransform {
372 fn default() -> Self {
373 Self::identity()
374 }
375}
376
377#[derive(Debug, Clone, PartialEq)]
379pub struct ViewportState {
380 pub canvas_width: u32,
382 pub canvas_height: u32,
384 pub transform: ViewportTransform,
386 pub visible_bounds: (f64, f64, f64, f64),
388 pub zoom_level: u32,
390}
391
392impl ViewportState {
393 pub fn new(canvas_width: u32, canvas_height: u32) -> Self {
395 Self {
396 canvas_width,
397 canvas_height,
398 transform: ViewportTransform::identity(),
399 visible_bounds: (0.0, 0.0, canvas_width as f64, canvas_height as f64),
400 zoom_level: 0,
401 }
402 }
403
404 pub fn update_transform(&mut self, transform: ViewportTransform) {
406 self.transform = transform;
407 self.update_visible_bounds();
408 }
409
410 fn update_visible_bounds(&mut self) {
412 let (min_x, min_y) = self.transform.inverse_transform_point(0.0, 0.0);
413 let (max_x, max_y) = self
414 .transform
415 .inverse_transform_point(self.canvas_width as f64, self.canvas_height as f64);
416
417 self.visible_bounds = (
418 min_x.min(max_x),
419 min_y.min(max_y),
420 min_x.max(max_x),
421 min_y.max(max_y),
422 );
423 }
424
425 pub fn pan(&mut self, dx: f64, dy: f64) {
427 self.transform.tx += dx;
428 self.transform.ty += dy;
429 self.update_visible_bounds();
430 }
431
432 pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
434 let factor = factor.abs().max(0.01);
436
437 let (world_x, world_y) = self.transform.inverse_transform_point(center_x, center_y);
439
440 self.transform.sx *= factor;
442 self.transform.sy *= factor;
443
444 let (new_screen_x, new_screen_y) = self.transform.transform_point(world_x, world_y);
446 self.transform.tx += center_x - new_screen_x;
447 self.transform.ty += center_y - new_screen_y;
448
449 self.update_visible_bounds();
450 }
451
452 pub fn fit_to_bounds(&mut self, bounds: (f64, f64, f64, f64)) {
454 let (min_x, min_y, max_x, max_y) = bounds;
455 let width = max_x - min_x;
456 let height = max_y - min_y;
457
458 let scale_x = self.canvas_width as f64 / width;
459 let scale_y = self.canvas_height as f64 / height;
460 let scale = scale_x.min(scale_y);
461
462 self.transform.sx = scale;
463 self.transform.sy = scale;
464 self.transform.tx = -min_x * scale;
465 self.transform.ty = -min_y * scale;
466
467 self.update_visible_bounds();
468 }
469
470 pub fn visible_tiles(&self, tile_size: u32) -> Vec<TileCoord> {
472 let (min_x, min_y, max_x, max_y) = self.visible_bounds;
473
474 let min_tile_x = (min_x / tile_size as f64).floor() as u32;
475 let min_tile_y = (min_y / tile_size as f64).floor() as u32;
476 let max_tile_x = (max_x / tile_size as f64).ceil() as u32;
477 let max_tile_y = (max_y / tile_size as f64).ceil() as u32;
478
479 let mut tiles = Vec::new();
480 for y in min_tile_y..=max_tile_y {
481 for x in min_tile_x..=max_tile_x {
482 tiles.push(TileCoord::new(self.zoom_level, x, y));
483 }
484 }
485
486 tiles
487 }
488}
489
490pub struct ViewportHistory {
492 history: VecDeque<ViewportState>,
494 current_index: usize,
496 max_size: usize,
498}
499
500impl ViewportHistory {
501 pub fn new(max_size: usize) -> Self {
503 Self {
504 history: VecDeque::new(),
505 current_index: 0,
506 max_size,
507 }
508 }
509
510 pub fn push(&mut self, state: ViewportState) {
512 while self.history.len() > self.current_index + 1 {
514 self.history.pop_back();
515 }
516
517 self.history.push_back(state);
519
520 if self.history.len() > self.max_size {
522 self.history.pop_front();
523 } else {
525 self.current_index = self.history.len() - 1;
526 }
527 }
528
529 pub fn undo(&mut self) -> Option<&ViewportState> {
531 if self.current_index > 0 {
532 self.current_index -= 1;
533 self.history.get(self.current_index)
534 } else {
535 None
536 }
537 }
538
539 pub fn redo(&mut self) -> Option<&ViewportState> {
541 if self.current_index + 1 < self.history.len() {
542 self.current_index += 1;
543 self.history.get(self.current_index)
544 } else {
545 None
546 }
547 }
548
549 pub fn current(&self) -> Option<&ViewportState> {
551 self.history.get(self.current_index)
552 }
553
554 pub const fn can_undo(&self) -> bool {
556 self.current_index > 0
557 }
558
559 pub fn can_redo(&self) -> bool {
561 self.current_index + 1 < self.history.len()
562 }
563
564 pub const fn current_index(&self) -> usize {
566 self.current_index
567 }
568
569 pub fn clear(&mut self) {
571 self.history.clear();
572 self.current_index = 0;
573 }
574}
575
576pub struct CanvasBuffer {
578 data: Vec<u8>,
580 width: u32,
582 height: u32,
584 dirty: bool,
586}
587
588impl CanvasBuffer {
589 pub fn new(width: u32, height: u32) -> WasmResult<Self> {
591 if width == 0 || height == 0 {
592 return Err(WasmError::InvalidOperation {
593 operation: "CanvasBuffer::new".to_string(),
594 reason: "Width and height must be greater than zero".to_string(),
595 });
596 }
597
598 let size = (width as usize) * (height as usize) * 4;
599 let data = vec![0u8; size];
600
601 Ok(Self {
602 data,
603 width,
604 height,
605 dirty: true,
606 })
607 }
608
609 pub fn resize(&mut self, width: u32, height: u32) -> WasmResult<()> {
611 if width == 0 || height == 0 {
612 return Err(WasmError::InvalidOperation {
613 operation: "CanvasBuffer::resize".to_string(),
614 reason: "Width and height must be greater than zero".to_string(),
615 });
616 }
617
618 if width != self.width || height != self.height {
619 let size = (width as usize) * (height as usize) * 4;
620 self.data.resize(size, 0);
621 self.width = width;
622 self.height = height;
623 self.dirty = true;
624 }
625 Ok(())
626 }
627
628 pub fn clear(&mut self) {
630 self.data.fill(0);
631 self.dirty = true;
632 }
633
634 pub fn clear_with_color(&mut self, r: u8, g: u8, b: u8, a: u8) {
636 for chunk in self.data.chunks_exact_mut(4) {
637 chunk[0] = r;
638 chunk[1] = g;
639 chunk[2] = b;
640 chunk[3] = a;
641 }
642 self.dirty = true;
643 }
644
645 pub fn draw_tile(
647 &mut self,
648 tile_data: &[u8],
649 tile_x: u32,
650 tile_y: u32,
651 tile_width: u32,
652 tile_height: u32,
653 ) -> WasmResult<()> {
654 let expected_size = (tile_width * tile_height * 4) as usize;
655 if tile_data.len() != expected_size {
656 return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
657 expected: expected_size,
658 actual: tile_data.len(),
659 }));
660 }
661
662 if tile_x + tile_width > self.width || tile_y + tile_height > self.height {
664 return Err(WasmError::Canvas(CanvasError::InvalidDimensions {
665 width: tile_x + tile_width,
666 height: tile_y + tile_height,
667 reason: "Tile extends beyond buffer bounds".to_string(),
668 }));
669 }
670
671 for y in 0..tile_height {
673 let src_offset = (y * tile_width * 4) as usize;
674 let dst_offset = (((tile_y + y) * self.width + tile_x) * 4) as usize;
675 let row_size = (tile_width * 4) as usize;
676
677 self.data[dst_offset..dst_offset + row_size]
678 .copy_from_slice(&tile_data[src_offset..src_offset + row_size]);
679 }
680
681 self.dirty = true;
682 Ok(())
683 }
684
685 pub fn composite_tile(
687 &mut self,
688 tile_data: &[u8],
689 tile_width: u32,
690 tile_height: u32,
691 dst_x: u32,
692 dst_y: u32,
693 opacity: f32,
694 ) -> WasmResult<()> {
695 let expected_size = (tile_width * tile_height * 4) as usize;
696 if tile_data.len() != expected_size {
697 return Err(WasmError::Canvas(CanvasError::BufferSizeMismatch {
698 expected: expected_size,
699 actual: tile_data.len(),
700 }));
701 }
702
703 let opacity = opacity.clamp(0.0, 1.0);
705
706 let max_x = (dst_x + tile_width).min(self.width);
708 let max_y = (dst_y + tile_height).min(self.height);
709
710 if dst_x >= self.width || dst_y >= self.height {
711 return Ok(()); }
713
714 for y in dst_y..max_y {
716 for x in dst_x..max_x {
717 let src_idx = (((y - dst_y) * tile_width + (x - dst_x)) * 4) as usize;
718 let dst_idx = ((y * self.width + x) * 4) as usize;
719
720 if src_idx + 3 < tile_data.len() && dst_idx + 3 < self.data.len() {
721 let src_alpha = (f32::from(tile_data[src_idx + 3]) / 255.0) * opacity;
722 let dst_alpha = 1.0 - src_alpha;
723
724 for c in 0..3 {
726 let src_val = f32::from(tile_data[src_idx + c]);
727 let dst_val = f32::from(self.data[dst_idx + c]);
728 let blended = (src_val * src_alpha + dst_val * dst_alpha).clamp(0.0, 255.0);
729 self.data[dst_idx + c] = blended as u8;
730 }
731
732 let new_alpha = (src_alpha
734 + dst_alpha * f32::from(self.data[dst_idx + 3]) / 255.0)
735 .clamp(0.0, 1.0);
736 self.data[dst_idx + 3] = (new_alpha * 255.0) as u8;
737 }
738 }
739 }
740
741 self.dirty = true;
742 Ok(())
743 }
744
745 pub fn data(&self) -> &[u8] {
747 &self.data
748 }
749
750 pub fn mark_clean(&mut self) {
752 self.dirty = false;
753 }
754
755 pub const fn is_dirty(&self) -> bool {
757 self.dirty
758 }
759
760 pub const fn dimensions(&self) -> (u32, u32) {
762 (self.width, self.height)
763 }
764
765 pub fn to_image_data(&self) -> Result<ImageData, JsValue> {
767 let clamped = wasm_bindgen::Clamped(self.data.as_slice());
768 ImageData::new_with_u8_clamped_array_and_sh(clamped, self.width, self.height)
769 }
770}
771
772pub struct ProgressiveRenderer {
774 queue: Vec<(TileCoord, u32)>, rendering: Vec<TileCoord>,
778 completed: Vec<TileCoord>,
780 max_parallel: usize,
782}
783
784impl ProgressiveRenderer {
785 pub fn new(max_parallel: usize) -> Self {
787 Self {
788 queue: Vec::new(),
789 rendering: Vec::new(),
790 completed: Vec::new(),
791 max_parallel,
792 }
793 }
794
795 pub fn add_tiles(&mut self, tiles: Vec<TileCoord>, priority: u32) {
797 for coord in tiles {
798 if !self.is_completed(&coord) && !self.is_rendering(&coord) {
799 self.queue.push((coord, priority));
800 }
801 }
802
803 self.queue.sort_by_key(|x| std::cmp::Reverse(x.1));
805 }
806
807 pub fn next_batch(&mut self) -> Vec<TileCoord> {
809 let available = self.max_parallel.saturating_sub(self.rendering.len());
810 let mut batch = Vec::new();
811
812 for _ in 0..available {
813 if let Some((coord, _)) = self.queue.pop() {
814 self.rendering.push(coord);
815 batch.push(coord);
816 } else {
817 break;
818 }
819 }
820
821 batch
822 }
823
824 pub fn mark_completed(&mut self, coord: TileCoord) {
826 if let Some(pos) = self.rendering.iter().position(|c| *c == coord) {
827 self.rendering.remove(pos);
828 self.completed.push(coord);
829 }
830 }
831
832 pub fn is_completed(&self, coord: &TileCoord) -> bool {
834 self.completed.contains(coord)
835 }
836
837 pub fn is_rendering(&self, coord: &TileCoord) -> bool {
839 self.rendering.contains(coord)
840 }
841
842 pub fn clear(&mut self) {
844 self.queue.clear();
845 self.rendering.clear();
846 self.completed.clear();
847 }
848
849 pub fn stats(&self) -> ProgressiveRenderStats {
851 ProgressiveRenderStats {
852 queued: self.queue.len(),
853 rendering: self.rendering.len(),
854 completed: self.completed.len(),
855 }
856 }
857}
858
859#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
861pub struct ProgressiveRenderStats {
862 pub queued: usize,
864 pub rendering: usize,
866 pub completed: usize,
868}
869
870impl ProgressiveRenderStats {
871 pub const fn total(&self) -> usize {
873 self.queued + self.rendering + self.completed
874 }
875
876 pub fn completion_percentage(&self) -> f64 {
878 let total = self.total();
879 if total == 0 {
880 100.0
881 } else {
882 (self.completed as f64 / total as f64) * 100.0
883 }
884 }
885}
886
887pub struct CanvasRenderer {
889 front_buffer: CanvasBuffer,
891 back_buffer: CanvasBuffer,
893 progressive: ProgressiveRenderer,
895 viewport: ViewportState,
897 history: ViewportHistory,
899 quality: RenderQuality,
901}
902
903impl CanvasRenderer {
904 pub fn new(width: u32, height: u32, max_parallel: usize) -> WasmResult<Self> {
906 Ok(Self {
907 front_buffer: CanvasBuffer::new(width, height)?,
908 back_buffer: CanvasBuffer::new(width, height)?,
909 progressive: ProgressiveRenderer::new(max_parallel),
910 viewport: ViewportState::new(width, height),
911 history: ViewportHistory::new(MAX_VIEWPORT_HISTORY),
912 quality: RenderQuality::Medium,
913 })
914 }
915
916 pub fn resize(&mut self, width: u32, height: u32) -> WasmResult<()> {
918 self.front_buffer.resize(width, height)?;
919 self.back_buffer.resize(width, height)?;
920 self.viewport.canvas_width = width;
921 self.viewport.canvas_height = height;
922 Ok(())
923 }
924
925 pub fn begin_frame(&mut self) {
927 self.back_buffer.clear();
928 }
929
930 pub fn draw_tile(
932 &mut self,
933 coord: TileCoord,
934 tile_data: &[u8],
935 tile_width: u32,
936 tile_height: u32,
937 ) -> WasmResult<()> {
938 let world_x = f64::from(coord.x * tile_width);
940 let world_y = f64::from(coord.y * tile_height);
941 let (screen_x, screen_y) = self.viewport.transform.transform_point(world_x, world_y);
942
943 self.back_buffer.draw_tile(
944 tile_data,
945 screen_x as u32,
946 screen_y as u32,
947 tile_width,
948 tile_height,
949 )?;
950
951 self.progressive.mark_completed(coord);
952 Ok(())
953 }
954
955 pub fn swap_buffers(&mut self) {
957 std::mem::swap(&mut self.front_buffer, &mut self.back_buffer);
958 self.front_buffer.mark_clean();
959 }
960
961 pub fn front_buffer_image_data(&self) -> Result<ImageData, JsValue> {
963 self.front_buffer.to_image_data()
964 }
965
966 pub fn update_viewport(&mut self, state: ViewportState) {
968 self.history.push(self.viewport.clone());
969 self.viewport = state;
970 }
971
972 pub fn pan(&mut self, dx: f64, dy: f64) {
974 let old_state = self.viewport.clone();
975 self.viewport.pan(dx, dy);
976 if old_state != self.viewport {
977 self.history.push(old_state);
978 }
979 }
980
981 pub fn zoom(&mut self, factor: f64, center_x: f64, center_y: f64) {
983 let old_state = self.viewport.clone();
984 self.viewport.zoom(factor, center_x, center_y);
985 if old_state != self.viewport {
986 self.history.push(old_state);
987 }
988 }
989
990 pub fn undo(&mut self) -> bool {
992 if let Some(state) = self.history.undo() {
993 self.viewport = state.clone();
994 true
995 } else {
996 false
997 }
998 }
999
1000 pub fn redo(&mut self) -> bool {
1002 if let Some(state) = self.history.redo() {
1003 self.viewport = state.clone();
1004 true
1005 } else {
1006 false
1007 }
1008 }
1009
1010 pub fn set_quality(&mut self, quality: RenderQuality) {
1012 self.quality = quality;
1013 }
1014
1015 pub const fn viewport(&self) -> &ViewportState {
1017 &self.viewport
1018 }
1019
1020 pub fn progressive_stats(&self) -> ProgressiveRenderStats {
1022 self.progressive.stats()
1023 }
1024}
1025
1026pub struct AnimationManager {
1028 frame_times: VecDeque<f64>,
1030 max_samples: usize,
1032 last_frame: Option<f64>,
1034 target_fps: f64,
1036}
1037
1038impl AnimationManager {
1039 pub fn new(target_fps: f64) -> Self {
1041 Self {
1042 frame_times: VecDeque::new(),
1043 max_samples: 60,
1044 last_frame: None,
1045 target_fps,
1046 }
1047 }
1048
1049 pub fn record_frame(&mut self, timestamp: f64) {
1051 if let Some(last) = self.last_frame {
1052 let frame_time = timestamp - last;
1053 self.frame_times.push_back(frame_time);
1054
1055 if self.frame_times.len() > self.max_samples {
1056 self.frame_times.pop_front();
1057 }
1058 }
1059
1060 self.last_frame = Some(timestamp);
1061 }
1062
1063 pub fn current_fps(&self) -> f64 {
1065 if self.frame_times.is_empty() {
1066 return 0.0;
1067 }
1068
1069 let avg_frame_time: f64 =
1070 self.frame_times.iter().sum::<f64>() / self.frame_times.len() as f64;
1071 if avg_frame_time > 0.0 {
1072 1000.0 / avg_frame_time
1073 } else {
1074 0.0
1075 }
1076 }
1077
1078 pub fn average_frame_time(&self) -> f64 {
1080 if self.frame_times.is_empty() {
1081 return 0.0;
1082 }
1083
1084 self.frame_times.iter().sum::<f64>() / self.frame_times.len() as f64
1085 }
1086
1087 pub fn is_below_target(&self) -> bool {
1089 self.current_fps() < self.target_fps
1090 }
1091
1092 pub fn stats(&self) -> AnimationStats {
1094 AnimationStats {
1095 current_fps: self.current_fps(),
1096 average_frame_time_ms: self.average_frame_time(),
1097 target_fps: self.target_fps,
1098 }
1099 }
1100}
1101
1102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1104pub struct AnimationStats {
1105 pub current_fps: f64,
1107 pub average_frame_time_ms: f64,
1109 pub target_fps: f64,
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116
1117 #[test]
1118 fn test_viewport_transform() {
1119 let transform = ViewportTransform::translate(10.0, 20.0);
1120 let (x, y) = transform.transform_point(0.0, 0.0);
1121 assert_eq!(x, 10.0);
1122 assert_eq!(y, 20.0);
1123 }
1124
1125 #[test]
1126 fn test_viewport_transform_inverse() {
1127 let transform = ViewportTransform::translate(10.0, 20.0);
1128 let (x, y) = transform.inverse_transform_point(10.0, 20.0);
1129 assert!((x - 0.0).abs() < 0.001);
1130 assert!((y - 0.0).abs() < 0.001);
1131 }
1132
1133 #[test]
1134 fn test_viewport_state() {
1135 let mut state = ViewportState::new(800, 600);
1136 state.pan(10.0, 20.0);
1137 assert_eq!(state.transform.tx, 10.0);
1138 assert_eq!(state.transform.ty, 20.0);
1139 }
1140
1141 #[test]
1142 fn test_viewport_history() {
1143 let mut history = ViewportHistory::new(10);
1144 let state1 = ViewportState::new(800, 600);
1145 let mut state2 = ViewportState::new(800, 600);
1146 state2.pan(10.0, 20.0);
1147
1148 history.push(state1);
1149 history.push(state2);
1150
1151 assert!(history.can_undo());
1152 history.undo();
1153 assert!(history.can_redo());
1154 }
1155
1156 #[test]
1157 fn test_canvas_buffer() {
1158 let mut buffer = CanvasBuffer::new(256, 256).expect("Failed to create buffer");
1159 assert!(buffer.is_dirty());
1160
1161 buffer.mark_clean();
1162 assert!(!buffer.is_dirty());
1163
1164 buffer.clear();
1165 assert!(buffer.is_dirty());
1166 }
1167
1168 #[test]
1169 fn test_progressive_renderer() {
1170 let mut renderer = ProgressiveRenderer::new(4);
1171 let tiles = vec![
1172 TileCoord::new(0, 0, 0),
1173 TileCoord::new(0, 1, 0),
1174 TileCoord::new(0, 0, 1),
1175 ];
1176
1177 renderer.add_tiles(tiles, 10);
1178 let batch = renderer.next_batch();
1179 assert!(!batch.is_empty());
1180 assert!(batch.len() <= 4);
1181 }
1182
1183 #[test]
1184 fn test_animation_manager() {
1185 let mut manager = AnimationManager::new(60.0);
1186 manager.record_frame(0.0);
1187 manager.record_frame(16.67); manager.record_frame(33.34);
1189
1190 let fps = manager.current_fps();
1191 assert!(fps > 50.0 && fps < 70.0);
1192 }
1193
1194 #[test]
1195 fn test_render_quality() {
1196 assert_eq!(RenderQuality::Low.resolution_multiplier(), 0.5);
1197 assert_eq!(RenderQuality::Medium.resolution_multiplier(), 1.0);
1198 assert_eq!(RenderQuality::High.resolution_multiplier(), 1.5);
1199 assert_eq!(RenderQuality::Ultra.resolution_multiplier(), 2.0);
1200 }
1201}