Feature/render pipeline (#232)

* refactor(engine): 重构2D渲染管线坐标系统

* feat(engine): 完善2D渲染管线和编辑器视口功能

* feat(editor): 实现Viewport变换工具系统

* feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示

* feat(editor): 实现Run on Device移动预览功能

* feat(editor): 添加组件属性控制和依赖关系系统

* feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器

* feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(ci): 迁移项目到pnpm并修复CI构建问题

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 移除 network 相关包

* chore: 移除 network 相关包
This commit is contained in:
YHH
2025-11-23 14:49:37 +08:00
committed by GitHub
parent b15cbab313
commit a3f7cc38b1
247 changed files with 33561 additions and 52047 deletions

View File

@@ -141,6 +141,14 @@ impl WebGLContext {
.viewport(0, 0, self.width() as i32, self.height() as i32);
}
/// Resize the canvas and viewport.
/// 调整canvas和视口大小。
pub fn resize(&mut self, width: u32, height: u32) {
self.canvas.set_width(width);
self.canvas.set_height(height);
self.gl.viewport(0, 0, width as i32, height as i32);
}
/// Enable alpha blending for transparency.
/// 启用透明度的alpha混合。
pub fn enable_blend(&self) {

View File

@@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*;
use super::context::WebGLContext;
use super::error::Result;
use crate::input::InputManager;
use crate::renderer::Renderer2D;
use crate::renderer::{Renderer2D, GridRenderer, GizmoRenderer, TransformMode, ViewportManager};
use crate::resource::TextureManager;
/// Engine configuration options.
@@ -45,6 +45,14 @@ pub struct Engine {
/// 2D渲染器。
renderer: Renderer2D,
/// Grid renderer for editor.
/// 编辑器网格渲染器。
grid_renderer: GridRenderer,
/// Gizmo renderer for editor overlays.
/// 编辑器叠加层Gizmo渲染器。
gizmo_renderer: GizmoRenderer,
/// Texture manager.
/// 纹理管理器。
texture_manager: TextureManager,
@@ -57,6 +65,18 @@ pub struct Engine {
/// 引擎配置。
#[allow(dead_code)]
config: EngineConfig,
/// Whether to show grid.
/// 是否显示网格。
show_grid: bool,
/// Viewport manager for multi-viewport rendering.
/// 多视口渲染的视口管理器。
viewport_manager: ViewportManager,
/// Whether to show gizmos.
/// 是否显示辅助工具。
show_gizmos: bool,
}
impl Engine {
@@ -78,6 +98,8 @@ impl Engine {
// Create subsystems | 创建子系统
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
let grid_renderer = GridRenderer::new(context.gl())?;
let gizmo_renderer = GizmoRenderer::new(context.gl())?;
let texture_manager = TextureManager::new(context.gl().clone());
let input_manager = InputManager::new();
@@ -86,9 +108,14 @@ impl Engine {
Ok(Self {
context,
renderer,
grid_renderer,
gizmo_renderer,
texture_manager,
input_manager,
config,
show_grid: true,
viewport_manager: ViewportManager::new(),
show_gizmos: true,
})
}
@@ -109,6 +136,8 @@ impl Engine {
context.enable_blend();
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
let grid_renderer = GridRenderer::new(context.gl())?;
let gizmo_renderer = GizmoRenderer::new(context.gl())?;
let texture_manager = TextureManager::new(context.gl().clone());
let input_manager = InputManager::new();
@@ -117,9 +146,14 @@ impl Engine {
Ok(Self {
context,
renderer,
grid_renderer,
gizmo_renderer,
texture_manager,
input_manager,
config,
show_grid: true,
viewport_manager: ViewportManager::new(),
show_gizmos: true,
})
}
@@ -152,6 +186,14 @@ impl Engine {
uvs: &[f32],
colors: &[u32],
) -> Result<()> {
// Debug: log once
use std::sync::atomic::{AtomicBool, Ordering};
static LOGGED: AtomicBool = AtomicBool::new(false);
if !LOGGED.swap(true, Ordering::Relaxed) {
let sprite_count = texture_ids.len();
log::info!("Engine submit_sprite_batch: {} sprites, texture_ids: {:?}", sprite_count, texture_ids);
}
self.renderer.submit_batch(
transforms,
texture_ids,
@@ -164,7 +206,56 @@ impl Engine {
/// Render the current frame.
/// 渲染当前帧。
pub fn render(&mut self) -> Result<()> {
self.renderer.render(self.context.gl())
// Clear background with clear color
let [r, g, b, a] = self.renderer.get_clear_color();
self.context.clear(r, g, b, a);
// Render grid first (background)
if self.show_grid {
self.grid_renderer.render(self.context.gl(), self.renderer.camera());
self.grid_renderer.render_axes(self.context.gl(), self.renderer.camera());
}
// Render sprites
self.renderer.render(self.context.gl(), &self.texture_manager)?;
// Render gizmos on top
self.gizmo_renderer.render(self.context.gl(), self.renderer.camera());
self.gizmo_renderer.clear();
Ok(())
}
/// Add a rectangle gizmo.
/// 添加矩形Gizmo。
pub fn add_gizmo_rect(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
rotation: f32,
origin_x: f32,
origin_y: f32,
r: f32,
g: f32,
b: f32,
a: f32,
show_handles: bool,
) {
self.gizmo_renderer.add_rect(x, y, width, height, rotation, origin_x, origin_y, r, g, b, a, show_handles);
}
/// Set transform tool mode.
/// 设置变换工具模式。
pub fn set_transform_mode(&mut self, mode: u8) {
let transform_mode = match mode {
1 => TransformMode::Move,
2 => TransformMode::Rotate,
3 => TransformMode::Scale,
_ => TransformMode::Select,
};
self.gizmo_renderer.set_transform_mode(transform_mode);
}
/// Load a texture from URL.
@@ -173,6 +264,24 @@ impl Engine {
self.texture_manager.load_texture(id, url)
}
/// Load texture by path, returning texture ID.
/// 按路径加载纹理返回纹理ID。
pub fn load_texture_by_path(&mut self, path: &str) -> Result<u32> {
self.texture_manager.load_texture_by_path(path)
}
/// Get texture ID by path.
/// 按路径获取纹理ID。
pub fn get_texture_id_by_path(&self, path: &str) -> Option<u32> {
self.texture_manager.get_texture_id_by_path(path)
}
/// Get or load texture by path.
/// 按路径获取或加载纹理。
pub fn get_or_load_by_path(&mut self, path: &str) -> Result<u32> {
self.texture_manager.get_or_load_by_path(path)
}
/// Check if a key is currently pressed.
/// 检查某个键是否当前被按下。
pub fn is_key_down(&self, key_code: &str) -> bool {
@@ -184,4 +293,169 @@ impl Engine {
pub fn update_input(&mut self) {
self.input_manager.update();
}
/// Resize viewport.
/// 调整视口大小。
pub fn resize(&mut self, width: f32, height: f32) {
self.context.resize(width as u32, height as u32);
self.renderer.resize(width, height);
}
/// Set camera position, zoom, and rotation.
/// 设置相机位置、缩放和旋转。
///
/// # Arguments | 参数
/// * `x` - Camera X position | 相机X位置
/// * `y` - Camera Y position | 相机Y位置
/// * `zoom` - Zoom level | 缩放级别
/// * `rotation` - Rotation in radians | 旋转角度(弧度)
pub fn set_camera(&mut self, x: f32, y: f32, zoom: f32, rotation: f32) {
let camera = self.renderer.camera_mut();
camera.position.x = x;
camera.position.y = y;
camera.set_zoom(zoom);
camera.rotation = rotation;
}
/// Get camera position.
/// 获取相机位置。
pub fn get_camera(&self) -> (f32, f32, f32, f32) {
let camera = self.renderer.camera();
(camera.position.x, camera.position.y, camera.zoom, camera.rotation)
}
/// Set grid visibility.
/// 设置网格可见性。
pub fn set_show_grid(&mut self, show: bool) {
self.show_grid = show;
}
/// Set gizmo visibility.
/// 设置辅助工具可见性。
pub fn set_show_gizmos(&mut self, show: bool) {
self.show_gizmos = show;
}
/// Get gizmo visibility.
/// 获取辅助工具可见性。
pub fn show_gizmos(&self) -> bool {
self.show_gizmos
}
/// Set clear color for the active viewport.
/// 设置活动视口的清除颜色。
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
if let Some(target) = self.viewport_manager.active_mut() {
target.set_clear_color(r, g, b, a);
} else {
// Fallback to primary renderer
self.renderer.set_clear_color(r, g, b, a);
}
}
// ===== Multi-viewport API =====
// ===== 多视口 API =====
/// Register a new viewport.
/// 注册新视口。
pub fn register_viewport(&mut self, id: &str, canvas_id: &str) -> Result<()> {
self.viewport_manager.register(id, canvas_id)
}
/// Unregister a viewport.
/// 注销视口。
pub fn unregister_viewport(&mut self, id: &str) {
self.viewport_manager.unregister(id);
}
/// Set the active viewport.
/// 设置活动视口。
pub fn set_active_viewport(&mut self, id: &str) -> bool {
self.viewport_manager.set_active(id)
}
/// Get active viewport ID.
/// 获取活动视口ID。
pub fn active_viewport_id(&self) -> Option<&str> {
self.viewport_manager.active().map(|v| v.id.as_str())
}
/// Set camera for a specific viewport.
/// 为特定视口设置相机。
pub fn set_viewport_camera(&mut self, viewport_id: &str, x: f32, y: f32, zoom: f32, rotation: f32) {
if let Some(viewport) = self.viewport_manager.get_mut(viewport_id) {
viewport.set_camera(x, y, zoom, rotation);
}
}
/// Get camera for a specific viewport.
/// 获取特定视口的相机。
pub fn get_viewport_camera(&self, viewport_id: &str) -> Option<(f32, f32, f32, f32)> {
self.viewport_manager.get(viewport_id).map(|v| v.get_camera())
}
/// Set viewport configuration.
/// 设置视口配置。
pub fn set_viewport_config(&mut self, viewport_id: &str, show_grid: bool, show_gizmos: bool) {
if let Some(viewport) = self.viewport_manager.get_mut(viewport_id) {
viewport.config.show_grid = show_grid;
viewport.config.show_gizmos = show_gizmos;
}
}
/// Resize a specific viewport.
/// 调整特定视口大小。
pub fn resize_viewport(&mut self, viewport_id: &str, width: u32, height: u32) {
if let Some(viewport) = self.viewport_manager.get_mut(viewport_id) {
viewport.resize(width, height);
}
}
/// Render to a specific viewport.
/// 渲染到特定视口。
pub fn render_to_viewport(&mut self, viewport_id: &str) -> Result<()> {
let viewport = match self.viewport_manager.get(viewport_id) {
Some(v) => v,
None => return Ok(()),
};
// Get viewport settings
let show_grid = viewport.config.show_grid;
let show_gizmos = viewport.config.show_gizmos;
let camera = viewport.camera.clone();
// Bind viewport and clear
viewport.bind();
viewport.clear();
// Update renderer camera to match viewport camera
let renderer_camera = self.renderer.camera_mut();
renderer_camera.position = camera.position;
renderer_camera.set_zoom(camera.zoom);
renderer_camera.rotation = camera.rotation;
renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height());
// Render grid if enabled
if show_grid {
self.grid_renderer.render(viewport.gl(), &camera);
self.grid_renderer.render_axes(viewport.gl(), &camera);
}
// Render sprites
self.renderer.render(viewport.gl(), &self.texture_manager)?;
// Render gizmos if enabled
if show_gizmos {
self.gizmo_renderer.render(viewport.gl(), &camera);
}
self.gizmo_renderer.clear();
Ok(())
}
/// Get all registered viewport IDs.
/// 获取所有已注册的视口ID。
pub fn viewport_ids(&self) -> Vec<String> {
self.viewport_manager.viewport_ids().into_iter().cloned().collect()
}
}

View File

@@ -176,6 +176,40 @@ impl GameEngine {
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Load texture by path, returning texture ID.
/// 按路径加载纹理返回纹理ID。
///
/// # Arguments | 参数
/// * `path` - Image path/URL to load | 要加载的图片路径/URL
#[wasm_bindgen(js_name = loadTextureByPath)]
pub fn load_texture_by_path(&mut self, path: &str) -> std::result::Result<u32, JsValue> {
self.engine
.load_texture_by_path(path)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Get texture ID by path.
/// 按路径获取纹理ID。
///
/// # Arguments | 参数
/// * `path` - Image path to lookup | 要查找的图片路径
#[wasm_bindgen(js_name = getTextureIdByPath)]
pub fn get_texture_id_by_path(&self, path: &str) -> Option<u32> {
self.engine.get_texture_id_by_path(path)
}
/// Get or load texture by path.
/// 按路径获取或加载纹理。
///
/// # Arguments | 参数
/// * `path` - Image path/URL | 图片路径/URL
#[wasm_bindgen(js_name = getOrLoadTextureByPath)]
pub fn get_or_load_by_path(&mut self, path: &str) -> std::result::Result<u32, JsValue> {
self.engine
.get_or_load_by_path(path)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Check if a key is currently pressed.
/// 检查某个键是否当前被按下。
///
@@ -192,4 +226,180 @@ impl GameEngine {
pub fn update_input(&mut self) {
self.engine.update_input();
}
/// Resize viewport.
/// 调整视口大小。
///
/// # Arguments | 参数
/// * `width` - New viewport width | 新视口宽度
/// * `height` - New viewport height | 新视口高度
pub fn resize(&mut self, width: u32, height: u32) {
self.engine.resize(width as f32, height as f32);
}
/// Set camera position, zoom, and rotation.
/// 设置相机位置、缩放和旋转。
///
/// # Arguments | 参数
/// * `x` - Camera X position | 相机X位置
/// * `y` - Camera Y position | 相机Y位置
/// * `zoom` - Zoom level | 缩放级别
/// * `rotation` - Rotation in radians | 旋转角度(弧度)
#[wasm_bindgen(js_name = setCamera)]
pub fn set_camera(&mut self, x: f32, y: f32, zoom: f32, rotation: f32) {
self.engine.set_camera(x, y, zoom, rotation);
}
/// Get camera state.
/// 获取相机状态。
///
/// # Returns | 返回
/// Array of [x, y, zoom, rotation] | 数组 [x, y, zoom, rotation]
#[wasm_bindgen(js_name = getCamera)]
pub fn get_camera(&self) -> Vec<f32> {
let (x, y, zoom, rotation) = self.engine.get_camera();
vec![x, y, zoom, rotation]
}
/// Set grid visibility.
/// 设置网格可见性。
#[wasm_bindgen(js_name = setShowGrid)]
pub fn set_show_grid(&mut self, show: bool) {
self.engine.set_show_grid(show);
}
/// Set clear color (background color).
/// 设置清除颜色(背景颜色)。
///
/// # Arguments | 参数
/// * `r`, `g`, `b`, `a` - Color components (0.0-1.0) | 颜色分量 (0.0-1.0)
#[wasm_bindgen(js_name = setClearColor)]
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
self.engine.set_clear_color(r, g, b, a);
}
/// Add a rectangle gizmo outline.
/// 添加矩形Gizmo边框。
///
/// # Arguments | 参数
/// * `x` - Center X position | 中心X位置
/// * `y` - Center Y position | 中心Y位置
/// * `width` - Rectangle width | 矩形宽度
/// * `height` - Rectangle height | 矩形高度
/// * `rotation` - Rotation in radians | 旋转角度(弧度)
/// * `origin_x` - Origin X (0-1) | 原点X (0-1)
/// * `origin_y` - Origin Y (0-1) | 原点Y (0-1)
/// * `r`, `g`, `b`, `a` - Color (0.0-1.0) | 颜色
/// * `show_handles` - Whether to show transform handles | 是否显示变换手柄
#[wasm_bindgen(js_name = addGizmoRect)]
pub fn add_gizmo_rect(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
rotation: f32,
origin_x: f32,
origin_y: f32,
r: f32,
g: f32,
b: f32,
a: f32,
show_handles: bool,
) {
self.engine.add_gizmo_rect(x, y, width, height, rotation, origin_x, origin_y, r, g, b, a, show_handles);
}
/// Set transform tool mode.
/// 设置变换工具模式。
///
/// # Arguments | 参数
/// * `mode` - 0=Select, 1=Move, 2=Rotate, 3=Scale
#[wasm_bindgen(js_name = setTransformMode)]
pub fn set_transform_mode(&mut self, mode: u8) {
self.engine.set_transform_mode(mode);
}
/// Set gizmo visibility.
/// 设置辅助工具可见性。
#[wasm_bindgen(js_name = setShowGizmos)]
pub fn set_show_gizmos(&mut self, show: bool) {
self.engine.set_show_gizmos(show);
}
// ===== Multi-viewport API =====
// ===== 多视口 API =====
/// Register a new viewport.
/// 注册新视口。
///
/// # Arguments | 参数
/// * `id` - Unique viewport identifier | 唯一视口标识符
/// * `canvas_id` - HTML canvas element ID | HTML canvas元素ID
#[wasm_bindgen(js_name = registerViewport)]
pub fn register_viewport(&mut self, id: &str, canvas_id: &str) -> std::result::Result<(), JsValue> {
self.engine
.register_viewport(id, canvas_id)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Unregister a viewport.
/// 注销视口。
#[wasm_bindgen(js_name = unregisterViewport)]
pub fn unregister_viewport(&mut self, id: &str) {
self.engine.unregister_viewport(id);
}
/// Set the active viewport.
/// 设置活动视口。
#[wasm_bindgen(js_name = setActiveViewport)]
pub fn set_active_viewport(&mut self, id: &str) -> bool {
self.engine.set_active_viewport(id)
}
/// Set camera for a specific viewport.
/// 为特定视口设置相机。
#[wasm_bindgen(js_name = setViewportCamera)]
pub fn set_viewport_camera(&mut self, viewport_id: &str, x: f32, y: f32, zoom: f32, rotation: f32) {
self.engine.set_viewport_camera(viewport_id, x, y, zoom, rotation);
}
/// Get camera for a specific viewport.
/// 获取特定视口的相机。
#[wasm_bindgen(js_name = getViewportCamera)]
pub fn get_viewport_camera(&self, viewport_id: &str) -> Option<Vec<f32>> {
self.engine
.get_viewport_camera(viewport_id)
.map(|(x, y, zoom, rotation)| vec![x, y, zoom, rotation])
}
/// Set viewport configuration.
/// 设置视口配置。
#[wasm_bindgen(js_name = setViewportConfig)]
pub fn set_viewport_config(&mut self, viewport_id: &str, show_grid: bool, show_gizmos: bool) {
self.engine.set_viewport_config(viewport_id, show_grid, show_gizmos);
}
/// Resize a specific viewport.
/// 调整特定视口大小。
#[wasm_bindgen(js_name = resizeViewport)]
pub fn resize_viewport(&mut self, viewport_id: &str, width: u32, height: u32) {
self.engine.resize_viewport(viewport_id, width, height);
}
/// Render to a specific viewport.
/// 渲染到特定视口。
#[wasm_bindgen(js_name = renderToViewport)]
pub fn render_to_viewport(&mut self, viewport_id: &str) -> std::result::Result<(), JsValue> {
self.engine
.render_to_viewport(viewport_id)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Get all registered viewport IDs.
/// 获取所有已注册的视口ID。
#[wasm_bindgen(js_name = getViewportIds)]
pub fn get_viewport_ids(&self) -> Vec<String> {
self.engine.viewport_ids()
}
}

View File

@@ -1,6 +1,7 @@
//! Sprite batch renderer for efficient 2D rendering.
//! 用于高效2D渲染的精灵批处理渲染器。
use std::collections::HashMap;
use web_sys::{
WebGl2RenderingContext, WebGlBuffer, WebGlVertexArrayObject,
};
@@ -34,7 +35,7 @@ const UV_STRIDE: usize = 4;
///
/// # Performance | 性能
/// - Uses dynamic vertex buffer for efficient updates | 使用动态顶点缓冲区以高效更新
/// - Minimizes state changes and draw calls | 最小化状态更改和绘制调用
/// - Groups sprites by texture to minimize state changes | 按纹理分组精灵以最小化状态更改
/// - Supports up to 10000+ sprites per batch | 每批次支持10000+精灵
pub struct SpriteBatch {
/// Vertex array object.
@@ -53,17 +54,13 @@ pub struct SpriteBatch {
/// 最大精灵数。
max_sprites: usize,
/// Vertex data buffer.
/// 顶点数据缓冲区。
vertices: Vec<f32>,
/// Per-texture vertex data buffers.
/// 按纹理分组的顶点数据缓冲区。
texture_batches: HashMap<u32, Vec<f32>>,
/// Current number of sprites in batch.
/// 当前批次中的精灵数。
/// Total sprite count across all batches.
/// 所有批次的总精灵数。
sprite_count: usize,
/// Current texture ID being batched.
/// 当前正在批处理的纹理ID。
current_texture: Option<u32>,
}
impl SpriteBatch {
@@ -127,9 +124,8 @@ impl SpriteBatch {
vbo,
ibo,
max_sprites,
vertices: Vec::with_capacity(max_sprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX),
texture_batches: HashMap::new(),
sprite_count: 0,
current_texture: None,
})
}
@@ -196,9 +192,10 @@ impl SpriteBatch {
/// Clear the batch for a new frame.
/// 为新帧清空批处理。
pub fn clear(&mut self) {
self.vertices.clear();
for batch in self.texture_batches.values_mut() {
batch.clear();
}
self.sprite_count = 0;
self.current_texture = None;
}
/// Add sprites from batch data.
@@ -209,14 +206,14 @@ impl SpriteBatch {
/// * `texture_ids` - Texture ID for each sprite | 每个精灵的纹理ID
/// * `uvs` - [u0, v0, u1, v1] per sprite | 每个精灵的UV坐标
/// * `colors` - Packed RGBA color per sprite | 每个精灵的打包RGBA颜色
/// * `texture_manager` - Texture manager for getting texture sizes | 纹理管理器
/// * `_texture_manager` - Texture manager for getting texture sizes | 纹理管理器
pub fn add_sprites(
&mut self,
transforms: &[f32],
texture_ids: &[u32],
uvs: &[f32],
colors: &[u32],
texture_manager: &TextureManager,
_texture_manager: &TextureManager,
) -> Result<()> {
let sprite_count = texture_ids.len();
@@ -253,7 +250,7 @@ impl SpriteBatch {
)));
}
// Add each sprite | 添加每个精灵
// Add each sprite grouped by texture | 按纹理分组添加每个精灵
for i in 0..sprite_count {
let t_offset = i * TRANSFORM_STRIDE;
let uv_offset = i * UV_STRIDE;
@@ -274,16 +271,21 @@ impl SpriteBatch {
let color = Color::from_packed(colors[i]);
let color_arr = [color.r, color.g, color.b, color.a];
// Get texture size for this sprite | 获取此精灵的纹理尺寸
let (tex_width, tex_height) = texture_manager
.get_texture_size(texture_ids[i])
.unwrap_or((64.0, 64.0));
// scale_x and scale_y are the actual display dimensions
// scale_x 和 scale_y 是实际显示尺寸
let width = scale_x;
let height = scale_y;
let width = tex_width * scale_x;
let height = tex_height * scale_y;
let texture_id = texture_ids[i];
// Calculate transformed vertices | 计算变换后的顶点
self.add_sprite_vertices(
// Get or create batch for this texture | 获取或创建此纹理的批次
let batch = self.texture_batches
.entry(texture_id)
.or_insert_with(Vec::new);
// Calculate transformed vertices and add to batch | 计算变换后的顶点并添加到批次
Self::add_sprite_vertices_to_batch(
batch,
x, y, width, height, rotation, origin_x, origin_y,
u0, v0, u1, v1, color_arr,
);
@@ -293,11 +295,11 @@ impl SpriteBatch {
Ok(())
}
/// Add vertices for a single sprite.
/// 为单个精灵添加顶点。
/// Add vertices for a single sprite to a batch.
/// 为单个精灵添加顶点到批次
#[inline]
fn add_sprite_vertices(
&mut self,
fn add_sprite_vertices_to_batch(
batch: &mut Vec<f32>,
x: f32,
y: f32,
width: f32,
@@ -315,22 +317,28 @@ impl SpriteBatch {
let sin = rotation.sin();
// Origin offset | 原点偏移
// origin (0,0) = bottom-left, (1,1) = top-right
// 原点 (0,0) = 左下角, (1,1) = 右上角
let ox = origin_x * width;
let oy = origin_y * height;
// Local corner positions (relative to origin) | 局部角点位置(相对于原点)
// Y-up coordinate system | Y向上坐标系
let corners = [
(-ox, -oy), // Top-left | 左上
(width - ox, -oy), // Top-right | 右上
(width - ox, height - oy), // Bottom-right | 右下
(-ox, height - oy), // Bottom-left | 左下
(-ox, height - oy), // Top-left | 左上
(width - ox, height - oy), // Top-right | 右上
(width - ox, -oy), // Bottom-right | 右下
(-ox, -oy), // Bottom-left | 左下
];
// UV coordinates match OpenGL/WebGL convention
// UV坐标匹配OpenGL/WebGL约定
// (0,0) = bottom-left of texture, (1,1) = top-right
let tex_coords = [
[u0, v0], // Top-left
[u1, v0], // Top-right
[u1, v1], // Bottom-right
[u0, v1], // Bottom-left
[u0, v1], // Top-left (texture top)
[u1, v1], // Top-right (texture top)
[u1, v0], // Bottom-right (texture bottom)
[u0, v0], // Bottom-left (texture bottom)
];
// Transform and add each vertex | 变换并添加每个顶点
@@ -346,32 +354,34 @@ impl SpriteBatch {
let py = ry + y;
// Position | 位置
self.vertices.push(px);
self.vertices.push(py);
batch.push(px);
batch.push(py);
// Texture coordinates | 纹理坐标
self.vertices.push(tex_coords[i][0]);
self.vertices.push(tex_coords[i][1]);
batch.push(tex_coords[i][0]);
batch.push(tex_coords[i][1]);
// Color | 颜色
self.vertices.extend_from_slice(&color);
batch.extend_from_slice(&color);
}
}
/// Flush the batch to GPU and render.
/// 将批处理刷新到GPU并渲染。
pub fn flush(&mut self, gl: &WebGl2RenderingContext) {
if self.sprite_count == 0 {
/// Flush a specific texture batch to GPU and render.
/// 将特定纹理批次刷新到GPU并渲染。
fn flush_texture_batch(&self, gl: &WebGl2RenderingContext, vertices: &[f32]) {
if vertices.is_empty() {
return;
}
let sprite_count = vertices.len() / (VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
// Bind VAO | 绑定VAO
gl.bind_vertex_array(Some(&self.vao));
// Upload vertex data | 上传顶点数据
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vbo));
unsafe {
let vertex_array = js_sys::Float32Array::view(&self.vertices);
let vertex_array = js_sys::Float32Array::view(vertices);
gl.buffer_sub_data_with_i32_and_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
0,
@@ -380,7 +390,7 @@ impl SpriteBatch {
}
// Draw | 绘制
let index_count = (self.sprite_count * INDICES_PER_SPRITE) as i32;
let index_count = (sprite_count * INDICES_PER_SPRITE) as i32;
gl.draw_elements_with_i32(
WebGl2RenderingContext::TRIANGLES,
index_count,
@@ -392,6 +402,20 @@ impl SpriteBatch {
gl.bind_vertex_array(None);
}
/// Get texture batches for rendering.
/// 获取用于渲染的纹理批次。
pub fn texture_batches(&self) -> &HashMap<u32, Vec<f32>> {
&self.texture_batches
}
/// Flush a specific texture batch.
/// 刷新特定纹理批次。
pub fn flush_for_texture(&self, gl: &WebGl2RenderingContext, texture_id: u32) {
if let Some(vertices) = self.texture_batches.get(&texture_id) {
self.flush_texture_batch(gl, vertices);
}
}
/// Get current sprite count.
/// 获取当前精灵数量。
#[inline]

View File

@@ -59,39 +59,87 @@ impl Camera2D {
/// Get the projection matrix.
/// 获取投影矩阵。
///
/// Creates an orthographic projection that maps screen coordinates
/// to normalized device coordinates.
/// 创建将屏幕坐标映射到标准化设备坐标的正交投影。
/// Creates an orthographic projection that maps world coordinates
/// to normalized device coordinates [-1, 1].
/// 创建将世界坐标映射到标准化设备坐标[-1, 1]的正交投影。
///
/// Coordinate system | 坐标系统:
/// - World: Y-up, origin at camera position | 世界坐标Y向上原点在相机位置
/// - Screen: Y-down, origin at top-left | 屏幕坐标Y向下原点在左上角
/// - NDC: Y-up, origin at center [-1, 1] | NDCY向上原点在中心
///
/// When zoom=1, 1 world unit = 1 screen pixel.
/// 当zoom=1时1个世界单位 = 1个屏幕像素。
pub fn projection_matrix(&self) -> Mat3 {
// Orthographic projection | 正交投影
// Maps [0, width] x [0, height] to [-1, 1] x [-1, 1]
let sx = 2.0 / self.width * self.zoom;
let sy = -2.0 / self.height * self.zoom; // Flip Y axis | 翻转Y轴
// Standard orthographic projection
// 标准正交投影
// Maps world coordinates to NDC [-1, 1]
// 将世界坐标映射到NDC [-1, 1]
// Scale factors: world units to NDC
// 缩放因子世界单位到NDC
let sx = 2.0 / self.width * self.zoom;
let sy = 2.0 / self.height * self.zoom;
// Handle rotation
// 处理旋转
let cos = self.rotation.cos();
let sin = self.rotation.sin();
// Apply zoom, rotation, and translation
// 应用缩放、旋转和平移
let tx = -self.position.x * sx * cos - self.position.y * sy * sin - 1.0;
let ty = -self.position.x * sx * sin + self.position.y * sy * cos + 1.0;
// Translation: camera position to NDC
// 平移相机位置到NDC
// We negate position because moving camera right should move world left
// 取反位置,因为相机向右移动应该使世界向左移动
let tx = -self.position.x * sx;
let ty = -self.position.y * sy;
Mat3::from_cols(
glam::Vec3::new(sx * cos, sx * sin, 0.0),
glam::Vec3::new(sy * -sin, sy * cos, 0.0),
glam::Vec3::new(tx, ty, 1.0),
)
// Combine scale, rotation, and translation
// 组合缩放、旋转和平移
// Matrix = Scale * Rotation * Translation (applied right to left)
// 矩阵 = 缩放 * 旋转 * 平移(从右到左应用)
if self.rotation != 0.0 {
// With rotation: need to rotate the translation as well
// 有旋转时:平移也需要旋转
let rtx = tx * cos - ty * sin;
let rty = tx * sin + ty * cos;
Mat3::from_cols(
glam::Vec3::new(sx * cos, sx * sin, 0.0),
glam::Vec3::new(-sy * sin, sy * cos, 0.0),
glam::Vec3::new(rtx, rty, 1.0),
)
} else {
// No rotation: simplified matrix
// 无旋转:简化矩阵
Mat3::from_cols(
glam::Vec3::new(sx, 0.0, 0.0),
glam::Vec3::new(0.0, sy, 0.0),
glam::Vec3::new(tx, ty, 1.0),
)
}
}
/// Convert screen coordinates to world coordinates.
/// 将屏幕坐标转换为世界坐标。
///
/// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角Y向下
/// World: Y-up, camera at center | 世界Y向上相机在中心
pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
let x = (screen.x / self.zoom) + self.position.x;
let y = (screen.y / self.zoom) + self.position.y;
// Convert screen to NDC-like coordinates (centered, Y-up)
// 将屏幕坐标转换为类NDC坐标居中Y向上
let centered_x = screen.x - self.width / 2.0;
let centered_y = self.height / 2.0 - screen.y; // Flip Y
// Apply inverse zoom and add camera position
// 应用反向缩放并加上相机位置
let world_x = centered_x / self.zoom + self.position.x;
let world_y = centered_y / self.zoom + self.position.y;
if self.rotation != 0.0 {
let dx = x - self.position.x;
let dy = y - self.position.y;
// Apply inverse rotation around camera position
// 围绕相机位置应用反向旋转
let dx = world_x - self.position.x;
let dy = world_y - self.position.y;
let cos = (-self.rotation).cos();
let sin = (-self.rotation).sin();
@@ -100,26 +148,33 @@ impl Camera2D {
dx * sin + dy * cos + self.position.y,
)
} else {
Vec2::new(x, y)
Vec2::new(world_x, world_y)
}
}
/// Convert world coordinates to screen coordinates.
/// 将世界坐标转换为屏幕坐标。
///
/// World: Y-up | 世界Y向上
/// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角Y向下
pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
let dx = world.x - self.position.x;
let dy = world.y - self.position.y;
if self.rotation != 0.0 {
let (rx, ry) = if self.rotation != 0.0 {
let cos = self.rotation.cos();
let sin = self.rotation.sin();
let rx = dx * cos - dy * sin;
let ry = dx * sin + dy * cos;
Vec2::new(rx * self.zoom, ry * self.zoom)
(dx * cos - dy * sin, dx * sin + dy * cos)
} else {
Vec2::new(dx * self.zoom, dy * self.zoom)
}
(dx, dy)
};
// Apply zoom and convert to screen coordinates
// 应用缩放并转换为屏幕坐标
let screen_x = rx * self.zoom + self.width / 2.0;
let screen_y = self.height / 2.0 - ry * self.zoom; // Flip Y
Vec2::new(screen_x, screen_y)
}
/// Move camera by delta.
@@ -133,7 +188,17 @@ impl Camera2D {
/// 设置缩放级别并限制范围。
#[inline]
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom.clamp(0.1, 10.0);
self.zoom = zoom.clamp(0.01, 100.0);
}
#[inline]
pub fn viewport_width(&self) -> f32 {
self.width
}
#[inline]
pub fn viewport_height(&self) -> f32 {
self.height
}
}

View File

@@ -0,0 +1,440 @@
//! Gizmo renderer for editor overlays.
//! 编辑器叠加层的Gizmo渲染器。
use web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram};
use crate::core::error::{Result, EngineError};
use super::camera::Camera2D;
const GIZMO_VERTEX_SHADER: &str = r#"#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
uniform mat3 u_projection;
void main() {
vec3 pos = u_projection * vec3(a_position, 1.0);
gl_Position = vec4(pos.xy, 0.0, 1.0);
}
"#;
const GIZMO_FRAGMENT_SHADER: &str = r#"#version 300 es
precision highp float;
uniform vec4 u_color;
out vec4 fragColor;
void main() {
fragColor = u_color;
}
"#;
/// Transform tool mode.
/// 变换工具模式。
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TransformMode {
/// Selection mode - show bounds only
Select,
/// Move mode - show translation arrows
Move,
/// Rotate mode - show rotation circle
Rotate,
/// Scale mode - show scale handles
Scale,
}
impl Default for TransformMode {
fn default() -> Self {
TransformMode::Select
}
}
/// Gizmo renderer for drawing editor overlays like selection bounds.
/// 用于绘制编辑器叠加层如选择边界的Gizmo渲染器。
pub struct GizmoRenderer {
program: WebGlProgram,
vertex_buffer: WebGlBuffer,
/// Pending rectangle data: [x, y, width, height, rotation, origin_x, origin_y, r, g, b, a, show_handles]
/// 待渲染的矩形数据
rects: Vec<f32>,
/// Current transform mode
transform_mode: TransformMode,
}
impl GizmoRenderer {
/// Create a new gizmo renderer.
/// 创建新的Gizmo渲染器。
pub fn new(gl: &WebGl2RenderingContext) -> Result<Self> {
let program = Self::create_program(gl)?;
let vertex_buffer = gl.create_buffer()
.ok_or(EngineError::BufferCreationFailed)?;
Ok(Self {
program,
vertex_buffer,
rects: Vec::new(),
transform_mode: TransformMode::default(),
})
}
fn create_program(gl: &WebGl2RenderingContext) -> Result<WebGlProgram> {
let vert_shader = gl.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create vertex shader".into()))?;
gl.shader_source(&vert_shader, GIZMO_VERTEX_SHADER);
gl.compile_shader(&vert_shader);
if !gl.get_shader_parameter(&vert_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_shader_info_log(&vert_shader).unwrap_or_default();
return Err(EngineError::ShaderCompileFailed(format!("Gizmo vertex shader: {}", log)));
}
let frag_shader = gl.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create fragment shader".into()))?;
gl.shader_source(&frag_shader, GIZMO_FRAGMENT_SHADER);
gl.compile_shader(&frag_shader);
if !gl.get_shader_parameter(&frag_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_shader_info_log(&frag_shader).unwrap_or_default();
return Err(EngineError::ShaderCompileFailed(format!("Gizmo fragment shader: {}", log)));
}
let program = gl.create_program()
.ok_or_else(|| EngineError::ProgramLinkFailed("Failed to create gizmo program".into()))?;
gl.attach_shader(&program, &vert_shader);
gl.attach_shader(&program, &frag_shader);
gl.link_program(&program);
if !gl.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_program_info_log(&program).unwrap_or_default();
return Err(EngineError::ProgramLinkFailed(format!("Gizmo program: {}", log)));
}
gl.delete_shader(Some(&vert_shader));
gl.delete_shader(Some(&frag_shader));
Ok(program)
}
/// Clear all pending gizmos.
/// 清空所有待渲染的Gizmo。
pub fn clear(&mut self) {
self.rects.clear();
}
/// Add a rectangle outline gizmo.
/// 添加矩形边框Gizmo。
///
/// # Arguments | 参数
/// * `x` - Center X position | 中心X位置
/// * `y` - Center Y position | 中心Y位置
/// * `width` - Rectangle width | 矩形宽度
/// * `height` - Rectangle height | 矩形高度
/// * `rotation` - Rotation in radians | 旋转角度(弧度)
/// * `origin_x` - Origin X (0-1) | 原点X (0-1)
/// * `origin_y` - Origin Y (0-1) | 原点Y (0-1)
/// * `r`, `g`, `b`, `a` - Color | 颜色
/// * `show_handles` - Whether to show transform handles | 是否显示变换手柄
pub fn add_rect(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
rotation: f32,
origin_x: f32,
origin_y: f32,
r: f32,
g: f32,
b: f32,
a: f32,
show_handles: bool,
) {
self.rects.extend_from_slice(&[
x, y, width, height, rotation, origin_x, origin_y, r, g, b, a, if show_handles { 1.0 } else { 0.0 }
]);
}
/// Render all pending gizmos.
/// 渲染所有待渲染的Gizmo。
pub fn render(&mut self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
if self.rects.is_empty() {
return;
}
gl.use_program(Some(&self.program));
let projection = camera.projection_matrix();
let proj_loc = gl.get_uniform_location(&self.program, "u_projection");
gl.uniform_matrix3fv_with_f32_array(proj_loc.as_ref(), false, &projection.to_cols_array());
let color_loc = gl.get_uniform_location(&self.program, "u_color");
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vertex_buffer));
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_with_i32(0, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
// Process each rectangle (12 floats per rect)
let rect_stride = 12;
let rect_count = self.rects.len() / rect_stride;
for i in 0..rect_count {
let offset = i * rect_stride;
let x = self.rects[offset];
let y = self.rects[offset + 1];
let width = self.rects[offset + 2];
let height = self.rects[offset + 3];
let rotation = self.rects[offset + 4];
let origin_x = self.rects[offset + 5];
let origin_y = self.rects[offset + 6];
let r = self.rects[offset + 7];
let g = self.rects[offset + 8];
let b = self.rects[offset + 9];
let a = self.rects[offset + 10];
let show_handles = self.rects[offset + 11] > 0.5;
// Calculate transformed corners
let vertices = self.calculate_rect_vertices(x, y, width, height, rotation, origin_x, origin_y);
unsafe {
let array = js_sys::Float32Array::view(&vertices);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), r, g, b, a);
gl.draw_arrays(WebGl2RenderingContext::LINE_LOOP, 0, 4);
// Only draw transform handles if explicitly requested
// 只有明确请求时才绘制变换手柄
if show_handles {
// Draw transform handles based on mode
match self.transform_mode {
TransformMode::Select => {
// Just the selection box (already drawn)
}
TransformMode::Move => {
// Draw move arrows at center
self.draw_move_handles(gl, &color_loc, x, y, rotation, camera);
}
TransformMode::Rotate => {
// Draw rotation circle
self.draw_rotate_handles(gl, &color_loc, x, y, width.max(height) * 0.6, camera);
}
TransformMode::Scale => {
// Draw scale handles at corners
self.draw_scale_handles(gl, &color_loc, x, y, width, height, rotation, origin_x, origin_y, camera);
}
}
}
}
gl.disable_vertex_attrib_array(0);
}
/// Set transform mode.
/// 设置变换模式。
pub fn set_transform_mode(&mut self, mode: TransformMode) {
self.transform_mode = mode;
}
/// Get transform mode.
/// 获取变换模式。
pub fn get_transform_mode(&self) -> TransformMode {
self.transform_mode
}
/// Draw move handles (arrows).
/// 绘制移动手柄(箭头)。
fn draw_move_handles(
&self,
gl: &WebGl2RenderingContext,
color_loc: &Option<web_sys::WebGlUniformLocation>,
x: f32,
y: f32,
rotation: f32,
camera: &Camera2D,
) {
let arrow_length = 50.0 / camera.zoom;
let arrow_head = 10.0 / camera.zoom;
let cos = rotation.cos();
let sin = rotation.sin();
// X axis (red)
let x_end_x = x + arrow_length * cos;
let x_end_y = y + arrow_length * sin;
let x_arrow = [
x, y,
x_end_x, x_end_y,
x_end_x - arrow_head * cos + arrow_head * 0.3 * sin,
x_end_y - arrow_head * sin - arrow_head * 0.3 * cos,
x_end_x, x_end_y,
x_end_x - arrow_head * cos - arrow_head * 0.3 * sin,
x_end_y - arrow_head * sin + arrow_head * 0.3 * cos,
];
unsafe {
let array = js_sys::Float32Array::view(&x_arrow);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 1.0, 0.3, 0.3, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINE_STRIP, 0, 5);
// Y axis (green)
let y_end_x = x - arrow_length * sin;
let y_end_y = y + arrow_length * cos;
let y_arrow = [
x, y,
y_end_x, y_end_y,
y_end_x + arrow_head * sin + arrow_head * 0.3 * cos,
y_end_y - arrow_head * cos + arrow_head * 0.3 * sin,
y_end_x, y_end_y,
y_end_x + arrow_head * sin - arrow_head * 0.3 * cos,
y_end_y - arrow_head * cos - arrow_head * 0.3 * sin,
];
unsafe {
let array = js_sys::Float32Array::view(&y_arrow);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 0.3, 1.0, 0.3, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINE_STRIP, 0, 5);
}
/// Draw rotation handle (circle).
/// 绘制旋转手柄(圆形)。
fn draw_rotate_handles(
&self,
gl: &WebGl2RenderingContext,
color_loc: &Option<web_sys::WebGlUniformLocation>,
x: f32,
y: f32,
radius: f32,
_camera: &Camera2D,
) {
let segments = 32;
let mut vertices = Vec::with_capacity(segments * 2);
for i in 0..segments {
let angle = (i as f32 / segments as f32) * std::f32::consts::PI * 2.0;
vertices.push(x + radius * angle.cos());
vertices.push(y + radius * angle.sin());
}
unsafe {
let array = js_sys::Float32Array::view(&vertices);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 0.3, 0.6, 1.0, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINE_LOOP, 0, segments as i32);
}
/// Draw scale handles (squares at corners).
/// 绘制缩放手柄(角落的方块)。
fn draw_scale_handles(
&self,
gl: &WebGl2RenderingContext,
color_loc: &Option<web_sys::WebGlUniformLocation>,
x: f32,
y: f32,
width: f32,
height: f32,
rotation: f32,
origin_x: f32,
origin_y: f32,
camera: &Camera2D,
) {
let handle_size = 6.0 / camera.zoom;
let corners = self.calculate_rect_vertices(x, y, width, height, rotation, origin_x, origin_y);
// Draw a small square at each corner
for i in 0..4 {
let cx = corners[i * 2];
let cy = corners[i * 2 + 1];
let square = [
cx - handle_size, cy - handle_size,
cx + handle_size, cy - handle_size,
cx + handle_size, cy + handle_size,
cx - handle_size, cy + handle_size,
];
unsafe {
let array = js_sys::Float32Array::view(&square);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 1.0, 0.8, 0.2, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINE_LOOP, 0, 4);
}
}
/// Calculate the 4 corner vertices of a rotated rectangle.
/// 计算旋转矩形的4个角点顶点。
fn calculate_rect_vertices(
&self,
x: f32,
y: f32,
width: f32,
height: f32,
rotation: f32,
origin_x: f32,
origin_y: f32,
) -> [f32; 8] {
let cos = rotation.cos();
let sin = rotation.sin();
// Origin offset
let ox = origin_x * width;
let oy = origin_y * height;
// Local corner positions (relative to origin)
// Y-up coordinate system
let corners = [
(-ox, height - oy), // Top-left
(width - ox, height - oy), // Top-right
(width - ox, -oy), // Bottom-right
(-ox, -oy), // Bottom-left
];
let mut vertices = [0.0f32; 8];
for (i, (lx, ly)) in corners.iter().enumerate() {
// Apply rotation
let rx = lx * cos - ly * sin;
let ry = lx * sin + ly * cos;
// Apply translation
vertices[i * 2] = rx + x;
vertices[i * 2 + 1] = ry + y;
}
vertices
}
}

View File

@@ -0,0 +1,241 @@
//! Grid renderer for editor viewport.
//! 编辑器视口的网格渲染器。
use web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram};
use crate::core::error::{Result, EngineError};
use super::camera::Camera2D;
const GRID_VERTEX_SHADER: &str = r#"#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
uniform mat3 u_projection;
void main() {
vec3 pos = u_projection * vec3(a_position, 1.0);
gl_Position = vec4(pos.xy, 0.0, 1.0);
}
"#;
const GRID_FRAGMENT_SHADER: &str = r#"#version 300 es
precision highp float;
uniform vec4 u_color;
out vec4 fragColor;
void main() {
fragColor = u_color;
}
"#;
pub struct GridRenderer {
program: WebGlProgram,
vertex_buffer: WebGlBuffer,
vertex_count: i32,
last_zoom: f32,
last_width: f32,
last_height: f32,
}
impl GridRenderer {
pub fn new(gl: &WebGl2RenderingContext) -> Result<Self> {
let program = Self::create_program(gl)?;
let vertex_buffer = gl.create_buffer()
.ok_or(EngineError::BufferCreationFailed)?;
Ok(Self {
program,
vertex_buffer,
vertex_count: 0,
last_zoom: 0.0,
last_width: 0.0,
last_height: 0.0,
})
}
fn create_program(gl: &WebGl2RenderingContext) -> Result<WebGlProgram> {
let vert_shader = gl.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create vertex shader".into()))?;
gl.shader_source(&vert_shader, GRID_VERTEX_SHADER);
gl.compile_shader(&vert_shader);
if !gl.get_shader_parameter(&vert_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_shader_info_log(&vert_shader).unwrap_or_default();
return Err(EngineError::ShaderCompileFailed(format!("Grid vertex shader: {}", log)));
}
let frag_shader = gl.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create fragment shader".into()))?;
gl.shader_source(&frag_shader, GRID_FRAGMENT_SHADER);
gl.compile_shader(&frag_shader);
if !gl.get_shader_parameter(&frag_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_shader_info_log(&frag_shader).unwrap_or_default();
return Err(EngineError::ShaderCompileFailed(format!("Grid fragment shader: {}", log)));
}
let program = gl.create_program()
.ok_or_else(|| EngineError::ProgramLinkFailed("Failed to create grid program".into()))?;
gl.attach_shader(&program, &vert_shader);
gl.attach_shader(&program, &frag_shader);
gl.link_program(&program);
if !gl.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
let log = gl.get_program_info_log(&program).unwrap_or_default();
return Err(EngineError::ProgramLinkFailed(format!("Grid program: {}", log)));
}
gl.delete_shader(Some(&vert_shader));
gl.delete_shader(Some(&frag_shader));
Ok(program)
}
fn update_grid(&mut self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
let zoom = camera.zoom;
let width = camera.viewport_width();
let height = camera.viewport_height();
if (zoom - self.last_zoom).abs() < 0.001
&& (width - self.last_width).abs() < 1.0
&& (height - self.last_height).abs() < 1.0
{
return;
}
self.last_zoom = zoom;
self.last_width = width;
self.last_height = height;
let half_width = width / (2.0 * zoom);
let half_height = height / (2.0 * zoom);
let max_size = half_width.max(half_height) * 2.0;
let base_step = if max_size > 10000.0 {
1000.0
} else if max_size > 1000.0 {
100.0
} else if max_size > 100.0 {
10.0
} else if max_size > 10.0 {
1.0
} else {
0.1
};
let fine_step = base_step;
let range = max_size * 1.5;
let start = -range;
let end = range;
let mut vertices = Vec::new();
let mut x = (start / fine_step).floor() * fine_step;
while x <= end {
vertices.extend_from_slice(&[x, start, x, end]);
x += fine_step;
}
let mut y = (start / fine_step).floor() * fine_step;
while y <= end {
vertices.extend_from_slice(&[start, y, end, y]);
y += fine_step;
}
self.vertex_count = (vertices.len() / 2) as i32;
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vertex_buffer));
unsafe {
let array = js_sys::Float32Array::view(&vertices);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
}
pub fn render(&mut self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
self.update_grid(gl, camera);
if self.vertex_count == 0 {
return;
}
gl.use_program(Some(&self.program));
let projection = camera.projection_matrix();
let proj_loc = gl.get_uniform_location(&self.program, "u_projection");
gl.uniform_matrix3fv_with_f32_array(proj_loc.as_ref(), false, &projection.to_cols_array());
let color_loc = gl.get_uniform_location(&self.program, "u_color");
gl.uniform4f(color_loc.as_ref(), 0.3, 0.3, 0.35, 1.0);
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vertex_buffer));
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_with_i32(0, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, self.vertex_count);
gl.disable_vertex_attrib_array(0);
}
pub fn render_axes(&self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
gl.use_program(Some(&self.program));
let projection = camera.projection_matrix();
let proj_loc = gl.get_uniform_location(&self.program, "u_projection");
gl.uniform_matrix3fv_with_f32_array(proj_loc.as_ref(), false, &projection.to_cols_array());
let half_width = camera.viewport_width() / (2.0 * camera.zoom);
let half_height = camera.viewport_height() / (2.0 * camera.zoom);
let axis_length = half_width.max(half_height) * 2.0;
let color_loc = gl.get_uniform_location(&self.program, "u_color");
let axis_buffer = gl.create_buffer().unwrap();
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&axis_buffer));
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_with_i32(0, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
// X axis (red)
let x_axis = [-axis_length, 0.0, axis_length, 0.0f32];
unsafe {
let array = js_sys::Float32Array::view(&x_axis);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::STATIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 1.0, 0.3, 0.3, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, 2);
// Y axis (green)
let y_axis = [0.0, -axis_length, 0.0, axis_length];
unsafe {
let array = js_sys::Float32Array::view(&y_axis);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&array,
WebGl2RenderingContext::STATIC_DRAW,
);
}
gl.uniform4f(color_loc.as_ref(), 0.3, 1.0, 0.3, 1.0);
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, 2);
gl.disable_vertex_attrib_array(0);
gl.delete_buffer(Some(&axis_buffer));
}
}

View File

@@ -7,8 +7,14 @@ pub mod texture;
mod renderer2d;
mod camera;
mod grid;
mod gizmo;
mod viewport;
pub use renderer2d::Renderer2D;
pub use camera::Camera2D;
pub use batch::SpriteBatch;
pub use texture::{Texture, TextureManager};
pub use grid::GridRenderer;
pub use gizmo::{GizmoRenderer, TransformMode};
pub use viewport::{RenderTarget, ViewportManager, ViewportConfig};

View File

@@ -27,6 +27,10 @@ pub struct Renderer2D {
/// 2D camera.
/// 2D相机。
camera: Camera2D,
/// Clear color (RGBA).
/// 清除颜色 (RGBA)。
clear_color: [f32; 4],
}
impl Renderer2D {
@@ -57,6 +61,7 @@ impl Renderer2D {
sprite_batch,
shader,
camera,
clear_color: [0.1, 0.1, 0.12, 1.0],
})
}
@@ -88,7 +93,7 @@ impl Renderer2D {
/// Render the current frame.
/// 渲染当前帧。
pub fn render(&mut self, gl: &WebGl2RenderingContext) -> Result<()> {
pub fn render(&mut self, gl: &WebGl2RenderingContext, texture_manager: &TextureManager) -> Result<()> {
if self.sprite_batch.sprite_count() == 0 {
return Ok(());
}
@@ -103,8 +108,21 @@ impl Renderer2D {
// Set texture sampler | 设置纹理采样器
self.shader.set_uniform_i32(gl, "u_texture", 0);
// Flush sprite batch | 刷新精灵批处理
self.sprite_batch.flush(gl);
// Render each texture batch | 渲染每个纹理批次
// Only collect non-empty batches | 只收集非空批次
let texture_ids: Vec<u32> = self.sprite_batch.texture_batches()
.iter()
.filter(|(_, vertices)| !vertices.is_empty())
.map(|(id, _)| *id)
.collect();
for texture_id in texture_ids {
// Bind texture for this batch | 绑定此批次的纹理
texture_manager.bind_texture(texture_id, 0);
// Flush this texture's sprites | 刷新此纹理的精灵
self.sprite_batch.flush_for_texture(gl, texture_id);
}
// Clear batch for next frame | 清空批处理以供下一帧使用
self.sprite_batch.clear();
@@ -126,6 +144,18 @@ impl Renderer2D {
&self.camera
}
/// Set clear color (RGBA, each component 0.0-1.0).
/// 设置清除颜色。
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
self.clear_color = [r, g, b, a];
}
/// Get clear color.
/// 获取清除颜色。
pub fn get_clear_color(&self) -> [f32; 4] {
self.clear_color
}
/// Update camera viewport size.
/// 更新相机视口大小。
pub fn resize(&mut self, width: f32, height: f32) {

View File

@@ -2,8 +2,6 @@
//! 纹理加载和管理。
use std::collections::HashMap;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{HtmlImageElement, WebGl2RenderingContext, WebGlTexture};
@@ -22,6 +20,14 @@ pub struct TextureManager {
/// 已加载的纹理。
textures: HashMap<u32, Texture>,
/// Path to texture ID mapping.
/// 路径到纹理ID的映射。
path_to_id: HashMap<String, u32>,
/// Next texture ID for auto-assignment.
/// 下一个自动分配的纹理ID。
next_id: u32,
/// Default white texture for untextured rendering.
/// 用于无纹理渲染的默认白色纹理。
default_texture: Option<WebGlTexture>,
@@ -34,6 +40,8 @@ impl TextureManager {
let mut manager = Self {
gl,
textures: HashMap::new(),
path_to_id: HashMap::new(),
next_id: 1, // Start from 1, 0 is reserved for default
default_texture: None,
};
@@ -105,32 +113,34 @@ impl TextureManager {
Some(&placeholder),
);
// Clone texture handle for async loading before storing | 在存储前克隆纹理句柄用于异步加载
let texture_for_closure = texture.clone();
// Store texture with placeholder size | 存储带占位符尺寸的纹理
self.textures.insert(id, Texture::new(texture.clone(), 1, 1));
self.textures.insert(id, Texture::new(texture, 1, 1));
// Load actual image asynchronously | 异步加载实际图片
let gl = self.gl.clone();
let texture_rc = Rc::new(RefCell::new(texture));
let texture_clone = Rc::clone(&texture_rc);
// We need to update the stored texture size after loading
// For MVP, we'll handle this through a callback mechanism
// 加载后需要更新存储的纹理尺寸
// 对于MVP我们通过回调机制处理
let image = HtmlImageElement::new()
.map_err(|_| EngineError::TextureLoadFailed("Failed to create image element".into()))?;
// Set crossOrigin for CORS support | 设置crossOrigin以支持CORS
image.set_cross_origin(Some("anonymous"));
// Clone image for use in closure | 克隆图片用于闭包
let image_clone = image.clone();
// Set up load callback | 设置加载回调
let onload = Closure::wrap(Box::new(move || {
let tex = texture_clone.borrow();
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&tex));
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture_for_closure));
// Flip Y axis for correct orientation (image coords vs WebGL coords)
// 翻转Y轴以获得正确的方向图像坐标系 vs WebGL坐标系
gl.pixel_storei(WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL, 1);
// Use the captured image element | 使用捕获的图片元素
let _ = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
let result = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA as i32,
@@ -139,6 +149,10 @@ impl TextureManager {
&image_clone,
);
if let Err(e) = result {
log::error!("Failed to upload texture: {:?} | 纹理上传失败: {:?}", e, e);
}
// Set texture parameters | 设置纹理参数
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
@@ -161,7 +175,6 @@ impl TextureManager {
WebGl2RenderingContext::LINEAR as i32,
);
log::debug!("Texture loaded | 纹理加载完成");
}) as Box<dyn Fn()>);
image.set_onload(Some(onload.as_ref().unchecked_ref()));
@@ -196,7 +209,14 @@ impl TextureManager {
if let Some(texture) = self.textures.get(&id) {
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture.handle));
} else if let Some(default) = &self.default_texture {
// ID 0 is the default texture, no warning needed
// ID 0 是默认纹理,不需要警告
if id != 0 {
log::warn!("Texture {} not found, using default | 未找到纹理 {},使用默认纹理", id, id);
}
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(default));
} else {
log::error!("Texture {} not found and no default texture! | 未找到纹理 {} 且没有默认纹理!", id, id);
}
}
@@ -213,5 +233,57 @@ impl TextureManager {
if let Some(texture) = self.textures.remove(&id) {
self.gl.delete_texture(Some(&texture.handle));
}
// Also remove from path mapping | 同时从路径映射中移除
self.path_to_id.retain(|_, &mut v| v != id);
}
/// Load texture by path, returning texture ID.
/// 按路径加载纹理返回纹理ID。
///
/// If the texture is already loaded, returns existing ID.
/// 如果纹理已加载返回现有ID。
pub fn load_texture_by_path(&mut self, path: &str) -> Result<u32> {
// Check if already loaded | 检查是否已加载
if let Some(&id) = self.path_to_id.get(path) {
return Ok(id);
}
// Assign new ID and load | 分配新ID并加载
let id = self.next_id;
self.next_id += 1;
// Store path mapping first | 先存储路径映射
self.path_to_id.insert(path.to_string(), id);
// Load texture with assigned ID | 用分配的ID加载纹理
self.load_texture(id, path)?;
Ok(id)
}
/// Get texture ID by path.
/// 按路径获取纹理ID。
///
/// Returns None if texture is not loaded.
/// 如果纹理未加载返回None。
#[inline]
pub fn get_texture_id_by_path(&self, path: &str) -> Option<u32> {
self.path_to_id.get(path).copied()
}
/// Get or load texture by path.
/// 按路径获取或加载纹理。
///
/// If texture is already loaded, returns existing ID.
/// If not loaded, loads it and returns new ID.
/// 如果纹理已加载返回现有ID。
/// 如果未加载加载它并返回新ID。
pub fn get_or_load_by_path(&mut self, path: &str) -> Result<u32> {
// Empty path means default texture | 空路径表示默认纹理
if path.is_empty() {
return Ok(0);
}
self.load_texture_by_path(path)
}
}

View File

@@ -0,0 +1,259 @@
//! Viewport and RenderTarget management for multi-view rendering.
//! 多视图渲染的视口和渲染目标管理。
use std::collections::HashMap;
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};
use wasm_bindgen::JsCast;
use super::camera::Camera2D;
use crate::core::error::{EngineError, Result};
/// Viewport configuration and settings.
/// 视口配置和设置。
#[derive(Debug, Clone)]
pub struct ViewportConfig {
/// Whether to show grid overlay.
pub show_grid: bool,
/// Whether to show gizmos.
pub show_gizmos: bool,
/// Clear color (RGBA).
pub clear_color: [f32; 4],
}
impl Default for ViewportConfig {
fn default() -> Self {
Self {
show_grid: true,
show_gizmos: true,
clear_color: [0.1, 0.1, 0.12, 1.0],
}
}
}
/// A render target representing a viewport.
/// 表示视口的渲染目标。
pub struct RenderTarget {
/// Unique identifier for this viewport.
pub id: String,
/// The canvas element this viewport renders to.
canvas: HtmlCanvasElement,
/// WebGL context for this canvas.
gl: WebGl2RenderingContext,
/// Camera for this viewport.
pub camera: Camera2D,
/// Viewport configuration.
pub config: ViewportConfig,
/// Width in pixels.
width: u32,
/// Height in pixels.
height: u32,
}
impl RenderTarget {
/// Create a new render target from a canvas ID.
pub fn new(id: &str, canvas_id: &str) -> Result<Self> {
let window = web_sys::window().expect("No window found");
let document = window.document().expect("No document found");
let canvas = document
.get_element_by_id(canvas_id)
.ok_or_else(|| EngineError::CanvasNotFound(canvas_id.to_string()))?
.dyn_into::<HtmlCanvasElement>()
.map_err(|_| EngineError::CanvasNotFound(canvas_id.to_string()))?;
let gl = canvas
.get_context("webgl2")
.map_err(|_| EngineError::ContextCreationFailed)?
.ok_or(EngineError::ContextCreationFailed)?
.dyn_into::<WebGl2RenderingContext>()
.map_err(|_| EngineError::ContextCreationFailed)?;
let width = canvas.width();
let height = canvas.height();
let camera = Camera2D::new(width as f32, height as f32);
log::info!(
"RenderTarget created: {} ({}x{})",
id, width, height
);
Ok(Self {
id: id.to_string(),
canvas,
gl,
camera,
config: ViewportConfig::default(),
width,
height,
})
}
/// Get the WebGL context.
#[inline]
pub fn gl(&self) -> &WebGl2RenderingContext {
&self.gl
}
/// Get canvas reference.
#[inline]
pub fn canvas(&self) -> &HtmlCanvasElement {
&self.canvas
}
/// Get viewport dimensions.
#[inline]
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
/// Resize the viewport.
pub fn resize(&mut self, width: u32, height: u32) {
self.width = width;
self.height = height;
self.canvas.set_width(width);
self.canvas.set_height(height);
self.gl.viewport(0, 0, width as i32, height as i32);
self.camera.set_viewport(width as f32, height as f32);
}
/// Clear the viewport with configured color.
pub fn clear(&self) {
let [r, g, b, a] = self.config.clear_color;
self.gl.clear_color(r, g, b, a);
self.gl.clear(
WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
);
}
/// Make this render target current (bind its context).
pub fn bind(&self) {
self.gl.viewport(0, 0, self.width as i32, self.height as i32);
}
/// Set camera parameters.
pub fn set_camera(&mut self, x: f32, y: f32, zoom: f32, rotation: f32) {
self.camera.position.x = x;
self.camera.position.y = y;
self.camera.set_zoom(zoom);
self.camera.rotation = rotation;
}
/// Get camera parameters.
pub fn get_camera(&self) -> (f32, f32, f32, f32) {
(
self.camera.position.x,
self.camera.position.y,
self.camera.zoom,
self.camera.rotation,
)
}
/// Set clear color (RGBA, each component 0.0-1.0).
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
self.config.clear_color = [r, g, b, a];
}
/// Get clear color.
pub fn get_clear_color(&self) -> [f32; 4] {
self.config.clear_color
}
}
/// Manages multiple viewports for the engine.
/// 管理引擎的多个视口。
pub struct ViewportManager {
/// All registered viewports.
viewports: HashMap<String, RenderTarget>,
/// Currently active viewport ID.
active_viewport: Option<String>,
}
impl ViewportManager {
/// Create a new viewport manager.
pub fn new() -> Self {
Self {
viewports: HashMap::new(),
active_viewport: None,
}
}
/// Register a new viewport.
pub fn register(&mut self, id: &str, canvas_id: &str) -> Result<()> {
if self.viewports.contains_key(id) {
log::warn!("Viewport already registered: {}", id);
return Ok(());
}
let target = RenderTarget::new(id, canvas_id)?;
self.viewports.insert(id.to_string(), target);
// Set as active if it's the first viewport
if self.active_viewport.is_none() {
self.active_viewport = Some(id.to_string());
}
Ok(())
}
/// Unregister a viewport.
pub fn unregister(&mut self, id: &str) {
self.viewports.remove(id);
if self.active_viewport.as_deref() == Some(id) {
self.active_viewport = self.viewports.keys().next().cloned();
}
}
/// Set the active viewport.
pub fn set_active(&mut self, id: &str) -> bool {
if self.viewports.contains_key(id) {
self.active_viewport = Some(id.to_string());
true
} else {
false
}
}
/// Get the active viewport.
pub fn active(&self) -> Option<&RenderTarget> {
self.active_viewport
.as_ref()
.and_then(|id| self.viewports.get(id))
}
/// Get mutable active viewport.
pub fn active_mut(&mut self) -> Option<&mut RenderTarget> {
let id = self.active_viewport.clone()?;
self.viewports.get_mut(&id)
}
/// Get a viewport by ID.
pub fn get(&self, id: &str) -> Option<&RenderTarget> {
self.viewports.get(id)
}
/// Get mutable viewport by ID.
pub fn get_mut(&mut self, id: &str) -> Option<&mut RenderTarget> {
self.viewports.get_mut(id)
}
/// Get all viewport IDs.
pub fn viewport_ids(&self) -> Vec<&String> {
self.viewports.keys().collect()
}
/// Iterate over all viewports.
pub fn iter(&self) -> impl Iterator<Item = (&String, &RenderTarget)> {
self.viewports.iter()
}
/// Iterate over all viewports mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut RenderTarget)> {
self.viewports.iter_mut()
}
}
impl Default for ViewportManager {
fn default() -> Self {
Self::new()
}
}