Die Community zu .NET und Classic VB.
Menü

VB 5/6-Tipp 0767: Polygon Clipping mit dem Algorithmus von Sutherland&Hodgman

 von 

Beschreibung 

Um Polygone miteinander verschneiden zu können gibt es verschiedene Algorithmen. Hier wird der einfache Algorithmus von Sutherland und Hodgman verwendet. Informationen zu dem Algorithmus gibt es auch bspw. bei [link url="http://de.wikipedia.org/wiki/Algorithmus_von_Sutherland-Hodgman"]de.Wikipedia[/link] und [link url="http://en.wikipedia.org/wiki/Sutherland-Hodgeman"]en.Wikipedia[/link], allerdings ist dort der verwendete Pseudocode nicht sehr aussagekräftig.
Der Algorithmus schneidet ein beliebiges Polygon (konkav oder konvex) an den Kanten eines konvexen Polygons, indem er an der Clipping-Kante den 2D-Raum in zwei Hälften teilt. Alles was dahinter liegt wird zum Ausgabepolygon hinzugefügt. Wenn Polygonsegmente durch diese Linie hindurchgehen, werden sie an der Kante abgeschnitten.

Schwierigkeitsgrad:

Schwierigkeitsgrad 2

Verwendete API-Aufrufe:

keine

Download:

Download des Beispielprojektes [7.74 KB]

'Dieser Quellcode stammt von http://www.activevb.de
'und kann frei verwendet werden. Für eventuelle Schäden
'wird nicht gehaftet.

'Um Fehler oder Fragen zu klären, nutzen Sie bitte unser Forum.
'Ansonsten viel Spaß und Erfolg mit diesem Source!

'------------- Anfang Projektdatei Projekt1.vbp -------------
'--------- Anfang Formular "Form1" alias Form1.frm  ---------
' Steuerelement: Kombinationsliste "Combo2"
' Steuerelement: Kombinationsliste "Combo1"
' Steuerelement: Schaltfläche "CmdClip"
' Steuerelement: Bildfeld-Steuerelement "Picture1"
' Steuerelement: Beschriftungsfeld "Label2"
' Steuerelement: Beschriftungsfeld "Label1"
Option Explicit
Private m_Poly    As Polygon2F
'Um die Beispiele zu vereinfachen werden nur Vierecke (Quad2F) verwendet
Private m_Q()     As Quad2F
Private m_QIndex1 As Integer
Private m_QIndex2 As Integer

Private Sub Form_Load()
    
    Dim i As Long
    With Picture1
        .BackColor = vbWhite
        .AutoRedraw = True
    End With
    
    ReDim m_Q(0 To 5)
    'ein paar Beispiel-Vierecke anlegen
    m_Q(i) = MQuad2F.New_Quad2F(20#, 50#, 50#, 50#, 50#, 20#, 20#, 20#): i = i + 1
    m_Q(i) = MQuad2F.New_Quad2F(15#, 35#, 35#, 55#, 55#, 35#, 35#, 15#): i = i + 1
    m_Q(i) = MQuad2F.New_Quad2F(40#, 40#, 50#, 40#, 50#, 30#, 15#, 15#): i = i + 1
    m_Q(i) = MQuad2F.New_Quad2F(20#, 30#, 20#, 40#, 30#, 40#, 55#, 15#): i = i + 1
    m_Q(i) = MQuad2F.New_Quad2F(15#, 30#, 30#, 50#, 45#, 30#, 30#, 10#): i = i + 1
    m_Q(i) = MQuad2F.New_Quad2F(50#, 50#, 40#, 30#, 50#, 10#, 20#, 30#)
    
    For i = 0 To 6
        'das sechste Quad ist konkav und deshalb als Clipper nicht geeignet
        If i < 6 Then Call Me.Combo1.AddItem(CStr(i))
        Call Me.Combo2.AddItem(CStr(i))
    Next
    Combo1.ListIndex = 1
    Combo2.ListIndex = 2
    Call Draw
End Sub

Private Sub Combo1_Click()
    m_QIndex1 = Combo1.List(Combo1.ListIndex) - 1
    m_Poly = ErasePolygon2F
    Call Draw
End Sub
Private Sub Combo2_Click()
    m_QIndex2 = Combo2.List(Combo2.ListIndex) - 1
    m_Poly = ErasePolygon2F
    Call Draw
End Sub

Private Sub CmdClip_Click()
    If m_QIndex1 >= 0 And m_QIndex2 >= 0 Then
        If MGeo.SutherlandHodgmanClip(m_Q(m_QIndex1).P, m_Q(m_QIndex2).P, m_Poly) Then
            Call Draw
        End If
    End If
End Sub

Private Sub Form_Resize()
    Dim L As Single, T As Single, W As Single, H As Single, m As Single
    L = Picture1.Left:              T = Picture1.Top
    W = Me.ScaleWidth - L - 8 * 15: H = Me.ScaleHeight - T - 8 * 15
    m = IIf(W < H, W, H)
    If W > 0 And H > 0 Then Picture1.Move L, T, m, m
End Sub
Private Sub Picture1_Resize()
    Picture1.Scale (-10#, 85#)-(85#, -10#)
    Call Draw
End Sub

Private Sub Picture1_Click()
    CmdClip_Click
End Sub

Private Sub Draw()

    Picture1.Cls
    Picture1.ForeColor = vbBlack
    Picture1.DrawWidth = 1
    'Koordinatenachsen zeichnen
    With Picture1
        'X-Achse zeichnen
        Picture1.Line (.ScaleLeft + 5, 0)-(.ScaleWidth + .ScaleLeft - 5, 0)
        Picture1.Print "x"
        'Y-Achse zeichnen
        Picture1.Line (0, .ScaleHeight + .ScaleTop + 5)-(0, .ScaleTop - 5)
        Picture1.Print " y"
    End With
    'das Quad zeichnen das clippt
    Picture1.ForeColor = vbGreen
    If m_QIndex1 >= 0 Then
        Call MQuad2F.Draw(m_Q(m_QIndex1), Picture1)
    End If
    
    'das Quad zeichnen das geclippt wird
    Picture1.ForeColor = vbBlack
    If m_QIndex2 >= 0 Then
        Call MQuad2F.Draw(m_Q(m_QIndex2), Picture1)
    End If
    
    'das resultierende Polygon zeichnen
    Picture1.DrawWidth = 2
    Picture1.ForeColor = vbRed
    If m_Poly.Count > 0 Then
        Call MPolygon2F.Draw(m_Poly, Picture1)
    End If
    
End Sub

'---------- Ende Formular "Form1" alias Form1.frm  ----------
'------------ Anfang Modul "MGeo" alias MGeo.bas ------------
Option Explicit

'Geometrie-Typen
'für die Typen braucht man ein eigenes Modul,
'da sonst der Fehler auftritt: "gegenseitige Abhängigkeit von Modulen"
'2 = 2-dimensional; F = Float (Double)

'stellt einen 2D-Punkt dar
'wird oft auch mit Vertex bezeichnet
Public Type Point2F
    X As Double
    Y As Double
End Type

'stellt eine 2D-Strecke dar
Public Type LineSegment2F
    P(0 To 1) As Point2F
End Type

'stellt ein 2D-Viereck dar
Public Type Quad2F
    P(0 To 3) As Point2F
End Type
Public EraseQuad2F As Quad2F

'stellt ein 2D-Polygon dar
Public Type Polygon2F
    Count As Long
    P()   As Point2F
End Type
Public ErasePolygon2F As Polygon2F

Public Enum Position2
    RightPosition = -1
    Collinear = 0
    LeftPosition = 1
End Enum
Private Const eps As Double = 0.00000000000001

Public Function IntersectionPoint(ByVal x1 As Double, ByVal y1 As Double, _
                                  ByVal x2 As Double, ByVal y2 As Double, _
                                  ByVal x3 As Double, ByVal y3 As Double, _
                                  ByVal x4 As Double, ByVal y4 As Double) As Point2F
    'gibt den Schnittpunkt zweier Geraden G1, G2 zurück
    'G1 gegeben durch die Punkte G1:{{x1, y1}, {x2, y2}}
    'G2 gegeben durch die Punkte G2:{{x3, y3}, {x4, y4}}
    Dim Dx1 As Double, Dx2 As Double, Dx3 As Double
    Dim Dy1 As Double, Dy2 As Double, Dy3 As Double
    Dim det As Double, Ratio As Double
    
    Dx1 = x1 - x2
    Dx2 = x3 - x4
    Dx3 = x4 - x2
    Dy1 = y1 - y2
    Dy2 = y3 - y4
    Dy3 = y4 - y2
    
    det = (Dx2 * Dy1) - (Dy2 * Dx1)
    
    If Abs(det) <= eps Then
        det = (Dx2 * Dy3) - (Dy2 * Dx3)
        If Abs(det) <= eps Then
            IntersectionPoint.X = x3
            IntersectionPoint.Y = y3
            Exit Function
        End If
    End If
    Ratio = ((Dx1 * Dy3) - (Dy1 * Dx3)) / det
    IntersectionPoint.X = (Ratio * Dx2) + x4
    IntersectionPoint.Y = (Ratio * Dy2) + y4
End Function
Public Function Position2(ByVal x1 As Double, ByVal y1 As Double, _
                          ByVal x2 As Double, ByVal y2 As Double, _
                          ByVal Px As Double, ByVal Py As Double) As Position2
    'gibt zurück auf welcher Seite sich ein Punkt P relativ zu einer Geraden G befindet
    'Gerade: G:{{x1, y1}, {x2, y2}}
    'Punkt : P: {Px, Py}
    Dim det As Double
    'Determinante aus den 3 Punkten
    det = (x2 - x1) * (Py - y1) - (Px - x1) * (y2 - y1)
    If Abs(det) <= eps Then
        Position2 = Collinear
    ElseIf det > 0# Then
        Position2 = LeftPosition
    ElseIf det < 0# Then
        Position2 = RightPosition
    End If
End Function
Private Function IsInsideClippingRegion(P As Point2F, s As LineSegment2F) As Boolean
    'bei der Computer-Geometrie taucht immer wieder eine wichtige Frage auf:
    'wo befindet sich ein Punkt relativ zu einer Geraden bzw auf welcher Seite der Geraden
    'liegt ein Punkt. Hier ist ein kartesisches KO-System definiert.
    'Außerdem ist die Umfahrungsrichtung eines Polygons rechts herum definiert.
    
    'RightHandside oder Collinear ist inside
    'ist das KO-System anders definiert so muß hier angepasst werden
    IsInsideClippingRegion = _
        ( _
            MGeo.Position2(s.P(0).X, s.P(0).Y, _
                           s.P(1).X, s.P(1).Y, _
                           P.X, P.Y _
                           ) <> LeftPosition _
        )
End Function
Public Function SutherlandHodgmanClip(Clipper() As Point2F, _
                                      ClippingPoly() As Point2F, _
                                      PolyOut As Polygon2F _
                                      ) As Boolean
    'Clippt ein beliebiges Polygon (konkav oder konvex) an einem konvexen Polygon
    'gibt zurück ob eine Verschneidung geklappt hat
    'Clipper       ein konvexes Polygon das clippt
    'ClippingPoly  das Polygon das geclippt wird
    'PolyOut       das Polygon das zurückgegeben wird
    'konkav: nach innen  gewölbt  ], >, u, v
    'konvex: nach aussen gewölbt  0, o, O,
    
    'Achtung:
    'bevor die Funktion angewendet wird, muß sichergestellt werden
    'daß der Datenbereich der Arrays der Polygone Clipper und ClippingPoly
    'nur so groß ist wie die Anzahl Ihrer Punkte ist.
    '
    Dim ClippedPoly As Polygon2F
    Dim CS As LineSegment2F 'ein Segment aus Clipper
    Dim PS As LineSegment2F 'ein Segment aus ClippingPoly
    Dim SP As Point2F   'ein Schnittpunkt aus CS und PS
    Dim i  As Long, i0 As Long, i1 As Long
    Dim j  As Long
    Dim uC As Long
    Dim uP As Long
    uC = UBound(Clipper)
    With PolyOut
        .Count = UBound(ClippingPoly) + 1
        .P = ClippingPoly
    End With
    For j = 0 To uC
        If j < uC Then i0 = j: i1 = j + 1 Else i0 = uC: i1 = 0
        CS.P(0) = Clipper(i0):  CS.P(1) = Clipper(i1)
        'das alte Polygon löschen, alle Punkte werden neu hinzugefügt
        'das hat den Vorteil daß die Punkte später nicht sortiert werden müssen
        ClippedPoly = MGeo.ErasePolygon2F
        uP = PolyOut.Count - 1
        For i = 0 To uP
            If i < uP Then i0 = i: i1 = i + 1 Else i0 = uP: i1 = 0
            PS.P(0) = PolyOut.P(i0):  PS.P(1) = PolyOut.P(i1)
            If IsInsideClippingRegion(PS.P(0), CS) Then
                If IsInsideClippingRegion(PS.P(1), CS) Then
                    'beide drinnen, den zweiten hinzufügen
                    Call MPolygon2F.Add(ClippedPoly, PS.P(1))
                Else
                    'der erste drinnen, der zweite nicht drinnen,
                    'dann den Schnittpunkt berechnen
                    Call MPolygon2F.Add(ClippedPoly, MLineSegment2F.LineIntersection(CS, PS))
                End If
            Else
                'der erste nicht drinnen
                If IsInsideClippingRegion(PS.P(1), CS) Then
                    'der zweite drinnen,
                    'dann den Schnittpunkt berechnen
                    Call MPolygon2F.Add(ClippedPoly, MLineSegment2F.LineIntersection(CS, PS))
                    'und den zweiten hinzufügen
                    Call MPolygon2F.Add(ClippedPoly, PS.P(1))
                End If
            End If
        Next
        'das geclippte Polygon wird der Input für den nächsten Schritt
        PolyOut = ClippedPoly
    Next
    If PolyOut.Count > 0 Then SutherlandHodgmanClip = True
End Function
'------------- Ende Modul "MGeo" alias MGeo.bas -------------
'------ Anfang Modul "MPolygon2F" alias MPolygon2F.bas ------
Option Explicit
Private Const C_InitPolygonSize As Long = 16 '3

Public Sub Add(this As Polygon2F, newPoint2F As Point2F)
    'fügt einem Polygon einen Punkt hinzu
    'Achtung durch den Listen-Algorithmus hat das Polygon immer eine
    'größere Kapazität (UBound) als Punkte vorhanden sind (Count)
    With this
        If .Count = 0 Then
            ReDim .P(0 To C_InitPolygonSize)
        Else
            Dim u As Long: u = UBound(.P)
            If .Count > u Then
                ReDim Preserve .P(0 To 2 * u - 1)
            End If
        End If
        .P(.Count) = newPoint2F
        .Count = .Count + 1
    End With
End Sub

Public Sub Draw(this As Polygon2F, aPB As PictureBox)
    'zeichnet ein Polygon auf eine PictureBox, inkl. Punktnummern
    Dim i As Long
    With this
        For i = 0 To .Count - 1
            aPB.PSet (.P(i).X, .P(i).Y)
            aPB.Print CStr(i + 1)
            If i < .Count - 1 Then
                aPB.Line (.P(i).X, .P(i).Y)-(.P(i + 1).X, .P(i + 1).Y)
            Else
                aPB.Line (.P(i).X, .P(i).Y)-(.P(0).X, .P(0).Y)
            End If
        Next
    End With
End Sub
'------- Ende Modul "MPolygon2F" alias MPolygon2F.bas -------
'--- Anfang Modul "MLineSegment2F" alias MLineSegment2F.bas ---
Option Explicit

Public Function New_LineSegment2F(P1 As Point2F, P2 As Point2F) As LineSegment2F
    'erzeugt eine neues LineSegment (Strecke)
    With New_LineSegment2F
        .P(0) = P1
        .P(1) = P2
    End With
End Function

Public Function LineIntersection(S1 As LineSegment2F, S2 As LineSegment2F) As Point2F
    'berechnet den Schnittpunkt zweier Geraden gegeben durch die Strecken S1, S2
    LineIntersection = MGeo.IntersectionPoint( _
        S1.P(0).X, S1.P(0).Y, S1.P(1).X, S1.P(1).Y, _
        S2.P(0).X, S2.P(0).Y, S2.P(1).X, S2.P(1).Y)
End Function

Public Function ToString(this As LineSegment2F) As String
    'ganz hilfreich für Debug-Zwecke
    With this
        ToString = "{" & MPoint2F.ToString(.P(0)) & "; " & MPoint2F.ToString(.P(1)) & "}"
    End With
End Function

'--- Ende Modul "MLineSegment2F" alias MLineSegment2F.bas ---
'-------- Anfang Modul "MPoint2F" alias MPoint2F.bas --------
Option Explicit

Public Function New_Point2F(ByVal X As Double, _
                            ByVal Y As Double) As Point2F
    'erzeugt einen neuen Punkt
    With New_Point2F
        .X = X
        .Y = Y
    End With
End Function

Public Function ToString(this As Point2F) As String
    'ganz hilfreich für Debug-Zwecke
    With this
        ToString = "{" & CStr(.X) & ", " & CStr(.Y) & "}"
    End With
End Function

Public Function Position2(this As Point2F, LineSegment As LineSegment2F) As Position2
    'ermittelt die Position des Punktes relative zu einer Strecke
    With LineSegment
        Position2 = MGeo.Position2(.P(0).X, .P(0).Y, .P(1).X, .P(1).Y, this.X, this.Y)
    End With
End Function


'--------- Ende Modul "MPoint2F" alias MPoint2F.bas ---------
'--------- Anfang Modul "MQuad2F" alias MQuad2F.bas ---------
Option Explicit

Public Function New_Quad2F(ParamArray coords()) As Quad2F
    'erzeugt ein neues Viereck
    Dim i As Long, u As Long: u = UBound(coords)
    With New_Quad2F
        For i = 0 To ((u + 1) / 2) - 1
            .P(i) = MPoint2F.New_Point2F(coords(i * 2), coords(i * 2 + 1))
            If i = 7 Then Exit Function
        Next
    End With
End Function

Public Sub Draw(this As Quad2F, aPB As PictureBox)
    Dim i As Long
    'zeichnet das Viereck auf die PictureBox
    With this
        For i = 0 To 3
            aPB.PSet (.P(i).X, .P(i).Y)
            aPB.Print CStr(i + 1)
            If i < 3 Then
                aPB.Line (.P(i).X, .P(i).Y)-(.P(i + 1).X, .P(i + 1).Y)
            Else
                aPB.Line (.P(i).X, .P(i).Y)-(.P(0).X, .P(0).Y)
            End If
        Next
    End With
End Sub
'---------- Ende Modul "MQuad2F" alias MQuad2F.bas ----------
'-------------- Ende Projektdatei Projekt1.vbp --------------

Tipp-Kompatibilität:

Windows/VB-VersionWin32sWin95Win98WinMEWinNT4Win2000WinXP
VB4
VB5
VB6

Hat dieser Tipp auf Ihrem Betriebsystem und mit Ihrer VB-Version funktioniert?

Ja, funktioniert!

Nein, funktioniert nicht bei mir!

VB-Version:

Windows-Version:

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.