diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/light-insert.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/light-insert.golden new file mode 100644 index 000000000..d6f6fc58b Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/light-insert.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-corners-detail.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-corners-detail.golden new file mode 100644 index 000000000..9004aebe5 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-corners-detail.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-coarse.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-coarse.golden new file mode 100644 index 000000000..396dc347a Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-coarse.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-detail.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-detail.golden new file mode 100644 index 000000000..c0570cf24 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/loop-smooth-detail.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/metal-wire-guide.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/metal-wire-guide.golden new file mode 100644 index 000000000..73c4f0714 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/metal-wire-guide.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-corners-detail.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-corners-detail.golden new file mode 100644 index 000000000..ab1d93c5c Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-corners-detail.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-coarse.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-coarse.golden new file mode 100644 index 000000000..ec00c0958 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-coarse.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-detail.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-detail.golden new file mode 100644 index 000000000..c1281400e Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/open-smooth-detail.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/ramp.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/ramp.golden new file mode 100644 index 000000000..c33735488 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/ramp.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/rubber.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/rubber.golden new file mode 100644 index 000000000..d1e419101 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/rubber.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/surface.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/surface.golden new file mode 100644 index 000000000..ae2b1a5c3 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/surface.golden differ diff --git a/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/trigger-outline.golden b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/trigger-outline.golden new file mode 100644 index 000000000..58f5541c2 Binary files /dev/null and b/VisualPinball.Engine.Test/Fixtures~/DragPointGolden/trigger-outline.golden differ diff --git a/VisualPinball.Engine.Test/Math/DragPointGoldenTests.cs b/VisualPinball.Engine.Test/Math/DragPointGoldenTests.cs new file mode 100644 index 000000000..84a7a0402 --- /dev/null +++ b/VisualPinball.Engine.Test/Math/DragPointGoldenTests.cs @@ -0,0 +1,266 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using VisualPinball.Engine.Math; +using VisualPinball.Engine.Test.Test; +using VisualPinball.Engine.VPT; +using VisualPinball.Engine.VPT.Light; +using VisualPinball.Engine.VPT.MetalWireGuide; +using VisualPinball.Engine.VPT.Ramp; +using VisualPinball.Engine.VPT.Rubber; +using VisualPinball.Engine.VPT.Surface; +using VisualPinball.Engine.VPT.Table; + +namespace VisualPinball.Engine.Test.Math +{ + public class DragPointGoldenTests + { + private const string UpdateGoldenEnvironmentVariable = "UPDATE_DRAG_POINT_GOLDENS"; + + [Test] + public void ShouldKeepConsumerGeometryByteIdentical() + { + AssertGolden("surface", writer => { + var container = FileTableContainer.Load(VpxPath.Surface); + var surface = container.Surface("Wall"); + WriteMesh(writer, surface.GetMesh(SurfaceMeshGenerator.Top, container.Table)); + WriteMesh(writer, surface.GetMesh(SurfaceMeshGenerator.Side, container.Table)); + }); + + AssertGolden("ramp", writer => { + var container = FileTableContainer.Load(VpxPath.Ramp); + var flat = container.Ramp("Flat"); + WriteMesh(writer, flat.GetMesh(RampMeshGenerator.Floor, container.Table)); + WriteMesh(writer, flat.GetMesh("RightWall", container.Table)); + WriteMesh(writer, flat.GetMesh("LeftWall", container.Table)); + + var wire = container.Ramp("Wire4"); + for (var i = 1; i <= 4; i++) { + WriteMesh(writer, wire.GetMesh($"Wire{i}", container.Table)); + } + }); + + AssertGolden("rubber", writer => { + var container = FileTableContainer.Load(VpxPath.Rubber); + var data = container.Rubber("Rubber1").Data; + WriteMesh(writer, new RubberMeshGenerator(data).GetMesh(data.Height, container.Table)); + }); + + AssertGolden("metal-wire-guide", writer => { + var container = FileTableContainer.Load(VpxPath.MetalWireGuide); + var data = container.MetalWireGuide("MetalWireGuide2").Data; + WriteMesh(writer, new MetalWireGuideMeshGenerator(data).GetMesh(container.Table, data)); + }); + + AssertGolden("trigger-outline", writer => { + var container = FileTableContainer.Load(VpxPath.Trigger); + var dragPoints = container.Trigger("Data").Data.DragPoints; + AssignStableIds(dragPoints); + WriteVertices(writer, DragPoint.GetRgVertex(dragPoints)); + }); + + AssertGolden("light-insert", writer => { + var container = FileTableContainer.Load(VpxPath.Light); + var data = container.Light("Light1").Data; + var generator = new SurfaceMeshGenerator( + new LightInsertData(data.DragPoints, 20f), Vertex3D.Zero); + WriteMesh(writer, generator.GetMesh(SurfaceMeshGenerator.Top, + container.Table.Width, container.Table.Height, 0f, false)); + WriteMesh(writer, generator.GetMesh(SurfaceMeshGenerator.Side, + container.Table.Width, container.Table.Height, 0f, false)); + }); + } + + [TestCase(false, false, 4f, "open-corners-detail")] + [TestCase(false, true, 4f, "open-smooth-detail")] + [TestCase(true, false, 4f, "loop-corners-detail")] + [TestCase(true, true, 4f, "loop-smooth-detail")] + [TestCase(false, true, 400f, "open-smooth-coarse")] + [TestCase(true, true, 400f, "loop-smooth-coarse")] + public void ShouldKeepTessellationByteIdentical(bool loop, bool smooth, float accuracy, + string goldenName) + { + var dragPoints = CreateTessellationPoints(smooth); + AssertGolden(goldenName, writer => { + WriteVertices(writer, DragPoint.GetRgVertex(dragPoints, loop, accuracy)); + WriteVertices(writer, DragPoint.GetRgVertex(dragPoints, loop, accuracy)); + }); + } + + [Test] + public void ShouldRoundTripEveryDragPointFieldThroughVpx() + { + var cases = new[] { + (VpxPath.Surface, "Surface", "Wall"), + (VpxPath.Ramp, "Ramp", "FlatL"), + (VpxPath.Rubber, "Rubber", "Rubber1"), + (VpxPath.Trigger, "Trigger", "Data"), + (VpxPath.MetalWireGuide, "MetalWireGuide", "MetalWireGuide2"), + (VpxPath.Light, "Light", "Light1"), + }; + + foreach (var (path, itemType, itemName) in cases) { + var source = FileTableContainer.Load(path); + var expected = GetDragPoints(source, itemType, itemName); + var outputPath = Path.Combine(Path.GetTempPath(), $"vpe-dragpoints-{Guid.NewGuid():N}.vpx"); + try { + source.Export(outputPath); + var actual = GetDragPoints(FileTableContainer.Load(outputPath), itemType, itemName); + AssertDragPointsEqual(expected, actual, $"{itemType} {itemName}"); + } + finally { + if (File.Exists(outputPath)) { + File.Delete(outputPath); + } + } + } + } + + private static DragPointData[] GetDragPoints(FileTableContainer container, string itemType, + string itemName) + { + switch (itemType) { + case "Surface": return container.Surface(itemName).Data.DragPoints; + case "Ramp": return container.Ramp(itemName).Data.DragPoints; + case "Rubber": return container.Rubber(itemName).Data.DragPoints; + case "Trigger": return container.Trigger(itemName).Data.DragPoints; + case "MetalWireGuide": return container.MetalWireGuide(itemName).Data.DragPoints; + case "Light": return container.Light(itemName).Data.DragPoints; + default: throw new ArgumentOutOfRangeException(nameof(itemType), itemType, null); + } + } + + private static void AssertDragPointsEqual(IReadOnlyList expected, + IReadOnlyList actual, string context) + { + Assert.That(actual.Count, Is.EqualTo(expected.Count), context); + for (var i = 0; i < expected.Count; i++) { + var e = expected[i]; + var a = actual[i]; + Assert.That(a.Center, Is.EqualTo(e.Center), $"{context}, point {i}, center"); + Assert.That(a.IsSmooth, Is.EqualTo(e.IsSmooth), $"{context}, point {i}, smooth"); + Assert.That(a.IsSlingshot, Is.EqualTo(e.IsSlingshot), $"{context}, point {i}, slingshot"); + Assert.That(a.HasAutoTexture, Is.EqualTo(e.HasAutoTexture), $"{context}, point {i}, auto texture"); + Assert.That(a.TextureCoord, Is.EqualTo(e.TextureCoord), $"{context}, point {i}, texture coordinate"); + Assert.That(a.IsLocked, Is.EqualTo(e.IsLocked), $"{context}, point {i}, locked"); + Assert.That(a.EditorLayer, Is.EqualTo(e.EditorLayer), $"{context}, point {i}, editor layer"); + Assert.That(a.EditorLayerName, Is.EqualTo(e.EditorLayerName), $"{context}, point {i}, editor layer name"); + Assert.That(a.EditorLayerVisibility, Is.EqualTo(e.EditorLayerVisibility), + $"{context}, point {i}, editor layer visibility"); + } + } + + private static DragPointData[] CreateTessellationPoints(bool smooth) + { + return new[] { + CreatePoint("p0", -120.25f, 15.5f, 1.25f, smooth, true), + CreatePoint("p1", -25.75f, 140.125f, 18.5f, smooth, false), + CreatePoint("p2", 95.375f, 80.625f, -7.75f, smooth, true), + CreatePoint("p3", 155.875f, -45.25f, 32.125f, smooth, false), + CreatePoint("p4", 5.5f, -110.875f, 4.25f, smooth, false), + }; + } + + private static DragPointData CreatePoint(string id, float x, float y, float z, + bool smooth, bool slingshot) + { + return new DragPointData(new Vertex3D(x, y, z)) { + Id = id, + IsSmooth = smooth, + IsSlingshot = slingshot, + }; + } + + private static void AssignStableIds(IReadOnlyList dragPoints) + { + for (var i = 0; i < dragPoints.Count; i++) { + dragPoints[i].Id = $"p{i}"; + } + } + + private static void WriteMesh(BinaryWriter writer, Mesh mesh) + { + writer.Write(mesh.Name ?? string.Empty); + writer.Write(mesh.Vertices.Length); + foreach (var vertex in mesh.Vertices) { + vertex.Write(writer); + } + + writer.Write(mesh.Indices.Length); + foreach (var index in mesh.Indices) { + writer.Write(index); + } + } + + private static void WriteVertices(BinaryWriter writer, IReadOnlyList vertices) + { + writer.Write(vertices.Count); + foreach (var vertex in vertices) { + writer.Write(vertex.X); + writer.Write(vertex.Y); + WriteVertexMetadata(writer, vertex); + } + } + + private static void WriteVertices(BinaryWriter writer, IReadOnlyList vertices) + { + writer.Write(vertices.Count); + foreach (var vertex in vertices) { + writer.Write(vertex.X); + writer.Write(vertex.Y); + writer.Write(vertex.Z); + WriteVertexMetadata(writer, vertex); + } + } + + private static void WriteVertexMetadata(BinaryWriter writer, IRenderVertex vertex) + { + writer.Write(vertex.Smooth); + writer.Write(vertex.IsSlingshot); + writer.Write(vertex.IsControlPoint); + writer.Write(vertex.Id ?? string.Empty); + } + + private static void AssertGolden(string name, Action write) + { + byte[] actual; + using (var stream = new MemoryStream()) + using (var writer = new BinaryWriter(stream)) { + write(writer); + writer.Flush(); + actual = stream.ToArray(); + } + + var goldenDirectory = PathHelper.GetFixturePath("DragPointGolden"); + var goldenPath = Path.Combine(goldenDirectory, $"{name}.golden"); + if (Environment.GetEnvironmentVariable(UpdateGoldenEnvironmentVariable) == "1") { + Directory.CreateDirectory(goldenDirectory); + File.WriteAllBytes(goldenPath, actual); + } + + Assert.That(File.Exists(goldenPath), Is.True, + $"Missing {goldenPath}. Run with {UpdateGoldenEnvironmentVariable}=1 to create it."); + Assert.That(actual, Is.EqualTo(File.ReadAllBytes(goldenPath)), name); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs deleted file mode 100644 index da4ca312e..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; - -namespace VisualPinball.Unity.Editor -{ - /// - /// An editable drag point in Unity's editor.

- /// - /// The inspector manages adding/removing control points and updates the - /// data of IDragPointEditable. - /// - /// ControlPoint also manages the ControlId used by Unity's Handles system - /// and the points that make the path in the scene view. - ///

- public class ControlPoint - { - public const float ScreenRadius = 0.25f; - - /// - /// Reference to the drag point data - /// - public DragPointData DragPoint; - - /// - /// Position in VPX space - /// - public Vector3 AbsolutePosition => new(DragPoint.Center.X, DragPoint.Center.Y, DragPoint.CalcHeight); - - /// - /// Position in world space - /// - public Vector3 EditorPositionWorld => AbsolutePosition.TranslateToWorld(_dragPointsInspector.Transform); - - public float HandleSize => HandleUtility.GetHandleSize(EditorPositionWorld) * ScreenRadius; - - /// - /// Currently selected or not? - /// - public bool IsSelected = false; - - /// - /// Unity's ControlID. - /// - public readonly int ControlId; - - /// - /// The DragPoint ID that is saved within the drag point. - /// - public string DragPointId => DragPoint.Id; - - /// - /// Index of the drag point within the game item's drag point array. - /// - public readonly int Index; - - private readonly IDragPointsInspector _dragPointsInspector; - - public ControlPoint(IDragPointsInspector dragPointsInspector, int controlId, int idx) - { - DragPoint = dragPointsInspector.DragPoints[idx]; - ControlId = controlId; - Index = idx; - _dragPointsInspector = dragPointsInspector; - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs.meta deleted file mode 100644 index a4ae1c2a1..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/ControlPoint.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 340f73844a6b43c4195e63ef808326af -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs deleted file mode 100644 index d2c9b16b0..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs +++ /dev/null @@ -1,222 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using UnityEditor; -using VisualPinball.Engine.Math; - -namespace VisualPinball.Unity.Editor -{ - public static class DragPointMenuItems - { - public const string ControlPointsMenuPath = "CONTEXT/DragPointsItemInspector/ControlPoint"; - public const string CurveTravellerMenuPath = "CONTEXT/DragPointsItemInspector/CurveTraveller"; - - private static DragPointData RetrieveDragPoint(IDragPointsInspector inspector, int controlId) - { - return inspector?.DragPointsHelper.GetDragPoint(controlId); - } - - // Drag Points - [MenuItem(ControlPointsMenuPath + "/Is Slingshot", false, 1)] - private static void SlingShot(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - var dragPoint = RetrieveDragPoint(inspector, command.userData); - if (dragPoint != null) { - inspector.DragPointsHelper.PrepareUndo("Toggle Drag Point Slingshot"); - dragPoint.IsSlingshot = !dragPoint.IsSlingshot; - inspector.DragPointsHelper.RebuildMeshes(); - } - } - - [MenuItem(ControlPointsMenuPath + "/Is Slingshot", true)] - private static bool SlingshotValidate(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector) || inspector.DragPointsHelper.IsItemLocked()) { - return false; - } - - if (!inspector.DragPointsHelper.HasDragPointExposure(DragPointExposure.SlingShot)) { - Menu.SetChecked($"{ControlPointsMenuPath}/IsSlingshot", false); - return false; - } - - var dragPoint = RetrieveDragPoint(inspector, command.userData); - if (dragPoint != null) { - Menu.SetChecked($"{ControlPointsMenuPath}/IsSlingshot", dragPoint.IsSlingshot); - } - - return true; - } - - [MenuItem(ControlPointsMenuPath + "/Is Smooth", false, 1)] - private static void Smooth(MenuCommand command) - { - var inspector = command.context as IDragPointsInspector; - if (inspector == null) { - return; - } - - var dragPoint = RetrieveDragPoint(inspector, command.userData); - if (dragPoint != null) { - inspector.DragPointsHelper.PrepareUndo("Toggle Drag Point Smooth"); - dragPoint.IsSmooth = !dragPoint.IsSmooth; - inspector.DragPointsHelper.RebuildMeshes(); - } - } - - [MenuItem(ControlPointsMenuPath + "/Is Smooth", true)] - private static bool SmoothValidate(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector) || inspector.DragPointsHelper.IsItemLocked()) { - return false; - } - - if (!inspector.DragPointsHelper.HasDragPointExposure(DragPointExposure.Smooth)) { - Menu.SetChecked($"{ControlPointsMenuPath}/IsSmooth", false); - return false; - } - - var dragPoint = RetrieveDragPoint(inspector, command.userData); - if (dragPoint != null) { - Menu.SetChecked($"{ControlPointsMenuPath}/IsSmooth", dragPoint.IsSmooth); - } - - return true; - } - - [MenuItem(ControlPointsMenuPath + "/Remove Point", false, 101)] - private static void Remove(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - if (EditorUtility.DisplayDialog("Drag point removal", "Are you sure you want to remove this drag point?", "Yes", "No")) { - inspector.DragPointsHelper.RemoveDragPoint(command.userData); - } - } - - [MenuItem(ControlPointsMenuPath + "/Remove Point", true)] - private static bool RemoveValidate(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector) || inspector.DragPointsHelper.IsItemLocked()) { - return false; - } - - if (inspector.DragPointsHelper.DragPointsHandler?.ControlPoints.Count <= 2) { - Menu.SetChecked($"{ControlPointsMenuPath}/Remove Point", false); - return false; - } - - return true; - } - - [MenuItem(ControlPointsMenuPath + "/Copy Point", false, 301)] - private static void Copy(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.CopyDragPoint(command.userData); - } - - [MenuItem(ControlPointsMenuPath + "/Paste Point", false, 302)] - private static void Paste(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.PasteDragPoint(command.userData); - } - - [MenuItem(ControlPointsMenuPath + "/Paste Point", true)] - private static bool PasteValidate(MenuCommand command) - { - return command.context is IDragPointsInspector inspector && !inspector.DragPointsHelper.IsItemLocked(); - } - - //Curve Traveller - [MenuItem(CurveTravellerMenuPath + "/Add Point", false, 1)] - private static void Add(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.AddDragPointOnTraveller(); - } - - [MenuItem(CurveTravellerMenuPath + "/Flip Drag Points/X", false, 101)] - [MenuItem(ControlPointsMenuPath + "/Flip Drag Points/X", false, 201)] - private static void FlipX(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.FlipDragPoints(FlipAxis.X); - } - - [MenuItem(CurveTravellerMenuPath + "/Flip Drag Points/Y", false, 102)] - [MenuItem(ControlPointsMenuPath + "/Flip Drag Points/Y", false, 202)] - private static void FlipY(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.FlipDragPoints(FlipAxis.Y); - } - - [MenuItem(CurveTravellerMenuPath + "/Flip Drag Points/Z", false, 103)] - [MenuItem(ControlPointsMenuPath + "/Flip Drag Points/Z", false, 203)] - private static void FlipZ(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.FlipDragPoints(FlipAxis.Z); - } - - [MenuItem(CurveTravellerMenuPath + "/Flip Drag Points/Z", true, 103)] - [MenuItem(ControlPointsMenuPath + "/Flip Drag Points/Z", true, 203)] - private static bool FlipZValidate(MenuCommand command) - { - if (command.context is IDragPointsInspector inspector) { - return inspector.HandleType == DragPointTransformType.ThreeD; - } - return false; - } - - [MenuItem(CurveTravellerMenuPath + "/Reverse", false, 501)] - [MenuItem(ControlPointsMenuPath + "/Reverse", false, 601)] - private static void Reverse(MenuCommand command) - { - if (!(command.context is IDragPointsInspector inspector)) { - return; - } - - inspector.DragPointsHelper.Reverse(); - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs.meta deleted file mode 100644 index ed844626e..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointMenuItems.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8533dafc1a83b294aa17ef1831cbdfd1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs new file mode 100644 index 000000000..9e6c6cf93 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs @@ -0,0 +1,356 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Mathematics; +using UnityEditor; +using UnityEditor.EditorTools; +using UnityEditor.Splines; +using UnityEngine; +using UnityEngine.Splines; +using VisualPinball.Engine.Math; + +namespace VisualPinball.Unity.Editor +{ + [Flags] + internal enum DragPointSplineExposure + { + None = 0, + Smooth = 1 << 0, + Slingshot = 1 << 1, + Texture = 1 << 2, + } + + internal enum DragPointSplineAxis + { + X, + Y, + Z, + } + + public static class DragPointSplineInspectorGUI + { + public static void OnInspectorGUI(DragPointSplineComponent component, + bool showEditButton = true) + { + EditorGUILayout.Space(10f); + EditorGUILayout.LabelField("Spline", EditorStyles.boldLabel); + if (showEditButton) { + using (new EditorGUI.DisabledScope(!IsEditable(component))) { + if (GUILayout.Button("Edit Spline")) { + EditSpline(component); + } + } + } + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Center Origin")) { + CenterOrigin(component); + } + if (GUILayout.Button("Reverse")) { + Reverse(component); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Flip X")) { + Flip(component, DragPointSplineAxis.X); + } + if (GUILayout.Button("Flip Y")) { + Flip(component, DragPointSplineAxis.Y); + } + using (new EditorGUI.DisabledScope(component.Owner?.SplinePlanar ?? true)) { + if (GUILayout.Button("Flip Z")) { + Flip(component, DragPointSplineAxis.Z); + } + } + EditorGUILayout.EndHorizontal(); + } + + public static void EditSpline(DragPointSplineComponent component) + { + Selection.activeGameObject = component.Container.gameObject; + ToolManager.SetActiveContext(); + ToolManager.SetActiveTool(); + SceneView.RepaintAll(); + } + + private static void Flip(DragPointSplineComponent component, + DragPointSplineAxis axis) + { + var dragPoints = component.DragPoints; + if (dragPoints.Length == 0) { + return; + } + + RecordUndo(component, $"Flip {axis} Drag Points"); + var center = dragPoints.Aggregate(Vertex3D.Zero, + (current, dragPoint) => current + dragPoint.Center) / dragPoints.Length; + foreach (var dragPoint in dragPoints) { + switch (axis) { + case DragPointSplineAxis.X: + dragPoint.Center.X = center.X + center.X - dragPoint.Center.X; + break; + case DragPointSplineAxis.Y: + dragPoint.Center.Y = center.Y + center.Y - dragPoint.Center.Y; + break; + case DragPointSplineAxis.Z: + dragPoint.Center.Z *= -1f; + break; + default: + throw new ArgumentOutOfRangeException(nameof(axis), axis, null); + } + } + + if (component.Owner?.SplineClosed == true) { + ReverseInPlace(dragPoints); + } + Apply(component, dragPoints); + } + + public static void Reverse(DragPointSplineComponent component) + { + var dragPoints = component.DragPoints; + if (dragPoints.Length < 2) { + return; + } + + RecordUndo(component, "Reverse Drag Points"); + ReverseInPlace(dragPoints); + Apply(component, dragPoints); + } + + public static void CenterOrigin(DragPointSplineComponent component) + { + var owner = component.Owner; + var dragPoints = component.DragPoints; + if (owner == null || dragPoints.Length == 0) { + return; + } + + RecordUndo(component, $"Center pivot point of {owner.SplineOwner.name}"); + var center = dragPoints.Aggregate(Vertex3D.Zero, + (current, dragPoint) => current + dragPoint.Center) / dragPoints.Length; + var centerUnity = center.ToUnityVector3(); + var ownerTransform = owner.SplineTransform; + ownerTransform.Translate(centerUnity.TranslateToWorld(ownerTransform) + - ownerTransform.position); + foreach (var dragPoint in dragPoints) { + dragPoint.Center -= center; + } + Apply(component, dragPoints); + } + + internal static DragPointSplineExposure GetExposure(DragPointSplineComponent component) + { + switch (component.Owner) { + case RubberComponent _: return DragPointSplineExposure.Smooth; + case MetalWireGuideComponent _: return DragPointSplineExposure.Smooth; + case TriggerComponent _: return DragPointSplineExposure.Smooth + | DragPointSplineExposure.Slingshot; + case RampComponent _: return DragPointSplineExposure.Smooth + | DragPointSplineExposure.Slingshot; + case SurfaceComponent _: return DragPointSplineExposure.Smooth + | DragPointSplineExposure.Slingshot | DragPointSplineExposure.Texture; + case LightInsertMeshComponent _: return DragPointSplineExposure.Smooth + | DragPointSplineExposure.Texture; + default: return DragPointSplineExposure.None; + } + } + + private static bool IsEditable(DragPointSplineComponent component) + { + if (component.Owner is TriggerComponent trigger) { + var mesh = trigger.GetComponent(); + return !mesh || !mesh.IsCircle; + } + return component.Owner != null; + } + + private static void ReverseInPlace(DragPointData[] dragPoints) + { + Array.Reverse(dragPoints, 1, dragPoints.Length - 1); + var slingshots = dragPoints.Select(dragPoint => dragPoint.IsSlingshot).ToArray(); + for (var i = 0; i < dragPoints.Length; i++) { + dragPoints[i].IsSlingshot = slingshots[(i + 1) % slingshots.Length]; + } + } + + private static void Apply(DragPointSplineComponent component, + DragPointData[] dragPoints) + { + component.SetDragPoints(dragPoints); + component.Owner?.RebuildSplineMeshes(); + EditorUtility.SetDirty(component); + PrefabUtility.RecordPrefabInstancePropertyModifications(component); + SceneView.RepaintAll(); + } + + private static void RecordUndo(DragPointSplineComponent component, string name) + { + var owner = component.Owner; + var objects = new List { component, component.Container }; + if (owner != null) { + objects.Add(owner.SplineOwner); + objects.Add(owner.SplineTransform); + } + Undo.RecordObjects(objects.Distinct().ToArray(), name); + } + } + + [CustomEditor(typeof(DragPointSplineComponent))] + public class DragPointSplineComponentInspector : UnityEditor.Editor + { + private readonly List _selectedKnots = new(); + + public override void OnInspectorGUI() + { + var component = (DragPointSplineComponent)target; + var owner = component.Owner; + if (owner == null) { + EditorGUILayout.HelpBox("This spline has no drag-point owner.", MessageType.Warning); + return; + } + + EditorGUILayout.LabelField("Owner", owner.SplineOwner.name); + if (GUILayout.Button("Select Owner")) { + Selection.activeObject = owner.SplineOwner; + } + DragPointSplineInspectorGUI.OnInspectorGUI(component, false); + DrawSelectedMetadata(component); + } + + private void DrawSelectedMetadata(DragPointSplineComponent component) + { + _selectedKnots.Clear(); + SplineSelection.GetElements(new SplineInfo(component.Container, 0), _selectedKnots); + EditorGUILayout.Space(10f); + EditorGUILayout.LabelField("Selected Knots", EditorStyles.boldLabel); + if (_selectedKnots.Count == 0) { + EditorGUILayout.HelpBox("Select one or more knots in the Scene view.", + MessageType.Info); + return; + } + + var exposure = DragPointSplineInspectorGUI.GetExposure(component); + if ((exposure & DragPointSplineExposure.Smooth) != 0) { + DrawBoolean(component, "Smooth", metadata => metadata.IsSmooth, + (metadata, value) => metadata.IsSmooth = value); + } + if ((exposure & DragPointSplineExposure.Slingshot) != 0) { + DrawBoolean(component, "Slingshot", metadata => metadata.IsSlingshot, + (metadata, value) => metadata.IsSlingshot = value); + } + if ((exposure & DragPointSplineExposure.Texture) != 0) { + DrawBoolean(component, "Auto Texture", metadata => metadata.HasAutoTexture, + (metadata, value) => metadata.HasAutoTexture = value); + DrawFloat(component, "Texture Coordinate", metadata => metadata.TextureCoord, + (metadata, value) => metadata.TextureCoord = value); + } + } + + private void DrawBoolean(DragPointSplineComponent component, string label, + Func getter, Action setter) + { + var first = getter(component.Metadata[_selectedKnots[0].KnotIndex]); + EditorGUI.showMixedValue = _selectedKnots.Any(knot => + getter(component.Metadata[knot.KnotIndex]) != first); + EditorGUI.BeginChangeCheck(); + var value = EditorGUILayout.Toggle(label, first); + EditorGUI.showMixedValue = false; + if (EditorGUI.EndChangeCheck()) { + UpdateMetadata(component, label, metadata => setter(metadata, value)); + } + } + + private void DrawFloat(DragPointSplineComponent component, string label, + Func getter, Action setter) + { + var first = getter(component.Metadata[_selectedKnots[0].KnotIndex]); + EditorGUI.showMixedValue = _selectedKnots.Any(knot => + math.abs(getter(component.Metadata[knot.KnotIndex]) - first) > 1e-6f); + EditorGUI.BeginChangeCheck(); + var value = EditorGUILayout.FloatField(label, first); + EditorGUI.showMixedValue = false; + if (EditorGUI.EndChangeCheck()) { + UpdateMetadata(component, label, metadata => setter(metadata, value)); + } + } + + private void UpdateMetadata(DragPointSplineComponent component, string label, + Action update) + { + Undo.RecordObjects(new UnityEngine.Object[] { component, component.Container }, + $"Change Drag Point {label}"); + foreach (var knot in _selectedKnots) { + update(component.Metadata[knot.KnotIndex]); + } + component.NotifyMetadataChanged(); + EditorUtility.SetDirty(component); + PrefabUtility.RecordPrefabInstancePropertyModifications(component); + } + } + + [InitializeOnLoad] + internal static class DragPointSplineSceneView + { + private const int SegmentResolution = 24; + + static DragPointSplineSceneView() + { + SceneView.duringSceneGui += OnSceneGUI; + } + + private static void OnSceneGUI(SceneView _) + { + var component = Selection.activeGameObject + ? Selection.activeGameObject.GetComponent() + : null; + if (!component || !component.Container || component.Container.Spline.Count == 0) { + return; + } + + var spline = component.Container.Spline; + var matrix = component.Container.transform.localToWorldMatrix; + var segmentCount = spline.Closed ? spline.Count : spline.Count - 1; + for (var segment = 0; segment < segmentCount; segment++) { + var points = new Vector3[SegmentResolution + 1]; + var curve = spline.GetCurve(segment); + for (var sample = 0; sample <= SegmentResolution; sample++) { + var point = CurveUtility.EvaluatePosition(curve, + sample / (float)SegmentResolution); + points[sample] = matrix.MultiplyPoint3x4(new Vector3(point.x, point.y, point.z)); + } + Handles.color = component.Metadata[segment].IsSlingshot + ? new UnityEngine.Color(1f, 0.15f, 0.1f, 0.9f) + : new UnityEngine.Color(0.1f, 0.45f, 1f, 0.65f); + Handles.DrawAAPolyLine(4f, points); + } + + var style = new GUIStyle(EditorStyles.boldLabel) { + normal = { textColor = UnityEngine.Color.white }, + alignment = TextAnchor.MiddleCenter, + }; + for (var i = 0; i < spline.Count; i++) { + var position = spline[i].Position; + var world = matrix.MultiplyPoint3x4(new Vector3(position.x, position.y, position.z)); + Handles.Label(world, i.ToString(), style); + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs.meta new file mode 100644 index 000000000..5bf40566f --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointSplineInspector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 98c41ad0ab4ad1f42a99d79c4eacfe5e \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs deleted file mode 100644 index 5eb064053..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs +++ /dev/null @@ -1,423 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; -using Color = UnityEngine.Color; -using Object = UnityEngine.Object; - -namespace VisualPinball.Unity.Editor -{ - public delegate void DragPointPositionChange(); - - public class DragPointsHandler - { - /// - /// Component - /// - public IMainRenderableComponent MainComponent { get; } - - /// - /// Component item as IDragPointsEditable - /// - public IDragPointsInspector DragPointInspector { get; } - - /// - /// Transform component of the game object - /// - public Transform Transform { get; } - - /// - /// Control points storing & rendering - /// - public List ControlPoints { get; } = new(); - - /// - /// Scene view handler - /// - /// - /// - /// Will handle all the rendering part and update some handler's variables about curve traveller - /// - private readonly DragPointsSceneViewHandler _sceneViewHandler; - - /// - /// Drag points selection - /// - public List SelectedControlPoints { get; } = new(); - - /// - /// Position of the tool handle. On the drag point when only one selected, otherwise in the geometric center of the selection. - /// - /// VPX space. - /// - private Vector3 _centerSelected = Vector3.zero; - - /// - /// Curve traveller handling - /// - /// - /// - /// CurveTravellerPosition, CurveTravellerControlPointIdx & CurveTravellerVisible will be updated by the DragPointsSceneViewHandler - /// - public int CurveTravellerControlId { get; private set; } - - /// - /// Traveller position in world space. - /// - public Vector3 CurveTravellerPosition { get; set; } = Vector3.zero; - - public bool CurveTravellerVisible { get; set; } - public int CurveTravellerControlPointIdx { get; set; } = -1; - - /// - /// The center of all drag points, in VPX space. - /// - private Vector3 _center = Vector3.zero; - - private Vector3 _startPos; - private readonly Dictionary _startPosZ = new(); - private float[] _startTopBottomZ; - - /// - /// Every DragPointsInspector instantiates this to manage its curve handling. - /// - /// The renderable main component, to retrieve IsLocked. - /// - /// - public DragPointsHandler(IMainRenderableComponent mainComponent, IDragPointsInspector dragPointsInspector) - { - MainComponent = mainComponent; - DragPointInspector = dragPointsInspector; - - Transform = mainComponent.gameObject.transform; - - _sceneViewHandler = new DragPointsSceneViewHandler(this){ - CurveWidth = 10.0f, - CurveColor = Color.blue, - CurveSlingShotColor = Color.red, - CurveTravellerSizeRatio = 0.75f - }; - } - - /// - /// References drag point data to control points. - /// - /// True if control points were re-built, false otherwise. - public bool RemapControlPoints() - { - // if count differs, rebuild - if (ControlPoints.Count != DragPointInspector.DragPoints.Length) { - RebuildControlPoints(); - return true; - } - - for (var i = 0; i < DragPointInspector.DragPoints.Length; ++i) { - ControlPoints[i].DragPoint = DragPointInspector.DragPoints[i]; - } - - return false; - } - - public DragPointData GetDragPoint(int controlId) => GetControlPoint(controlId)?.DragPoint; - public ControlPoint GetControlPoint(string dragPointId) => ControlPoints.Find(cp => cp.DragPointId == dragPointId); - private ControlPoint GetControlPoint(int controlId) => ControlPoints.Find(cp => cp.ControlId == controlId); - - /// - /// Adds a new control point to the scene view and its drag point data - /// to the game object. - /// - public void AddDragPointOnTraveller() - { - if (CurveTravellerControlPointIdx < 0 || CurveTravellerControlPointIdx >= ControlPoints.Count) { - return; - } - - var dragPoint = new DragPointData(DragPointInspector.DragPoints[CurveTravellerControlPointIdx]); - var newIdx = CurveTravellerControlPointIdx + 1; - var dragPointPosition = CurveTravellerPosition.TranslateToVpx(Transform); - dragPointPosition.z = 0; - dragPoint.Center = dragPointPosition.ToVertex3D(); - var dragPoints = DragPointInspector.DragPoints.ToList(); - dragPoints.Insert(newIdx, dragPoint); - DragPointInspector.DragPoints = dragPoints.ToArray(); - - ControlPoints.Insert(newIdx, - new ControlPoint( - DragPointInspector, - GUIUtility.GetControlID(FocusType.Passive), - newIdx - )); - RebuildControlPoints(); - } - - /// - /// Removes a control point and its data. - /// - /// - public void RemoveDragPoint(int controlId) - { - var idx = ControlPoints.FindIndex(controlPoint => controlPoint.ControlId == controlId); - if (idx < 0) { - return; - } - var dragPoints = DragPointInspector.DragPoints.ToList(); - dragPoints.RemoveAt(idx); - DragPointInspector.DragPoints = dragPoints.ToArray(); - - RebuildControlPoints(); - } - - /// - /// Flips all drag points around the given axis. - /// - /// Axis to flip - public void FlipDragPoints(FlipAxis flipAxis) - { - foreach (var controlPoint in ControlPoints) { - switch (flipAxis) { - case FlipAxis.X: - controlPoint.DragPoint.Center.X = _center.x + (_center.x - controlPoint.DragPoint.Center.X); - break; - case FlipAxis.Y: - controlPoint.DragPoint.Center.Y = _center.y + (_center.y - controlPoint.DragPoint.Center.Y); - break; - case FlipAxis.Z: - controlPoint.DragPoint.Center.Z *= -1; - break; - default: - throw new ArgumentOutOfRangeException(nameof(flipAxis), flipAxis, null); - } - } - } - - public void ReverseDragPoints() - { - var dragPoints = DragPointInspector.DragPoints.ToList(); - dragPoints.Reverse(1, dragPoints.Count - 1); - - // rotate slingshot props - var isSling = dragPoints.Select(dp => dp.IsSlingshot).ToArray(); - isSling = isSling.Skip(1).Concat(isSling.Take(1)).ToArray(); - for (var i = 0; i < dragPoints.Count; i++) { - dragPoints[i].IsSlingshot = isSling[i]; - } - DragPointInspector.DragPoints = dragPoints.ToArray(); - RebuildControlPoints(); - } - - public void CenterPivot() - { - var transform = Transform; - var centerVpx = Vector3.zero; - foreach (var dragPoint in DragPointInspector.DragPoints) { - centerVpx += dragPoint.Center.ToUnityVector3(); - } - centerVpx /= DragPointInspector.DragPoints.Length; - - Undo.RecordObjects(new[] { MainComponent as Object, transform }, $"Center pivot point of {MainComponent.name}"); - transform.Translate(centerVpx.TranslateToWorld(transform) - transform.position); - foreach (var dragPoint in DragPointInspector.DragPoints) { - dragPoint.Center -= centerVpx.ToVertex3D(); - } - - MainComponent.RebuildMeshes(); - } - - /// - /// Updates the lock status on all drag points to the given value. - /// - /// New lock status - /// True if at least one lock status changed, false otherwise. - // public bool UpdateDragPointsLock(bool itemLock) - // { - // var lockChanged = false; - // foreach (var controlPoint in ControlPoints) { - // if (controlPoint.DragPoint.IsLocked != itemLock) { - // controlPoint.DragPoint.IsLocked = itemLock; - // lockChanged = true; - // } - // } - // return lockChanged; - // } - - /// - /// Re-creates the control points of the scene view and references their - /// drag point data. - /// - private void RebuildControlPoints() - { - ControlPoints.Clear(); - var dragPoints = DragPointInspector.DragPoints; - for (var i = 0; i < dragPoints.Length; ++i) { - dragPoints[i].AssertId(); - var cp = new ControlPoint( - DragPointInspector, - GUIUtility.GetControlID(FocusType.Passive), - i - ); - ControlPoints.Add(cp); - } - CurveTravellerControlId = GUIUtility.GetControlID(FocusType.Passive); - - // persist prefab changes - EditorUtility.SetDirty(MainComponent.gameObject); - PrefabUtility.RecordPrefabInstancePropertyModifications(MainComponent as Object); - } - - /// - /// Un-selects all control points. - /// - private void ClearAllSelection() - { - foreach (var controlPoint in ControlPoints) { - controlPoint.IsSelected = false; - } - } - - /// - /// Takes care of the rendering-related stuff. - /// - /// - /// - /// This is called by the drag point inspector. - /// - /// - /// Event from the inspector - /// - public void OnSceneGUI(Event evt, DragPointPositionChange onChange = null) - { - switch (evt.type) { - case EventType.Layout: - OnSceneLayout(); - break; - - case EventType.MouseDown: - OnMouseDown(); - break; - - case EventType.Repaint: - CurveTravellerVisible = false; - break; - } - - if (SelectedControlPoints.Count > 0) { - - // set start positions since clicked - if (evt.type == EventType.MouseDown) { - _startPos = _centerSelected; - _startTopBottomZ = DragPointInspector.TopBottomZ; - _startPosZ.Clear(); - foreach (var cp in SelectedControlPoints) { - _startPosZ[cp.DragPointId] = cp.DragPoint.Center.Z; - } - } - - // get new pos since last frame - EditorGUI.BeginChangeCheck(); - var newHandlePos = HandlesUtils.HandlePosition( - _centerSelected, - Transform.localToWorldMatrix, - DragPointInspector.HandleType - ); - if (EditorGUI.EndChangeCheck()) { - var delta = newHandlePos - _centerSelected; - var deltaZ = newHandlePos.z - _startPos.z; - - Undo.RecordObject(MainComponent as MonoBehaviour, "move Drag Points"); - foreach (var controlPoint in SelectedControlPoints) { - DragPointInspector.SetDragPointPosition(controlPoint.DragPoint, new Vertex3D( - controlPoint.DragPoint.Center.X + delta.x, - controlPoint.DragPoint.Center.Y + delta.y, - _startPosZ[controlPoint.DragPointId] + deltaZ - ), SelectedControlPoints.Count, _startTopBottomZ); - } - onChange?.Invoke(); - } - } - - //Render the curve & drag points - _sceneViewHandler.OnSceneGUI(); - } - - private void OnSceneLayout() - { - SelectedControlPoints.Clear(); - _center = Vector3.zero; - - Handles.matrix = Matrix4x4.identity; - - //Setup Screen positions & controlID for control points (in case of modification of drag points coordinates outside) - foreach (var controlPoint in ControlPoints) { - _center += controlPoint.AbsolutePosition; - if (controlPoint.IsSelected) { - SelectedControlPoints.Add(controlPoint); - } - - HandleUtility.AddControl( - controlPoint.ControlId, - HandleUtility.DistanceToCircle( - controlPoint.EditorPositionWorld, - controlPoint.HandleSize - ) - ); - } - - if (ControlPoints.Count > 0) { - _center /= ControlPoints.Count; - } - - //Setup PositionHandle if some control points are selected - if (SelectedControlPoints.Count > 0) { - _centerSelected = Vector3.zero; - foreach (var controlPoint in SelectedControlPoints) { - _centerSelected += controlPoint.AbsolutePosition; - } - _centerSelected /= SelectedControlPoints.Count; - } - - if (CurveTravellerVisible) { - HandleUtility.AddControl( - CurveTravellerControlId, - HandleUtility.DistanceToCircle( - CurveTravellerPosition, - HandleUtility.GetHandleSize(CurveTravellerPosition) * ControlPoint.ScreenRadius * _sceneViewHandler.CurveTravellerSizeRatio * 0.5f)); - } - } - - private void OnMouseDown() - { - if (Event.current.button == 0) { - var nearestControlPoint = ControlPoints.Find(cp => cp.ControlId == HandleUtility.nearestControl); - if (nearestControlPoint != null) { - if (!Event.current.control) { - ClearAllSelection(); - nearestControlPoint.IsSelected = true; - } - else { - nearestControlPoint.IsSelected = !nearestControlPoint.IsSelected; - } - - Event.current.Use(); - } - } - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs.meta deleted file mode 100644 index c777e1b65..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsHandler.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 957cc5809e8ab9b4e9c24df01c6c68c3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs deleted file mode 100644 index d7428b1fe..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs +++ /dev/null @@ -1,309 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using System.Linq; -using UnityEditor; -using UnityEngine; -using UnityEngine.Assertions; -using VisualPinball.Engine.Math; - -namespace VisualPinball.Unity.Editor -{ - public class DragPointsInspectorHelper - { - /// - /// Catmull Curve Handler - /// - public DragPointsHandler DragPointsHandler { get; private set; } - - private bool EditingEnabled => _dragPointsInspector.DragPointsActive; - - /// - /// If true, a list of the drag points is displayed in the inspector. - /// - private bool _foldoutControlPoints; - - /// - /// stored vector during the copy/paste process - /// - /// - /// - /// It is globally stored so it can be copy/pasted on different items - /// - private static Vector3 _storedControlPoint = Vector3.zero; - - private readonly MonoBehaviour _mb; - private readonly IMainRenderableComponent _mainComponent; - private readonly IDragPointsInspector _dragPointsInspector; - private readonly PlayfieldComponent _playfieldComponent; - - public DragPointsInspectorHelper(IMainRenderableComponent mainComponent, IDragPointsInspector dragPointsInspector) - { - Assert.IsNotNull(mainComponent); - _mb = mainComponent as MonoBehaviour; - _mainComponent = mainComponent; - _dragPointsInspector = dragPointsInspector; - _playfieldComponent = mainComponent.gameObject.GetComponentInParent(); - DragPointsHandler = new DragPointsHandler(mainComponent, _dragPointsInspector); - } - - public void RebuildMeshes() - { - _mainComponent.RebuildMeshes(); - } - - public void OnEnable() - { - Undo.undoRedoPerformed += OnUndoRedoPerformed; - } - - public void OnDisable() - { - DragPointsHandler = null; - Undo.undoRedoPerformed -= OnUndoRedoPerformed; - } - - /// - /// Returns a reference to the drag point data for a given control ID. - /// - /// Control ID of the drag point - /// Drag point data or null if no linked data. - public DragPointData GetDragPoint(int controlId) - { - return DragPointsHandler?.GetDragPoint(controlId); - } - - /// - /// Copies the position of a drag point. - /// - /// Control ID of the drag point - public void CopyDragPoint(int controlId) - { - var dp = GetDragPoint(controlId); - if (dp != null) { - _storedControlPoint = dp.Center.ToUnityVector3(); - } - } - - /// - /// Sets the position of a previously copied drag point to another drag point. - /// - /// Control ID of the drag point to which the new position is applied. - public void PasteDragPoint(int controlId) - { - var dp = GetDragPoint(controlId); - if (dp != null) { - PrepareUndo($"Paste Drag Point {controlId}"); - dp.Center = _storedControlPoint.ToVertex3D(); - RebuildMeshes(); - } - } - - /// - /// Returns true if the game item is locked. - /// - /// True if game item is locked, false otherwise. - public bool IsItemLocked() - { - // todo delete or implement properly - return false; - } - - /// - /// Returns whether this game item has a given drag point exposure. - /// - /// Exposure to check - /// True if exposed, false otherwise. - public bool HasDragPointExposure(DragPointExposure exposure) - { - return _dragPointsInspector.DragPointExposition.Contains(exposure); - } - - /// - /// Flips all drag points on a given axis. - /// - /// Axis to flip on - public void FlipDragPoints(FlipAxis flipAxis) - { - if (_dragPointsInspector.HandleType != DragPointTransformType.ThreeD && flipAxis == FlipAxis.Z) { - return; - } - - PrepareUndo($"Flip-{flipAxis} Drag Points"); - DragPointsHandler.FlipDragPoints(flipAxis); - if (_dragPointsInspector.PointsAreLooping) { - DragPointsHandler.ReverseDragPoints(); // keep counter-clockwise orientation - } - RebuildMeshes(); - } - - /// - /// Copies drag point data to the control points used in the editor. - /// - public void RemapControlPoints() - { - var rebuilt = DragPointsHandler.RemapControlPoints(); - if (rebuilt) { - RebuildMeshes(); - } - } - - /// - /// Adds a new drag point at the traveller's current position. - /// - public void AddDragPointOnTraveller() - { - PrepareUndo($"Add drag point at position {DragPointsHandler.CurveTravellerPosition}"); - DragPointsHandler.AddDragPointOnTraveller(); - RebuildMeshes(); - } - - /// - /// Removes a drag point of a given control ID. - /// - /// Control ID of the drag point to remove. - public void RemoveDragPoint(int controlId) - { - PrepareUndo("Remove Drag Point"); - DragPointsHandler.RemoveDragPoint(controlId); - RebuildMeshes(); - } - - public void Reverse() - { - PrepareUndo("Reverse Drag Points"); - DragPointsHandler.ReverseDragPoints(); - RebuildMeshes(); - } - - /// - /// Sets an UNDO point before the next operation. - /// - /// Message to appear in the UNDO menu - public void PrepareUndo(string message) - { - Undo.RecordObject(_mb, message); - } - - public void OnInspectorGUI(ItemInspector inspector) - { - // todo remove or implement properly - // if (_mainComponent.IsLocked) { - // EditorGUILayout.LabelField("Drag Points are Locked"); - // return; - // } - - GUILayout.Space(10); - if (GUILayout.Button("Center Origin")) { - DragPointsHandler.CenterPivot(); - } - - _foldoutControlPoints = EditorGUILayout.BeginFoldoutHeaderGroup(_foldoutControlPoints, "Drag Points"); - if (_foldoutControlPoints) { - EditorGUI.indentLevel++; - for (var i = 0; i < DragPointsHandler.ControlPoints.Count; ++i) { - var controlPoint = DragPointsHandler.ControlPoints[i]; - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField($"Drag Point #{i}"); - if (GUILayout.Button("Copy")) { - CopyDragPoint(controlPoint.ControlId); - } - else if (GUILayout.Button("Paste")) { - PasteDragPoint(controlPoint.ControlId); - } - EditorGUILayout.EndHorizontal(); - EditorGUI.indentLevel++; - EditorGUI.BeginChangeCheck(); - if (_dragPointsInspector.HandleType == DragPointTransformType.TwoD) { - var pos = EditorGUILayout.Vector2Field("Position", controlPoint.DragPoint.Center.ToUnityVector2()); - if (EditorGUI.EndChangeCheck()) { - controlPoint.DragPoint.Center.X = pos.x; - controlPoint.DragPoint.Center.Y = pos.y; - RebuildMeshes(); - } - } else { - var pos = EditorGUILayout.Vector3Field("Position", controlPoint.DragPoint.Center.ToUnityVector3()); - if (EditorGUI.EndChangeCheck()) { - controlPoint.DragPoint.Center = pos.ToVertex3D(); - RebuildMeshes(); - } - } - - if (HasDragPointExposure(DragPointExposure.SlingShot)) { - inspector.ItemDataField("Slingshot", ref controlPoint.DragPoint.IsSlingshot); - } - if (HasDragPointExposure(DragPointExposure.Smooth)) { - inspector.ItemDataField("Smooth", ref controlPoint.DragPoint.IsSmooth); - } - if (HasDragPointExposure(DragPointExposure.Texture)) { - inspector.ItemDataField("Has AutoTexture", ref controlPoint.DragPoint.HasAutoTexture); - inspector.ItemDataSlider("Texture Coord", ref controlPoint.DragPoint.TextureCoord, 0.0f, 1.0f); - } - EditorGUI.indentLevel--; - } - EditorGUI.indentLevel--; - } - EditorGUILayout.EndFoldoutHeaderGroup(); - } - - private void UpdateDragPointsLock() - { - // todo remove or implement properly - // if (DragPointsHandler.UpdateDragPointsLock(_mainComponent.IsLocked)) { - // HandleUtility.Repaint(); - // } - } - - private void OnDragPointPositionChange() - { - RebuildMeshes(); - PrepareUndo("Change Drag Point Position"); - } - - private void OnUndoRedoPerformed() - { - RemapControlPoints(); - } - - public void OnSceneGUI(ItemInspector inspector) - { - if (!EditingEnabled) { - return; - } - - RemapControlPoints(); - UpdateDragPointsLock(); - - DragPointsHandler.OnSceneGUI(Event.current, OnDragPointPositionChange); - - // right mouse button clicked? - Handles.matrix = Matrix4x4.identity; - if (Event.current.type == EventType.MouseDown && Event.current.button == 1) { - var nearestControlPoint = DragPointsHandler.ControlPoints.Find(cp => cp.ControlId == HandleUtility.nearestControl); - - if (nearestControlPoint != null) { - var command = new MenuCommand(inspector, nearestControlPoint.ControlId); - EditorUtility.DisplayPopupMenu(new Rect(Event.current.mousePosition.x, Event.current.mousePosition.y, 0, 0), DragPointMenuItems.ControlPointsMenuPath, command); - Event.current.Use(); - } else if (DragPointsHandler.CurveTravellerVisible && HandleUtility.nearestControl == DragPointsHandler.CurveTravellerControlId) { - var command = new MenuCommand(inspector, 0); - EditorUtility.DisplayPopupMenu(new Rect(Event.current.mousePosition.x, Event.current.mousePosition.y, 0, 0), DragPointMenuItems.CurveTravellerMenuPath, command); - Event.current.Use(); - } - } - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs.meta deleted file mode 100644 index f2b666b5f..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsInspectorHelper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8f2c609845b713246b88a786eaf768dc -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs deleted file mode 100644 index 8ba9fb8f7..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs +++ /dev/null @@ -1,259 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using System.Collections.Generic; -using System.Linq; -using Unity.Mathematics; -using UnityEditor; -using UnityEngine; -using UnityEngine.Profiling; -using VisualPinball.Engine.Math; -using Color = UnityEngine.Color; - -namespace VisualPinball.Unity.Editor -{ - public class DragPointsSceneViewHandler - { - /// - /// Owning Handler - /// - private readonly DragPointsHandler _handler; - - /// - /// Curve points in world space - /// - private readonly List _pathPoints = new(); - - private bool _curveTravellerMoved = false; - - public float CurveWidth { get; set; } = 10.0f; - - public float CurveTravellerSizeRatio { get; set; } = 1.0f; - - public Color CurveColor { get; set; } = Color.blue; - - public Color CurveSlingShotColor { get; set; } = Color.red; - - private Transform Transform => _handler.MainComponent.gameObject.transform; - - public DragPointsSceneViewHandler(DragPointsHandler handler) - { - _handler = handler; - } - - public void OnSceneGUI() - { - if (_handler == null) { - return; - } - - DisplayCurve(); - DisplayControlPoints(); - } - - /// - /// Construct & display the curve from DragPointsHandler control points - /// Find the curve traveller position along the curve - /// - /// - /// - /// Will use the DragPointExposure from the handler's item to display slingshot segments accordingly - /// Will update handler's curve traveller position and control point base index for point insertion - /// - private void DisplayCurve() - { - Profiler.BeginSample("DisplayCurve"); - - // Display Curve & handle curve traveller - if (_handler.ControlPoints.Count > 1) { - - Profiler.BeginSample("Transform Points"); - var dragPointsVpx = new DragPointData[_handler.ControlPoints.Count]; - for (var i = 0; i < _handler.ControlPoints.Count; i++) { - var pos = _handler.ControlPoints[i].AbsolutePosition.ToVertex3D(); - dragPointsVpx[i] = new DragPointData(_handler.ControlPoints[i].DragPoint) { - Center = new Vertex3D(pos.X, pos.Y, pos.Z), - Id = _handler.ControlPoints[i].DragPoint.Id - }; - } - Profiler.EndSample(); - - Profiler.BeginSample("Create Curve Vertices"); - var vAccuracy = Vector3.one; - vAccuracy = _handler.Transform.localToWorldMatrix.MultiplyVector(vAccuracy); - var accuracy = Mathf.Abs(vAccuracy.x * vAccuracy.y * vAccuracy.z); - accuracy *= HandleUtility.GetHandleSize(_handler.CurveTravellerPosition) * ControlPoint.ScreenRadius; - var curveVerticesVpx = DragPoint.GetRgVertex( - dragPointsVpx, _handler.DragPointInspector.PointsAreLooping, accuracy - ); - Profiler.EndSample(); - - if (curveVerticesVpx.Length > 0) { - - var curveVerticesByDragPoint = new List[_handler.ControlPoints.Count].Select(_ => new List()).ToArray(); - - Profiler.BeginSample("Fill Control Points"); - // Fill Control points paths - ControlPoint currentControlPoint = null; - foreach (var curveVertex in curveVerticesVpx) { - if (curveVertex.IsControlPoint) { - if (currentControlPoint != null) { - curveVerticesByDragPoint[currentControlPoint.Index].Add(curveVertex.ToUnityVector3()); - } - - currentControlPoint = _handler.GetControlPoint(curveVertex.Id); - } - if (currentControlPoint != null) { - curveVerticesByDragPoint[currentControlPoint.Index].Add(curveVertex.ToUnityVector3()); - } - } - Profiler.EndSample(); - - // close loop if needed - Profiler.BeginSample("Close Loops"); - if (_handler.DragPointInspector.PointsAreLooping) { - curveVerticesByDragPoint[_handler.ControlPoints.Count - 1].Add(curveVerticesByDragPoint[0][0]); - } - Profiler.EndSample(); - - // construct full path - Profiler.BeginSample("Construct full path"); - _pathPoints.Clear(); - const float splitRatio = 0.1f; - foreach (var controlPoint in _handler.ControlPoints) { - // Split straight segments to avoid HandleUtility.ClosestPointToPolyLine issues - ref var segments = ref curveVerticesByDragPoint[controlPoint.Index]; - if (segments.Count == 2) { - var dir = segments[1] - segments[0]; - var dist = dir.magnitude; - dir = Vector3.Normalize(dir); - var newPath = new List { - segments[0] - }; - for (var splitDist = dist * splitRatio; splitDist < dist; splitDist += dist * splitRatio) { - newPath.Add(newPath[0] + dir * splitDist); - } - newPath.Add(segments[1]); - segments = newPath; - } - foreach (var segment in segments) { - _pathPoints.Add(segment.TranslateToWorld(Transform)); - } - } - Profiler.EndSample(); - - Profiler.BeginSample("Handle Traveller"); - _curveTravellerMoved = false; - if (_pathPoints.Count > 1) { - Handles.matrix = Matrix4x4.identity; - Profiler.BeginSample("Calculate closest"); - var newPos = HandleUtility.ClosestPointToPolyLine(_pathPoints.ToArray()); - Profiler.EndSample(); - Profiler.BeginSample("Calculate if moved"); - if ((newPos - _handler.CurveTravellerPosition).magnitude >= HandleUtility.GetHandleSize(_handler.CurveTravellerPosition) * ControlPoint.ScreenRadius * CurveTravellerSizeRatio * 0.1f) { - _handler.CurveTravellerPosition = newPos; - _curveTravellerMoved = true; - } - Profiler.EndSample(); - } - Profiler.EndSample(); - - Profiler.BeginSample("Draw Line"); - // Render Curve with correct color regarding drag point properties & find curve section where the curve traveller is - _handler.CurveTravellerControlPointIdx = -1; - var minDist = float.MaxValue; - Handles.matrix = Transform.localToWorldMatrix; - foreach (var controlPoint in _handler.ControlPoints) { - Profiler.BeginSample("Compute Segments"); - var segments = curveVerticesByDragPoint[controlPoint.Index].Select(cp => cp.TranslateToWorld()).ToArray(); - Profiler.EndSample(); - if (segments.Length > 1) { - Profiler.BeginSample("Determine Color"); - Handles.color = _handler.DragPointInspector.DragPointExposition.Contains(DragPointExposure.SlingShot) && controlPoint.DragPoint.IsSlingshot - ? CurveSlingShotColor - : CurveColor; - Profiler.EndSample(); - Profiler.BeginSample("Handles.DrawAAPolyLine"); - Handles.DrawAAPolyLine(CurveWidth, segments); - Profiler.EndSample(); - Profiler.BeginSample("Calculate closes point"); - var closestToPath = HandleUtility.ClosestPointToPolyLine(segments); - var dist = (closestToPath - (Vector3)math.inverse(Transform.localToWorldMatrix).MultiplyPoint(_handler.CurveTravellerPosition)).magnitude; - if (dist < minDist) { - minDist = dist; - _handler.CurveTravellerControlPointIdx = controlPoint.Index; - } - Profiler.EndSample(); - } - } - Profiler.EndSample(); - } - } - - Profiler.EndSample(); - } - - /// - /// Display all the control points - /// Evaluate if the curve traveller has to be shown and display it if it's needed. - /// - /// - /// - /// Will update handler's properties about curve traveller visibility - /// - private void DisplayControlPoints() - { - Profiler.BeginSample("DisplayControlPoints"); - // Render Control Points and check traveler distance from CP - var distToControlPoint = Mathf.Infinity; - Handles.matrix = Matrix4x4.identity; - var style = new GUIStyle { - alignment = TextAnchor.MiddleCenter, - }; - for (var i = 0; i < _handler.ControlPoints.Count; ++i) { - var controlPoint = _handler.ControlPoints[i]; - Handles.color = controlPoint.IsSelected ? Color.green : Color.gray; - - var pos = controlPoint.EditorPositionWorld; - var handleSize = controlPoint.HandleSize; - Handles.SphereHandleCap(-1, pos, Quaternion.identity, handleSize, EventType.Repaint); - Handles.Label(pos, $"{i}", style); - var dist = Vector3.Distance(_handler.CurveTravellerPosition, controlPoint.EditorPositionWorld); - distToControlPoint = Mathf.Min(distToControlPoint, dist); - } - - Handles.matrix = Matrix4x4.identity; - // if (!_handler.MainComponent.IsLocked) { - // curve traveller is not overlapping a control point, we can draw it. - if (distToControlPoint > HandleUtility.GetHandleSize(_handler.CurveTravellerPosition) * ControlPoint.ScreenRadius) { - Handles.color = Color.grey; - Handles.SphereHandleCap( - _handler.CurveTravellerControlId, - _handler.CurveTravellerPosition, - Quaternion.identity, - HandleUtility.GetHandleSize(_handler.CurveTravellerPosition) * ControlPoint.ScreenRadius * CurveTravellerSizeRatio, - EventType.Repaint - ); - _handler.CurveTravellerVisible = true; - if (EditorWindow.mouseOverWindow && _curveTravellerMoved) { - HandleUtility.Repaint(); - } - } - // } - Profiler.EndSample(); - } - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs.meta deleted file mode 100644 index a36113bcc..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/DragPointsSceneViewHandler.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c74729b59c98f6e4d81a0e74b7d18392 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs.meta deleted file mode 100644 index 69602b193..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4bc482d86a5df294f8007194a6a07b62 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs deleted file mode 100644 index 6e30d55bd..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using System.Collections.Generic; -using UnityEngine; -using VisualPinball.Engine.Math; -using VisualPinball.Unity.Editor; - -namespace VisualPinball.Unity -{ - /// - /// Visual Pinball keeps the same data for drag points across all game - /// items. However, not all properties are used in every game item.

- /// - /// This defines the data sets that can be used in a given game item, and - /// thus how they are exposed in the editor. - ///

- public enum DragPointExposure - { - /// - /// A drag point can be set to "smooth". - /// - Smooth, - - /// - /// A drag point can be set as "slingshot". - /// - SlingShot, - - /// - /// A drag point can be set as "auto texture". - /// - Texture - } - - public enum DragPointTransformType - { - TwoD, - ThreeD, - } - - /// - /// Abstraction inspectors that support drag points. - /// - public interface IDragPointsInspector - { - Transform Transform { get; } - - /// - /// Access to the drag point data - /// - DragPointData[] DragPoints { get; set; } - - /// - /// Whether to render drag points at all. Because sometimes we don't want to, like for trigger buttons. - /// - bool DragPointsActive { get; } - - /// - /// Returns whether the drag points are looping or not. - /// - bool PointsAreLooping { get; } - - /// - /// Returns exposed drag points features - /// - IEnumerable DragPointExposition { get; } - - /// - /// Returns the applied constrains to drag points position edition. - /// - /// - DragPointTransformType HandleType { get; } - - DragPointsInspectorHelper DragPointsHelper { get; } - - float[] TopBottomZ { get; } - - /// - /// Sets the drag point position - /// - /// Drag point to which the new position is applied to - /// The new value, where Z is the z-offset for ramps (and irrelevant for other items) - /// How many other drag points are selected - /// Only used for ramps, so top and bottom height can be adapted instead of the drag point's z. - void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints = 1, float[] topBottomZ = null); - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs.meta deleted file mode 100644 index 618c27c7e..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/IDragPointsInspector.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 6f0f2996dddc2e243b834359e370e51b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs index ecf711ff4..33d1b923c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs @@ -101,6 +101,7 @@ private async Task WritePackage(string path, IProgress progress, Logger.Info($"Writing table to {path}..."); IPackageStorage storage = null; List reactivatedObjects = null; + List excludedObjects = null; try { Report(progress, 0f, "Starting export…"); storage = PackageApi.StorageManager.CreateStorage(path); @@ -113,6 +114,7 @@ private async Task WritePackage(string path, IProgress progress, // Cabinet and backbox are authored inactive; activate them (located via their // marker components) so they flow into the active-only export. Restored in finally. reactivatedObjects = ActivateMarkedObjects(); + excludedObjects = DeactivateExcludedObjects(); // Every exported node gets a stable id; items, refs, mappings, renderer states and // light profiles all reference nodes by these ids. The ids are written into the glTF @@ -232,6 +234,7 @@ private async Task WritePackage(string path, IProgress progress, throw; } finally { storage?.Close(); + RestoreExcludedObjects(excludedObjects); RestoreMarkedObjects(reactivatedObjects); } } @@ -344,6 +347,43 @@ private static void RestoreMarkedObjects(List reactivated) } } + private List DeactivateExcludedObjects() + { + var excluded = new HashSet(); + foreach (var spline in _table.GetComponentsInChildren(true)) { + excluded.Add(spline.gameObject); + } + foreach (var marker in _table.GetComponentsInChildren(true)) { + excluded.Add(marker.gameObject); + } + + var deactivated = new List(); + try { + foreach (var go in excluded) { + if (go && go.activeSelf) { + go.SetActive(false); + deactivated.Add(go); + } + } + } catch { + RestoreExcludedObjects(deactivated); + throw; + } + return deactivated; + } + + private static void RestoreExcludedObjects(List deactivated) + { + if (deactivated == null) { + return; + } + foreach (var go in deactivated) { + if (go) { + go.SetActive(true); + } + } + } + private List CollectMarkedObjects() { var objects = new List(); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs deleted file mode 100644 index 24f18f67e..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -using UnityEditor; -using UnityEngine; - -namespace VisualPinball.Unity.Editor -{ - public static class HandlesUtils - { - /// - /// Returns the position of the moved object in VPX space. - /// - /// Original position in VPX space - /// The local-to-world matrix of the item - /// Allowed position type - /// - /// - /// Moved position in VPX space. - public static Vector3 HandlePosition(Vector3 position, Matrix4x4 localToWorld, DragPointTransformType type, float handleSize = 0.2f, float snap = 0.0f) - { - - var pos = position.TranslateToWorld(); - Handles.matrix = localToWorld; - - switch (type) { - case DragPointTransformType.TwoD: { - - var forward = Vector3.forward.TranslateToWorld().normalized; - var right = Vector3.right.TranslateToWorld().normalized; - var up = Vector3.up.TranslateToWorld().normalized; - - Handles.color = Handles.xAxisColor; - pos = Handles.Slider(pos, right); - - Handles.color = Handles.yAxisColor; - pos = Handles.Slider(pos, up); - - Handles.color = Handles.zAxisColor; - pos = Handles.Slider2D( - pos, - forward, - right, - up, - HandleUtility.GetHandleSize(pos) * handleSize, - Handles.RectangleHandleCap, - snap); - break; - } - - case DragPointTransformType.ThreeD: { - pos = Handles.PositionHandle(pos, Quaternion.identity.RotateToWorld()); - break; - } - } - return pos.TranslateToVpx(); - } - - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs.meta deleted file mode 100644 index 8a7586a27..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Utils/HandlesUtils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e81e48f5f44d97244918f6cf0d978f73 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Light/LightInsertMeshInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Light/LightInsertMeshInspector.cs index 23b1857af..5f4e45461 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Light/LightInsertMeshInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Light/LightInsertMeshInspector.cs @@ -16,41 +16,25 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Light; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(LightInsertMeshComponent)), CanEditMultipleObjects] - public class LightInsertMeshInspector : MeshInspector, IDragPointsInspector + public class LightInsertMeshInspector : MeshInspector { - public Transform Transform => null; // not needed for lights, IDragpointInspector will probably be removed. - private SerializedProperty _insertHeightProperty; private SerializedProperty _positionZProperty; - public bool DragPointsActive => true; - protected override void OnEnable() { base.OnEnable(); - DragPointsHelper = new DragPointsInspectorHelper(MeshComponent.MainComponent, this); - DragPointsHelper.OnEnable(); - _insertHeightProperty = serializedObject.FindProperty(nameof(LightInsertMeshComponent.InsertHeight)); _positionZProperty = serializedObject.FindProperty(nameof(LightInsertMeshComponent.PositionZ)); } - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -64,31 +48,12 @@ public override void OnInspectorGUI() PropertyField(_insertHeightProperty, rebuildMesh: true); PropertyField(_positionZProperty, updateTransforms: true); - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MeshComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public DragPointData[] DragPoints { get => MeshComponent.DragPoints; set => MeshComponent.DragPoints = value; } - - public bool PointsAreLooping => true; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth, DragPointExposure.Texture }; - public DragPointTransformType HandleType => DragPointTransformType.TwoD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => MeshComponent.PositionZ; - public float[] TopBottomZ => null; - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, - float[] topBottomZ) => dragPoint.Center = value; - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/MetalWireGuide/MetalWireGuideInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/MetalWireGuide/MetalWireGuideInspector.cs index e565f534a..0328de430 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/MetalWireGuide/MetalWireGuideInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/MetalWireGuide/MetalWireGuideInspector.cs @@ -16,20 +16,14 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.MetalWireGuide; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(MetalWireGuideComponent)), CanEditMultipleObjects] - public class MetalWireGuideInspector : MainInspector, IDragPointsInspector + public class MetalWireGuideInspector : MainInspector { - - public Transform Transform => MainComponent.transform; - private SerializedProperty _heightProperty; private SerializedProperty _thicknessProperty; private SerializedProperty _rotationProperty; @@ -41,9 +35,6 @@ protected override void OnEnable() base.OnEnable(); - DragPointsHelper = new DragPointsInspectorHelper(MainComponent, this); - DragPointsHelper.OnEnable(); - _heightProperty = serializedObject.FindProperty(nameof(MetalWireGuideComponent._height)); _thicknessProperty = serializedObject.FindProperty(nameof(MetalWireGuideComponent._thickness)); _rotationProperty = serializedObject.FindProperty(nameof(MetalWireGuideComponent.Rotation)); @@ -51,12 +42,6 @@ protected override void OnEnable() _standheightProperty = serializedObject.FindProperty(nameof(MetalWireGuideComponent._standheight)); } - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -73,31 +58,12 @@ public override void OnInspectorGUI() PropertyField(_thicknessProperty, rebuildMesh: true); PropertyField(_bendradiusProperty, rebuildMesh: true); - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MainComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public bool DragPointsActive => true; - public DragPointData[] DragPoints { get => MainComponent.DragPoints; set => MainComponent.DragPoints = value; } - public bool PointsAreLooping => false; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth }; - public DragPointTransformType HandleType => DragPointTransformType.TwoD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => 0f; - public float[] TopBottomZ => null; - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, - float[] topBottomZ) => dragPoint.Center = value; - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Ramp/RampInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Ramp/RampInspector.cs index fafa4313b..c8244b203 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Ramp/RampInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Ramp/RampInspector.cs @@ -16,20 +16,15 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Ramp; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(RampComponent)), CanEditMultipleObjects] - public class RampInspector : MainInspector, IDragPointsInspector + public class RampInspector : MainInspector { - public Transform Transform => MainComponent.transform; - private bool _foldoutGeometry = true; private static readonly string[] RampTypeLabels = { @@ -74,9 +69,6 @@ protected override void OnEnable() { base.OnEnable(); - DragPointsHelper = new DragPointsInspectorHelper(MainComponent, this); - DragPointsHelper.OnEnable(); - _heightBottomProperty = serializedObject.FindProperty(nameof(RampComponent._heightBottom)); _heightTopProperty = serializedObject.FindProperty(nameof(RampComponent._heightTop)); _imageAlignmentProperty = serializedObject.FindProperty(nameof(RampComponent._imageAlignment)); @@ -90,12 +82,6 @@ protected override void OnEnable() _wireDistanceYProperty = serializedObject.FindProperty(nameof(RampComponent._wireDistanceY)); } - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -137,50 +123,12 @@ public override void OnInspectorGUI() } EditorGUILayout.EndFoldoutHeaderGroup(); - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MainComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public bool DragPointsActive => true; - public DragPointData[] DragPoints { get => MainComponent.DragPoints; set => MainComponent.DragPoints = value; } - public bool PointsAreLooping => false; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth, DragPointExposure.SlingShot }; - public DragPointTransformType HandleType => DragPointTransformType.ThreeD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => 0f; - public float[] TopBottomZ => new[] { MainComponent._heightBottom, MainComponent._heightTop }; - - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, float[] topBottomZ) - { - var isFirst = MainComponent.DragPoints[0].Id == dragPoint.Id; - var isLast = MainComponent.DragPoints[^1].Id == dragPoint.Id; - var zDiff = value.Z - dragPoint.Center.Z; - - if (isFirst && numSelectedDragPoints == 1) { - MainComponent._heightBottom = topBottomZ[0] + zDiff; - dragPoint.Center.X = value.X; - dragPoint.Center.Y = value.Y; - - } else if (isLast && numSelectedDragPoints == 1) { - MainComponent._heightTop = topBottomZ[1] + zDiff; - dragPoint.Center.X = value.X; - dragPoint.Center.Y = value.Y; - - } else { - dragPoint.Center = value; - } - } - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Rubber/RubberInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Rubber/RubberInspector.cs index 183a6d755..b7d75946a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Rubber/RubberInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Rubber/RubberInspector.cs @@ -16,38 +16,24 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Rubber; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(RubberComponent)), CanEditMultipleObjects] - public class RubberInspector : MainInspector, IDragPointsInspector + public class RubberInspector : MainInspector { - - public Transform Transform => MainComponent.transform; - private SerializedProperty _thicknessProperty; protected override void OnEnable() { base.OnEnable(); - DragPointsHelper = new DragPointsInspectorHelper(MainComponent, this); - DragPointsHelper.OnEnable(); - _thicknessProperty = serializedObject.FindProperty(nameof(RubberComponent._thickness)); } - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -67,31 +53,12 @@ public override void OnInspectorGUI() } PropertyField(_thicknessProperty, rebuildMesh: true); - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MainComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public bool DragPointsActive => true; - public DragPointData[] DragPoints { get => MainComponent.DragPoints; set => MainComponent.DragPoints = value; } - public bool PointsAreLooping => true; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth }; - public DragPointTransformType HandleType => DragPointTransformType.TwoD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => 0; - public float[] TopBottomZ => null; - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, - float[] topBottomZ) => dragPoint.Center = value; - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Surface/SurfaceInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Surface/SurfaceInspector.cs index 81d0c4ae0..ed4fda8bc 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Surface/SurfaceInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Surface/SurfaceInspector.cs @@ -16,42 +16,25 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; -using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Surface; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(SurfaceComponent)), CanEditMultipleObjects] - public class SurfaceInspector : MainInspector, IDragPointsInspector + public class SurfaceInspector : MainInspector { - - public Transform Transform => MainComponent.transform; - private SerializedProperty _heightTopProperty; private SerializedProperty _heightBottomProperty; - public bool DragPointsActive => true; - protected override void OnEnable() { base.OnEnable(); - DragPointsHelper = new DragPointsInspectorHelper(MainComponent, this); - DragPointsHelper.OnEnable(); - _heightTopProperty = serializedObject.FindProperty(nameof(SurfaceComponent.HeightTop)); _heightBottomProperty = serializedObject.FindProperty(nameof(SurfaceComponent.HeightBottom)); } - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -65,30 +48,12 @@ public override void OnInspectorGUI() PropertyField(_heightTopProperty, "Top Height", true); PropertyField(_heightBottomProperty, "Bottom Height", true); - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MainComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public DragPointData[] DragPoints { get => MainComponent.DragPoints; set => MainComponent.DragPoints = value; } - public bool PointsAreLooping => true; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth, DragPointExposure.SlingShot, DragPointExposure.Texture }; - public DragPointTransformType HandleType => DragPointTransformType.TwoD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => MainComponent.HeightTop; - public float[] TopBottomZ => null; - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, - float[] topBottomZ) => dragPoint.Center = value; - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Trigger/TriggerInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Trigger/TriggerInspector.cs index 682a3545a..d032ad469 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Trigger/TriggerInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Trigger/TriggerInspector.cs @@ -16,33 +16,15 @@ // ReSharper disable AssignmentInConditionalExpression -using System.Collections.Generic; using UnityEditor; using UnityEngine; -using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT.Trigger; namespace VisualPinball.Unity.Editor { [CustomEditor(typeof(TriggerComponent)), CanEditMultipleObjects] - public class TriggerInspector : MainInspector, IDragPointsInspector + public class TriggerInspector : MainInspector { - public Transform Transform => MainComponent.transform; - - protected override void OnEnable() - { - base.OnEnable(); - - DragPointsHelper = new DragPointsInspectorHelper(MainComponent, this); - DragPointsHelper.OnEnable(); - } - - protected override void OnDisable() - { - base.OnDisable(); - DragPointsHelper.OnDisable(); - } - public override void OnInspectorGUI() { if (HasErrors()) { @@ -77,37 +59,12 @@ public override void OnInspectorGUI() MainComponent.Rotation = newRotation; } - DragPointsHelper.OnInspectorGUI(this); + DragPointSplineInspectorGUI.OnInspectorGUI(MainComponent.DragPointSpline); base.OnInspectorGUI(); EndEditing(); } - private void OnSceneGUI() - { - DragPointsHelper.OnSceneGUI(this); - } - - #region Dragpoint Tooling - - public bool DragPointsActive { - get { - var meshComp = MainComponent.GetComponent(); - return !meshComp || !meshComp.IsCircle; - } - } - - public DragPointData[] DragPoints { get => MainComponent.DragPoints; set => MainComponent.DragPoints = value; } - public bool PointsAreLooping => true; - public IEnumerable DragPointExposition => new[] { DragPointExposure.Smooth, DragPointExposure.SlingShot }; - public DragPointTransformType HandleType => DragPointTransformType.TwoD; - public DragPointsInspectorHelper DragPointsHelper { get; private set; } - public float ZOffset => MainComponent.Position.z; - public float[] TopBottomZ => null; - public void SetDragPointPosition(DragPointData dragPoint, Vertex3D value, int numSelectedDragPoints, - float[] topBottomZ) => dragPoint.Center = value; - - #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VisualPinball.Unity.Editor.asmdef b/VisualPinball.Unity/VisualPinball.Unity.Editor/VisualPinball.Unity.Editor.asmdef index 411abede6..9f12766a8 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VisualPinball.Unity.Editor.asmdef +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VisualPinball.Unity.Editor.asmdef @@ -9,6 +9,8 @@ "Unity.Collections", "Unity.Formats.Fbx.Editor", "Unity.Mathematics", + "Unity.Splines", + "Unity.Splines.Editor", "Unity.InputSystem", "VisualPinball.Engine", "VisualPinball.Unity" diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs new file mode 100644 index 000000000..d703750ce --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs @@ -0,0 +1,349 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using NUnit.Framework; +using System.Reflection; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Splines; +using UnityEngine.TestTools; +using VisualPinball.Engine.Math; +using VisualPinball.Unity.Editor; + +namespace VisualPinball.Unity.Test +{ + public class DragPointSplineComponentTests + { + [Test] + public void ShouldStoreRawVpxFloatsUnderTheBasisTransform() + { + var go = new GameObject("Rubber"); + try { + go.transform.SetPositionAndRotation(new Vector3(2.5f, -1.25f, 4.75f), + Quaternion.Euler(13f, 27f, -9f)); + go.transform.localScale = new Vector3(1.25f, 0.75f, 1.5f); + var rubber = go.AddComponent(); + var dragPoints = CreateDragPoints(); + rubber.DragPoints = dragPoints; + + var spline = rubber.DragPointSpline.Container.Spline; + for (var i = 0; i < spline.Count; i++) { + Assert.That(spline[i].Position, + Is.EqualTo(new float3(dragPoints[i].Center.X, dragPoints[i].Center.Y, + dragPoints[i].Center.Z))); + var expectedWorld = dragPoints[i].Center.ToUnityVector3() + .TranslateToWorld(go.transform); + var actualWorld = rubber.DragPointSpline.Container.transform + .TransformPoint(spline[i].Position); + Assert.That(Vector3.Distance(actualWorld, expectedWorld), Is.LessThan(1e-5f)); + } + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldKeepMetadataAlignedWhenKnotsAreInsertedAndRemoved() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var dragPointSpline = rubber.DragPointSpline; + var inheritedId = dragPointSpline.Metadata[1].Id; + + dragPointSpline.Container.Spline.Insert(1, + new float3(-80f, 72f, 15f), TangentMode.Broken); + + Assert.That(dragPointSpline.Metadata.Count, Is.EqualTo(5)); + Assert.That(dragPointSpline.Metadata[1].IsSmooth, + Is.EqualTo(dragPointSpline.Metadata[2].IsSmooth)); + Assert.That(dragPointSpline.Metadata[1].IsSlingshot, + Is.EqualTo(dragPointSpline.Metadata[2].IsSlingshot)); + Assert.That(dragPointSpline.Metadata[1].Id, Is.Not.EqualTo(inheritedId)); + Assert.That(dragPointSpline.DragPoints[1].Center, + Is.EqualTo(new Vertex3D(-80f, 72f, 0f))); + + dragPointSpline.Container.Spline.RemoveAt(1); + Assert.That(dragPointSpline.Metadata.Count, Is.EqualTo(4)); + Assert.That(dragPointSpline.DragPoints[1].Id, Is.EqualTo(inheritedId)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldClampPlanarKnotEditsAndRestoreDerivedTangents() + { + var go = new GameObject("Surface"); + try { + var surface = go.AddComponent(); + surface.DragPoints = CreateDragPoints(); + var spline = surface.DragPointSpline.Container.Spline; + var knot = spline[2]; + knot.Position.z = 123f; + knot.TangentIn = new float3(900f); + knot.TangentOut = new float3(-900f); + spline.SetKnot(2, knot); + + Assert.That(spline[2].Position.z, Is.Zero); + Assert.That(math.length(spline[2].TangentIn), Is.LessThan(900f)); + Assert.That(math.length(spline[2].TangentOut), Is.LessThan(900f)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldMapRampEndpointZEditsToHeights() + { + var go = new GameObject("Ramp"); + try { + var ramp = go.AddComponent(); + ramp._heightBottom = 10f; + ramp._heightTop = 80f; + ramp.DragPoints = CreateDragPoints(); + var spline = ramp.DragPointSpline.Container.Spline; + var bottomZ = spline[0].Position.z; + var topZ = spline[^1].Position.z; + + var bottom = spline[0]; + bottom.Position.z += 12.5f; + spline.SetKnot(0, bottom); + Assert.That(ramp._heightBottom, Is.EqualTo(22.5f)); + Assert.That(spline[0].Position.z, Is.EqualTo(bottomZ)); + + var top = spline[^1]; + top.Position.z -= 4.25f; + spline.SetKnot(spline.Count - 1, top); + Assert.That(ramp._heightTop, Is.EqualTo(75.75f)); + Assert.That(spline[^1].Position.z, Is.EqualTo(topZ)); + + var middle = spline[1]; + middle.Position.z += 6f; + spline.SetKnot(1, middle); + Assert.That(spline[1].Position.z, Is.EqualTo(middle.Position.z)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldUpgradeTheTriggerDefaultArrayLazily() + { + var go = new GameObject("Trigger"); + try { + var trigger = go.AddComponent(); + Assert.That(trigger.DragPoints.Length, Is.EqualTo(4)); + Assert.That(trigger.DragPointSpline.Container.Spline.Closed, Is.True); + Assert.That(trigger.DragPointSpline.transform.parent, Is.EqualTo(go.transform)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldRebindTheGeneratedSplineWhenTheSerializedReferenceIsMissing() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var original = rubber.DragPointSpline; + + ClearSplineReference(rubber); + + Assert.That(rubber.DragPointSpline, Is.SameAs(original)); + Assert.That(go.GetComponentsInChildren(true), + Has.Length.EqualTo(1)); + Assert.That(original.GetComponent(), Is.Not.Null); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldEmitOneSplineChangePerKnotEdit() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var spline = rubber.DragPointSpline.Container.Spline; + var changes = 0; + void CountChanges(Spline changedSpline, int _, SplineModification __) + { + if (ReferenceEquals(changedSpline, spline)) { + changes++; + } + } + + Spline.Changed += CountChanges; + try { + var knot = spline[1]; + knot.Position.x += 10f; + spline.SetKnot(1, knot); + } + finally { + Spline.Changed -= CountChanges; + } + + Assert.That(changes, Is.EqualTo(1)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldRemoveDuplicateGeneratedSplinesDeterministically() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var original = rubber.DragPointSpline; + DragPointSplineComponent.Create(rubber, CreateDragPoints()); + ClearSplineReference(rubber); + LogAssert.Expect(LogType.Warning, + "Removing generated spline child 'Spline' from 'Rubber' because it duplicates another drag-point spline."); + + Assert.That(rubber.DragPointSpline, Is.SameAs(original)); + Assert.That(go.GetComponentsInChildren(true), + Has.Length.EqualTo(1)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldRemoveInvalidGeneratedChildrenWithoutSkippingTheSpline() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var original = rubber.DragPointSpline; + var invalid = new GameObject("Invalid Spline"); + invalid.transform.SetParent(go.transform, false); + invalid.transform.SetSiblingIndex(0); + invalid.AddComponent(); + ClearSplineReference(rubber); + LogAssert.Expect(LogType.Warning, + "Removing generated spline child 'Invalid Spline' from 'Rubber' because it has no functional drag-point spline."); + + Assert.That(rubber.DragPointSpline, Is.SameAs(original)); + Assert.That(go.GetComponentsInChildren(true), + Has.Length.EqualTo(1)); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldReverseAroundTheFirstKnotAndRotateSlingshots() + { + var go = new GameObject("Surface"); + try { + var surface = go.AddComponent(); + surface.DragPoints = CreateDragPoints(); + + DragPointSplineInspectorGUI.Reverse(surface.DragPointSpline); + + var dragPoints = surface.DragPoints; + Assert.That(dragPoints, Has.Length.EqualTo(4)); + Assert.That(dragPoints[0].Id, Is.EqualTo("a")); + Assert.That(dragPoints[1].Id, Is.EqualTo("d")); + Assert.That(dragPoints[2].Id, Is.EqualTo("c")); + Assert.That(dragPoints[3].Id, Is.EqualTo("b")); + Assert.That(dragPoints[0].IsSlingshot, Is.True); + Assert.That(dragPoints[1].IsSlingshot, Is.False); + Assert.That(dragPoints[2].IsSlingshot, Is.True); + Assert.That(dragPoints[3].IsSlingshot, Is.False); + } + finally { + Object.DestroyImmediate(go); + } + } + + [Test] + public void ShouldCenterTheOriginWithoutMovingTheWorldCurve() + { + var go = new GameObject("Rubber"); + try { + var rubber = go.AddComponent(); + rubber.DragPoints = CreateDragPoints(); + var before = GetWorldPositions(rubber.DragPointSpline.Container); + + DragPointSplineInspectorGUI.CenterOrigin(rubber.DragPointSpline); + + var after = GetWorldPositions(rubber.DragPointSpline.Container); + for (var i = 0; i < before.Length; i++) { + Assert.That(Vector3.Distance(after[i], before[i]), Is.LessThan(1e-5f)); + } + } + finally { + Object.DestroyImmediate(go); + } + } + + private static Vector3[] GetWorldPositions(SplineContainer container) + { + var positions = new Vector3[container.Spline.Count]; + for (var i = 0; i < positions.Length; i++) { + positions[i] = container.transform.TransformPoint(container.Spline[i].Position); + } + return positions; + } + + private static void ClearSplineReference(RubberComponent rubber) + { + typeof(RubberComponent) + .GetField("_dragPointSpline", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(rubber, null); + } + + private static DragPointData[] CreateDragPoints() + { + return new[] { + CreateDragPoint("a", -120.25f, 15.5f, 1.25f, true, false), + CreateDragPoint("b", -25.75f, 140.125f, 18.5f, false, true), + CreateDragPoint("c", 95.375f, 80.625f, -7.75f, true, false), + CreateDragPoint("d", 155.875f, -45.25f, 32.125f, true, true), + }; + } + + private static DragPointData CreateDragPoint(string id, float x, float y, float z, + bool smooth, bool slingshot) + { + return new DragPointData(new Vertex3D(x, y, z)) { + Id = id, + IsSmooth = smooth, + IsSlingshot = slingshot, + HasAutoTexture = false, + TextureCoord = (id[0] - 'a') * 0.25f, + }; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs.meta new file mode 100644 index 000000000..3374bfd3c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineComponentTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8b5f92d3016cab48bf4c3a28cb8df75 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs new file mode 100644 index 000000000..a6b47f243 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs @@ -0,0 +1,144 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using NUnit.Framework; +using Unity.Mathematics; +using UnityEngine.Splines; +using VisualPinball.Engine.Math; + +namespace VisualPinball.Unity.Test +{ + public class DragPointSplineConverterTests + { + [TestCase(false)] + [TestCase(true)] + public void ShouldRoundTripDragPointsBitIdentically(bool loop) + { + var expected = CreateDragPoints(); + var spline = DragPointSplineConverter.ToSpline(expected, loop); + var actual = DragPointSplineConverter.ToDragPoints(spline, + DragPointSplineConverter.ToMetadata(expected)); + + Assert.That(spline.Closed, Is.EqualTo(loop)); + Assert.That(actual.Length, Is.EqualTo(expected.Length)); + for (var i = 0; i < expected.Length; i++) { + AssertDragPointEqual(expected[i], actual[i], i); + Assert.That(spline.GetTangentMode(i), Is.EqualTo(TangentMode.Broken)); + } + } + + [TestCase(false)] + [TestCase(true)] + public void ShouldMatchCentripetalCatmullRomSegments(bool loop) + { + var dragPoints = CreateDragPoints(); + var spline = DragPointSplineConverter.ToSpline(dragPoints, loop); + var segmentCount = loop ? dragPoints.Length : dragPoints.Length - 1; + for (var i = 0; i < segmentCount; i++) { + var nextIndex = i < dragPoints.Length - 1 ? i + 1 : 0; + var previousIndex = dragPoints[i].IsSmooth ? i - 1 : i; + if (previousIndex < 0) { + previousIndex = loop ? dragPoints.Length - 1 : 0; + } + + var followingIndex = dragPoints[nextIndex].IsSmooth ? i + 2 : i + 1; + if (followingIndex >= dragPoints.Length) { + followingIndex = loop ? followingIndex - dragPoints.Length : dragPoints.Length - 1; + } + + var catmull = CatmullCurve.GetInstance< + CatmullCurve3DCatmullCurveFactory>( + dragPoints[previousIndex].Center, + dragPoints[i].Center, + dragPoints[nextIndex].Center, + dragPoints[followingIndex].Center); + var bezier = spline.GetCurve(i); + for (var sample = 0; sample <= 20; sample++) { + var t = sample / 20f; + var expected = catmull.GetPointAt(t); + var actual = CurveUtility.EvaluatePosition(bezier, t); + Assert.That(math.distance(actual, + new float3(expected.X, expected.Y, expected.Z)), Is.LessThan(2e-5f), + $"segment {i}, t={t}"); + } + } + } + + [Test] + public void ShouldRestoreDerivedTangentsAfterSplineEdits() + { + var dragPoints = CreateDragPoints(); + var metadata = DragPointSplineConverter.ToMetadata(dragPoints); + var spline = DragPointSplineConverter.ToSpline(dragPoints, true); + var knot = spline[1]; + knot.Position += new float3(12.5f, -4.25f, 7.75f); + knot.TangentIn = new float3(900f); + knot.TangentOut = new float3(-900f); + spline.SetKnot(1, knot); + + DragPointSplineConverter.RecalculateTangents(spline, metadata); + var expected = DragPointSplineConverter.ToSpline( + DragPointSplineConverter.ToDragPoints(spline, metadata), true); + for (var i = 0; i < spline.Count; i++) { + Assert.That(spline[i], Is.EqualTo(expected[i])); + } + } + + private static DragPointData[] CreateDragPoints() + { + return new[] { + CreateDragPoint("a", -120.25f, 15.5f, 1.25f, true, false, 0.125f), + CreateDragPoint("b", -25.75f, 140.125f, 18.5f, false, true, 0.375f), + CreateDragPoint("c", 95.375f, 80.625f, -7.75f, true, false, 0.625f), + CreateDragPoint("d", 155.875f, -45.25f, 32.125f, true, true, 0.875f), + }; + } + + private static DragPointData CreateDragPoint(string id, float x, float y, float z, + bool smooth, bool slingshot, float textureCoord) + { + return new DragPointData(new Vertex3D(x, y, z)) { + Id = id, + IsSmooth = smooth, + IsSlingshot = slingshot, + HasAutoTexture = false, + TextureCoord = textureCoord, + IsLocked = id == "c", + EditorLayer = id[0], + EditorLayerName = $"layer-{id}", + EditorLayerVisibility = id != "b", + CalcHeight = textureCoord * 100f, + }; + } + + private static void AssertDragPointEqual(DragPointData expected, DragPointData actual, + int index) + { + Assert.That(actual.Center, Is.EqualTo(expected.Center), $"point {index}, center"); + Assert.That(actual.IsSmooth, Is.EqualTo(expected.IsSmooth), $"point {index}, smooth"); + Assert.That(actual.IsSlingshot, Is.EqualTo(expected.IsSlingshot), $"point {index}, slingshot"); + Assert.That(actual.HasAutoTexture, Is.EqualTo(expected.HasAutoTexture), $"point {index}, auto texture"); + Assert.That(actual.TextureCoord, Is.EqualTo(expected.TextureCoord), $"point {index}, texture coordinate"); + Assert.That(actual.IsLocked, Is.EqualTo(expected.IsLocked), $"point {index}, locked"); + Assert.That(actual.EditorLayer, Is.EqualTo(expected.EditorLayer), $"point {index}, editor layer"); + Assert.That(actual.EditorLayerName, Is.EqualTo(expected.EditorLayerName), $"point {index}, editor layer name"); + Assert.That(actual.EditorLayerVisibility, Is.EqualTo(expected.EditorLayerVisibility), + $"point {index}, editor layer visibility"); + Assert.That(actual.Id, Is.EqualTo(expected.Id), $"point {index}, id"); + Assert.That(actual.CalcHeight, Is.EqualTo(expected.CalcHeight), $"point {index}, calculated height"); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs.meta new file mode 100644 index 000000000..d7f4fd678 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/DragPointSplineConverterTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 57b1859bddfd191438cfe4543310d94e \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/VPT/RubberTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/VPT/RubberTests.cs index 379b280a5..1a2d9928e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Test/VPT/RubberTests.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/VPT/RubberTests.cs @@ -14,13 +14,20 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using System; +using System.Collections; using System.IO; +using System.Linq; +using System.Text; using NUnit.Framework; using UnityEngine; +using UnityEngine.TestTools; +using VisualPinball.Engine.Math; using VisualPinball.Engine.Test.Test; using VisualPinball.Engine.Test.VPT.Rubber; using VisualPinball.Engine.VPT.Table; using VisualPinball.Unity.Editor; +using Object = UnityEngine.Object; namespace VisualPinball.Unity.Test { @@ -42,5 +49,90 @@ public void ShouldWriteImportedRubberData() Object.DestroyImmediate(go); } + [UnityTest] + public IEnumerator ShouldKeepGeneratedSplinesOutOfPackageRoundTrips() + { + var firstPackage = Path.Combine(Path.GetTempPath(), + $"vpe-spline-package-{Guid.NewGuid():N}.vpe"); + var secondPackage = Path.Combine(Path.GetTempPath(), + $"vpe-spline-package-{Guid.NewGuid():N}.vpe"); + GameObject source = null; + GameObject imported = null; + try { + source = CreatePackageTable(); + Object.DestroyImmediate(source.GetComponentInChildren()); + new PackageWriter(source).WritePackageSync(firstPackage); + Assert.That(source.GetComponentInChildren(), Is.Null); + AssertPackageHasNoSplineNodes(firstPackage); + + var importTask = new RuntimePackageReader(firstPackage).ImportIntoScene(); + while (!importTask.IsCompleted) { + yield return null; + } + if (importTask.IsFaulted) { + throw importTask.Exception!.GetBaseException(); + } + imported = importTask.Result; + AssertFunctionalGeneratedSplines(imported); + + new PackageWriter(imported).WritePackageSync(secondPackage); + AssertPackageHasNoSplineNodes(secondPackage); + AssertFunctionalGeneratedSplines(imported); + } + finally { + if (source) { + Object.DestroyImmediate(source); + } + if (imported) { + Object.DestroyImmediate(imported); + } + File.Delete(firstPackage); + File.Delete(secondPackage); + } + } + + private static void AssertPackageHasNoSplineNodes(string path) + { + using var storage = PackageApi.StorageManager.OpenStorage(path); + var sceneData = storage.GetFolder(PackageApi.TableFolder) + .GetFile(PackageApi.SceneFile).GetData(); + var glbContents = Encoding.UTF8.GetString(sceneData); + Assert.That(glbContents, Does.Not.Match("\\\"name\\\"\\s*:\\s*\\\"Spline\\\"")); + } + + private static GameObject CreatePackageTable() + { + var table = new GameObject("Table"); + table.AddComponent(); + var rubberObject = new GameObject("Rubber"); + rubberObject.transform.SetParent(table.transform, false); + var rubber = rubberObject.AddComponent(); + rubber.DragPoints = new[] { + new DragPointData(-100f, -100f), + new DragPointData(-100f, 100f), + new DragPointData(100f, 100f), + new DragPointData(100f, -100f), + }; + return table; + } + + private static void AssertFunctionalGeneratedSplines(GameObject table) + { + var rubbers = table.GetComponentsInChildren(true); + Assert.That(rubbers, Is.Not.Empty); + foreach (var rubber in rubbers) { + var generated = rubber.GetComponentsInChildren(true); + Assert.That(generated, Has.Length.EqualTo(1), rubber.name); + var spline = generated[0].GetComponent(); + Assert.That(spline, Is.Not.Null, rubber.name); + Assert.That(spline!.Container, Is.Not.Null, rubber.name); + Assert.That(spline.DragPoints, Is.Not.Empty, rubber.name); + Assert.That(rubber.DragPointSpline, Is.SameAs(spline), rubber.name); + Assert.That(rubber.transform.Cast() + .Count(child => child.name == "Spline" + && !child.GetComponent()), Is.Zero, rubber.name); + } + } + } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/VisualPinball.Unity.Test.asmdef b/VisualPinball.Unity/VisualPinball.Unity.Test/VisualPinball.Unity.Test.asmdef index 19148d705..0f2fb156b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Test/VisualPinball.Unity.Test.asmdef +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/VisualPinball.Unity.Test.asmdef @@ -6,6 +6,7 @@ "Unity.Entities", "Unity.Entities.Hybrid", "Unity.Mathematics", + "Unity.Splines", "Unity.Transforms", "com.bartofzo.nativetrees", "VisualPinball.Engine", diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint.meta new file mode 100644 index 000000000..113d51de3 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 71b7101902bc0aa4e9418b18f6a82049 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs new file mode 100644 index 000000000..132b3d98f --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs @@ -0,0 +1,98 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using UnityEngine; +using VisualPinball.Engine.Math; + +namespace VisualPinball.Unity +{ + [Serializable] + public class DragPointMetadata + { + [SerializeField] private bool _isSmooth; + [SerializeField] private bool _isSlingshot; + [SerializeField] private bool _hasAutoTexture = true; + [SerializeField] private float _textureCoord; + [SerializeField] private bool _isLocked; + [SerializeField] private int _editorLayer; + [SerializeField] private string _editorLayerName = string.Empty; + [SerializeField] private bool _editorLayerVisibility = true; + [SerializeField] private string _id; + [SerializeField] private float _calcHeight; + + public bool IsSmooth { get => _isSmooth; set => _isSmooth = value; } + public bool IsSlingshot { get => _isSlingshot; set => _isSlingshot = value; } + public bool HasAutoTexture { get => _hasAutoTexture; set => _hasAutoTexture = value; } + public float TextureCoord { get => _textureCoord; set => _textureCoord = value; } + public bool IsLocked { get => _isLocked; set => _isLocked = value; } + public int EditorLayer { get => _editorLayer; set => _editorLayer = value; } + public string EditorLayerName { get => _editorLayerName; set => _editorLayerName = value; } + public bool EditorLayerVisibility { get => _editorLayerVisibility; set => _editorLayerVisibility = value; } + public string Id { get => _id; set => _id = value; } + public float CalcHeight { get => _calcHeight; set => _calcHeight = value; } + + public DragPointMetadata() { } + + public DragPointMetadata(DragPointData dragPoint) + { + CopyFrom(dragPoint); + } + + public static DragPointMetadata CreateInserted(DragPointMetadata previous, + DragPointMetadata next) + { + return new DragPointMetadata { + _isSmooth = next._isSmooth, + _isSlingshot = next._isSlingshot, + _hasAutoTexture = next._hasAutoTexture, + _textureCoord = next._textureCoord, + _editorLayer = next._editorLayer, + _editorLayerName = next._editorLayerName, + _editorLayerVisibility = previous._editorLayerVisibility, + _id = Guid.NewGuid().ToString()[..8], + }; + } + + public void CopyFrom(DragPointData dragPoint) + { + _isSmooth = dragPoint.IsSmooth; + _isSlingshot = dragPoint.IsSlingshot; + _hasAutoTexture = dragPoint.HasAutoTexture; + _textureCoord = dragPoint.TextureCoord; + _isLocked = dragPoint.IsLocked; + _editorLayer = dragPoint.EditorLayer; + _editorLayerName = dragPoint.EditorLayerName; + _editorLayerVisibility = dragPoint.EditorLayerVisibility; + _id = dragPoint.Id; + _calcHeight = dragPoint.CalcHeight; + } + + public void CopyTo(DragPointData dragPoint) + { + dragPoint.IsSmooth = _isSmooth; + dragPoint.IsSlingshot = _isSlingshot; + dragPoint.HasAutoTexture = _hasAutoTexture; + dragPoint.TextureCoord = _textureCoord; + dragPoint.IsLocked = _isLocked; + dragPoint.EditorLayer = _editorLayer; + dragPoint.EditorLayerName = _editorLayerName; + dragPoint.EditorLayerVisibility = _editorLayerVisibility; + dragPoint.Id = _id; + dragPoint.CalcHeight = _calcHeight; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs.meta new file mode 100644 index 000000000..3d9c2d23a --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointMetadata.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5059e56786f5c8f4c9f77838ae9495d9 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs new file mode 100644 index 000000000..93ad54ac6 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs @@ -0,0 +1,378 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Splines; +using VisualPinball.Engine.Math; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace VisualPinball.Unity +{ + [AddComponentMenu("")] + [DisallowMultipleComponent] + public class DragPointSplineComponent : MonoBehaviour + { + [SerializeField] private SplineContainer _container; + [SerializeField] private MonoBehaviour _owner; + [SerializeField] private List _metadata = new(); + + [NonSerialized] private DragPointData[] _dragPoints; + [NonSerialized] private float3[] _positions = Array.Empty(); + [NonSerialized] private bool _handlingChange; + + public SplineContainer Container => _container; + public IReadOnlyList Metadata => _metadata; + public IDragPointSplineOwner Owner => _owner as IDragPointSplineOwner; + + public DragPointData[] DragPoints + { + get { + if (_dragPoints == null) { + _dragPoints = DragPointSplineConverter.ToDragPoints(_container.Spline, _metadata); + } + return _dragPoints; + } + } + + private void OnEnable() + { + Subscribe(); + RememberPositions(); + } + + private void OnDisable() + { + Spline.Changed -= OnSplineChanged; + } + + public static DragPointSplineComponent Create(IDragPointSplineOwner owner, + IReadOnlyList dragPoints) + { + var splineObject = new GameObject("Spline"); + splineObject.transform.SetParent(owner.SplineTransform, false); + splineObject.transform.localPosition = Vector3.zero; + splineObject.transform.localRotation = ((Matrix4x4)Physics.VpxToWorld).rotation; + splineObject.transform.localScale = Physics.ScaleInvVector; + + splineObject.AddComponent(); + var container = splineObject.AddComponent(); + var component = splineObject.AddComponent(); + component._container = container; + component.Bind(owner); + component.SetDragPoints(dragPoints); + +#if UNITY_EDITOR + EditorUtility.SetDirty(owner.SplineOwner); + EditorUtility.SetDirty(component); +#endif + return component; + } + + public static DragPointSplineComponent GetOrCreate(IDragPointSplineOwner owner, + DragPointSplineComponent current, IReadOnlyList dragPoints) + { + if (owner == null) { + throw new ArgumentNullException(nameof(owner)); + } + + if (IsFunctionalChild(owner, current)) { + current.Bind(owner); + if (dragPoints != null) { + current.SetDragPoints(dragPoints); + } + return current; + } + + var component = ResolveExisting(owner, current); + if (!component) { + return Create(owner, dragPoints ?? Array.Empty()); + } + + component.Bind(owner); + if (dragPoints != null) { + component.SetDragPoints(dragPoints); + } + return component; + } + + private static bool IsFunctionalChild(IDragPointSplineOwner owner, + DragPointSplineComponent component) + { + return component && component.transform.parent == owner.SplineTransform + && component.GetComponent(); + } + + private static DragPointSplineComponent ResolveExisting(IDragPointSplineOwner owner, + DragPointSplineComponent current) + { + var candidates = new List(); + var ownerTransform = owner.SplineTransform; + var generatedChildren = new List(); + for (var i = 0; i < ownerTransform.childCount; i++) { + var child = ownerTransform.GetChild(i); + var marker = child.GetComponent(); + var component = child.GetComponent(); + if (marker || component) { + generatedChildren.Add(child); + } + } + + foreach (var child in generatedChildren) { + var marker = child.GetComponent(); + var component = child.GetComponent(); + if (component && component.GetComponent()) { + EnsureExportMarker(component.gameObject); + candidates.Add(component); + } else if (marker) { + RemoveGeneratedChild(owner, child.gameObject, + "it has no functional drag-point spline"); + } + } + + DragPointSplineComponent selected = null; + if (current && current.transform.parent == ownerTransform + && current.GetComponent()) { + EnsureExportMarker(current.gameObject); + selected = current; + if (!candidates.Contains(current)) { + candidates.Insert(0, current); + } + } else if (candidates.Count > 0) { + selected = candidates[0]; + } + + foreach (var candidate in candidates) { + if (candidate != selected) { + RemoveGeneratedChild(owner, candidate.gameObject, + "it duplicates another drag-point spline"); + } + } + return selected; + } + + private static void EnsureExportMarker(GameObject splineObject) + { + if (!splineObject.TryGetComponent(out _)) { +#if UNITY_EDITOR + if (!Application.isPlaying) { + Undo.AddComponent(splineObject); + EditorUtility.SetDirty(splineObject); + return; + } +#endif + splineObject.AddComponent(); + } + } + + private static void RemoveGeneratedChild(IDragPointSplineOwner owner, + GameObject generatedChild, string reason) + { + var canDestroy = CanDestroyGeneratedChild(generatedChild); + var action = canDestroy ? "Removing" : "Disabling"; + var suffix = canDestroy + ? string.Empty + : " It belongs to a prefab instance and cannot be removed without unpacking it."; + Debug.LogWarning( + $"{action} generated spline child '{generatedChild.name}' from '{owner.SplineOwner.name}' because {reason}.{suffix}", + owner.SplineOwner); + generatedChild.SetActive(false); + if (!canDestroy) { + return; + } + if (Application.isPlaying) { + UnityEngine.Object.Destroy(generatedChild); + } else { +#if UNITY_EDITOR + Undo.DestroyObjectImmediate(generatedChild); +#else + UnityEngine.Object.DestroyImmediate(generatedChild); +#endif + } + } + + private static bool CanDestroyGeneratedChild(GameObject generatedChild) + { +#if UNITY_EDITOR + return Application.isPlaying || !PrefabUtility.IsPartOfPrefabInstance(generatedChild) + || PrefabUtility.IsAddedGameObjectOverride(generatedChild); +#else + return true; +#endif + } + + public void Bind(IDragPointSplineOwner owner) + { + _owner = owner.SplineOwner; + if (!_container) { + _container = GetComponent(); + } + Subscribe(); + RememberPositions(); + } + + private void Subscribe() + { + Spline.Changed -= OnSplineChanged; + Spline.Changed += OnSplineChanged; + } + + public void SetDragPoints(IReadOnlyList dragPoints) + { + if (dragPoints == null) { + throw new ArgumentNullException(nameof(dragPoints)); + } + + _handlingChange = true; + try { + _metadata = DragPointSplineConverter.ToMetadata(dragPoints); + _container.Spline = DragPointSplineConverter.ToSpline(dragPoints, + Owner?.SplineClosed ?? false); + _dragPoints = null; + RememberPositions(); + } + finally { + _handlingChange = false; + } + } + + public void NotifyMetadataChanged() + { + _handlingChange = true; + try { + CaptureRuntimeMetadata(); + DragPointSplineConverter.RecalculateTangents(_container.Spline, _metadata); + _dragPoints = null; + RememberPositions(); + } + finally { + _handlingChange = false; + } + Owner?.RebuildSplineMeshes(); + } + + private void OnSplineChanged(Spline spline, int knotIndex, + SplineModification modification) + { + if (_handlingChange || !_container || !ReferenceEquals(spline, _container.Spline)) { + return; + } + + var owner = Owner; + if (owner == null) { + return; + } + + _handlingChange = true; + try { +#if UNITY_EDITOR + Undo.RecordObjects(new UnityEngine.Object[] { this, owner.SplineOwner }, + "Edit Drag Points"); +#endif + CaptureRuntimeMetadata(); + AlignMetadata(modification, knotIndex, spline.Closed); + ApplyPlanarConstraint(spline, knotIndex, modification, owner.SplinePlanar); + owner.ApplySplineConstraints(spline, knotIndex, modification, _positions); + DragPointSplineConverter.RecalculateTangents(spline, _metadata); + _dragPoints = null; + RememberPositions(); + +#if UNITY_EDITOR + EditorUtility.SetDirty(this); + EditorUtility.SetDirty(owner.SplineOwner); + PrefabUtility.RecordPrefabInstancePropertyModifications(this); + PrefabUtility.RecordPrefabInstancePropertyModifications(owner.SplineOwner); +#endif + } + finally { + _handlingChange = false; + } + + owner.RebuildSplineMeshes(); + } + + private void AlignMetadata(SplineModification modification, int knotIndex, bool closed) + { + switch (modification) { + case SplineModification.KnotInserted: + if (_metadata.Count == 0) { + _metadata.Add(new DragPointMetadata(new DragPointData(0f, 0f))); + break; + } + var previousIndex = knotIndex - 1; + if (previousIndex < 0) { + previousIndex = closed ? _metadata.Count - 1 : 0; + } + var nextIndex = math.min(knotIndex, _metadata.Count - 1); + _metadata.Insert(knotIndex, DragPointMetadata.CreateInserted( + _metadata[previousIndex], _metadata[nextIndex])); + break; + + case SplineModification.KnotRemoved: + if (knotIndex >= 0 && knotIndex < _metadata.Count) { + _metadata.RemoveAt(knotIndex); + } + break; + } + + if (_metadata.Count != _container.Spline.Count) { + throw new InvalidOperationException( + $"Spline has {_container.Spline.Count} knots but metadata has {_metadata.Count} entries after {modification}."); + } + } + + private static void ApplyPlanarConstraint(Spline spline, int knotIndex, + SplineModification modification, bool planar) + { + if (!planar || modification is not (SplineModification.KnotModified + or SplineModification.KnotInserted) || knotIndex < 0 || knotIndex >= spline.Count) { + return; + } + + var knot = spline[knotIndex]; + knot.Position.z = 0f; + spline.SetKnotNoNotify(knotIndex, knot); + } + + private void CaptureRuntimeMetadata() + { + if (_dragPoints == null || _dragPoints.Length != _metadata.Count) { + return; + } + for (var i = 0; i < _metadata.Count; i++) { + _metadata[i].CalcHeight = _dragPoints[i].CalcHeight; + } + } + + private void RememberPositions() + { + if (!_container || _container.Spline == null) { + _positions = Array.Empty(); + return; + } + + _positions = new float3[_container.Spline.Count]; + for (var i = 0; i < _positions.Length; i++) { + _positions[i] = _container.Spline[i].Position; + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs.meta new file mode 100644 index 000000000..7ca91f8d9 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineComponent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 736303edf4395b3419a3787655fac458 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs new file mode 100644 index 000000000..d8c46868e --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs @@ -0,0 +1,178 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine.Splines; +using VisualPinball.Engine.Math; + +namespace VisualPinball.Unity +{ + public static class DragPointSplineConverter + { + private const float MinKnotDistance = 1e-4f; + + public static Spline ToSpline(IReadOnlyList dragPoints, bool loop) + { + if (dragPoints == null) { + throw new ArgumentNullException(nameof(dragPoints)); + } + + var positions = new float3[dragPoints.Count]; + var smooth = new bool[dragPoints.Count]; + for (var i = 0; i < dragPoints.Count; i++) { + positions[i] = ToFloat3(dragPoints[i].Center); + smooth[i] = dragPoints[i].IsSmooth; + } + + var (tangentIn, tangentOut) = CalculateTangents(positions, smooth, loop); + var spline = new Spline(dragPoints.Count, loop); + for (var i = 0; i < dragPoints.Count; i++) { + spline.Add(new BezierKnot(positions[i], tangentIn[i], tangentOut[i], + quaternion.identity), TangentMode.Broken); + } + return spline; + } + + public static List ToMetadata(IReadOnlyList dragPoints) + { + if (dragPoints == null) { + throw new ArgumentNullException(nameof(dragPoints)); + } + + var metadata = new List(dragPoints.Count); + for (var i = 0; i < dragPoints.Count; i++) { + metadata.Add(new DragPointMetadata(dragPoints[i])); + } + return metadata; + } + + public static DragPointData[] ToDragPoints(Spline spline, + IReadOnlyList metadata) + { + ValidateMetadata(spline, metadata); + + var dragPoints = new DragPointData[spline.Count]; + for (var i = 0; i < spline.Count; i++) { + var position = spline[i].Position; + var dragPoint = new DragPointData(new Vertex3D(position.x, position.y, position.z)); + metadata[i].CopyTo(dragPoint); + dragPoints[i] = dragPoint; + } + return dragPoints; + } + + public static void RecalculateTangents(Spline spline, + IReadOnlyList metadata) + { + ValidateMetadata(spline, metadata); + + var positions = new float3[spline.Count]; + var smooth = new bool[spline.Count]; + for (var i = 0; i < spline.Count; i++) { + positions[i] = spline[i].Position; + smooth[i] = metadata[i].IsSmooth; + } + + var (tangentIn, tangentOut) = CalculateTangents(positions, smooth, spline.Closed); + for (var i = 0; i < spline.Count; i++) { + var knot = spline[i]; + knot.TangentIn = tangentIn[i]; + knot.TangentOut = tangentOut[i]; + knot.Rotation = quaternion.identity; + spline.SetTangentModeNoNotify(i, TangentMode.Broken); + spline.SetKnotNoNotify(i, knot); + } + } + + private static (float3[] tangentIn, float3[] tangentOut) CalculateTangents( + IReadOnlyList positions, IReadOnlyList smooth, bool loop) + { + var tangentIn = new float3[positions.Count]; + var tangentOut = new float3[positions.Count]; + var segmentCount = loop ? positions.Count : math.max(0, positions.Count - 1); + for (var i = 0; i < segmentCount; i++) { + var nextIndex = i < positions.Count - 1 ? i + 1 : 0; + var p1 = positions[i]; + var p2 = positions[nextIndex]; + if (math.all(p1 == p2)) { + continue; + } + + var previousIndex = smooth[i] ? i - 1 : i; + if (previousIndex < 0) { + previousIndex = loop ? positions.Count - 1 : 0; + } + + var followingIndex = smooth[nextIndex] ? i + 2 : i + 1; + if (followingIndex >= positions.Count) { + followingIndex = loop ? followingIndex - positions.Count : positions.Count - 1; + } + + var (startTangent, endTangent) = CalculateSegmentTangents( + positions[previousIndex], p1, p2, positions[followingIndex]); + tangentOut[i] = startTangent / 3f; + tangentIn[nextIndex] = -endTangent / 3f; + } + return (tangentIn, tangentOut); + } + + private static (float3 start, float3 end) CalculateSegmentTangents(float3 p0, + float3 p1, float3 p2, float3 p3) + { + var dt0 = math.sqrt(math.distance(p0, p1)); + var dt1 = math.sqrt(math.distance(p1, p2)); + var dt2 = math.sqrt(math.distance(p2, p3)); + if (dt1 < MinKnotDistance) { + dt1 = 1f; + } + if (dt0 < MinKnotDistance) { + dt0 = dt1; + } + if (dt2 < MinKnotDistance) { + dt2 = dt1; + } + + var start = (p1 - p0) / dt0 - (p2 - p0) / (dt0 + dt1) + + (p2 - p1) / dt1; + var end = (p2 - p1) / dt1 - (p3 - p1) / (dt1 + dt2) + + (p3 - p2) / dt2; + return (start * dt1, end * dt1); + } + + private static void ValidateMetadata(Spline spline, + IReadOnlyList metadata) + { + if (spline == null) { + throw new ArgumentNullException(nameof(spline)); + } + if (metadata == null) { + throw new ArgumentNullException(nameof(metadata)); + } + if (spline.Count != metadata.Count) { + throw new ArgumentException( + $"Spline has {spline.Count} knots but metadata has {metadata.Count} entries.", + nameof(metadata)); + } + } + + private static float3 ToFloat3(Vertex3D vertex) + { + return new float3(vertex.X, vertex.Y, vertex.Z); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs.meta new file mode 100644 index 000000000..f5d59a153 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/DragPointSplineConverter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1ad9e007b11e75c44aecb12b15d3cdad \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs similarity index 59% rename from VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs rename to VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs index d2fab0e9b..5f681c6b7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/DragPoint/FlipAxis.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs @@ -1,25 +1,30 @@ -// Visual Pinball Engine -// Copyright (C) 2023 freezy and VPE Team -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -namespace VisualPinball.Unity.Editor -{ - public enum FlipAxis - { - X, - Y, - Z, - } -} +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using UnityEngine; + +namespace VisualPinball.Unity +{ + /// + /// Marks a generated drag-point spline GameObject. The object is authoring-only and is + /// rebuilt from its owner's packed drag points, so the package writer excludes its subtree. + /// + [AddComponentMenu("")] + [DisallowMultipleComponent] + public sealed class GeneratedDragPointSplineComponent : MonoBehaviour + { + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs.meta new file mode 100644 index 000000000..18604a946 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/GeneratedDragPointSplineComponent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aad20eee022b486f82c422e6b36380f3 diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs new file mode 100644 index 000000000..997f27c9d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs @@ -0,0 +1,36 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Splines; + +namespace VisualPinball.Unity +{ + public interface IDragPointSplineOwner + { + MonoBehaviour SplineOwner { get; } + Transform SplineTransform { get; } + DragPointSplineComponent SplineComponent { get; } + bool SplineClosed { get; } + bool SplinePlanar { get; } + + void ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions); + void RebuildSplineMeshes(); + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs.meta new file mode 100644 index 000000000..aa5b1b260 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/DragPoint/IDragPointSplineOwner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a877333d51388f2468ee7a8bbf23c062 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs index 536bb0fa9..4e3743944 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Flipper/FlipperComponent.cs @@ -648,10 +648,10 @@ private void SetupFlipperCorrection(FlipperColliderComponent colliderComponent) triggerCollider.TimeThresholdMs = fc.TimeThresholdMs; var localPos = transform.localPosition.TranslateToVpx(); - triggerComponent.Position = new Vector2(localPos.x, localPos.y); - - var poly = GetEnclosingPolygon(23, 12); - triggerComponent.DragPoints = new DragPointData[poly.Count]; + triggerComponent.Position = new Vector2(localPos.x, localPos.y); + + var poly = GetEnclosingPolygon(23, 12); + var dragPoints = new DragPointData[poly.Count]; triggerCollider.HitHeight = 150F; // nFozzy's recommendation, but I think 50 should be ok // this was taken from the 2021 code when we had the playfield transformed to vpx world: @@ -674,8 +674,9 @@ private void SetupFlipperCorrection(FlipperColliderComponent colliderComponent) for (var i = 0; i < poly.Count; i++) { // Poly points are expressed in flipper's frame: rotate to get it to the correct position. var p = flipperToPlayfield.MultiplyPoint3x4(poly[i]); - triggerComponent.DragPoints[poly.Count - i - 1] = new DragPointData(p.x, p.y); - } + dragPoints[poly.Count - i - 1] = new DragPointData(p.x, p.y); + } + triggerComponent.DragPoints = dragPoints; // create polarities and velocities curves var polarities = new float2[fc.PolaritiesCurveSlicingCount + 1]; diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightInsertMeshComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightInsertMeshComponent.cs index 692d93e78..1a99886a1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightInsertMeshComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Light/LightInsertMeshComponent.cs @@ -16,7 +16,11 @@ // ReSharper disable InconsistentNaming +using System; +using System.Collections.Generic; +using Unity.Mathematics; using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Light; @@ -26,7 +30,8 @@ namespace VisualPinball.Unity { - public class LightInsertMeshComponent : MeshComponent + public class LightInsertMeshComponent : MeshComponent, + IDragPointSplineOwner { #region Data @@ -34,16 +39,31 @@ public class LightInsertMeshComponent : MeshComponent public float PositionZ = 0.1f; - [SerializeField] + [SerializeField, HideInInspector] private DragPointData[] _dragPoints; - public DragPointData[] DragPoints { get => _dragPoints; set => _dragPoints = value; } + + [SerializeField] + private DragPointSplineComponent _dragPointSpline; + + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); #endregion protected override Mesh GetMesh(LightData data) { var playfieldComponent = GetComponentInParent(); - var meshGen = new SurfaceMeshGenerator(new LightInsertData(_dragPoints, InsertHeight), MainComponent.transform.position.TranslateToVpx().ToVertex3D()); + var meshGen = new SurfaceMeshGenerator(new LightInsertData(DragPoints, InsertHeight), MainComponent.transform.position.TranslateToVpx().ToVertex3D()); var topMesh = meshGen.GetMesh(SurfaceMeshGenerator.Top, playfieldComponent.Width, playfieldComponent.Height, 0, false); var sideMesh = meshGen.GetMesh(SurfaceMeshGenerator.Side, playfieldComponent.Width, playfieldComponent.Height, 0, false); return topMesh.Merge(sideMesh).TransformToWorld(); @@ -60,5 +80,22 @@ protected override PbrMaterial GetMaterial(LightData data, Table table) mat = new Engine.VPT.Material("Playfield Insert"); return new PbrMaterial(mat, table.GetTexture(table.Data.Image)) { DiffusionProfile = DiffusionProfileTemplate.Plastics }; } + + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => MainComponent.transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => true; + bool IDragPointSplineOwner.SplinePlanar => true; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) { } + void IDragPointSplineOwner.RebuildSplineMeshes() => MainComponent.RebuildMeshes(); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/MetalWireGuide/MetalWireGuideComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/MetalWireGuide/MetalWireGuideComponent.cs index 824e2fac5..6a5a9888a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/MetalWireGuide/MetalWireGuideComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/MetalWireGuide/MetalWireGuideComponent.cs @@ -24,7 +24,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Unity.Mathematics; using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.MetalWireGuide; @@ -36,7 +38,7 @@ namespace VisualPinball.Unity [PackAs("MetalWireGuide")] [AddComponentMenu("Pinball/Game Item/Metal Wire Guide")] public class MetalWireGuideComponent : MainRenderableComponent, - IMetalWireGuideData, IPackable + IMetalWireGuideData, IPackable, IDragPointSplineOwner { #region Data @@ -57,9 +59,12 @@ public class MetalWireGuideComponent : MainRenderableComponent _dragPoints; set => _dragPoints = value; } + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); public float Thickness => _thickness; public float Standheight => _standheight; public float Height => _height; @@ -217,10 +233,27 @@ public override void CopyFromObject(GameObject go) _standheight = srcMainComp._standheight; Rotation = srcMainComp.Rotation; _bendradius = srcMainComp._bendradius; - _dragPoints = srcMainComp._dragPoints.Select(dp => dp.Clone()).ToArray(); + DragPoints = srcMainComp.DragPoints.Select(dp => dp.Clone()).ToArray(); } } + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => false; + bool IDragPointSplineOwner.SplinePlanar => true; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) { } + void IDragPointSplineOwner.RebuildSplineMeshes() => RebuildMeshes(); + #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampComponent.cs index e8ad6497b..3209ff83e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Ramp/RampComponent.cs @@ -22,9 +22,11 @@ #endregion using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Ramp; @@ -37,7 +39,8 @@ namespace VisualPinball.Unity [SelectionBase] [PackAs("Ramp")] [AddComponentMenu("Pinball/Game Item/Ramp")] - public class RampComponent : MainRenderableComponent, IRampData, ISurfaceComponent, IPackable + public class RampComponent : MainRenderableComponent, IRampData, ISurfaceComponent, IPackable, + IDragPointSplineOwner { #region Data @@ -81,8 +84,11 @@ public class RampComponent : MainRenderableComponent, IRampData, ISurf [Tooltip("Vertical distance between the wires.")] public float _wireDistanceY = 88f; - [SerializeField] - private DragPointData[] _dragPoints; + [SerializeField, HideInInspector] + private DragPointData[] _dragPoints; + + [SerializeField] + private DragPointSplineComponent _dragPointSpline; [SerializeField] public Vector3 uvOffset = Vector3.zero; @@ -102,7 +108,18 @@ public class RampComponent : MainRenderableComponent, IRampData, ISurf public float WireDiameter => _wireDiameter; public float WidthTop => _widthTop; public float WidthBottom => _widthBottom; - public DragPointData[] DragPoints { get => _dragPoints; set => _dragPoints = value; } + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); #endregion @@ -408,27 +425,63 @@ public override void CopyFromObject(GameObject go) _wireDiameter = srcMainComp._wireDiameter; _wireDistanceX = srcMainComp._wireDistanceX; _wireDistanceY = srcMainComp._wireDistanceY; - _dragPoints = srcMainComp._dragPoints.Select(dp => dp.Clone()).ToArray(); + DragPoints = srcMainComp.DragPoints.Select(dp => dp.Clone()).ToArray(); } RebuildMeshes(); } private void CenterPivot() { - var centerVpx = DragPoints.Aggregate(Vector3.zero, (current, dragPoint) => current + dragPoint.Center.ToUnityVector3()); - centerVpx /= DragPoints.Length; + var dragPoints = DragPoints; + var centerVpx = dragPoints.Aggregate(Vector3.zero, (current, dragPoint) => current + dragPoint.Center.ToUnityVector3()); + centerVpx /= dragPoints.Length; if (uvOffset == Vector3.zero) { uvOffset = centerVpx; } transform.Translate(centerVpx.TranslateToWorld(transform) - transform.position); - foreach (var dragPoint in DragPoints) { - dragPoint.Center -= centerVpx.ToVertex3D(); - } + foreach (var dragPoint in dragPoints) { + dragPoint.Center -= centerVpx.ToVertex3D(); + } + DragPoints = dragPoints; RebuildMeshes(); } + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => false; + bool IDragPointSplineOwner.SplinePlanar => false; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) + { + if (modification != SplineModification.KnotModified || knotIndex < 0 + || previousPositions.Count != spline.Count + || knotIndex != 0 && knotIndex != spline.Count - 1) { + return; + } + + var knot = spline[knotIndex]; + var delta = knot.Position.z - previousPositions[knotIndex].z; + if (knotIndex == 0) { + _heightBottom += delta; + } else { + _heightTop += delta; + } + knot.Position.z = previousPositions[knotIndex].z; + spline.SetKnotNoNotify(knotIndex, knot); + } + void IDragPointSplineOwner.RebuildSplineMeshes() => RebuildMeshes(); + internal static (float rightWallHeight, float leftWallHeight) GetCollisionWallHeights(int type, float flatRightWallHeight, float flatLeftWallHeight) { switch (type) { diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberComponent.cs index 52ded96fa..4c56467f3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Rubber/RubberComponent.cs @@ -26,6 +26,7 @@ using System.Linq; using Unity.Mathematics; using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Rubber; @@ -36,7 +37,8 @@ namespace VisualPinball.Unity [SelectionBase] [PackAs("Rubber")] [AddComponentMenu("Pinball/Game Item/Rubber")] - public class RubberComponent : MainRenderableComponent, IRubberData, IPackable + public class RubberComponent : MainRenderableComponent, IRubberData, IPackable, + IDragPointSplineOwner { #region Data @@ -50,9 +52,12 @@ public float Height { [Tooltip("How thick the rubber band is rendered.")] public int _thickness = 8; - [SerializeField] + [SerializeField, HideInInspector] private DragPointData[] _dragPoints; + [SerializeField] + private DragPointSplineComponent _dragPointSpline; + [NonSerialized] private Vertex3D[] _scalingDragPoints; @@ -60,7 +65,18 @@ public float Height { #region IRubberData - public DragPointData[] DragPoints { get => _dragPoints; set => _dragPoints = value; } + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); public int Thickness => math.max(1, _thickness); // don't allow zero thickness #endregion @@ -215,11 +231,28 @@ public override void CopyFromObject(GameObject go) var srcMainComp = go.GetComponent(); if (srcMainComp) { _thickness = srcMainComp._thickness; - _dragPoints = srcMainComp._dragPoints.Select(dp => dp.Clone()).ToArray(); + DragPoints = srcMainComp.DragPoints.Select(dp => dp.Clone()).ToArray(); } RebuildMeshes(); } + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => true; + bool IDragPointSplineOwner.SplinePlanar => true; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) { } + void IDragPointSplineOwner.RebuildSplineMeshes() => RebuildMeshes(); + #endregion } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceComponent.cs index 505fbfeb9..b747c2d48 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Surface/SurfaceComponent.cs @@ -24,7 +24,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Unity.Mathematics; using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; using VisualPinball.Engine.VPT.Surface; @@ -35,7 +37,8 @@ namespace VisualPinball.Unity [SelectionBase] [PackAs("Surface")] [AddComponentMenu("Pinball/Game Item/Surface")] - public class SurfaceComponent : MainRenderableComponent, ISurfaceComponent, IPackable + public class SurfaceComponent : MainRenderableComponent, ISurfaceComponent, IPackable, + IDragPointSplineOwner { #region Data @@ -45,9 +48,24 @@ public class SurfaceComponent : MainRenderableComponent, ISurfaceCo [Tooltip("Bottom height of the wall, i.e. at which height the wall starts.")] public float HeightBottom; - [SerializeField] + [SerializeField, HideInInspector] private DragPointData[] _dragPoints; - public DragPointData[] DragPoints { get => _dragPoints; set => _dragPoints = value; } + + [SerializeField] + private DragPointSplineComponent _dragPointSpline; + + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); [SerializeField] public Vector3 uvOffset = Vector3.zero; @@ -220,7 +238,7 @@ public override void CopyFromObject(GameObject go) if (surfaceComponent) { HeightTop = surfaceComponent.HeightTop; HeightBottom = surfaceComponent.HeightBottom; - _dragPoints = surfaceComponent._dragPoints.Select(dp => dp.Clone()).ToArray(); + DragPoints = surfaceComponent.DragPoints.Select(dp => dp.Clone()).ToArray(); } RebuildMeshes(); @@ -235,20 +253,39 @@ private void CenterPivot() // - changing bottom height will just change the height // - change mesh and collider creation to create top-down instead of bottom-up. - var centerVpx = DragPoints.Aggregate(Vector3.zero, (current, dragPoint) => current + dragPoint.Center.ToUnityVector3()); - centerVpx /= DragPoints.Length; + var dragPoints = DragPoints; + var centerVpx = dragPoints.Aggregate(Vector3.zero, (current, dragPoint) => current + dragPoint.Center.ToUnityVector3()); + centerVpx /= dragPoints.Length; if (uvOffset == Vector3.zero) { uvOffset = centerVpx; } transform.Translate(centerVpx.TranslateToWorld(transform) - transform.position); - foreach (var dragPoint in DragPoints) { + foreach (var dragPoint in dragPoints) { dragPoint.Center -= centerVpx.ToVertex3D(); } + DragPoints = dragPoints; RebuildMeshes(); } + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => true; + bool IDragPointSplineOwner.SplinePlanar => true; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) { } + void IDragPointSplineOwner.RebuildSplineMeshes() => RebuildMeshes(); + #endregion #region State diff --git a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs index 27856786b..37b1a1738 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/VPT/Trigger/TriggerComponent.cs @@ -27,6 +27,7 @@ using Unity.Collections; using Unity.Mathematics; using UnityEngine; +using UnityEngine.Splines; using VisualPinball.Engine.Game.Engines; using VisualPinball.Engine.Math; using VisualPinball.Engine.VPT; @@ -39,7 +40,7 @@ namespace VisualPinball.Unity [PackAs("Trigger")] [AddComponentMenu("Pinball/Game Item/Trigger")] public class TriggerComponent : MainRenderableComponent, - ITriggerComponent, IAnimationValueEmitter, IPackable + ITriggerComponent, IAnimationValueEmitter, IPackable, IDragPointSplineOwner { #region Data @@ -69,9 +70,24 @@ public float Rotation { set => transform.SetLocalYRotation(math.radians(value)); } - [SerializeField] + [SerializeField, HideInInspector] private DragPointData[] _dragPoints = { new(-50f, -50f), new(-50f, 50f), new(50f, 50f), new(50f, -50f) }; - public DragPointData[] DragPoints { get => _dragPoints; set => _dragPoints = value; } + + [SerializeField] + private DragPointSplineComponent _dragPointSpline; + + public DragPointData[] DragPoints { + get => GetOrCreateDragPointSpline().DragPoints; + set { + if (!_dragPointSpline) { + _dragPoints = value; + GetOrCreateDragPointSpline(); + } else { + _dragPointSpline.SetDragPoints(value); + } + } + } + public DragPointSplineComponent DragPointSpline => GetOrCreateDragPointSpline(); #endregion @@ -237,11 +253,28 @@ public override void CopyFromObject(GameObject go) { var triggerComponent = go.GetComponent(); if (triggerComponent) { - _dragPoints = triggerComponent._dragPoints.Select(dp => dp.Clone()).ToArray(); + DragPoints = triggerComponent.DragPoints.Select(dp => dp.Clone()).ToArray(); } RebuildMeshes(); } + private DragPointSplineComponent GetOrCreateDragPointSpline() + { + _dragPointSpline = DragPointSplineComponent.GetOrCreate(this, + _dragPointSpline, _dragPoints); + _dragPoints = null; + return _dragPointSpline; + } + + MonoBehaviour IDragPointSplineOwner.SplineOwner => this; + Transform IDragPointSplineOwner.SplineTransform => transform; + DragPointSplineComponent IDragPointSplineOwner.SplineComponent => DragPointSpline; + bool IDragPointSplineOwner.SplineClosed => true; + bool IDragPointSplineOwner.SplinePlanar => true; + void IDragPointSplineOwner.ApplySplineConstraints(Spline spline, int knotIndex, + SplineModification modification, IReadOnlyList previousPositions) { } + void IDragPointSplineOwner.RebuildSplineMeshes() => RebuildMeshes(); + #endregion #region State diff --git a/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef b/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef index f0edebb73..768b51a73 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef +++ b/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef @@ -7,6 +7,7 @@ "Unity.Burst", "Unity.Collections", "Unity.Mathematics", + "Unity.Splines", "Unity.InputSystem", "VisualPinball.Engine", "com.bartofzo.nativetrees", diff --git a/package.json b/package.json index a6e127e5d..d93a1c8f2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "com.unity.modules.unitywebrequest": "1.0.0", "com.unity.modules.unitywebrequestaudio": "1.0.0", "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.unity.splines": "2.8.2", "com.unity.ugui": "2.5.0", "com.unity.test-framework": "1.7.0" },