{"TotalCount":12,"Files":[{"Ident":"fluidplay.waymaker","Path":"Code/Waymaker/WaymakerAssemblyInfo.cs","FileName":"WaymakerAssemblyInfo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(\u0022guanowars.unittest\u0022)]\n[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(\u0022waymaker.unittest\u0022)]\n"},{"Ident":"fluidplay.waymaker","Path":"Code/Waymaker/WaymakerMeshUtility.cs","FileName":"WaymakerMeshUtility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System.Collections.Generic;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\n/// \u003Csummary\u003E\n/// Waymaker-owned helpers for assigning UVs and materials to generated polygon faces.\n/// \u003C/summary\u003E\ninternal static class WaymakerMeshUtility\n{\n\tpublic static void AddTexturedQuad(PolygonMesh mesh, Material material,\n\t\tHalfEdgeMesh.VertexHandle a, HalfEdgeMesh.VertexHandle b,\n\t\tHalfEdgeMesh.VertexHandle c, HalfEdgeMesh.VertexHandle d,\n\t\tVector2 uvA, Vector2 uvB, Vector2 uvC, Vector2 uvD)\n\t{\n\t\tif (a.Equals(b) || a.Equals(c) || a.Equals(d) || b.Equals(c) || b.Equals(d) || c.Equals(d))\n\t\t\treturn;\n\n\t\tvar face = mesh.AddFace(a, b, c, d);\n\t\tif (!face.IsValid)\n\t\t\treturn;\n\n\t\tif (material is not null)\n\t\t\tmesh.SetFaceMaterial(face, material);\n\n\t\tmesh.SetFaceTextureCoords(face, new List\u003CVector2\u003E { uvA, uvB, uvC, uvD });\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Waymaker/WaymakerAssemblyInfo.cs","FileName":"WaymakerAssemblyInfo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(\u0022guanowars.unittest\u0022)]\n[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(\u0022waymaker.unittest\u0022)]\n"},{"Ident":"fluidplay.waymaker","Path":"Waymaker/Waymaker.cs","FileName":"Waymaker.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System;\nusing System.Linq;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\n/// \u003Csummary\u003E\n/// Generates a UV-mapped surface that follows an editable spline.\n/// \u003C/summary\u003E\n[Icon(\u0022route\u0022)]\npublic partial class Waymaker : Component, Component.ExecuteInEditor, Component.IHasBounds\n{\n\tprivate const string GeneratedMeshTag = \u0022waymaker_generated_mesh\u0022;\n\tprivate const float SampleSpacing = 40.0f;\n\n\t[Property, Feature(\u0022Waymaker\u0022), Hide]\n\tpublic Spline Spline\n\t{\n\t\tget;\n\t\tset\n\t\t{\n\t\t\tfield = value ?? new Spline();\n\t\t\tSubscribeToSpline();\n\t\t\tMarkDirty();\n\t\t}\n\t} = new();\n\n\t[Property(Title = \u0022Loop\u0022), Feature(\u0022Spline\u0022, Icon = \u0022all_inclusive\u0022)]\n\tprivate bool Loop\n\t{\n\t\tget =\u003E Spline?.IsLoop ?? false;\n\t\tset\n\t\t{\n\t\t\tif (Spline is null || Spline.IsLoop == value || (value \u0026\u0026 Spline.PointCount \u003C 2))\n\t\t\t\treturn;\n\n\t\t\tSpline.IsLoop = value;\n\t\t\tMarkDirty();\n\t\t}\n\t}\n\n\tprivate Spline m_SubscribedSpline;\n\tprivate bool m_IsDirty = true;\n\n\t[Property(Title = \u0022Source Material\u0022), Feature(\u0022Surface\u0022, Icon = \u0022texture\u0022), Validate(nameof(IsSourceMaterialAssigned), \u0022Assign a source material to generate the Waymaker surface.\u0022, LogLevel.Warn)]\n\tprivate Material SourceMaterial { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Surface Width\u0022), Feature(\u0022Surface\u0022)]\n\tprivate float SurfaceWidth { get; set { field = Math.Max(0.01f, value); MarkDirty(); } } = 500.0f;\n\n\t[Property(Title = \u0022Dynamic V Tiling\u0022), Feature(\u0022Texturing\u0022, Icon = \u0022texture\u0022)]\n\tprivate bool DynamicVTiling { get; set { field = value; MarkDirty(); } } = true;\n\n\t[Property(Title = \u0022Dynamic V Tile Length\u0022), Feature(\u0022Texturing\u0022), ShowIf(nameof(DynamicVTiling), true)]\n\tprivate float DynamicVTileLength { get; set { field = Math.Max(0.1f, value); MarkDirty(); } } = 500.0f;\n\n\t[Property(Title = \u0022Vertical Tiles\u0022), Feature(\u0022Texturing\u0022), ShowIf(nameof(DynamicVTiling), false)]\n\tprivate int VerticalTiles { get; set { field = Math.Max(1, value); MarkDirty(); } } = 1;\n\n\t[Property(Title = \u0022Horizontal Tiling\u0022), Feature(\u0022Texturing\u0022)]\n\tprivate float HorizontalTiling { get; set { field = Math.Max(0.01f, value); MarkDirty(); } } = 1.0f;\n\n\t[Property(Title = \u0022Horizontal Padding\u0022), Feature(\u0022Texturing\u0022), Range(0.0f, 1.0f)]\n\tprivate float HorizontalPadding { get; set { field = value.Clamp(0.0f, 1.0f); MarkDirty(); } } = 0.0f;\n\n\t[Property(Title = \u0022Cap Texture\u0022), Feature(\u0022Caps\u0022, Icon = \u0022vertical_align_bottom\u0022), ShowIf(nameof(CanUseCapSettings), true)]\n\tprivate bool CapTexture { get; set { field = value; MarkDirty(); } } = false;\n\n\t[Property(Title = \u0022Cap Texture Start\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate Material CapTextureStart { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Cap Texture End\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate Material CapTextureEnd { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Cap Length\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate float CapLength { get; set { field = Math.Max(0.0f, value); MarkDirty(); } } = 100.0f;\n\n\t// Keep the serialized member name so existing scenes retain their value; the inspector now reflects its edge-length meaning.\n\t[Property(Title = \u0022Minimum Edge Length\u0022), Feature(\u0022Geometry\u0022), Validate(nameof(IsMinimumEdgeLengthInRecommendedRange), \u0022Values above 0.1 may create coarser quads and distort tight turns.\u0022, LogLevel.Warn)]\n\tprivate float MinimumEdgeWidth { get; set { field = Math.Max(0.0f, value); MarkDirty(); } } = 0.01f;\n\n\tprivate bool CanUseCapSettings =\u003E Spline is not null \u0026\u0026 WaymakerMeshBuilder.CanUseCaps(DynamicVTiling, Spline.IsLoop);\n\tprivate bool ShowCapMaterials =\u003E CanUseCapSettings \u0026\u0026 CapTexture;\n\n\tpublic BBox LocalBounds\n\t{\n\t\tget\n\t\t{\n\t\t\tvar bounds = Spline?.Bounds ?? new BBox();\n\t\t\tvar padding = new Vector3(SurfaceWidth * 0.5f);\n\t\t\treturn new BBox(bounds.Mins - padding, bounds.Maxs \u002B padding);\n\t\t}\n\t}\n\n\tpublic Waymaker()\n\t{\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = Vector3.Zero });\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1000.0f, 0.0f, 0.0f) });\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1600.0f, 1000.0f, 0.0f) });\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\tSubscribeToSpline();\n\t\tEnsureGeneratedMeshExists();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tUnsubscribeFromSpline();\n\t\tRemoveGeneratedMesh();\n\t}\n\n\tprotected override void OnValidate()\n\t{\n\t\tSubscribeToSpline();\n\t\tMarkDirty();\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tSyncSplineSubscription();\n\n\t\tif (!m_IsDirty || !Scene.IsEditor)\n\t\t\treturn;\n\n\t\tRebuildGeneratedMesh();\n\t}\n\n\tprivate bool IsMinimumEdgeLengthInRecommendedRange(float value) =\u003E value \u003C= 0.1f;\n\tprivate bool IsSourceMaterialAssigned(Material material) =\u003E material is not null;\n\n\tprivate void MarkDirty() =\u003E m_IsDirty = true;\n\n\tprivate void SyncSplineSubscription()\n\t{\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\n\t\t\treturn;\n\n\t\tSubscribeToSpline();\n\t\tMarkDirty();\n\t}\n\n\tprivate void SubscribeToSpline()\n\t{\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\n\t\t\treturn;\n\n\t\tUnsubscribeFromSpline();\n\t\tm_SubscribedSpline = Spline;\n\n\t\tif (m_SubscribedSpline is not null)\n\t\t\tm_SubscribedSpline.SplineChanged \u002B= MarkDirty;\n\t}\n\n\tprivate void UnsubscribeFromSpline()\n\t{\n\t\tif (m_SubscribedSpline is null)\n\t\t\treturn;\n\n\t\tm_SubscribedSpline.SplineChanged -= MarkDirty;\n\t\tm_SubscribedSpline = null;\n\t}\n\n\tprivate void EnsureGeneratedMeshExists()\n\t{\n\t\tif (!Scene.IsEditor || HasGeneratedMesh())\n\t\t\treturn;\n\n\t\tRebuildGeneratedMesh();\n\t}\n\n\tprivate void RebuildGeneratedMesh()\n\t{\n\t\tif (!Scene.IsEditor)\n\t\t\treturn;\n\n\t\tRemoveGeneratedMesh();\n\n\t\tif (Spline is { PointCount: \u003E= 2 } \u0026\u0026 SourceMaterial is not null \u0026\u0026 Spline.Length \u003E 0.001f)\n\t\t{\n\t\t\tvar settings = new WaymakerMeshSettings\n\t\t\t{\n\t\t\t\tSourceMaterial = SourceMaterial,\n\t\t\t\tStartCapMaterial = CapTextureStart,\n\t\t\t\tEndCapMaterial = CapTextureEnd,\n\t\t\t\tSurfaceWidth = SurfaceWidth,\n\t\t\t\tDynamicVTiling = DynamicVTiling,\n\t\t\t\tDynamicVTileLength = DynamicVTileLength,\n\t\t\t\tVerticalTiles = VerticalTiles,\n\t\t\t\tHorizontalTiling = HorizontalTiling,\n\t\t\t\tHorizontalPadding = HorizontalPadding,\n\t\t\t\tUseCaps = CapTexture \u0026\u0026 CanUseCapSettings,\n\t\t\t\tCapLength = CapLength,\n\t\t\t\tMinimumEdgeLength = MinimumEdgeWidth\n\t\t\t};\n\n\t\t\tvar mesh = WaymakerMeshBuilder.Build(Spline, settings, SampleSpacing);\n\t\t\tif (mesh is not null \u0026\u0026 mesh.FaceHandles.Any())\n\t\t\t{\n\t\t\t\tvar child = new GameObject(GameObject, true, \u0022Waymaker Surface\u0022);\n\t\t\t\tchild.Tags.Add(GeneratedMeshTag);\n\n\t\t\t\tvar meshComponent = child.AddComponent\u003CMeshComponent\u003E();\n\t\t\t\tmeshComponent.Mesh = mesh;\n\t\t\t\tmeshComponent.SmoothingAngle = 40.0f;\n\t\t\t}\n\t\t}\n\n\t\tm_IsDirty = false;\n\t}\n\n\tprivate bool HasGeneratedMesh() =\u003E GameObject.Children.Any(child =\u003E child.Tags.Has(GeneratedMeshTag));\n\n\tprivate void RemoveGeneratedMesh()\n\t{\n\t\tif (!GameObject.IsValid())\n\t\t\treturn;\n\n\t\tforeach (var child in GameObject.Children.Where(child =\u003E child.Tags.Has(GeneratedMeshTag)).ToArray())\n\t\t\tchild.Destroy();\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Waymaker\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022waymaker\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022fluidplay\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022fluidplay.waymaker\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002232\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-11-01T17:23:02.4498014Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.142.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.142.0\u0022)]"},{"Ident":"fluidplay.waymaker","Path":"UnitTests/WaymakerMeshBuilderTests.cs","FileName":"WaymakerMeshBuilderTests.cs","PackageType":"library","CodeKind":"UnitTest","AssetVersionId":443795,"IsPrivate":false,"Code":"using Microsoft.VisualStudio.TestTools.UnitTesting;\nusing FluidPlay.Waymaker;\nusing Sandbox;\nusing System.Linq;\n\nnamespace FluidPlay.Waymaker.Tests;\n\n[TestClass]\npublic sealed class WaymakerMeshBuilderTests\n{\n\t[TestMethod]\n\tpublic void MainUvRangeAppliesAbsoluteUvPadding()\n\t{\n\t\tvar singleTile = WaymakerMeshBuilder.GetMainURange(1.0f, 0.2f);\n\t\tvar doubleTile = WaymakerMeshBuilder.GetMainURange(2.0f, 0.2f);\n\n\t\tAssert.AreEqual(0.1f, singleTile.Min, 0.0001f);\n\t\tAssert.AreEqual(0.9f, singleTile.Max, 0.0001f);\n\t\tAssert.AreEqual(0.1f, doubleTile.Min, 0.0001f);\n\t\tAssert.AreEqual(1.9f, doubleTile.Max, 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void DynamicVTilingUsesAccumulatedCenterlineDistance()\n\t{\n\t\tvar range = WaymakerMeshBuilder.GetDynamicVRange(25.0f, 275.0f, 100.0f);\n\n\t\tAssert.AreEqual(0.25f, range.Min, 0.0001f);\n\t\tAssert.AreEqual(2.75f, range.Max, 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void FixedVTilingDistributesWholeRepeatsOverTheRequestedPath()\n\t{\n\t\tvar range = WaymakerMeshBuilder.GetFixedVRange(10.0f, 60.0f, 100.0f, 3);\n\n\t\tAssert.AreEqual(0.3f, range.Min, 0.0001f);\n\t\tAssert.AreEqual(1.8f, range.Max, 0.0001f);\n\t\tAssert.AreEqual(1.0f, WaymakerMeshBuilder.GetFixedVRange(0.0f, 1.0f, 1.0f, 0).Max, 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void CapsAreIneligibleForDynamicOrLoopedSplines()\n\t{\n\t\tAssert.IsTrue(WaymakerMeshBuilder.CanUseCaps(dynamicVTiling: false, isLoop: false));\n\t\tAssert.IsFalse(WaymakerMeshBuilder.CanUseCaps(dynamicVTiling: true, isLoop: false));\n\t\tAssert.IsFalse(WaymakerMeshBuilder.CanUseCaps(dynamicVTiling: false, isLoop: true));\n\t\tAssert.AreEqual(0.0f, WaymakerMeshBuilder.GetEffectiveCapLength(100.0f, 30.0f, true, true, false), 0.0001f);\n\t\tAssert.AreEqual(0.0f, WaymakerMeshBuilder.GetEffectiveCapLength(100.0f, 30.0f, true, false, true), 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void SplineLoopCanBeSetThroughItsPublicApi()\n\t{\n\t\tvar spline = new Spline();\n\t\tspline.InsertPoint(0, new Spline.Point { Position = Vector3.Zero });\n\t\tspline.InsertPoint(1, new Spline.Point { Position = new Vector3(100.0f, 0.0f, 0.0f) });\n\t\tspline.InsertPoint(2, new Spline.Point { Position = new Vector3(100.0f, 100.0f, 0.0f) });\n\t\tvar property = typeof(Spline).GetProperty(nameof(Spline.IsLoop));\n\t\tAssert.IsNotNull(property);\n\t\tAssert.IsTrue(property.CanWrite, $\u0022{nameof(Spline.IsLoop)} must be writable for looped Waymaker validation.\u0022);\n\n\t\tproperty.SetValue(spline, true);\n\t\tAssert.IsTrue(spline.IsLoop);\n\t}\n\n\t[TestMethod]\n\tpublic void ShortPathClampsEachCapToHalfItsLength()\n\t{\n\t\tAssert.AreEqual(50.0f, WaymakerMeshBuilder.GetEffectiveCapLength(100.0f, 80.0f, true, false, false), 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void MinimumEdgeLengthMergesShortStraightSectionsWithoutNarrowingTheSurface()\n\t{\n\t\tvar frames = new[]\n\t\t{\n\t\t\tnew WaymakerFrame(Vector3.Zero, Rotation.Identity, 0.0f),\n\t\t\tnew WaymakerFrame(new Vector3(40.0f, 0.0f, 0.0f), Rotation.Identity, 40.0f),\n\t\t\tnew WaymakerFrame(new Vector3(80.0f, 0.0f, 0.0f), Rotation.Identity, 80.0f),\n\t\t\tnew WaymakerFrame(new Vector3(120.0f, 0.0f, 0.0f), Rotation.Identity, 120.0f),\n\t\t\tnew WaymakerFrame(new Vector3(160.0f, 0.0f, 0.0f), Rotation.Identity, 160.0f)\n\t\t};\n\t\tconst float halfWidth = 250.0f;\n\t\tvar merged = WaymakerMeshBuilder.MergeShortEdges(frames, halfWidth, minimumEdgeLength: 60.0f);\n\n\t\tCollectionAssert.AreEqual(new[] { 0.0f, 80.0f, 160.0f }, merged.Select(frame =\u003E frame.SplineDistance).ToArray());\n\t\tAssert.AreEqual(500.0f, 2.0f * halfWidth, 0.0001f);\n\t}\n\n\t[TestMethod]\n\tpublic void MinimumEdgeLengthSkipsTightTurnSectionsInsteadOfShrinkingWidth()\n\t{\n\t\tvar frames = new[]\n\t\t{\n\t\t\tnew WaymakerFrame(Vector3.Zero, Rotation.Identity, 0.0f),\n\t\t\tnew WaymakerFrame(new Vector3(10.0f, 0.0f, 0.0f), Rotation.FromAxis(Vector3.Up, 45.0f), 10.0f),\n\t\t\tnew WaymakerFrame(new Vector3(10.0f, 10.0f, 0.0f), Rotation.FromAxis(Vector3.Up, 90.0f), 20.0f),\n\t\t\tnew WaymakerFrame(new Vector3(10.0f, 20.0f, 0.0f), Rotation.FromAxis(Vector3.Up, 90.0f), 30.0f)\n\t\t};\n\t\tvar merged = WaymakerMeshBuilder.MergeShortEdges(frames, halfWidth: 10.0f, minimumEdgeLength: 25.0f);\n\n\t\tAssert.IsTrue(merged.Length \u003C frames.Length, \u0022Short, collapsed inner edges should be merged into longer quads.\u0022);\n\t\tfor (int i = 0; i \u003C merged.Length - 1; i\u002B\u002B)\n\t\t\tAssert.IsTrue(WaymakerMeshBuilder.HasMinimumLongitudinalEdgeLength(merged[i], merged[i \u002B 1], 10.0f, 2.0f), $\u0022Merged quad {i} should have both longitudinal edges at least the requested length.\u0022);\n\t}\n\n\t[TestMethod]\n\tpublic void CapBoundariesAreInsertedIntoTheSampledPath()\n\t{\n\t\tvar spline = new Spline();\n\t\tspline.InsertPoint(0, new Spline.Point { Position = Vector3.Zero, Mode = Spline.HandleMode.Linear });\n\t\tspline.InsertPoint(1, new Spline.Point { Position = new Vector3(1000.0f, 0.0f, 0.0f), Mode = Spline.HandleMode.Linear });\n\n\t\tvar distances = WaymakerMeshBuilder.CreateSampleDistances(spline, 250.0f, 100.0f);\n\n\t\tCollectionAssert.Contains(distances, 100.0f);\n\t\tCollectionAssert.Contains(distances, 900.0f);\n\t\tCollectionAssert.AreEqual(distances.OrderBy(distance =\u003E distance).ToArray(), distances.ToArray());\n\t}\n\n\t[TestMethod]\n\tpublic void DynamicVAccumulationUsesGeneratedCenterlineChordLengths()\n\t{\n\t\tvar frames = new[]\n\t\t{\n\t\t\tnew WaymakerFrame(Vector3.Zero, Rotation.Identity, 0.0f),\n\t\t\tnew WaymakerFrame(new Vector3(3.0f, 4.0f, 0.0f), Rotation.Identity, 5.0f),\n\t\t\tnew WaymakerFrame(new Vector3(3.0f, 4.0f, 12.0f), Rotation.Identity, 17.0f)\n\t\t};\n\n\t\tvar lengths = WaymakerMeshBuilder.GetCumulativeCenterlineLengths(frames);\n\n\t\tAssert.AreEqual(0.0f, lengths[0], 0.0001f);\n\t\tAssert.AreEqual(5.0f, lengths[1], 0.0001f);\n\t\tAssert.AreEqual(17.0f, lengths[2], 0.0001f);\n\t}\n\n\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Code/Waymaker/WaymakerMeshBuilder.cs","FileName":"WaymakerMeshBuilder.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\ninternal struct WaymakerMeshSettings\n{\n\tpublic Material SourceMaterial;\n\tpublic Material StartCapMaterial;\n\tpublic Material EndCapMaterial;\n\tpublic float SurfaceWidth;\n\tpublic bool DynamicVTiling;\n\tpublic float DynamicVTileLength;\n\tpublic int VerticalTiles;\n\tpublic float HorizontalTiling;\n\tpublic float HorizontalPadding;\n\tpublic bool UseCaps;\n\tpublic float CapLength;\n\tpublic float MinimumEdgeLength;\n}\n\ninternal readonly record struct WaymakerFrame(Vector3 Position, Rotation Rotation, float SplineDistance)\n{\n\tpublic Vector3 Right =\u003E Rotation.Right;\n}\n\n/// \u003Csummary\u003E\n/// Samples Waymaker\u0027s spline and builds the textured quad strip, including UVs and optional endpoint cap materials.\n/// \u003C/summary\u003E\ninternal static class WaymakerMeshBuilder\n{\n\tprivate const float Epsilon = 0.001f;\n\n\tinternal static (float Min, float Max) GetMainURange(float horizontalTiling, float horizontalPadding)\n\t{\n\t\tfloat tiling = Math.Max(0.01f, horizontalTiling);\n\t\tfloat padding = Math.Min(Math.Max(0.0f, horizontalPadding), tiling);\n\t\treturn (padding * 0.5f, tiling - padding * 0.5f);\n\t}\n\n\tinternal static (float Min, float Max) GetDynamicVRange(float startDistance, float endDistance, float tileLength)\n\t{\n\t\tfloat length = Math.Max(Epsilon, tileLength);\n\t\treturn (startDistance / length, endDistance / length);\n\t}\n\n\tinternal static (float Min, float Max) GetFixedVRange(float startDistance, float endDistance, float totalLength, int verticalTiles)\n\t{\n\t\tfloat length = Math.Max(Epsilon, totalLength);\n\t\tfloat tiles = Math.Max(1, verticalTiles);\n\t\treturn (startDistance / length * tiles, endDistance / length * tiles);\n\t}\n\n\tinternal static bool CanUseCaps(bool dynamicVTiling, bool isLoop) =\u003E !dynamicVTiling \u0026\u0026 !isLoop;\n\n\tinternal static float GetEffectiveCapLength(float totalLength, float requestedLength, bool useCaps, bool dynamicVTiling, bool isLoop)\n\t{\n\t\tif (!useCaps || !CanUseCaps(dynamicVTiling, isLoop))\n\t\t\treturn 0.0f;\n\n\t\treturn Math.Clamp(requestedLength, 0.0f, Math.Max(0.0f, totalLength) * 0.5f);\n\t}\n\n\tpublic static PolygonMesh Build(Spline spline, WaymakerMeshSettings settings, float sampleSpacing)\n\t{\n\t\tif (spline is null || spline.PointCount \u003C 2 || spline.Length \u003C= Epsilon)\n\t\t\treturn null;\n\n\t\tfloat totalLength = spline.Length;\n\t\tbool useCaps = settings.UseCaps \u0026\u0026 CanUseCaps(settings.DynamicVTiling, spline.IsLoop);\n\t\tfloat capLength = GetEffectiveCapLength(totalLength, settings.CapLength, useCaps, settings.DynamicVTiling, spline.IsLoop);\n\n\t\tvar distances = CreateSampleDistances(spline, sampleSpacing, capLength);\n\t\tif (distances.Count \u003C 2)\n\t\t\treturn null;\n\n\t\tvar sampledFrames = distances.Select(distance =\u003E SampleFrame(spline, distance)).ToArray();\n\t\tfloat halfWidth = Math.Max(0.0f, settings.SurfaceWidth) * 0.5f;\n\t\tvar frames = MergeShortEdges(sampledFrames, halfWidth, Math.Max(0.0f, settings.MinimumEdgeLength),\n\t\t\tcapLength \u003E Epsilon ? capLength : -1.0f,\n\t\t\tcapLength \u003E Epsilon ? totalLength - capLength : -1.0f);\n\t\tif (frames.Length \u003C 2)\n\t\t\treturn null;\n\n\t\tvar cumulativeCenterlineLengths = GetCumulativeCenterlineLengths(frames);\n\n\t\tvar mesh = new PolygonMesh();\n\t\tvar vertices = new HalfEdgeMesh.VertexHandle[frames.Length][];\n\n\t\tfor (int i = 0; i \u003C frames.Length; i\u002B\u002B)\n\t\t{\n\t\t\tVector3 halfSpan = frames[i].Right * halfWidth;\n\t\t\tvertices[i] = mesh.AddVertices(frames[i].Position - halfSpan, frames[i].Position \u002B halfSpan);\n\t\t}\n\n\t\tvar mainURange = GetMainURange(settings.HorizontalTiling, settings.HorizontalPadding);\n\t\tfloat mainU0 = mainURange.Min;\n\t\tfloat mainU1 = mainURange.Max;\n\t\tint verticalTiles = Math.Max(1, settings.VerticalTiles);\n\t\tfloat dynamicTileLength = Math.Max(Epsilon, settings.DynamicVTileLength);\n\t\tfloat interiorStart = capLength;\n\t\tfloat interiorEnd = Math.Max(interiorStart, totalLength - capLength);\n\t\tfloat interiorLength = Math.Max(0.0f, interiorEnd - interiorStart);\n\n\t\tfor (int i = 0; i \u003C frames.Length - 1; i\u002B\u002B)\n\t\t{\n\t\t\tfloat distance0 = frames[i].SplineDistance;\n\t\t\tfloat distance1 = frames[i \u002B 1].SplineDistance;\n\t\t\tfloat centerline0 = cumulativeCenterlineLengths[i];\n\t\t\tfloat centerline1 = cumulativeCenterlineLengths[i \u002B 1];\n\t\t\tfloat segmentMidpoint = (distance0 \u002B distance1) * 0.5f;\n\n\t\t\tbool isStartCap = capLength \u003E Epsilon \u0026\u0026 segmentMidpoint \u003C capLength;\n\t\t\tbool isEndCap = capLength \u003E Epsilon \u0026\u0026 segmentMidpoint \u003E totalLength - capLength;\n\n\t\t\tMaterial material = isStartCap\n\t\t\t\t? settings.StartCapMaterial ?? settings.SourceMaterial\n\t\t\t\t: isEndCap\n\t\t\t\t\t? settings.EndCapMaterial ?? settings.SourceMaterial\n\t\t\t\t\t: settings.SourceMaterial;\n\n\t\t\tfloat v0;\n\t\t\tfloat v1;\n\t\t\tif (isStartCap)\n\t\t\t{\n\t\t\t\tv0 = (distance0 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t\tv1 = (distance1 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t}\n\t\t\telse if (isEndCap)\n\t\t\t{\n\t\t\t\tv0 = ((distance0 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t\tv1 = ((distance1 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t}\n\t\t\telse if (settings.DynamicVTiling)\n\t\t\t{\n\t\t\t\tvar vRange = GetDynamicVRange(centerline0, centerline1, dynamicTileLength);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\t\t\telse if (useCaps \u0026\u0026 interiorLength \u003E Epsilon)\n\t\t\t{\n\t\t\t\tvar vRange = GetFixedVRange(distance0 - interiorStart, distance1 - interiorStart, interiorLength, verticalTiles);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar vRange = GetFixedVRange(centerline0, centerline1, cumulativeCenterlineLengths[^1], verticalTiles);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\n\t\t\tWaymakerMeshUtility.AddTexturedQuad(mesh, material,\n\t\t\t\tvertices[i][0], vertices[i][1], vertices[i \u002B 1][1], vertices[i \u002B 1][0],\n\t\t\t\tnew Vector2(mainU0, v0), new Vector2(mainU1, v0),\n\t\t\t\tnew Vector2(mainU1, v1), new Vector2(mainU0, v1));\n\t\t}\n\n\t\tmesh.RemoveBadGeometry();\n\n\t\t// Translucent shaders multiply texture alpha by vertex alpha. Explicitly initialize every half-edge color;\n\t\t// leaving PolygonMesh\u0027s color stream sparse can make some translucent face corners resolve to zero alpha.\n\t\tforeach (var halfEdge in mesh.HalfEdgeHandles)\n\t\t{\n\t\t\tmesh.SetVertexColor(halfEdge, Color32.White);\n\t\t\tif (halfEdge.OppositeEdge.IsValid)\n\t\t\t\tmesh.SetVertexColor(halfEdge.OppositeEdge, Color32.White);\n\t\t}\n\n\t\treturn mesh;\n\t}\n\n\tinternal static List\u003Cfloat\u003E CreateSampleDistances(Spline spline, float sampleSpacing, float capLength)\n\t{\n\t\tfloat totalLength = spline.Length;\n\t\tfloat spacing = Math.Max(1.0f, sampleSpacing);\n\t\tvar samples = new List\u003Cfloat\u003E { 0.0f, totalLength };\n\n\t\tint subdivisions = Math.Max(2, (int)MathF.Ceiling(totalLength / spacing));\n\t\tfor (int i = 1; i \u003C subdivisions; i\u002B\u002B)\n\t\t\tsamples.Add(totalLength * i / subdivisions);\n\n\t\tfor (int i = 0; i \u003C spline.PointCount; i\u002B\u002B)\n\t\t\tsamples.Add(spline.GetDistanceAtPoint(i).Clamp(0.0f, totalLength));\n\n\t\tif (capLength \u003E Epsilon)\n\t\t{\n\t\t\tsamples.Add(capLength);\n\t\t\tsamples.Add(totalLength - capLength);\n\t\t}\n\n\t\tsamples.Sort();\n\t\tvar unique = new List\u003Cfloat\u003E(samples.Count);\n\t\tforeach (float sample in samples)\n\t\t{\n\t\t\tif (unique.Count == 0 || sample - unique[^1] \u003E Epsilon)\n\t\t\t\tunique.Add(sample);\n\t\t}\n\n\t\tif (unique.Count == 1)\n\t\t\tunique.Add(totalLength);\n\n\t\treturn unique;\n\t}\n\n\tprivate static WaymakerFrame SampleFrame(Spline spline, float distance)\n\t{\n\t\tvar sample = spline.SampleAtDistance(distance);\n\t\tVector3 tangent = sample.Tangent.Normal;\n\t\tif (tangent.IsNearZeroLength)\n\t\t\ttangent = Vector3.Forward;\n\n\t\tVector3 up = sample.Up;\n\t\tif (up.IsNearZeroLength)\n\t\t\tup = Vector3.Up;\n\t\tif (MathF.Abs(Vector3.Dot(tangent, up.Normal)) \u003E 0.999f)\n\t\t\tup = Vector3.Right;\n\n\t\tup = Rotation.FromAxis(tangent, sample.Roll) * up;\n\t\tRotation rotation = Rotation.LookAt(tangent, up);\n\t\treturn new WaymakerFrame(sample.Position, rotation, distance);\n\t}\n\n\tinternal static float[] GetCumulativeCenterlineLengths(WaymakerFrame[] frames)\n\t{\n\t\tvar lengths = new float[frames.Length];\n\t\tfor (int i = 1; i \u003C frames.Length; i\u002B\u002B)\n\t\t\tlengths[i] = lengths[i - 1] \u002B Vector3.DistanceBetween(frames[i - 1].Position, frames[i].Position);\n\n\t\treturn lengths;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Coalesces short consecutive sections instead of narrowing the surface. Each retained quad is checked using the\n\t/// actual left/right longitudinal edge lengths at the requested surface width. Cap boundaries remain exact.\n\t/// \u003C/summary\u003E\n\tinternal static WaymakerFrame[] MergeShortEdges(\n\t\tWaymakerFrame[] frames,\n\t\tfloat halfWidth,\n\t\tfloat minimumEdgeLength,\n\t\tfloat capStartDistance = -1.0f,\n\t\tfloat capEndDistance = -1.0f)\n\t{\n\t\tif (frames.Length \u003C 3 || minimumEdgeLength \u003C= 0.0f)\n\t\t\treturn frames;\n\n\t\tvar merged = new List\u003CWaymakerFrame\u003E { frames[0] };\n\t\tint spanStart = 0;\n\n\t\tfor (int i = 1; i \u003C frames.Length; i\u002B\u002B)\n\t\t{\n\t\t\tbool isEnd = i == frames.Length - 1;\n\t\t\tbool isCapBoundary = IsAtDistance(frames[i].SplineDistance, capStartDistance)\n\t\t\t\t|| IsAtDistance(frames[i].SplineDistance, capEndDistance);\n\t\t\tif (!isEnd \u0026\u0026 !isCapBoundary)\n\t\t\t\tcontinue;\n\n\t\t\tAppendSpan(merged, frames, spanStart, i, halfWidth, minimumEdgeLength);\n\t\t\tspanStart = i;\n\t\t}\n\n\t\treturn merged.ToArray();\n\t}\n\n\tprivate static void AppendSpan(\n\t\tList\u003CWaymakerFrame\u003E merged,\n\t\tWaymakerFrame[] frames,\n\t\tint spanStart,\n\t\tint spanEnd,\n\t\tfloat halfWidth,\n\t\tfloat minimumEdgeLength)\n\t{\n\t\tint currentStart = spanStart;\n\n\t\tfor (int candidate = spanStart \u002B 1; candidate \u003C spanEnd; candidate\u002B\u002B)\n\t\t{\n\t\t\tif (!HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[candidate], halfWidth, minimumEdgeLength))\n\t\t\t\tcontinue;\n\n\t\t\t// Avoid accepting a short quad at the tail when the same span can be represented by one valid longer quad.\n\t\t\tbool leavesShortTail = !HasMinimumLongitudinalEdgeLength(frames[candidate], frames[spanEnd], halfWidth, minimumEdgeLength);\n\t\t\tbool fullSpanFits = HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[spanEnd], halfWidth, minimumEdgeLength);\n\t\t\tif (leavesShortTail \u0026\u0026 fullSpanFits)\n\t\t\t\tcontinue;\n\n\t\t\tmerged.Add(frames[candidate]);\n\t\t\tcurrentStart = candidate;\n\t\t}\n\n\t\tif (spanEnd \u003E currentStart)\n\t\t\tmerged.Add(frames[spanEnd]);\n\t}\n\n\tinternal static bool HasMinimumLongitudinalEdgeLength(WaymakerFrame start, WaymakerFrame end, float halfWidth, float minimumEdgeLength)\n\t{\n\t\tVector3 centerDelta = end.Position - start.Position;\n\t\tif (centerDelta.Length \u003C= Epsilon)\n\t\t\treturn false;\n\n\t\tVector3 centerDirection = centerDelta.Normal;\n\t\tVector3 leftEdge = (end.Position - end.Right * halfWidth) - (start.Position - start.Right * halfWidth);\n\t\tVector3 rightEdge = (end.Position \u002B end.Right * halfWidth) - (start.Position \u002B start.Right * halfWidth);\n\n\t\treturn IsLongitudinalEdgeLongEnough(leftEdge, centerDirection, minimumEdgeLength)\n\t\t\t\u0026\u0026 IsLongitudinalEdgeLongEnough(rightEdge, centerDirection, minimumEdgeLength);\n\t}\n\n\tprivate static bool IsLongitudinalEdgeLongEnough(Vector3 edge, Vector3 centerDirection, float minimumEdgeLength)\n\t{\n\t\treturn edge.Length \u003E= minimumEdgeLength \u0026\u0026 Vector3.Dot(edge, centerDirection) \u003E 0.0f;\n\t}\n\n\tprivate static bool IsAtDistance(float actual, float expected)\n\t{\n\t\treturn expected \u003E= 0.0f \u0026\u0026 MathF.Abs(actual - expected) \u003C= Epsilon;\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Code/Waymaker/Waymaker.cs","FileName":"Waymaker.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System;\nusing System.Linq;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\n/// \u003Csummary\u003E\n/// Generates a UV-mapped surface that follows an editable spline.\n/// \u003C/summary\u003E\n[Icon(\u0022route\u0022)]\npublic partial class Waymaker : Component, Component.ExecuteInEditor, Component.IHasBounds\n{\n\tprivate const string GeneratedMeshTag = \u0022waymaker_generated_mesh\u0022;\n\tprivate const float SampleSpacing = 40.0f;\n\n\t[Property, Feature(\u0022Waymaker\u0022), Hide]\n\tpublic Spline Spline\n\t{\n\t\tget;\n\t\tset\n\t\t{\n\t\t\tfield = value ?? new Spline();\n\t\t\tSubscribeToSpline();\n\t\t\tMarkDirty();\n\t\t}\n\t} = new();\n\n\t[Property(Title = \u0022Loop\u0022), Feature(\u0022Spline\u0022, Icon = \u0022all_inclusive\u0022)]\n\tprivate bool Loop\n\t{\n\t\tget =\u003E Spline?.IsLoop ?? false;\n\t\tset\n\t\t{\n\t\t\tif (Spline is null || Spline.IsLoop == value || (value \u0026\u0026 Spline.PointCount \u003C 2))\n\t\t\t\treturn;\n\n\t\t\tSpline.IsLoop = value;\n\t\t\tMarkDirty();\n\t\t}\n\t}\n\n\tprivate Spline m_SubscribedSpline;\n\tprivate bool m_IsDirty = true;\n\n\t[Property(Title = \u0022Source Material\u0022), Feature(\u0022Surface\u0022, Icon = \u0022texture\u0022), Validate(nameof(IsSourceMaterialAssigned), \u0022Assign a source material to generate the Waymaker surface.\u0022, LogLevel.Warn)]\n\tprivate Material SourceMaterial { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Surface Width\u0022), Feature(\u0022Surface\u0022)]\n\tprivate float SurfaceWidth { get; set { field = Math.Max(0.01f, value); MarkDirty(); } } = 500.0f;\n\n\t[Property(Title = \u0022Dynamic V Tiling\u0022), Feature(\u0022Texturing\u0022, Icon = \u0022texture\u0022)]\n\tprivate bool DynamicVTiling { get; set { field = value; MarkDirty(); } } = true;\n\n\t[Property(Title = \u0022Dynamic V Tile Length\u0022), Feature(\u0022Texturing\u0022), ShowIf(nameof(DynamicVTiling), true)]\n\tprivate float DynamicVTileLength { get; set { field = Math.Max(0.1f, value); MarkDirty(); } } = 500.0f;\n\n\t[Property(Title = \u0022Vertical Tiles\u0022), Feature(\u0022Texturing\u0022), ShowIf(nameof(DynamicVTiling), false)]\n\tprivate int VerticalTiles { get; set { field = Math.Max(1, value); MarkDirty(); } } = 1;\n\n\t[Property(Title = \u0022Horizontal Tiling\u0022), Feature(\u0022Texturing\u0022)]\n\tprivate float HorizontalTiling { get; set { field = Math.Max(0.01f, value); MarkDirty(); } } = 1.0f;\n\n\t[Property(Title = \u0022Horizontal Padding\u0022), Feature(\u0022Texturing\u0022), Range(0.0f, 1.0f)]\n\tprivate float HorizontalPadding { get; set { field = value.Clamp(0.0f, 1.0f); MarkDirty(); } } = 0.0f;\n\n\t[Property(Title = \u0022Cap Texture\u0022), Feature(\u0022Caps\u0022, Icon = \u0022vertical_align_bottom\u0022), ShowIf(nameof(CanUseCapSettings), true)]\n\tprivate bool CapTexture { get; set { field = value; MarkDirty(); } } = false;\n\n\t[Property(Title = \u0022Cap Texture Start\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate Material CapTextureStart { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Cap Texture End\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate Material CapTextureEnd { get; set { field = value; MarkDirty(); } }\n\n\t[Property(Title = \u0022Cap Length\u0022), Feature(\u0022Caps\u0022), ShowIf(nameof(ShowCapMaterials), true)]\n\tprivate float CapLength { get; set { field = Math.Max(0.0f, value); MarkDirty(); } } = 100.0f;\n\n\t// Keep the serialized member name so existing scenes retain their value; the inspector now reflects its edge-length meaning.\n\t[Property(Title = \u0022Minimum Edge Length\u0022), Feature(\u0022Geometry\u0022), Validate(nameof(IsMinimumEdgeLengthInRecommendedRange), \u0022Values above 0.1 may create coarser quads and distort tight turns.\u0022, LogLevel.Warn)]\n\tprivate float MinimumEdgeWidth { get; set { field = Math.Max(0.0f, value); MarkDirty(); } } = 0.01f;\n\n\tprivate bool CanUseCapSettings =\u003E Spline is not null \u0026\u0026 WaymakerMeshBuilder.CanUseCaps(DynamicVTiling, Spline.IsLoop);\n\tprivate bool ShowCapMaterials =\u003E CanUseCapSettings \u0026\u0026 CapTexture;\n\n\tpublic BBox LocalBounds\n\t{\n\t\tget\n\t\t{\n\t\t\tvar bounds = Spline?.Bounds ?? new BBox();\n\t\t\tvar padding = new Vector3(SurfaceWidth * 0.5f);\n\t\t\treturn new BBox(bounds.Mins - padding, bounds.Maxs \u002B padding);\n\t\t}\n\t}\n\n\tpublic Waymaker()\n\t{\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = Vector3.Zero });\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1000.0f, 0.0f, 0.0f) });\n\t\tSpline.InsertPoint(Spline.PointCount, new Spline.Point { Position = new Vector3(1600.0f, 1000.0f, 0.0f) });\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\tSubscribeToSpline();\n\t\tEnsureGeneratedMeshExists();\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tUnsubscribeFromSpline();\n\t\tRemoveGeneratedMesh();\n\t}\n\n\tprotected override void OnValidate()\n\t{\n\t\tSubscribeToSpline();\n\t\tMarkDirty();\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tSyncSplineSubscription();\n\n\t\tif (!m_IsDirty || !Scene.IsEditor)\n\t\t\treturn;\n\n\t\tRebuildGeneratedMesh();\n\t}\n\n\tprivate bool IsMinimumEdgeLengthInRecommendedRange(float value) =\u003E value \u003C= 0.1f;\n\tprivate bool IsSourceMaterialAssigned(Material material) =\u003E material is not null;\n\n\tprivate void MarkDirty() =\u003E m_IsDirty = true;\n\n\tprivate void SyncSplineSubscription()\n\t{\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\n\t\t\treturn;\n\n\t\tSubscribeToSpline();\n\t\tMarkDirty();\n\t}\n\n\tprivate void SubscribeToSpline()\n\t{\n\t\tif (ReferenceEquals(m_SubscribedSpline, Spline))\n\t\t\treturn;\n\n\t\tUnsubscribeFromSpline();\n\t\tm_SubscribedSpline = Spline;\n\n\t\tif (m_SubscribedSpline is not null)\n\t\t\tm_SubscribedSpline.SplineChanged \u002B= MarkDirty;\n\t}\n\n\tprivate void UnsubscribeFromSpline()\n\t{\n\t\tif (m_SubscribedSpline is null)\n\t\t\treturn;\n\n\t\tm_SubscribedSpline.SplineChanged -= MarkDirty;\n\t\tm_SubscribedSpline = null;\n\t}\n\n\tprivate void EnsureGeneratedMeshExists()\n\t{\n\t\tif (!Scene.IsEditor || HasGeneratedMesh())\n\t\t\treturn;\n\n\t\tRebuildGeneratedMesh();\n\t}\n\n\tprivate void RebuildGeneratedMesh()\n\t{\n\t\tif (!Scene.IsEditor)\n\t\t\treturn;\n\n\t\tRemoveGeneratedMesh();\n\n\t\tif (Spline is { PointCount: \u003E= 2 } \u0026\u0026 SourceMaterial is not null \u0026\u0026 Spline.Length \u003E 0.001f)\n\t\t{\n\t\t\tvar settings = new WaymakerMeshSettings\n\t\t\t{\n\t\t\t\tSourceMaterial = SourceMaterial,\n\t\t\t\tStartCapMaterial = CapTextureStart,\n\t\t\t\tEndCapMaterial = CapTextureEnd,\n\t\t\t\tSurfaceWidth = SurfaceWidth,\n\t\t\t\tDynamicVTiling = DynamicVTiling,\n\t\t\t\tDynamicVTileLength = DynamicVTileLength,\n\t\t\t\tVerticalTiles = VerticalTiles,\n\t\t\t\tHorizontalTiling = HorizontalTiling,\n\t\t\t\tHorizontalPadding = HorizontalPadding,\n\t\t\t\tUseCaps = CapTexture \u0026\u0026 CanUseCapSettings,\n\t\t\t\tCapLength = CapLength,\n\t\t\t\tMinimumEdgeLength = MinimumEdgeWidth\n\t\t\t};\n\n\t\t\tvar mesh = WaymakerMeshBuilder.Build(Spline, settings, SampleSpacing);\n\t\t\tif (mesh is not null \u0026\u0026 mesh.FaceHandles.Any())\n\t\t\t{\n\t\t\t\tvar child = new GameObject(GameObject, true, \u0022Waymaker Surface\u0022);\n\t\t\t\tchild.Tags.Add(GeneratedMeshTag);\n\n\t\t\t\tvar meshComponent = child.AddComponent\u003CMeshComponent\u003E();\n\t\t\t\tmeshComponent.Mesh = mesh;\n\t\t\t\tmeshComponent.SmoothingAngle = 40.0f;\n\t\t\t}\n\t\t}\n\n\t\tm_IsDirty = false;\n\t}\n\n\tprivate bool HasGeneratedMesh() =\u003E GameObject.Children.Any(child =\u003E child.Tags.Has(GeneratedMeshTag));\n\n\tprivate void RemoveGeneratedMesh()\n\t{\n\t\tif (!GameObject.IsValid())\n\t\t\treturn;\n\n\t\tforeach (var child in GameObject.Children.Where(child =\u003E child.Tags.Has(GeneratedMeshTag)).ToArray())\n\t\t\tchild.Destroy();\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Waymaker/WaymakerMeshBuilder.cs","FileName":"WaymakerMeshBuilder.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\ninternal struct WaymakerMeshSettings\n{\n\tpublic Material SourceMaterial;\n\tpublic Material StartCapMaterial;\n\tpublic Material EndCapMaterial;\n\tpublic float SurfaceWidth;\n\tpublic bool DynamicVTiling;\n\tpublic float DynamicVTileLength;\n\tpublic int VerticalTiles;\n\tpublic float HorizontalTiling;\n\tpublic float HorizontalPadding;\n\tpublic bool UseCaps;\n\tpublic float CapLength;\n\tpublic float MinimumEdgeLength;\n}\n\ninternal readonly record struct WaymakerFrame(Vector3 Position, Rotation Rotation, float SplineDistance)\n{\n\tpublic Vector3 Right =\u003E Rotation.Right;\n}\n\n/// \u003Csummary\u003E\n/// Samples Waymaker\u0027s spline and builds the textured quad strip, including UVs and optional endpoint cap materials.\n/// \u003C/summary\u003E\ninternal static class WaymakerMeshBuilder\n{\n\tprivate const float Epsilon = 0.001f;\n\n\tinternal static (float Min, float Max) GetMainURange(float horizontalTiling, float horizontalPadding)\n\t{\n\t\tfloat tiling = Math.Max(0.01f, horizontalTiling);\n\t\tfloat padding = Math.Min(Math.Max(0.0f, horizontalPadding), tiling);\n\t\treturn (padding * 0.5f, tiling - padding * 0.5f);\n\t}\n\n\tinternal static (float Min, float Max) GetDynamicVRange(float startDistance, float endDistance, float tileLength)\n\t{\n\t\tfloat length = Math.Max(Epsilon, tileLength);\n\t\treturn (startDistance / length, endDistance / length);\n\t}\n\n\tinternal static (float Min, float Max) GetFixedVRange(float startDistance, float endDistance, float totalLength, int verticalTiles)\n\t{\n\t\tfloat length = Math.Max(Epsilon, totalLength);\n\t\tfloat tiles = Math.Max(1, verticalTiles);\n\t\treturn (startDistance / length * tiles, endDistance / length * tiles);\n\t}\n\n\tinternal static bool CanUseCaps(bool dynamicVTiling, bool isLoop) =\u003E !dynamicVTiling \u0026\u0026 !isLoop;\n\n\tinternal static float GetEffectiveCapLength(float totalLength, float requestedLength, bool useCaps, bool dynamicVTiling, bool isLoop)\n\t{\n\t\tif (!useCaps || !CanUseCaps(dynamicVTiling, isLoop))\n\t\t\treturn 0.0f;\n\n\t\treturn Math.Clamp(requestedLength, 0.0f, Math.Max(0.0f, totalLength) * 0.5f);\n\t}\n\n\tpublic static PolygonMesh Build(Spline spline, WaymakerMeshSettings settings, float sampleSpacing)\n\t{\n\t\tif (spline is null || spline.PointCount \u003C 2 || spline.Length \u003C= Epsilon)\n\t\t\treturn null;\n\n\t\tfloat totalLength = spline.Length;\n\t\tbool useCaps = settings.UseCaps \u0026\u0026 CanUseCaps(settings.DynamicVTiling, spline.IsLoop);\n\t\tfloat capLength = GetEffectiveCapLength(totalLength, settings.CapLength, useCaps, settings.DynamicVTiling, spline.IsLoop);\n\n\t\tvar distances = CreateSampleDistances(spline, sampleSpacing, capLength);\n\t\tif (distances.Count \u003C 2)\n\t\t\treturn null;\n\n\t\tvar sampledFrames = distances.Select(distance =\u003E SampleFrame(spline, distance)).ToArray();\n\t\tfloat halfWidth = Math.Max(0.0f, settings.SurfaceWidth) * 0.5f;\n\t\tvar frames = MergeShortEdges(sampledFrames, halfWidth, Math.Max(0.0f, settings.MinimumEdgeLength),\n\t\t\tcapLength \u003E Epsilon ? capLength : -1.0f,\n\t\t\tcapLength \u003E Epsilon ? totalLength - capLength : -1.0f);\n\t\tif (frames.Length \u003C 2)\n\t\t\treturn null;\n\n\t\tvar cumulativeCenterlineLengths = GetCumulativeCenterlineLengths(frames);\n\n\t\tvar mesh = new PolygonMesh();\n\t\tvar vertices = new HalfEdgeMesh.VertexHandle[frames.Length][];\n\n\t\tfor (int i = 0; i \u003C frames.Length; i\u002B\u002B)\n\t\t{\n\t\t\tVector3 halfSpan = frames[i].Right * halfWidth;\n\t\t\tvertices[i] = mesh.AddVertices(frames[i].Position - halfSpan, frames[i].Position \u002B halfSpan);\n\t\t}\n\n\t\tvar mainURange = GetMainURange(settings.HorizontalTiling, settings.HorizontalPadding);\n\t\tfloat mainU0 = mainURange.Min;\n\t\tfloat mainU1 = mainURange.Max;\n\t\tint verticalTiles = Math.Max(1, settings.VerticalTiles);\n\t\tfloat dynamicTileLength = Math.Max(Epsilon, settings.DynamicVTileLength);\n\t\tfloat interiorStart = capLength;\n\t\tfloat interiorEnd = Math.Max(interiorStart, totalLength - capLength);\n\t\tfloat interiorLength = Math.Max(0.0f, interiorEnd - interiorStart);\n\n\t\tfor (int i = 0; i \u003C frames.Length - 1; i\u002B\u002B)\n\t\t{\n\t\t\tfloat distance0 = frames[i].SplineDistance;\n\t\t\tfloat distance1 = frames[i \u002B 1].SplineDistance;\n\t\t\tfloat centerline0 = cumulativeCenterlineLengths[i];\n\t\t\tfloat centerline1 = cumulativeCenterlineLengths[i \u002B 1];\n\t\t\tfloat segmentMidpoint = (distance0 \u002B distance1) * 0.5f;\n\n\t\t\tbool isStartCap = capLength \u003E Epsilon \u0026\u0026 segmentMidpoint \u003C capLength;\n\t\t\tbool isEndCap = capLength \u003E Epsilon \u0026\u0026 segmentMidpoint \u003E totalLength - capLength;\n\n\t\t\tMaterial material = isStartCap\n\t\t\t\t? settings.StartCapMaterial ?? settings.SourceMaterial\n\t\t\t\t: isEndCap\n\t\t\t\t\t? settings.EndCapMaterial ?? settings.SourceMaterial\n\t\t\t\t\t: settings.SourceMaterial;\n\n\t\t\tfloat v0;\n\t\t\tfloat v1;\n\t\t\tif (isStartCap)\n\t\t\t{\n\t\t\t\tv0 = (distance0 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t\tv1 = (distance1 / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t}\n\t\t\telse if (isEndCap)\n\t\t\t{\n\t\t\t\tv0 = ((distance0 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t\tv1 = ((distance1 - (totalLength - capLength)) / Math.Max(Epsilon, capLength)).Clamp(0.0f, 1.0f);\n\t\t\t}\n\t\t\telse if (settings.DynamicVTiling)\n\t\t\t{\n\t\t\t\tvar vRange = GetDynamicVRange(centerline0, centerline1, dynamicTileLength);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\t\t\telse if (useCaps \u0026\u0026 interiorLength \u003E Epsilon)\n\t\t\t{\n\t\t\t\tvar vRange = GetFixedVRange(distance0 - interiorStart, distance1 - interiorStart, interiorLength, verticalTiles);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tvar vRange = GetFixedVRange(centerline0, centerline1, cumulativeCenterlineLengths[^1], verticalTiles);\n\t\t\t\tv0 = vRange.Min;\n\t\t\t\tv1 = vRange.Max;\n\t\t\t}\n\n\t\t\tWaymakerMeshUtility.AddTexturedQuad(mesh, material,\n\t\t\t\tvertices[i][0], vertices[i][1], vertices[i \u002B 1][1], vertices[i \u002B 1][0],\n\t\t\t\tnew Vector2(mainU0, v0), new Vector2(mainU1, v0),\n\t\t\t\tnew Vector2(mainU1, v1), new Vector2(mainU0, v1));\n\t\t}\n\n\t\tmesh.RemoveBadGeometry();\n\n\t\t// Translucent shaders multiply texture alpha by vertex alpha. Explicitly initialize every half-edge color;\n\t\t// leaving PolygonMesh\u0027s color stream sparse can make some translucent face corners resolve to zero alpha.\n\t\tforeach (var halfEdge in mesh.HalfEdgeHandles)\n\t\t{\n\t\t\tmesh.SetVertexColor(halfEdge, Color32.White);\n\t\t\tif (halfEdge.OppositeEdge.IsValid)\n\t\t\t\tmesh.SetVertexColor(halfEdge.OppositeEdge, Color32.White);\n\t\t}\n\n\t\treturn mesh;\n\t}\n\n\tinternal static List\u003Cfloat\u003E CreateSampleDistances(Spline spline, float sampleSpacing, float capLength)\n\t{\n\t\tfloat totalLength = spline.Length;\n\t\tfloat spacing = Math.Max(1.0f, sampleSpacing);\n\t\tvar samples = new List\u003Cfloat\u003E { 0.0f, totalLength };\n\n\t\tint subdivisions = Math.Max(2, (int)MathF.Ceiling(totalLength / spacing));\n\t\tfor (int i = 1; i \u003C subdivisions; i\u002B\u002B)\n\t\t\tsamples.Add(totalLength * i / subdivisions);\n\n\t\tfor (int i = 0; i \u003C spline.PointCount; i\u002B\u002B)\n\t\t\tsamples.Add(spline.GetDistanceAtPoint(i).Clamp(0.0f, totalLength));\n\n\t\tif (capLength \u003E Epsilon)\n\t\t{\n\t\t\tsamples.Add(capLength);\n\t\t\tsamples.Add(totalLength - capLength);\n\t\t}\n\n\t\tsamples.Sort();\n\t\tvar unique = new List\u003Cfloat\u003E(samples.Count);\n\t\tforeach (float sample in samples)\n\t\t{\n\t\t\tif (unique.Count == 0 || sample - unique[^1] \u003E Epsilon)\n\t\t\t\tunique.Add(sample);\n\t\t}\n\n\t\tif (unique.Count == 1)\n\t\t\tunique.Add(totalLength);\n\n\t\treturn unique;\n\t}\n\n\tprivate static WaymakerFrame SampleFrame(Spline spline, float distance)\n\t{\n\t\tvar sample = spline.SampleAtDistance(distance);\n\t\tVector3 tangent = sample.Tangent.Normal;\n\t\tif (tangent.IsNearZeroLength)\n\t\t\ttangent = Vector3.Forward;\n\n\t\tVector3 up = sample.Up;\n\t\tif (up.IsNearZeroLength)\n\t\t\tup = Vector3.Up;\n\t\tif (MathF.Abs(Vector3.Dot(tangent, up.Normal)) \u003E 0.999f)\n\t\t\tup = Vector3.Right;\n\n\t\tup = Rotation.FromAxis(tangent, sample.Roll) * up;\n\t\tRotation rotation = Rotation.LookAt(tangent, up);\n\t\treturn new WaymakerFrame(sample.Position, rotation, distance);\n\t}\n\n\tinternal static float[] GetCumulativeCenterlineLengths(WaymakerFrame[] frames)\n\t{\n\t\tvar lengths = new float[frames.Length];\n\t\tfor (int i = 1; i \u003C frames.Length; i\u002B\u002B)\n\t\t\tlengths[i] = lengths[i - 1] \u002B Vector3.DistanceBetween(frames[i - 1].Position, frames[i].Position);\n\n\t\treturn lengths;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Coalesces short consecutive sections instead of narrowing the surface. Each retained quad is checked using the\n\t/// actual left/right longitudinal edge lengths at the requested surface width. Cap boundaries remain exact.\n\t/// \u003C/summary\u003E\n\tinternal static WaymakerFrame[] MergeShortEdges(\n\t\tWaymakerFrame[] frames,\n\t\tfloat halfWidth,\n\t\tfloat minimumEdgeLength,\n\t\tfloat capStartDistance = -1.0f,\n\t\tfloat capEndDistance = -1.0f)\n\t{\n\t\tif (frames.Length \u003C 3 || minimumEdgeLength \u003C= 0.0f)\n\t\t\treturn frames;\n\n\t\tvar merged = new List\u003CWaymakerFrame\u003E { frames[0] };\n\t\tint spanStart = 0;\n\n\t\tfor (int i = 1; i \u003C frames.Length; i\u002B\u002B)\n\t\t{\n\t\t\tbool isEnd = i == frames.Length - 1;\n\t\t\tbool isCapBoundary = IsAtDistance(frames[i].SplineDistance, capStartDistance)\n\t\t\t\t|| IsAtDistance(frames[i].SplineDistance, capEndDistance);\n\t\t\tif (!isEnd \u0026\u0026 !isCapBoundary)\n\t\t\t\tcontinue;\n\n\t\t\tAppendSpan(merged, frames, spanStart, i, halfWidth, minimumEdgeLength);\n\t\t\tspanStart = i;\n\t\t}\n\n\t\treturn merged.ToArray();\n\t}\n\n\tprivate static void AppendSpan(\n\t\tList\u003CWaymakerFrame\u003E merged,\n\t\tWaymakerFrame[] frames,\n\t\tint spanStart,\n\t\tint spanEnd,\n\t\tfloat halfWidth,\n\t\tfloat minimumEdgeLength)\n\t{\n\t\tint currentStart = spanStart;\n\n\t\tfor (int candidate = spanStart \u002B 1; candidate \u003C spanEnd; candidate\u002B\u002B)\n\t\t{\n\t\t\tif (!HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[candidate], halfWidth, minimumEdgeLength))\n\t\t\t\tcontinue;\n\n\t\t\t// Avoid accepting a short quad at the tail when the same span can be represented by one valid longer quad.\n\t\t\tbool leavesShortTail = !HasMinimumLongitudinalEdgeLength(frames[candidate], frames[spanEnd], halfWidth, minimumEdgeLength);\n\t\t\tbool fullSpanFits = HasMinimumLongitudinalEdgeLength(frames[currentStart], frames[spanEnd], halfWidth, minimumEdgeLength);\n\t\t\tif (leavesShortTail \u0026\u0026 fullSpanFits)\n\t\t\t\tcontinue;\n\n\t\t\tmerged.Add(frames[candidate]);\n\t\t\tcurrentStart = candidate;\n\t\t}\n\n\t\tif (spanEnd \u003E currentStart)\n\t\t\tmerged.Add(frames[spanEnd]);\n\t}\n\n\tinternal static bool HasMinimumLongitudinalEdgeLength(WaymakerFrame start, WaymakerFrame end, float halfWidth, float minimumEdgeLength)\n\t{\n\t\tVector3 centerDelta = end.Position - start.Position;\n\t\tif (centerDelta.Length \u003C= Epsilon)\n\t\t\treturn false;\n\n\t\tVector3 centerDirection = centerDelta.Normal;\n\t\tVector3 leftEdge = (end.Position - end.Right * halfWidth) - (start.Position - start.Right * halfWidth);\n\t\tVector3 rightEdge = (end.Position \u002B end.Right * halfWidth) - (start.Position \u002B start.Right * halfWidth);\n\n\t\treturn IsLongitudinalEdgeLongEnough(leftEdge, centerDirection, minimumEdgeLength)\n\t\t\t\u0026\u0026 IsLongitudinalEdgeLongEnough(rightEdge, centerDirection, minimumEdgeLength);\n\t}\n\n\tprivate static bool IsLongitudinalEdgeLongEnough(Vector3 edge, Vector3 centerDirection, float minimumEdgeLength)\n\t{\n\t\treturn edge.Length \u003E= minimumEdgeLength \u0026\u0026 Vector3.Dot(edge, centerDirection) \u003E 0.0f;\n\t}\n\n\tprivate static bool IsAtDistance(float actual, float expected)\n\t{\n\t\treturn expected \u003E= 0.0f \u0026\u0026 MathF.Abs(actual - expected) \u003C= Epsilon;\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Waymaker/WaymakerMeshUtility.cs","FileName":"WaymakerMeshUtility.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":443795,"IsPrivate":false,"Code":"using System.Collections.Generic;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker;\n\n/// \u003Csummary\u003E\n/// Waymaker-owned helpers for assigning UVs and materials to generated polygon faces.\n/// \u003C/summary\u003E\ninternal static class WaymakerMeshUtility\n{\n\tpublic static void AddTexturedQuad(PolygonMesh mesh, Material material,\n\t\tHalfEdgeMesh.VertexHandle a, HalfEdgeMesh.VertexHandle b,\n\t\tHalfEdgeMesh.VertexHandle c, HalfEdgeMesh.VertexHandle d,\n\t\tVector2 uvA, Vector2 uvB, Vector2 uvC, Vector2 uvD)\n\t{\n\t\tif (a.Equals(b) || a.Equals(c) || a.Equals(d) || b.Equals(c) || b.Equals(d) || c.Equals(d))\n\t\t\treturn;\n\n\t\tvar face = mesh.AddFace(a, b, c, d);\n\t\tif (!face.IsValid)\n\t\t\treturn;\n\n\t\tif (material is not null)\n\t\t\tmesh.SetFaceMaterial(face, material);\n\n\t\tmesh.SetFaceTextureCoords(face, new List\u003CVector2\u003E { uvA, uvB, uvC, uvD });\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Editor/Waymaker/WaymakerSplineEditorWindow.cs","FileName":"WaymakerSplineEditorWindow.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":443795,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Editor;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker.Editor;\n\n/// \u003Csummary\u003E\n/// Waymaker-owned spline control sheet and gizmos.\n/// \u003C/summary\u003E\npublic sealed class WaymakerSplineEditorWindow : WidgetWindow\n{\n\tprivate const float WaymakerHeaderHeight = 32.0f;\n\tprivate const float GizmoBoxSize = 2.0f;\n\tprivate const float LineThickness = 2.0f;\n\tprivate const float TangentLineThickness = 0.8f;\n\tprivate const float GeneratedWireThickness = 1.0f;\n\n\tprivate static bool s_IsClosed;\n\tprivate Waymaker m_Target;\n\tprivate ControlWidget m_InTangentControl;\n\tprivate ControlWidget m_OutTangentControl;\n\tprivate bool m_InTangentSelected;\n\tprivate bool m_OutTangentSelected;\n\tprivate bool m_DraggingOutNewPoint;\n\tprivate bool m_MoveInProgress;\n\tprivate List\u003CVector3\u003E m_Polyline = [];\n\tprivate IDisposable m_MovementUndoScope;\n\n\t/// \u003Csummary\u003E\n\t/// Editor-facing tangent mode enum with registered icons; Spline.HandleMode itself has no editor enum control.\n\t/// \u003C/summary\u003E\n\tpublic enum WaymakerHandleMode\n\t{\n\t\t[Icon(\u0022auto_fix_high\u0022)]\n\t\tAuto,\n\n\t\t[Icon(\u0022show_chart\u0022)]\n\t\tLinear,\n\n\t\t[Icon(\u0022open_in_full\u0022)]\n\t\tMirrored,\n\n\t\t[Icon(\u0022call_split\u0022)]\n\t\tSplit\n\t}\n\n\tpublic WaymakerSplineEditorWindow()\n\t{\n\t\tContentMargins = 0;\n\t\tLayout = Layout.Column();\n\t\tMaximumWidth = 500;\n\t\tMinimumWidth = 300;\n\t\tRebuild();\n\t}\n\n\tpublic void OnDisabled()\n\t{\n\t\tm_MovementUndoScope?.Dispose();\n\t\tm_MovementUndoScope = null;\n\t}\n\n\tpublic void OnUpdate()\n\t{\n\t\tif (m_Target.IsValid())\n\t\t\tDrawGizmos();\n\t}\n\n\tpublic void OnSelectionChanged(Waymaker target)\n\t{\n\t\tm_Target = target;\n\t\tSelectedPointIndex = 0;\n\t\tm_InTangentSelected = false;\n\t\tm_OutTangentSelected = false;\n\t\tRebuild();\n\t}\n\n\t[Title(\u0022Tangent Mode\u0022)]\n\tprivate WaymakerHandleMode _selectedPointTangentMode\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? (WaymakerHandleMode)SelectedPoint.Mode : WaymakerHandleMode.Auto;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Mode = (Spline.HandleMode)value });\n\t}\n\n\t[Title(\u0022Position\u0022)]\n\tprivate Vector3 _selectedPointPosition\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.Position : Vector3.Zero;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Position = value });\n\t}\n\n\t[Title(\u0022In\u0022)]\n\tprivate Vector3 _selectedPointIn\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.In : Vector3.Zero;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { In = value });\n\t}\n\n\t[Title(\u0022Out\u0022)]\n\tprivate Vector3 _selectedPointOut\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.Out : Vector3.Zero;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Out = value });\n\t}\n\n\t[Title(\u0022Roll (Degrees)\u0022)]\n\tprivate float _selectedPointRoll\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.Roll : 0.0f;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Roll = value });\n\t}\n\n\t[Title(\u0022Scale (_, Width, Height)\u0022)]\n\tprivate Vector3 _selectedPointScale\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.Scale : Vector3.One;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Scale = value });\n\t}\n\n\t[Title(\u0022Up Vector\u0022)]\n\tprivate Vector3 _selectedPointUp\n\t{\n\t\tget =\u003E IsSelectedPointValid() ? SelectedPoint.Up : Vector3.Up;\n\t\tset =\u003E UpdateSelectedPoint(SelectedPoint with { Up = value });\n\t}\n\n\tprivate int SelectedPointIndex { get; set; }\n\tprivate Spline.Point SelectedPoint =\u003E IsSelectedPointValid() ? m_Target.Spline.GetPoint(SelectedPointIndex) : new Spline.Point();\n\n\tprivate bool IsSelectedPointValid() =\u003E m_Target.IsValid() \u0026\u0026 SelectedPointIndex \u003E= 0 \u0026\u0026 SelectedPointIndex \u003C m_Target.Spline.PointCount;\n\n\tprivate void UpdateSelectedPoint(Spline.Point point)\n\t{\n\t\tif (!IsSelectedPointValid())\n\t\t\treturn;\n\n\t\tusing (CreateUndoScope(\u0022Update Waymaker Spline Point\u0022))\n\t\t\tm_Target.Spline.UpdatePoint(SelectedPointIndex, point);\n\n\t\tToggleTangentInput();\n\t}\n\n\tprivate void Rebuild()\n\t{\n\t\tLayout.Clear(true);\n\t\tLayout.Margin = 0;\n\t\tIcon = s_IsClosed ? \u0022\u0022 : \u0022route\u0022;\n\t\tWindowTitle = s_IsClosed ? \u0022\u0022 : $\u0022Waymaker Spline Editor - {m_Target?.GameObject?.Name ?? \u0022\u0022}\u0022;\n\t\tIsGrabbable = !s_IsClosed;\n\n\t\tif (s_IsClosed)\n\t\t{\n\t\t\tvar closedRow = Layout.AddRow();\n\t\t\tclosedRow.Add(new IconButton(\u0022route\u0022, ReopenWindow)\n\t\t\t{\n\t\t\t\tToolTip = \u0022Open Waymaker Spline Editor\u0022,\n\t\t\t\tFixedHeight = WaymakerHeaderHeight,\n\t\t\t\tFixedWidth = WaymakerHeaderHeight,\n\t\t\t\tBackground = Color.Transparent\n\t\t\t});\n\t\t\tMinimumWidth = 0;\n\t\t\treturn;\n\t\t}\n\n\t\tMinimumWidth = 400;\n\t\tBuildHeader();\n\t\tif (m_Target.IsValid())\n\t\t\tBuildControlSheet();\n\t\tLayout.Margin = 4;\n\t}\n\n\tprivate void BuildHeader()\n\t{\n\t\tvar header = Layout.AddRow();\n\t\theader.AddStretchCell();\n\t\theader.Add(new IconButton(\u0022info\u0022)\n\t\t{\n\t\t\tToolTip = \u0022Edit spline points with the control sheet or 3D gizmos. Click a segment to insert a point; Shift-drag a point to extend the spline.\u0022,\n\t\t\tFixedHeight = WaymakerHeaderHeight,\n\t\t\tFixedWidth = WaymakerHeaderHeight,\n\t\t\tBackground = Color.Transparent\n\t\t});\n\t\theader.Add(new IconButton(\u0022close\u0022, CloseWindow)\n\t\t{\n\t\t\tToolTip = \u0022Close Editor\u0022,\n\t\t\tFixedHeight = WaymakerHeaderHeight,\n\t\t\tFixedWidth = WaymakerHeaderHeight,\n\t\t\tBackground = Color.Transparent\n\t\t});\n\t}\n\n\tprivate void BuildControlSheet()\n\t{\n\t\tvar serialized = this.GetSerialized();\n\t\tvar controlSheet = new ControlSheet();\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointTangentMode)));\n\t\tcontrolSheet.AddRow(serialized.GetProperty(nameof(_selectedPointPosition)));\n\t\tm_InTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointIn)));\n\t\tm_OutTangentControl = controlSheet.AddRow(serialized.GetProperty(nameof(_selectedPointOut)));\n\t\tcontrolSheet.AddGroup(\u0022Advanced\u0022, [\n\t\t\tserialized.GetProperty(nameof(_selectedPointRoll)),\n\t\t\tserialized.GetProperty(nameof(_selectedPointScale)),\n\t\t\tserialized.GetProperty(nameof(_selectedPointUp))\n\t\t]);\n\t\tcontrolSheet.AddLayout(BuildControlButtons());\n\t\tLayout.Add(controlSheet);\n\t\tToggleTangentInput();\n\t}\n\n\tprivate Layout BuildControlButtons()\n\t{\n\t\tvar row = Layout.Row();\n\t\trow.Spacing = 16;\n\t\trow.Margin = 8;\n\t\trow.Add(new IconButton(\u0022skip_previous\u0022, SelectPreviousPoint) { ToolTip = \u0022Go to previous point\u0022 });\n\t\trow.Add(new IconButton(\u0022skip_next\u0022, SelectNextPoint) { ToolTip = \u0022Go to next point\u0022 });\n\t\trow.Add(new IconButton(\u0022delete\u0022, DeleteSelectedPoint) { ToolTip = \u0022Delete point (at least two points are kept)\u0022 });\n\t\trow.Add(new IconButton(\u0022add\u0022, AddPointAfterSelection) { ToolTip = \u0022Insert point after current point\u0022 });\n\t\treturn row;\n\t}\n\n\tprivate void ReopenWindow()\n\t{\n\t\ts_IsClosed = false;\n\t\tRebuild();\n\t}\n\n\tprivate void SelectPreviousPoint() =\u003E SelectPoint(Math.Max(0, SelectedPointIndex - 1));\n\n\tprivate void SelectNextPoint() =\u003E SelectPoint(Math.Min(m_Target.Spline.PointCount - 1, SelectedPointIndex \u002B 1));\n\n\tprivate void DeleteSelectedPoint()\n\t{\n\t\tif (!IsSelectedPointValid() || m_Target.Spline.PointCount \u003C= 2)\n\t\t\treturn;\n\n\t\tusing (CreateUndoScope(\u0022Delete Waymaker Spline Point\u0022))\n\t\t\tm_Target.Spline.RemovePoint(SelectedPointIndex);\n\n\t\tSelectPoint(Math.Min(SelectedPointIndex, m_Target.Spline.PointCount - 1));\n\t}\n\n\tprivate void AddPointAfterSelection()\n\t{\n\t\tif (!IsSelectedPointValid())\n\t\t\treturn;\n\n\t\tusing (CreateUndoScope(\u0022Add Waymaker Spline Point\u0022))\n\t\t{\n\t\t\tvar spline = m_Target.Spline;\n\t\t\tif (SelectedPointIndex == spline.PointCount - 1)\n\t\t\t{\n\t\t\t\tfloat distance = spline.GetDistanceAtPoint(SelectedPointIndex);\n\t\t\t\tVector3 tangent = spline.SampleAtDistance(distance).Tangent;\n\t\t\t\tvar newPoint = SelectedPoint with { Position = SelectedPoint.Position \u002B tangent * 200.0f };\n\t\t\t\tspline.InsertPoint(SelectedPointIndex \u002B 1, newPoint);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tfloat current = spline.GetDistanceAtPoint(SelectedPointIndex);\n\t\t\t\tfloat next = spline.GetDistanceAtPoint(SelectedPointIndex \u002B 1);\n\t\t\t\tspline.AddPointAtDistance((current \u002B next) * 0.5f, true);\n\t\t\t}\n\t\t}\n\n\t\tSelectPoint(Math.Min(SelectedPointIndex \u002B 1, m_Target.Spline.PointCount - 1));\n\t}\n\n\tprivate void SelectPoint(int index)\n\t{\n\t\tSelectedPointIndex = Math.Clamp(index, 0, Math.Max(0, m_Target.Spline.PointCount - 1));\n\t\tm_InTangentSelected = false;\n\t\tm_OutTangentSelected = false;\n\t\tWindowTitle = $\u0022Waymaker Spline Point [{SelectedPointIndex}] Editor - {m_Target.GameObject.Name}\u0022;\n\t\tToggleTangentInput();\n\t}\n\n\tprivate void ToggleTangentInput()\n\t{\n\t\tif (!m_Target.IsValid())\n\t\t\treturn;\n\n\t\tbool editable = SelectedPoint.Mode is not (Spline.HandleMode.Auto or Spline.HandleMode.Linear);\n\t\tif (m_InTangentControl is not null)\n\t\t\tm_InTangentControl.Enabled = editable;\n\t\tif (m_OutTangentControl is not null)\n\t\t\tm_OutTangentControl.Enabled = editable;\n\t}\n\n\tprivate IDisposable CreateUndoScope(string name)\n\t{\n\t\treturn SceneEditorSession.Active.UndoScope(name).WithComponentChanges(m_Target).Push();\n\t}\n\n\tprivate void CloseWindow()\n\t{\n\t\ts_IsClosed = true;\n\t\tRebuild();\n\t\tPosition = Parent.Size - 32;\n\t}\n\n\tprivate void DrawGizmos()\n\t{\n\t\tusing (Gizmo.Scope(\u0022waymaker_spline_editor\u0022, m_Target.WorldTransform))\n\t\t{\n\t\t\tDrawSplineSegments();\n\t\t\tif (IsViewportWireframeEnabled())\n\t\t\t\tDrawGeneratedMeshWireframe();\n\t\t\tDrawPositionGizmo();\n\t\t\tDrawPointControls();\n\t\t}\n\t}\n\n\tprivate bool IsViewportWireframeEnabled()\n\t{\n\t\tvar editorCamera = m_Target.Scene.GetAllComponents\u003CCameraComponent\u003E()\n\t\t\t.FirstOrDefault(camera =\u003E camera.GameObject.Name == \u0022editor_camera\u0022);\n\t\treturn editorCamera?.WireframeMode == true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Draw the baked quad topology while editing the Waymaker spline. The mesh is a generated child, so it would\n\t/// otherwise only show a wireframe when that child is selected directly.\n\t/// \u003C/summary\u003E\n\tprivate void DrawGeneratedMeshWireframe()\n\t{\n\t\tforeach (var child in m_Target.GameObject.Children)\n\t\t{\n\t\t\tif (!child.Tags.Has(\u0022waymaker_generated_mesh\u0022))\n\t\t\t\tcontinue;\n\n\t\t\tvar meshComponent = child.Components.Get\u003CMeshComponent\u003E();\n\t\t\tvar mesh = meshComponent?.Mesh;\n\t\t\tif (mesh is null)\n\t\t\t\tcontinue;\n\n\t\t\tGizmo.Draw.Color = Color.Cyan;\n\t\t\tGizmo.Draw.LineThickness = GeneratedWireThickness;\n\t\t\tGizmo.Draw.IgnoreDepth = true;\n\n\t\t\tforeach (var face in mesh.FaceHandles)\n\t\t\t{\n\t\t\t\tforeach (var halfEdge in mesh.GetFaceEdges(face))\n\t\t\t\t{\n\t\t\t\t\tif (!halfEdge.NextEdge.IsValid)\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\tVector3 start = mesh.GetVertexPosition(halfEdge.Vertex);\n\t\t\t\t\tVector3 end = mesh.GetVertexPosition(halfEdge.NextEdge.Vertex);\n\t\t\t\t\tstart = m_Target.WorldTransform.PointToLocal(meshComponent.WorldTransform.PointToWorld(start));\n\t\t\t\t\tend = m_Target.WorldTransform.PointToLocal(meshComponent.WorldTransform.PointToWorld(end));\n\t\t\t\t\tGizmo.Draw.Line(start, end);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void DrawSplineSegments()\n\t{\n\t\tm_Target.Spline.ConvertToPolyline(ref m_Polyline);\n\t\tfor (int i = 0; i \u003C m_Polyline.Count - 1; i\u002B\u002B)\n\t\t{\n\t\t\tVector3 start = m_Polyline[i];\n\t\t\tVector3 end = m_Polyline[i \u002B 1];\n\t\t\tusing (Gizmo.Scope(\u0022segment\u0022 \u002B i))\n\t\t\tusing (Gizmo.Hitbox.LineScope())\n\t\t\t{\n\t\t\t\tGizmo.Draw.LineThickness = LineThickness;\n\t\t\t\tGizmo.Hitbox.AddPotentialLine(start, end, LineThickness * 2.0f);\n\t\t\t\tGizmo.Draw.Line(start, end);\n\t\t\t\tif (Gizmo.IsHovered \u0026\u0026 Gizmo.HasMouseFocus \u0026\u0026 Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\n\t\t\t\t\tInsertPointAtClosestPosition(start, end);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void InsertPointAtClosestPosition(Vector3 start, Vector3 end)\n\t{\n\t\tif (!new Line(start, end).ClosestPoint(Gizmo.CurrentRay.ToLocal(Gizmo.Transform), out Vector3 pointOnLine, out _))\n\t\t\treturn;\n\n\t\tfloat distance = m_Target.Spline.SampleAtClosestPosition(pointOnLine).Distance;\n\t\tusing (CreateUndoScope(\u0022Add Waymaker Spline Point\u0022))\n\t\t\tSelectedPointIndex = m_Target.Spline.AddPointAtDistance(distance, true);\n\n\t\tm_InTangentSelected = false;\n\t\tm_OutTangentSelected = false;\n\t\tToggleTangentInput();\n\t}\n\n\tprivate void DrawPositionGizmo()\n\t{\n\t\tif (!Gizmo.IsShiftPressed)\n\t\t\tm_DraggingOutNewPoint = false;\n\n\t\tif (!IsSelectedPointValid() || (SelectedPointIndex == 0 \u0026\u0026 !m_InTangentSelected \u0026\u0026 !m_OutTangentSelected))\n\t\t\treturn;\n\n\t\tm_MoveInProgress = false;\n\t\tVector3 gizmoPosition = SelectedPoint.Position;\n\t\tif (m_InTangentSelected)\n\t\t\tgizmoPosition \u002B= SelectedPoint.In;\n\t\telse if (m_OutTangentSelected)\n\t\t\tgizmoPosition \u002B= SelectedPoint.Out;\n\n\t\t// The handle is drawn at the current scope\u0027s transform, so the point/tangent position has to go into\n\t\t// a child scope. Passing it as the control\u0027s value instead leaves the handle on the component origin,\n\t\t// where it sits under the GameObject\u0027s own move gizmo and can never be grabbed. Translation-only\n\t\t// scope, so the delta still comes back in component-local space.\n\t\tusing (Gizmo.Scope(\u0022position\u0022, new Transform(gizmoPosition)))\n\t\t{\n\t\t\tif (Gizmo.Control.Position(\u0022waymaker_spline_control_\u0022, Vector3.Zero, out Vector3 delta, GetHandleRotation()))\n\t\t\t{\n\t\t\t\tm_MoveInProgress = true;\n\t\t\t\tm_MovementUndoScope ??= CreateUndoScope(\u0022Move Waymaker Spline Point\u0022);\n\t\t\t\tMoveSelectedControl(delta);\n\t\t\t}\n\n\t\t\tif (!m_MoveInProgress \u0026\u0026 Gizmo.WasLeftMouseReleased)\n\t\t\t{\n\t\t\t\tm_MovementUndoScope?.Dispose();\n\t\t\t\tm_MovementUndoScope = null;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void MoveSelectedControl(Vector3 delta)\n\t{\n\t\tvar updated = SelectedPoint;\n\t\tif (m_InTangentSelected)\n\t\t{\n\t\t\tupdated.In \u002B= delta;\n\t\t\tif (updated.Mode == Spline.HandleMode.Auto)\n\t\t\t\tupdated.Mode = Spline.HandleMode.Mirrored;\n\t\t\tif (updated.Mode == Spline.HandleMode.Mirrored)\n\t\t\t\tupdated.Out = -updated.In;\n\t\t}\n\t\telse if (m_OutTangentSelected)\n\t\t{\n\t\t\tupdated.Out \u002B= delta;\n\t\t\tif (updated.Mode == Spline.HandleMode.Auto)\n\t\t\t\tupdated.Mode = Spline.HandleMode.Mirrored;\n\t\t\tif (updated.Mode == Spline.HandleMode.Mirrored)\n\t\t\t\tupdated.In = -updated.Out;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif (Gizmo.IsShiftPressed \u0026\u0026 !m_DraggingOutNewPoint)\n\t\t\t{\n\t\t\t\tm_DraggingOutNewPoint = true;\n\t\t\t\tm_Target.Spline.InsertPoint(SelectedPointIndex \u002B 1, updated);\n\t\t\t\tSelectedPointIndex\u002B\u002B;\n\t\t\t\tupdated = SelectedPoint;\n\t\t\t}\n\t\t\tupdated.Position \u002B= delta;\n\t\t}\n\n\t\tm_Target.Spline.UpdatePoint(SelectedPointIndex, updated);\n\t\tif (!Gizmo.IsShiftPressed)\n\t\t\tm_DraggingOutNewPoint = false;\n\t}\n\n\tprivate Rotation GetHandleRotation()\n\t{\n\t\tif (Gizmo.Settings.GlobalSpace || !IsSelectedPointValid())\n\t\t\treturn Rotation.Identity;\n\n\t\tfloat distance = m_Target.Spline.GetDistanceAtPoint(SelectedPointIndex);\n\t\tvar sample = m_Target.Spline.SampleAtDistance(distance);\n\t\tif (sample.Tangent.IsNearlyZero())\n\t\t\treturn Rotation.Identity;\n\n\t\tVector3 up = sample.Up.IsNearZeroLength ? Vector3.Up : sample.Up;\n\t\tup = Rotation.FromAxis(sample.Tangent, sample.Roll) * up;\n\t\treturn Rotation.LookAt(sample.Tangent, up);\n\t}\n\n\tprivate void DrawPointControls()\n\t{\n\t\tvar spline = m_Target.Spline;\n\t\tfor (int i = 0; i \u003C spline.PointCount; i\u002B\u002B)\n\t\t{\n\t\t\tif (spline.IsLoop \u0026\u0026 i == spline.SegmentCount)\n\t\t\t\tcontinue;\n\n\t\t\tusing (Gizmo.Scope(\u0022point_controls\u0022 \u002B i, new Transform(spline.GetPoint(i).Position)))\n\t\t\t{\n\t\t\t\tGizmo.Draw.IgnoreDepth = true;\n\t\t\t\tDrawPointPositionHandle(i);\n\t\t\t\tif (SelectedPointIndex == i)\n\t\t\t\t\tDrawTangentHandles(spline.GetPoint(i));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void DrawPointPositionHandle(int index)\n\t{\n\t\tusing (Gizmo.Scope(\u0022position\u0022))\n\t\tusing (Gizmo.GizmoControls.PushFixedScale())\n\t\t{\n\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\n\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GizmoBoxSize));\n\t\t\tif (Gizmo.IsHovered || (index == SelectedPointIndex \u0026\u0026 !m_InTangentSelected \u0026\u0026 !m_OutTangentSelected))\n\t\t\t\tGizmo.Draw.Color = Color.Cyan;\n\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GizmoBoxSize));\n\t\t\tif (Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\n\t\t\t\tSelectPoint(index);\n\t\t}\n\t}\n\n\tprivate void DrawTangentHandles(Spline.Point point)\n\t{\n\t\tGizmo.Draw.Color = Color.White;\n\t\tGizmo.Draw.LineThickness = TangentLineThickness;\n\t\tDrawTangentHandle(\u0022in_tangent\u0022, point.In, ref m_InTangentSelected, ref m_OutTangentSelected);\n\t\tDrawTangentHandle(\u0022out_tangent\u0022, point.Out, ref m_OutTangentSelected, ref m_InTangentSelected);\n\t}\n\n\tprivate void DrawTangentHandle(string name, Vector3 offset, ref bool selected, ref bool otherSelected)\n\t{\n\t\tusing (Gizmo.Scope(name, new Transform(offset)))\n\t\t{\n\t\t\tGizmo.Draw.Line(-offset, Vector3.Zero);\n\t\t\tif (SelectedPoint.Mode == Spline.HandleMode.Linear)\n\t\t\t\treturn;\n\n\t\t\tusing (Gizmo.GizmoControls.PushFixedScale())\n\t\t\t{\n\t\t\t\tGizmo.Hitbox.DepthBias = 0.1f;\n\t\t\t\tGizmo.Hitbox.BBox(BBox.FromPositionAndSize(Vector3.Zero, GizmoBoxSize));\n\t\t\t\tif (Gizmo.IsHovered || selected)\n\t\t\t\t\tGizmo.Draw.Color = Color.Cyan;\n\t\t\t\tGizmo.Draw.SolidBox(BBox.FromPositionAndSize(Vector3.Zero, GizmoBoxSize));\n\t\t\t\tif (Gizmo.HasClicked \u0026\u0026 Gizmo.Pressed.This)\n\t\t\t\t{\n\t\t\t\t\tselected = true;\n\t\t\t\t\totherSelected = false;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"},{"Ident":"fluidplay.waymaker","Path":"Editor/Waymaker/WaymakerSplineEditorTool.cs","FileName":"WaymakerSplineEditorTool.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":443795,"IsPrivate":false,"Code":"using Editor;\nusing Sandbox;\n\nnamespace FluidPlay.Waymaker.Editor;\n\n/// \u003Csummary\u003E\n/// Standalone spline editing tool for Waymaker components.\n/// \u003C/summary\u003E\n[EditorTool]\n[Title(\u0022Waymaker Splines\u0022)]\n[Icon(\u0022timeline\u0022)]\n[Alias(\u0022waymaker_splines\u0022)]\n[Group(\u00221\u0022)]\n[Order(3)]\npublic sealed class WaymakerSplineEditorTool : EditorTool\u003CWaymaker\u003E\n{\n\tprivate WaymakerSplineEditorWindow m_Window;\n\tprivate Waymaker m_SelectedWaymaker;\n\n\tpublic override void OnEnabled()\n\t{\n\t\tm_Window = new WaymakerSplineEditorWindow();\n\t\tAddOverlay(m_Window, TextFlag.RightBottom, 10);\n\t}\n\n\tpublic override void OnDisabled()\n\t{\n\t\tm_Window?.OnDisabled();\n\t}\n\n\tpublic override void OnUpdate()\n\t{\n\t\tm_Window?.OnUpdate();\n\t}\n\n\tpublic override void OnSelectionChanged()\n\t{\n\t\tWaymaker target = GetSelectedComponent\u003CWaymaker\u003E();\n\t\tif (!target.IsValid())\n\t\t{\n\t\t\tm_SelectedWaymaker = null;\n\t\t\tm_Window?.OnSelectionChanged(null);\n\t\t\treturn;\n\t\t}\n\n\t\tif (target == m_SelectedWaymaker)\n\t\t\treturn;\n\n\t\tm_Window?.OnSelectionChanged(target);\n\t\tm_SelectedWaymaker = target;\n\t}\n}\n"}]}