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:
@@ -30,6 +30,7 @@
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/material-system": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -15,21 +15,23 @@ function uiTransformGizmoProvider(
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use world coordinates (computed by UILayoutSystem) if available
|
||||
// Otherwise fallback to local coordinates
|
||||
// 使用世界坐标(由 UILayoutSystem 计算),如果可用
|
||||
// 否则回退到本地坐标
|
||||
const x = transform.worldX ?? transform.x;
|
||||
const y = transform.worldY ?? transform.y;
|
||||
// Use world scale for proper hierarchical transform inheritance
|
||||
// 使用世界缩放以正确继承层级变换
|
||||
// 使用 UILayoutSystem 计算的世界坐标
|
||||
// Use world coordinates computed by UILayoutSystem
|
||||
// 如果 layoutComputed = false,说明 UILayoutSystem 还没运行,回退到本地坐标
|
||||
// If layoutComputed = false, UILayoutSystem hasn't run yet, fallback to local coordinates
|
||||
const x = transform.layoutComputed ? transform.worldX : transform.x;
|
||||
const y = transform.layoutComputed ? transform.worldY : transform.y;
|
||||
const scaleX = transform.worldScaleX ?? transform.scaleX;
|
||||
const scaleY = transform.worldScaleY ?? transform.scaleY;
|
||||
const width = (transform.computedWidth ?? transform.width) * scaleX;
|
||||
const height = (transform.computedHeight ?? transform.height) * scaleY;
|
||||
// Use world rotation for proper hierarchical transform inheritance
|
||||
// 使用世界旋转以正确继承层级变换
|
||||
const rotation = transform.worldRotation ?? transform.rotation;
|
||||
const width = (transform.layoutComputed && transform.computedWidth > 0
|
||||
? transform.computedWidth
|
||||
: transform.width) * scaleX;
|
||||
const height = (transform.layoutComputed && transform.computedHeight > 0
|
||||
? transform.computedHeight
|
||||
: transform.height) * scaleY;
|
||||
// 角度转弧度 | Convert degrees to radians
|
||||
const rotationDegrees = transform.worldRotation ?? transform.rotation;
|
||||
const rotation = (rotationDegrees * Math.PI) / 180;
|
||||
// 使用 transform 的 pivot 作为旋转/缩放中心
|
||||
const pivotX = transform.pivotX;
|
||||
const pivotY = transform.pivotY;
|
||||
|
||||
@@ -33,11 +33,11 @@ import {
|
||||
UISliderComponent,
|
||||
UIScrollViewComponent
|
||||
} from '@esengine/ui';
|
||||
import { UITransformInspector } from './inspectors';
|
||||
import { UITransformInspector, UIRenderInspector } from './inspectors';
|
||||
import { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos';
|
||||
|
||||
// Re-exports
|
||||
export { UITransformInspector } from './inspectors';
|
||||
export { UITransformInspector, UIRenderInspector } from './inspectors';
|
||||
export { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos';
|
||||
|
||||
/**
|
||||
@@ -76,6 +76,7 @@ export class UIEditorModule implements IEditorModuleLoader {
|
||||
const componentInspectorRegistry = services.tryResolve(ComponentInspectorRegistry);
|
||||
if (componentInspectorRegistry) {
|
||||
componentInspectorRegistry.register(new UITransformInspector());
|
||||
componentInspectorRegistry.register(new UIRenderInspector());
|
||||
}
|
||||
|
||||
// 注册 Gizmo | Register gizmo
|
||||
|
||||
852
packages/ui-editor/src/inspectors/UIRenderInspector.tsx
Normal file
852
packages/ui-editor/src/inspectors/UIRenderInspector.tsx
Normal file
@@ -0,0 +1,852 @@
|
||||
/**
|
||||
* UI Render Component Inspector.
|
||||
* UI 渲染组件检查器。
|
||||
*
|
||||
* Provides unified material editing for UIRenderComponent.
|
||||
* 为 UIRenderComponent 提供统一的材质编辑。
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { Component, Core } from '@esengine/ecs-framework';
|
||||
import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { UIRenderComponent } from '@esengine/ui';
|
||||
import type { ShaderPropertyMeta } from '@esengine/material-system';
|
||||
import { getShaderPropertiesById } from '@esengine/material-system';
|
||||
import { ChevronDown, ChevronRight, Palette, X, Plus, FileBox } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Material source type.
|
||||
* 材质来源类型。
|
||||
*/
|
||||
type MaterialSource = 'none' | 'builtin' | 'asset';
|
||||
|
||||
/**
|
||||
* Built-in effect options.
|
||||
* 内置效果选项。
|
||||
*/
|
||||
const BUILTIN_EFFECTS = [
|
||||
{ id: 1, name: 'Grayscale', description: 'Convert to grayscale' },
|
||||
{ id: 2, name: 'Tint', description: 'Apply color tint' },
|
||||
{ id: 3, name: 'Flash', description: 'Flash effect for hit feedback' },
|
||||
{ id: 4, name: 'Outline', description: 'Add outline border' },
|
||||
{ id: 5, name: 'Shiny', description: 'Animated shine sweep' },
|
||||
];
|
||||
|
||||
// Uniform type display names
|
||||
const UNIFORM_TYPE_LABELS: Record<string, string> = {
|
||||
'float': 'Float',
|
||||
'int': 'Int',
|
||||
'vec2': 'Vec2',
|
||||
'vec3': 'Vec3',
|
||||
'vec4': 'Vec4',
|
||||
'color': 'Color',
|
||||
};
|
||||
|
||||
/**
|
||||
* Single number input with local state to prevent focus loss.
|
||||
* 带本地状态的单数字输入框,防止失焦。
|
||||
*/
|
||||
function NumberInput({ value, onChange, min, max, step, style }: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const [localValue, setLocalValue] = useState(String(value));
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Sync from prop when not focused
|
||||
// 未聚焦时从 prop 同步
|
||||
React.useEffect(() => {
|
||||
if (!isFocused) {
|
||||
setLocalValue(String(value));
|
||||
}
|
||||
}, [value, isFocused]);
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsFocused(false);
|
||||
const parsed = parseFloat(localValue);
|
||||
if (!isNaN(parsed)) {
|
||||
onChange(parsed);
|
||||
} else {
|
||||
setLocalValue(String(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
value={localValue}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert radians to degrees.
|
||||
* 弧度转角度。
|
||||
*/
|
||||
function radToDeg(rad: number): number {
|
||||
return rad * 180 / Math.PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert degrees to radians.
|
||||
* 角度转弧度。
|
||||
*/
|
||||
function degToRad(deg: number): number {
|
||||
return deg * Math.PI / 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Property value editor component.
|
||||
* 属性值编辑器组件。
|
||||
*/
|
||||
function PropertyValueEditor({ meta, value, onChange }: {
|
||||
meta: ShaderPropertyMeta;
|
||||
value: number | number[];
|
||||
onChange: (value: number | number[]) => void;
|
||||
}) {
|
||||
const inputStyle: React.CSSProperties = {
|
||||
backgroundColor: 'var(--color-bg-inset)',
|
||||
color: 'var(--color-text-primary)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '2px 6px',
|
||||
fontSize: '11px',
|
||||
width: '60px'
|
||||
};
|
||||
|
||||
switch (meta.type) {
|
||||
case 'float':
|
||||
case 'int': {
|
||||
// Handle 'angle' hint: display degrees, store radians
|
||||
// 处理 'angle' 提示:显示角度,存储弧度
|
||||
const isAngle = meta.hint === 'angle';
|
||||
const numValue = typeof value === 'number' ? value : 0;
|
||||
const displayValue = isAngle ? radToDeg(numValue) : numValue;
|
||||
const displayMin = isAngle && meta.min !== undefined ? radToDeg(meta.min) : meta.min;
|
||||
const displayMax = isAngle && meta.max !== undefined ? radToDeg(meta.max) : meta.max;
|
||||
const displayStep = isAngle ? 1 : (meta.step ?? (meta.type === 'int' ? 1 : 0.01));
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<NumberInput
|
||||
value={displayValue}
|
||||
min={displayMin}
|
||||
max={displayMax}
|
||||
step={displayStep}
|
||||
onChange={(v) => {
|
||||
const storeValue = isAngle ? degToRad(v) : v;
|
||||
(onChange as (v: number) => void)(storeValue);
|
||||
}}
|
||||
style={inputStyle}
|
||||
/>
|
||||
{isAngle && (
|
||||
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '10px' }}>°</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'vec2': {
|
||||
const v2 = Array.isArray(value) ? value : [0, 0];
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<NumberInput
|
||||
value={v2[0] ?? 0}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(v) => onChange([v, v2[1] ?? 0])}
|
||||
style={{ ...inputStyle, width: '50px' }}
|
||||
/>
|
||||
<NumberInput
|
||||
value={v2[1] ?? 0}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(v) => onChange([v2[0] ?? 0, v])}
|
||||
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 => (
|
||||
<NumberInput
|
||||
key={i}
|
||||
value={v3[i] ?? 0}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(v) => {
|
||||
const newVal = [...v3];
|
||||
newVal[i] = v;
|
||||
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 => (
|
||||
<NumberInput
|
||||
key={i}
|
||||
value={v4[i] ?? 0}
|
||||
step={meta.step ?? 0.01}
|
||||
onChange={(v) => {
|
||||
const newVal = [...v4];
|
||||
newVal[i] = v;
|
||||
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' }}
|
||||
/>
|
||||
<NumberInput
|
||||
value={ca}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(v) => onChange([cr, cg, cb, v])}
|
||||
style={{ ...inputStyle, width: '40px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return <span style={{ color: 'var(--color-text-tertiary)' }}>Unsupported</span>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine material source from component state.
|
||||
* 从组件状态确定材质来源。
|
||||
*/
|
||||
function getMaterialSource(render: UIRenderComponent): MaterialSource {
|
||||
if (render.materialGuid && render.materialGuid.length > 0) {
|
||||
return 'asset';
|
||||
}
|
||||
if (render.getMaterialId() !== 0) {
|
||||
return 'builtin';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Render Inspector content component.
|
||||
* UI 渲染检查器内容组件。
|
||||
*/
|
||||
function UIRenderInspectorContent({ context }: { context: ComponentInspectorContext }) {
|
||||
const render = context.component as UIRenderComponent;
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['Effect', 'Default']));
|
||||
const [showAddMenu, setShowAddMenu] = useState(false);
|
||||
const [, forceUpdate] = useState({});
|
||||
|
||||
// Determine current state
|
||||
const materialSource = getMaterialSource(render);
|
||||
const materialId = render.getMaterialId();
|
||||
const properties = getShaderPropertiesById(materialId);
|
||||
|
||||
// Get effect name for display
|
||||
const effectName = BUILTIN_EFFECTS.find(e => e.id === materialId)?.name || '';
|
||||
|
||||
// Group properties
|
||||
const groupedProps = useMemo(() => {
|
||||
if (!properties) return {};
|
||||
|
||||
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
|
||||
for (const [name, meta] of Object.entries(properties) as [string, ShaderPropertyMeta][]) {
|
||||
if (meta.hidden) continue;
|
||||
const group = meta.group || 'Default';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push([name, meta]);
|
||||
}
|
||||
return groups;
|
||||
}, [properties]);
|
||||
|
||||
// Get available properties for override
|
||||
const availableProperties = useMemo((): Array<{ name: string; meta: ShaderPropertyMeta }> => {
|
||||
if (!properties) return [];
|
||||
const currentOverrides = render.materialOverrides || {};
|
||||
return (Object.entries(properties) as [string, ShaderPropertyMeta][])
|
||||
.filter(([name, meta]) => !meta.hidden && !currentOverrides[name])
|
||||
.map(([name, meta]) => ({ name, meta }));
|
||||
}, [properties, render.materialOverrides]);
|
||||
|
||||
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 notifyChange = useCallback(() => {
|
||||
forceUpdate({});
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('scene:modified', {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle source change
|
||||
const handleSourceChange = useCallback((newSource: MaterialSource) => {
|
||||
if (newSource === 'none') {
|
||||
render.materialGuid = '';
|
||||
render.setMaterialId(0);
|
||||
render.clearOverrides();
|
||||
} else if (newSource === 'builtin') {
|
||||
render.materialGuid = '';
|
||||
// Set to first effect if currently none
|
||||
if (render.getMaterialId() === 0) {
|
||||
render.setMaterialId(1); // Grayscale
|
||||
}
|
||||
render.clearOverrides();
|
||||
} else if (newSource === 'asset') {
|
||||
render.setMaterialId(0);
|
||||
render.clearOverrides();
|
||||
// materialGuid will be set by asset picker
|
||||
}
|
||||
context.onChange?.('materialGuid', render.materialGuid);
|
||||
notifyChange();
|
||||
}, [render, context, notifyChange]);
|
||||
|
||||
// Handle effect change
|
||||
const handleEffectChange = useCallback((effectId: number) => {
|
||||
render.setMaterialId(effectId);
|
||||
render.clearOverrides();
|
||||
context.onChange?.('_materialId', effectId);
|
||||
notifyChange();
|
||||
}, [render, context, notifyChange]);
|
||||
|
||||
// Handle asset change
|
||||
const handleAssetChange = useCallback((assetGuid: string) => {
|
||||
render.materialGuid = assetGuid;
|
||||
context.onChange?.('materialGuid', assetGuid);
|
||||
notifyChange();
|
||||
}, [render, context, notifyChange]);
|
||||
|
||||
// Handle property change
|
||||
const handlePropertyChange = useCallback((name: string, meta: ShaderPropertyMeta, newValue: number | number[]) => {
|
||||
switch (meta.type) {
|
||||
case 'float':
|
||||
render.setOverrideFloat(name, newValue as number);
|
||||
break;
|
||||
case 'int':
|
||||
render.setOverrideInt(name, newValue as number);
|
||||
break;
|
||||
case 'vec2': {
|
||||
const v2 = newValue as number[];
|
||||
render.setOverrideVec2(name, v2[0] ?? 0, v2[1] ?? 0);
|
||||
break;
|
||||
}
|
||||
case 'vec3': {
|
||||
const v3 = newValue as number[];
|
||||
render.setOverrideVec3(name, v3[0] ?? 0, v3[1] ?? 0, v3[2] ?? 0);
|
||||
break;
|
||||
}
|
||||
case 'vec4': {
|
||||
const v4 = newValue as number[];
|
||||
render.setOverrideVec4(name, v4[0] ?? 0, v4[1] ?? 0, v4[2] ?? 0, v4[3] ?? 0);
|
||||
break;
|
||||
}
|
||||
case 'color': {
|
||||
const c = newValue as number[];
|
||||
render.setOverrideColor(name, c[0] ?? 1, c[1] ?? 1, c[2] ?? 1, c[3] ?? 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
context.onChange?.('materialOverrides', render.materialOverrides);
|
||||
notifyChange();
|
||||
}, [render, context, notifyChange]);
|
||||
|
||||
const handleRemoveOverride = useCallback((name: string) => {
|
||||
render.removeOverride(name);
|
||||
context.onChange?.('materialOverrides', render.materialOverrides);
|
||||
notifyChange();
|
||||
}, [render, context, notifyChange]);
|
||||
|
||||
const handleAddOverride = useCallback((name: string, meta: ShaderPropertyMeta) => {
|
||||
const defaultValue = meta.default ?? (meta.type === 'color' ? [1, 1, 1, 1] : 0);
|
||||
handlePropertyChange(name, meta, defaultValue as number | number[]);
|
||||
setShowAddMenu(false);
|
||||
}, [handlePropertyChange]);
|
||||
|
||||
const getCurrentValue = (name: string, meta: ShaderPropertyMeta): number | number[] => {
|
||||
const override = render.getOverride(name);
|
||||
if (override) {
|
||||
return override.value as number | number[];
|
||||
}
|
||||
return meta.default as number | number[] ?? (meta.type === 'color' ? [1, 1, 1, 1] : 0);
|
||||
};
|
||||
|
||||
const currentOverrides = render.materialOverrides || {};
|
||||
const overrideKeys = Object.keys(currentOverrides);
|
||||
|
||||
// Styles
|
||||
const selectStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--color-bg-inset)',
|
||||
color: 'var(--color-text-primary)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px'
|
||||
};
|
||||
|
||||
const rowStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
marginBottom: '4px'
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
color: 'var(--color-text-secondary)',
|
||||
marginRight: '8px',
|
||||
minWidth: '60px',
|
||||
fontSize: '12px'
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ fontSize: '12px', marginTop: '8px' }}>
|
||||
{/* Section header */}
|
||||
<div style={{
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'var(--color-bg-elevated)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
marginBottom: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Palette size={14} style={{ color: materialSource !== 'none' ? 'var(--color-primary)' : 'var(--color-text-tertiary)' }} />
|
||||
<span style={{ fontWeight: 500, color: 'var(--color-text-primary)' }}>Material</span>
|
||||
</div>
|
||||
{materialSource === 'builtin' && effectName && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: 'var(--color-primary-subtle)',
|
||||
color: 'var(--color-primary)',
|
||||
borderRadius: 'var(--radius-sm)'
|
||||
}}>
|
||||
{effectName}
|
||||
</span>
|
||||
)}
|
||||
{materialSource === 'asset' && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: 'var(--color-success-subtle)',
|
||||
color: 'var(--color-success)',
|
||||
borderRadius: 'var(--radius-sm)'
|
||||
}}>
|
||||
Asset
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Source selector */}
|
||||
<div style={rowStyle}>
|
||||
<span style={labelStyle}>Source</span>
|
||||
<select
|
||||
value={materialSource}
|
||||
onChange={(e) => handleSourceChange(e.target.value as MaterialSource)}
|
||||
style={selectStyle}
|
||||
>
|
||||
<option value="none">None (Default)</option>
|
||||
<option value="builtin">Built-in Effect</option>
|
||||
<option value="asset">Material Asset</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* None selected hint */}
|
||||
{materialSource === 'none' && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
margin: '4px 8px 8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
fontSize: '11px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
No material effect applied.<br />
|
||||
Select a source above to add visual effects.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Built-in effect selector */}
|
||||
{materialSource === 'builtin' && (
|
||||
<>
|
||||
<div style={rowStyle}>
|
||||
<span style={labelStyle}>Effect</span>
|
||||
<select
|
||||
value={materialId}
|
||||
onChange={(e) => handleEffectChange(Number(e.target.value))}
|
||||
style={selectStyle}
|
||||
>
|
||||
{BUILTIN_EFFECTS.map(effect => (
|
||||
<option key={effect.id} value={effect.id}>
|
||||
{effect.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Effect description */}
|
||||
{effectName && (
|
||||
<div style={{
|
||||
padding: '4px 8px',
|
||||
marginBottom: '8px',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
fontSize: '10px',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{BUILTIN_EFFECTS.find(e => e.id === materialId)?.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overrides section */}
|
||||
{overrideKeys.length > 0 && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{
|
||||
padding: '4px 8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
marginBottom: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px' }}>
|
||||
Overrides ({overrideKeys.length})
|
||||
</span>
|
||||
</div>
|
||||
{overrideKeys.map(key => {
|
||||
const override = currentOverrides[key];
|
||||
if (!override) return null;
|
||||
const meta = properties?.[key];
|
||||
if (!meta) return null;
|
||||
|
||||
return (
|
||||
<div key={key} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '4px 8px',
|
||||
borderBottom: '1px solid var(--color-border-muted)'
|
||||
}}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', minWidth: '80px' }} title={meta.tooltip}>
|
||||
{key.replace(/^u_/, '')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<PropertyValueEditor
|
||||
meta={meta}
|
||||
value={override.value as number | number[]}
|
||||
onChange={(v) => handlePropertyChange(key, meta, v)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveOverride(key)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
title="Remove override"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Property groups */}
|
||||
{Object.entries(groupedProps).map(([group, props]) => (
|
||||
<div key={group} style={{ marginBottom: '4px' }}>
|
||||
<div
|
||||
onClick={() => toggleGroup(group)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
{expandedGroups.has(group) ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span style={{ marginLeft: '4px', color: 'var(--color-text-secondary)', fontWeight: 500 }}>{group}</span>
|
||||
</div>
|
||||
|
||||
{expandedGroups.has(group) && (
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
{props.map(([name, meta]) => {
|
||||
const hasOverride = !!currentOverrides[name];
|
||||
return (
|
||||
<div key={name} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '3px 0',
|
||||
borderBottom: '1px solid var(--color-border-muted)',
|
||||
opacity: hasOverride ? 1 : 0.7
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: hasOverride ? 'var(--color-text-primary)' : 'var(--color-text-tertiary)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title={meta.tooltip || `Click to add override for ${name}`}
|
||||
onClick={() => !hasOverride && handleAddOverride(name, meta)}
|
||||
>
|
||||
{name.replace(/^u_/, '')}
|
||||
{!hasOverride && <Plus size={10} style={{ marginLeft: '4px', opacity: 0.5 }} />}
|
||||
</span>
|
||||
{hasOverride ? (
|
||||
<PropertyValueEditor
|
||||
meta={meta}
|
||||
value={getCurrentValue(name, meta)}
|
||||
onChange={(v) => handlePropertyChange(name, meta, v)}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '10px' }}>
|
||||
{typeof meta.default === 'number' ? meta.default.toFixed(2) : 'default'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add override button */}
|
||||
{availableProperties.length > 0 && (
|
||||
<div style={{ position: 'relative', padding: '4px 8px' }}>
|
||||
<button
|
||||
onClick={() => setShowAddMenu(!showAddMenu)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: 'var(--color-text-secondary)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
<span>Add Override</span>
|
||||
</button>
|
||||
{showAddMenu && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: '8px',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'var(--color-bg-elevated)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
minWidth: '180px',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{availableProperties.map(({ name, meta }) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => handleAddOverride(name, meta)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid var(--color-border-muted)',
|
||||
color: 'var(--color-text-primary)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
<span>{name.replace(/^u_/, '')}</span>
|
||||
<span style={{ color: 'var(--color-text-tertiary)', fontSize: '10px' }}>
|
||||
{UNIFORM_TYPE_LABELS[meta.type] || meta.type}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state for effect without properties */}
|
||||
{!properties && (
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
margin: '0 8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
fontSize: '11px',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
No editable properties for this effect
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Material Asset selector */}
|
||||
{materialSource === 'asset' && (
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px',
|
||||
backgroundColor: 'var(--color-bg-subtle)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px dashed var(--color-border-default)'
|
||||
}}>
|
||||
<FileBox size={16} style={{ color: 'var(--color-text-tertiary)' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={render.materialGuid}
|
||||
onChange={(e) => handleAssetChange(e.target.value)}
|
||||
placeholder="Drag .mat file here or enter GUID"
|
||||
style={{
|
||||
width: '100%',
|
||||
backgroundColor: 'var(--color-bg-inset)',
|
||||
color: 'var(--color-text-primary)',
|
||||
border: '1px solid var(--color-border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '4px 8px',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{render.materialGuid && (
|
||||
<button
|
||||
onClick={() => handleAssetChange('')}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--color-text-tertiary)'
|
||||
}}
|
||||
title="Clear"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '8px',
|
||||
color: 'var(--color-text-tertiary)',
|
||||
fontSize: '10px'
|
||||
}}>
|
||||
Material assets (.mat) define shared shader configurations
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* UI Render component inspector implementation.
|
||||
* UI 渲染组件检查器实现。
|
||||
*
|
||||
* Uses 'append' mode to add unified material UI after default properties.
|
||||
* 使用 'append' 模式在默认属性后添加统一的材质 UI。
|
||||
*/
|
||||
export class UIRenderInspector implements IComponentInspector<UIRenderComponent> {
|
||||
readonly id = 'uirender-inspector';
|
||||
readonly name = 'UIRender Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['UIRender', 'UIRenderComponent'];
|
||||
readonly renderMode = 'append' as const;
|
||||
|
||||
canHandle(component: Component): component is UIRenderComponent {
|
||||
return component instanceof UIRenderComponent ||
|
||||
component.constructor.name === 'UIRenderComponent';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
return React.createElement(UIRenderInspectorContent, {
|
||||
context,
|
||||
key: `uirender-${context.version}`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,15 @@ const AnchorPresetGrid: React.FC<{
|
||||
[AnchorPreset.BottomLeft]: { x: 3, y: 17 },
|
||||
[AnchorPreset.BottomCenter]: { x: 10, y: 17 },
|
||||
[AnchorPreset.BottomRight]: { x: 17, y: 17 },
|
||||
// Stretch presets (horizontal) | 拉伸预设(水平)
|
||||
[AnchorPreset.StretchTop]: { x: 10, y: 3 },
|
||||
[AnchorPreset.StretchMiddle]: { x: 10, y: 10 },
|
||||
[AnchorPreset.StretchBottom]: { x: 10, y: 17 },
|
||||
// Stretch presets (vertical) | 拉伸预设(垂直)
|
||||
[AnchorPreset.StretchLeft]: { x: 3, y: 10 },
|
||||
[AnchorPreset.StretchCenter]: { x: 10, y: 10 },
|
||||
[AnchorPreset.StretchRight]: { x: 17, y: 10 },
|
||||
// Full stretch | 完全拉伸
|
||||
[AnchorPreset.StretchAll]: { x: 10, y: 10 },
|
||||
};
|
||||
return positions[preset];
|
||||
@@ -320,30 +329,44 @@ export class UITransformInspector implements IComponentInspector<UITransformComp
|
||||
return AnchorPreset.StretchAll;
|
||||
}
|
||||
if (anchorMinX === anchorMaxX && anchorMinY === anchorMaxY) {
|
||||
if (anchorMinX === 0 && anchorMinY === 0) return AnchorPreset.TopLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 0) return AnchorPreset.TopCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 0) return AnchorPreset.TopRight;
|
||||
// Y-up 坐标系:anchorMinY=1 是顶部,anchorMinY=0 是底部
|
||||
// Y-up coordinate system: anchorMinY=1 is top, anchorMinY=0 is bottom
|
||||
if (anchorMinX === 0 && anchorMinY === 1) return AnchorPreset.TopLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 1) return AnchorPreset.TopCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 1) return AnchorPreset.TopRight;
|
||||
if (anchorMinX === 0 && anchorMinY === 0.5) return AnchorPreset.MiddleLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 0.5) return AnchorPreset.MiddleCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 0.5) return AnchorPreset.MiddleRight;
|
||||
if (anchorMinX === 0 && anchorMinY === 1) return AnchorPreset.BottomLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 1) return AnchorPreset.BottomCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 1) return AnchorPreset.BottomRight;
|
||||
if (anchorMinX === 0 && anchorMinY === 0) return AnchorPreset.BottomLeft;
|
||||
if (anchorMinX === 0.5 && anchorMinY === 0) return AnchorPreset.BottomCenter;
|
||||
if (anchorMinX === 1 && anchorMinY === 0) return AnchorPreset.BottomRight;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handlePresetSelect = (preset: AnchorPreset) => {
|
||||
// [anchorMinX, anchorMinY, anchorMaxX, anchorMaxY]
|
||||
// Y-up 坐标系:Y=1 是顶部,Y=0 是底部
|
||||
// Y-up coordinate system: Y=1 is top, Y=0 is bottom
|
||||
const presetValues: Record<AnchorPreset, [number, number, number, number]> = {
|
||||
[AnchorPreset.TopLeft]: [0, 0, 0, 0],
|
||||
[AnchorPreset.TopCenter]: [0.5, 0, 0.5, 0],
|
||||
[AnchorPreset.TopRight]: [1, 0, 1, 0],
|
||||
[AnchorPreset.TopLeft]: [0, 1, 0, 1],
|
||||
[AnchorPreset.TopCenter]: [0.5, 1, 0.5, 1],
|
||||
[AnchorPreset.TopRight]: [1, 1, 1, 1],
|
||||
[AnchorPreset.MiddleLeft]: [0, 0.5, 0, 0.5],
|
||||
[AnchorPreset.MiddleCenter]: [0.5, 0.5, 0.5, 0.5],
|
||||
[AnchorPreset.MiddleRight]: [1, 0.5, 1, 0.5],
|
||||
[AnchorPreset.BottomLeft]: [0, 1, 0, 1],
|
||||
[AnchorPreset.BottomCenter]: [0.5, 1, 0.5, 1],
|
||||
[AnchorPreset.BottomRight]: [1, 1, 1, 1],
|
||||
[AnchorPreset.BottomLeft]: [0, 0, 0, 0],
|
||||
[AnchorPreset.BottomCenter]: [0.5, 0, 0.5, 0],
|
||||
[AnchorPreset.BottomRight]: [1, 0, 1, 0],
|
||||
// Horizontal stretch | 水平拉伸
|
||||
[AnchorPreset.StretchTop]: [0, 1, 1, 1],
|
||||
[AnchorPreset.StretchMiddle]: [0, 0.5, 1, 0.5],
|
||||
[AnchorPreset.StretchBottom]: [0, 0, 1, 0],
|
||||
// Vertical stretch | 垂直拉伸
|
||||
[AnchorPreset.StretchLeft]: [0, 0, 0, 1],
|
||||
[AnchorPreset.StretchCenter]: [0.5, 0, 0.5, 1],
|
||||
[AnchorPreset.StretchRight]: [1, 0, 1, 1],
|
||||
// Full stretch | 完全拉伸
|
||||
[AnchorPreset.StretchAll]: [0, 0, 1, 1],
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './UITransformInspector';
|
||||
export * from './UIRenderInspector';
|
||||
|
||||
Reference in New Issue
Block a user