Files
ichni_Creator_Studio/Assets/Scripts/Console/EditorConsoleMethods.cs
2026-05-23 21:05:16 +08:00

1213 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Ichni.NodeScript;
using Ichni.RhythmGame;
using Ichni.RhythmGame.Beatmap;
using Ichni.RhythmGame.ThemeBundles.DepartureToMultiverse;
using UniRx;
using UnityEngine;
namespace Ichni.Editor
{
public static class EditorConsoleMethods
{
#region UI/Manager Accessors (UI/访)
public static Inspector inspector => EditorManager.instance.uiManager.inspector;
public static Hierarchy hierarchy => EditorManager.instance.uiManager.hierarchy;
public static LogWindow logWindow => EditorManager.instance.uiManager.mainPage.logWindow;
#endregion
#region Scene/Transform Operations (/)
/// <summary>
/// 传送场景相机到指定位置
/// </summary>
public static void tp(Vector3 pos)
{
if (EditorManager.instance.cameraManager.isSceneCameraActive)
{
EditorManager.instance.cameraManager.sceneCamera.sceneCamera.transform.position = pos;
}
}
/// <summary>
/// 传送场景相机到选中元素位置
/// </summary>
public static void tp()
{
if (EditorManager.instance.cameraManager.isSceneCameraActive)
{
EditorManager.instance.cameraManager.sceneCamera.sceneCamera.transform.position =
inspector.connectedGameElement.transform.position;
}
}
/// <summary>
/// 重命名选中元素
/// </summary>
public static void ReName(string message)
{
inspector.connectedGameElement.elementName = message;
inspector.connectedGameElement.Refresh();
}
#endregion
#region PathNode Generation ()
/// <summary>
/// 直线路径节点生成 (Line PathNode Generator)
/// </summary>
public static void Lgp(int loop, Vector3 start, Vector3 end, bool Clear = false, bool offsetOrigin = false)
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Track))
{
LogWindow.Log("Please select a Track first!");
return;
}
if (loop <= 1)
{
LogWindow.Log("Loop must be greater than 1!");
return;
}
Track track = (Track)inspector.connectedGameElement;
List<PathNode> oldNodes = track.trackPathSubmodule.pathNodeList.ToList();
List<PathNode> newNodes = new List<PathNode>();
// 如果 Clear 且有旧节点,迁移变换
if (Clear)
{
if (offsetOrigin && oldNodes.Count > 0 && newNodes.Count > 0)
{
AdjustPathNodesToNearest(track, newNodes, oldNodes);
}
// 清除之前的PathNode
foreach (var node in oldNodes)
{
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElement(node);
}
}
for (int i = 0; i < loop; i++)
{
float t = (float)i / (loop - 1); // 修正插值
float x = start.x + (end.x - start.x) * t;
float y = start.y + (end.y - start.y) * t;
float z = start.z + (end.z - start.z) * t;
PathNode j = PathNode.GenerateElement("PathNode" + i.ToString(), Guid.NewGuid(), new List<string>(), true, track, true);
j.transformSubmodule.originalPosition = new Vector3(x, y, z);
newNodes.Add(j);
}
}
/// <summary>
/// 主轴方向的螺旋线式 PathNode
/// </summary>
public static void Spiral(int loop, Vector3 center, float r, float h, int pointsPerTurn, string axis = "y")
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Track))
{
LogWindow.Log("Please select a Track first!");
return;
}
Track track = (Track)inspector.connectedGameElement;
for (int i = 0; i < loop; i++)
{
float t = (float)i / loop;
float angle = 2 * Mathf.PI * (i % pointsPerTurn) / pointsPerTurn;
Vector3 pos = new Vector3(center.x, center.y, center.z);
switch (axis.ToLower())
{
case "x":
pos.x += h * t;
pos.y += r * Mathf.Cos(angle);
pos.z += r * Mathf.Sin(angle);
break;
case "y":
pos.x += r * Mathf.Cos(angle);
pos.y += h * t;
pos.z += r * Mathf.Sin(angle);
break;
case "z":
pos.x += r * Mathf.Cos(angle);
pos.y += r * Mathf.Sin(angle);
pos.z += h * t;
break;
default:
pos.x += r * Mathf.Cos(angle);
pos.y += h * t;
pos.z += r * Mathf.Sin(angle);
break;
}
PathNode node = PathNode.GenerateElement("SpiralNode" + i.ToString(), Guid.NewGuid(), new List<string>(), true, track, true);
node.transformSubmodule.originalPosition = pos;
}
}
/// <summary>
/// 任意方向的螺旋线式 PathNode中心点和方向均为Vector3
/// </summary>
public static void Spiral(int loop, Vector3 center, float r, float h, int pointsPerTurn, Vector3 dir)
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Track))
{
LogWindow.Log("Please select a Track first!");
return;
}
Vector3 direction = dir.normalized;
if (direction == Vector3.zero) direction = Vector3.up; // 默认Y轴
Quaternion rot = Quaternion.FromToRotation(Vector3.up, direction);
Track track = (Track)inspector.connectedGameElement;
for (int i = 0; i < loop; i++)
{
float t = (float)i / loop;
float angle = 2 * Mathf.PI * (i % pointsPerTurn) / pointsPerTurn;
Vector3 localPos = new Vector3(
r * Mathf.Cos(angle),
h * t,
r * Mathf.Sin(angle)
);
Vector3 pos = center + rot * localPos;
PathNode node = PathNode.GenerateElement("SpiralNode" + i.ToString(), Guid.NewGuid(), new List<string>(), true, track, true);
node.transformSubmodule.originalPosition = pos;
}
}
#endregion
#region PathNode/Track Utilities (/)
/// <summary>
/// 将原有 PathNode 的变换(位置、旋转、缩放)迁移到新生成的最近 PathNode 上
/// </summary>
public static bool AdjustPathNodesToNearest(Track track, List<PathNode> newNodes, List<PathNode> oldNodes)
{
foreach (var oldNode in oldNodes)
{
// 找到距离 oldNode 最近的新节点
PathNode nearest = newNodes
.OrderBy(n => Vector3.Distance(n.transformSubmodule.originalPosition, oldNode.transformSubmodule.originalPosition))
.FirstOrDefault();
if (nearest != null)
{
// 计算 oldNode 的变换(直接用欧拉角,不用四元数)
Vector3 deltaPos = oldNode.transformSubmodule.originalPosition - oldNode.transformSubmodule.originalPosition;
Vector3 deltaEuler = oldNode.transformSubmodule.originalEulerAngles - oldNode.transformSubmodule.originalEulerAngles;
Vector3 deltaScale = oldNode.transformSubmodule.originalScale - oldNode.transformSubmodule.originalScale;
// 将变换应用到新节点
nearest.transformSubmodule.originalPosition += deltaPos;
nearest.transformSubmodule.originalEulerAngles += deltaEuler;
nearest.transformSubmodule.originalScale += deltaScale;
}
}
return true;
}
/// <summary>
/// 删除父元素下所有与当前选中元素类型相同的其他元素,最后删除当前选中元素
/// </summary>
public static void DelSameInParent()
{
Type type = inspector.connectedGameElement.GetType();
for (int i = inspector.connectedGameElement.parentElement.childElementList.Count - 1; i >= 0; i--)
{
GameElement element = inspector.connectedGameElement.parentElement.childElementList[i];
if (element.GetType() == type && element != inspector.connectedGameElement)
{
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElement(element);
}
}
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElement(inspector.connectedGameElement);
}
/// <summary>
/// 将选中轨道下所有音符附着到最近的 Trail 上
/// </summary>
public static void AttachNoteInNearestTrail()
{
Track track = inspector.connectedGameElement as Track;
if (track == null)
{
LogWindow.Log("Please select a Track first!", Color.red);
return;
}
List<NoteBase> noteBases = track.childElementList.OfType<NoteBase>().ToList();
List<IHaveTrail> trails = track.GetAllGameElementsFromThis().OfType<IHaveTrail>().ToList();
if (trails.Count == 0)
{
LogWindow.Log("The Track has no Trail!", Color.red);
return;
}
foreach (var note in noteBases)
{
IHaveTrail nearestTrail = null;
Vector3 FinalPos = Vector3.positiveInfinity;
foreach (var trail in trails)
{
if (trail is IHaveTransformSubmodule haveTransform && trail is GameElement gameElement)
{
Vector3 pos = haveTransform.transformSubmodule.originalPosition;
GameElement gameElement1 = gameElement;
while (gameElement1 != track)
{
if (gameElement1 is not IHaveTransformSubmodule)
{
gameElement1 = gameElement1.parentElement;
continue;
}
List<Displacement> animationBases = gameElement1.childElementList.OfType<Displacement>().ToList();
foreach (var displacement in animationBases)
{
pos += displacement.getValue(note.exactJudgeTime);
}
gameElement1 = gameElement1.parentElement;
}
if (Vector3.Distance(pos, (note.noteVisual.submoduleList.First(i => i is TransformSubmodule) as TransformSubmodule).originalPosition) <=
Vector3.Distance(FinalPos, (note.noteVisual.submoduleList.First(i => i is TransformSubmodule) as TransformSubmodule).originalPosition))
{
nearestTrail = trail;
FinalPos = pos;
}
}
}
if (nearestTrail != null)
{
(note.noteVisual.submoduleList.First(i => i is TransformSubmodule) as TransformSubmodule).originalPosition = FinalPos;
note.Refresh();
(note.noteVisual.submoduleList.First(i => i is TransformSubmodule) as TransformSubmodule).Refresh();//捏妈妈滴为什么notevisual的TransformSubmodule不刷新
}
}
}
/// <summary>
/// 调整路径节点的 Z 坐标(可用于多轨道)
/// </summary>
public static void AdjustPathnodeZ(float OriginZpoint, float scale)
{
if (inspector.connectedGameElement == null)
{
LogWindow.Log($"please select a element (folder or track)", Color.red);
return;
}
if (inspector.connectedGameElement.GetType() != typeof(Track))
{
foreach (var i in inspector.connectedGameElement.childElementList.OfType<Track>())
{
inspector.connectedGameElement = i;
AdjustPathnodeZ(OriginZpoint, scale);
}
return;
}
Track track = (Track)inspector.connectedGameElement;
var pathnodes = track.trackPathSubmodule.pathNodeList;
foreach (var pathnode in pathnodes)
{
if (pathnode.childElementList.OfType<Displacement>().Count() > 0)
{
LogWindow.Log($"PathNode {pathnode.elementName} has Displacement, which may cause issues", Color.yellow);
}
float worldZ = pathnode.transform.position.z;
float deltaZ = worldZ - OriginZpoint;
float newZ = OriginZpoint + deltaZ * scale;
pathnode.transform.position = new Vector3(pathnode.transform.position.x, pathnode.transform.position.y, newZ);
pathnode.transformSubmodule.originalPosition = pathnode.transform.localPosition;
pathnode.transformSubmodule.Refresh();
pathnode.Refresh();
}
foreach (var i in track.childElementList.OfType<Track>())
{
inspector.connectedGameElement = i;
AdjustPathnodeZ(OriginZpoint, scale);
}
}
/// <summary>
/// 将 Hold Note 拆分为具有路径节点的 Track
/// </summary>
public static void SplitHoldToTrack(int PathnodesCount)
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Hold))
{
LogWindow.Log("Please select a Hold first!");
return;
}
Hold hold = (Hold)inspector.connectedGameElement;
Track parentTrack = hold.parentElement as Track;
if (parentTrack == null || parentTrack.trackTimeSubmodule is not TrackTimeSubmoduleMovable)
{
LogWindow.Log("Track Illegal (Only Movable)", Color.red);
return;
}
TrackTimeSubmoduleMovable trackTimeSubmoduleMovable = hold.track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
if (PathnodesCount < 2)
{
LogWindow.Log("PathnodesCount must be greater than 1!", Color.red);
return;
}
float startTime = hold.exactJudgeTime;
float endTime = hold.holdEndTime;
float interval = 1f / (PathnodesCount - 1);
hold.UpdateNoteInMovableTrack(CoreServices.TimeProvider.SongTime);
Vector3 HoldStartPos = default;
Vector3 HoldEndPos = default;
if (hold.noteVisual is DTMNoteVisualHold dTMNoteVisualHold)
{
dTMNoteVisualHold.headPoint.SetPercent(trackTimeSubmoduleMovable.GetTrackPercent(hold.exactJudgeTime));
dTMNoteVisualHold.tailPoint.SetPercent(trackTimeSubmoduleMovable.GetTrackPercent(hold.holdEndTime));
HoldStartPos = dTMNoteVisualHold.headPoint.transform.position;
HoldEndPos = dTMNoteVisualHold.tailPoint.transform.position;
}
else
{
LogWindow.Log("The selected Hold's NoteVisual is not DTMNoteVisualHold!", Color.red);
return;
}
if (hold.track.trackPathSubmodule.pathNodeList.Count > 2)
{
LogWindow.Log("the Hold may not be split currently", Color.yellow);
}
hold.UpdateNoteInMovableTrack(CoreServices.TimeProvider.SongTime);
Track NewTrack = Track.GenerateElement(hold.elementName + "_SplitTrack", Guid.NewGuid(), new List<string>(), true, parentTrack);
new TrackTimeSubmoduleMovable(NewTrack, startTime, endTime, 1, AnimationCurveType.Linear);
for (int i = 0; i < PathnodesCount; i++)
{
PathNode j = PathNode.GenerateElement("PathNode" + i.ToString(), Guid.NewGuid(), new List<string>(), true, NewTrack, true);
j.transform.position = Vector3.Lerp(HoldStartPos, HoldEndPos, i * interval);
j.transformSubmodule.originalPosition = j.transform.localPosition;
}
EditorManager.instance.operationManager.CopyPasteDeleteModule.CopyElement(hold);
EditorManager.instance.operationManager.CopyPasteDeleteModule.PasteElement(NewTrack);
EditorManager.instance.operationManager.CopyPasteDeleteModule.DeleteElement(hold);
Hold newHold = NewTrack.childElementList.OfType<Hold>().First();
newHold.noteVisual.transformSubmodule.originalPosition = Vector3.zero;
NewTrack.Refresh();
Observable.Timer(TimeSpan.FromSeconds(0.3f)).Subscribe(_ =>
{
NewTrack?.trackPathSubmodule.path.RebuildImmediate(true, true);
DTMNoteVisualHold dTMNoteVisualHold = newHold.noteVisual as DTMNoteVisualHold;
dTMNoteVisualHold.meshGenerator.Rebuild();
});
}
/// <summary>
/// 将选中轨道下所有 Hold Note 拆分为具有路径节点的 Track
/// </summary>
public static void SplitAllHoldToTrack(int PathnodesCount)
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Track))
{
LogWindow.Log("Please select a Track first!");
return;
}
Track track = (Track)inspector.connectedGameElement;
var holds = track.childElementList.OfType<Hold>().ToList();
if (track.trackPathSubmodule.pathNodeList.Count > 2)
{
LogWindow.Log("the Hold may not be split currently", Color.yellow);
}
foreach (var hold in holds)
{
inspector.connectedGameElement = hold;
SplitHoldToTrack(PathnodesCount);
}
}
public static void ScalePathNodesXY(float scale, Vector2 scalePoint)
{
if (inspector.connectedGameElement == null || (inspector.connectedGameElement.GetType() != typeof(Track) && inspector.connectedGameElement.GetType() != typeof(ElementFolder)))
{
LogWindow.Log("Please select a Folder or Track first!");
return;
}
foreach (var i in inspector.connectedGameElement.GetAllGameElementsFromThis().OfType<Track>())
{
Track track = (Track)i;
var pathnodes = track.trackPathSubmodule.pathNodeList;
foreach (var pathnode in pathnodes)
{
Vector2 dir = new Vector2(pathnode.transform.position.x, pathnode.transform.position.y) - scalePoint;
Vector3 newPos = new Vector3(dir.x * scale + scalePoint.x, dir.y * scale + scalePoint.y, pathnode.transform.position.z);
pathnode.transform.position = newPos;
pathnode.transformSubmodule.originalPosition = pathnode.transform.localPosition;
pathnode.transformSubmodule.Refresh();
pathnode.Refresh();
}
}
}
#endregion
#region Note Import/Export (/)
/// <summary>
/// 从正则格式文本导入音符数据
/// </summary>
public static void SamplerImport(string inputData)
{
if (!EditorManager.instance.useNotePrefab)
{
LogWindow.Log("Pleasee nable \"Note Prefab\" in EditorManager", Color.red);
return;
}
// 改进的正则表达式支持5个字段和负数
Regex dataPattern = new Regex(
@"\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([-+]?\d*\.?\d+)\s*,\s*([-+]?\d*\.?\d+)\s*(?:,\s*([-+]?\d*\.?\d+)\s*)?\)",
RegexOptions.Compiled
);
Debug.Log("===== =====");
MatchCollection matches = dataPattern.Matches(inputData);
Debug.Log($": {matches.Count}");
int recordCount = 1;
Track findTrack(string Findtext)
{
List<Track> tracks = EditorManager.instance.beatmapContainer.gameElementList.OfType<Track>().Where(i => i.elementName == Findtext).ToList();
if (tracks.Count == 0)
{
Debug.LogError($"未找到名为 {Findtext} 的轨道");
return null;
}
else if (tracks.Count > 1)
{
LogWindow.Log($"Repeat Track Of {Findtext}, please Cautious", Color.yellow);
}
return tracks[0];
}
foreach (Match match in matches)
{
if (match.Success)
{
string action = match.Groups[1].Value.Trim();
string id = match.Groups[2].Value.Trim();
// 解析公共字段
float timestamp = float.Parse(match.Groups[3].Value);
float value = float.Parse(match.Groups[4].Value);
// 处理Hold操作的特殊字段
float holdDuration = 0f;
bool isHold = false;
if (action == "Hold" && match.Groups[5].Success)
{
isHold = true;
holdDuration = float.Parse(match.Groups[5].Value);
}
// 构建输出信息
string logEntry = $"[记录#{recordCount++}] " +
$"Note: {action.PadRight(5)} | " +
$"ID: {id} | " +
$"时间: {timestamp.ToString("0.000").PadLeft(7)} | " +
$"X值: {value.ToString("0.000").PadLeft(7)}";
if (isHold)
{
logEntry += $" | 持续时间: {holdDuration.ToString("0.000")}";
}
Debug.Log(logEntry);
if (findTrack(id) is null)
{
Debug.LogError($"未找到名为 {id} 的轨道");
continue;
}
// 根据动作类型处理
switch (action)
{
case "Tap":
Tap a = Tap.GenerateElement("New Tap", Guid.NewGuid(), new List<string>(), true, findTrack(id), timestamp);
((TransformSubmodule)a.noteVisual.submoduleList.Where(i => i is TransformSubmodule)?.First()).originalPosition = new Vector3(value, 0, 0);
a.noteVisual.SetEditorSubmodules(); // 设置selset
a.Refresh();
break;
case "Stay":
Stay b = Stay.GenerateElement("New Stay", Guid.NewGuid(), new List<string>(), true, findTrack(id), timestamp);
((TransformSubmodule)b.noteVisual.submoduleList.Where(i => i is TransformSubmodule)?.First()).originalPosition = new Vector3(value, 0, 0);
b.noteVisual.SetEditorSubmodules(); // 设置selset
b.Refresh();
break;
case "Hold":
Hold c = Hold.GenerateElement("New Hold", Guid.NewGuid(), new List<string>(), true, findTrack(id), timestamp, timestamp + holdDuration);
((TransformSubmodule)c.noteVisual.submoduleList.Where(i => i is TransformSubmodule)?.First()).originalPosition = new Vector3(value, 0, 0);
c.noteVisual.SetEditorSubmodules(); // 设置selset
c.Refresh();
break;
case "Flick":
Flick d = Flick.GenerateElement("New Flick", Guid.NewGuid(), new List<string>(), true, findTrack(id), timestamp, new List<Vector2>());
((TransformSubmodule)d.noteVisual.submoduleList.Where(i => i is TransformSubmodule)?.First()).originalPosition = new Vector3(value, 0, 0);
d.noteVisual.SetEditorSubmodules(); // 设置selset
d.Refresh();
break;
default:
Debug.LogError($"未知类型: {action}");
break;
}
}
}
Debug.Log("===== =====");
}
/// <summary>
/// 导出选中轨道下所有音符为正则格式文本(复制到剪贴板)
/// </summary>
public static void ExportNotesFromTrack()
{
if (inspector.connectedGameElement == null || inspector.connectedGameElement.GetType() != typeof(Track))
{
LogWindow.Log("Please select a Track first!");
return;
}
Track track = (Track)inspector.connectedGameElement;
var notes = track.childElementList.OfType<NoteBase>().OrderBy(n => n.exactJudgeTime).ToList();
List<string> lines = new List<string>();
foreach (var note in notes)
{
string type = note switch
{
Tap => "Tap",
Stay => "Stay",
Hold => "Hold",
Flick => "Flick",
_ => "Unknown"
};
string id = track.elementName;
float time = note.exactJudgeTime;
float x = 0f;
// 获取X值
if (note.noteVisual.submoduleList.FirstOrDefault(i => i is TransformSubmodule) is TransformSubmodule ts)
{
x = ts.originalPosition.x;
}
if (note is Hold hold)
{
float duration = hold.holdEndTime - hold.exactJudgeTime;
lines.Add($"({type}, {id}, {time:0.###}, {x:0.###}, {duration:0.###})");
}
else
{
lines.Add($"({type}, {id}, {time:0.###}, {x:0.###})");
}
}
string result = string.Join("\n", lines);
Debug.Log(result);
//LogWindow.Log(result, Color.green);
// 复制到剪贴板
GUIUtility.systemCopyBuffer = result;
LogWindow.Log("Colped Done!", Color.green);
}
#endregion
#region Highlighting/Visuals (/)
/// <summary>
/// 在游戏内设置时间相同的音符高亮显示
/// </summary>
public static void SetNoteHLInGame(bool forceSetOff = false, bool SameTheme = false)
{
var noteBases = EditorManager.instance.beatmapContainer.gameElementList.OfType<NoteBase>().ToList();
// 先全部关闭高亮如果forceSetOff为true
if (forceSetOff)
{
foreach (var note in noteBases)
{
if (note.noteVisual != null)
{
note.noteVisual.isHighlighted = false;
try
{
note.noteVisual?.SetHighlight();
}
catch (Exception ex)
{
Debug.LogError($"Error setting highlight for note {note.name}: {ex.Message}");
}
}
}
}
// 按时间分组
var groups = SameTheme
? noteBases.GroupBy(n => (object)new { n.exactJudgeTime, Type = n.GetType() })
: noteBases.GroupBy(n => (object)n.exactJudgeTime);
foreach (var group in groups)
{
if (group.Count() > 1)
{
foreach (var note in group)
{
if (note.noteVisual != null)
{
note.noteVisual.isHighlighted = true;
try
{
note.noteVisual?.SetHighlight();
}
catch (Exception ex)
{
Debug.LogError($"Error setting highlight for note {note.name}: {ex.Message}");
}
}
}
}
}
}
/// <summary>
/// 在选中元素下设置时间相同的音符高亮显示
/// </summary>
public static void SetNoteHLInElement(bool forceSetOff = false, bool SameTheme = false)
{
var noteBases = inspector.connectedGameElement.GetAllGameElementsFromThis().OfType<NoteBase>().ToList();
// 先全部关闭高亮如果forceSetOff为true
if (forceSetOff)
{
foreach (var note in noteBases)
{
if (note.noteVisual != null)
{
note.noteVisual.isHighlighted = false;
try
{
note.noteVisual?.SetHighlight();
}
catch (Exception ex)
{
Debug.LogError($"Error setting highlight for note {note.name}: {ex.Message}");
}
}
}
}
// 按时间分组
var groups = SameTheme
? noteBases.GroupBy(n => (object)new { n.exactJudgeTime, Type = n.GetType() })
: noteBases.GroupBy(n => (object)n.exactJudgeTime);
foreach (var group in groups)
{
if (group.Count() > 1)
{
foreach (var note in group)
{
if (note.noteVisual != null)
{
note.noteVisual.isHighlighted = true;
try
{
note.noteVisual?.SetHighlight();
}
catch (Exception ex)
{
Debug.LogError($"Error setting highlight for note {note.name}: {ex.Message}");
}
}
}
}
}
}
public static void NoteScale(float scale)
{
var noteBases = inspector.connectedGameElement.GetAllGameElementsFromThis().OfType<NoteBase>().ToList();
foreach (var note in noteBases)
{
if (note.noteVisual != null)
{
if (note.noteVisual.submoduleList.FirstOrDefault(i => i is TransformSubmodule) is TransformSubmodule ts)
{
ts.originalScale = ts.originalScale * scale;
ts.Refresh();
}
note.noteVisual.Refresh();
}
}
}
#endregion
#region Animation ()
/// <summary>
/// 交换 Displacement 动画值的正负号
/// </summary>
public static void swapDisplacement()
{
Displacement displacement = inspector.connectedGameElement as Displacement;
if (displacement == null)
{
LogWindow.Log("Please select a Displacement first!", Color.red);
return;
}
foreach (var anim in displacement.positionX.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in displacement.positionY.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in displacement.positionZ.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
}
/// <summary>
/// 交换 Swirl 动画值的正负号
/// </summary>
public static void swapSwirl()
{
Swirl swirl = inspector.connectedGameElement as Swirl;
if (swirl == null)
{
LogWindow.Log("Please select a Swirl first!", Color.red);
return;
}
foreach (var anim in swirl.eulerAngleX.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in swirl.eulerAngleY.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in swirl.eulerAngleZ.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
}
/// <summary>
/// 交换 Scale 动画值的正负号
/// </summary>
public static void swapScale()
{
Scale scale = inspector.connectedGameElement as Scale;
if (scale == null)
{
LogWindow.Log("Please select a Scale first!", Color.red);
return;
}
foreach (var anim in scale.scaleX.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in scale.scaleY.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
foreach (var anim in scale.scaleZ.animations)
{
anim.endValue = -anim.endValue;
anim.startValue = -anim.startValue;
}
}
// public static void randomAnimationValue(float range)
// {
// AnimationBase animationBase = inspector.connectedGameElement as AnimationBase;
// if (animationBase == null)
// {
// LogWindow.Log("Please select a AnimationBase first!", Color.red);
// return;
// }
// System.Random random = new System.Random();
// foreach (var property in animationBase.GetType().GetProperties())
// {
// if (property.PropertyType == typeof(FlexibleFloat))
// {
// FlexibleFloat ff = property.GetValue(animationBase) as FlexibleFloat;
// if (ff != null)
// {
// foreach (var anim in ff.animations)
// {
// float randomOffset = (float)(random.NextDouble() * 2 - 1) * range;
// anim.endValue += randomOffset;
// }
// }
// }
// }
// }
#endregion
#region Global Utilities ()
/// <summary>
/// 确保所有动画的起始时间为 0 时有关键帧
/// </summary>
public static void FloorAnim()
{
if (inspector.connectedGameElement == null)
{
LogWindow.Log("Please select a Element first!");
return;
}
List<AnimationBase> elements = inspector.connectedGameElement.GetAllGameElementsFromThis().OfType<AnimationBase>().ToList();
// 预先缓存属性信息(如果在循环外部知道具体类型)
var propertiesToCheck = typeof(GameElement).GetProperties()
.Where(p => p.PropertyType == typeof(FlexibleFloat))
.ToArray();
foreach (var element in elements)
{
bool needsRefresh = false;
foreach (var prop in propertiesToCheck)
{
var ff = prop.GetValue(element) as FlexibleFloat;
if (ff?.animations?.Count > 0 && ff.animations[0] != null)
{
var firstAnimation = ff.animations[0];
if (firstAnimation.startTime > 0)
{
ff.animations.Insert(0, new AnimatedFloat(
0,
Math.Min(firstAnimation.startTime, 1),
firstAnimation.startValue,
firstAnimation.startValue,
AnimationCurveType.Linear
));
needsRefresh = true;
Debug.Log($"Added 0 keyframe to {element.elementName}'s {prop.Name}");
}
}
}
if (needsRefresh)
{
element.Refresh();
element.animatedObject?.Refresh(); // 使用空条件运算符
}
}
}
/// <summary>
/// 全局刷新所有元素和轨道路径
/// </summary>
public static void Rebuild()
{
foreach (GameElement element in EditorManager.instance.beatmapContainer.gameElementList)
{
foreach (GameElement e in element.GetAllGameElementsFromThis())
{
if (e != null) e.Refresh();
}
foreach (Track track in element.GetAllGameElementsFromThis().OfType<Track>())
{
track?.trackPathSubmodule.path.RebuildImmediate(true, true);
}
}
#if UNITY_EDITOR
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
#endif
}
public static void TrybakeAllMesh()
{
EditorManager.instance.beatmapContainer.gameElementList.OfType<Track>().ToList().ForEach(
track =>
{
if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable ||
track.trackRendererSubmodule is TrackRendererSubmoduleAutoOrient ||
track.childElementList.OfType<Scale>().Count() > 0 ||
(track.trackPathSubmodule.pathNodeList.Any(o => o.childElementList.OfType<AnimationBase>().Count() == 0) == false))
{
Debug.LogWarning("TrackRendererSubmodule.getMeshFromGenerator: Track has animation, cannot get mesh.");
}
else
{
track.Refresh();
track.trackRendererSubmodule?.meshGenerator.Bake(true, false);
track.Refresh();
}
}
);
}
/// <summary>
/// 添加 Tag Matcher 到 TagManager
/// </summary>
public static void AddTagMatcher(string name, string parameterName)
{
var tagManager = EditorManager.instance.projectInformation.tagManager;
IBaseElement element = inspector.connectedGameElement;
tagManager.AddTagMatcher(name, element.GetType(), parameterName);
}
/// <summary>
/// 查找元素中匹配给定值的参数名称
/// </summary>
public static void FindParameterName(object value)
{
// 假设 inspector.connectedGameElement 存在且是你希望搜索的对象
var element = inspector.connectedGameElement;
if (element == null || value == null) return;
string o = FindParameterName(element, value);
if (!string.IsNullOrEmpty(o)) LogWindow.Log(o);
foreach (var i in element.GetType().GetProperties(DeepMatchFlags))
{
if (i == null) continue;
var propertyInfo = element.GetType().GetProperty(i.Name);
if (propertyInfo == null) continue;
var propertyValue = propertyInfo.GetValue(element);
var p = FindParameterName(propertyValue as IBaseElement, value);
if (!string.IsNullOrEmpty(p))
LogWindow.Log($"{i.Name}.{p}");
}
}
public static string FindParameterName(IBaseElement element, object value)
{
// 假设 inspector.connectedGameElement 存在且是你希望搜索的对象
if (element == null || value == null) return "";
// 1. 获取目标类型
Type targetType = value.GetType();
// 如果是 Nullable<T>,获取其基础类型
if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
targetType = Nullable.GetUnderlyingType(targetType);
}
// 2. 遍历 GameElement 的所有字段
foreach (var field in element.GetType().GetFields(DeepMatchFlags))
{
// 检查字段类型是否匹配
if (field.FieldType == targetType)
{
// 获取字段的值 (作为 object)
object fieldValue = field.GetValue(element);
if (IsValueMatch(fieldValue, value, field.FieldType))
{
Debug.Log($"Found parameter: {field.Name} (Type: {targetType.Name})");
// 假设 LogWindow 可用
return field.Name;
}
}
}
return "";
}
/// <summary>
/// 同步标签元素
/// </summary>
public static void SyncTagedElement()
{
var q = inspector.connectedGameElement;
var tagManager = EditorManager.instance.projectInformation.tagManager;
tagManager.SyncTagedElement((q));
}
#endregion
#region Internal Helpers ()
private const BindingFlags DeepMatchFlags = BindingFlags.Instance | BindingFlags.Public;
private const float FloatEpsilon = 0.0001f; // 定义浮点数容差
/// <summary>
/// 比较两个值是否匹配,对浮点数使用容差比较
/// </summary>
private static bool IsValueMatch(object fieldVal, object targetVal, Type t)
{
if (fieldVal == null || targetVal == null)
{
return fieldVal == targetVal;
}
// 1. 类型是 float
if (t == typeof(float))
{
// 必须进行类型转换
float f1 = (float)fieldVal;
float f2 = (float)targetVal;
return Mathf.Abs(f1 - f2) < FloatEpsilon;
}
// 2. 类型是 Vector3
if (t == typeof(Vector3))
{
Vector3 v1 = (Vector3)fieldVal;
Vector3 v2 = (Vector3)targetVal;
// 使用 Unity 的 Distance 容差比较
return Vector3.Distance(v1, v2) < FloatEpsilon;
}
// 3. 类型是 Color
if (t == typeof(Color))
{
Color c1 = (Color)fieldVal;
Color c2 = (Color)targetVal;
// 手动解构 Color 的四个 float 分量,使用容差
return Mathf.Abs(c1.r - c2.r) < FloatEpsilon &&
Mathf.Abs(c1.g - c2.g) < FloatEpsilon &&
Mathf.Abs(c1.b - c2.b) < FloatEpsilon &&
Mathf.Abs(c1.a - c2.a) < FloatEpsilon;
}
// 4. 其他包含 float 的复杂 Struct/Class 的浅层解构
if (!t.IsPrimitive && t != typeof(string) && (t.IsClass || t.IsValueType))
{
// 如果字段值是复杂类型,并且我们想比较它的内部 float 字段 (浅层)
// 这里只能进行浅层比较,因为深层递归需要辅助函数来管理栈。
// 遍历所有顶层字段,检查是否有 float
foreach (var field in t.GetFields(DeepMatchFlags))
{
object fieldValMember = field.GetValue(fieldVal);
object targetValMember = field.GetValue(targetVal);
if (field.FieldType == typeof(float))
{
// 对 float 成员进行容差比较
if (fieldValMember != null && targetValMember != null)
{
float f1 = (float)fieldValMember;
float f2 = (float)targetValMember;
if (Mathf.Abs(f1 - f2) >= FloatEpsilon)
{
return false; // 找到不匹配的 float
}
}
else if (fieldValMember != targetValMember) // 检查是否一个为 null另一个不为 null
{
return false;
}
}
else
{
// 对其他类型使用默认 Equals
if (!fieldValMember.Equals(targetValMember))
{
return false;
}
}
}
// 所有字段都匹配
return true;
}
// 5. 默认路径对于所有其他类型int, string, bool, etc.),使用默认的 Equals 比较
return fieldVal.Equals(targetVal);
}
#endregion
#region NodeScript Console Commands ()
/// <summary>
/// 新建 NodeScript 编辑器,从当前选中 GameElement 开始 Init
/// </summary>
public static void newNode()
{
if (NodeManager.Instance == null)
{
var prefab = EditorManager.instance.basePrefabs.NodeEditor;
if (prefab == null)
{
LogWindow.Log("NodeEditor prefab is null, check BasePrefabsCollection!", Color.red);
return;
}
var go = UnityEngine.Object.Instantiate(prefab,
EditorManager.instance.uiManager.WindowsCanvas.gameObject.transform);
var mgr = go.GetComponent<NodeManager>();
if (mgr != null && EditorManager.instance.operationManager.currentSelectedElements.Count > 0)
mgr.Init(EditorManager.instance.operationManager.currentSelectedElements[0]);
LogWindow.Log("NodeScript Editor created.", Color.green);
}
else
{
UnityEngine.Object.Destroy(NodeManager.Instance.gameObject);
LogWindow.Log("NodeScript Editor destroyed.", Color.yellow);
}
}
/// <summary>
/// 另存为指定文件名到 StreamingAssets/NodeScript/{name}.json
/// </summary>
public static void saveNode(string name)
{
if (NodeManager.Instance == null)
{
LogWindow.Log("No NodeScript Editor active. Use newNode first.", Color.red);
return;
}
NodeManager.Instance.SaveToFile(name);
LogWindow.Log($"NodeScript graph saved as {name}.", Color.green);
}
/// <summary>
/// 从 StreamingAssets/NodeScript/{name}.json 读取 NodeScript 图
/// </summary>
public static void loadNode(string name)
{
if (string.IsNullOrEmpty(name))
{
LogWindow.Log("Usage: loadNode <name> — loads StreamingAssets/NodeScript/<name>.json", Color.yellow);
return;
}
// 确保 NodeManager 存在
if (NodeManager.Instance == null)
{
var prefab = EditorManager.instance.basePrefabs.NodeEditor;
if (prefab == null)
{
LogWindow.Log("NodeEditor prefab is null, check BasePrefabsCollection!", Color.red);
return;
}
UnityEngine.Object.Instantiate(prefab,
EditorManager.instance.uiManager.WindowsCanvas.gameObject.transform);
}
NodeManager.Instance.LoadFromFile(name);
LogWindow.Log($"NodeScript graph loaded from {name}.", Color.green);
}
}
}
#endregion