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:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -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) => {