/** * 粒子系统 Inspector Provider * Particle System Inspector Provider */ import React, { useState, useCallback } from 'react'; import { Play, Pause, RotateCcw, Sparkles } from 'lucide-react'; import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core'; import type { ParticleSystemComponent } from '@esengine/particle'; import { EmissionShape, ParticleBlendMode } from '@esengine/particle'; interface ParticleInspectorData { entityId: string; component: ParticleSystemComponent; } export class ParticleInspectorProvider implements IInspectorProvider { readonly id = 'particle-system-inspector'; readonly name = 'Particle System Inspector'; readonly priority = 100; canHandle(target: unknown): target is ParticleInspectorData { if (typeof target !== 'object' || target === null) return false; const obj = target as Record; return 'entityId' in obj && 'component' in obj && obj.component !== null && typeof obj.component === 'object' && 'maxParticles' in (obj.component as Record) && 'emissionRate' in (obj.component as Record); } render(data: ParticleInspectorData, _context: InspectorContext): React.ReactElement { return ; } } interface ParticleInspectorUIProps { data: ParticleInspectorData; } function ParticleInspectorUI({ data }: ParticleInspectorUIProps) { const { component } = data; const [, forceUpdate] = useState({}); const refresh = useCallback(() => forceUpdate({}), []); const handlePlay = () => { component.play(); refresh(); }; const handlePause = () => { component.pause(); refresh(); }; const handleStop = () => { component.stop(true); refresh(); }; const handleBurst = () => { component.burst(10); refresh(); }; const handleChange = ( key: K, value: ParticleSystemComponent[K] ) => { (component as any)[key] = value; component.markDirty(); refresh(); }; return (
{/* 控制按钮 | Control buttons */}
Controls
{component.activeParticleCount} / {component.maxParticles}
{/* 基础属性 | Basic Properties */}
Basic
handleChange('maxParticles', v)} /> handleChange('looping', v)} /> handleChange('duration', v)} /> handleChange('playbackSpeed', v)} />
{/* 发射属性 | Emission Properties */}
Emission
handleChange('emissionRate', v)} /> handleChange('emissionShape', v as EmissionShape)} /> {component.emissionShape !== EmissionShape.Point && ( handleChange('shapeRadius', v)} /> )}
{/* 粒子属性 | Particle Properties */}
Particle
handleChange('lifetimeMin', v)} onMaxChange={v => handleChange('lifetimeMax', v)} /> handleChange('speedMin', v)} onMaxChange={v => handleChange('speedMax', v)} /> handleChange('direction', v)} /> handleChange('directionSpread', v)} /> handleChange('scaleMin', v)} onMaxChange={v => handleChange('scaleMax', v)} /> handleChange('gravityX', v)} /> handleChange('gravityY', v)} />
{/* 颜色属性 | Color Properties */}
Color
handleChange('startColor', v)} /> handleChange('startAlpha', v)} /> handleChange('endAlpha', v)} /> handleChange('endScale', v)} />
{/* 渲染属性 | Rendering Properties */}
Rendering
handleChange('particleSize', v)} /> handleChange('blendMode', v as ParticleBlendMode)} /> handleChange('sortingOrder', v)} />
); } // ============= UI Components ============= const inputStyle: React.CSSProperties = { width: '100%', padding: '4px 8px', border: '1px solid var(--border-color, #3a3a3a)', borderRadius: '4px', background: 'var(--input-background, #2a2a2a)', color: 'var(--text-color, #e0e0e0)', fontSize: '12px', }; const buttonStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '6px 10px', border: 'none', borderRadius: '4px', background: 'var(--button-background, #3a3a3a)', color: 'var(--text-color, #e0e0e0)', cursor: 'pointer', fontSize: '12px', }; interface NumberInputProps { label: string; value: number; min?: number; max?: number; step?: number; onChange: (value: number) => void; } function NumberInput({ label, value, min, max, step = 1, onChange }: NumberInputProps) { return (
onChange(parseFloat(e.target.value) || 0)} style={inputStyle} />
); } interface RangeInputProps { label: string; minValue: number; maxValue: number; min?: number; max?: number; step?: number; onMinChange: (value: number) => void; onMaxChange: (value: number) => void; } function RangeInput({ label, minValue, maxValue, min, max, step = 1, onMinChange, onMaxChange }: RangeInputProps) { return (
onMinChange(parseFloat(e.target.value) || 0)} style={{ ...inputStyle, width: '50%' }} title="Min" /> onMaxChange(parseFloat(e.target.value) || 0)} style={{ ...inputStyle, width: '50%' }} title="Max" />
); } interface CheckboxInputProps { label: string; checked: boolean; onChange: (value: boolean) => void; } function CheckboxInput({ label, checked, onChange }: CheckboxInputProps) { return (
onChange(e.target.checked)} />
); } interface SelectInputProps { label: string; value: string; options: { value: string; label: string }[]; onChange: (value: string) => void; } function SelectInput({ label, value, options, onChange }: SelectInputProps) { return (
); } interface ColorInputProps { label: string; value: string; onChange: (value: string) => void; } function ColorInput({ label, value, onChange }: ColorInputProps) { return (
onChange(e.target.value)} style={{ ...inputStyle, padding: '2px', height: '24px' }} />
); }