Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion VisualPinball.Engine.Test/Test/Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,21 @@ public static class PathHelper
{
public static string GetFixturePath(string filename)
{
return Path.GetFullPath(Path.Combine(GetTestPath(), "Fixtures~", filename));
var expectedPath = Path.GetFullPath(Path.Combine(GetTestPath(), "Fixtures~", filename));
if (File.Exists(expectedPath)) {
return expectedPath;
}

var searchDir = new DirectoryInfo(Path.GetDirectoryName(new System.Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath));
while (searchDir != null) {
var fixturePath = Path.Combine(searchDir.FullName, "VisualPinball.Engine.Test", "Fixtures~", filename);
if (File.Exists(fixturePath)) {
return fixturePath;
}
searchDir = searchDir.Parent;
}

return expectedPath;
}

private static string GetTestPath()
Expand Down
129 changes: 129 additions & 0 deletions VisualPinball.Engine.Test/VPT/Plunger/PlungerMeshTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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 <https://www.gnu.org/licenses/>.

using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using VisualPinball.Engine.VPT;
using VisualPinball.Engine.VPT.Plunger;

namespace VisualPinball.Engine.Test.VPT.Plunger
{
public class PlungerMeshTests
{
[Test]
public void ShouldCloseModernRodTip()
{
var mesh = new PlungerMeshGenerator(new PlungerData()).GetMesh(0.0f, PlungerMeshGenerator.Rod);
var maxZ = mesh.Vertices.Max(v => v.Z);

FindBoundaryEdges(mesh)
.Where(e => System.Math.Abs(mesh.Vertices[e.a].Z - maxZ) < 0.0001f && System.Math.Abs(mesh.Vertices[e.b].Z - maxZ) < 0.0001f)
.Should().BeEmpty();
}

[Test]
public void ShouldKeepCustomRodTipCapCoplanarWithOffsetFirstTipPoint()
{
var data = new PlungerData {
Type = PlungerType.PlungerTypeCustom,
TipShape = "5 .34; 10 .5"
};

var mesh = new PlungerMeshGenerator(data).GetMesh(0.0f, PlungerMeshGenerator.Rod);
var maxZ = mesh.Vertices.Max(v => v.Z);

mesh.Vertices.Count(v => System.Math.Abs(v.Z - maxZ) < 0.0001f).Should().BeGreaterThan(1);
}

[Test]
public void ShouldBuildEmptySpringForZeroLoops()
{
var data = new PlungerData {
Type = PlungerType.PlungerTypeCustom,
SpringLoops = 0.0f,
SpringEndLoops = 0.0f
};

var mesh = new PlungerMeshGenerator(data).GetMesh(0.0f, PlungerMeshGenerator.Spring);

mesh.Vertices.Should().BeEmpty();
mesh.Indices.Should().BeEmpty();
}

[Test]
public void ShouldBuildEmptySpringForNegativeLoops()
{
var data = new PlungerData {
Type = PlungerType.PlungerTypeCustom,
SpringLoops = -1.0f,
SpringEndLoops = 0.0f
};

var mesh = new PlungerMeshGenerator(data).GetMesh(0.0f, PlungerMeshGenerator.Spring);

mesh.Vertices.Should().BeEmpty();
mesh.Indices.Should().BeEmpty();
}

[TestCase(PlungerMeshGenerator.Rod)]
[TestCase(PlungerMeshGenerator.Spring)]
[TestCase(PlungerMeshGenerator.Flat)]
public void ShouldBuildLocalMeshIndependentOfZAdjust(string meshId)
{
var baseMesh = new PlungerMeshGenerator(CreateData(meshId, 0.0f)).GetLocalMesh(meshId);
var elevatedMesh = new PlungerMeshGenerator(CreateData(meshId, 17.0f)).GetLocalMesh(meshId);

elevatedMesh.Vertices.Should().HaveCount(baseMesh.Vertices.Length);
for (var i = 0; i < baseMesh.Vertices.Length; i++) {
elevatedMesh.Vertices[i].X.Should().BeApproximately(baseMesh.Vertices[i].X, 0.0001f);
elevatedMesh.Vertices[i].Y.Should().BeApproximately(baseMesh.Vertices[i].Y, 0.0001f);
elevatedMesh.Vertices[i].Z.Should().BeApproximately(baseMesh.Vertices[i].Z, 0.0001f);
}
}

private static IEnumerable<(int a, int b)> FindBoundaryEdges(Mesh mesh)
{
var edgeCounts = new Dictionary<(int a, int b), int>();
for (var i = 0; i < mesh.Indices.Length; i += 3) {
AddEdge(mesh.Indices[i], mesh.Indices[i + 1], edgeCounts);
AddEdge(mesh.Indices[i + 1], mesh.Indices[i + 2], edgeCounts);
AddEdge(mesh.Indices[i + 2], mesh.Indices[i], edgeCounts);
}
return edgeCounts.Where(e => e.Value == 1).Select(e => e.Key);
}

private static void AddEdge(int a, int b, IDictionary<(int a, int b), int> edgeCounts)
{
var edge = a < b ? (a, b) : (b, a);
edgeCounts.TryGetValue(edge, out var count);
edgeCounts[edge] = count + 1;
}

private static PlungerData CreateData(string meshId, float zAdjust)
{
return new PlungerData {
Type = meshId switch {
PlungerMeshGenerator.Flat => PlungerType.PlungerTypeFlat,
PlungerMeshGenerator.Spring => PlungerType.PlungerTypeCustom,
_ => PlungerType.PlungerTypeModern
},
ZAdjust = zAdjust
};
}
}
}
135 changes: 86 additions & 49 deletions VisualPinball.Engine/VPT/Plunger/PlungerMeshGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,23 @@ public PlungerMeshGenerator(PlungerData data)
Init(0);
}

public Mesh GetMesh(float height, string id)
{
Init(height);
switch (id) {
case Flat:
return BuildFlatMesh();
public Mesh GetMesh(float height, string id)
{
Init(height);
return BuildMesh(id);
}

public Mesh GetLocalMesh(string id)
{
Init(0.0f, false);
return BuildMesh(id);
}

private Mesh BuildMesh(string id)
{
switch (id) {
case Flat:
return BuildFlatMesh();
case Rod:
CalculateArraySizes();
return BuildRodMesh();
Expand Down Expand Up @@ -116,8 +127,8 @@ public PbrMaterial GetMaterial(Table.Table table)
}


private void Init(float height)
{
private void Init(float height, bool includeZAdjust = true)
{
var stroke = _data.Stroke;
_beginY = 0;
_endY = -stroke;
Expand All @@ -141,10 +152,10 @@ private void Init(float height)
// figure the width in relative units (0..1) of each cell
_cellWid = 1.0f / _srcCells;

_zHeight = height + _data.ZAdjust;
_zScale = 1f;
_desc = GetPlungerDesc();
}
_zHeight = height + (includeZAdjust ? _data.ZAdjust : 0.0f);
_zScale = 1f;
_desc = GetPlungerDesc();
}

private PlungerDesc GetPlungerDesc()
{
Expand Down Expand Up @@ -175,7 +186,7 @@ private void CalculateArraySizes()
// spirals, where each spiral has 'springLoops' loops
// times 'circlePoints' vertices.
_latheVts = _lathePoints * _circlePoints;
_springVts = (int)((_springLoops + _springEndLoops) * _circlePoints) * 3;
_springVts = System.Math.Max(0, (int)((_springLoops + _springEndLoops) * _circlePoints) * 3);

// For the lathed section, we need two triangles == 6
// indices for every point on every lathe circle past
Expand All @@ -193,13 +204,13 @@ private void CalculateArraySizes()
// of sets. 12*vts/3 = 4*vts.
//
// The spring only applies to the custom plunger.
_springIndices = 0;
if (_data.Type == PlungerType.PlungerTypeCustom) {
_springIndices = 4 * _springVts - 12;
if (_springVts < 0) {
_springIndices = 0;
}
}
_springIndices = 0;
if (_data.Type == PlungerType.PlungerTypeCustom) {
_springIndices = 4 * _springVts - 12;
if (_springVts <= 3) {
_springIndices = 0;
}
}
}

/// <summary>
Expand Down Expand Up @@ -297,12 +308,12 @@ public Vertex3DNoTex2[] BuildFlatVertices(int frame)
/// cylinder.
/// </summary>
/// <returns></returns>
private Mesh BuildRodMesh()
{
var mesh = new Mesh("rod") {
Vertices = BuildRodVertices(0),
Indices = new int[_latheIndices]
};
private Mesh BuildRodMesh()
{
var mesh = new Mesh("rod") {
Vertices = BuildRodVertices(0),
Indices = new int[_latheIndices + 3 * _circlePoints]
};

// set up the vertex list for the lathe circles
var k = 0;
Expand All @@ -314,25 +325,37 @@ private Mesh BuildRodMesh()

mesh.Indices[k++] = (m + offset + 1 + _lathePoints) % _latheVts;
mesh.Indices[k++] = (m + offset + 1) % _latheVts;
mesh.Indices[k++] = (m + offset) % _latheVts;
}
}

mesh.AnimationFrames = new List<Mesh.VertData[]>(1);
mesh.AnimationDefaultPosition = DefaultPosition;
var vertices = BuildRodVertices(NumFrames);
mesh.Indices[k++] = (m + offset) % _latheVts;
}
}

// Close the front tip. The side faces connect the lathe rings but leave
// the first ring open, so add a center vertex and fan triangles.
var tipCenter = _latheVts;
for (var l = 0; l < _circlePoints; l++) {
var current = l * _lathePoints;
var next = ((l + 1) % _circlePoints) * _lathePoints;

mesh.Indices[k++] = tipCenter;
mesh.Indices[k++] = next;
mesh.Indices[k++] = current;
}

mesh.AnimationFrames = new List<Mesh.VertData[]>(1);
mesh.AnimationDefaultPosition = DefaultPosition;
var vertices = BuildRodVertices(NumFrames);
mesh.AnimationFrames.Add(vertices.Select(v => new Mesh.VertData(v.X, v.Y, v.Z, v.Nx, v.Ny, v.Nz)).ToArray());

return mesh;
}

public Vertex3DNoTex2[] BuildRodVertices(int frame)
{
if (_lathePoints == 0) {
CalculateArraySizes();
}
var vertices = new Vertex3DNoTex2[_latheVts];
var yTip = _beginY + _dyPerFrame * frame;
if (_lathePoints == 0) {
CalculateArraySizes();
}
var vertices = new Vertex3DNoTex2[_latheVts + 1];
var yTip = _beginY + _dyPerFrame * frame;

var tu = 0.51f;
var stepU = 1.0f / _circlePoints;
Expand Down Expand Up @@ -385,11 +408,22 @@ public Vertex3DNoTex2[] BuildRodVertices(int frame)
Tu = tu,
Tv = tv
};
}
}

return vertices;
}
}
}

vertices[i] = new Vertex3DNoTex2 {
X = 0.0f,
Y = ((_data.Width + _zHeight) * _zScale) * ScaleInv,
Z = -(_desc.c[0].y + yTip) * ScaleInv,
Nx = 0.0f,
Ny = 0.0f,
Nz = 1.0f,
Tu = 0.5f,
Tv = _desc.c[0].tv
};

return vertices;
}

/// <summary>
/// Build the spring.
Expand Down Expand Up @@ -476,12 +510,15 @@ private Mesh BuildSpringMesh()

public Vertex3DNoTex2[] BuildSpringVertices(int frame)
{
if (_lathePoints == 0) {
CalculateArraySizes();
}
var vertices = new Vertex3DNoTex2[_springVts];

var springGaugeRel = _springGauge / _data.Width;
if (_lathePoints == 0) {
CalculateArraySizes();
}
var vertices = new Vertex3DNoTex2[_springVts];
if (_springVts == 0) {
return vertices;
}

var springGaugeRel = _springGauge / _data.Width;

var yTip = _beginY + _dyPerFrame * frame;
ref var c = ref _desc.c[_lathePoints - 2];
Expand Down
Loading
Loading