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:
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/ecs-components';
|
||||
import { ChevronDown, Lock, Unlock } from 'lucide-react';
|
||||
import '../../../styles/TransformInspector.css';
|
||||
|
||||
interface AxisInputProps {
|
||||
axis: 'x' | 'y' | 'z';
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
function AxisInput({ axis, value, onChange, suffix }: AxisInputProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(String(value ?? 0));
|
||||
const dragStartRef = useRef({ x: 0, value: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(String(value ?? 0));
|
||||
}, [value]);
|
||||
|
||||
const handleBarMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
dragStartRef.current = { x: e.clientX, value: value ?? 0 };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = e.clientX - dragStartRef.current.x;
|
||||
const sensitivity = e.shiftKey ? 0.01 : e.ctrlKey ? 1 : 0.1;
|
||||
const newValue = dragStartRef.current.value + delta * sensitivity;
|
||||
const rounded = Math.round(newValue * 1000) / 1000;
|
||||
onChange(rounded);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, onChange]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleInputBlur = () => {
|
||||
const parsed = parseFloat(inputValue);
|
||||
if (!isNaN(parsed)) {
|
||||
onChange(parsed);
|
||||
} else {
|
||||
setInputValue(String(value ?? 0));
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
(e.target as HTMLInputElement).blur();
|
||||
} else if (e.key === 'Escape') {
|
||||
setInputValue(String(value ?? 0));
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`tf-axis-input ${isDragging ? 'dragging' : ''}`}>
|
||||
<div
|
||||
className={`tf-axis-bar tf-axis-${axis}`}
|
||||
onMouseDown={handleBarMouseDown}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
{suffix && <span className="tf-axis-suffix">{suffix}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 双向箭头重置图标
|
||||
function ResetIcon() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 6H11M1 6L3 4M1 6L3 8M11 6L9 4M11 6L9 8" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransformRowProps {
|
||||
label: string;
|
||||
value: { x: number; y: number; z: number };
|
||||
showLock?: boolean;
|
||||
isLocked?: boolean;
|
||||
onLockChange?: (locked: boolean) => void;
|
||||
onChange: (value: { x: number; y: number; z: number }) => void;
|
||||
onReset?: () => void;
|
||||
suffix?: string;
|
||||
showDivider?: boolean;
|
||||
}
|
||||
|
||||
function TransformRow({
|
||||
label,
|
||||
value,
|
||||
showLock = false,
|
||||
isLocked = false,
|
||||
onLockChange,
|
||||
onChange,
|
||||
onReset,
|
||||
suffix,
|
||||
showDivider = true
|
||||
}: TransformRowProps) {
|
||||
const handleAxisChange = (axis: 'x' | 'y' | 'z', newValue: number) => {
|
||||
if (isLocked && showLock) {
|
||||
const oldVal = value[axis];
|
||||
if (oldVal !== 0) {
|
||||
const ratio = newValue / oldVal;
|
||||
onChange({
|
||||
x: axis === 'x' ? newValue : value.x * ratio,
|
||||
y: axis === 'y' ? newValue : value.y * ratio,
|
||||
z: axis === 'z' ? newValue : value.z * ratio
|
||||
});
|
||||
} else {
|
||||
onChange({ ...value, [axis]: newValue });
|
||||
}
|
||||
} else {
|
||||
onChange({ ...value, [axis]: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tf-row">
|
||||
<button className="tf-label-btn">
|
||||
{label}
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
<div className="tf-inputs">
|
||||
<AxisInput
|
||||
axis="x"
|
||||
value={value?.x ?? 0}
|
||||
onChange={(v) => handleAxisChange('x', v)}
|
||||
suffix={suffix}
|
||||
/>
|
||||
<AxisInput
|
||||
axis="y"
|
||||
value={value?.y ?? 0}
|
||||
onChange={(v) => handleAxisChange('y', v)}
|
||||
suffix={suffix}
|
||||
/>
|
||||
<AxisInput
|
||||
axis="z"
|
||||
value={value?.z ?? 0}
|
||||
onChange={(v) => handleAxisChange('z', v)}
|
||||
suffix={suffix}
|
||||
/>
|
||||
</div>
|
||||
{showLock && (
|
||||
<button
|
||||
className={`tf-lock-btn ${isLocked ? 'locked' : ''}`}
|
||||
onClick={() => onLockChange?.(!isLocked)}
|
||||
title={isLocked ? 'Unlock' : 'Lock'}
|
||||
>
|
||||
{isLocked ? <Lock size={12} /> : <Unlock size={12} />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="tf-reset-btn"
|
||||
onClick={onReset}
|
||||
title="Reset"
|
||||
>
|
||||
<ResetIcon />
|
||||
</button>
|
||||
</div>
|
||||
{showDivider && <div className="tf-divider" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface MobilityRowProps {
|
||||
value: 'static' | 'stationary' | 'movable';
|
||||
onChange: (value: 'static' | 'stationary' | 'movable') => void;
|
||||
}
|
||||
|
||||
function MobilityRow({ value, onChange }: MobilityRowProps) {
|
||||
return (
|
||||
<div className="tf-mobility-row">
|
||||
<span className="tf-mobility-label">Mobility</span>
|
||||
<div className="tf-mobility-buttons">
|
||||
<button
|
||||
className={`tf-mobility-btn ${value === 'static' ? 'active' : ''}`}
|
||||
onClick={() => onChange('static')}
|
||||
>
|
||||
Static
|
||||
</button>
|
||||
<button
|
||||
className={`tf-mobility-btn ${value === 'stationary' ? 'active' : ''}`}
|
||||
onClick={() => onChange('stationary')}
|
||||
>
|
||||
Stationary
|
||||
</button>
|
||||
<button
|
||||
className={`tf-mobility-btn ${value === 'movable' ? 'active' : ''}`}
|
||||
onClick={() => onChange('movable')}
|
||||
>
|
||||
Movable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TransformInspectorContent({ context }: { context: ComponentInspectorContext }) {
|
||||
const transform = context.component as TransformComponent;
|
||||
const [isScaleLocked, setIsScaleLocked] = useState(false);
|
||||
const [mobility, setMobility] = useState<'static' | 'stationary' | 'movable'>('static');
|
||||
const [, forceUpdate] = useState({});
|
||||
|
||||
const handlePositionChange = (value: { x: number; y: number; z: number }) => {
|
||||
transform.position = value;
|
||||
context.onChange?.('position', value);
|
||||
forceUpdate({});
|
||||
};
|
||||
|
||||
const handleRotationChange = (value: { x: number; y: number; z: number }) => {
|
||||
transform.rotation = value;
|
||||
context.onChange?.('rotation', value);
|
||||
forceUpdate({});
|
||||
};
|
||||
|
||||
const handleScaleChange = (value: { x: number; y: number; z: number }) => {
|
||||
transform.scale = value;
|
||||
context.onChange?.('scale', value);
|
||||
forceUpdate({});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tf-inspector">
|
||||
<TransformRow
|
||||
label="Location"
|
||||
value={transform.position}
|
||||
onChange={handlePositionChange}
|
||||
onReset={() => handlePositionChange({ x: 0, y: 0, z: 0 })}
|
||||
/>
|
||||
<TransformRow
|
||||
label="Rotation"
|
||||
value={transform.rotation}
|
||||
onChange={handleRotationChange}
|
||||
onReset={() => handleRotationChange({ x: 0, y: 0, z: 0 })}
|
||||
suffix="°"
|
||||
/>
|
||||
<TransformRow
|
||||
label="Scale"
|
||||
value={transform.scale}
|
||||
showLock
|
||||
isLocked={isScaleLocked}
|
||||
onLockChange={setIsScaleLocked}
|
||||
onChange={handleScaleChange}
|
||||
onReset={() => handleScaleChange({ x: 1, y: 1, z: 1 })}
|
||||
showDivider={false}
|
||||
/>
|
||||
<div className="tf-divider" />
|
||||
<MobilityRow value={mobility} onChange={setMobility} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class TransformComponentInspector implements IComponentInspector<TransformComponent> {
|
||||
readonly id = 'transform-component-inspector';
|
||||
readonly name = 'Transform Component Inspector';
|
||||
readonly priority = 100;
|
||||
readonly targetComponents = ['Transform', 'TransformComponent'];
|
||||
|
||||
canHandle(component: Component): component is TransformComponent {
|
||||
return component instanceof TransformComponent ||
|
||||
component.constructor.name === 'TransformComponent' ||
|
||||
(component.constructor as any).componentName === 'Transform';
|
||||
}
|
||||
|
||||
render(context: ComponentInspectorContext): React.ReactElement {
|
||||
return React.createElement(TransformInspectorContent, {
|
||||
context,
|
||||
key: `transform-${context.version}`
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user