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:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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] | NDC:Y向上,原点在中心
|
||||
///
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
440
packages/engine/src/renderer/gizmo.rs
Normal file
440
packages/engine/src/renderer/gizmo.rs
Normal 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
|
||||
}
|
||||
}
|
||||
241
packages/engine/src/renderer/grid.rs
Normal file
241
packages/engine/src/renderer/grid.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
259
packages/engine/src/renderer/viewport.rs
Normal file
259
packages/engine/src/renderer/viewport.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user