Skip to content

Instantly share code, notes, and snippets.

@alladinian
Created May 13, 2024 11:26
Show Gist options
  • Save alladinian/e7a15006ba83385bb90055830b77737d to your computer and use it in GitHub Desktop.
Save alladinian/e7a15006ba83385bb90055830b77737d to your computer and use it in GitHub Desktop.
//
// TrimmingExample.swift
//
// Created by Vasilis Akoinoglou on 13/5/24.
//
import SwiftUI
struct Line: Identifiable {
let id = UUID()
var segments: [Segment]
struct Segment: Identifiable {
let id = UUID()
var p1: CGPoint
var p2: CGPoint
var points: [CGPoint] {
[p1, p2]
}
}
}
struct TrimmingExample: View {
@State
private var lines: [Line] = []
@State
private var placeholderSegment: Line.Segment?
var body: some View {
VStack {
ZStack {
if let placeholderSegment {
Path { path in
path.addLines(placeholderSegment.points)
}
.stroke(lineWidth: 2)
}
ForEach(lines.flatMap(\.segments)) { segment in
// Segments
Path { path in
path.addLines(segment.points)
}
.stroke(lineWidth: 4)
.onTapGesture {
for (index, line) in lines.enumerated() {
for (segmentIndex, _segment) in line.segments.enumerated() {
if _segment.id == segment.id {
lines[index].segments.remove(at: segmentIndex)
if lines[index].segments.isEmpty {
lines.remove(at: index)
}
break
}
}
}
}
// Vertices
Path { path in
for point in segment.points {
path.addEllipse(in: CGRect(x: point.x - 3, y: point.y - 3, width: 6, height: 6))
}
}
.fill(.blue)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.gesture(dragGesture)
Text(lines.count.description + ", " + lines.flatMap(\.segments).count.description)
}
}
private var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
if placeholderSegment == nil {
placeholderSegment = Line.Segment(p1: value.location, p2: value.location)
}
placeholderSegment?.p2 = value.location
}
.onEnded { value in
if let placeholderSegment {
addSegment(placeholderSegment)
}
placeholderSegment = nil
}
}
private func addSegment(_ proposedSegment: Line.Segment) {
var proposedLine = Line(segments: [proposedSegment])
// For every line
outerLoop: for (lineIndex, line) in lines.enumerated() {
// and every segment
for (segmentIndex, segment) in line.segments.enumerated() {
for (proposedSegmentIndex, proposedSegment) in proposedLine.segments.enumerated() {
if let intersection = segment.intersectionPointForSegment(proposedSegment) {
// Split both segments
let split1 = segment.splitAt(intersection)
let split2 = proposedSegment.splitAt(intersection)
// Remove original segments
lines[lineIndex].segments.remove(at: segmentIndex)
proposedLine.segments.remove(at: proposedSegmentIndex)
// Add new segments
lines[lineIndex].segments.append(contentsOf: split1)
proposedLine.segments.append(contentsOf: split2)
continue outerLoop
}
}
}
}
lines.append(proposedLine)
}
}
// https://stackoverflow.com/questions/15690103/intersection-between-two-lines-in-coordinates
extension Line.Segment {
func intersectionPointForSegment(_ otherSegment: Line.Segment) -> CGPoint? {
let (p1, p2) = (self.p1, self.p2)
let (p3, p4) = (otherSegment.p1, otherSegment.p2)
let d: CGFloat = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x)
if (d == 0) {
return nil // parallel lines
}
let u: CGFloat = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / d
let v: CGFloat = ((p3.x - p1.x) * (p2.y - p1.y) - (p3.y - p1.y) * (p2.x - p1.x)) / d
if (u < 0.0 || u > 1.0) {
return nil // intersection point not between p1 and p2
}
if (v < 0.0 || v > 1.0) {
return nil // intersection point not between p3 and p4
}
var intersection: CGPoint = .zero
intersection.x = p1.x + u * (p2.x - p1.x)
intersection.y = p1.y + u * (p2.y - p1.y)
return intersection
}
func splitAt(_ point: CGPoint) -> [Line.Segment] {
// Split a segment into two
let (p1, p2) = (self.p1, point)
let (p3, p4) = (point, self.p2)
return [Line.Segment(p1: p1, p2: p2), Line.Segment(p1: p3, p2: p4)]
}
}
#Preview {
TrimmingExample()
.frame(width: 800, height: 600)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment