refactor(ui): UI 系统架构重构 (#309)
* feat(ui): 动态图集系统与渲染调试增强 ## 核心功能 ### 动态图集系统 (Dynamic Atlas) - 新增 DynamicAtlasManager:运行时纹理打包,支持 MaxRects 算法 - 新增 DynamicAtlasService:自动纹理加载与图集管理 - 新增 BinPacker:高效矩形打包算法 - 支持动态/固定两种扩展策略 - 自动 UV 重映射,实现 UI 元素合批渲染 ### Frame Debugger 增强 - 新增合批分析面板,显示批次中断原因 - 新增 UI 元素层级信息(depth, worldOrderInLayer) - 新增实体高亮功能,点击可在场景中定位 - 新增动态图集可视化面板 - 改进渲染原语详情展示 ### 闪光效果 (Shiny Effect) - 新增 UIShinyEffectComponent:UI 闪光参数配置 - 新增 UIShinyEffectSystem:材质覆盖驱动的闪光动画 - 新增 ShinyEffectComponent/System(Sprite 版本) ## 引擎层改进 ### Rust 纹理管理扩展 - create_blank_texture:创建空白 GPU 纹理 - update_texture_region:局部纹理更新 - 支持动态图集的 GPU 端操作 ### 材质系统 - 新增 effects/ 目录:ShinyEffect 等效果实现 - 新增 interfaces/ 目录:IMaterial 等接口定义 - 新增 mixins/ 目录:可组合的材质功能 ### EngineBridge 扩展 - 新增 createBlankTexture/updateTextureRegion 方法 - 改进纹理加载回调机制 ## UI 渲染改进 - UIRenderCollector:支持合批调试信息 - 稳定排序:addIndex 保证渲染顺序一致性 - 九宫格渲染优化 - 材质覆盖支持 ## 其他改进 - 国际化:新增 Frame Debugger 相关翻译 - 编辑器:新增渲染调试入口 - 文档:新增架构设计文档目录 * refactor(ui): 引入新基础组件架构与渲染工具函数 Phase 1 重构 - 组件职责分离与代码复用: 新增基础组件层: - UIGraphicComponent: 所有可视 UI 元素的基类(颜色、透明度、raycast) - UIImageComponent: 纹理显示组件(支持简单、切片、平铺、填充模式) - UISelectableComponent: 可交互元素的基类(状态管理、颜色过渡) 新增渲染工具: - UIRenderUtils: 提取共享的坐标计算、边框渲染、阴影渲染等工具函数 - getUIRenderTransform: 统一的变换数据提取 - renderBorder/renderShadow: 复用的边框和阴影渲染逻辑 新增渲染系统: - UIGraphicRenderSystem: 处理新基础组件的统一渲染器 重构现有系统: - UIRectRenderSystem: 使用新工具函数,移除重复代码 - UIButtonRenderSystem: 使用新工具函数,移除重复代码 这些改动为后续统一渲染系统奠定基础。 * refactor(ui): UIProgressBarRenderSystem 使用渲染工具函数 - 使用 getUIRenderTransform 替代手动变换计算 - 使用 renderBorder 工具函数替代重复的边框渲染 - 使用 lerpColor 工具函数替代重复的颜色插值 - 简化方法签名,使用 UIRenderTransform 类型 - 移除约 135 行重复代码 * refactor(ui): Slider 和 ScrollView 渲染系统使用工具函数 - UISliderRenderSystem: 使用 getUIRenderTransform,简化方法签名 - UIScrollViewRenderSystem: 使用 getUIRenderTransform,简化方法签名 - 统一使用 UIRenderTransform 类型减少参数传递 - 消除重复的变换计算代码 * refactor(ui): 使用 UIWidgetMarker 消除硬编码组件依赖 - 新增 UIWidgetMarker 标记组件 - UIRectRenderSystem 改为检查标记而非硬编码4种组件类型 - 各 Widget 渲染系统自动添加标记组件 - 减少模块间耦合,提高可扩展性 * feat(ui): 实现 Canvas 隔离机制 - 新增 UICanvasComponent 定义 Canvas 渲染组 - UITransformComponent 添加 Canvas 相关字段:canvasEntityId, worldSortingLayer, pixelPerfect - UILayoutSystem 传播 Canvas 设置给子元素 - UIRenderUtils 使用 Canvas 继承的排序层 - 支持嵌套 Canvas 和不同渲染模式 * refactor(ui): 统一纹理管理工具函数 Phase 4: 纹理管理统一 新增: - UITextureUtils.ts: 统一的纹理描述符接口和验证函数 - UITextureDescriptor: 支持 GUID/textureId/path 多种纹理源 - isValidTextureGuid: GUID 验证 - getTextureKey: 获取用于合批的纹理键 - normalizeTextureDescriptor: 规范化各种输入格式 - utils/index.ts: 工具函数导出 修改: - UIGraphicRenderSystem: 使用新的纹理工具函数 - index.ts: 导出纹理工具类型和函数 * refactor(ui): 实现统一的脏标记机制 Phase 5: Dirty 标记机制 新增: - UIDirtyFlags.ts: 位标记枚举和追踪工具 - UIDirtyFlags: Visual/Layout/Transform/Material/Text 标记 - IDirtyTrackable: 脏追踪接口 - DirtyTracker: 辅助工具类 - 帧级别脏状态追踪 (markFrameDirty, isFrameDirty) 修改: - UIGraphicComponent: 实现 IDirtyTrackable - 属性 setter 自动设置脏标记 - 保留 setDirty/clearDirty 向后兼容 - UIImageComponent: 所有属性支持脏追踪 - textureGuid/imageType/fillAmount 等变化自动标记 - UIGraphicRenderSystem: 使用 clearDirtyFlags() 导出: - UIDirtyFlags, IDirtyTrackable, DirtyTracker - markFrameDirty, isFrameDirty, clearFrameDirty * refactor(ui): 移除过时的 dirty flag API 移除 UIGraphicComponent 中的兼容性 API: - 移除 _isDirty getter/setter - 移除 setDirty() 方法 - 移除 clearDirty() 方法 现在统一使用新的 dirty flag 系统: - isDirty() / hasDirtyFlag(flags) - markDirty(flags) / clearDirtyFlags() * fix(ui): 修复两个 TODO 功能 1. 滑块手柄命中测试 (UIInputSystem) - UISliderComponent 添加 getHandleBounds() 计算手柄边界 - UISliderComponent 添加 isPointInHandle() 精确命中测试 - UIInputSystem.handleSlider() 使用精确测试更新悬停状态 2. 径向填充渲染 (UIGraphicRenderSystem) - 实现 renderRadialFill() 方法 - 支持 radial90/radial180/radial360 三种模式 - 支持 fillOrigin (top/right/bottom/left) 和 fillClockwise - 使用多段矩形近似饼形填充效果 * feat(ui): 完善 UI 系统架构和九宫格渲染 * fix(ui): 修复文本渲染层级问题并清理调试代码 - 修复纹理就绪后调用 invalidateUIRenderCaches() 导致的无限循环 - 移除 UITextRenderSystem、UIButtonRenderSystem、UIRectRenderSystem 中的首帧调试输出 - 移除 UILayoutSystem 中的布局调试日志 - 清理所有 __UI_RENDER_DEBUG__ 条件日志 * refactor(ui): 优化渲染批处理和输入框组件 渲染系统: - 修复 RenderBatcher 保持渲染顺序 - 优化 Rust SpriteBatch 避免合并非连续精灵 - 增强 EngineRenderSystem 纹理就绪检测 输入框组件: - 增强 UIInputFieldComponent 功能 - 改进 UIInputSystem 输入处理 - 新增 TextMeasureService 文本测量服务 * fix(ui): 修复九宫格首帧渲染和InputField输入问题 - 修复九宫格首帧 size=0x0 问题: - Viewport.tsx: 预览模式读取图片尺寸存储到 importSettings - AssetDatabase: ISpriteSettings 添加 width/height 字段 - AssetMetadataService: getTextureSpriteInfo 使用元数据尺寸作为后备 - UIRectRenderSystem: 当 atlasEntry 不存在时使用 spriteInfo 尺寸 - WebBuildPipeline: 构建时包含 importSettings - AssetManager: 从 catalog 初始化时复制 importSettings - AssetTypes: IAssetCatalogEntry 添加 importSettings 字段 - 修复 InputField 无法输入问题: - UIRuntimeModule: manifest 添加 pluginExport: 'UIPlugin' - 确保预览模式正确加载 UI 插件并绑定 UIInputSystem - 添加调试日志用于排查纹理加载问题 * fix(sprite): 修复类型导出错误 MaterialPropertyOverride 和 MaterialOverrides 应从 @esengine/material-system 导出 * fix(ui-editor): 补充 AnchorPreset 拉伸预设的映射 添加 StretchTop, StretchMiddle, StretchBottom, StretchLeft, StretchCenter, StretchRight 的位置和锚点值映射
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Entity Reference Field Styles
|
||||
* 实体引用字段样式
|
||||
*
|
||||
* Uses property-field and property-label from PropertyInspector.css for consistency.
|
||||
* 使用 PropertyInspector.css 中的 property-field 和 property-label 以保持一致性。
|
||||
*/
|
||||
|
||||
/* Input container - matches property-input styling */
|
||||
.entity-ref-field__input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 2px;
|
||||
gap: 4px;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.entity-ref-field__input:hover:not(.readonly) {
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.entity-ref-field__input.drag-over {
|
||||
border-color: var(--accent-color, #4a9eff);
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.entity-ref-field__input.readonly {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Entity name - clickable to navigate */
|
||||
.entity-ref-field__name {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entity-ref-field__name:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--accent-color, #4a9eff);
|
||||
}
|
||||
|
||||
/* Clear button */
|
||||
.entity-ref-field__clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entity-ref-field__clear:hover {
|
||||
background: rgba(255, 100, 100, 0.2);
|
||||
color: #ff6464;
|
||||
}
|
||||
|
||||
/* Placeholder text */
|
||||
.entity-ref-field__placeholder {
|
||||
font-size: 11px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Entity Reference Field
|
||||
* 实体引用字段
|
||||
*
|
||||
* Allows drag-and-drop of entities from SceneHierarchy.
|
||||
* 支持从场景层级面板拖拽实体。
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { useHierarchyStore } from '../../../stores';
|
||||
import './EntityRefField.css';
|
||||
|
||||
export interface EntityRefFieldProps {
|
||||
/** Field label | 字段标签 */
|
||||
label: string;
|
||||
/** Current entity ID (0 = none) | 当前实体 ID (0 = 无) */
|
||||
value: number;
|
||||
/** Value change callback | 值变更回调 */
|
||||
onChange: (value: number) => void;
|
||||
/** Placeholder text | 占位文本 */
|
||||
placeholder?: string;
|
||||
/** Read-only mode | 只读模式 */
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const EntityRefField: React.FC<EntityRefFieldProps> = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '拖拽实体到此处 / Drop entity here',
|
||||
readonly = false
|
||||
}) => {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Get entity name for display
|
||||
// 获取实体名称用于显示
|
||||
const getEntityName = useCallback((): string | null => {
|
||||
if (!value || value === 0) return null;
|
||||
const scene = Core.scene;
|
||||
if (!scene) return null;
|
||||
const entity = scene.entities.findEntityById(value);
|
||||
return entity?.name || `Entity #${value}`;
|
||||
}, [value]);
|
||||
|
||||
const entityName = getEntityName();
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
if (readonly) return;
|
||||
|
||||
// Check if dragging an entity
|
||||
// 检查是否拖拽实体
|
||||
if (e.dataTransfer.types.includes('entity-id')) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'link';
|
||||
setIsDragOver(true);
|
||||
}
|
||||
}, [readonly]);
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
if (readonly) return;
|
||||
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const entityIdStr = e.dataTransfer.getData('entity-id');
|
||||
if (entityIdStr) {
|
||||
const entityId = parseInt(entityIdStr, 10);
|
||||
if (!isNaN(entityId) && entityId > 0) {
|
||||
onChange(entityId);
|
||||
}
|
||||
}
|
||||
}, [readonly, onChange]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
if (readonly) return;
|
||||
onChange(0);
|
||||
}, [readonly, onChange]);
|
||||
|
||||
const handleNavigateToEntity = useCallback(() => {
|
||||
if (!value || value === 0) return;
|
||||
|
||||
// Select the referenced entity in SceneHierarchy
|
||||
// 在场景层级面板中选择引用的实体
|
||||
const { setSelectedIds } = useHierarchyStore.getState();
|
||||
setSelectedIds(new Set([value]));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="property-field entity-ref-field">
|
||||
<label className="property-label">{label}</label>
|
||||
<div
|
||||
className={`entity-ref-field__input ${isDragOver ? 'drag-over' : ''} ${readonly ? 'readonly' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{entityName ? (
|
||||
<>
|
||||
<span
|
||||
className="entity-ref-field__name"
|
||||
onClick={handleNavigateToEntity}
|
||||
title="点击选择此实体 / Click to select this entity"
|
||||
>
|
||||
{entityName}
|
||||
</span>
|
||||
{!readonly && (
|
||||
<button
|
||||
className="entity-ref-field__clear"
|
||||
onClick={handleClear}
|
||||
title="清除引用 / Clear reference"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="entity-ref-field__placeholder">{placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Material properties editor component.
|
||||
* 材质属性编辑器组件。
|
||||
*
|
||||
* This component provides a UI for editing shader uniform values
|
||||
* based on shader property metadata.
|
||||
* 此组件提供基于着色器属性元数据编辑着色器 uniform 值的 UI。
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ChevronDown, ChevronRight, Palette } from 'lucide-react';
|
||||
import type {
|
||||
IMaterialOverridable,
|
||||
ShaderPropertyMeta,
|
||||
MaterialPropertyOverride
|
||||
} from '@esengine/material-system';
|
||||
import {
|
||||
BuiltInShaders,
|
||||
getShaderPropertiesById
|
||||
} from '@esengine/material-system';
|
||||
|
||||
// Shader name mapping
|
||||
const SHADER_NAMES: Record<number, string> = {
|
||||
0: 'DefaultSprite',
|
||||
1: 'Grayscale',
|
||||
2: 'Tint',
|
||||
3: 'Flash',
|
||||
4: 'Outline',
|
||||
5: 'Shiny'
|
||||
};
|
||||
|
||||
interface MaterialPropertiesEditorProps {
|
||||
/** Target component implementing IMaterialOverridable */
|
||||
target: IMaterialOverridable;
|
||||
/** Callback when property changes */
|
||||
onChange?: (name: string, value: MaterialPropertyOverride) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material properties editor.
|
||||
* 材质属性编辑器。
|
||||
*/
|
||||
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
|
||||
target,
|
||||
onChange
|
||||
}) => {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['Effect', 'Default']));
|
||||
|
||||
const materialId = target.getMaterialId();
|
||||
const shaderName = SHADER_NAMES[materialId] || `Custom(${materialId})`;
|
||||
const properties = getShaderPropertiesById(materialId);
|
||||
|
||||
// Group properties
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(properties)) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [properties]);
|
||||
|
||||
const toggleGroup = (group: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(group)) {
|
||||
next.delete(group);
|
||||
} else {
|
||||
next.add(group);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (name: string, meta: ShaderPropertyMeta, newValue: number | number[]) => {
|
||||
const override: MaterialPropertyOverride = {
|
||||
type: meta.type === 'texture' ? 'int' : meta.type as MaterialPropertyOverride['type'],
|
||||
value: newValue
|
||||
};
|
||||
|
||||
// Apply to target
|
||||
switch (meta.type) {
|
||||
case 'float':
|
||||
target.setOverrideFloat(name, newValue as number);
|
||||
break;
|
||||
case 'int':
|
||||
target.setOverrideInt(name, newValue as number);
|
||||
break;
|
||||
case 'vec2':
|
||||
const v2 = newValue as number[];
|
||||
target.setOverrideVec2(name, v2[0] ?? 0, v2[1] ?? 0);
|
||||
break;
|
||||
case 'vec3':
|
||||
const v3 = newValue as number[];
|
||||
target.setOverrideVec3(name, v3[0] ?? 0, v3[1] ?? 0, v3[2] ?? 0);
|
||||
break;
|
||||
case 'vec4':
|
||||
const v4 = newValue as number[];
|
||||
target.setOverrideVec4(name, v4[0] ?? 0, v4[1] ?? 0, v4[2] ?? 0, v4[3] ?? 0);
|
||||
break;
|
||||
case 'color':
|
||||
const c = newValue as number[];
|
||||
target.setOverrideColor(name, c[0] ?? 1, c[1] ?? 1, c[2] ?? 1, c[3] ?? 1);
|
||||
break;
|
||||
}
|
||||
|
||||
onChange?.(name, override);
|
||||
};
|
||||
|
||||
const getCurrentValue = (name: string, meta: ShaderPropertyMeta): number | number[] => {
|
||||
const override = target.getOverride(name);
|
||||
if (override) {
|
||||
return override.value as number | number[];
|
||||
}
|
||||
return meta.default as number | number[] ?? (meta.type === 'color' ? [1, 1, 1, 1] : 0);
|
||||
};
|
||||
|
||||
// Parse i18n label
|
||||
const parseLabel = (label: string): string => {
|
||||
// Format: "中文 | English" - for now just return as-is
|
||||
return label;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="material-properties-editor" style={{ fontSize: '12px' }}>
|
||||
{/* Shader selector */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#3a3a3a',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '8px'
|
||||
}}>
|
||||
<Palette size={14} style={{ marginRight: '8px', color: '#888' }} />
|
||||
<span style={{ color: '#aaa', marginRight: '8px' }}>Shader:</span>
|
||||
<select
|
||||
value={materialId}
|
||||
onChange={(e) => target.setMaterialId(Number(e.target.value))}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #4a4a4a',
|
||||
borderRadius: '3px',
|
||||
padding: '3px 6px',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value={0}>DefaultSprite</option>
|
||||
<option value={1}>Grayscale</option>
|
||||
<option value={2}>Tint</option>
|
||||
<option value={3}>Flash</option>
|
||||
<option value={4}>Outline</option>
|
||||
<option value={5}>Shiny</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Property groups */}
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<div key={group} style={{ marginBottom: '4px' }}>
|
||||
{/* Group header */}
|
||||
<div
|
||||
onClick={() => toggleGroup(group)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#333',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
{expandedGroups.has(group) ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span style={{ marginLeft: '4px', color: '#aaa', fontWeight: 500 }}>{group}</span>
|
||||
</div>
|
||||
|
||||
{/* Properties */}
|
||||
{expandedGroups.has(group) && (
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
{props.map(([name, meta]) => (
|
||||
<PropertyEditor
|
||||
key={name}
|
||||
name={name}
|
||||
meta={meta}
|
||||
value={getCurrentValue(name, meta)}
|
||||
onChange={(v) => handleChange(name, meta, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!properties && (
|
||||
<div style={{ color: '#666', padding: '8px', fontStyle: 'italic' }}>
|
||||
No editable properties for {shaderName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface PropertyEditorProps {
|
||||
name: string;
|
||||
meta: ShaderPropertyMeta;
|
||||
value: number | number[];
|
||||
onChange: (value: number | number[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual property editor.
|
||||
* 单个属性编辑器。
|
||||
*/
|
||||
const PropertyEditor: React.FC<PropertyEditorProps> = ({ name, meta, value, onChange }) => {
|
||||
const displayName = name.replace(/^u_/, '');
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
backgroundColor: '#2a2a2a',
|
||||
color: '#e0e0e0',
|
||||
border: '1px solid #4a4a4a',
|
||||
borderRadius: '3px',
|
||||
padding: '2px 6px',
|
||||
fontSize: '11px',
|
||||
width: '60px'
|
||||
};
|
||||
|
||||
const renderInput = () => {
|
||||
switch (meta.type) {
|
||||
case 'float':
|
||||
case 'int':
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
value={typeof value === 'number' ? value : 0}
|
||||
min={meta.min}
|
||||
max={meta.max}
|
||||
step={meta.step ?? (meta.type === 'int' ? 1 : 0.01)}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
style={inputStyle}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'vec2':
|
||||
const v2 = Array.isArray(value) ? value : [0, 0];
|
||||
const v2x = v2[0] ?? 0;
|
||||
const v2y = v2[1] ?? 0;
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<input
|
||||
type="number"
|
||||
value={v2x}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(e) => onChange([parseFloat(e.target.value) || 0, v2y])}
|
||||
style={{ ...inputStyle, width: '50px' }}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={v2y}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(e) => onChange([v2x, parseFloat(e.target.value) || 0])}
|
||||
style={{ ...inputStyle, width: '50px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'vec3':
|
||||
const v3 = Array.isArray(value) ? value : [0, 0, 0];
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{[0, 1, 2].map(i => (
|
||||
<input
|
||||
key={i}
|
||||
type="number"
|
||||
value={v3[i]}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(e) => {
|
||||
const newVal = [...v3];
|
||||
newVal[i] = parseFloat(e.target.value) || 0;
|
||||
onChange(newVal);
|
||||
}}
|
||||
style={{ ...inputStyle, width: '40px' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'vec4':
|
||||
const v4 = Array.isArray(value) ? value : [0, 0, 0, 0];
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<input
|
||||
key={i}
|
||||
type="number"
|
||||
value={v4[i]}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(e) => {
|
||||
const newVal = [...v4];
|
||||
newVal[i] = parseFloat(e.target.value) || 0;
|
||||
onChange(newVal);
|
||||
}}
|
||||
style={{ ...inputStyle, width: '35px' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'color':
|
||||
const c = Array.isArray(value) ? value : [1, 1, 1, 1];
|
||||
const cr = c[0] ?? 1;
|
||||
const cg = c[1] ?? 1;
|
||||
const cb = c[2] ?? 1;
|
||||
const ca = c[3] ?? 1;
|
||||
const hexColor = `#${Math.round(cr * 255).toString(16).padStart(2, '0')}${Math.round(cg * 255).toString(16).padStart(2, '0')}${Math.round(cb * 255).toString(16).padStart(2, '0')}`;
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="color"
|
||||
value={hexColor}
|
||||
onChange={(e) => {
|
||||
const hex = e.target.value;
|
||||
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
||||
onChange([r, g, b, ca]);
|
||||
}}
|
||||
style={{ width: '24px', height: '20px', padding: 0, border: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={ca}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => onChange([cr, cg, cb, parseFloat(e.target.value) || 1])}
|
||||
style={{ ...inputStyle, width: '40px' }}
|
||||
title="Alpha"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <span style={{ color: '#666' }}>Unsupported type: {meta.type}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '3px 0',
|
||||
borderBottom: '1px solid #333'
|
||||
}}>
|
||||
<span style={{ color: '#aaa' }} title={meta.tooltip}>
|
||||
{displayName}
|
||||
</span>
|
||||
{renderInput()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialPropertiesEditor;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Material Inspector components.
|
||||
* 材质 Inspector 组件。
|
||||
*/
|
||||
|
||||
export { MaterialPropertiesEditor } from './MaterialPropertiesEditor';
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive, Settings2, Grid3X3 } from 'lucide-react';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { AssetRegistryService } from '@esengine/editor-core';
|
||||
import { AssetRegistryService, MessageHub } from '@esengine/editor-core';
|
||||
import type { ISpriteSettings } from '@esengine/asset-system-editor';
|
||||
import { EngineService } from '../../../services/EngineService';
|
||||
import { AssetFileInfo } from '../types';
|
||||
@@ -315,6 +315,18 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
|
||||
|
||||
setSpriteSettings(newSettings);
|
||||
console.log(`[AssetFileInspector] Updated sprite settings for ${fileInfo.name}:`, newSettings);
|
||||
|
||||
// 通知 EngineService 同步资产数据库(以便渲染系统获取最新的九宫格设置)
|
||||
// Notify EngineService to sync asset database (so render systems get latest sprite settings)
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('assets:changed', {
|
||||
type: 'modify',
|
||||
path: fileInfo.path,
|
||||
relativePath: assetRegistry.absoluteToRelative(fileInfo.path) || fileInfo.path,
|
||||
guid: meta.guid
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update sprite settings:', error);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user