Skip to content

Commit

Permalink
Add some basic factory classes to create known solid types
Browse files Browse the repository at this point in the history
  • Loading branch information
LogicAndTrick committed Jul 8, 2024
1 parent ca6619c commit 183c776
Show file tree
Hide file tree
Showing 17 changed files with 661 additions and 20 deletions.
47 changes: 47 additions & 0 deletions Sledge.Formats.Map/Factories/IMapObjectFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Sledge.Formats.Geometric;
using Sledge.Formats.Map.Objects;

namespace Sledge.Formats.Map.Factories
{
/// <summary>
/// A class that creates <see cref="MapObject"/> instances of any type
/// </summary>
public interface IMapObjectFactory
{
/// <summary>
/// The name of this object factory
/// </summary>
string Name { get; }

/// <summary>
/// Create the map objects from the given box
/// </summary>
/// <param name="box">The box to define the bounds of the created objects</param>
/// <returns>0 or more MapObject instances</returns>
IEnumerable<MapObject> Create(Box box);

/// <summary>
/// Get a list of properties for this factory.
/// </summary>
/// <returns>The list of properties</returns>
IEnumerable<MapObjectFactoryProperty> GetProperties();

/// <summary>
/// Get a property value.
/// </summary>
/// <param name="name">The name of the property</param>
/// <exception cref="ArgumentException">If the property of the given name doesn't exist</exception>
object GetProperty(string name);

/// <summary>
/// Set a property value.
/// </summary>
/// <param name="name">The name of the property</param>
/// <param name="value">The value of the property</param>
/// <exception cref="ArgumentException">If the property of the given name doesn't exist</exception>
/// <exception cref="InvalidCastException">If the property value could not be set due to an invalid type</exception>
void SetProperty(string name, object value);
}
}
47 changes: 47 additions & 0 deletions Sledge.Formats.Map/Factories/MapObjectFactoryBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Sledge.Formats.Geometric;
using Sledge.Formats.Map.Objects;

namespace Sledge.Formats.Map.Factories
{
/// <summary>
/// Abstract base class that provides automatic property get/set information using reflection.
/// </summary>
public abstract class MapObjectFactoryBase : IMapObjectFactory
{
private readonly Lazy<Dictionary<string, ReflectionMapObjectFactoryProperty>> _properties;

protected MapObjectFactoryBase()
{
_properties = new Lazy<Dictionary<string, ReflectionMapObjectFactoryProperty>>(() => GetType().GetProperties().Where(x => x.CanRead && x.CanWrite).ToDictionary(x => x.Name, x => new ReflectionMapObjectFactoryProperty(x)));
}

/// <inheritdoc cref="IMapObjectFactory.Name"/>
public abstract string Name { get; }

/// <inheritdoc cref="IMapObjectFactory.Create"/>
public abstract IEnumerable<MapObject> Create(Box box);

/// <inheritdoc cref="IMapObjectFactory.GetProperties"/>
public IEnumerable<MapObjectFactoryProperty> GetProperties()
{
return _properties.Value.Values;
}

/// <inheritdoc cref="IMapObjectFactory.GetProperty"/>
public object GetProperty(string name)
{
if (!_properties.Value.ContainsKey(name)) throw new ArgumentException($"Property with name '{name}' not found", nameof(name));
return _properties.Value[name].Property.GetValue(this);
}

/// <inheritdoc cref="IMapObjectFactory.SetProperty"/>
public void SetProperty(string name, object value)
{
if (!_properties.Value.ContainsKey(name)) throw new ArgumentException($"Property with name '{name}' not found", nameof(name));
_properties.Value[name].Property.SetValue(this, value);
}
}
}
45 changes: 45 additions & 0 deletions Sledge.Formats.Map/Factories/MapObjectFactoryProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;

namespace Sledge.Formats.Map.Factories
{
/// <summary>
/// A class describing a property of a <see cref="IMapObjectFactory"/>
/// </summary>
public class MapObjectFactoryProperty
{
/// <summary>
/// The name of the property
/// </summary>
public string Name { get; set; }

/// <summary>
/// The display name of the property
/// </summary>
public string DisplayName { get; set; }

/// <summary>
/// A description of the property
/// </summary>
public string Description { get; set; }

/// <summary>
/// The type of the property
/// </summary>
public Type Type { get; set; }

/// <summary>
/// For decimal properties, the minimum allowed value. Will default to <see cref="int.MinValue"/>.
/// </summary>
public decimal MinValue { get; set; } = int.MinValue;

/// <summary>
/// For decimal properties, the maximum allowed value. Will default to <see cref="int.MaxValue"/>.
/// </summary>
public decimal MaxValue { get; set; } = int.MaxValue;

/// <summary>
/// For decimal properties, the number of decimal places allowed.
/// </summary>
public int DecimalPrecision { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace Sledge.Formats.Map.Factories
{
[AttributeUsage(AttributeTargets.Property)]
public class MapObjectFactoryPropertyDataAttribute : System.Attribute
{
public double MinValue { get; set; } = int.MinValue;
public double MaxValue { get; set; } = int.MaxValue;
public int DecimalPrecision { get; set; } = 2;
}
}
32 changes: 32 additions & 0 deletions Sledge.Formats.Map/Factories/ReflectionMapObjectFactoryProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.ComponentModel;
using System.Reflection;

namespace Sledge.Formats.Map.Factories
{
/// <summary>
/// A <see cref="MapObjectFactoryProperty"/> with a <see cref="PropertyInfo"/> instance for get/set.
/// </summary>
public class ReflectionMapObjectFactoryProperty : MapObjectFactoryProperty
{
/// <summary>
/// The reflection property
/// </summary>
public PropertyInfo Property { get; }

public ReflectionMapObjectFactoryProperty(PropertyInfo property)
{
Property = property;
Name = property.Name;
DisplayName = property.GetCustomAttribute<DisplayNameAttribute>(true)?.DisplayName ?? property.Name;
Description = property.GetCustomAttribute<DescriptionAttribute>(true)?.Description ?? "";
Type = property.PropertyType;
var pd = property.GetCustomAttribute<MapObjectFactoryPropertyDataAttribute>(true);
if (pd != null)
{
MinValue = (decimal) pd.MinValue;
MaxValue = (decimal) pd.MaxValue;
DecimalPrecision = pd.DecimalPrecision;
}
}
}
}
35 changes: 35 additions & 0 deletions Sledge.Formats.Map/Factories/Solids/BlockSolidFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using Sledge.Formats.Geometric;
using Sledge.Formats.Map.Objects;

namespace Sledge.Formats.Map.Factories.Solids
{
/// <summary>
/// Factory that creates a six sided rectangular prism
/// </summary>
public class BlockSolidFactory : SolidFactoryBase
{
public override string Name => "Block";

public override IEnumerable<MapObject> Create(Box box)
{
var solid = new Solid
{
Color = ColorUtils.GetRandomBrushColour()
};

foreach (var polygon in box.GetBoxFaces())
{
var face = new Face
{
Plane = polygon.Plane,
TextureName = VisibleTextureName
};
face.Vertices.AddRange(polygon.Vertices.Select(x => x.Round(RoundDecimals)));
solid.Faces.Add(face);
}
yield return solid;
}
}
}
71 changes: 71 additions & 0 deletions Sledge.Formats.Map/Factories/Solids/ConeSolidFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Numerics;
using Sledge.Formats.Geometric;
using Sledge.Formats.Map.Objects;

namespace Sledge.Formats.Map.Factories.Solids
{
public class ConeSolidFactory : SolidFactoryBase
{
public override string Name => "Cone";

[DisplayName("Number of sides")]
[Description("The number of sides the base of the cone will have")]
[MapObjectFactoryPropertyData(MinValue = 3)]
public int NumberOfSides { get; set; }

public override IEnumerable<MapObject> Create(Box box)
{
var numSides = NumberOfSides;
if (numSides < 3) throw new ArgumentException("NumberOfSides must be >= 3", nameof(NumberOfSides));

// This is all very similar to the cylinder brush.
var width = box.Width;
var length = box.Length;
var major = width / 2;
var minor = length / 2;
var angle = 2 * Math.PI / numSides;

var points = new List<Vector3>();
for (var i = 0; i < numSides; i++)
{
var a = i * angle;
var xval = box.Center.X + major * (float)Math.Cos(a);
var yval = box.Center.Y + minor * (float)Math.Sin(a);
var zval = box.Start.Z;
points.Add(new Vector3(xval, yval, zval).Round(RoundDecimals));
}
points.Reverse();

var faces = new List<Vector3[]>();

var point = new Vector3(box.Center.X, box.Center.Y, box.End.Z).Round(RoundDecimals);
for (var i = 0; i < numSides; i++)
{
var next = (i + 1) % numSides;
faces.Add(new[] { points[next], points[i], point });
}
faces.Add(points.ToArray());

var solid = new Solid
{
Color = ColorUtils.GetRandomBrushColour()
};

foreach (var arr in faces)
{
var face = new Face
{
Plane = Plane.CreateFromVertices(arr[0], arr[1], arr[2]),
TextureName = VisibleTextureName
};
face.Vertices.AddRange(arr);
solid.Faces.Add(face);
}
yield return solid;
}
}
}
79 changes: 79 additions & 0 deletions Sledge.Formats.Map/Factories/Solids/CylinderSolidFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Numerics;
using Sledge.Formats.Geometric;
using Sledge.Formats.Map.Objects;

namespace Sledge.Formats.Map.Factories.Solids
{
public class CylinderSolidFactory : SolidFactoryBase
{
public override string Name => "Cone";

[DisplayName("Number of sides")]
[Description("The number of sides the base of the cylinder will have")]
[MapObjectFactoryPropertyData(MinValue = 3)]
public int NumberOfSides { get; set; }

public override IEnumerable<MapObject> Create(Box box)
{
var numSides = NumberOfSides;
if (numSides < 3) throw new ArgumentException("NumberOfSides must be >= 3", nameof(NumberOfSides));

// Cylinders can be elliptical so use both major and minor rather than just the radius
// NOTE: when a low number (< 10ish) of faces are selected this will cause the cylinder to not touch all the edges of the box.
var width = box.Width;
var length = box.Length;
var height = box.Height;
var major = width / 2;
var minor = length / 2;
var angle = 2 * (float)Math.PI / numSides;

// Calculate the X and Y points for the ellipse
var points = new List<Vector3>();
for (var i = 0; i < numSides; i++)
{
var a = i * angle;
var xval = box.Center.X + major * (float)Math.Cos(a);
var yval = box.Center.Y + minor * (float)Math.Sin(a);
var zval = box.Start.Z;
points.Add(new Vector3(xval, yval, zval).Round(RoundDecimals));
}
points.Reverse();

var faces = new List<Vector3[]>();

// Add the vertical faces
var z = new Vector3(0, 0, height).Round(RoundDecimals);
for (var i = 0; i < numSides; i++)
{
var next = (i + 1) % numSides;
faces.Add(new[] { points[i], points[i] + z, points[next] + z, points[next] });
}

// Add the elliptical top and bottom faces
faces.Add(points.ToArray());
faces.Add(points.Select(x => x + z).Reverse().ToArray());

// Nothing new here, move along
var solid = new Solid
{
Color = ColorUtils.GetRandomBrushColour()
};

foreach (var arr in faces)
{
var face = new Face
{
Plane = Plane.CreateFromVertices(arr[0], arr[1], arr[2]),
TextureName = VisibleTextureName
};
face.Vertices.AddRange(arr);
solid.Faces.Add(face);
}
yield return solid;
}
}
}
Loading

0 comments on commit 183c776

Please sign in to comment.