AABB obróconego łuku eliptycznego

Autor: Wojciech Muła
Dodany:25.03.2007

Łuk eliptyczny o środku (0, 0) dany jest parametrami:

img/elarc-bbox.png

Postać parametryczna nieobróconego łuku P(t) = (x, y) = (rx ⋅ cost, ry ⋅ sint), gdzie t∈[start, end]

Żeby wyznaczyć pudełko otaczające potrzebne są następujące punkty:

  1. P(start)
  2. P(end)
  3. punkty P(txmax) i P(tymax), tj punkty o maksymalnej współrzędnej x i y (punkty o minimalnych współrzędnych są odbite symetrycznie, przesunięte o pi)

Problem: jak wyznaczyć txmax i tymax?

Rozwiązanie: należy wyznaczyć kąty t punktów styczności obróconej elipsy ze stycznymi równoległymi do osi układu współrzędnych i wziąć tylko te, które leżą w przedziale startend.

Punkt styczności nieobróconej elipsy i prostej o zadanej normalnej
Prosta dana jest wzorem ax + by + c = 0, gdzie dana jest normalna N = [a, b], c jest nieważne. Promienie elipsy to odpowiednio rx i ry. Elipsa jest przekształcana w okrąg o promieniu rx (można oczywiście w okrąg o dowolnym promieniu, np. jednostkowy), tj. współczynniki skalowania wynoszą odpowiednio sx = 1 i sy = rx/ry. Przy skalowaniu wektory normalne są skalowane przez odwrotności współczynników, tj. N' = [a/sx, b/sy] = [a, brx/ry]. Stąd kąt odpowiadający punktowi styczności wynosi atan((brx/ry)/a).
Pudełko otaczające obróconej elipsy
Granice pudełka otaczającego wyznaczają proste równoległe do osi układu współrzędnych. Gdy kąt obrotu =0, wówczas normalne do tych prostych to Nx = [1, 0] i Ny = [0, 1]. Po obróceniu o kąt rot normalne są dane jako Nx = [sin(rot), cos(rot)] oraz Ny = [ − cos(rot), sin(rot)]. Wystarczy teraz wyznaczyć punkty styczności obróconych normalnych i nieobróconej elipsy, a to już wiadomo jak zrobić.

Ostatecznie funkcja wyznaczająca pudełko otaczające ma postać:

# wm, $Date: 2007-03-25 16:27:30 $, $Revision: 1.3 $

from math import sin, cos, pi, atan2

def ellipse_tp(a, b, dx, dy):
        return atan2(dy*b/a, dx)


def elliptical_arc_bbox(x0, y0, rx, ry, start=0.0, end=2*pi, rot=0.0):
        cr = cos(rot)
        sr = sin(rot)

        def P(a):
                x = cos(a)*rx
                y = sin(a)*ry
                return (x*cr - y*sr, x*sr + y*cr)
        
        def angle(a):
                if a < 0.0:
                        return a + 2*pi
                elif a > 2*pi:
                        return a - 2*pi 
                else:
                        return a

        X = [P(start)[0], P(end)[0]]
        Y = [P(start)[1], P(end)[1]]

        start = angle(start)
        end   = angle(end)
        a1    = ellipse_tp(rx, ry, sr, cr)
        a2    = ellipse_tp(rx, ry, -cr, sr)

        for a in map(angle, [a1, a2, a1+pi, a2+pi]):
                if start < end:
                        if start <= a <= end:
                                x, y = P(a)
                                X.append(x)
                                Y.append(y)
                else:
                        if not (end < a < start):
                                x, y = P(a)
                                X.append(x)
                                Y.append(y)

        return (x0 + min(X), y0 + min(Y)), (x0 + max(X), y0 + max(Y))

Jest również dostępne demko SVG+JavaScript, należy klikać.