// GPCanvasView.swift // Composites the Metal viewport with a PencilCaptureOverlay on top. // Pencil strokes are projected into 3D and drawn by the renderer. import SwiftUI import simd struct GPCanvasView: View { let renderer: ViewportRenderer @State private var gpData = GPObjectData() @State private var activeStroke: GPStroke? @State private var brushColor: Color = .black @State private var brushWidth: Float = 0.05 var body: some View { GeometryReader { geo in ZStack { // 3D viewport (renders scene + finalized GP strokes) ViewportPane(renderer: renderer, label: "GP Canvas") // Pencil input overlay PencilCaptureOverlay( onStrokeBegan: { beginStroke() }, onSample: { sample in addSample(sample, viewportSize: geo.size) }, onStrokeEnded: { finalizeStroke() } ) .allowsHitTesting(true) // Drawing indicator if activeStroke != nil { VStack { HStack { Spacer() Label("Drawing", systemImage: "pencil.tip") .font(.caption2) .foregroundStyle(.orange) .padding(.horizontal, 10) .padding(.vertical, 4) .background(.black.opacity(0.6), in: Capsule()) .padding(8) } Spacer() } } } } } // MARK: - Stroke lifecycle private func beginStroke() { let stroke = GPStroke() stroke.color = colorToSIMD(brushColor) stroke.baseWidth = brushWidth activeStroke = stroke } private func addSample(_ sample: PencilSample, viewportSize: CGSize) { guard let stroke = activeStroke, let device = renderer.device else { return } let projector = StrokeProjector( viewMatrix: renderer.camera.viewMatrix, projectionMatrix: MathUtils.perspective( fovYRadians: .pi / 4, aspect: Float(viewportSize.width / viewportSize.height), near: 0.1, far: 500 ), viewportSize: SIMD2( Float(viewportSize.width), Float(viewportSize.height) ), drawingPlaneNormal: drawingPlaneNormal(), drawingPlanePoint: renderer.camera.target ) stroke.addSample(sample, projector: projector) // Live tessellation for preview stroke.tessellate(device: device) } private func finalizeStroke() { guard let stroke = activeStroke, stroke.points.count >= 2 else { activeStroke = nil return } // Commit to the active layer gpData.activeLayer?.strokes.append(stroke) activeStroke = nil } // MARK: - Helpers /// Drawing plane faces the camera (billboard mode). private func drawingPlaneNormal() -> SIMD3 { normalize(renderer.camera.eye - renderer.camera.target) } private func colorToSIMD(_ color: Color) -> SIMD4 { var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 UIColor(color).getRed(&r, green: &g, blue: &b, alpha: &a) return SIMD4(Float(r), Float(g), Float(b), Float(a)) } }