Feature/tilemap editor (#237)
* feat: 添加 Tilemap 编辑器插件和组件生命周期支持 * feat(editor-core): 添加声明式插件注册 API * feat(editor-core): 改进tiledmap结构合并tileset进tiledmapeditor * feat: 添加 editor-runtime SDK 和插件系统改进 * fix(ci): 修复SceneResourceManager里变量未使用问题
This commit is contained in:
150
packages/tilemap-editor/src/components/panels/TilesetPanel.tsx
Normal file
150
packages/tilemap-editor/src/components/panels/TilesetPanel.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Tileset Panel - Display tileset for selection
|
||||
*/
|
||||
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { TilemapComponent, type ITilesetData } from '@esengine/tilemap';
|
||||
import { useTilemapEditorStore } from '../../stores/TilemapEditorStore';
|
||||
import { TilesetPreview } from '../TilesetPreview';
|
||||
import '../../styles/TilemapEditor.css';
|
||||
|
||||
// Helper to convert file path to URL
|
||||
function convertFileSrc(path: string): string {
|
||||
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('asset://')) {
|
||||
return path;
|
||||
}
|
||||
return `asset://localhost/${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
interface TilesetPanelProps {
|
||||
projectPath?: string | null;
|
||||
}
|
||||
|
||||
export const TilesetPanel: React.FC<TilesetPanelProps> = () => {
|
||||
const {
|
||||
entityId,
|
||||
tilesetImageUrl,
|
||||
tilesetColumns,
|
||||
tilesetRows,
|
||||
tileWidth,
|
||||
tileHeight,
|
||||
selectedTiles,
|
||||
setTileset
|
||||
} = useTilemapEditorStore();
|
||||
|
||||
// Load tileset from component
|
||||
const loadTilesetFromComponent = useCallback(() => {
|
||||
if (!entityId) return;
|
||||
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const foundEntity = scene.findEntityById(parseInt(entityId, 10));
|
||||
if (!foundEntity) return;
|
||||
|
||||
const tilemapComp = foundEntity.getComponent(TilemapComponent);
|
||||
if (!tilemapComp) return;
|
||||
|
||||
// Get tileset source from first tileset
|
||||
const tilesetRef = tilemapComp.tilesets[0];
|
||||
if (!tilesetRef) return;
|
||||
|
||||
const tilesetPath = tilesetRef.source;
|
||||
const imageUrl = convertFileSrc(tilesetPath);
|
||||
const currentState = useTilemapEditorStore.getState();
|
||||
|
||||
// Check if URL or tile dimensions changed
|
||||
const urlChanged = imageUrl !== currentState.tilesetImageUrl;
|
||||
const dimensionsChanged =
|
||||
tilemapComp.tileWidth !== currentState.tileWidth ||
|
||||
tilemapComp.tileHeight !== currentState.tileHeight;
|
||||
|
||||
if (!urlChanged && !dimensionsChanged) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const columns = Math.floor(img.width / tilemapComp.tileWidth);
|
||||
const rows = Math.floor(img.height / tilemapComp.tileHeight);
|
||||
|
||||
// Create tileset data and set it
|
||||
const tilesetData: ITilesetData = {
|
||||
name: 'tileset',
|
||||
version: 1,
|
||||
image: tilesetPath,
|
||||
imageWidth: img.width,
|
||||
imageHeight: img.height,
|
||||
tileWidth: tilemapComp.tileWidth,
|
||||
tileHeight: tilemapComp.tileHeight,
|
||||
tileCount: columns * rows,
|
||||
columns,
|
||||
rows
|
||||
};
|
||||
tilemapComp.setTilesetData(0, tilesetData);
|
||||
setTileset(imageUrl, columns, rows, tilemapComp.tileWidth, tilemapComp.tileHeight);
|
||||
};
|
||||
img.src = imageUrl;
|
||||
}, [entityId, setTileset]);
|
||||
|
||||
// Load tileset when entityId is set but tilesetImageUrl is not yet loaded
|
||||
useEffect(() => {
|
||||
if (!entityId || tilesetImageUrl) return;
|
||||
loadTilesetFromComponent();
|
||||
}, [entityId, tilesetImageUrl, loadTilesetFromComponent]);
|
||||
|
||||
// Listen for scene modifications to reload tileset when property changes
|
||||
useEffect(() => {
|
||||
if (!entityId) return;
|
||||
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
if (!messageHub) return;
|
||||
|
||||
const unsubscribe = messageHub.subscribe('scene:modified', () => {
|
||||
loadTilesetFromComponent();
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [entityId, loadTilesetFromComponent]);
|
||||
|
||||
if (!tilesetImageUrl) {
|
||||
return (
|
||||
<div className="tileset-panel">
|
||||
<div className="tileset-panel-header">
|
||||
<h3>Tileset</h3>
|
||||
</div>
|
||||
<div className="tileset-empty">
|
||||
<p>
|
||||
No tileset loaded.
|
||||
<br />
|
||||
Select a TilemapComponent to edit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tileset-panel">
|
||||
<div className="tileset-panel-header">
|
||||
<h3>Tileset</h3>
|
||||
</div>
|
||||
<div className="tileset-canvas-container">
|
||||
<TilesetPreview
|
||||
imageUrl={tilesetImageUrl}
|
||||
tileWidth={tileWidth}
|
||||
tileHeight={tileHeight}
|
||||
columns={tilesetColumns}
|
||||
rows={tilesetRows}
|
||||
/>
|
||||
</div>
|
||||
{selectedTiles && (
|
||||
<div className="tilemap-info-bar">
|
||||
<span>
|
||||
Selected: {selectedTiles.width}×{selectedTiles.height}
|
||||
</span>
|
||||
<span>Tile: {selectedTiles.tiles[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user