diff --git a/Euclid.xcodeproj/project.pbxproj b/Euclid.xcodeproj/project.pbxproj index d538d5f1..16b62374 100644 --- a/Euclid.xcodeproj/project.pbxproj +++ b/Euclid.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0101BAF525687A450096B1E7 /* CodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0101BAF425687A450096B1E7 /* CodingTests.swift */; }; 010A63392A951165000E3306 /* Mesh+OBJ.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010A63382A951165000E3306 /* Mesh+OBJ.swift */; }; + 010A633B2A955BE9000E3306 /* MeshExportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010A633A2A955BE9000E3306 /* MeshExportTests.swift */; }; 0112D5C928EE29BB00A1C085 /* Euclid+RealityKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0112D5C828EE29BB00A1C085 /* Euclid+RealityKit.swift */; }; 0125478027AFD53900C442C3 /* MeshShapeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0125477F27AFD53900C442C3 /* MeshShapeTests.swift */; }; 013312DD21CA532A00626F1B /* PlaneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013312DC21CA532A00626F1B /* PlaneTests.swift */; }; @@ -107,6 +108,7 @@ /* Begin PBXFileReference section */ 0101BAF425687A450096B1E7 /* CodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodingTests.swift; sourceTree = ""; }; 010A63382A951165000E3306 /* Mesh+OBJ.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Mesh+OBJ.swift"; sourceTree = ""; }; + 010A633A2A955BE9000E3306 /* MeshExportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshExportTests.swift; sourceTree = ""; }; 0112D5C828EE29BB00A1C085 /* Euclid+RealityKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Euclid+RealityKit.swift"; sourceTree = ""; }; 0125477F27AFD53900C442C3 /* MeshShapeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshShapeTests.swift; sourceTree = ""; }; 013312DC21CA532A00626F1B /* PlaneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaneTests.swift; sourceTree = ""; }; @@ -282,6 +284,7 @@ 52A3852D238D6E5700BE8407 /* LineTests.swift */, 01CBE2672775E3EE00B7ED45 /* MeshTests.swift */, 016FAB5F21BFE7CE00AF60DC /* MeshCSGTests.swift */, + 010A633A2A955BE9000E3306 /* MeshExportTests.swift */, 0125477F27AFD53900C442C3 /* MeshShapeTests.swift */, 013B5BE426923087000860DC /* MetadataTests.swift */, 016FAB5C21BFE7CD00AF60DC /* PathTests.swift */, @@ -560,6 +563,7 @@ 0101BAF525687A450096B1E7 /* CodingTests.swift in Sources */, 016FAB6521BFE7CE00AF60DC /* MeshCSGTests.swift in Sources */, 01BA297A2235E34C0088D36B /* CGPathTests.swift in Sources */, + 010A633B2A955BE9000E3306 /* MeshExportTests.swift in Sources */, 0125478027AFD53900C442C3 /* MeshShapeTests.swift in Sources */, 01D96AB523D8E36A00D0D267 /* BoundsTests.swift in Sources */, 013312DD21CA532A00626F1B /* PlaneTests.swift in Sources */, diff --git a/Sources/Mesh+OBJ.swift b/Sources/Mesh+OBJ.swift index e8059cf7..41fa7107 100644 --- a/Sources/Mesh+OBJ.swift +++ b/Sources/Mesh+OBJ.swift @@ -62,23 +62,12 @@ public extension Mesh { "vt \(vector.x.objString) \(vector.y.objString)\(vector.z == 0 ? "" : " \(vector.z.objString)")" } - return """ - # Vertices - \(vertices.map(vertexString).joined(separator: "\n")) - \(hasTexcoords ? """ - - # Texcoords - \(texcoords.map(textcoordString).joined(separator: "\n")) - """ : "") - \(hasVertexNormals ? """ - - # Normals - \(normals.map { "vn \($0.objString)" }.joined(separator: "\n")) - """ : "") - - # Faces - \(indices.map { "f \($0.map(vertexIndexString).joined(separator: " "))" }.joined(separator: "\n")) - """ + return [ + vertices.map(vertexString).joined(separator: "\n"), + hasTexcoords ? texcoords.map(textcoordString).joined(separator: "\n") : nil, + hasVertexNormals ? normals.map { "vn \($0.objString)" }.joined(separator: "\n") : nil, + indices.map { "f \($0.map(vertexIndexString).joined(separator: " "))" }.joined(separator: "\n"), + ].compactMap { $0 }.joined(separator: "\n\n") } } diff --git a/Tests/MeshExportTests.swift b/Tests/MeshExportTests.swift new file mode 100644 index 00000000..a86a08e1 --- /dev/null +++ b/Tests/MeshExportTests.swift @@ -0,0 +1,288 @@ +// +// MeshExportTests.swift +// EuclidTests +// +// Created by Nick Lockwood on 22/08/2023. +// Copyright © 2023 Nick Lockwood. All rights reserved. +// + +@testable import Euclid +import XCTest + +class MeshExportTests: XCTestCase { + // MARK: STL export + + func testCubeSTL() { + let cube = Mesh.cube().translated(by: Vector(0.5, 0.5, 0.5)) + let stl = cube.stlString(name: "Foo") + XCTAssertEqual(stl, """ + solid Foo + facet normal 1 0 0 + \touter loop + \t\tvertex 1 0 1 + \t\tvertex 1 0 0 + \t\tvertex 1 1 0 + \tendloop + endfacet + facet normal 1 0 0 + \touter loop + \t\tvertex 1 0 1 + \t\tvertex 1 1 0 + \t\tvertex 1 1 1 + \tendloop + endfacet + facet normal -1 0 0 + \touter loop + \t\tvertex 0 0 0 + \t\tvertex 0 0 1 + \t\tvertex 0 1 1 + \tendloop + endfacet + facet normal -1 0 0 + \touter loop + \t\tvertex 0 0 0 + \t\tvertex 0 1 1 + \t\tvertex 0 1 0 + \tendloop + endfacet + facet normal 0 1 0 + \touter loop + \t\tvertex 0 1 1 + \t\tvertex 1 1 1 + \t\tvertex 1 1 0 + \tendloop + endfacet + facet normal 0 1 0 + \touter loop + \t\tvertex 0 1 1 + \t\tvertex 1 1 0 + \t\tvertex 0 1 0 + \tendloop + endfacet + facet normal 0 -1 0 + \touter loop + \t\tvertex 0 0 0 + \t\tvertex 1 0 0 + \t\tvertex 1 0 1 + \tendloop + endfacet + facet normal 0 -1 0 + \touter loop + \t\tvertex 0 0 0 + \t\tvertex 1 0 1 + \t\tvertex 0 0 1 + \tendloop + endfacet + facet normal 0 0 1 + \touter loop + \t\tvertex 0 0 1 + \t\tvertex 1 0 1 + \t\tvertex 1 1 1 + \tendloop + endfacet + facet normal 0 0 1 + \touter loop + \t\tvertex 0 0 1 + \t\tvertex 1 1 1 + \t\tvertex 0 1 1 + \tendloop + endfacet + facet normal 0 0 -1 + \touter loop + \t\tvertex 1 0 0 + \t\tvertex 0 0 0 + \t\tvertex 0 1 0 + \tendloop + endfacet + facet normal 0 0 -1 + \touter loop + \t\tvertex 1 0 0 + \t\tvertex 0 1 0 + \t\tvertex 1 1 0 + \tendloop + endfacet + endsolid Foo + """) + } + + func testCubeSTLData() { + let cube = Mesh.cube().translated(by: Vector(0.5, 0.5, 0.5)) + let stlData = cube.stlData() + XCTAssertEqual(stlData.count, 80 + 4 + 12 * 50) + let hex = stlData.reduce(into: "") { $0 += String(format: "%02x", $1) } + XCTAssertEqual(hex, """ + 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 00000000000000000000000000000000000000000000000000c0000000000803f00000000000000000000803f000000000000803f000080\ + 3f00000000000000000000803f0000803f0000000000000000803f00000000000000000000803f000000000000803f0000803f0000803f0\ + 00000000000803f0000803f0000803f0000000080bf000000800000008000000000000000000000000000000000000000000000803f0000\ + 00000000803f0000803f0000000080bf0000008000000080000000000000000000000000000000000000803f0000803f000000000000803\ + f000000000000000000000000803f00000000000000000000803f0000803f0000803f0000803f0000803f0000803f0000803f0000000000\ + 00000000000000803f00000000000000000000803f0000803f0000803f0000803f00000000000000000000803f000000000000000000800\ + 00080bf000000800000000000000000000000000000803f00000000000000000000803f000000000000803f000000000080000080bf0000\ + 00800000000000000000000000000000803f000000000000803f00000000000000000000803f000000000000000000000000803f0000000\ + 0000000000000803f0000803f000000000000803f0000803f0000803f0000803f000000000000000000000000803f000000000000000000\ + 00803f0000803f0000803f0000803f000000000000803f0000803f00000000008000000080000080bf0000803f000000000000000000000\ + 0000000000000000000000000000000803f0000000000000000008000000080000080bf0000803f0000000000000000000000000000803f\ + 000000000000803f0000803f000000000000 + """) + } + + func testRedCubeSTLData() { + let cube = Mesh.cube(material: Color.red).translated(by: Vector(0.5, 0.5, 0.5)) + let stlData = cube.stlData() + XCTAssertEqual(stlData.count, 80 + 4 + 12 * 50) + let hex = stlData.reduce(into: "") { $0 += String(format: "%02x", $1) } + XCTAssertEqual(hex, """ + 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 00000000000000000000000000000000000000000000000000c0000000000803f00000000000000000000803f000000000000803f000080\ + 3f00000000000000000000803f0000803f0000000000fc0000803f00000000000000000000803f000000000000803f0000803f0000803f0\ + 00000000000803f0000803f0000803f00fc000080bf000000800000008000000000000000000000000000000000000000000000803f0000\ + 00000000803f0000803f00fc000080bf0000008000000080000000000000000000000000000000000000803f0000803f000000000000803\ + f0000000000fc000000000000803f00000000000000000000803f0000803f0000803f0000803f0000803f0000803f0000803f0000000000\ + fc000000000000803f00000000000000000000803f0000803f0000803f0000803f00000000000000000000803f0000000000fc000000800\ + 00080bf000000800000000000000000000000000000803f00000000000000000000803f000000000000803f00fc00000080000080bf0000\ + 00800000000000000000000000000000803f000000000000803f00000000000000000000803f00fc00000000000000000000803f0000000\ + 0000000000000803f0000803f000000000000803f0000803f0000803f0000803f00fc00000000000000000000803f000000000000000000\ + 00803f0000803f0000803f0000803f000000000000803f0000803f00fc0000008000000080000080bf0000803f000000000000000000000\ + 0000000000000000000000000000000803f0000000000fc0000008000000080000080bf0000803f0000000000000000000000000000803f\ + 000000000000803f0000803f0000000000fc + """) + } + + // MARK: OBJ export + + func testCubeOBJ() { + let cube = Mesh.cube().translated(by: Vector(0.5, 0.5, 0.5)) + let obj = cube.objString() + XCTAssertEqual(obj, """ + v 1 0 1 + v 1 0 0 + v 1 1 0 + v 1 1 1 + v 0 0 0 + v 0 0 1 + v 0 1 1 + v 0 1 0 + + vt 0 1 + vt 1 1 + vt 1 0 + vt 0 0 + + f 1/1 2/2 3/3 4/4 + f 5/1 6/2 7/3 8/4 + f 7/1 4/2 3/3 8/4 + f 5/1 2/2 1/3 6/4 + f 6/1 1/2 4/3 7/4 + f 2/1 5/2 8/3 3/4 + """) + } + + func testCylinderOBJ() { + let cylinder = Mesh.cylinder(slices: 4) + let obj = cylinder.objString() + XCTAssertEqual(obj, """ + v 0 0.5 0 + v -0.5 0.5 0 + v 0 0.5 0.5 + v -0.5 -0.5 0 + v 0 -0.5 0.5 + v 0 -0.5 0 + v 0.5 0.5 0 + v 0.5 -0.5 0 + v 0 0.5 -0.5 + v 0 -0.5 -0.5 + + vt 0.125 0 + vt 0 0 + vt 0.25 0 + vt 0 1 + vt 0.25 1 + vt 0.125 1 + vt 0.375 0 + vt 0.5 0 + vt 0.5 1 + vt 0.375 1 + vt 0.625 0 + vt 0.75 0 + vt 0.75 1 + vt 0.625 1 + vt 0.875 0 + vt 1 0 + vt 1 1 + vt 0.875 1 + + vn 0 1 0 + vn -6.12323e-17 0 1 + vn -1 0 0 + vn 0 -1 0 + vn 1 0 1.22465e-16 + vn 1.83697e-16 0 -1 + vn -1 0 -2.44929e-16 + + f 1/1/1 2/2/1 3/3/1 + f 3/3/2 2/2/3 4/4/3 5/5/2 + f 5/5/4 4/4/4 6/6/4 + f 1/7/1 3/3/1 7/8/1 + f 7/8/5 3/3/2 5/5/2 8/9/5 + f 8/9/4 5/5/4 6/10/4 + f 1/11/1 7/8/1 9/12/1 + f 9/12/6 7/8/5 8/9/5 10/13/6 + f 10/13/4 8/9/4 6/14/4 + f 1/15/1 9/12/1 2/16/1 + f 2/16/7 9/12/6 10/13/6 4/17/7 + f 4/17/4 10/13/4 6/18/4 + """) + } + + func testGradientLatheOBJ() { + let cylinder = Mesh.lathe(Path([ + .point(0, 1, color: .red), + .point(1, 0, color: .green), + .point(0, -1, color: .blue), + ]), slices: 4) + let obj = cylinder.objString() + XCTAssertEqual(obj, """ + v 0 1 0 1 0 0 + v -1 0 0 0 1 0 + v 0 0 1 0 1 0 + v 0 -1 0 0 0 1 + v 1 0 0 0 1 0 + v 0 0 -1 0 1 0 + + vt 0.125 0 + vt 0 0.5 + vt 0.25 0.5 + vt 0.125 1 + vt 0.375 0 + vt 0.5 0.5 + vt 0.375 1 + vt 0.625 0 + vt 0.75 0.5 + vt 0.625 1 + vt 0.875 0 + vt 1 0.5 + vt 0.875 1 + + vn -0.707107 0.707107 0 + vn -4.32978e-17 0.707107 0.707107 + vn -4.32978e-17 -0.707107 0.707107 + vn -0.707107 -0.707107 0 + vn 0.707107 0.707107 8.65956e-17 + vn 0.707107 -0.707107 8.65956e-17 + vn 1.29893e-16 0.707107 -0.707107 + vn 1.29893e-16 -0.707107 -0.707107 + vn -0.707107 0.707107 -1.73191e-16 + vn -0.707107 -0.707107 -1.73191e-16 + + f 1/1/1 2/2/1 3/3/2 + f 3/3/3 2/2/4 4/4/4 + f 1/5/2 3/3/2 5/6/5 + f 5/6/6 3/3/3 4/7/3 + f 1/8/5 5/6/5 6/9/7 + f 6/9/8 5/6/6 4/10/6 + f 1/11/7 6/9/7 2/12/9 + f 2/12/10 6/9/8 4/13/8 + """) + } +} diff --git a/Tests/MeshTests.swift b/Tests/MeshTests.swift index 12b7a18f..397325d0 100644 --- a/Tests/MeshTests.swift +++ b/Tests/MeshTests.swift @@ -328,121 +328,4 @@ class MeshTests: XCTestCase { XCTAssertFalse(bsp.containsPoint(point)) } } - - // MARK: export - - func testCubeSTL() { - let cube = Mesh.cube().translated(by: Vector(0.5, 0.5, 0.5)) - let stl = cube.stlString(name: "Foo") - XCTAssertEqual(stl, """ - solid Foo - facet normal 1 0 0 - \touter loop - \t\tvertex 1 0 1 - \t\tvertex 1 0 0 - \t\tvertex 1 1 0 - \tendloop - endfacet - facet normal 1 0 0 - \touter loop - \t\tvertex 1 0 1 - \t\tvertex 1 1 0 - \t\tvertex 1 1 1 - \tendloop - endfacet - facet normal -1 0 0 - \touter loop - \t\tvertex 0 0 0 - \t\tvertex 0 0 1 - \t\tvertex 0 1 1 - \tendloop - endfacet - facet normal -1 0 0 - \touter loop - \t\tvertex 0 0 0 - \t\tvertex 0 1 1 - \t\tvertex 0 1 0 - \tendloop - endfacet - facet normal 0 1 0 - \touter loop - \t\tvertex 0 1 1 - \t\tvertex 1 1 1 - \t\tvertex 1 1 0 - \tendloop - endfacet - facet normal 0 1 0 - \touter loop - \t\tvertex 0 1 1 - \t\tvertex 1 1 0 - \t\tvertex 0 1 0 - \tendloop - endfacet - facet normal 0 -1 0 - \touter loop - \t\tvertex 0 0 0 - \t\tvertex 1 0 0 - \t\tvertex 1 0 1 - \tendloop - endfacet - facet normal 0 -1 0 - \touter loop - \t\tvertex 0 0 0 - \t\tvertex 1 0 1 - \t\tvertex 0 0 1 - \tendloop - endfacet - facet normal 0 0 1 - \touter loop - \t\tvertex 0 0 1 - \t\tvertex 1 0 1 - \t\tvertex 1 1 1 - \tendloop - endfacet - facet normal 0 0 1 - \touter loop - \t\tvertex 0 0 1 - \t\tvertex 1 1 1 - \t\tvertex 0 1 1 - \tendloop - endfacet - facet normal 0 0 -1 - \touter loop - \t\tvertex 1 0 0 - \t\tvertex 0 0 0 - \t\tvertex 0 1 0 - \tendloop - endfacet - facet normal 0 0 -1 - \touter loop - \t\tvertex 1 0 0 - \t\tvertex 0 1 0 - \t\tvertex 1 1 0 - \tendloop - endfacet - endsolid Foo - """) - } - - func testCubeSTLData() { - let cube = Mesh.cube().translated(by: Vector(0.5, 0.5, 0.5)) - let stlData = cube.stlData() - XCTAssertEqual(stlData.count, 80 + 4 + 12 * 50) - let hex = stlData.reduce(into: "") { $0 += String(format: "%02x", $1) } - XCTAssertEqual(hex, """ - 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ - 00000000000000000000000000000000000000000000000000c0000000000803f00000000000000000000803f000000000000803f000080\ - 3f00000000000000000000803f0000803f0000000000000000803f00000000000000000000803f000000000000803f0000803f0000803f0\ - 00000000000803f0000803f0000803f0000000080bf000000800000008000000000000000000000000000000000000000000000803f0000\ - 00000000803f0000803f0000000080bf0000008000000080000000000000000000000000000000000000803f0000803f000000000000803\ - f000000000000000000000000803f00000000000000000000803f0000803f0000803f0000803f0000803f0000803f0000803f0000000000\ - 00000000000000803f00000000000000000000803f0000803f0000803f0000803f00000000000000000000803f000000000000000000800\ - 00080bf000000800000000000000000000000000000803f00000000000000000000803f000000000000803f000000000080000080bf0000\ - 00800000000000000000000000000000803f000000000000803f00000000000000000000803f000000000000000000000000803f0000000\ - 0000000000000803f0000803f000000000000803f0000803f0000803f0000803f000000000000000000000000803f000000000000000000\ - 00803f0000803f0000803f0000803f000000000000803f0000803f00000000008000000080000080bf0000803f000000000000000000000\ - 0000000000000000000000000000000803f0000000000000000008000000080000080bf0000803f0000000000000000000000000000803f\ - 000000000000803f0000803f000000000000 - """) - } }