Feature/physics and tilemap enhancement (#247)

* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统

* feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统

* feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误

* fix: 修复CodeQL安全警告和CI类型检查错误
This commit is contained in:
YHH
2025-11-29 23:00:48 +08:00
committed by GitHub
parent f03b73b58e
commit 359886c72f
198 changed files with 33879 additions and 13121 deletions

View File

@@ -59,6 +59,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
const [isPanning, setIsPanning] = useState(false);
const [lastPanPos, setLastPanPos] = useState({ x: 0, y: 0 });
const [mousePos, setMousePos] = useState<{ tileX: number; tileY: number } | null>(null);
const [spacePressed, setSpacePressed] = useState(false);
// Get canvas size
const canvasWidth = tilemap.width * tileWidth;
@@ -189,20 +190,83 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
const container = containerRef.current;
if (!canvas || !container) return;
let rafId: number | null = null;
const resizeObserver = new ResizeObserver(() => {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
draw();
// 使用 requestAnimationFrame 避免 ResizeObserver loop 错误
// Use requestAnimationFrame to avoid ResizeObserver loop errors
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(() => {
const newWidth = container.clientWidth;
const newHeight = container.clientHeight;
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
draw();
}
rafId = null;
});
});
resizeObserver.observe(container);
return () => resizeObserver.disconnect();
return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
resizeObserver.disconnect();
};
}, [draw]);
useEffect(() => {
draw();
}, [draw]);
// Center view on first mount
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Only center if pan is at default position (0, 0)
if (panX === 0 && panY === 0) {
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const mapPixelWidth = canvasWidth * zoom;
const mapPixelHeight = canvasHeight * zoom;
const centerX = (containerWidth - mapPixelWidth) / 2;
const centerY = (containerHeight - mapPixelHeight) / 2;
setPan(centerX, centerY);
}
}, []); // Only run on mount
// Space key for panning mode
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Space' && !e.repeat) {
e.preventDefault();
setSpacePressed(true);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.code === 'Space') {
setSpacePressed(false);
setIsPanning(false);
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
// Convert screen coordinates to tile coordinates
const screenToTile = useCallback((screenX: number, screenY: number) => {
const x = (screenX - panX) / zoom;
@@ -221,8 +285,8 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Middle mouse button or space+left click for panning
if (e.button === 1 || (e.button === 0 && e.altKey)) {
// 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 });
return;
@@ -346,6 +410,13 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
setZoom(newZoom);
};
// Determine cursor style
const getCursor = () => {
if (isPanning) return 'grabbing';
if (spacePressed) return 'grab';
return tools[currentTool]?.cursor || 'crosshair';
};
return (
<div ref={containerRef} className="tilemap-canvas-container">
<canvas
@@ -357,7 +428,7 @@ export const TilemapCanvas: React.FC<TilemapCanvasProps> = ({
onMouseLeave={handleMouseLeave}
onWheel={handleWheel}
onContextMenu={(e) => e.preventDefault()}
style={{ cursor: isPanning ? 'grabbing' : tools[currentTool]?.cursor || 'default' }}
style={{ cursor: getCursor() }}
/>
</div>
);