Feature/runtime cdn and plugin loader (#240)
* feat(ui): 完善 UI 布局系统和编辑器可视化工具 * refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统 * fix: 修复 CodeQL 警告并提升测试覆盖率 * refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题 * fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤 * docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明 * fix(ci): 修复 type-check 失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): 修复类型检查失败问题 * fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖 * fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖 * fix(ci): platform-web 添加缺失的 behavior-tree 依赖 * fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Layer Panel Component
|
||||
* 图层面板组件
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Eye, EyeOff, Lock, Unlock, Plus, Trash2, ChevronUp, ChevronDown, Paintbrush } from 'lucide-react';
|
||||
import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore';
|
||||
import type { TilemapComponent } from '@esengine/tilemap';
|
||||
|
||||
interface LayerPanelProps {
|
||||
tilemap: TilemapComponent | null;
|
||||
onAddLayer?: () => void;
|
||||
onRemoveLayer?: (index: number) => void;
|
||||
onMoveLayer?: (fromIndex: number, toIndex: number) => void;
|
||||
}
|
||||
|
||||
export const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
tilemap,
|
||||
onAddLayer,
|
||||
onRemoveLayer,
|
||||
onMoveLayer,
|
||||
}) => {
|
||||
const {
|
||||
currentLayer,
|
||||
layers,
|
||||
setCurrentLayer,
|
||||
toggleLayerVisibility,
|
||||
toggleLayerLocked,
|
||||
setLayerOpacity,
|
||||
renameLayer,
|
||||
} = useTilemapEditorStore();
|
||||
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
|
||||
const handleDoubleClick = useCallback((index: number, name: string) => {
|
||||
setEditingIndex(index);
|
||||
setEditName(name);
|
||||
}, []);
|
||||
|
||||
const handleNameSubmit = useCallback((index: number) => {
|
||||
if (editName.trim()) {
|
||||
renameLayer(index, editName.trim());
|
||||
// Also update the tilemap component
|
||||
if (tilemap && tilemap.layers[index]) {
|
||||
tilemap.layers[index].name = editName.trim();
|
||||
}
|
||||
}
|
||||
setEditingIndex(null);
|
||||
}, [editName, renameLayer, tilemap]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent, index: number) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleNameSubmit(index);
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingIndex(null);
|
||||
}
|
||||
}, [handleNameSubmit]);
|
||||
|
||||
const handleVisibilityToggle = useCallback((index: number) => {
|
||||
toggleLayerVisibility(index);
|
||||
// Also update the tilemap component
|
||||
if (tilemap && tilemap.layers[index]) {
|
||||
tilemap.layers[index].visible = !tilemap.layers[index].visible;
|
||||
tilemap.renderDirty = true;
|
||||
}
|
||||
}, [toggleLayerVisibility, tilemap]);
|
||||
|
||||
const handleOpacityChange = useCallback((index: number, opacity: number) => {
|
||||
setLayerOpacity(index, opacity);
|
||||
// Also update the tilemap component
|
||||
if (tilemap && tilemap.layers[index]) {
|
||||
tilemap.layers[index].opacity = opacity;
|
||||
tilemap.renderDirty = true;
|
||||
}
|
||||
}, [setLayerOpacity, tilemap]);
|
||||
|
||||
if (!tilemap || layers.length === 0) {
|
||||
return (
|
||||
<div className="layer-panel">
|
||||
<div className="layer-panel-header">
|
||||
<span>图层</span>
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={onAddLayer}
|
||||
title="添加图层"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="layer-panel-empty">
|
||||
暂无图层
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="layer-panel">
|
||||
<div className="layer-panel-header">
|
||||
<span>图层 ({layers.length})</span>
|
||||
<div className="layer-panel-actions">
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={onAddLayer}
|
||||
title="添加图层"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="layer-list">
|
||||
{layers.map((layer, index) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className={`layer-item ${index === currentLayer ? 'selected' : ''} ${layer.locked ? 'locked' : ''}`}
|
||||
onClick={() => setCurrentLayer(index)}
|
||||
>
|
||||
<div className="layer-controls">
|
||||
<button
|
||||
className="icon-button small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleVisibilityToggle(index);
|
||||
}}
|
||||
title={layer.visible ? '隐藏图层' : '显示图层'}
|
||||
>
|
||||
{layer.visible ? <Eye size={12} /> : <EyeOff size={12} />}
|
||||
</button>
|
||||
<button
|
||||
className="icon-button small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleLayerLocked(index);
|
||||
}}
|
||||
title={layer.locked ? '解锁图层' : '锁定图层'}
|
||||
>
|
||||
{layer.locked ? <Lock size={12} /> : <Unlock size={12} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="layer-info">
|
||||
{index === currentLayer && (
|
||||
<span className="layer-active-indicator" title="当前绘制图层">
|
||||
<Paintbrush size={14} />
|
||||
</span>
|
||||
)}
|
||||
{editingIndex === index ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onBlur={() => handleNameSubmit(index)}
|
||||
onKeyDown={(e) => handleKeyDown(e, index)}
|
||||
autoFocus
|
||||
className="layer-name-input"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="layer-name"
|
||||
onDoubleClick={() => handleDoubleClick(index, layer.name)}
|
||||
>
|
||||
{layer.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="layer-actions">
|
||||
<button
|
||||
className="icon-button small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveLayer?.(index, index - 1);
|
||||
}}
|
||||
disabled={index === 0}
|
||||
title="上移图层"
|
||||
>
|
||||
<ChevronUp size={12} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveLayer?.(index, index + 1);
|
||||
}}
|
||||
disabled={index === layers.length - 1}
|
||||
title="下移图层"
|
||||
>
|
||||
<ChevronDown size={12} />
|
||||
</button>
|
||||
<button
|
||||
className="icon-button small danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveLayer?.(index);
|
||||
}}
|
||||
disabled={layers.length <= 1}
|
||||
title="删除图层"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Opacity slider for selected layer */}
|
||||
{layers[currentLayer] && (
|
||||
<div className="layer-opacity-control">
|
||||
<label>Opacity</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={layers[currentLayer].opacity}
|
||||
onChange={(e) => handleOpacityChange(currentLayer, parseFloat(e.target.value))}
|
||||
title={`Opacity for ${layers[currentLayer].name}`}
|
||||
/>
|
||||
<span>{Math.round(layers[currentLayer].opacity * 100)}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayerPanel;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* 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