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:
YHH
2025-11-29 23:00:48 +08:00
committed by GitHub
parent f03b73b58e
commit 359886c72f
198 changed files with 33879 additions and 13121 deletions

View File

@@ -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}`
});
}
}