diff --git a/Sledge.BspEditor/Providers/ObjProvider.cs b/Sledge.BspEditor/Providers/ObjBspSourceProvider.cs similarity index 50% rename from Sledge.BspEditor/Providers/ObjProvider.cs rename to Sledge.BspEditor/Providers/ObjBspSourceProvider.cs index d1ec44019..fe03d9bb6 100644 --- a/Sledge.BspEditor/Providers/ObjProvider.cs +++ b/Sledge.BspEditor/Providers/ObjBspSourceProvider.cs @@ -1,71 +1,74 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Composition; using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; +using Sledge.BspEditor.Environment; +using Sledge.BspEditor.Primitives; +using Sledge.BspEditor.Primitives.MapObjectData; +using Sledge.BspEditor.Primitives.MapObjects; using Sledge.Common; +using Sledge.Common.Shell.Documents; using Sledge.DataStructures.Geometric; -using Sledge.DataStructures.MapObjects; +using Plane = Sledge.DataStructures.Geometric.Plane; -namespace Sledge.Providers.Map +namespace Sledge.BspEditor.Providers { - // Parts of this code is borrowed from the Helix Toolkit: http://helixtoolkit.codeplex.com, license: MIT - // http://www.martinreddy.net/gfx/3d/OBJ.spec - /// - /// OBJ is a model format and does not have the intersecting planes limitation. - /// Detection of a convex solid is performed and on the off chance it is okay a solid is generated, - /// but in most cases the loader must fall back to tetrehedrons. - /// OBJ also embeds textures which doesn't work the the standard resource model of a Sledge map:- - /// all texture data is ignored and untextured faces are generated. - /// - public class ObjProvider : MapProvider + [Export(typeof(IBspSourceProvider))] + public class ObjBspSourceProvider : IBspSourceProvider { - protected override IEnumerable GetFormatFeatures() + private static readonly IEnumerable SupportedTypes = new List { - return new[] - { - MapFeature.Solids - }; - } + // Sledge only supports solids in the OBJ format + typeof(Solid), + }; + + public IEnumerable SupportedDataTypes => SupportedTypes; - protected override bool IsValidForFileName(string filename) + public IEnumerable SupportedFileExtensions { get; } = new[] { - return filename.EndsWith(".obj"); - } + new FileExtensionInfo("Wavefront model format", ".obj") + }; - private string CleanLine(string line) + public Task Load(Stream stream, IEnvironment environment) { - if (line == null) return null; - return line.StartsWith("#") ? "" : line.Trim(); + return Task.Run(() => + { + using (var reader = new StreamReader(stream, Encoding.ASCII, true, 1024, false)) + { + var result = new BspFileLoadResult(); + + var map = new Map(); + + Read(map, reader); + + result.Map = map; + return result; + } + }); } - protected override DataStructures.MapObjects.Map GetFromStream(Stream stream) + public Task Save(Stream stream, Map map) { - using (var reader = new StreamReader(stream)) + return Task.Run(() => { - var map = new DataStructures.MapObjects.Map(); - Read(map, reader); - /* - var allentities = ReadAllEntities(reader, map.IDGenerator); - var worldspawn = allentities.FirstOrDefault(x => x.EntityData.Name == "worldspawn") - ?? new Entity(0) { EntityData = { Name = "worldspawn" } }; - allentities.Remove(worldspawn); - map.WorldSpawn.EntityData = worldspawn.EntityData; - allentities.ForEach(x => x.SetParent(map.WorldSpawn)); - foreach (var obj in worldspawn.Children.ToArray()) + using (var writer = new StreamWriter(stream, Encoding.ASCII, 1024, true)) { - obj.SetParent(map.WorldSpawn); + Write(map, writer); } - map.WorldSpawn.UpdateBoundingBox(false); - */ - return map; - } + }); } + #region Reading + private struct ObjFace { - public string Group { get; set; } - public List Vertices { get; set; } + public string Group { get; } + public List Vertices { get; } public ObjFace(string group, IEnumerable vertices) : this() { @@ -74,12 +77,20 @@ public ObjFace(string group, IEnumerable vertices) : this() } } - private void Read(DataStructures.MapObjects.Map map, StreamReader reader) + private string CleanLine(string line) + { + if (line == null) return null; + return line.StartsWith("#") ? "" : line.Trim(); + } + + private void Read(Map map, StreamReader reader) { - var points = new List(); + const NumberStyles ns = NumberStyles.Float; + + var points = new List(); var faces = new List(); var currentGroup = "default"; - var scale = 100m; + var scale = 100f; string line; while ((line = reader.ReadLine()) != null) @@ -87,16 +98,14 @@ private void Read(DataStructures.MapObjects.Map map, StreamReader reader) if (line.StartsWith("# Scale: ")) { var num = line.Substring(9); - decimal s; - if (decimal.TryParse(num, NumberStyles.Float, CultureInfo.InvariantCulture, out s)) + if (float.TryParse(num, NumberStyles.Float, CultureInfo.InvariantCulture, out var s)) { scale = s; } } line = CleanLine(line); - string keyword, values; - SplitLine(line, out keyword, out values); + SplitLine(line, out var keyword, out var values); if (String.IsNullOrWhiteSpace(keyword)) continue; var vals = (values ?? "").Split(' ').Where(x => !String.IsNullOrWhiteSpace(x)).ToArray(); @@ -104,7 +113,8 @@ private void Read(DataStructures.MapObjects.Map map, StreamReader reader) { // Things I care about case "v": // geometric vertices - points.Add(Coordinate.Parse(vals[0], vals[1], vals[2]) * scale); + var vec = NumericsExtensions.Parse(vals[0], vals[1], vals[2], ns, CultureInfo.InvariantCulture); + points.Add(vec * scale); break; case "f": // face faces.Add(new ObjFace(currentGroup, vals.Select(x => ParseFaceIndex(points, x)))); @@ -180,7 +190,7 @@ private void Read(DataStructures.MapObjects.Map map, StreamReader reader) // not relevant break; - #endregion + #endregion } } @@ -196,22 +206,23 @@ private void Read(DataStructures.MapObjects.Map map, StreamReader reader) { foreach (var face in solid.Faces) { - face.Colour = solid.Colour; - face.AlignTextureToFace(); + face.Texture.AlignToNormal(face.Plane.Normal); } - solid.SetParent(map.WorldSpawn); + + solid.Hierarchy.Parent = map.Root; } + + map.Root.DescendantsChanged(); } - private IEnumerable CreateSolids(DataStructures.MapObjects.Map map, List points, IEnumerable objFaces) + private IEnumerable CreateSolids(Map map, List points, IEnumerable objFaces) { var faces = objFaces.Select(x => CreateFace(map, points, x)).ToList(); // See if the solid is valid - var solid = new Solid(map.IDGenerator.GetNextObjectID()); - solid.Colour = Colour.GetRandomBrushColour(); - solid.Faces.AddRange(faces); - faces.ForEach(x => x.Parent = solid); + var solid = new Solid(map.NumberGenerator.Next("MapObject")); + solid.Data.Add(new ObjectColor(Colour.GetRandomBrushColour())); + solid.Data.AddRange(faces); if (solid.IsValid()) { // Do an additional check to ensure that all edges are shared @@ -227,16 +238,17 @@ private IEnumerable CreateSolids(DataStructures.MapObjects.Map map, List< // Not a valid solid, decompose into tetrahedrons/etc foreach (var face in faces) { - var polygon = new Polygon(face.Vertices.Select(x => x.Location)); + var polygon = face.ToPolygon(); if (!polygon.IsValid() || !polygon.IsConvex()) { // tetrahedrons - foreach (var triangle in face.GetTriangles()) + foreach (var triangle in GetTriangles(face)) { - var tf = new Face(map.IDGenerator.GetNextFaceID()); - tf.Plane = new Plane(triangle[0].Location, triangle[1].Location, triangle[2].Location); - tf.Vertices.AddRange(triangle.Select(x => new Vertex(x.Location, tf))); - tf.UpdateBoundingBox(); + var tf = new Face(map.NumberGenerator.Next("Face")) + { + Plane = new Plane(triangle[0], triangle[1], triangle[2]) + }; + tf.Vertices.AddRange(triangle); yield return SolidifyFace(map, tf); } } @@ -248,44 +260,65 @@ private IEnumerable CreateSolids(DataStructures.MapObjects.Map map, List< } } - private Solid SolidifyFace(DataStructures.MapObjects.Map map, Face face) + private IEnumerable GetTriangles(Face face) { - var solid = new Solid(map.IDGenerator.GetNextObjectID()); - solid.Colour = Colour.GetRandomBrushColour(); - solid.Faces.Add(face); - face.Parent = solid; - var center = face.Vertices.Aggregate(Coordinate.Zero, (sum, v) => sum + v.Location) / face.Vertices.Count; + for (var i = 1; i < face.Vertices.Count - 1; i++) + { + yield return new[] + { + face.Vertices[0], + face.Vertices[i], + face.Vertices[i + 1] + }; + } + } + + private Solid SolidifyFace(Map map, Face face) + { + var solid = new Solid(map.NumberGenerator.Next("MapObject")); + solid.Data.Add(new ObjectColor(Colour.GetRandomBrushColour())); + solid.Data.Add(face); + + var center = face.Vertices.Aggregate(Vector3.Zero, (sum, v) => sum + v) / face.Vertices.Count; var offset = center - face.Plane.Normal * 5; for (var i = 0; i < face.Vertices.Count; i++) { var v1 = face.Vertices[i]; var v2 = face.Vertices[(i + 1) % face.Vertices.Count]; - var f = new Face(map.IDGenerator.GetNextFaceID()); - f.Parent = solid; - f.Plane = new Plane(v1.Location, offset, v2.Location); - f.Parent = solid; - f.Vertices.Add(new Vertex(offset, f)); - f.Vertices.Add(new Vertex(v2.Location, f)); - f.Vertices.Add(new Vertex(v1.Location, f)); - f.UpdateBoundingBox(); - - solid.Faces.Add(f); + + var f = new Face(map.NumberGenerator.Next("Face")) + { + Plane = new Plane(v1, offset, v2) + }; + + f.Vertices.Add(offset); + f.Vertices.Add(v2); + f.Vertices.Add(v1); + + solid.Data.Add(f); } + + solid.DescendantsChanged(); return solid; } - private Face CreateFace(DataStructures.MapObjects.Map map, List points, ObjFace objFace) + private Face CreateFace(Map map, List points, ObjFace objFace) { var verts = objFace.Vertices.Select(x => points[x]).ToList(); - var f = new Face(map.IDGenerator.GetNextFaceID()); - f.Plane = new Plane(verts[2], verts[1], verts[0]); - f.Vertices.AddRange(verts.Select(x => new Vertex(x, f)).Reverse()); - f.UpdateBoundingBox(); + + var f = new Face(map.NumberGenerator.Next("Face")) + { + Plane = new Plane(verts[2], verts[1], verts[0]) + }; + + verts.Reverse(); + f.Vertices.AddRange(verts); + return f; } - private int ParseFaceIndex(List list, string index) + private int ParseFaceIndex(List list, string index) { if (index.Contains('/')) index = index.Substring(0, index.IndexOf('/')); var idx = int.Parse(index); @@ -316,86 +349,47 @@ private static void SplitLine(string line, out string keyword, out string argume arguments = line.Substring(idx + 1); } - public static IEnumerable SplitValues(string values) - { - return values.Split(' ', '\t').Where(x => !String.IsNullOrWhiteSpace(x)); - } + #endregion + + #region Writing - protected override void SaveToStream(Stream stream, DataStructures.MapObjects.Map map) + private void Write(Map map, StreamWriter writer) { - /* - // Csg version - - var csg = new CsgSolid(); - foreach (var mo in map.WorldSpawn.Find(x => x is Solid).OfType()) - { - csg = csg.Union(new CsgSolid(mo.Faces.Select(x => new Polygon(x.Vertices.Select(v => v.Location))))); - } + writer.WriteLine("# Sledge Object Export"); + writer.WriteLine("# Scale: 1"); + writer.WriteLine(); - using (var sw = new StreamWriter(stream)) + foreach (var solid in map.Root.Find(x => x is Solid).OfType()) { - foreach (var polygon in csg.Polygons) + writer.Write("g solid_"); + writer.Write(solid.ID); + writer.WriteLine(); + + foreach (var face in solid.Faces) { - foreach (var v in polygon.Vertices) + foreach (var v in face.Vertices) { - sw.Write("v "); - sw.Write(v.X.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.Write(' '); - sw.Write(v.Y.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.Write(' '); - sw.Write(v.Z.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.WriteLine(); + writer.Write("v "); + writer.Write(v.X.ToString("0.0000", CultureInfo.InvariantCulture)); + writer.Write(' '); + writer.Write(v.Y.ToString("0.0000", CultureInfo.InvariantCulture)); + writer.Write(' '); + writer.Write(v.Z.ToString("0.0000", CultureInfo.InvariantCulture)); + writer.WriteLine(); } - - sw.Write("f "); - for (int i = polygon.Vertices.Count; i > 0; i--) - { - sw.Write(-i); - sw.Write(' '); - } - sw.WriteLine(); - sw.WriteLine(); - } - } - */ - // Semi-recoverable version - using (var sw = new StreamWriter(stream)) - { - sw.WriteLine("# Sledge Object Export"); - sw.WriteLine("# Scale: 1"); - sw.WriteLine(); - - foreach (var solid in map.WorldSpawn.Find(x => x is Solid).OfType()) - { - sw.Write("g solid_"); - sw.Write(solid.ID); - sw.WriteLine(); - - foreach (var face in solid.Faces) + writer.Write("f "); + for (var i = 1; i <= face.Vertices.Count; i++) { - foreach (var v in face.Vertices) - { - sw.Write("v "); - sw.Write(v.Location.X.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.Write(' '); - sw.Write(v.Location.Y.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.Write(' '); - sw.Write(v.Location.Z.ToString("0.0000", CultureInfo.InvariantCulture)); - sw.WriteLine(); - } - - sw.Write("f "); - for (var i = 1; i <= face.Vertices.Count; i++) - { - sw.Write(-i); - sw.Write(' '); - } - sw.WriteLine(); - sw.WriteLine(); + writer.Write(-i); + writer.Write(' '); } + writer.WriteLine(); + writer.WriteLine(); } } } + + #endregion } } diff --git a/Sledge.BspEditor/Sledge.BspEditor.csproj b/Sledge.BspEditor/Sledge.BspEditor.csproj index f3bb90c2d..b9c9963cb 100644 --- a/Sledge.BspEditor/Sledge.BspEditor.csproj +++ b/Sledge.BspEditor/Sledge.BspEditor.csproj @@ -245,6 +245,7 @@ Resources.resx +