From 70604a54f9b3192c2a8099bacdd17c4e5e40d3ec Mon Sep 17 00:00:00 2001 From: GeneralD Date: Mon, 15 Jun 2026 01:16:37 +0900 Subject: [PATCH] refactor(#248): extract geodesic geometry into dedicated non-view file Move Vertex3D and GeodesicGeometry (the Goldberg-polyhedron edge generation) out of OverlayContentView.swift into GeodesicGeometry.swift, keeping the *View.swift file to SwiftUI view types only per the repo's .coderabbit.yaml guideline ("Views do not own business logic"). The rendering constants GeodesicMetrics/GeodesicGold stay private in the view file since they are only consumed by the Canvas drawing. Geometry remains internal so GeodesicGeometryTests still reaches it via @testable import Views. No behavior change; all 942 tests pass. Addresses CodeRabbit nitpick on #286. --- Sources/VersionHandler/Resources/version.txt | 2 +- Sources/Views/Overlay/GeodesicGeometry.swift | 81 +++++++++++++++++++ .../Views/Overlay/OverlayContentView.swift | 80 ------------------ 3 files changed, 82 insertions(+), 81 deletions(-) create mode 100644 Sources/Views/Overlay/GeodesicGeometry.swift diff --git a/Sources/VersionHandler/Resources/version.txt b/Sources/VersionHandler/Resources/version.txt index edcfe40..b70ae75 100644 --- a/Sources/VersionHandler/Resources/version.txt +++ b/Sources/VersionHandler/Resources/version.txt @@ -1 +1 @@ -2.14.0 +2.14.1 diff --git a/Sources/Views/Overlay/GeodesicGeometry.swift b/Sources/Views/Overlay/GeodesicGeometry.swift new file mode 100644 index 0000000..5b3e7e6 --- /dev/null +++ b/Sources/Views/Overlay/GeodesicGeometry.swift @@ -0,0 +1,81 @@ +// MARK: - Geodesic sphere geometry + +/// A point on the unit sphere. Internal (not `private`) so the pure geometry in +/// `GeodesicGeometry` can be unit-tested via `@testable import Views`. +struct Vertex3D { + let x, y, z: Double +} + +/// Wireframe edges of a gold geodesic sphere. The geometry is the DUAL of a +/// once-subdivided icosphere: start from an icosahedron, subdivide each of its +/// 20 triangles into 4 (an 80-triangle "icosphere"), then connect the centroid +/// of every triangle to its edge-neighbours. The result is a Goldberg +/// polyhedron — 12 pentagons + 30 hexagons, a soccer ball with a few extra +/// faces. Geometry is independent of rotation, so it is built once and reused +/// for every frame. Internal (not `private`) so the edge generation can be +/// unit-tested via `@testable import Views`. +enum GeodesicGeometry { + static let edges: [(Vertex3D, Vertex3D)] = buildEdges() + + private static func normalized(_ v: Vertex3D) -> Vertex3D { + let length = (v.x * v.x + v.y * v.y + v.z * v.z).squareRoot() + return Vertex3D(x: v.x / length, y: v.y / length, z: v.z / length) + } + + /// Order-independent key for an undirected vertex pair. + private static func key(_ a: Int, _ b: Int) -> Int64 { + a < b ? (Int64(a) << 32) | Int64(b) : (Int64(b) << 32) | Int64(a) + } + + private static func buildEdges() -> [(Vertex3D, Vertex3D)] { + let t = (1 + 5.0.squareRoot()) / 2 // golden ratio + var verts: [Vertex3D] = [ + Vertex3D(x: -1, y: t, z: 0), Vertex3D(x: 1, y: t, z: 0), + Vertex3D(x: -1, y: -t, z: 0), Vertex3D(x: 1, y: -t, z: 0), + Vertex3D(x: 0, y: -1, z: t), Vertex3D(x: 0, y: 1, z: t), + Vertex3D(x: 0, y: -1, z: -t), Vertex3D(x: 0, y: 1, z: -t), + Vertex3D(x: t, y: 0, z: -1), Vertex3D(x: t, y: 0, z: 1), + Vertex3D(x: -t, y: 0, z: -1), Vertex3D(x: -t, y: 0, z: 1), + ].map(normalized) + let base: [[Int]] = [ + [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], + [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], + [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], + [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], + ] + var cache: [Int64: Int] = [:] + func mid(_ a: Int, _ b: Int) -> Int { + let k = key(a, b) + if let cached = cache[k] { return cached } + let va = verts[a] + let vb = verts[b] + verts.append( + normalized( + Vertex3D(x: (va.x + vb.x) / 2, y: (va.y + vb.y) / 2, z: (va.z + vb.z) / 2))) + cache[k] = verts.count - 1 + return verts.count - 1 + } + let faces = base.flatMap { f -> [[Int]] in + let ab = mid(f[0], f[1]) + let bc = mid(f[1], f[2]) + let ca = mid(f[2], f[0]) + return [[f[0], ab, ca], [f[1], bc, ab], [f[2], ca, bc], [ab, bc, ca]] + } + let centroids = faces.map { f -> Vertex3D in + let a = verts[f[0]] + let b = verts[f[1]] + let c = verts[f[2]] + return normalized( + Vertex3D(x: (a.x + b.x + c.x) / 3, y: (a.y + b.y + c.y) / 3, z: (a.z + b.z + c.z) / 3)) + } + var edgeFaces: [Int64: [Int]] = [:] + for (index, f) in faces.enumerated() { + for (u, v) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] { + edgeFaces[key(u, v), default: []].append(index) + } + } + return edgeFaces.values.compactMap { + $0.count == 2 ? (centroids[$0[0]], centroids[$0[1]]) : nil + } + } +} diff --git a/Sources/Views/Overlay/OverlayContentView.swift b/Sources/Views/Overlay/OverlayContentView.swift index c3d64ae..fbb451f 100644 --- a/Sources/Views/Overlay/OverlayContentView.swift +++ b/Sources/Views/Overlay/OverlayContentView.swift @@ -187,86 +187,6 @@ private enum GeodesicGold { static let mid = Color(red: 0.929, green: 0.812, blue: 0.451) } -/// A point on the unit sphere. Internal (not `private`) so the pure geometry in -/// `GeodesicGeometry` can be unit-tested via `@testable import Views`. -struct Vertex3D { - let x, y, z: Double -} - -/// Wireframe edges of a gold geodesic sphere. The geometry is the DUAL of a -/// once-subdivided icosphere: start from an icosahedron, subdivide each of its -/// 20 triangles into 4 (an 80-triangle "icosphere"), then connect the centroid -/// of every triangle to its edge-neighbours. The result is a Goldberg -/// polyhedron — 12 pentagons + 30 hexagons, a soccer ball with a few extra -/// faces. Geometry is independent of rotation, so it is built once and reused -/// for every frame. Internal (not `private`) so the edge generation can be -/// unit-tested via `@testable import Views`. -enum GeodesicGeometry { - static let edges: [(Vertex3D, Vertex3D)] = buildEdges() - - private static func normalized(_ v: Vertex3D) -> Vertex3D { - let length = (v.x * v.x + v.y * v.y + v.z * v.z).squareRoot() - return Vertex3D(x: v.x / length, y: v.y / length, z: v.z / length) - } - - /// Order-independent key for an undirected vertex pair. - private static func key(_ a: Int, _ b: Int) -> Int64 { - a < b ? (Int64(a) << 32) | Int64(b) : (Int64(b) << 32) | Int64(a) - } - - private static func buildEdges() -> [(Vertex3D, Vertex3D)] { - let t = (1 + 5.0.squareRoot()) / 2 // golden ratio - var verts: [Vertex3D] = [ - Vertex3D(x: -1, y: t, z: 0), Vertex3D(x: 1, y: t, z: 0), - Vertex3D(x: -1, y: -t, z: 0), Vertex3D(x: 1, y: -t, z: 0), - Vertex3D(x: 0, y: -1, z: t), Vertex3D(x: 0, y: 1, z: t), - Vertex3D(x: 0, y: -1, z: -t), Vertex3D(x: 0, y: 1, z: -t), - Vertex3D(x: t, y: 0, z: -1), Vertex3D(x: t, y: 0, z: 1), - Vertex3D(x: -t, y: 0, z: -1), Vertex3D(x: -t, y: 0, z: 1), - ].map(normalized) - let base: [[Int]] = [ - [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], - [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], - [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], - [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1], - ] - var cache: [Int64: Int] = [:] - func mid(_ a: Int, _ b: Int) -> Int { - let k = key(a, b) - if let cached = cache[k] { return cached } - let va = verts[a] - let vb = verts[b] - verts.append( - normalized( - Vertex3D(x: (va.x + vb.x) / 2, y: (va.y + vb.y) / 2, z: (va.z + vb.z) / 2))) - cache[k] = verts.count - 1 - return verts.count - 1 - } - let faces = base.flatMap { f -> [[Int]] in - let ab = mid(f[0], f[1]) - let bc = mid(f[1], f[2]) - let ca = mid(f[2], f[0]) - return [[f[0], ab, ca], [f[1], bc, ab], [f[2], ca, bc], [ab, bc, ca]] - } - let centroids = faces.map { f -> Vertex3D in - let a = verts[f[0]] - let b = verts[f[1]] - let c = verts[f[2]] - return normalized( - Vertex3D(x: (a.x + b.x + c.x) / 3, y: (a.y + b.y + c.y) / 3, z: (a.z + b.z + c.z) / 3)) - } - var edgeFaces: [Int64: [Int]] = [:] - for (index, f) in faces.enumerated() { - for (u, v) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] { - edgeFaces[key(u, v), default: []].append(index) - } - } - return edgeFaces.values.compactMap { - $0.count == 2 ? (centroids[$0[0]], centroids[$0[1]]) : nil - } - } -} - #if DEBUG #Preview("Overlay") { OverlayContentView(