diff --git a/.swiftformat b/.swiftformat index 1e0344c4..ec6571be 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,4 +1,6 @@ --hexgrouping ignore --decimalgrouping ignore --enable isEmpty ---ifdef no-indent \ No newline at end of file +--ifdef no-indent +--wraparguments before-first +--maxwidth 120 \ No newline at end of file diff --git a/Euclid.xcodeproj/project.pbxproj b/Euclid.xcodeproj/project.pbxproj index 8607eb57..c651c8f5 100644 --- a/Euclid.xcodeproj/project.pbxproj +++ b/Euclid.xcodeproj/project.pbxproj @@ -54,6 +54,8 @@ 01BA297A2235E34C0088D36B /* CGPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BA29792235E34C0088D36B /* CGPathTests.swift */; }; 01BA297C2235E3590088D36B /* CoreGraphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BA297B2235E3580088D36B /* CoreGraphics.swift */; }; 01BA297D2235E3590088D36B /* CoreGraphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BA297B2235E3580088D36B /* CoreGraphics.swift */; }; + 01F2382023BF4160005EC9DB /* LineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F2381F23BF4160005EC9DB /* LineSegment.swift */; }; + 01F2382123BF4160005EC9DB /* LineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F2381F23BF4160005EC9DB /* LineSegment.swift */; }; 52A3852E238D6E5700BE8407 /* LineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A3852D238D6E5700BE8407 /* LineTests.swift */; }; 52A663A123857D5300FACF9D /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A663A023857D5300FACF9D /* Line.swift */; }; 52A663A223858A3700FACF9D /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A663A023857D5300FACF9D /* Line.swift */; }; @@ -115,6 +117,7 @@ 01BA29702235A63F0088D36B /* CoreText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreText.swift; sourceTree = ""; }; 01BA29792235E34C0088D36B /* CGPathTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGPathTests.swift; sourceTree = ""; }; 01BA297B2235E3580088D36B /* CoreGraphics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreGraphics.swift; sourceTree = ""; }; + 01F2381F23BF4160005EC9DB /* LineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineSegment.swift; sourceTree = ""; }; 52A3852D238D6E5700BE8407 /* LineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineTests.swift; sourceTree = ""; }; 52A663A023857D5300FACF9D /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; 52C844E023854C87009C0A73 /* VectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorTests.swift; sourceTree = ""; }; @@ -189,6 +192,7 @@ 016FAB4D21BFE7C200AF60DC /* Plane.swift */, 016FAB4921BFE7C200AF60DC /* Paths.swift */, 52A663A023857D5300FACF9D /* Line.swift */, + 01F2381F23BF4160005EC9DB /* LineSegment.swift */, 016FAB4E21BFE7C200AF60DC /* Shapes.swift */, 016FAB4421BFE7C100AF60DC /* CSG.swift */, 016FAB4B21BFE7C200AF60DC /* Transforms.swift */, @@ -417,6 +421,7 @@ 016FAB5321BFE7C200AF60DC /* Bounds.swift in Sources */, 01BA297C2235E3590088D36B /* CoreGraphics.swift in Sources */, 01BA29712235A63F0088D36B /* CoreText.swift in Sources */, + 01F2382023BF4160005EC9DB /* LineSegment.swift in Sources */, 016FAB5221BFE7C200AF60DC /* Vertex.swift in Sources */, 016FAB5821BFE7C200AF60DC /* Polygon.swift in Sources */, 52A663A123857D5300FACF9D /* Line.swift in Sources */, @@ -458,6 +463,7 @@ 016FAB6F21BFE80000AF60DC /* Bounds.swift in Sources */, 01BA297D2235E3590088D36B /* CoreGraphics.swift in Sources */, 01BA29722235A63F0088D36B /* CoreText.swift in Sources */, + 01F2382123BF4160005EC9DB /* LineSegment.swift in Sources */, 016FAB7021BFE80000AF60DC /* Vertex.swift in Sources */, 016FAB7121BFE80000AF60DC /* Polygon.swift in Sources */, 52A663A223858A3700FACF9D /* Line.swift in Sources */, diff --git a/Sources/CSG.swift b/Sources/CSG.swift index b755bf11..6fa82b01 100644 --- a/Sources/CSG.swift +++ b/Sources/CSG.swift @@ -226,8 +226,11 @@ private func reduce(_ meshes: [Mesh], using fn: (Mesh, Mesh) -> Mesh) -> Mesh { return reduce(&meshesAndBounds, at: 0, using: fn) } -private func reduce(_ meshesAndBounds: inout [(Mesh, Bounds)], - at i: Int, using fn: (Mesh, Mesh) -> Mesh) -> Mesh { +private func reduce( + _ meshesAndBounds: inout [(Mesh, Bounds)], + at i: Int, + using fn: (Mesh, Mesh) -> Mesh +) -> Mesh { var (m, mb) = meshesAndBounds[i] var j = i + 1, count = meshesAndBounds.count while j < count { @@ -299,10 +302,12 @@ private class BSPNode { return clip(polygons, keeping, clipBackfaces, &id) } - private func clip(_ polygons: [Polygon], - _ keeping: ClipRule, - _ clipBackfaces: Bool, - _ id: inout Int) -> [Polygon] { + private func clip( + _ polygons: [Polygon], + _ keeping: ClipRule, + _ clipBackfaces: Bool, + _ id: inout Int + ) -> [Polygon] { var polygons = polygons var node = self var total = [Polygon]() @@ -331,8 +336,12 @@ private class BSPNode { } for polygon in coplanar { var inside = [Polygon](), outside = [Polygon]() - polygon.clip(to: node.polygons.flatMap { $0.tessellate() }, - &inside, &outside, &id) + polygon.clip( + to: node.polygons.flatMap { $0.tessellate() }, + &inside, + &outside, + &id + ) switch keeping { case .greaterThan: if node.plane!.normal.dot(polygon.plane.normal) > 0 { @@ -455,10 +464,12 @@ extension Polygon { return planes } - func clip(to polygons: [Polygon], - _ inside: inout [Polygon], - _ outside: inout [Polygon], - _ id: inout Int) { + func clip( + to polygons: [Polygon], + _ inside: inout [Polygon], + _ outside: inout [Polygon], + _ id: inout Int + ) { precondition(isConvex) var toTest = [self] for polygon in polygons where !toTest.isEmpty { @@ -472,10 +483,12 @@ extension Polygon { outside = toTest } - func clip(_ polygon: Polygon, - _ inside: inout [Polygon], - _ outside: inout [Polygon], - _ id: inout Int) { + func clip( + _ polygon: Polygon, + _ inside: inout [Polygon], + _ outside: inout [Polygon], + _ id: inout Int + ) { precondition(isConvex) guard polygon.isConvex else { polygon.tessellate().forEach { @@ -496,11 +509,13 @@ extension Polygon { inside.append(polygon) } - func split(along plane: Plane, - _ coplanar: inout [Polygon], - _ front: inout [Polygon], - _ back: inout [Polygon], - _ id: inout Int) { + func split( + along plane: Plane, + _ coplanar: inout [Polygon], + _ front: inout [Polygon], + _ back: inout [Polygon], + _ id: inout Int + ) { enum PolygonType: Int { case coplanar = 0 case front = 1 diff --git a/Sources/CoreText.swift b/Sources/CoreText.swift index 1238009c..dffff468 100644 --- a/Sources/CoreText.swift +++ b/Sources/CoreText.swift @@ -38,12 +38,13 @@ public extension Path { public extension Mesh { /// Create an extruded text model from a String - init(text: String, - font: CTFont? = nil, - width: Double? = nil, - depth: Double = 1, - detail: Int = 2, - material: Polygon.Material = nil + init( + text: String, + font: CTFont? = nil, + width: Double? = nil, + depth: Double = 1, + detail: Int = 2, + material: Polygon.Material = nil ) { let font = font ?? CTFontCreateWithName("Helvetica" as CFString, 1, nil) let attributes = [NSAttributedString.Key.font: font] @@ -52,11 +53,12 @@ public extension Mesh { } /// Create an extruded text model from an attributed string - init(text: NSAttributedString, - width: Double? = nil, - depth: Double = 1, - detail: Int = 2, - material: Polygon.Material = nil + init( + text: NSAttributedString, + width: Double? = nil, + depth: Double = 1, + detail _: Int = 2, + material: Polygon.Material = nil ) { var meshes = [Mesh]() var cache = [CGPath: Mesh]() diff --git a/Sources/Line.swift b/Sources/Line.swift index 001d81c6..342b466a 100644 --- a/Sources/Line.swift +++ b/Sources/Line.swift @@ -29,62 +29,17 @@ // SOFTWARE. // -import Foundation - -public struct LineSegment : Hashable { - public init(_ point1: Vector, _ point2: Vector) { - self.point1 = point1 - self.point2 = point2 - } - - public var point1: Vector { - didSet { point1 = point1.quantized() } - } - - public var point2: Vector { - didSet { point2 = point2.quantized() } - } - - public var direction : Vector { - let diff = point2 - point1 - return diff.normalized() - } - - public func intersects(with: LineSegment) -> Bool { - if ((self.direction.z == 0) && (with.direction.z == 0) && (self.point1.z == with.point1.z)) { - return lineSegmentsIntersect(self.point1, self.point2, with.point1, with.point2) - } else if ((self.direction.y == 0) && (with.direction.y == 0) && (self.point1.y == with.point1.y)) { - // Switch dimensions and then solve - let p0 = Vector(self.point1.x, self.point1.z, 0) - let p1 = Vector(self.point2.x, self.point2.z, 0) - let p2 = Vector(with.point1.x, with.point1.z, 0) - let p3 = Vector(with.point2.x, with.point2.z, 0) - return lineSegmentsIntersect(p0, p1, p2, p3) - } else if ((self.direction.x == 0) && (with.direction.x == 0) && (self.point1.x == with.point1.x)) { - // Switch dimensions and then solve - let p0 = Vector(self.point1.y, self.point1.z, 0) - let p1 = Vector(self.point2.y, self.point2.z, 0) - let p2 = Vector(with.point1.y, with.point1.z, 0) - let p3 = Vector(with.point2.y, with.point2.z, 0) - return lineSegmentsIntersect(p0, p1, p2, p3) - } else { - // TOOO: Generalize to 3D - return false; - } - } -} - -public struct Line : Hashable { +public struct Line: Hashable { public init(point: Vector, direction: Vector) { self.point = point self.direction = direction } - + public init(from: LineSegment) { - self.point = from.point1 - self.direction = from.direction + point = from.point1 + direction = from.direction } - + public var point: Vector { didSet { point = point.quantized() } } @@ -92,94 +47,36 @@ public struct Line : Hashable { public var direction: Vector { didSet { direction = direction.normalized() } } - + public func distance(to: Vector) -> Double { // See "Vector formulation" at https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line - let aMinusP = self.point - to - let v = aMinusP - (self.direction * aMinusP.dot(self.direction)) + let aMinusP = point - to + let v = aMinusP - (direction * aMinusP.dot(direction)) return v.length } - + public func intersection(with: Line) -> Vector? { - if ((self.direction.z == 0) && (with.direction.z == 0) && (self.point.z == with.point.z)) { - return lineIntersection(self.point, self.point + self.direction, with.point, with.point + with.direction) - } else if ((self.direction.y == 0) && (with.direction.y == 0) && (self.point.y == with.point.y)) { + if direction.z == 0, with.direction.z == 0, point.z == with.point.z { + return lineIntersection(point, point + direction, with.point, with.point + with.direction) + } else if direction.y == 0, with.direction.y == 0, point.y == with.point.y { // Switch dimensions and then solve - let p0 = Vector(self.point.x, self.point.z, self.point.y) - let p1 = p0 + Vector(self.direction.x, self.direction.z, 0) + let p0 = Vector(point.x, point.z, point.y) + let p1 = p0 + Vector(direction.x, direction.z, 0) let p2 = Vector(with.point.x, with.point.z, with.point.y) let p3 = p2 + Vector(with.direction.x, with.direction.z, 0) let solution = lineIntersection(p0, p1, p2, p3) - if (solution != nil) { - return Vector(solution!.x, solution!.z, solution!.y) - } else { - return nil; - } - } else if ((self.direction.x == 0) && (with.direction.x == 0) && (self.point.x == with.point.x)) { + return solution.map { Vector($0.x, $0.z, $0.y) } + } else if direction.x == 0, with.direction.x == 0, point.x == with.point.x { // Switch dimensions and then solve - let p0 = Vector(self.point.y, self.point.z, self.point.x) - let p1 = p0 + Vector(self.direction.y, self.direction.z, 0) + let p0 = Vector(point.y, point.z, point.x) + let p1 = p0 + Vector(direction.y, direction.z, 0) let p2 = Vector(with.point.y, with.point.z, with.point.x) let p3 = p2 + Vector(with.direction.y, with.direction.z, 0) let solution = lineIntersection(p0, p1, p2, p3) - if (solution != nil) { - return Vector(solution!.z, solution!.x, solution!.y) - } else { - return nil; - } + return solution.map { Vector($0.z, $0.x, $0.y) } } else { - // TOOO: Generalize to 3D - return nil; + // TODO: Generalize to 3D + return nil } } } - -// MARK: Private utility functions - -// Get the intersection point between two lines -// TODO: extend this to work in 3D -// TODO: improve this using https://en.wikipedia.org/wiki/Line–line_intersection -private func lineIntersection(_ p0: Vector, _ p1: Vector, - _ p2: Vector, _ p3: Vector) -> Vector? { - let x1 = p0.x, y1 = p0.y - let x2 = p1.x, y2 = p1.y - let x3 = p2.x, y3 = p2.y - let x4 = p3.x, y4 = p3.y - - let x1y2 = x1 * y2, y1x2 = y1 * x2 - let x1y2minusy1x2 = x1y2 - y1x2 - - let x3minusx4 = x3 - x4 - let x1minusx2 = x1 - x2 - - let x3y4 = x3 * y4, y3x4 = y3 * x4 - let x3y4minusy3x4 = x3y4 - y3x4 - - let y3minusy4 = y3 - y4 - let y1minusy2 = y1 - y2 - - let d = x1minusx2 * y3minusy4 - y1minusy2 * x3minusx4 - if abs(d) < epsilon { - return nil // lines are parallel - } - let ix = (x1y2minusy1x2 * x3minusx4 - x1minusx2 * x3y4minusy3x4) / d - let iy = (x1y2minusy1x2 * y3minusy4 - y1minusy2 * x3y4minusy3x4) / d - - return Vector(ix, iy, p0.z).quantized() -} - -// TODO: extend this to work in 3D -private func lineSegmentsIntersect(_ p0: Vector, _ p1: Vector, - _ p2: Vector, _ p3: Vector) -> Bool { - guard let pi = lineIntersection(p0, p1, p2, p3) else { - return false // lines are parallel - } - // TODO: is there a cheaper way to do this? - if pi.x < min(p0.x, p1.x) || pi.x > max(p0.x, p1.x) || - pi.x < min(p2.x, p3.x) || pi.x > max(p2.x, p3.x) || - pi.y < min(p0.y, p1.y) || pi.y > max(p0.y, p1.y) || - pi.y < min(p2.y, p3.y) || pi.y > max(p2.y, p3.y) { - return false - } - return true -} diff --git a/Sources/LineSegment.swift b/Sources/LineSegment.swift new file mode 100644 index 00000000..3cfe8dcb --- /dev/null +++ b/Sources/LineSegment.swift @@ -0,0 +1,75 @@ +// +// Line.swift +// Euclid +// +// Created by Nick Lockwood on 20/11/2019. +// Copyright © 2018 Nick Lockwood. All rights reserved. +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/nicklockwood/Euclid +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +public struct LineSegment: Hashable { + public var point1: Vector { + didSet { point1 = point1.quantized() } + } + + public var point2: Vector { + didSet { point2 = point2.quantized() } + } + + public init(_ point1: Vector, _ point2: Vector) { + self.point1 = point1 + self.point2 = point2 + } +} + +public extension LineSegment { + var direction: Vector { + let diff = point2 - point1 + return diff.normalized() + } + + func intersects(_ segment: LineSegment) -> Bool { + if direction.z == 0, segment.direction.z == 0, point1.z == segment.point1.z { + return lineSegmentsIntersect(point1, point2, segment.point1, segment.point2) + } else if direction.y == 0, segment.direction.y == 0, point1.y == segment.point1.y { + // Switch dimensions and then solve + let p0 = Vector(point1.x, point1.z, 0) + let p1 = Vector(point2.x, point2.z, 0) + let p2 = Vector(segment.point1.x, segment.point1.z, 0) + let p3 = Vector(segment.point2.x, segment.point2.z, 0) + return lineSegmentsIntersect(p0, p1, p2, p3) + } else if direction.x == 0, segment.direction.x == 0, point1.x == segment.point1.x { + // Switch dimensions and then solve + let p0 = Vector(point1.y, point1.z, 0) + let p1 = Vector(point2.y, point2.z, 0) + let p2 = Vector(segment.point1.y, segment.point1.z, 0) + let p3 = Vector(segment.point2.y, segment.point2.z, 0) + return lineSegmentsIntersect(p0, p1, p2, p3) + } else { + // TOOO: Generalize to 3D + return false + } + } +} diff --git a/Sources/Paths.swift b/Sources/Paths.swift index ce1c4e62..e82f15ca 100644 --- a/Sources/Paths.swift +++ b/Sources/Paths.swift @@ -305,7 +305,7 @@ internal extension Path { } let l1 = LineSegment(p0, p1) let l2 = LineSegment(p2, p3) - if l1.intersects(with: l2) { + if l1.intersects(l2) { return false } } @@ -476,7 +476,7 @@ func extrapolate(_ p0: PathPoint, _ p1: PathPoint, _ p2: PathPoint) -> PathPoint p0p1 = p0p1 / length let p1p2 = (p2.position - p1.position).normalized() let axis = p0p1.cross(p1p2) - let angle = -p0p1.angleWith(p1p2) + let angle = -p0p1.angle(with: p1p2) let r = Rotation(axis: axis, radians: angle) ?? .identity let p2pe = p1p2.rotated(by: r) * length return .curve(p2.position + p2pe) diff --git a/Sources/Plane.swift b/Sources/Plane.swift index aa0f42e3..d5c93902 100644 --- a/Sources/Plane.swift +++ b/Sources/Plane.swift @@ -86,17 +86,17 @@ public extension Plane { func containsPoint(_ p: Vector) -> Bool { return abs(normal.dot(p) - w) < epsilon } - - func intersectionWith(_ p: Plane) -> Line? { - if (self.normal.isEqual(to: p.normal)) { + + /// Returns line of intersection between planes + func intersection(with p: Plane) -> Line? { + if normal.isEqual(to: p.normal) { // Planes do not intersect - return nil; + return nil + } + let direction = normal.cross(p.normal) + guard let point = self.solveSimultaneousEquationsWith(p) else { + return nil } - - let direction = self.normal.cross(p.normal) - - guard let point = self.solveSimultaneousEquationsWith(p) else { return nil; } - return Line(point: point, direction: direction) } } @@ -132,71 +132,71 @@ internal extension Plane { func isEqual(to other: Plane, withPrecision p: Double = epsilon) -> Bool { return abs(w - other.w) < p && normal.isEqual(to: other.normal, withPrecision: p) } - + func solveSimultaneousEquationsWith(_ p2: Plane) -> Vector? { // Try all the permutations of the equations we could solve until we find a solvable combination let vars1 = [self.normal.x, self.normal.y, self.normal.z] let vars2 = [p2.normal.x, p2.normal.y, p2.normal.z] - - for k1 in 0...2 { - for k2 in 0...2 { - if (k2 == k1) { + + for k1 in 0 ... 2 { + for k2 in 0 ... 2 { + if k2 == k1 { continue } - for k3 in 0...2 { - if ((k3 == k1) || (k3 == k2)) { + for k3 in 0 ... 2 { + if (k3 == k1) || (k3 == k2) { continue } - + let vv1 = Vector(vars1[k1], vars1[k2], vars1[k3]) let vv2 = Vector(vars2[k1], vars2[k2], vars2[k3]) - let point = Plane.performGaussianElimination(v1: vv1, w1: self.w, v2: vv2, w2: p2.w) - if (point != nil) { + let point = Plane.performGaussianElimination(v1: vv1, w1: w, v2: vv2, w2: p2.w) + if point != nil { let pointVars = [point!.x, point!.y, point!.z] - + // Rotate the variables back in to their proper place return Vector(pointVars[k1], pointVars[k2], pointVars[k3]) } } } } - - return nil; - } - + + return nil + } + static func performGaussianElimination(v1: Vector, w1: Double, v2: Vector, w2: Double) -> Vector? { // Solve simultaneous equations using Gaussian elimination // http://mathsfirst.massey.ac.nz/Algebra/SystemsofLinEq/EMeth.htm - - if (v1.x == 0) { - return nil; + + if v1.x == 0 { + return nil } - + // Assume z = 0 always - + // Multiply the two equations until they have an equal leading coefficient let n1 = v1 * v2.x let n2 = v2 * v1.x let ww1 = w1 * v2.x let ww2 = w2 * v1.x - + // Subtract the second from the first let diff = n1 - n2 let wdiff = ww1 - ww2 - + // Solve this new equation for y: // diff.y * y = wdiff - if (diff.y == 0) { - return nil; + if diff.y == 0 { + return nil } let y = wdiff / diff.y - + // Substitute this back in to the first equation // self.normal.x * x + self.normal.y * y = self.w // self.normal.x * x = self.w - self.normal.y * y // x = (self.w - self.normal.y * y) / self.normal.x let x = (w1 - v1.y * y) / v1.x - + return Vector(x, y, 0) } } diff --git a/Sources/Polygon.swift b/Sources/Polygon.swift index 5d282ef3..aebbcc87 100644 --- a/Sources/Polygon.swift +++ b/Sources/Polygon.swift @@ -164,7 +164,7 @@ public extension Polygon { i = 0 } } - while vertices.count > 3 { + while vertices.count > 3 { let p0 = vertices[(i - 1 + vertices.count) % vertices.count] let p1 = vertices[i] let p2 = vertices[(i + 1) % vertices.count] @@ -184,8 +184,8 @@ public extension Polygon { let triangle = Polygon([p0, p1, p2]) if triangle == nil || triangle!.plane.normal.dot(plane.normal) <= 0 || vertices.contains(where: { - !triangle!.vertices.contains($0) && triangle!.containsPoint($0.position) - }) { + !triangle!.vertices.contains($0) && triangle!.containsPoint($0.position) + }) { i += 1 if i == vertices.count { i = 0 diff --git a/Sources/Shapes.swift b/Sources/Shapes.swift index 85bd424a..10bf0649 100644 --- a/Sources/Shapes.swift +++ b/Sources/Shapes.swift @@ -71,8 +71,13 @@ public extension Path { case lhs, rhs, all } - func arc(_ p0: PathPoint, _ p1: PathPoint, _ p2: PathPoint, - _ detail: Int, _ range: ArcRange = .all) -> [PathPoint] { + func arc( + _ p0: PathPoint, + _ p1: PathPoint, + _ p2: PathPoint, + _ detail: Int, + _ range: ArcRange = .all + ) -> [PathPoint] { let detail = detail + 1 assert(detail >= 2) let steps: [Double] @@ -97,7 +102,7 @@ public extension Path { } } - var points = sanitizePoints(points) + let points = sanitizePoints(points) guard detail > 0, !points.isEmpty else { return Path(unchecked: points, plane: nil, subpathIndices: nil) } @@ -178,10 +183,12 @@ public extension Mesh { } /// Construct an axis-aligned cuboid mesh - static func cube(center c: Vector = .init(0, 0, 0), - size s: Vector, - faces: Faces = .default, - material: Polygon.Material = nil) -> Mesh { + static func cube( + center c: Vector = .init(0, 0, 0), + size s: Vector, + faces: Faces = .default, + material: Polygon.Material = nil + ) -> Mesh { let polygons: [Polygon] = [ [[5, 1, 3, 7], [+1, 0, 0]], [[0, 4, 6, 2], [-1, 0, 0]], @@ -226,21 +233,25 @@ public extension Mesh { } } - static func cube(center c: Vector = .init(0, 0, 0), - size s: Double = 1, - faces: Faces = .default, - material: Polygon.Material = nil) -> Mesh { + static func cube( + center c: Vector = .init(0, 0, 0), + size s: Double = 1, + faces: Faces = .default, + material: Polygon.Material = nil + ) -> Mesh { return cube(center: c, size: Vector(s, s, s), faces: faces, material: material) } /// Construct a sphere mesh - static func sphere(radius r: Double = 0.5, - slices: Int = 16, - stacks: Int? = nil, - poleDetail: Int = 0, - faces: Faces = .default, - wrapMode: WrapMode = .default, - material: Polygon.Material = nil) -> Mesh { + static func sphere( + radius r: Double = 0.5, + slices: Int = 16, + stacks: Int? = nil, + poleDetail: Int = 0, + faces: Faces = .default, + wrapMode: WrapMode = .default, + material: Polygon.Material = nil + ) -> Mesh { var semicircle = [PathPoint]() let stacks = max(2, stacks ?? (slices / 2)) let r = max(abs(r), epsilon) @@ -259,13 +270,15 @@ public extension Mesh { } /// Construct a cylindrical mesh - static func cylinder(radius r: Double = 0.5, - height h: Double = 1, - slices: Int = 16, - poleDetail: Int = 0, - faces: Faces = .default, - wrapMode: WrapMode = .default, - material: Polygon.Material = nil) -> Mesh { + static func cylinder( + radius r: Double = 0.5, + height h: Double = 1, + slices: Int = 16, + poleDetail: Int = 0, + faces: Faces = .default, + wrapMode: WrapMode = .default, + material: Polygon.Material = nil + ) -> Mesh { let r = max(abs(r), epsilon) let h = max(abs(h), epsilon) let wrapMode = wrapMode == .default ? .tube : wrapMode @@ -286,14 +299,16 @@ public extension Mesh { } /// Construct as conical mesh - static func cone(radius r: Double = 0.5, - height h: Double = 1, - slices: Int = 16, - poleDetail: Int? = nil, - addDetailAtBottomPole: Bool = false, - faces: Faces = .default, - wrapMode: WrapMode = .default, - material: Polygon.Material = nil) -> Mesh { + static func cone( + radius r: Double = 0.5, + height h: Double = 1, + slices: Int = 16, + poleDetail: Int? = nil, + addDetailAtBottomPole: Bool = false, + faces: Faces = .default, + wrapMode: WrapMode = .default, + material: Polygon.Material = nil + ) -> Mesh { let r = max(abs(r), epsilon) let h = max(abs(h), epsilon) let poleDetail = poleDetail ?? Int(sqrt(Double(slices))) @@ -326,13 +341,15 @@ public extension Mesh { /// * Open paths that do not start and end on the Y axis will produce /// a shape with a hole in it /// - static func lathe(_ profile: Path, - slices: Int = 16, - poleDetail: Int = 0, - addDetailForFlatPoles: Bool = false, - faces: Faces = .default, - wrapMode: WrapMode = .default, - material: Polygon.Material = nil) -> Mesh { + static func lathe( + _ profile: Path, + slices: Int = 16, + poleDetail: Int = 0, + addDetailForFlatPoles: Bool = false, + faces: Faces = .default, + wrapMode: WrapMode = .default, + material: Polygon.Material = nil + ) -> Mesh { let subpaths = profile.subpaths if subpaths.count > 1 { return .xor(subpaths.map { @@ -419,14 +436,18 @@ public extension Mesh { Vector(cos0 * v0.normal.x, v0.normal.y, sin0 * -v0.normal.x), Vector(v0.texcoord.x + (t0 + t1) / 2, v0.texcoord.y, 0) ) - let v2 = Vertex(unchecked: + let v2 = Vertex( + unchecked: Vector(cos0 * v1.position.x, v1.position.y, sin0 * -v1.position.x), - Vector(cos0 * v1.normal.x, v1.normal.y, sin0 * -v1.normal.x), - Vector(v1.texcoord.x + t0, v1.texcoord.y, 0)) - let v3 = Vertex(unchecked: + Vector(cos0 * v1.normal.x, v1.normal.y, sin0 * -v1.normal.x), + Vector(v1.texcoord.x + t0, v1.texcoord.y, 0) + ) + let v3 = Vertex( + unchecked: Vector(cos1 * v1.position.x, v1.position.y, sin1 * -v1.position.x), - Vector(cos1 * v1.normal.x, v1.normal.y, sin1 * -v1.normal.x), - Vector(v1.texcoord.x + t1, v1.texcoord.y, 0)) + Vector(cos1 * v1.normal.x, v1.normal.y, sin1 * -v1.normal.x), + Vector(v1.texcoord.x + t1, v1.texcoord.y, 0) + ) polygons.append(Polygon(unchecked: [v0, v2, v3], isConvex: true, material: material)) } } else if v1.position.x == 0 { @@ -436,33 +457,45 @@ public extension Mesh { Vector(cos0 * v1.normal.x, v1.normal.y, sin0 * -v1.normal.x), Vector(v1.texcoord.x + (t0 + t1) / 2, v1.texcoord.y, 0) ) - let v2 = Vertex(unchecked: + let v2 = Vertex( + unchecked: Vector(cos1 * v0.position.x, v0.position.y, sin1 * -v0.position.x), - Vector(cos1 * v0.normal.x, v0.normal.y, sin1 * -v0.normal.x), - Vector(v0.texcoord.x + t1, v0.texcoord.y, 0)) - let v3 = Vertex(unchecked: + Vector(cos1 * v0.normal.x, v0.normal.y, sin1 * -v0.normal.x), + Vector(v0.texcoord.x + t1, v0.texcoord.y, 0) + ) + let v3 = Vertex( + unchecked: Vector(cos0 * v0.position.x, v0.position.y, sin0 * -v0.position.x), - Vector(cos0 * v0.normal.x, v0.normal.y, sin0 * -v0.normal.x), - Vector(v0.texcoord.x + t0, v0.texcoord.y, 0)) + Vector(cos0 * v0.normal.x, v0.normal.y, sin0 * -v0.normal.x), + Vector(v0.texcoord.x + t0, v0.texcoord.y, 0) + ) polygons.append(Polygon(unchecked: [v2, v3, v1], isConvex: true, material: material)) } else { // quad face - let v2 = Vertex(unchecked: + let v2 = Vertex( + unchecked: Vector(cos1 * v0.position.x, v0.position.y, sin1 * -v0.position.x), - Vector(cos1 * v0.normal.x, v0.normal.y, sin1 * -v0.normal.x), - Vector(v0.texcoord.x + t1, v0.texcoord.y, 0)) - let v3 = Vertex(unchecked: + Vector(cos1 * v0.normal.x, v0.normal.y, sin1 * -v0.normal.x), + Vector(v0.texcoord.x + t1, v0.texcoord.y, 0) + ) + let v3 = Vertex( + unchecked: Vector(cos0 * v0.position.x, v0.position.y, sin0 * -v0.position.x), - Vector(cos0 * v0.normal.x, v0.normal.y, sin0 * -v0.normal.x), - Vector(v0.texcoord.x + t0, v0.texcoord.y, 0)) - let v4 = Vertex(unchecked: + Vector(cos0 * v0.normal.x, v0.normal.y, sin0 * -v0.normal.x), + Vector(v0.texcoord.x + t0, v0.texcoord.y, 0) + ) + let v4 = Vertex( + unchecked: Vector(cos0 * v1.position.x, v1.position.y, sin0 * -v1.position.x), - Vector(cos0 * v1.normal.x, v1.normal.y, sin0 * -v1.normal.x), - Vector(v1.texcoord.x + t0, v1.texcoord.y, 0)) - let v5 = Vertex(unchecked: + Vector(cos0 * v1.normal.x, v1.normal.y, sin0 * -v1.normal.x), + Vector(v1.texcoord.x + t0, v1.texcoord.y, 0) + ) + let v5 = Vertex( + unchecked: Vector(cos1 * v1.position.x, v1.position.y, sin1 * -v1.position.x), - Vector(cos1 * v1.normal.x, v1.normal.y, sin1 * -v1.normal.x), - Vector(v1.texcoord.x + t1, v1.texcoord.y, 0)) + Vector(cos1 * v1.normal.x, v1.normal.y, sin1 * -v1.normal.x), + Vector(v1.texcoord.x + t1, v1.texcoord.y, 0) + ) let vertices = [v2, v3, v4, v5] if !verticesAreDegenerate(vertices) { polygons.append(Polygon(unchecked: vertices, isConvex: true, material: material)) @@ -491,10 +524,12 @@ public extension Mesh { } /// Extrude a path along its face normal - static func extrude(_ shape: Path, - depth: Double = 1, - faces: Faces = .default, - material: Polygon.Material = nil) -> Mesh { + static func extrude( + _ shape: Path, + depth: Double = 1, + faces: Faces = .default, + material: Polygon.Material = nil + ) -> Mesh { let offset = (shape.plane?.normal ?? Vector(0, 0, 1)) * (depth / 2) if offset.lengthSquared < epsilon { return fill(shape, faces: faces, material: material) @@ -506,9 +541,11 @@ public extension Mesh { } /// Connect multiple 3D paths - static func loft(_ shapes: [Path], - faces: Faces = .default, - material: Polygon.Material = nil) -> Mesh { + static func loft( + _ shapes: [Path], + faces: Faces = .default, + material: Polygon.Material = nil + ) -> Mesh { var subpathCount = 0 let arrayOfSubpaths: [[Path]] = shapes.map { let subpaths = $0.subpaths @@ -615,9 +652,11 @@ public extension Mesh { } /// Fill a path to form a polygon - static func fill(_ shape: Path, - faces: Faces = .default, - material: Polygon.Material = nil) -> Mesh { + static func fill( + _ shape: Path, + faces: Faces = .default, + material: Polygon.Material = nil + ) -> Mesh { let subpaths = shape.subpaths if subpaths.count > 1 { return .xor(subpaths.map { .fill($0, faces: faces, material: material) }) diff --git a/Sources/Transforms.swift b/Sources/Transforms.swift index 01fd1250..324894db 100644 --- a/Sources/Transforms.swift +++ b/Sources/Transforms.swift @@ -81,9 +81,17 @@ public extension Rotation { } /// Define a rotation using 3x3 matrix coefficients - init(_ m11: Double, _ m12: Double, _ m13: Double, - _ m21: Double, _ m22: Double, _ m23: Double, - _ m31: Double, _ m32: Double, _ m33: Double) { + init( + _ m11: Double, + _ m12: Double, + _ m13: Double, + _ m21: Double, + _ m22: Double, + _ m23: Double, + _ m31: Double, + _ m32: Double, + _ m33: Double + ) { assert(!m11.isNaN) self.m11 = m11 self.m12 = m12 @@ -426,13 +434,15 @@ public extension Path { func translated(by v: Vector) -> Path { return Path( unchecked: points.map { $0.translated(by: v) }, - plane: plane?.translated(by: v), subpathIndices: subpathIndices) + plane: plane?.translated(by: v), subpathIndices: subpathIndices + ) } func rotated(by r: Rotation) -> Path { return Path( unchecked: points.map { $0.rotated(by: r) }, - plane: plane?.rotated(by: r), subpathIndices: subpathIndices) + plane: plane?.rotated(by: r), subpathIndices: subpathIndices + ) } func scaled(by v: Vector) -> Path { diff --git a/Sources/Utilities.swift b/Sources/Utilities.swift index 0290d008..8e5e44fa 100644 --- a/Sources/Utilities.swift +++ b/Sources/Utilities.swift @@ -192,3 +192,60 @@ func cubicBezier(_ p0: Double, _ p1: Double, _ p2: Double, _ p3: Double, _ t: Do let c3 = t * t * t * p3 return c0 + c1 + c2 + c3 } + +// MARK: Line utilities + +// TODO: extend this to work in 3D +// TODO: improve this using https://en.wikipedia.org/wiki/Line–line_intersection +func lineIntersection( + _ p0: Vector, + _ p1: Vector, + _ p2: Vector, + _ p3: Vector +) -> Vector? { + let x1 = p0.x, y1 = p0.y + let x2 = p1.x, y2 = p1.y + let x3 = p2.x, y3 = p2.y + let x4 = p3.x, y4 = p3.y + + let x1y2 = x1 * y2, y1x2 = y1 * x2 + let x1y2minusy1x2 = x1y2 - y1x2 + + let x3minusx4 = x3 - x4 + let x1minusx2 = x1 - x2 + + let x3y4 = x3 * y4, y3x4 = y3 * x4 + let x3y4minusy3x4 = x3y4 - y3x4 + + let y3minusy4 = y3 - y4 + let y1minusy2 = y1 - y2 + + let d = x1minusx2 * y3minusy4 - y1minusy2 * x3minusx4 + if abs(d) < epsilon { + return nil // lines are parallel + } + let ix = (x1y2minusy1x2 * x3minusx4 - x1minusx2 * x3y4minusy3x4) / d + let iy = (x1y2minusy1x2 * y3minusy4 - y1minusy2 * x3y4minusy3x4) / d + + return Vector(ix, iy, p0.z).quantized() +} + +// TODO: extend this to work in 3D +func lineSegmentsIntersect( + _ p0: Vector, + _ p1: Vector, + _ p2: Vector, + _ p3: Vector +) -> Bool { + guard let pi = lineIntersection(p0, p1, p2, p3) else { + return false // lines are parallel + } + // TODO: is there a cheaper way to do this? + if pi.x < min(p0.x, p1.x) || pi.x > max(p0.x, p1.x) || + pi.x < min(p2.x, p3.x) || pi.x > max(p2.x, p3.x) || + pi.y < min(p0.y, p1.y) || pi.y > max(p0.y, p1.y) || + pi.y < min(p2.y, p3.y) || pi.y > max(p2.y, p3.y) { + return false + } + return true +} diff --git a/Sources/Vector.swift b/Sources/Vector.swift index 512cdc0d..565b9898 100644 --- a/Sources/Vector.swift +++ b/Sources/Vector.swift @@ -112,15 +112,15 @@ public extension Vector { func quantized() -> Vector { return Vector(quantize(x), quantize(y), quantize(z)) } - - func angleWith(_ a: Vector) -> Double { - let cosineAngle = (self.dot(a) / (self.length * a.length)); + + func angle(with a: Vector) -> Double { + let cosineAngle = (dot(a) / (length * a.length)) return acos(cosineAngle) } - - func angleWith(plane: Plane) -> Double { + + func angle(with plane: Plane) -> Double { // We know that plane.normal.length == 1 - let complementeryAngle = self.dot(plane.normal) / self.length; + let complementeryAngle = dot(plane.normal) / length return asin(complementeryAngle) } } diff --git a/Tests/LineTests.swift b/Tests/LineTests.swift index ef73bf18..1139989b 100644 --- a/Tests/LineTests.swift +++ b/Tests/LineTests.swift @@ -10,7 +10,6 @@ import XCTest class LineTests: XCTestCase { - func testLinuxTestSuiteIncludesAllTests() { #if os(macOS) let thisClass = type(of: self) @@ -19,50 +18,50 @@ class LineTests: XCTestCase { XCTAssertEqual(linuxCount, darwinCount, "run swift test --generate-linuxmain") #endif } - + // MARK: Vector distance - + func testDistanceFromPointSimple() { let l = Line(point: Vector(0, 0, 0), direction: Vector(1, 0, 0)) let p = Vector(15, 2, 0) XCTAssertEqual(l.distance(to: p), 2) } - + func testDistanceFromPointHarder() { let l = Line(point: Vector(0, 0, 0), direction: Vector(1, 0, 0)) let p = Vector(15, 2, 3) - XCTAssertEqual(l.distance(to: p), (2*2 + 3*3).squareRoot()) + XCTAssertEqual(l.distance(to: p), (2 * 2 + 3 * 3).squareRoot()) } - + func testLineIntersectionXY() { let l1 = Line(point: Vector(1, 0, 3), direction: Vector(1, 0, 0)) let l2 = Line(point: Vector(0, 1, 3), direction: Vector(0, -1, 0)) - + let intersection = l1.intersection(with: l2) XCTAssertNotNil(intersection) - if (intersection != nil) { + if intersection != nil { XCTAssertEqual(intersection, Vector(0, 0, 3).quantized()) } } - + func testLineIntersectionXZ() { let l1 = Line(point: Vector(1, 3, 0), direction: Vector(1, 0, 0)) let l2 = Line(point: Vector(0, 3, 1), direction: Vector(0, 0, -1)) - + let intersection = l1.intersection(with: l2) XCTAssertNotNil(intersection) - if (intersection != nil) { + if intersection != nil { XCTAssertEqual(intersection, Vector(0, 3, 0).quantized()) } } - + func testLineIntersectionYZ() { let l1 = Line(point: Vector(3, 1, 0), direction: Vector(0, 1, 0)) let l2 = Line(point: Vector(3, 0, 1), direction: Vector(0, 0, -1)) - + let intersection = l1.intersection(with: l2) XCTAssertNotNil(intersection) - if (intersection != nil) { + if intersection != nil { XCTAssertEqual(intersection, Vector(3, 0, 0).quantized()) } } diff --git a/Tests/PlaneTests.swift b/Tests/PlaneTests.swift index 72429fbe..a32a3340 100644 --- a/Tests/PlaneTests.swift +++ b/Tests/PlaneTests.swift @@ -32,44 +32,46 @@ class PlaneTests: XCTestCase { let plane = Plane(points: points) XCTAssertEqual(plane?.normal, Vector(0, 0, -1)) } - + func testIntersectionWithParallelPlane() { - let plane1 = Plane(normal: Vector(0, 1, 0), pointOnPlane: Vector(0, 0, 0)) - let plane2 = Plane(normal: Vector(0, 1, 0), pointOnPlane: Vector(0, 1, 0)) - - XCTAssertNil(plane1!.intersectionWith(plane2!)) + let plane1 = Plane(unchecked: Vector(0, 1, 0), pointOnPlane: Vector(0, 0, 0)) + let plane2 = Plane(unchecked: Vector(0, 1, 0), pointOnPlane: Vector(0, 1, 0)) + + XCTAssertNil(plane1.intersection(with: plane2)) } - + func testIntersectionWithPerpendicularPlane() { - let plane1 = Plane(normal: Vector(0, 1, 0), pointOnPlane: Vector(0, 0, 0)) - let plane2 = Plane(normal: Vector(1, 0, 0), pointOnPlane: Vector(0, 0, 0)) - - let intersection = plane1!.intersectionWith(plane2!) - XCTAssertNotNil(intersection) - if (intersection != nil) { - XCTAssert(plane1!.containsPoint(intersection!.point)) - XCTAssert(plane2!.containsPoint(intersection!.point)) - - XCTAssert(plane1!.containsPoint(intersection!.point + intersection!.direction)) - XCTAssert(plane2!.containsPoint(intersection!.point + intersection!.direction)) + let plane1 = Plane(unchecked: Vector(0, 1, 0), pointOnPlane: Vector(0, 0, 0)) + let plane2 = Plane(unchecked: Vector(1, 0, 0), pointOnPlane: Vector(0, 0, 0)) + + guard let intersection = plane1.intersection(with: plane2) else { + XCTFail() + return } + + XCTAssert(plane1.containsPoint(intersection.point)) + XCTAssert(plane2.containsPoint(intersection.point)) + + XCTAssert(plane1.containsPoint(intersection.point + intersection.direction)) + XCTAssert(plane2.containsPoint(intersection.point + intersection.direction)) } - + func testIntersectionWithRandomPlane() { - let plane1 = Plane(normal: Vector(1.2, 0.4, 5.7), w: 6) - let plane2 = Plane(normal: Vector(0.5, 0.7, 0.1), w: 8) - - let intersection = plane1!.intersectionWith(plane2!) - XCTAssertNotNil(intersection) - if (intersection != nil) { - XCTAssertEqual(plane1!.normal.dot(intersection!.point), plane1!.w); - XCTAssertEqual(plane2!.normal.dot(intersection!.point), plane2!.w); - - XCTAssert(plane1!.containsPoint(intersection!.point)) - XCTAssert(plane2!.containsPoint(intersection!.point)) - - XCTAssert(plane1!.containsPoint(intersection!.point + intersection!.direction)) - XCTAssert(plane2!.containsPoint(intersection!.point + intersection!.direction)) + let plane1 = Plane(normal: Vector(1.2, 0.4, 5.7), w: 6)! + let plane2 = Plane(normal: Vector(0.5, 0.7, 0.1), w: 8)! + + guard let intersection = plane1.intersection(with: plane2) else { + XCTFail() + return } + + XCTAssertEqual(plane1.normal.dot(intersection.point), plane1.w) + XCTAssertEqual(plane2.normal.dot(intersection.point), plane2.w) + + XCTAssert(plane1.containsPoint(intersection.point)) + XCTAssert(plane2.containsPoint(intersection.point)) + + XCTAssert(plane1.containsPoint(intersection.point + intersection.direction)) + XCTAssert(plane2.containsPoint(intersection.point + intersection.direction)) } } diff --git a/Tests/TransformTests.swift b/Tests/TransformTests.swift index 50083e01..ed7a2751 100644 --- a/Tests/TransformTests.swift +++ b/Tests/TransformTests.swift @@ -179,7 +179,7 @@ class TransformTests: XCTestCase { let path = Path(unchecked: [ .point(1, 2, 3), .point(7, -2, 12), - .point(-2, 7, 14) + .point(-2, 7, 14), ]) let plane = path.plane! let transform = Transform( diff --git a/Tests/VectorTests.swift b/Tests/VectorTests.swift index 9749dacf..ddce1758 100644 --- a/Tests/VectorTests.swift +++ b/Tests/VectorTests.swift @@ -10,7 +10,6 @@ import XCTest class VectorTests: XCTestCase { - func testLinuxTestSuiteIncludesAllTests() { #if os(macOS) let thisClass = type(of: self) @@ -19,46 +18,45 @@ class VectorTests: XCTestCase { XCTAssertEqual(linuxCount, darwinCount, "run swift test --generate-linuxmain") #endif } - + // MARK: Vector length - + func testAxisAlignedLength() { let vector = Vector(1, 0, 0) XCTAssertEqual(vector.length, 1) } - + func testAngledLength() { let vector = Vector(2, 5, 3) let length = (2.0 * 2.0 + 5.0 * 5.0 + 3.0 * 3.0).squareRoot() XCTAssertEqual(vector.length, length) } - + // MARK: Angle with vector - + func testRightAngle() { let vector1 = Vector(1, 0, 0) let vector2 = Vector(0, 1, 0) - XCTAssertEqual(vector1.angleWith(vector2), Double.pi / 2.0) + XCTAssertEqual(vector1.angle(with: vector2), Double.pi / 2.0) } - + func testNonNormalizedAngle() { let vector1 = Vector(10, 0, 0) let vector2 = Vector(-10, 0, 0) - XCTAssertEqual(vector1.angleWith(vector2), Double.pi) + XCTAssertEqual(vector1.angle(with: vector2), Double.pi) } - + // MARK: Angle with plane - + func testRightAngleWithPlane() { let vector1 = Vector(1, 0, 0) - let plane = Plane(normal: vector1, pointOnPlane: Vector.zero) - XCTAssertEqual(vector1.angleWith(plane: plane!), Double.pi / 2.0) + let plane = Plane(unchecked: vector1, pointOnPlane: Vector.zero) + XCTAssertEqual(vector1.angle(with: plane), Double.pi / 2.0) } - + func testNonNormalizedAngleWithPlane() { let vector1 = Vector(7, 0, 0) - let plane = Plane(normal: vector1, pointOnPlane: Vector.zero) - XCTAssertEqual(vector1.angleWith(plane: plane!), Double.pi / 2.0) + let plane = Plane(normal: vector1, pointOnPlane: Vector.zero)! + XCTAssertEqual(vector1.angle(with: plane), Double.pi / 2.0) } } - diff --git a/Tests/XCTestManifests.swift b/Tests/XCTestManifests.swift index 2e6ff3c0..61f88d13 100644 --- a/Tests/XCTestManifests.swift +++ b/Tests/XCTestManifests.swift @@ -89,7 +89,7 @@ extension PlaneTests { ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), ("testIntersectionWithParallelPlane", testIntersectionWithParallelPlane), ("testIntersectionWithPerpendicularPlane", testIntersectionWithPerpendicularPlane), - ("testIntersectionWithRandomPlane", testIntersectionWithRandomPlane) + ("testIntersectionWithRandomPlane", testIntersectionWithRandomPlane), ] } @@ -107,8 +107,14 @@ extension PolygonTests { ("testConvexPolygonClockwiseWinding", testConvexPolygonClockwiseWinding), ("testDegeneratePolygonWithColinearPoints", testDegeneratePolygonWithColinearPoints), ("testHouseShapedPolygonCorrectlyTriangulated", testHouseShapedPolygonCorrectlyTriangulated), - ("testInvertedConcaveAnticlockwisePolygonCorrectlyTessellated", testInvertedConcaveAnticlockwisePolygonCorrectlyTessellated), - ("testInvertedConcaveAnticlockwisePolygonCorrectlyTriangulated", testInvertedConcaveAnticlockwisePolygonCorrectlyTriangulated), + ( + "testInvertedConcaveAnticlockwisePolygonCorrectlyTessellated", + testInvertedConcaveAnticlockwisePolygonCorrectlyTessellated + ), + ( + "testInvertedConcaveAnticlockwisePolygonCorrectlyTriangulated", + testInvertedConcaveAnticlockwisePolygonCorrectlyTriangulated + ), ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), ("testMerge1", testMerge1), ("testMerge2", testMerge2), @@ -130,7 +136,10 @@ extension ShapeTests { ("testClosedCurvedPathWithSharpFirstCorner", testClosedCurvedPathWithSharpFirstCorner), ("testClosedCurvedPathWithSharpSecondAndThirdCorner", testClosedCurvedPathWithSharpSecondAndThirdCorner), ("testClosedCurvedPathWithSharpSecondCorner", testClosedCurvedPathWithSharpSecondCorner), - ("testCurveWithConsecutiveMixedTypePointsWithSamePosition", testCurveWithConsecutiveMixedTypePointsWithSamePosition), + ( + "testCurveWithConsecutiveMixedTypePointsWithSamePosition", + testCurveWithConsecutiveMixedTypePointsWithSamePosition + ), ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), ("testSimpleCurvedPath", testSimpleCurvedPath), ("testSimpleCurveEndedPath", testSimpleCurveEndedPath), @@ -189,7 +198,7 @@ extension VectorTests { ("testRightAngle", testRightAngle), ("testNonNormalizedAngle", testNonNormalizedAngle), ("testRightAngleWithPlane", testRightAngleWithPlane), - ("testNonNormalizedAngleWithPlane", testNonNormalizedAngleWithPlane) + ("testNonNormalizedAngleWithPlane", testNonNormalizedAngleWithPlane), ] } @@ -200,7 +209,7 @@ extension LineTests { ("testLineIntersectionXY", testLineIntersectionXY), ("testLineIntersectionXZ", testLineIntersectionXZ), ("testLineIntersectionYZ", testLineIntersectionYZ), - ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests) + ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), ] }