可动态识别属性

This commit is contained in:
YHH
2025-10-15 17:15:05 +08:00
parent b69b81f63a
commit fb7a1b1282
30 changed files with 2069 additions and 461 deletions

View File

@@ -16,13 +16,26 @@ export function AddComponent({ entity, componentRegistry, onAdd, onCancel }: Add
const [filter, setFilter] = useState('');
useEffect(() => {
if (!componentRegistry) {
console.error('ComponentRegistry is null');
return;
}
const allComponents = componentRegistry.getAllComponents();
console.log('All registered components:', allComponents);
allComponents.forEach(comp => {
console.log(`Component ${comp.name}: has type = ${!!comp.type}`);
});
const existingComponentNames = entity.components.map(c => c.constructor.name);
const availableComponents = allComponents.filter(
comp => !existingComponentNames.includes(comp.name)
comp => comp.type && !existingComponentNames.includes(comp.name)
);
console.log('Available components to add:', availableComponents);
console.log('Components filtered out:', allComponents.filter(comp => !comp.type).map(c => c.name));
setComponents(availableComponents);
}, [entity, componentRegistry]);

View File

@@ -3,6 +3,7 @@ import { Entity, Core } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { AddComponent } from './AddComponent';
import { PropertyInspector } from './PropertyInspector';
import { FileSearch, Plus, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
@@ -37,11 +38,16 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
return;
}
console.log('Attempting to create component:', componentName);
const component = componentRegistry.createInstance(componentName);
console.log('Created component:', component);
if (component) {
selectedEntity.addComponent(component);
messageHub.publish('component:added', { entity: selectedEntity, component });
setShowAddComponent(false);
} else {
console.error('Failed to create component instance for:', componentName);
}
};
@@ -80,10 +86,15 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content">
<div className="empty-state">No entity selected</div>
<div className="empty-state">
<FileSearch size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">No entity selected</div>
<div className="empty-hint">Select an entity from the hierarchy</div>
</div>
</div>
</div>
);
@@ -94,11 +105,15 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content">
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">Entity Info</div>
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
@@ -117,44 +132,47 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({components.length})</span>
<button
className="add-component-btn"
onClick={() => setShowAddComponent(true)}
title="Add Component"
>
+
<Plus size={12} />
</button>
</div>
<div className="section-content">
{components.length === 0 ? (
<div className="empty-state">No components</div>
<div className="empty-state-small">No components</div>
) : (
<ul className="component-list">
{components.map((component, index) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className="component-item">
<div className="component-header">
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
onClick={() => toggleComponentExpanded(index)}
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? '▼' : '▶'}
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<span className="component-icon">🔧</span>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={() => handleRemoveComponent(index)}
onClick={(e) => {
e.stopPropagation();
handleRemoveComponent(index);
}}
title="Remove Component"
>
×
<X size={14} />
</button>
</div>
{isExpanded && (
<div className="component-properties">
<div className="component-properties animate-slideDown">
<PropertyInspector
component={component}
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Component, Core } from '@esengine/ecs-framework';
import { PropertyMetadataService, PropertyMetadata } from '@esengine/editor-core';
import { ChevronRight, ChevronDown } from 'lucide-react';
import '../styles/PropertyInspector.css';
interface PropertyInspectorProps {
@@ -48,97 +49,83 @@ export function PropertyInspector({ component, onChange }: PropertyInspectorProp
switch (metadata.type) {
case 'number':
return (
<div key={propertyName} className="property-field">
<label className="property-label">{label}</label>
<input
type="number"
className="property-input"
value={value ?? 0}
min={metadata.min}
max={metadata.max}
step={metadata.step ?? 1}
disabled={metadata.readOnly}
onChange={(e) => handleChange(propertyName, parseFloat(e.target.value) || 0)}
/>
</div>
<NumberField
key={propertyName}
label={label}
value={value ?? 0}
min={metadata.min}
max={metadata.max}
step={metadata.step}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'string':
return (
<div key={propertyName} className="property-field">
<label className="property-label">{label}</label>
<input
type="text"
className="property-input"
value={value ?? ''}
disabled={metadata.readOnly}
onChange={(e) => handleChange(propertyName, e.target.value)}
/>
</div>
<StringField
key={propertyName}
label={label}
value={value ?? ''}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'boolean':
return (
<div key={propertyName} className="property-field property-field-checkbox">
<label className="property-label">{label}</label>
<input
type="checkbox"
className="property-checkbox"
checked={value ?? false}
disabled={metadata.readOnly}
onChange={(e) => handleChange(propertyName, e.target.checked)}
/>
</div>
<BooleanField
key={propertyName}
label={label}
value={value ?? false}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'color':
return (
<div key={propertyName} className="property-field">
<label className="property-label">{label}</label>
<input
type="color"
className="property-input property-color"
value={value ?? '#ffffff'}
disabled={metadata.readOnly}
onChange={(e) => handleChange(propertyName, e.target.value)}
/>
</div>
<ColorField
key={propertyName}
label={label}
value={value ?? '#ffffff'}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'vector2':
return (
<Vector2Field
key={propertyName}
label={label}
value={value ?? { x: 0, y: 0 }}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'vector3':
return (
<div key={propertyName} className="property-field">
<label className="property-label">{label}</label>
<div className="property-vector">
<input
type="number"
className="property-input property-vector-input"
value={value?.x ?? 0}
disabled={metadata.readOnly}
placeholder="X"
onChange={(e) => handleChange(propertyName, { ...value, x: parseFloat(e.target.value) || 0 })}
/>
<input
type="number"
className="property-input property-vector-input"
value={value?.y ?? 0}
disabled={metadata.readOnly}
placeholder="Y"
onChange={(e) => handleChange(propertyName, { ...value, y: parseFloat(e.target.value) || 0 })}
/>
{metadata.type === 'vector3' && (
<input
type="number"
className="property-input property-vector-input"
value={value?.z ?? 0}
disabled={metadata.readOnly}
placeholder="Z"
onChange={(e) => handleChange(propertyName, { ...value, z: parseFloat(e.target.value) || 0 })}
/>
)}
</div>
</div>
<Vector3Field
key={propertyName}
label={label}
value={value ?? { x: 0, y: 0, z: 0 }}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'enum':
return (
<EnumField
key={propertyName}
label={label}
value={value}
options={metadata.options || []}
readOnly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
default:
@@ -154,3 +141,377 @@ export function PropertyInspector({ component, onChange }: PropertyInspectorProp
</div>
);
}
interface NumberFieldProps {
label: string;
value: number;
min?: number;
max?: number;
step?: number;
readOnly?: boolean;
onChange: (value: number) => void;
}
function NumberField({ label, value, min, max, step = 0.1, readOnly, onChange }: NumberFieldProps) {
const [isDragging, setIsDragging] = useState(false);
const [dragStartX, setDragStartX] = useState(0);
const [dragStartValue, setDragStartValue] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const handleMouseDown = (e: React.MouseEvent) => {
if (readOnly) return;
setIsDragging(true);
setDragStartX(e.clientX);
setDragStartValue(value);
e.preventDefault();
};
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - dragStartX;
const sensitivity = e.shiftKey ? 0.1 : 1;
let newValue = dragStartValue + delta * step * sensitivity;
if (min !== undefined) newValue = Math.max(min, newValue);
if (max !== undefined) newValue = Math.min(max, newValue);
onChange(parseFloat(newValue.toFixed(3)));
};
const handleMouseUp = () => {
setIsDragging(false);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, dragStartX, dragStartValue, step, min, max, onChange]);
return (
<div className="property-field">
<label
className="property-label property-label-draggable"
onMouseDown={handleMouseDown}
style={{ cursor: readOnly ? 'default' : 'ew-resize' }}
>
{label}
</label>
<input
ref={inputRef}
type="number"
className="property-input property-input-number"
value={value}
min={min}
max={max}
step={step}
disabled={readOnly}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
onFocus={(e) => e.target.select()}
/>
</div>
);
}
interface StringFieldProps {
label: string;
value: string;
readOnly?: boolean;
onChange: (value: string) => void;
}
function StringField({ label, value, readOnly, onChange }: StringFieldProps) {
return (
<div className="property-field">
<label className="property-label">{label}</label>
<input
type="text"
className="property-input property-input-text"
value={value}
disabled={readOnly}
onChange={(e) => onChange(e.target.value)}
onFocus={(e) => e.target.select()}
/>
</div>
);
}
interface BooleanFieldProps {
label: string;
value: boolean;
readOnly?: boolean;
onChange: (value: boolean) => void;
}
function BooleanField({ label, value, readOnly, onChange }: BooleanFieldProps) {
return (
<div className="property-field property-field-boolean">
<label className="property-label">{label}</label>
<button
className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'}`}
disabled={readOnly}
onClick={() => onChange(!value)}
>
<span className="property-toggle-thumb" />
</button>
</div>
);
}
interface ColorFieldProps {
label: string;
value: string;
readOnly?: boolean;
onChange: (value: string) => void;
}
function ColorField({ label, value, readOnly, onChange }: ColorFieldProps) {
return (
<div className="property-field">
<label className="property-label">{label}</label>
<div className="property-color-wrapper">
<div className="property-color-preview" style={{ backgroundColor: value }} />
<input
type="color"
className="property-input property-input-color"
value={value}
disabled={readOnly}
onChange={(e) => onChange(e.target.value)}
/>
<input
type="text"
className="property-input property-input-color-text"
value={value.toUpperCase()}
disabled={readOnly}
onChange={(e) => onChange(e.target.value)}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
);
}
interface Vector2FieldProps {
label: string;
value: { x: number; y: number };
readOnly?: boolean;
onChange: (value: { x: number; y: number }) => void;
}
function Vector2Field({ label, value, readOnly, onChange }: Vector2FieldProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="property-field">
<div className="property-label-row">
<button
className="property-expand-btn"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<label className="property-label">{label}</label>
</div>
{isExpanded ? (
<div className="property-vector-expanded">
<div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number"
value={value?.x ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number"
value={value?.y ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
) : (
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value?.x ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value?.y ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div>
);
}
interface Vector3FieldProps {
label: string;
value: { x: number; y: number; z: number };
readOnly?: boolean;
onChange: (value: { x: number; y: number; z: number }) => void;
}
function Vector3Field({ label, value, readOnly, onChange }: Vector3FieldProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="property-field">
<div className="property-label-row">
<button
className="property-expand-btn"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<label className="property-label">{label}</label>
</div>
{isExpanded ? (
<div className="property-vector-expanded">
<div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number"
value={value?.x ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number"
value={value?.y ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number"
value={value?.z ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
) : (
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value?.x ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, x: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value?.y ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, y: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value?.z ?? 0}
disabled={readOnly}
step={0.1}
onChange={(e) => onChange({ ...value, z: parseFloat(e.target.value) || 0 })}
onFocus={(e) => e.target.select()}
/>
</div>
</div>
)}
</div>
);
}
interface EnumFieldProps {
label: string;
value: any;
options: Array<{ label: string; value: any }>;
readOnly?: boolean;
onChange: (value: any) => void;
}
function EnumField({ label, value, options, readOnly, onChange }: EnumFieldProps) {
return (
<div className="property-field">
<label className="property-label">{label}</label>
<select
className="property-input property-input-select"
value={value ?? ''}
disabled={readOnly}
onChange={(e) => {
const selectedOption = options.find(opt => String(opt.value) === e.target.value);
if (selectedOption) {
onChange(selectedOption.value);
}
}}
>
{options.length === 0 && (
<option value="">No options</option>
)}
{options.map((option, index) => (
<option key={index} value={String(option.value)}>
{option.label}
</option>
))}
</select>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { useLocale } from '../hooks/useLocale';
import { Box, Layers } from 'lucide-react';
import '../styles/SceneHierarchy.css';
interface SceneHierarchyProps {
@@ -45,11 +46,16 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
return (
<div className="scene-hierarchy">
<div className="hierarchy-header">
<Layers size={16} className="hierarchy-header-icon" />
<h3>{t('hierarchy.title')}</h3>
</div>
<div className="hierarchy-content">
<div className="hierarchy-content scrollable">
{entities.length === 0 ? (
<div className="empty-state">{t('hierarchy.empty')}</div>
<div className="empty-state">
<Box size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">{t('hierarchy.empty')}</div>
<div className="empty-hint">Create an entity to get started</div>
</div>
) : (
<ul className="entity-list">
{entities.map(entity => (
@@ -58,7 +64,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`}
onClick={() => handleEntityClick(entity)}
>
<span className="entity-icon">📦</span>
<Box size={14} className="entity-icon" />
<span className="entity-name">Entity {entity.id}</span>
</li>
))}