Bezier

Einige Funktionen um Kubische Bezier-Kurven. Eine Bezier-Kurve hat zwei Richtungs- und zwei Endpunkte. Sie wird repräsentiert durch eine Liste von acht Fließkommazahlen. Schnittpunkte werden repräsentiert durch eine Fließkommazahl zwischen 0 und 1.

Einrichtung

Anwendung

package require Bezier
lädt das Paket, macht dieses Prozeduren verfügbar:
bezier bbox (Kurve)
übergibt Koordinaten des umgebenden Rechtecks
bezier tangens2 (Kurve1) (Kurve2)
ermittelt die gemeinsam verbindende Tangente zweier Kurven, übergibt die Berührpunkte der Kurven
bezier segment (Kurve) (Start) (Ende)
übergibt das Segment im Bereich Start, Ende
bezier cut (Kurve1) (Kurve2)
übergibt ein Schnittpunktpaar der Kurven
bezier cuts (Kurve1) (Kurve2)
übergibt alle Schnittpunktpaare der Kurven als unsortierte Liste
bezier help
gibt eine kurze Übersicht über verfügbare Prozeduren

Anmerkungen


# file: Bezier-0.2.tm

package provide Bezier 0.2

namespace eval bezier namespace import\
 ::tcl::mathfunc::*\
 ::tcl::mathop::*

proc bezier::Extrema {a b c d} {
  # taken a, b, c, d as x-coords of bezier dots,
  # return list of fractions where the appropriate function has extrema
  set factor2 [expr {3.0 * (-$a + (3.0 * $b) + (-3.0 * $c) + $d)}]
  set factor1 [expr {2.0 * ((3.0 * $a) + (-6.0 * $b) + (3.0 * $c))}]
  set factor0 [expr {(-3.0 * $a) + (3.0 * $b)}]
  if {$factor2 != 0} then {
    set p [expr { $factor1 / $factor2 / 2 }]
    set q [expr { $factor0 / $factor2 }]
    set p2q [expr { ($p * $p) - $q }]
    if {$p2q > 0} then {
      set sqrtp2q [expr {sqrt($p2q)}]
      list [expr { -$p - $sqrtp2q }] [expr { -$p + $sqrtp2q }] 
    } elseif {$p2q == 0} then {
      expr {-$p}
    }
  } elseif { $factor1 != 0 } then {
    expr { -$factor0 / $factor1 }
  }
}

proc bezier::bbox coords {
  lassign $coords ax ay bx by cx cy dx dy
  lappend allX $ax $dx
  lappend allY $ay $dy
  foreach frac [Extrema $ax $bx $cx $dx] {
    if {$frac > 0.0 && $frac < 1.0} then {
      foreach {x y} [At $coords $frac] {
        lappend allX $x
        lappend allY $y
      }
    }
  }
  foreach frac [Extrema $ay $by $cy $dy] {
    if {$frac > 0.0 && $frac < 1.0} then {
      foreach {x y} [At $coords $frac] {
        lappend allX $x
        lappend allY $y
      }
    }
  }
  list [min {*}$allX] [min {*}$allY] [max {*}$allX] [max {*}$allY]
}

proc bezier::Rotate {x y phi {cX 0.0} {cY 0.0}} {
  # coords x, y rotated by phi around cX, cY
  set c [cos $phi]
  set s [sin $phi]
  list\
    [expr {($x - $cX) * $c - ($y - $cY) * $s + $cX}]\
    [expr {($y - $cY) * $c + ($x - $cX) * $s + $cY}]
}

proc bezier::RotateCoords {coords phi {cX 200.0} {cY 150.0}} {
  concat {*}[lmap {x y} $coords {
      Rotate $x $y $phi $cX $cY
    }]
}

proc bezier::ScaleCoords {coords fx fy {cx 200} {cy 150}} {
  concat {*}[lmap {x y} $coords {
      list\
        [expr {($x - $cx) * $fx + $cx}]\
        [expr {($y - $cy) * $fy + $cy}]
    }]
}

# === updated ===

# outdated version returned rotated copies of beziers
# new version TangensAngle below returns just the angle

proc bezier::AngleOfTwoCurves {curve1 curve2 {side top}} {
  # angle of bounding boxes on top, bottom, or alternating
  switch -exact -- $side {
    top {
      lassign [bbox $curve1] x0  y0  -  -
      lassign [bbox $curve2]  -  y1  x1 -
    }
    bottom {
      lassign [bbox $curve1] x0   -   -  y0
      lassign [bbox $curve2]  -   -  x1  y1
    }
    topBottom {
      lassign [bbox $curve1] x0  y0  -  -
      lassign [bbox $curve2]  -   -  x1  y1
    }
    bottomTop {
      lassign [bbox $curve1] x0   -   -  y0
      lassign [bbox $curve2]  -  y1  x1 -
    }
  }
  atan2 [- $y1 $y0] [- $x1 $x0]
}

proc bezier::CurveCenter coords {
  # center of bounding box
  lassign [bbox $coords] x0 y0 x1 y1
  list [/ [+ $x0 $x1] 2.0] [/ [+ $y0 $y1] 2.0]
}

proc bezier::CurveIsArc curve {
  # return true if bent like Arc
  # return false if bent like Bowl
  lassign $curve x0 y0 - - - - x1 y1
  if {$x0 > $x1} then {
    lassign "$x0 $y0 $x1 $y1" x1 y1 x0 y0
  }
  set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
  set c1 [RotateCoords $curve [- $phi]]
  lassign $c1 - y2 - y3
  expr {
    $y3 < $y2 ? yes : no
  }
}

proc bezier::TangensAngle {curve1 curve2 {how auto}} {
  # return angle of common tangential line
  #
  # calc left/right
  #
  lassign [CurveCenter $curve1] x0 y0
  lassign [CurveCenter $curve2] x1 y1
  set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
  if {$phi < 0} then {
    lassign [list $curve1 $curve2] c2 c1
  } else {
    lassign [list $curve1 $curve2] c1 c2
  }
  #
  # adjust horizontally
  #
  lassign [CurveCenter $c1] x0 y0
  lassign [CurveCenter $c2] x1 y1
  set phi [atan2 [- $y1 $y0] [- $x1 $x0]]
  set c1 [RotateCoords $c1 [- $phi]]
  set c2 [RotateCoords $c2 [- $phi]]
  set result [- $phi]
  #
  # check if bowl or arc
  #
  if {$how ne "auto"} then {
    set side $how
  } else {
    if {[CurveIsArc $c1]} then {
      # $c1 yes
      if {[CurveIsArc $c2]} then {
        # $c1 yes, $c2 yes
        set side top
      } else {
        # $c1 yes, $c2 no
        set side topBottom
      }
    } else {
      # $c1 no
      if {[CurveIsArc $c2]} then {
        # $c1 no, $c2 yes
        set side bottomTop
      } else {
        # $c1 no, $c2 no
        set side bottom
      }
    }
  }
  for {set i 0} {$i < 10} {incr i} {
    set phi [AngleOfTwoCurves $c1 $c2 $side]
    set result [- $result $phi]
    set c1 [RotateCoords $c1 [- $phi]]
    set c2 [RotateCoords $c2 [- $phi]]
    if {abs($phi) < 0.0001} break
  }
  set result
}

# === 

proc bezier::tangens2 {left right} {
  # return fractions of beziers left, right
  # where common tangens touches
  set phi [TangensAngle $left $right]
  set left [RotateCoords $left $phi]
  set right [RotateCoords $right $phi]
  # extrema for $left
  lassign $left - y0 - y1 - y2 - y3
  set fracsLeft {}
  foreach frac [Extrema $y0 $y1 $y2 $y3] {
    if {$frac <= 1 && $frac >= 0} then {
      lappend fracsLeft $frac
    }
  }
  # extrema for $right
  lassign $right - y0 - y1 - y2 - y3
  set fracsRight {}
  foreach frac [Extrema $y0 $y1 $y2 $y3] {
    if {$frac <= 1 && $frac >= 0} then {
      lappend fracsRight $frac
    }
  }
  if {[llength $fracsLeft] == 1 && [llength $fracsRight] == 1} then {
    concat $fracsLeft $fracsRight
  }
}

proc bezier::Part {coords frac} {
  lassign $coords ax ay bx by cx cy dx dy
  #
  set abx [expr {$ax + ($bx - $ax) * $frac}]
  set bcx [expr {$bx + ($cx - $bx) * $frac}]
  set cdx [expr {$cx + ($dx - $cx) * $frac}]
  set abcx [expr {$abx + ($bcx - $abx) * $frac}]
  set bcdx [expr {$bcx + ($cdx - $bcx) * $frac}]
  set abcdx [expr {$abcx + ($bcdx - $abcx) * $frac}]
  #
  set aby [expr {$ay + ($by - $ay) * $frac}]
  set bcy [expr {$by + ($cy - $by) * $frac}]
  set cdy [expr {$cy + ($dy - $cy) * $frac}]
  set abcy [expr {$aby + ($bcy - $aby) * $frac}]
  set bcdy [expr {$bcy + ($cdy - $bcy) * $frac}]
  set abcdy [expr {$abcy + ($bcdy - $abcy) * $frac}]
  #
  list $ax $ay $abx $aby $abcx $abcy $abcdx $abcdy
}

proc bezier::segment {coords from to} {
  set frac0 $to
  set coords1 [Part $coords $frac0]
  set frac1 [expr {($to - $from) / $to}]
  lreverse [Part [lreverse $coords1] $frac1]
}

proc bezier::At {coords frac} {
  lrange [Part $coords $frac] end-1 end
}

proc bezier::Det2val det {
    # return numeric value of 2x2 determinante $det
    lassign $det l1 l2
    lassign $l1 a1 a2
    lassign $l2 b1 b2
    # - [* $a1 $b2] [* $a2 $b1]
    expr {double($a1 * $b2 - $a2 * $b1)}
}

proc bezier::lineCutLine {l0 l1 args} {
  # return list of fracs where lines $l0, $l1 cross each other
  # ---
  # 2 lines l0=P-Q, l1R-S
  # P  + u( Q - P ) = R  + v( S - R )
  # ---
  # px + u(qx - px) = rx + v(sx - rx)
  # py + u(qy - py) = ry + v(sy - ry)
  # ---
  # solve equation by u, v
  lassign $l0 px py qx qy
  lassign $l1 rx ry sx sy
  set x1 [expr {$qx - $px}]
  set x2 [expr {$rx - $sx}]
  set xr [expr {$rx - $px}]
  set y1 [expr {$qy - $py}]
  set y2 [expr {$ry - $sy}]
  set yr [expr {$ry - $py}]
  set det [list [list $x1 $x2] [list $y1 $y2]]
  set divisor [Det2val $det]
  if {$divisor != 0} then {
    set uDet [list [list $xr $x2] [list $yr $y2]]
    set uDetVal [Det2val $uDet]
    set u [expr {$uDetVal / $divisor}]
    set vDet [list [list $x1 $xr] [list $y1 $yr]]
    set vDetVal [Det2val $vDet]
    set v [expr {$vDetVal / $divisor}]
    if {0 < $u && $u < 1 && 0 < $v && $v < 1} then {
      list $u $v
    }
  }
}

proc bezier::Cross1 {bez1 bez2 {limit 1e-5}} {
  lassign $bez1 ax0 ay0 ax1 ay1 ax2 ay2 ax3 ay3
  lassign $bez2 bx0 by0 bx1 by1 bx2 by2 bx3 by3
  set ax "$ax0 $ax1 $ax2 $ax3"
  set ay "$ay0 $ay1 $ay2 $ay3"
  set bx "$bx0 $bx1 $bx2 $bx3"
  set by "$by0 $by1 $by2 $by3"
  if {
    [min {*}$ax] < [max {*}$bx] &&
    [min {*}$ay] < [max {*}$by] &&
    [max {*}$ax] > [min {*}$bx] &&
    [max {*}$ay] > [min {*}$by]
  } then {
    set width [- [max {*}$ax {*}$bx] [min {*}$ax {*}$bx]]
    set height [- [max {*}$ay {*}$by] [min {*}$ay {*}$by]]
    if {max($width, $height) < $limit} then {
      # only true crossing, no touching!
      lineCutLine [lreplace $bez1 2 end-2] [lreplace $bez2 2 end-2]
    } else {
      set bez1a [segment $bez1 0 0.5]
      set bez2a [segment $bez2 0 0.5]
      set bez1b [segment $bez1 0.5 1]
      set bez2b [segment $bez2 0.5 1]
      lassign [Cross1 $bez1a $bez2a $limit] frac1 frac2
      if {$frac1 ne ""} then {
        list [expr {$frac1 / 2.0}] [expr {$frac2 / 2.0}]
      } else {
        lassign [Cross1 $bez1a $bez2b $limit] frac1 frac2
        if {$frac1 ne ""} then {
          list [expr {$frac1 / 2.0}] [expr {$frac2 / 2.0 + 0.5}]
        } else {
          lassign [Cross1 $bez1b $bez2a $limit] frac1 frac2
          if {$frac1 ne ""} then {
            list [expr {$frac1 / 2.0 + 0.5}] [expr {$frac2 / 2.0}]
          } else {
            lassign [Cross1 $bez1b $bez2b $limit] frac1 frac2
            if {$frac1 ne ""} then {
              list [expr {$frac1 / 2.0 + 0.5}] [expr {$frac2 / 2.0 + 0.5}]
            }
          }
        }
      }
    }
  }
}

proc bezier::Nearby {x0 y0 x1 y1 {limit 1e-5}} {
  expr {
    abs($x1 - $x0) <= $limit && abs($y1 - $y0) <= $limit
  }
}

proc bezier::BezierPosNearby {coords pos1 pos2 {limit 1e-5}} {
  Nearby {*}[At $coords $pos1] {*}[At $coords $pos2] $limit
}

proc bezier::CoordsReverse coords {
  concat {*}[lreverse [lmap {x y} $coords {list $x $y}]]
}

proc bezier::cut {bez1 bez2 {limit 1e-5} {tolerant no}} {
  if {
    !$tolerant &&
    ( [Nearby {*}[At $bez1 0.0] {*}[At $bez2 0.0] $limit] ||
      [Nearby {*}[At $bez1 1.0] {*}[At $bez2 1.0] $limit] )
  } then {
    lassign [cut [CoordsReverse $bez1] $bez2 $limit yes] frac1 frac2
    if {$frac2 ne {}} then {
      list [expr {1.0 - $frac1}] $frac2
    }
  } else {
    lassign [Cross1 $bez1 $bez2 $limit] frac1 frac2
    if {$frac2 ne {}} then {
      if {[BezierPosNearby $bez1 $frac1 0]} then {
        set frac1 0.0
      } elseif {[BezierPosNearby $bez1 $frac1 1.0]} then {
        set frac1 1.0
      }
      if {[BezierPosNearby $bez2 $frac2 0]} then {
        set frac2 0.0
      } elseif {[BezierPosNearby $bez2 $frac2 1.0]} then {
        set frac2 1.0
      }
      if {(0 < $frac1 && $frac1 < 1) || (0 < $frac2 && $frac2 < 1)} then {
        list $frac1 $frac2
      }
    }
  }
}

proc bezier::cuts {bez1 bez2 {limit 1e-5}} {
  lassign [cut $bez1 $bez2 $limit] frac1 frac2
  if {$frac2 ne {}} then {
    lappend result $frac1 $frac2
    #
    set seg1a [segment $bez1 0 $frac1]
    set seg2a [segment $bez2 0 $frac2]
    set seg1b [segment $bez1 $frac1 1.0]
    set seg2b [segment $bez2 $frac2 1.0]
    #
    foreach {f1 f2} [cuts $seg1a $seg2a $limit] {
      lappend result\
        [expr {$f1 * $frac1}]\
        [expr {$f2 * $frac2}]
    }
    foreach {f1 f2} [cuts $seg1a $seg2b $limit] {
      lappend result\
        [expr {$f1 * $frac1}]\
        [expr {$frac2 + $f2 * (1-$frac2)}]
    }
    foreach {f1 f2} [cuts $seg1b $seg2a $limit] {
      lappend result\
        [expr {$frac1 + $f1 * (1-$frac1)}]\
        [expr {$f2 * $frac2}]
    }
    foreach {f1 f2} [cuts $seg1b $seg2b $limit] {
      lappend result\
        [expr {$frac1 + $f1 * (1-$frac1)}]\
        [expr {$frac2 + $f2 * (1-$frac2)}]
    }
    set result
  }
}

proc bezier::help {} {
  foreach proc [namespace export] {
    set line "[namespace tail [namespace current]] $proc"
    foreach arg [info args $proc] {
      if {[info default $proc $arg def]} then {
        lappend line [list $arg $def]
      } else {
        lappend line $arg
      }
    }
    lappend result $line
  }
  join $result \n
}

namespace eval bezier {
  namespace export bbox tangens2 segment cut cuts help lineCutLine
  namespace ensemble create
}

29.3.2022