Skip to content

Commit

Permalink
Adding Compound2Periodic
Browse files Browse the repository at this point in the history
  • Loading branch information
LuizZak committed Jul 8, 2024
1 parent 7a69ef8 commit 01a7df2
Show file tree
Hide file tree
Showing 14 changed files with 264 additions and 16 deletions.
8 changes: 4 additions & 4 deletions Sources/Geometria/2D/LinePolygon2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,13 @@ extension LinePolygon2 where Vector: Vector2FloatingPoint {
}

/// Returns `true` if the given point lies within an edge of the polygon
/// represented by `self`, up to a given `tolerance` value.
/// represented by `self`, up to a given `toleranceSquared` value.
///
/// Points lie within the edges of the polygon if the distance between the
/// point and any two adjacent vertices is equal to the distance of the
/// adjacent vertices, up to `sqrt(tolerance)`.
/// adjacent vertices, up to `√(toleranceSquared)`.
@inlinable
public func isPointOnEdge(_ point: Vector, tolerance: Scalar) -> Bool {
public func isPointOnEdge(_ point: Vector, toleranceSquared: Scalar) -> Bool {
for (i, vertex) in vertices.enumerated() {
let next = vertices[(i + 1) % vertices.count]

Expand All @@ -257,7 +257,7 @@ extension LinePolygon2 where Vector: Vector2FloatingPoint {

let diff = (edgeSquared - (d1 + d2)).magnitude

if diff < tolerance {
if diff < toleranceSquared {
return true
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/GeometriaPeriodics/2D/Circle2Periodic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public struct Circle2Periodic<Vector: Vector2Real>: Periodic2Geometry, Equatable
circle2.contains(point)
}

public func isOnSurface(_ point: Vector, tolerance: Scalar) -> Bool {
circle2.distanceSquared(to: point) < tolerance * tolerance
public func isOnSurface(_ point: Vector, toleranceSquared: Scalar) -> Bool {
circle2.distanceSquared(to: point) < toleranceSquared
}

public func allSimplexes() -> [Simplex] {
Expand Down
115 changes: 115 additions & 0 deletions Sources/GeometriaPeriodics/2D/Compound2Periodic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import Geometria

/// A 2-dimensional periodic shape that is composed of generic simplexes that
/// are joined end-to-end in a loop.
public struct Compound2Periodic<Vector: Vector2Real>: Periodic2Geometry {
public typealias Scalar = Vector.Scalar
public typealias Simplex = Periodic2GeometrySimplex<Vector>

/// The list of simplexes that compose this compound periodic.
public var simplexes: [Simplex]

public var startPeriod: Period
public var endPeriod: Period

/// Initializes a new compound periodic with a given list of simplexes, using
/// the start period of the first simplex and the end period of the last
/// simplex as the start and end periods for the geometry.
public init(simplexes: [Simplex]) {
self.init(
simplexes: simplexes,
startPeriod: simplexes.first?.startPeriod ?? .zero,
endPeriod: simplexes.last?.endPeriod ?? 1
)
}

/// Initializes a new compound periodic with a given list of simplexes, first
/// normalizing their period intervals so they lie in the range
/// `(0, 1]`.
public init(
normalizing simplexes: [Simplex]
) {
self.init(
normalizing: simplexes,
startPeriod: .zero,
endPeriod: 1
)
}

/// Initializes a new compound periodic with a given list of simplexes, first
/// normalizing their period intervals so they lie in the range
/// `(startPeriod, endPeriod]`.
public init(
normalizing simplexes: [Simplex],
startPeriod: Period,
endPeriod: Period
) {
self.init(
simplexes: simplexes.normalized(
startPeriod: startPeriod,
endPeriod: endPeriod
),
startPeriod: startPeriod,
endPeriod: endPeriod
)
}

/// Initializes a new compound periodic with a given list of simplexes and
/// a pre-defined start/end period range.
///
/// - note: The period of the contained simplexes is not modified and is
/// assumed to match the range `(startPeriod, endPeriod]`.
public init(simplexes: [Simplex], startPeriod: Period, endPeriod: Period) {
self.simplexes = simplexes
self.startPeriod = startPeriod
self.endPeriod = endPeriod
}

public func contains(_ point: Vector) -> Bool {
// Construct a line segment that starts at the queried point and ends at
// a point known to be outside the geometry, then count the number of
// unique point-intersections along the way; if the intersection count
// is divisible by two, then the point is not contained within the
// geometry.

var points: [Vector] = []

let bounds = simplexes.bounds()

let lineSegment = LineSegment2<Vector>(
start: point,
end: .init(x: bounds.right + 10, y: point.y)
)
let lineSimplex = Periodic2GeometrySimplex.lineSegment2(
.init(lineSegment: lineSegment, startPeriod: .zero, endPeriod: 1)
)

for simplex in simplexes {
let intersections = simplex.intersectionPeriods(with: lineSimplex)

for intersection in intersections {
let point = simplex.compute(at: intersection.`self`)

if !points.contains(point) {
points.append(point)
}
}
}

return points.count % 2 == 1
}

public func isOnSurface(_ point: Vector, toleranceSquared: Scalar) -> Bool {
for simplex in simplexes {
if simplex.isOnSurface(point, toleranceSquared: toleranceSquared) {
return true
}
}

return false
}

public func allSimplexes() -> [Simplex] {
simplexes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ extension Simplex2Graph {
let start = getOrCreateGeometry(simplex.start, onLhs: onLhs)
let end = getOrCreateGeometry(simplex.end, onLhs: onLhs)

// Remove existing edge
if let edge = result.edge(from: start, to: end) {
result.removeEdge(edge)
}

addSimplexEdge(clampedLow, from: start, to: node)
addSimplexEdge(clampedHigh, from: node, to: end)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/GeometriaPeriodics/2D/LinePolygon2Periodic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public struct LinePolygon2Periodic<Vector: Vector2Real>: Periodic2Geometry, Equa
linePolygon2.contains(point)
}

public func isOnSurface(_ point: Vector, tolerance: Scalar) -> Bool {
linePolygon2.isPointOnEdge(point, tolerance: tolerance)
public func isOnSurface(_ point: Vector, toleranceSquared: Scalar) -> Bool {
linePolygon2.isPointOnEdge(point, toleranceSquared: toleranceSquared)
}

public func allSimplexes() -> [Simplex] {
Expand Down
19 changes: 19 additions & 0 deletions Sources/GeometriaPeriodics/2D/Periodic2GeometrySimplex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum Periodic2GeometrySimplex<Vector: Vector2Real>: Periodic2Simplex, Equ
}
}

@usableFromInline
var lengthSquared: Vector.Scalar {
switch self {
case .circleArc2(let simplex): return simplex.lengthSquared
Expand All @@ -70,6 +71,16 @@ public enum Periodic2GeometrySimplex<Vector: Vector2Real>: Periodic2Simplex, Equ
}
}

public func isOnSurface(_ vector: Vector, toleranceSquared: Scalar) -> Bool {
switch self {
case .lineSegment2(let lineSegment):
return lineSegment.isOnSurface(vector, toleranceSquared: toleranceSquared)

case .circleArc2(let circleArc):
return circleArc.isOnSurface(vector, toleranceSquared: toleranceSquared)
}
}

/// Returns `startPeriod + (endPeriod - startPeriod) * ratio`.
///
/// - note: The result is unclamped.
Expand Down Expand Up @@ -224,9 +235,17 @@ public enum Periodic2GeometrySimplex<Vector: Vector2Real>: Periodic2Simplex, Equ
}

extension Collection {
/// Computes the minimal bounding box capable of containing this collection
/// of simplexes.
@inlinable
func bounds<Vector>() -> AABB2<Vector> where Element == Periodic2GeometrySimplex<Vector> {
return AABB2(aabbs: self.map(\.bounds))
}

/// Renormalizes the simplexes within this collection such that the periods
/// of the simplexes have a sequential value within the given start and end
/// periods, relative to each simplex's length.
@inlinable
func normalized<Vector>(startPeriod: Vector.Scalar, endPeriod: Vector.Scalar) -> [Element] where Element == Periodic2GeometrySimplex<Vector> {
typealias Scalar = Vector.Scalar

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public protocol Periodic2Geometry: PeriodicGeometry {
func periodPrecedes(from start: Period, _ lhs: Period, _ rhs: Period) -> Bool

/// Performs a point-surface check against this periodic geometry, up to a
/// given tolerance value.
func isOnSurface(_ point: Vector, tolerance: Scalar) -> Bool
/// given squared tolerance value.
func isOnSurface(_ point: Vector, toleranceSquared: Scalar) -> Bool

/// Fetches all simplexes that form this 2-dimensional periodic geometry,
/// ordered by their relative period within the geometry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public protocol Periodic2Simplex: PeriodicSimplex where Vector: Vector2Type {
/// is `end`, with values in between continuously translating from start to
/// the end, not necessarily in a straight line.
func compute(at period: Period) -> Vector

/// Returns `true` if a given vector is at most `√(toleranceSquared)`-distance
/// away from this simplex's surface.
func isOnSurface(_ vector: Vector, toleranceSquared: Vector.Scalar) -> Bool
}

extension Periodic2Simplex {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import Geometria

/// A 2-dimensional simplex composed of a circular arc segment.
public struct CircleArc2Simplex<Vector: Vector2Real>: Periodic2Simplex, Equatable {
public typealias Scalar = Vector.Scalar

/// The circular arc segment associated with this simplex.
public var circleArc: CircleArc2<Vector>

public var startPeriod: Period
public var endPeriod: Period

var lengthSquared: Vector.Scalar {
var lengthSquared: Scalar {
circleArc.arcLength * circleArc.arcLength
}

Expand Down Expand Up @@ -52,6 +54,10 @@ public struct CircleArc2Simplex<Vector: Vector2Real>: Periodic2Simplex, Equatabl
)
}

public func isOnSurface(_ vector: Vector, toleranceSquared: Scalar) -> Bool {
circleArc.distanceSquared(to: vector) < toleranceSquared
}

/// Clamps this simplex so its contained geometry is only present within a
/// given period range.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Geometria

/// A 2-dimensional simplex composed of a line segment.
public struct LineSegment2Simplex<Vector: Vector2FloatingPoint>: Periodic2Simplex, Equatable {
public typealias Scalar = Vector.Scalar

/// The line segment associated with this simplex.
public var lineSegment: LineSegment2<Vector>

Expand Down Expand Up @@ -47,6 +49,10 @@ public struct LineSegment2Simplex<Vector: Vector2FloatingPoint>: Periodic2Simple
return lineSegment.projectedNormalizedMagnitude(ratio)
}

public func isOnSurface(_ vector: Vector, toleranceSquared: Scalar) -> Bool {
lineSegment.distanceSquared(to: vector) < toleranceSquared
}

/// Clamps this simplex so its contained geometry is only present within a
/// given period range.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ extension Circle2Periodic: VisualizableGeometricType2 where Vector.Scalar: Custo
}
}

extension Compound2Periodic: VisualizableGeometricType2 where Vector.Scalar: CustomStringConvertible {
public func addVisualization2D(to printer: P5Printer, style: P5Printer.Style?, file: StaticString = #file, line: UInt = #line) {
printer.add(self, style: style, file: file, line: line)
}
}

extension Periodic2GeometrySimplex: VisualizableGeometricType2 where Vector.Scalar: CustomStringConvertible {
public func addVisualization2D(to printer: P5Printer, style: P5Printer.Style?, file: StaticString = #file, line: UInt = #line) {
printer.add(self, style: style, file: file, line: line)
Expand Down
42 changes: 42 additions & 0 deletions Sources/TestCommons/TestFixture/TestFixture+Periodics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,48 @@ public extension TestFixture {
}

public extension TestFixture.AssertionWrapperBase where T: Periodic2Geometry, T.Scalar: CustomStringConvertible {
@discardableResult
func assertContains(
_ point: T.Vector,
file: StaticString = #file,
line: UInt = #line
) -> Self where T.Vector: VisualizableGeometricType2 {
if !value.contains(point) {
visualize()

fixture.add(point)

fixture.failure(
"Expected geometry to contain point @ \(point)",
file: file,
line: line
)
}

return self
}

@discardableResult
func assertDoesNotContain(
_ point: T.Vector,
file: StaticString = #file,
line: UInt = #line
) -> Self where T.Vector: VisualizableGeometricType2 {
if value.contains(point) {
visualize()

fixture.add(point)

fixture.failure(
"Expected geometry to not contain point @ \(point)",
file: file,
line: line
)
}

return self
}

func assertSimplexes(
_ expected: [T.Simplex],
file: StaticString = #file,
Expand Down
45 changes: 45 additions & 0 deletions Tests/GeometriaPeriodicsTests/2D/Compound2PeriodicTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import XCTest
import Geometria
import TestCommons

@testable import GeometriaPeriodics

class Compound2PeriodicTests: XCTestCase {
typealias Sut = Compound2Periodic<Vector2D>

func testContains_onSimplexVertex() {
let sut = makeSut(lines: [
.init(x1: -150, y1: -150, x2: 150, y2: 0),
.init(x1: 150, y1: 0, x2: -150, y2: 150),
.init(x1: -150, y1: 150, x2: -150, y2: -150),
])

TestFixture.beginFixture { fixture in
fixture.assertions(on: sut)
.assertContains(.init(x: 0, y: -10))
.assertContains(.init(x: 0, y: 0))
.assertDoesNotContain(.init(x: -200, y: -10))
.assertDoesNotContain(.init(x: -200, y: 0))
}
}
}

// MARK: - Test internals

private func makeSut(
lines: [LineSegment2D]
) -> Compound2PeriodicTests.Sut {
let simplexes = lines.map { line in
Periodic2GeometrySimplex.lineSegment2(
.init(lineSegment: line, startPeriod: 0, endPeriod: 0)
)
}

return makeSut(normalizing: simplexes)
}

private func makeSut(
normalizing simplexes: [Periodic2GeometrySimplex<Vector2D>]
) -> Compound2PeriodicTests.Sut {
.init(normalizing: simplexes)
}
Loading

0 comments on commit 01a7df2

Please sign in to comment.