Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
@@ -347,414 +347,414 @@ export const BlackboardPanel: React.FC<BlackboardPanelProps> = ({
|
||||
overflowY: 'auto',
|
||||
padding: '10px'
|
||||
}}>
|
||||
{variableEntries.length === 0 && !isAdding && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
padding: '20px'
|
||||
}}>
|
||||
{variableEntries.length === 0 && !isAdding && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
padding: '20px'
|
||||
}}>
|
||||
No variables yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 添加新变量表单 */}
|
||||
{isAdding && (
|
||||
<div style={{
|
||||
marginBottom: '10px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #3c3c3c'
|
||||
}}>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
placeholder="variable.name"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#9cdcfe',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Type</div>
|
||||
<select
|
||||
value={newType}
|
||||
onChange={(e) => setNewType(e.target.value as SimpleBlackboardType)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Value</div>
|
||||
<textarea
|
||||
placeholder={
|
||||
newType === 'object' ? '{"key": "value"}' :
|
||||
newType === 'boolean' ? 'true or false' :
|
||||
newType === 'number' ? '0' : 'value'
|
||||
}
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: newType === 'object' ? '80px' : '30px',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button
|
||||
onClick={handleAddVariable}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdding(false);
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分组显示变量 */}
|
||||
{groupNames.map((groupName) => {
|
||||
const isGroupCollapsed = collapsedGroups.has(groupName);
|
||||
const groupVars = groupedVariables[groupName];
|
||||
if (!groupVars) return null;
|
||||
|
||||
return (
|
||||
<div key={groupName} style={{ marginBottom: '8px' }}>
|
||||
{groupName !== 'default' && (
|
||||
<div
|
||||
onClick={() => toggleGroup(groupName)}
|
||||
{/* 添加新变量表单 */}
|
||||
{isAdding && (
|
||||
<div style={{
|
||||
marginBottom: '10px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #3c3c3c'
|
||||
}}>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
placeholder="variable.name"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#252525',
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '4px',
|
||||
userSelect: 'none'
|
||||
color: '#9cdcfe',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Type</div>
|
||||
<select
|
||||
value={newType}
|
||||
onChange={(e) => setNewType(e.target.value as SimpleBlackboardType)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{isGroupCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
color: '#888'
|
||||
}}>
|
||||
{groupName} ({groupVars.length})
|
||||
</span>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Value</div>
|
||||
<textarea
|
||||
placeholder={
|
||||
newType === 'object' ? '{"key": "value"}' :
|
||||
newType === 'boolean' ? 'true or false' :
|
||||
newType === 'number' ? '0' : 'value'
|
||||
}
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: newType === 'object' ? '80px' : '30px',
|
||||
padding: '6px',
|
||||
marginBottom: '8px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '3px',
|
||||
color: '#cccccc',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button
|
||||
onClick={handleAddVariable}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdding(false);
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '6px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '3px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isGroupCollapsed && groupVars.map(({ fullKey: key, varName, value }) => {
|
||||
const type = getVariableType(value);
|
||||
const isEditing = editingKey === key;
|
||||
{/* 分组显示变量 */}
|
||||
{groupNames.map((groupName) => {
|
||||
const isGroupCollapsed = collapsedGroups.has(groupName);
|
||||
const groupVars = groupedVariables[groupName];
|
||||
if (!groupVars) return null;
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
const variableData = {
|
||||
variableName: key,
|
||||
variableValue: value,
|
||||
variableType: type
|
||||
};
|
||||
e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData));
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
};
|
||||
return (
|
||||
<div key={groupName} style={{ marginBottom: '8px' }}>
|
||||
{groupName !== 'default' && (
|
||||
<div
|
||||
onClick={() => toggleGroup(groupName)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
padding: '4px 6px',
|
||||
backgroundColor: '#252525',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '4px',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
{isGroupCollapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
|
||||
<span style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold',
|
||||
color: '#888'
|
||||
}}>
|
||||
{groupName} ({groupVars.length})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
const typeColor =
|
||||
{!isGroupCollapsed && groupVars.map(({ fullKey: key, varName, value }) => {
|
||||
const type = getVariableType(value);
|
||||
const isEditing = editingKey === key;
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
const variableData = {
|
||||
variableName: key,
|
||||
variableValue: value,
|
||||
variableType: type
|
||||
};
|
||||
e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData));
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
};
|
||||
|
||||
const typeColor =
|
||||
type === 'number' ? '#4ec9b0' :
|
||||
type === 'boolean' ? '#569cd6' :
|
||||
type === 'object' ? '#ce9178' : '#d4d4d4';
|
||||
|
||||
const displayValue = type === 'object' ?
|
||||
JSON.stringify(value) :
|
||||
String(value);
|
||||
const displayValue = type === 'object' ?
|
||||
JSON.stringify(value) :
|
||||
String(value);
|
||||
|
||||
const truncatedValue = displayValue.length > 30 ?
|
||||
displayValue.substring(0, 30) + '...' :
|
||||
displayValue;
|
||||
const truncatedValue = displayValue.length > 30 ?
|
||||
displayValue.substring(0, 30) + '...' :
|
||||
displayValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
draggable={!isEditing}
|
||||
onDragStart={handleDragStart}
|
||||
style={{
|
||||
marginBottom: '6px',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '3px',
|
||||
borderLeft: `3px solid ${typeColor}`,
|
||||
cursor: isEditing ? 'default' : 'grab'
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editingNewKey}
|
||||
onChange={(e) => setEditingNewKey(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#9cdcfe',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Type</div>
|
||||
<select
|
||||
value={editType}
|
||||
onChange={(e) => setEditType(e.target.value as SimpleBlackboardType)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Value</div>
|
||||
<textarea
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: editType === 'object' ? '60px' : '24px',
|
||||
padding: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #0e639c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical',
|
||||
marginBottom: '4px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(key)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
draggable={!isEditing}
|
||||
onDragStart={handleDragStart}
|
||||
style={{
|
||||
marginBottom: '6px',
|
||||
padding: '6px 8px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
borderRadius: '3px',
|
||||
borderLeft: `3px solid ${typeColor}`,
|
||||
cursor: isEditing ? 'default' : 'grab'
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<div>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editingNewKey}
|
||||
onChange={(e) => setEditingNewKey(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#9cdcfe',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Type</div>
|
||||
<select
|
||||
value={editType}
|
||||
onChange={(e) => setEditType(e.target.value as SimpleBlackboardType)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
marginBottom: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #3c3c3c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object (JSON)</option>
|
||||
</select>
|
||||
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>Value</div>
|
||||
<textarea
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: editType === 'object' ? '60px' : '24px',
|
||||
padding: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
border: '1px solid #0e639c',
|
||||
borderRadius: '2px',
|
||||
color: '#cccccc',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'vertical',
|
||||
marginBottom: '4px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(key)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#0e639c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingKey(null)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingKey(null)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '3px 8px',
|
||||
backgroundColor: '#3c3c3c',
|
||||
border: 'none',
|
||||
borderRadius: '2px',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#9cdcfe',
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
gap: '8px'
|
||||
}}>
|
||||
<GripVertical size={10} style={{ opacity: 0.3, flexShrink: 0 }} />
|
||||
{varName}
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '10px'
|
||||
}}>({type})</span>
|
||||
{isModified(key) && (
|
||||
<span style={{
|
||||
fontSize: '9px',
|
||||
color: '#ff9800',
|
||||
fontWeight: 'bold'
|
||||
}}>*</span>
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#9cdcfe',
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<GripVertical size={10} style={{ opacity: 0.3, flexShrink: 0 }} />
|
||||
{varName}
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '10px'
|
||||
}}>({type})</span>
|
||||
{isModified(key) && (
|
||||
<span style={{
|
||||
fontSize: '9px',
|
||||
color: '#ff9800',
|
||||
fontWeight: 'bold'
|
||||
}}>*</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: '#888',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{truncatedValue}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '2px', flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => handleCopyVariable(key, value)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Copy"
|
||||
>
|
||||
<Copy size={11} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStartEdit(key, value)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 size={11} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => currentOnDelete && currentOnDelete(key)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#5a1a1a';
|
||||
e.currentTarget.style.color = '#f48771';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
color: '#888',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{truncatedValue}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '2px', flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => handleCopyVariable(key, value)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Copy"
|
||||
>
|
||||
<Copy size={11} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStartEdit(key, value)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#3c3c3c';
|
||||
e.currentTarget.style.color = '#ccc';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Edit"
|
||||
>
|
||||
<Edit2 size={11} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => currentOnDelete && currentOnDelete(key)}
|
||||
style={{
|
||||
padding: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = '#5a1a1a';
|
||||
e.currentTarget.style.color = '#f48771';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
e.currentTarget.style.color = '#888';
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 底部信息栏 */}
|
||||
<div style={{
|
||||
|
||||
@@ -65,7 +65,7 @@ export const NodeContextMenu: React.FC<NodeContextMenuProps> = ({
|
||||
{onDeleteNode && (
|
||||
<div
|
||||
onClick={onDeleteNode}
|
||||
style={{...menuItemStyle, color: '#f48771'}}
|
||||
style={{ ...menuItemStyle, color: '#f48771' }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#5a1a1a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
|
||||
>
|
||||
|
||||
@@ -69,13 +69,13 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower]);
|
||||
|
||||
const flattenedTemplates = React.useMemo(() => {
|
||||
return categoryGroups.flatMap(group =>
|
||||
return categoryGroups.flatMap((group) =>
|
||||
group.isExpanded ? group.templates : []
|
||||
);
|
||||
}, [categoryGroups]);
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setExpandedCategories(prev => {
|
||||
setExpandedCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(category)) {
|
||||
newSet.delete(category);
|
||||
@@ -88,7 +88,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (allTemplates.length > 0 && expandedCategories.size === 0) {
|
||||
const categories = new Set(allTemplates.map(t => t.category || '未分类'));
|
||||
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
|
||||
setExpandedCategories(categories);
|
||||
}
|
||||
}, [allTemplates, expandedCategories.size]);
|
||||
|
||||
@@ -256,30 +256,30 @@ const BehaviorTreeNodeComponent: React.FC<BehaviorTreeNodeProps> = ({
|
||||
!nodes.some((n) =>
|
||||
connections.some((c) => c.from === node.id && c.to === n.id)
|
||||
) && (
|
||||
<div
|
||||
className="bt-node-empty-warning-container"
|
||||
<div
|
||||
className="bt-node-empty-warning-container"
|
||||
style={{
|
||||
marginLeft: ((!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className)) || isUncommitted) ? '4px' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
style={{
|
||||
marginLeft: ((!isRoot && node.template.className && executorRef.current && !executorRef.current.hasExecutor(node.template.className)) || isUncommitted) ? '4px' : 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'help',
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative'
|
||||
color: '#ff9800',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
style={{
|
||||
color: '#ff9800',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<div className="bt-node-empty-warning-tooltip">
|
||||
/>
|
||||
<div className="bt-node-empty-warning-tooltip">
|
||||
空节点:没有子节点,执行时会直接跳过
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bt-node-body">
|
||||
@@ -341,16 +341,16 @@ const BehaviorTreeNodeComponent: React.FC<BehaviorTreeNodeProps> = ({
|
||||
|
||||
{(isRoot || node.template.type === 'composite' || node.template.type === 'decorator') &&
|
||||
(node.template.requiresChildren === undefined || node.template.requiresChildren === true) && (
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-port-type="node-output"
|
||||
onMouseDown={(e) => onPortMouseDown(e, node.id)}
|
||||
onMouseUp={(e) => onPortMouseUp(e, node.id)}
|
||||
className="bt-node-port bt-node-port-output"
|
||||
title="Output"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-port="true"
|
||||
data-node-id={node.id}
|
||||
data-port-type="node-output"
|
||||
onMouseDown={(e) => onPortMouseDown(e, node.id)}
|
||||
onMouseUp={(e) => onPortMouseUp(e, node.id)}
|
||||
className="bt-node-port bt-node-port-output"
|
||||
title="Output"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -99,7 +99,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
);
|
||||
|
||||
// 更新树
|
||||
const nodes = state.getNodes().map(n =>
|
||||
const nodes = state.getNodes().map((n) =>
|
||||
n.id === data.nodeId ? updatedNode : n
|
||||
);
|
||||
|
||||
|
||||
@@ -44,4 +44,4 @@ export const NODE_COLORS = {
|
||||
[NodeType.Decorator]: '#4dffb8',
|
||||
[NodeType.Action]: '#ff4d4d',
|
||||
[NodeType.Condition]: '#4dff9e'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,17 +7,17 @@ import { useBehaviorTreeDataStore, useUIStore } from '../stores';
|
||||
*/
|
||||
export function useCanvasInteraction() {
|
||||
// 从数据 store 获取画布状态
|
||||
const canvasOffset = useBehaviorTreeDataStore(state => state.canvasOffset);
|
||||
const canvasScale = useBehaviorTreeDataStore(state => state.canvasScale);
|
||||
const setCanvasOffset = useBehaviorTreeDataStore(state => state.setCanvasOffset);
|
||||
const setCanvasScale = useBehaviorTreeDataStore(state => state.setCanvasScale);
|
||||
const resetView = useBehaviorTreeDataStore(state => state.resetView);
|
||||
const canvasOffset = useBehaviorTreeDataStore((state) => state.canvasOffset);
|
||||
const canvasScale = useBehaviorTreeDataStore((state) => state.canvasScale);
|
||||
const setCanvasOffset = useBehaviorTreeDataStore((state) => state.setCanvasOffset);
|
||||
const setCanvasScale = useBehaviorTreeDataStore((state) => state.setCanvasScale);
|
||||
const resetView = useBehaviorTreeDataStore((state) => state.resetView);
|
||||
|
||||
// 从 UI store 获取平移状态
|
||||
const isPanning = useUIStore(state => state.isPanning);
|
||||
const panStart = useUIStore(state => state.panStart);
|
||||
const setIsPanning = useUIStore(state => state.setIsPanning);
|
||||
const setPanStart = useUIStore(state => state.setPanStart);
|
||||
const isPanning = useUIStore((state) => state.isPanning);
|
||||
const panStart = useUIStore((state) => state.panStart);
|
||||
const setIsPanning = useUIStore((state) => state.setIsPanning);
|
||||
const setPanStart = useUIStore((state) => state.setPanStart);
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -181,7 +181,7 @@ export function useCanvasMouseEvents(params: UseCanvasMouseEventsParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceNode = nodes.find(n => n.id === connectingFrom);
|
||||
const sourceNode = nodes.find((n) => n.id === connectingFrom);
|
||||
if (sourceNode && !sourceNode.canAddChild()) {
|
||||
const maxChildren = sourceNode.template.maxChildren ?? Infinity;
|
||||
showToast?.(
|
||||
|
||||
@@ -92,6 +92,6 @@ export class NodeFactory implements INodeFactory {
|
||||
*/
|
||||
getTemplateByImplementationType(implementationType: string): NodeTemplate | null {
|
||||
const allTemplates = this.getAllTemplates();
|
||||
return allTemplates.find(t => t.className === implementationType) || null;
|
||||
return allTemplates.find((t) => t.className === implementationType) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class NodeRegistryService {
|
||||
const template = this.createTemplate(config, metadata);
|
||||
this.customTemplates.set(config.implementationType, template);
|
||||
|
||||
this.registrationCallbacks.forEach(cb => cb(template));
|
||||
this.registrationCallbacks.forEach((cb) => cb(template));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +139,7 @@ export class NodeRegistryService {
|
||||
description: prop.description,
|
||||
min: prop.min,
|
||||
max: prop.max,
|
||||
options: prop.options?.map(o => o.value)
|
||||
options: prop.options?.map((o) => o.value)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class NodeRegistryService {
|
||||
color: config.color || this.getDefaultColor(config.type),
|
||||
className: config.implementationType,
|
||||
defaultConfig,
|
||||
properties: (config.properties || []).map(p => ({
|
||||
properties: (config.properties || []).map((p) => ({
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
label: p.label,
|
||||
|
||||
@@ -201,7 +201,7 @@ const NodePropertiesPanel: React.FC<NodePropertiesPanelProps> = ({ node, onPrope
|
||||
const [localData, setLocalData] = useState<Record<string, any>>(node.data);
|
||||
|
||||
const handlePropertyChange = useCallback((name: string, value: any) => {
|
||||
setLocalData(prev => ({ ...prev, [name]: value }));
|
||||
setLocalData((prev) => ({ ...prev, [name]: value }));
|
||||
onPropertyChange?.(node.id, name, value);
|
||||
}, [node.id, onPropertyChange]);
|
||||
|
||||
|
||||
@@ -58,4 +58,4 @@ export class NotificationService {
|
||||
// 导出单例实例的便捷方法
|
||||
export const showToast = (message: string, type?: 'success' | 'error' | 'warning' | 'info') => {
|
||||
NotificationService.getInstance().showToast(message, type);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -451,15 +451,15 @@ export class BehaviorTreeExecutor {
|
||||
|
||||
// 检查断点
|
||||
logger.info(`[Breakpoint Debug] Node ${nodeData.name} (${nodeId}) started running`);
|
||||
logger.info(`[Breakpoint Debug] Breakpoints count:`, this.breakpoints.size);
|
||||
logger.info(`[Breakpoint Debug] Has breakpoint:`, this.breakpoints.has(nodeId));
|
||||
logger.info('[Breakpoint Debug] Breakpoints count:', this.breakpoints.size);
|
||||
logger.info('[Breakpoint Debug] Has breakpoint:', this.breakpoints.has(nodeId));
|
||||
|
||||
const breakpoint = this.breakpoints.get(nodeId);
|
||||
if (breakpoint) {
|
||||
logger.info(`[Breakpoint Debug] Breakpoint found, enabled:`, breakpoint.enabled);
|
||||
logger.info('[Breakpoint Debug] Breakpoint found, enabled:', breakpoint.enabled);
|
||||
if (breakpoint.enabled) {
|
||||
this.addLog(`断点触发: ${nodeData.name}`, 'warning', nodeId);
|
||||
logger.info(`[Breakpoint Debug] Calling onBreakpointHit callback:`, !!this.onBreakpointHit);
|
||||
logger.info('[Breakpoint Debug] Calling onBreakpointHit callback:', !!this.onBreakpointHit);
|
||||
if (this.onBreakpointHit) {
|
||||
this.onBreakpointHit(nodeId, nodeData.name);
|
||||
}
|
||||
|
||||
@@ -58,4 +58,4 @@ export function getPortPosition(
|
||||
const worldY = (screenY - canvasOffset.y) / canvasScale;
|
||||
|
||||
return { x: worldX, y: worldY };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user