diff --git a/zzio/EnumerableExtensions.cs b/zzio/EnumerableExtensions.cs index cfaa48ce..131930ee 100644 --- a/zzio/EnumerableExtensions.cs +++ b/zzio/EnumerableExtensions.cs @@ -112,6 +112,11 @@ public static Range Sub(this Range full, Range sub, int maxValue = int.MaxValue) return newOffset..(newOffset + subLength); } + public static Range Suffix(this Range range, int suffixLength) => + range.End.IsFromEnd + ? range.End..(range.End.Value - suffixLength) + : range.End..(range.End.Value + suffixLength); + public static IEnumerable PrefixSums( this IEnumerable set, TOutput first, Func next) { diff --git a/zzio/primitives/IColor.cs b/zzio/primitives/IColor.cs index 9425c28b..10ce3876 100644 --- a/zzio/primitives/IColor.cs +++ b/zzio/primitives/IColor.cs @@ -7,7 +7,7 @@ public struct IColor { public byte r, g, b, a; - public IColor(byte r, byte g, byte b, byte a) + public IColor(byte r, byte g, byte b, byte a = 255) { this.r = r; this.g = g; @@ -53,6 +53,7 @@ public void Write(BinaryWriter w) public static implicit operator FColor(IColor c) => new(c.r / 255f, c.g / 255f, c.b / 255f, c.a / 255f); public static readonly IColor White = new(0xFFFFFFFF); + public static readonly IColor Grey = new(0xFF808080); public static readonly IColor Black = new(0xFF000000); public static readonly IColor Clear = new(0x00000000); public static readonly IColor Red = new(0xFF0000FF); diff --git a/zzio/scn/WaypointSystem.cs b/zzio/scn/WaypointSystem.cs index b80e0ba6..6c79e385 100644 --- a/zzio/scn/WaypointSystem.cs +++ b/zzio/scn/WaypointSystem.cs @@ -1,139 +1,115 @@ using System; using System.IO; -using System.Diagnostics; using System.Numerics; +using System.Collections.Generic; // This surely needs some reverse engineering work being done... namespace zzio.scn; [Serializable] -public struct WaypointInnerData +public struct Waypoint { - public uint iiv2; - public uint[] data; -} - -[Serializable] -public struct WaypointData -{ - public uint ii1, ii1ext, iiv2; - public Vector3 v1; - public uint[] innerdata1, innerdata2; - public uint[] inner3data1; + public uint Id, Group; + public Vector3 Position; + public uint[] WalkableIds, JumpableIds; + public uint[]? VisibleIds; } [Serializable] public class WaypointSystem : ISceneSection { - public uint version; - public byte[] data = new byte[0x18]; - public WaypointData[] waypointData = []; - public WaypointInnerData[] inner2data1 = []; + public uint Version; + public byte[] Data = new byte[0x18]; // most likely just for generation? AFAIK not used in game + public Waypoint[] Waypoints = []; + public Dictionary CompatibleGroups = []; public void Read(Stream stream) { using BinaryReader reader = new(stream); - version = reader.ReadUInt32(); - uint mustBeZero = reader.ReadUInt32(); - Debug.Assert(mustBeZero == 0); + Version = reader.ReadUInt32(); + if (reader.ReadUInt32() != 0) + throw new InvalidDataException("Waypoint system start magic is not correct"); - if (version >= 5) - data = reader.ReadBytes(0x18); + if (Version >= 5) + Data = reader.ReadBytes(0x18); uint count1 = reader.ReadUInt32(); - WaypointData[] d = new WaypointData[count1]; + Waypoint[] d = new Waypoint[count1]; for (uint i = 0; i < count1; i++) { - d[i].ii1 = reader.ReadUInt32(); - if (version >= 4) - d[i].ii1ext = reader.ReadUInt32(); - d[i].v1 = reader.ReadVector3(); - - uint ci1 = reader.ReadUInt32(); - d[i].innerdata1 = new uint[ci1]; - for (uint j = 0; j < ci1; j++) - d[i].innerdata1[j] = reader.ReadUInt32(); + d[i].Id = reader.ReadUInt32(); + if (Version >= 4) + d[i].Group = reader.ReadUInt32(); + d[i].Position = reader.ReadVector3(); - uint ci2 = reader.ReadUInt32(); - d[i].innerdata2 = new uint[ci2]; - for (uint j = 0; j < ci2; j++) - d[i].innerdata2[j] = reader.ReadUInt32(); + d[i].WalkableIds = reader.ReadStructureArray(reader.ReadInt32(), 4); + d[i].JumpableIds = reader.ReadStructureArray(reader.ReadInt32(), 4); } - if (version >= 2) + if (Version >= 2) { - uint count2 = reader.ReadUInt32(); - inner2data1 = new WaypointInnerData[count2]; - for (uint j = 0; j < count2; j++) + int groupCount = reader.ReadInt32(); + CompatibleGroups = new(groupCount); + for (int j = 0; j < groupCount; j++) { - inner2data1[j].iiv2 = reader.ReadUInt32(); - uint ci3 = reader.ReadUInt32(); - inner2data1[j].data = new uint[ci3]; - for (uint k = 0; k < ci3; k++) - inner2data1[j].data[k] = reader.ReadUInt32(); + CompatibleGroups.Add( + reader.ReadUInt32(), + reader.ReadStructureArray(reader.ReadInt32(), 4)); } } - if (version >= 3) + if (Version >= 3) { for (uint j = 0; j < count1; j++) - { - uint ci4 = reader.ReadUInt32(); - d[j].inner3data1 = new uint[ci4]; - for (uint k = 0; k < ci4; k++) - d[j].inner3data1[k] = reader.ReadUInt32(); - } + d[j].VisibleIds = reader.ReadStructureArray(reader.ReadInt32(), 4); } - waypointData = d; + Waypoints = d; - uint mustBeFFFF = reader.ReadUInt32(); - Debug.Assert(mustBeFFFF == 0xffff); + if (reader.ReadUInt32() != 0xffff) + throw new InvalidDataException("Waypoint system end magic is not correct"); } public void Write(Stream stream) { using BinaryWriter writer = new(stream); - writer.Write(version); + writer.Write(Version); writer.Write(0); - if (version >= 5) - writer.Write(data, 0, 0x18); - WaypointData[] d = waypointData; + if (Version >= 5) + writer.Write(Data, 0, 0x18); + Waypoint[] d = Waypoints; writer.Write(d.Length); for (int i = 0; i < d.Length; i++) { - writer.Write(d[i].ii1); - if (version >= 4) - writer.Write(d[i].ii1ext); - writer.Write(d[i].v1); - - writer.Write(d[i].innerdata1.Length); - for (int j = 0; j < d[i].innerdata1.Length; j++) - writer.Write(d[i].innerdata1[j]); - - writer.Write(d[i].innerdata2.Length); - for (int j = 0; j < d[i].innerdata2.Length; j++) - writer.Write(d[i].innerdata2[j]); + writer.Write(d[i].Id); + if (Version >= 4) + writer.Write(d[i].Group); + writer.Write(d[i].Position); + + writer.Write(d[i].WalkableIds.Length); + for (int j = 0; j < d[i].WalkableIds.Length; j++) + writer.Write(d[i].WalkableIds[j]); + + writer.Write(d[i].JumpableIds.Length); + for (int j = 0; j < d[i].JumpableIds.Length; j++) + writer.Write(d[i].JumpableIds[j]); } - if (version >= 2) + if (Version >= 2) { - WaypointInnerData[] d2 = inner2data1; - writer.Write(d2.Length); - for (int i = 0; i < d2.Length; i++) + writer.Write(CompatibleGroups.Count); + foreach (var (groupId, wpIds) in CompatibleGroups) { - writer.Write(d2[i].iiv2); - writer.Write(d2[i].data.Length); - for (int j = 0; j < d2[i].data.Length; j++) - writer.Write(d2[i].data[j]); + writer.Write(groupId); + writer.WriteStructureArray(wpIds, 4); } } - if (version >= 3) + if (Version >= 3) { for (int i = 0; i < d.Length; i++) { - writer.Write(d[i].inner3data1.Length); - for (int j = 0; j < d[i].inner3data1.Length; j++) - writer.Write(d[i].inner3data1[j]); + var links = d[i].VisibleIds ?? []; + writer.Write(links.Length); + writer.WriteStructureArray(links, 4); } } diff --git a/zzio/utils/BinaryIOExtension.cs b/zzio/utils/BinaryIOExtension.cs index f1b37e34..a3d22533 100644 --- a/zzio/utils/BinaryIOExtension.cs +++ b/zzio/utils/BinaryIOExtension.cs @@ -54,6 +54,7 @@ public static unsafe void ReadStructureArray(this BinaryReader reader, T[] ar public static T[] ReadStructureArray(this BinaryReader reader, int count, int expectedSizeOfElement) where T : unmanaged { + ArgumentOutOfRangeException.ThrowIfNegative(count); var array = new T[count]; reader.ReadStructureArray(array, expectedSizeOfElement); return array; diff --git a/zzre.core/imgui/MenuBar.cs b/zzre.core/imgui/MenuBar.cs index 593e3381..1951012e 100644 --- a/zzre.core/imgui/MenuBar.cs +++ b/zzre.core/imgui/MenuBar.cs @@ -80,6 +80,24 @@ public void AddRadio(string path, IReadOnlyList labels, GetRefValueFunc< EndMenu(); }); + public void AddRadio(string path, GetRefValueFunc getValue, Action? onChanged = null) + where TEnum : struct, Enum => AddItem(path, name => + { + if (!BeginMenu(name)) + return; + ref var curValue = ref getValue(); + var values = Enum.GetValues(); + foreach (var value in values) + { + if (MenuItem(value.ToString(), "", curValue.GetHashCode() == value.GetHashCode())) + { + curValue = value; + onChanged?.Invoke(); + } + } + EndMenu(); + }); + public void AddSlider(string path, float minVal, float maxVal, GetRefValueFunc getValue, Action? onChanged = null) => AddItem(path, name => { if (SliderFloat(name, ref getValue(), minVal, maxVal)) diff --git a/zzre.core/math/Frustum.cs b/zzre.core/math/Frustum.cs index dd2bbdfd..fb608963 100644 --- a/zzre.core/math/Frustum.cs +++ b/zzre.core/math/Frustum.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; namespace zzre; diff --git a/zzre.core/math/MathEx.cs b/zzre.core/math/MathEx.cs index 76ba8cae..66c0bbf6 100644 --- a/zzre.core/math/MathEx.cs +++ b/zzre.core/math/MathEx.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; +using zzio; namespace zzre; @@ -131,4 +132,24 @@ public static Vector3 HorizontalSlerp(Vector3 from, Vector3 to, float curvature, from.Y, MathF.Cos(newAngle)); } + + public const float GoldenRatioFract = 0.61803398875f; + public const float GoldenRatio = 1.61803398875f; + + public static IEnumerable GoldenRatioSequence(float acc = 0f) + { + while (true) + { + yield return acc; + acc = (acc + GoldenRatioFract) % 1f; + } + } + + public static IEnumerable GoldenRatioColors( + float hueStart = 0f, + float saturation = 1f, + float luminosity = 0.5f, + float alpha = 1f) => + GoldenRatioSequence(hueStart) + .Select(hue => new FColor(hue, saturation, luminosity, alpha).HSLToRGB()); } diff --git a/zzre.core/math/ZZIOExtensions.cs b/zzre.core/math/ZZIOExtensions.cs index 3aed469d..01248009 100644 --- a/zzre.core/math/ZZIOExtensions.cs +++ b/zzre.core/math/ZZIOExtensions.cs @@ -3,6 +3,8 @@ using Veldrid; using zzio; using zzio.rwbs; +using static System.MathF; +using static zzre.MathEx; namespace zzre; @@ -41,4 +43,69 @@ public static Vector3 ToZZRotationVector(this Quaternion rotation) => CollisionSectorType.Z => Vector3.UnitZ, _ => throw new ArgumentOutOfRangeException($"Unknown collision sector type {sectorType}") }; + + // adapted from https://gist.github.com/ciembor/1494530 - "It's the do what you want license :)" + + public static FColor RGBToHSL(this FColor c) + { + float max = Max(Max(c.r, c.g), c.b); + float min = Min(Min(c.r, c.g), c.b); + float mid = (max + min) / 2; + + FColor result = new(mid, mid, mid, c.a); + if (Cmp(max, min)) + result.r = result.g = 0f; // achromatic + else + { + float d = max - min; + result.g = (result.b > 0.5) ? d / (2 - max - min) : d / (max + min); + + if (Cmp(max, c.r)) + { + result.r = (c.g - c.b) / d + (c.g < c.b ? 6 : 0); + } + else if (Cmp(max, c.g)) + { + result.r = (c.b - c.r) / d + 2; + } + else if (Cmp(max, c.b)) + { + result.r = (c.r - c.g) / d + 4; + } + + result.r /= 6; + } + return result; + } + + private static float HueToRGB(float p, float q, float t) + { + if (t < 0) + t += 1; + if (t > 1) + t -= 1; + return t switch + { + < 1f / 6 => p + (q - p) * 6 * t, + < 1f / 2 => q, + < 2f / 3 => p + (q - p) * (2f / 3 - t) * 6, + _ => p + }; + } + + public static FColor HSLToRGB(this FColor c) + { + if (CmpZero(c.g)) + return new(c.b, c.b, c.b, c.a); + + float q = c.b < 0.5f + ? c.b * (1 + c.g) + : c.b + c.g - c.b * c.g; + float p = 2 * c.b - q; + return new( + HueToRGB(p, q, c.r + 1f / 3), + HueToRGB(p, q, c.r), + HueToRGB(p, q, c.r - 1f / 3), + c.a); + } } diff --git a/zzre/debug/DebugLineRenderer.cs b/zzre/debug/DebugLineRenderer.cs index e96c4875..ccd640bc 100644 --- a/zzre/debug/DebugLineRenderer.cs +++ b/zzre/debug/DebugLineRenderer.cs @@ -37,18 +37,29 @@ protected override void DisposeManaged() mesh.Dispose(); } - public void Render(CommandList cl) + public void Render(CommandList cl) => Render(cl, ..); + + public void Render(CommandList cl, Range range) { - if (mesh.VertexCount == 0) + var (offset, count) = range.GetOffsetAndLength(Count); + if (count == 0) return; mesh.Update(cl); (Material as IMaterial).Apply(cl); Material.ApplyAttributes(cl, mesh); - cl.Draw((uint)mesh.VertexCount); + cl.Draw((uint)count * 2, 1, (uint)offset * 2, 0); } public void Clear() => mesh.Clear(); + public void Reserve(int additional) + { + var vertices = mesh.RentVertices(additional * 2); + mesh.AttrPos.Read(vertices); + mesh.AttrColor.Read(vertices); + mesh.ReturnVertices(vertices); + } + public void Add(IColor color, Vector3 start, Vector3 end) => Add(color, new Line(start, end)); public void Add(IColor color, params Line[] lines) => Add(color, lines as IEnumerable); public void Add(IColor color, IEnumerable lines) @@ -63,6 +74,19 @@ public void Add(IColor color, IEnumerable lines) } } + public void AddCross(IColor color, Vector3 center, float radius) + { + var range = mesh.RentVertices(6); + mesh.AttrColor.Write(range).Fill(color); + var index = range.Start.Value; + mesh.AttrPos[index++] = center - Vector3.UnitX * radius; + mesh.AttrPos[index++] = center + Vector3.UnitX * radius; + mesh.AttrPos[index++] = center - Vector3.UnitY * radius; + mesh.AttrPos[index++] = center + Vector3.UnitY * radius; + mesh.AttrPos[index++] = center - Vector3.UnitZ * radius; + mesh.AttrPos[index++] = center + Vector3.UnitZ * radius; + } + public void AddTriangles(IColor color, IEnumerable triangles) { Add(color, triangles.SelectMany(t => t.Edges())); diff --git a/zzre/materials/DebugMaterial.cs b/zzre/materials/DebugMaterial.cs index d00203df..cebb854f 100644 --- a/zzre/materials/DebugMaterial.cs +++ b/zzre/materials/DebugMaterial.cs @@ -44,6 +44,7 @@ public enum TopologyMode : uint public ColorMode Color { set => SetOption(nameof(ColorMode), (uint)value); } public TopologyMode Topology { set => SetOption(nameof(Topology), (uint)value); } public bool BothSided { set => SetOption(nameof(BothSided), value); } + public bool DepthTest { set => SetOption(nameof(DepthTest), value); } public UniformBinding Projection { get; } public UniformBinding View { get; } diff --git a/zzre/shaders/debug.mlang b/zzre/shaders/debug.mlang index 88ed4675..5b0dad25 100644 --- a/zzre/shaders/debug.mlang +++ b/zzre/shaders/debug.mlang @@ -2,6 +2,7 @@ option IsSkinned; option ColorMode = VertexColor, SkinWeights, SingleBoneWeight; option Topology = Triangles, Lines; option BothSided; +option DepthTest; variants exclude if (ColorMode != VertexColor && IsSkinned); variants exclude if (Topology == Lines && BothSided); @@ -43,6 +44,12 @@ pipeline if (BothSided) cull none; } +pipeline if (DepthTest) +{ + DepthTest On; + DepthWrite On; +} + vec4 GetDebugColorOf(uint index) { vec4 color = vec4(0, 0, 0, 1); diff --git a/zzre/tools/sceneeditor/SceneEditor.Waypoints.cs b/zzre/tools/sceneeditor/SceneEditor.Waypoints.cs new file mode 100644 index 00000000..ef4a7b26 --- /dev/null +++ b/zzre/tools/sceneeditor/SceneEditor.Waypoints.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Veldrid; +using zzio; +using zzio.scn; +using zzre.imgui; +using zzre.materials; +using zzre.rendering; +using static ImGuiNET.ImGui; +using static zzre.imgui.ImGuiEx; + +namespace zzre.tools; + +public partial class SceneEditor +{ + private sealed class WaypointComponent : BaseDisposable + { + private const float WaypointCrossSize = 0.1f; + private const float WaypointSphereSize = 0.2f; + + private enum Selection + { + None, + Waypoint, + Group + } + + private enum EdgeVisibility + { + None, + Traversable, + Visibility + } + + private readonly DebugLineRenderer lineRenderer; + private readonly SceneEditor editor; + private readonly Dictionary<(int, int), bool> walkableLinks = [], jumpableLinks = []; + private readonly Dictionary idToIndex = []; + private readonly Dictionary groupColors = []; + + private Range rangePoints, rangeTraversableEdges; + private bool + showPoints = true, + showTraversableEdges = true, + colorGroups, + hasPrecomputedVisibility; + private EdgeVisibility edgeVisibility = EdgeVisibility.Traversable; // for selected waypoints + private Selection selection = Selection.None; + private int selectedWaypoint = -1, selectedGroup = -1; + + public WaypointComponent(ITagContainer diContainer) + { + diContainer.AddTag(this); + editor = diContainer.GetTag(); + editor.fbArea.OnRender += HandleRender; + editor.OnLoadScene += HandleLoadScene; + editor.editor.AddInfoSection("Waypoints", HandleInfoSection, false); + + var menuBar = diContainer.GetTag(); + menuBar.AddCheckbox("View/Waypoints/Points", () => ref showPoints, () => editor.fbArea.IsDirty = true); + menuBar.AddCheckbox("View/Waypoints/Traversable", () => ref showTraversableEdges, () => editor.fbArea.IsDirty = true); + + lineRenderer = new(diContainer); + lineRenderer.Material.LinkTransformsTo(diContainer.GetTag()); + lineRenderer.Material.World.Ref = Matrix4x4.Identity; + lineRenderer.Material.DepthTest = true; + } + + protected override void DisposeManaged() + { + base.DisposeManaged(); + lineRenderer.Dispose(); + } + + private void HandleLoadScene() + { + selectedWaypoint = selectedGroup = -1; + lineRenderer.Clear(); + walkableLinks.Clear(); + jumpableLinks.Clear(); + idToIndex.Clear(); + groupColors.Clear(); + showPoints = true; + showTraversableEdges = true; + rangePoints = rangeTraversableEdges = default; + edgeVisibility = EdgeVisibility.Traversable; + selection = Selection.None; + if (editor.scene == null) + return; + + var wpSystem = editor.scene.waypointSystem; + idToIndex.EnsureCapacity(wpSystem.Waypoints.Length); + foreach (var (wp, index) in wpSystem.Waypoints.Indexed()) + idToIndex.Add(wp.Id, index); + LinkSet(wpSystem.Waypoints.Select(wp => wp.WalkableIds), walkableLinks); + LinkSet(wpSystem.Waypoints.Select(wp => wp.JumpableIds), jumpableLinks); + hasPrecomputedVisibility = wpSystem.Waypoints.Any(wp => wp.VisibleIds?.Length > 0); + + var groupIds = wpSystem.Waypoints.Select(wp => wp.Group).Distinct(); + var groupColors_ = groupIds.Zip(MathEx.GoldenRatioColors().Zip(MathEx.GoldenRatioColors(saturation: 0.5f))); + foreach (var (groupId, colors) in groupColors_) + groupColors[groupId] = colors; + + rangePoints = 0..(wpSystem.Waypoints.Length * 3); + rangeTraversableEdges = rangePoints.Suffix(walkableLinks.Count + jumpableLinks.Count); + UpdateUnselectedEdges(); + + void LinkSet(IEnumerable?> halfLinks, Dictionary<(int, int), bool> fullLinks) + { + if (!halfLinks.Any()) + return; + foreach (var (halfLinkSet, i) in halfLinks.Indexed()) + { + if (halfLinkSet == null) + continue; + foreach (var jId in halfLinkSet) + { + var j = idToIndex[jId]; + var key = i < j ? (i, j) : (j, i); + if (!fullLinks.TryAdd(key, false)) + fullLinks[key] = true; + } + } + } + } + + private void HandleRender(CommandList cl) + { + if (lineRenderer.Count == 0) + return; + + if (selection == Selection.None) + { + if (showPoints) + lineRenderer.Render(cl, rangePoints); + if (showTraversableEdges) + lineRenderer.Render(cl, rangeTraversableEdges); + } + else + lineRenderer.Render(cl); + } + + private void HandleInfoSection() + { + var wpSystem = editor.scene?.waypointSystem; + var wpCount = wpSystem?.Waypoints?.Length ?? 0; + LabelText("Version", wpSystem?.Version.ToString() ?? "n/a"); + LabelText("Waypoints", wpCount.ToString()); + LabelText("Groups", groupColors.Count.ToString()); + LabelText("Precomp. visibility", hasPrecomputedVisibility.ToString()); + NewLine(); + Text("Selection:"); + if (EnumRadioButtonGroup(ref selection)) + { + selectedGroup = selectedWaypoint = -1; + switch(selection) + { + case Selection.None: UpdateUnselectedEdges(); break; + case Selection.Waypoint: UpdateSelectedWaypointEdges(); break; + case Selection.Group: UpdateSelectedGroupEdges(); break; + default: break; + } + } + NewLine(); + switch(selection) + { + case Selection.None: + if (Checkbox("Color groups", ref colorGroups)) + UpdateUnselectedEdges(); + break; + case Selection.Waypoint: + if (DragInt("Index", ref selectedWaypoint, 0.2f, -1, wpCount - 1) | + EnumCombo("Edges", ref edgeVisibility)) + UpdateSelectedWaypointEdges(); + break; + case Selection.Group: + if (DragInt("Index", ref selectedGroup, 0.1f, -1, groupColors.Count - 1)) + UpdateSelectedGroupEdges(); + break; + default: break; + } + } + + private void UpdateUnselectedEdges() + { + editor.fbArea.IsDirty = true; + var wpSystem = editor.scene!.waypointSystem; + lineRenderer.Clear(); + lineRenderer.Reserve( + wpSystem.Waypoints.Length * 3 + + walkableLinks.Count + jumpableLinks.Count); + foreach (var wp in wpSystem.Waypoints) + { + var color = colorGroups ? groupColors[wp.Group].full : IColor.White; + lineRenderer.AddCross(color, wp.Position, WaypointCrossSize); + } + AddLinkSet(walkableLinks, new(0, 0, 230), new(0, 0, 130)); + AddLinkSet(jumpableLinks, new(230, 0, 0), new(130, 0, 0)); + + void AddLinkSet(Dictionary<(int, int), bool> linkSet, IColor fullColor, IColor halfColor) + { + foreach (var ((linkFrom, linkTo), isFull) in linkSet) + lineRenderer.Add( + isFull ? fullColor : halfColor, + wpSystem.Waypoints[linkFrom].Position, + wpSystem.Waypoints[linkTo].Position); + } + } + + private void UpdateSelectedWaypointEdges() + { + editor.fbArea.IsDirty = true; + lineRenderer.Clear(); + var wpSystem = editor.scene!.waypointSystem; + var waypoint = selectedWaypoint < 0 + ? null as Waypoint? + : wpSystem.Waypoints[selectedWaypoint]; + var selectedPosition = waypoint?.Position ?? Vector3.Zero; + var allLinkedIds = + (waypoint is null ? null + : edgeVisibility is EdgeVisibility.Traversable ? waypoint.Value.WalkableIds.Concat(waypoint.Value.JumpableIds) + : edgeVisibility is EdgeVisibility.Visibility ? waypoint.Value.VisibleIds + : null) + ?? []; + for (int i = 0; i < wpSystem.Waypoints.Length; i++) + { + if (i == selectedWaypoint) + { + lineRenderer.AddCross(IColor.Red, selectedPosition, WaypointCrossSize); + lineRenderer.AddDiamondSphere(new(selectedPosition, WaypointSphereSize), IColor.Red); + } + else + { + var color = allLinkedIds.Contains(wpSystem.Waypoints[i].Id) ? IColor.Blue : IColor.Grey; + lineRenderer.AddCross(color, wpSystem.Waypoints[i].Position, WaypointCrossSize); + } + } + + switch (edgeVisibility) + { + case EdgeVisibility.Traversable: + AddLinkSet(waypoint?.WalkableIds, new(0, 0, 230)); + AddLinkSet(waypoint?.JumpableIds, new(230, 0, 0)); + break; + case EdgeVisibility.Visibility: + AddLinkSet(waypoint?.VisibleIds, new(0, 230, 0)); + break; + default: break; + } + + void AddLinkSet(uint[]? edges, IColor color) + { + foreach (var otherId in edges ?? []) + { + var otherWaypoint = wpSystem.Waypoints[idToIndex[otherId]]; + lineRenderer.Add(color, otherWaypoint.Position, selectedPosition); + } + } + } + + private void UpdateSelectedGroupEdges() + { + editor.fbArea.IsDirty = true; + lineRenderer.Clear(); + if (selectedGroup < 0) + return; + var wpSystem = editor.scene!.waypointSystem; + AddGroup((uint)selectedGroup, true); + if (!wpSystem.CompatibleGroups.TryGetValue((uint)selectedGroup, out var compatibleGroups)) + return; + foreach (var compatibleGroup in compatibleGroups) + AddGroup(compatibleGroup, false); + + void AddGroup(uint groupId, bool fullColor) + { + var color = fullColor ? groupColors[groupId].full : groupColors[groupId].half; + foreach (ref readonly var waypoint in wpSystem.Waypoints.AsSpan()) + { + if (waypoint.Group != groupId) + continue; + lineRenderer.AddCross(color, waypoint.Position, WaypointCrossSize); + } + } + } + } +} diff --git a/zzre/tools/sceneeditor/SceneEditor.cs b/zzre/tools/sceneeditor/SceneEditor.cs index b9940fe0..738f75fc 100644 --- a/zzre/tools/sceneeditor/SceneEditor.cs +++ b/zzre/tools/sceneeditor/SceneEditor.cs @@ -86,6 +86,7 @@ public SceneEditor(ITagContainer diContainer) new LightComponent(localDiContainer); new EffectComponent(localDiContainer); new Sample3DComponent(localDiContainer); + new WaypointComponent(localDiContainer); new SelectionComponent(localDiContainer); diContainer.GetTag().AddEditor(this); }