// PrimitiveGenerator.swift // Procedural mesh generation for all primitive types. import simd /// Output from a primitive generator — ready for GPU upload. struct PrimitiveMeshData { let vertices: [MeshVertex] // Solid triangles let wireVertices: [MeshVertex] // Wireframe lines let faceCount: Int let edgeCount: Int let boundingBoxMin: SIMD3 let boundingBoxMax: SIMD3 } enum PrimitiveGenerator { // MARK: - Cube static func cube( size: Float = 2.0, color: SIMD4 = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { let h = size * 0.5 let positions: [SIMD3] = [ .init(-h, -h, h), .init( h, -h, h), .init( h, h, h), .init(-h, h, h), .init( h, -h, -h), .init(-h, -h, -h), .init(-h, h, -h), .init( h, h, -h), ] let faces: [(indices: [Int], normal: SIMD3)] = [ ([0,1,2,3], .init( 0, 0, 1)), // front ([4,5,6,7], .init( 0, 0,-1)), // back ([3,2,7,6], .init( 0, 1, 0)), // top ([5,4,1,0], .init( 0,-1, 0)), // bottom ([1,4,7,2], .init( 1, 0, 0)), // right ([5,0,3,6], .init(-1, 0, 0)), // left ] var verts: [MeshVertex] = [] for face in faces { let (idx, n) = (face.indices, face.normal) // Two triangles per quad for ti in [(0,1,2), (0,2,3)] { for i in [ti.0, ti.1, ti.2] { verts.append(MeshVertex(position: positions[idx[i]], normal: n, color: color)) } } } let edges: [(Int,Int)] = [ (0,1),(1,2),(2,3),(3,0), (4,5),(5,6),(6,7),(7,4), (0,5),(1,4),(2,7),(3,6), ] let wireColor = SIMD4(0, 0, 0, 0.4) var wireVerts: [MeshVertex] = [] for (a, b) in edges { wireVerts.append(.init(position: positions[a], normal: .zero, color: wireColor)) wireVerts.append(.init(position: positions[b], normal: .zero, color: wireColor)) } return PrimitiveMeshData( vertices: verts, wireVertices: wireVerts, faceCount: 6, edgeCount: 12, boundingBoxMin: .init(repeating: -h), boundingBoxMax: .init(repeating: h) ) } // MARK: - Plane static func plane( size: Float = 2.0, color: SIMD4 = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { let h = size * 0.5 let n: SIMD3 = .init(0, 1, 0) let p: [SIMD3] = [ .init(-h, 0, -h), .init( h, 0, -h), .init( h, 0, h), .init(-h, 0, h), ] let verts: [MeshVertex] = [ .init(position: p[0], normal: n, color: color), .init(position: p[1], normal: n, color: color), .init(position: p[2], normal: n, color: color), .init(position: p[0], normal: n, color: color), .init(position: p[2], normal: n, color: color), .init(position: p[3], normal: n, color: color), ] let wc = SIMD4(0, 0, 0, 0.4) let wireVerts: [MeshVertex] = [ .init(position: p[0], normal: .zero, color: wc), .init(position: p[1], normal: .zero, color: wc), .init(position: p[1], normal: .zero, color: wc), .init(position: p[2], normal: .zero, color: wc), .init(position: p[2], normal: .zero, color: wc), .init(position: p[3], normal: .zero, color: wc), .init(position: p[3], normal: .zero, color: wc), .init(position: p[0], normal: .zero, color: wc), ] return PrimitiveMeshData( vertices: verts, wireVertices: wireVerts, faceCount: 1, edgeCount: 4, boundingBoxMin: .init(-h, 0, -h), boundingBoxMax: .init(h, 0, h) ) } // MARK: - UV Sphere static func uvSphere( radius: Float = 1.0, segments: Int = 32, rings: Int = 16, color: SIMD4 = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { var verts: [MeshVertex] = [] var wireVerts: [MeshVertex] = [] let wc = SIMD4(0, 0, 0, 0.3) // Generate grid of positions var grid: [[SIMD3]] = [] for ring in 0...rings { var row: [SIMD3] = [] let phi = Float.pi * Float(ring) / Float(rings) for seg in 0...segments { let theta = 2.0 * Float.pi * Float(seg) / Float(segments) let x = radius * sinf(phi) * cosf(theta) let y = radius * cosf(phi) let z = radius * sinf(phi) * sinf(theta) row.append(.init(x, y, z)) } grid.append(row) } // Triangulate for ring in 0.. = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { let halfH = height * 0.5 var verts: [MeshVertex] = [] var wireVerts: [MeshVertex] = [] let wc = SIMD4(0, 0, 0, 0.3) for i in 0..(radius * c0, -halfH, radius * s0) let br = SIMD3(radius * c1, -halfH, radius * s1) let tl = SIMD3(radius * c0, halfH, radius * s0) let tr = SIMD3(radius * c1, halfH, radius * s1) let n0 = normalize(SIMD3(c0, 0, s0)) let n1 = normalize(SIMD3(c1, 0, s1)) // Side quad (2 tris) verts.append(.init(position: bl, normal: n0, color: color)) verts.append(.init(position: br, normal: n1, color: color)) verts.append(.init(position: tl, normal: n0, color: color)) verts.append(.init(position: tl, normal: n0, color: color)) verts.append(.init(position: br, normal: n1, color: color)) verts.append(.init(position: tr, normal: n1, color: color)) // Top cap tri let topN: SIMD3 = .init(0, 1, 0) verts.append(.init(position: .init(0, halfH, 0), normal: topN, color: color)) verts.append(.init(position: tl, normal: topN, color: color)) verts.append(.init(position: tr, normal: topN, color: color)) // Bottom cap tri let botN: SIMD3 = .init(0, -1, 0) verts.append(.init(position: .init(0, -halfH, 0), normal: botN, color: color)) verts.append(.init(position: br, normal: botN, color: color)) verts.append(.init(position: bl, normal: botN, color: color)) // Wireframe wireVerts.append(.init(position: tl, normal: .zero, color: wc)) wireVerts.append(.init(position: tr, normal: .zero, color: wc)) wireVerts.append(.init(position: bl, normal: .zero, color: wc)) wireVerts.append(.init(position: br, normal: .zero, color: wc)) wireVerts.append(.init(position: bl, normal: .zero, color: wc)) wireVerts.append(.init(position: tl, normal: .zero, color: wc)) } return PrimitiveMeshData( vertices: verts, wireVertices: wireVerts, faceCount: segments * 3, edgeCount: segments * 3, boundingBoxMin: .init(-radius, -halfH, -radius), boundingBoxMax: .init(radius, halfH, radius) ) } // MARK: - Torus static func torus( majorRadius: Float = 1.0, minorRadius: Float = 0.3, majorSegments: Int = 48, minorSegments: Int = 12, color: SIMD4 = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { var verts: [MeshVertex] = [] var wireVerts: [MeshVertex] = [] let wc = SIMD4(0, 0, 0, 0.3) func torusPoint(_ u: Float, _ v: Float) -> SIMD3 { let cu = cosf(u), su = sinf(u) let cv = cosf(v), sv = sinf(v) return SIMD3( (majorRadius + minorRadius * cv) * cu, minorRadius * sv, (majorRadius + minorRadius * cv) * su ) } func torusNormal(_ u: Float, _ v: Float) -> SIMD3 { let cu = cosf(u), su = sinf(u) let cv = cosf(v), sv = sinf(v) return normalize(SIMD3(cv * cu, sv, cv * su)) } for i in 0.. = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { let halfH = height * 0.5 let apex = SIMD3(0, halfH, 0) var verts: [MeshVertex] = [] var wireVerts: [MeshVertex] = [] let wc = SIMD4(0, 0, 0, 0.3) for i in 0..(radius * cosf(a0), -halfH, radius * sinf(a0)) let b1 = SIMD3(radius * cosf(a1), -halfH, radius * sinf(a1)) // Side normal (approximation) let mid = normalize((b0 + b1) * 0.5 - SIMD3(0, -halfH, 0)) let sideN = normalize(SIMD3(mid.x, radius / height, mid.z)) // Side tri verts.append(.init(position: apex, normal: sideN, color: color)) verts.append(.init(position: b0, normal: sideN, color: color)) verts.append(.init(position: b1, normal: sideN, color: color)) // Bottom cap let botN = SIMD3(0, -1, 0) verts.append(.init(position: .init(0, -halfH, 0), normal: botN, color: color)) verts.append(.init(position: b1, normal: botN, color: color)) verts.append(.init(position: b0, normal: botN, color: color)) // Wire wireVerts.append(.init(position: b0, normal: .zero, color: wc)) wireVerts.append(.init(position: b1, normal: .zero, color: wc)) wireVerts.append(.init(position: apex, normal: .zero, color: wc)) wireVerts.append(.init(position: b0, normal: .zero, color: wc)) } return PrimitiveMeshData( vertices: verts, wireVertices: wireVerts, faceCount: segments * 2, edgeCount: segments * 2, boundingBoxMin: .init(-radius, -halfH, -radius), boundingBoxMax: .init(radius, halfH, radius) ) } // MARK: - Icosphere static func icosphere( radius: Float = 1.0, subdivisions: Int = 2, color: SIMD4 = SIMD4(0.63, 0.63, 0.63, 1) ) -> PrimitiveMeshData { // Start with icosahedron let t = (1.0 + sqrtf(5.0)) * 0.5 var positions: [SIMD3] = [ normalize(.init(-1, t, 0)), normalize(.init(1, t, 0)), normalize(.init(-1,-t, 0)), normalize(.init(1,-t, 0)), normalize(.init(0,-1, t)), normalize(.init(0, 1, t)), normalize(.init(0,-1,-t)), normalize(.init(0, 1,-t)), normalize(.init(t, 0,-1)), normalize(.init(t, 0, 1)), normalize(.init(-t,0,-1)), normalize(.init(-t,0, 1)), ].map { $0 * radius } var triangles: [(Int,Int,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), ] // Subdivide var midpointCache: [UInt64: Int] = [:] func midpoint(_ a: Int, _ b: Int) -> Int { let key = UInt64(min(a,b)) << 32 | UInt64(max(a,b)) if let cached = midpointCache[key] { return cached } let mid = normalize((positions[a] + positions[b]) * 0.5) * radius positions.append(mid) let idx = positions.count - 1 midpointCache[key] = idx return idx } for _ in 0..(0, 0, 0, 0.3) for (a,b,c) in triangles { let pa = positions[a], pb = positions[b], pc = positions[c] let na = normalize(pa), nb = normalize(pb), nc = normalize(pc) verts.append(.init(position: pa, normal: na, color: color)) verts.append(.init(position: pb, normal: nb, color: color)) verts.append(.init(position: pc, normal: nc, color: color)) wireVerts.append(.init(position: pa, normal: .zero, color: wc)) wireVerts.append(.init(position: pb, normal: .zero, color: wc)) wireVerts.append(.init(position: pb, normal: .zero, color: wc)) wireVerts.append(.init(position: pc, normal: .zero, color: wc)) wireVerts.append(.init(position: pc, normal: .zero, color: wc)) wireVerts.append(.init(position: pa, normal: .zero, color: wc)) } return PrimitiveMeshData( vertices: verts, wireVertices: wireVerts, faceCount: triangles.count, edgeCount: triangles.count * 3 / 2, boundingBoxMin: .init(repeating: -radius), boundingBoxMax: .init(repeating: radius) ) } // MARK: - Camera wireframe icon static func cameraWireframe() -> PrimitiveMeshData { let wc = SIMD4(0.4, 0.4, 0.4, 1.0) // Frustum shape let body: [SIMD3] = [ .init(-0.5, -0.3, 0.5), .init(0.5, -0.3, 0.5), .init(0.5, 0.3, 0.5), .init(-0.5, 0.3, 0.5), .init(-0.3, -0.2, -0.5), .init(0.3, -0.2, -0.5), .init(0.3, 0.2, -0.5), .init(-0.3, 0.2, -0.5), ] let edges: [(Int,Int)] = [ (0,1),(1,2),(2,3),(3,0), (4,5),(5,6),(6,7),(7,4), (0,4),(1,5),(2,6),(3,7), ] var wireVerts: [MeshVertex] = [] for (a,b) in edges { wireVerts.append(.init(position: body[a], normal: .zero, color: wc)) wireVerts.append(.init(position: body[b], normal: .zero, color: wc)) } // Up arrow wireVerts.append(.init(position: .init(-0.3, 0.3, 0.5), normal: .zero, color: wc)) wireVerts.append(.init(position: .init(0, 0.55, 0.5), normal: .zero, color: wc)) wireVerts.append(.init(position: .init(0, 0.55, 0.5), normal: .zero, color: wc)) wireVerts.append(.init(position: .init(0.3, 0.3, 0.5), normal: .zero, color: wc)) return PrimitiveMeshData( vertices: [], wireVertices: wireVerts, faceCount: 0, edgeCount: edges.count + 2, boundingBoxMin: .init(-0.5, -0.3, -0.5), boundingBoxMax: .init(0.5, 0.55, 0.5) ) } // MARK: - Light point icon static func lightIcon() -> PrimitiveMeshData { let lc = SIMD4(1.0, 0.85, 0.4, 1.0) var wireVerts: [MeshVertex] = [] let r: Float = 0.3 let segments = 16 // Circle on XY for i in 0..(r * cosf(a), r * sinf(a), 0) let outer = SIMD3((r + rayLen) * cosf(a), (r + rayLen) * sinf(a), 0) wireVerts.append(.init(position: inner, normal: .zero, color: lc)) wireVerts.append(.init(position: outer, normal: .zero, color: lc)) } return PrimitiveMeshData( vertices: [], wireVertices: wireVerts, faceCount: 0, edgeCount: segments * 2 + 8, boundingBoxMin: .init(repeating: -(r + rayLen)), boundingBoxMax: .init(repeating: r + rayLen) ) } }