feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API * feat(rapier2d): 新增Rapier2D WASM绑定包 * feat(physics-rapier2d): 添加跨平台WASM加载器 * feat(asset-system): 添加运行时资产目录和bundle格式 * feat(asset-system-editor): 新增编辑器资产管理包 * feat(editor-core): 添加构建系统和模块管理 * feat(editor-app): 重构浏览器预览使用import maps * feat(platform-web): 添加BrowserRuntime和资产读取 * feat(engine): 添加材质系统和着色器管理 * feat(material): 新增材质系统和着色器编辑器 * feat(tilemap): 增强tilemap编辑器和动画系统 * feat(modules): 添加module.json配置 * feat(core): 添加module.json和类型定义更新 * chore: 更新依赖和构建配置 * refactor(plugins): 更新插件模板使用ModuleManifest * chore: 添加第三方依赖库 * chore: 移除BehaviourTree-ai和ecs-astar子模块 * docs: 更新README和文档主题样式 * fix: 修复Rust文档测试和添加rapier2d WASM绑定 * fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题 * feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea) * fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖 * fix: 添加缺失的包依赖修复CI构建 * fix: 修复CodeQL检测到的代码问题 * fix: 修复构建错误和缺失依赖 * fix: 修复类型检查错误 * fix(material-system): 修复tsconfig配置支持TypeScript项目引用 * fix(editor-core): 修复Rollup构建配置添加tauri external * fix: 修复CodeQL检测到的代码问题 * fix: 修复CodeQL检测到的代码问题
This commit is contained in:
@@ -4,11 +4,14 @@
|
||||
|
||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import type { TilemapComponent } from '@esengine/tilemap';
|
||||
import { tilemapAnimationSystem } from '@esengine/tilemap';
|
||||
import { useTilemapEditorStore } from '../stores/TilemapEditorStore';
|
||||
import type { ITilemapTool, ToolContext } from '../tools/ITilemapTool';
|
||||
import { BrushTool } from '../tools/BrushTool';
|
||||
import { EraserTool } from '../tools/EraserTool';
|
||||
import { FillTool } from '../tools/FillTool';
|
||||
import { RectangleTool } from '../tools/RectangleTool';
|
||||
import { SelectTool } from '../tools/SelectTool';
|
||||
|
||||
interface TilemapCanvasProps {
|
||||
tilemap: TilemapComponent;
|
||||
@@ -20,6 +23,8 @@ const tools: Record<string, ITilemapTool> = {
|
||||
brush: new BrushTool(),
|
||||
eraser: new EraserTool(),
|
||||
fill: new FillTool(),
|
||||
rectangle: new RectangleTool(),
|
||||
select: new SelectTool(),
|
||||
};
|
||||
|
||||
export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
@@ -57,9 +62,12 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
const layersKey = layers.map(l => `${l.visible}-${l.opacity}`).join(',');
|
||||
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
const [lastPanPos, setLastPanPos] = useState({ x: 0, y: 0 });
|
||||
const lastPanPosRef = useRef({ x: 0, y: 0 });
|
||||
const [mousePos, setMousePos] = useState<{ tileX: number; tileY: number } | null>(null);
|
||||
const [spacePressed, setSpacePressed] = useState(false);
|
||||
const [animationTime, setAnimationTime] = useState(0);
|
||||
const lastFrameTimeRef = useRef<number>(0);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
// Get canvas size
|
||||
const canvasWidth = tilemap.width * tileWidth;
|
||||
@@ -73,11 +81,14 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Clear
|
||||
ctx.fillStyle = '#2d2d2d';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.save();
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.translate(panX, panY);
|
||||
ctx.scale(zoom, zoom);
|
||||
|
||||
@@ -104,9 +115,16 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
for (let x = 0; x < tilemap.width; x++) {
|
||||
const tileIndex = tilemap.getTile(layerIndex, x, y);
|
||||
if (tileIndex > 0) {
|
||||
// Get the tileset index for this tile (assuming single tileset for now)
|
||||
// tileIndex is 1-based (0 = empty), so tileId = tileIndex - 1
|
||||
const tileId = tileIndex - 1;
|
||||
|
||||
// Get current animation frame tile ID (returns original if not animated)
|
||||
const displayTileId = tilemapAnimationSystem.getCurrentTileId(0, tileId);
|
||||
|
||||
// Calculate source position in tileset
|
||||
const srcX = ((tileIndex - 1) % tilesetColumns) * tileWidth;
|
||||
const srcY = Math.floor((tileIndex - 1) / tilesetColumns) * tileHeight;
|
||||
const srcX = (displayTileId % tilesetColumns) * tileWidth;
|
||||
const srcY = Math.floor(displayTileId / tilesetColumns) * tileHeight;
|
||||
|
||||
// Only draw if tile is within tileset bounds
|
||||
if (srcX + tileWidth <= tilesetImage.width && srcY + tileHeight <= tilesetImage.height) {
|
||||
@@ -182,7 +200,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}, [tilemap, tilesetImage, zoom, panX, panY, showGrid, showCollision, mousePos, currentTool, selectedTiles, brushSize, currentLayer, layerLocked, editingCollision, tileWidth, tileHeight, tilesetColumns, canvasWidth, canvasHeight, layersKey]);
|
||||
}, [tilemap, tilesetImage, zoom, panX, panY, showGrid, showCollision, mousePos, currentTool, selectedTiles, brushSize, currentLayer, layerLocked, editingCollision, tileWidth, tileHeight, tilesetColumns, canvasWidth, canvasHeight, layersKey, animationTime]);
|
||||
|
||||
// Update canvas size
|
||||
useEffect(() => {
|
||||
@@ -199,11 +217,16 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const newWidth = container.clientWidth;
|
||||
const newHeight = container.clientHeight;
|
||||
if (canvas.width !== newWidth || canvas.height !== newHeight) {
|
||||
canvas.width = newWidth;
|
||||
canvas.height = newHeight;
|
||||
const scaledWidth = Math.floor(newWidth * dpr);
|
||||
const scaledHeight = Math.floor(newHeight * dpr);
|
||||
if (canvas.width !== scaledWidth || canvas.height !== scaledHeight) {
|
||||
canvas.width = scaledWidth;
|
||||
canvas.height = scaledHeight;
|
||||
canvas.style.width = `${newWidth}px`;
|
||||
canvas.style.height = `${newHeight}px`;
|
||||
draw();
|
||||
}
|
||||
rafId = null;
|
||||
@@ -223,6 +246,44 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
draw();
|
||||
}, [draw]);
|
||||
|
||||
// Register tileset animations when tilemap changes
|
||||
useEffect(() => {
|
||||
tilemapAnimationSystem.clear();
|
||||
for (let i = 0; i < tilemap.tilesets.length; i++) {
|
||||
const tilesetRef = tilemap.tilesets[i];
|
||||
if (tilesetRef.data) {
|
||||
tilemapAnimationSystem.registerTileset(i, tilesetRef.data);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
tilemapAnimationSystem.clear();
|
||||
};
|
||||
}, [tilemap]);
|
||||
|
||||
// Animation loop for animated tiles
|
||||
useEffect(() => {
|
||||
const animate = (time: number) => {
|
||||
if (lastFrameTimeRef.current === 0) {
|
||||
lastFrameTimeRef.current = time;
|
||||
}
|
||||
const deltaTime = time - lastFrameTimeRef.current;
|
||||
lastFrameTimeRef.current = time;
|
||||
|
||||
tilemapAnimationSystem.update(deltaTime);
|
||||
setAnimationTime(time);
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Center view on first mount
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
@@ -288,7 +349,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
// Middle mouse button, Alt+left click, or Space+left click for panning
|
||||
if (e.button === 1 || (e.button === 0 && (e.altKey || spacePressed))) {
|
||||
setIsPanning(true);
|
||||
setLastPanPos({ x: e.clientX, y: e.clientY });
|
||||
lastPanPosRef.current = { x: e.clientX, y: e.clientY };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,7 +374,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
};
|
||||
tool.onMouseDown(tileX, tileY, toolContext);
|
||||
onTilemapChange?.();
|
||||
draw();
|
||||
// draw() 由 useEffect 统一处理,避免重复绘制导致闪烁
|
||||
}
|
||||
};
|
||||
|
||||
@@ -326,10 +387,11 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
|
||||
// Handle panning
|
||||
if (isPanning) {
|
||||
const dx = e.clientX - lastPanPos.x;
|
||||
const dy = e.clientY - lastPanPos.y;
|
||||
setPan(panX + dx, panY + dy);
|
||||
setLastPanPos({ x: e.clientX, y: e.clientY });
|
||||
const dx = e.clientX - lastPanPosRef.current.x;
|
||||
const dy = e.clientY - lastPanPosRef.current.y;
|
||||
const state = useTilemapEditorStore.getState();
|
||||
setPan(state.panX + dx, state.panY + dy);
|
||||
lastPanPosRef.current = { x: e.clientX, y: e.clientY };
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -354,8 +416,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
onTilemapChange?.();
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
// draw() 由 setMousePos 触发的 useEffect 统一处理
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: React.MouseEvent) => {
|
||||
@@ -389,7 +450,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMousePos(null);
|
||||
draw();
|
||||
// draw() 由 setMousePos 触发的 useEffect 统一处理
|
||||
};
|
||||
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user