| 1 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 2 |
#~ Imports |
|---|
| 3 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 4 |
|
|---|
| 5 |
import math |
|---|
| 6 |
import Numeric |
|---|
| 7 |
|
|---|
| 8 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 9 |
#~ Constants / Variables / Etc. |
|---|
| 10 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 11 |
|
|---|
| 12 |
_2pi = 2 * Numeric.pi |
|---|
| 13 |
|
|---|
| 14 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 15 |
#~ Definitions |
|---|
| 16 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 17 |
|
|---|
| 18 |
class CurveBase(object): |
|---|
| 19 |
_steps = 16 |
|---|
| 20 |
NumericType = Numeric.Float |
|---|
| 21 |
|
|---|
| 22 |
def __init__(self, steps=None): |
|---|
| 23 |
if steps is not None: |
|---|
| 24 |
self.SetSteps(steps) |
|---|
| 25 |
|
|---|
| 26 |
def GetSteps(self): |
|---|
| 27 |
return self._steps |
|---|
| 28 |
def SetSteps(self, steps): |
|---|
| 29 |
if steps != self._steps: |
|---|
| 30 |
self._steps = steps |
|---|
| 31 |
self._Invalidate() |
|---|
| 32 |
|
|---|
| 33 |
def asCurve(self): |
|---|
| 34 |
raise NotImplementedError |
|---|
| 35 |
|
|---|
| 36 |
def __iter__(self): |
|---|
| 37 |
return iter(self.asCurve()) |
|---|
| 38 |
|
|---|
| 39 |
def _Invalidate(self): |
|---|
| 40 |
pass |
|---|
| 41 |
|
|---|
| 42 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 43 |
#~ Bezier Based Curves |
|---|
| 44 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 45 |
|
|---|
| 46 |
class BezierBase(CurveBase): |
|---|
| 47 |
def __init__(self, *points, **kw): |
|---|
| 48 |
if points: |
|---|
| 49 |
self.SetControlPoints(*points) |
|---|
| 50 |
CurveBase.__init__(self, **kw) |
|---|
| 51 |
|
|---|
| 52 |
def _CreateUVector(self): |
|---|
| 53 |
return self._BuildUVector(Numeric.arrayrange(0., 1., 1./(self.GetSteps()-1), self.NumericType), True) |
|---|
| 54 |
|
|---|
| 55 |
def _BuildUVector(self, u, addend=False): |
|---|
| 56 |
result = [u, Numeric.ones(len(u), self.NumericType)] |
|---|
| 57 |
count = self.GetPolyDimension()+1 |
|---|
| 58 |
for i in xrange(2, count): |
|---|
| 59 |
result.insert(0, u*result[0]) |
|---|
| 60 |
if addend: |
|---|
| 61 |
result = Numeric.concatenate((result, Numeric.ones((count, 1), self.NumericType)), 1) |
|---|
| 62 |
return Numeric.transpose(result) |
|---|
| 63 |
|
|---|
| 64 |
def GetUVector(self): |
|---|
| 65 |
result = getattr(self, '_uvector', ()) |
|---|
| 66 |
if len(result) != self.GetSteps(): |
|---|
| 67 |
result = self._uvector = self._CreateUVector() |
|---|
| 68 |
return result |
|---|
| 69 |
|
|---|
| 70 |
def SetControlPoints(self, *points): |
|---|
| 71 |
assert len(points) == self.GetPolyDimension()+1 |
|---|
| 72 |
self.points = Numeric.asarray(points) |
|---|
| 73 |
def GetControlPoints(self): |
|---|
| 74 |
return self.points |
|---|
| 75 |
|
|---|
| 76 |
def CurveFromUVector(self, uvector): |
|---|
| 77 |
Ubezier = Numeric.dot(uvector, self.GetBezierMatrix()) |
|---|
| 78 |
return Numeric.dot(Ubezier, self.GetControlPoints()) |
|---|
| 79 |
|
|---|
| 80 |
def CurveAtPoints(self, *points): |
|---|
| 81 |
uvector = self._BuildUVector(Numeric.asarray(points, self.NumericType)) |
|---|
| 82 |
return self.CurveFromUVector(uvector) |
|---|
| 83 |
|
|---|
| 84 |
def CurveRange(self, start=0., stop=1., stepsize=None): |
|---|
| 85 |
stepsize = stepsize or 1./(self.GetSteps()-1) |
|---|
| 86 |
uvector = self._BuildUVector(Numeric.arrayrange(start, stop, stepsize, self.NumericType)) |
|---|
| 87 |
return self.CurveFromUVector(uvector) |
|---|
| 88 |
|
|---|
| 89 |
def asCurve(self): |
|---|
| 90 |
return self.CurveFromUVector(self.GetUVector()) |
|---|
| 91 |
|
|---|
| 92 |
def GetPolyDimension(self): |
|---|
| 93 |
return len(self.GetBezierMatrix())-1 |
|---|
| 94 |
def GetBezierMatrix(self): |
|---|
| 95 |
raise NotImplementedError |
|---|
| 96 |
|
|---|
| 97 |
def _Invalidate(self): |
|---|
| 98 |
try: delattr(self, '_uvector') |
|---|
| 99 |
except AttributeError: pass |
|---|
| 100 |
|
|---|
| 101 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 102 |
|
|---|
| 103 |
class LinearBezier(BezierBase): |
|---|
| 104 |
_matrix = Numeric.asarray([ |
|---|
| 105 |
[-1., 1.], |
|---|
| 106 |
[ 1., 0.]], |
|---|
| 107 |
BezierBase.NumericType) |
|---|
| 108 |
|
|---|
| 109 |
def GetBezierMatrix(self): |
|---|
| 110 |
return self._matrix |
|---|
| 111 |
|
|---|
| 112 |
def GetPolyDimension(self): |
|---|
| 113 |
return 1 |
|---|
| 114 |
|
|---|
| 115 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 116 |
|
|---|
| 117 |
class QuadraticBezier(BezierBase): |
|---|
| 118 |
_matrix = Numeric.asarray([ |
|---|
| 119 |
[ 1., -2., 1.], |
|---|
| 120 |
[-2., 2., 0.], |
|---|
| 121 |
[ 1., 0., 0.]], |
|---|
| 122 |
BezierBase.NumericType) |
|---|
| 123 |
|
|---|
| 124 |
def GetBezierMatrix(self): |
|---|
| 125 |
return self._matrix |
|---|
| 126 |
|
|---|
| 127 |
def GetPolyDimension(self): |
|---|
| 128 |
return 2 |
|---|
| 129 |
|
|---|
| 130 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 131 |
|
|---|
| 132 |
class CubicBezier(BezierBase): |
|---|
| 133 |
_matrix = Numeric.asarray([ |
|---|
| 134 |
[-1., 3., -3., 1.], |
|---|
| 135 |
[ 3., -6., 3., 0.], |
|---|
| 136 |
[-3., 3., 0., 0.], |
|---|
| 137 |
[ 1., 0., 0., 0.]], |
|---|
| 138 |
BezierBase.NumericType) |
|---|
| 139 |
|
|---|
| 140 |
def GetBezierMatrix(self): |
|---|
| 141 |
return self._matrix |
|---|
| 142 |
|
|---|
| 143 |
def GetPolyDimension(self): |
|---|
| 144 |
return 3 |
|---|
| 145 |
|
|---|
| 146 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 147 |
#~ Ellipse Based Curves |
|---|
| 148 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 149 |
|
|---|
| 150 |
class Ellipse(CurveBase): |
|---|
| 151 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 152 |
#~ Constants / Variables / Etc. |
|---|
| 153 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 154 |
|
|---|
| 155 |
center = (0., 0.) |
|---|
| 156 |
rx = 1. |
|---|
| 157 |
ry = 1. |
|---|
| 158 |
sweep = (0., _2pi) |
|---|
| 159 |
xrotation = 0 |
|---|
| 160 |
|
|---|
| 161 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 162 |
#~ Public Methods |
|---|
| 163 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 164 |
|
|---|
| 165 |
def __init__(self, center=(0., 0.), rx=1., ry=1., startangle=0, endangle=360, xrotation=0, inDegrees=True, **kw): |
|---|
| 166 |
CurveBase.__init__(self, **kw) |
|---|
| 167 |
self.SetCenter(center) |
|---|
| 168 |
self.SetRadii(rx, ry) |
|---|
| 169 |
if inDegrees: |
|---|
| 170 |
self.SetSweepAngles(startangle, endangle) |
|---|
| 171 |
self.SetXRotationAngle(xrotation) |
|---|
| 172 |
else: |
|---|
| 173 |
self.SetSweep(startangle, endangle) |
|---|
| 174 |
self.SetXRotation(xrotation) |
|---|
| 175 |
|
|---|
| 176 |
|
|---|
| 177 |
def fromArc(klass, fromxy, toxy, rx=1., ry=1., xrotation=0, largeArcFlag=False, sweepFlag=False, inDegrees=True, **kw): |
|---|
| 178 |
""" |
|---|
| 179 |
Derived from SVG Specification: |
|---|
| 180 |
http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes |
|---|
| 181 |
""" |
|---|
| 182 |
if inDegrees: |
|---|
| 183 |
xrotation = math.radians(xrotation) |
|---|
| 184 |
fromxy = Numeric.asarray(fromxy, klass.NumericType) |
|---|
| 185 |
toxy = Numeric.asarray(toxy, klass.NumericType) |
|---|
| 186 |
if xrotation: |
|---|
| 187 |
x, y = Numeric.dot(klass._RotationMatrix(-xrotation), 0.5*(fromxy-toxy)) |
|---|
| 188 |
else: |
|---|
| 189 |
x, y = 0.5*(fromxy-toxy) |
|---|
| 190 |
x2,y2 = x**2, y**2 |
|---|
| 191 |
|
|---|
| 192 |
rx2, ry2 = rx**2, ry**2 |
|---|
| 193 |
radiscale = x2/rx2 + y2/ry2 |
|---|
| 194 |
if radiscale > 1: |
|---|
| 195 |
# ellipse is not big enough... scale it |
|---|
| 196 |
radiscale = Numeric.sqrt(radiscale) |
|---|
| 197 |
rx, ry = radiscale*rx, radiscale*ry |
|---|
| 198 |
rx2, ry2 = rx**2, ry**2 |
|---|
| 199 |
|
|---|
| 200 |
tmp = rx2*y2 + ry2*x2 |
|---|
| 201 |
radicand = (rx2*ry2-tmp)/tmp |
|---|
| 202 |
if radicand > 0: |
|---|
| 203 |
sign = (largeArcFlag != sweepFlag) and 1 or -1 |
|---|
| 204 |
scale = sign * Numeric.sqrt(radicand) |
|---|
| 205 |
cxP, cyP = scale*y*rx/ry, scale*x*-ry/rx |
|---|
| 206 |
else: |
|---|
| 207 |
cxP, cyP = 0., 0. |
|---|
| 208 |
|
|---|
| 209 |
if xrotation: |
|---|
| 210 |
rot = klass._RotTranMatrix(xrotation, (0.5*(fromxy+toxy))) |
|---|
| 211 |
center = Numeric.dot(rot, Numeric.asarray([cxP, cyP, 1.], klass.NumericType)) |
|---|
| 212 |
else: |
|---|
| 213 |
center = Numeric.asarray([cxP, cyP]) + (0.5*(fromxy+toxy)) |
|---|
| 214 |
center = center[:2] |
|---|
| 215 |
|
|---|
| 216 |
startpt = ((x-cxP)/rx, (y-cyP)/ry) |
|---|
| 217 |
endpt = ((-x-cxP)/rx, (-y-cyP)/ry) |
|---|
| 218 |
startradians = klass._FindAngle((1., 0.), startpt) |
|---|
| 219 |
sweepradians = klass._FindAngle(startpt, endpt) % _2pi |
|---|
| 220 |
if sweepFlag: |
|---|
| 221 |
if sweepradians < 0: |
|---|
| 222 |
sweepradians += _2pi |
|---|
| 223 |
else: |
|---|
| 224 |
if sweepradians > 0: |
|---|
| 225 |
sweepradians -= _2pi |
|---|
| 226 |
|
|---|
| 227 |
endradians = startradians + sweepradians |
|---|
| 228 |
return klass(center, rx, ry, startradians, endradians, xrotation, inDegrees=False, **kw) |
|---|
| 229 |
fromArc = classmethod(fromArc) |
|---|
| 230 |
|
|---|
| 231 |
def SetCenter(self, center): |
|---|
| 232 |
self.center = center |
|---|
| 233 |
def GetCenter(self): |
|---|
| 234 |
return self.center |
|---|
| 235 |
|
|---|
| 236 |
def SetRadius(self, radius): |
|---|
| 237 |
self.SetRadii(radius, radius) |
|---|
| 238 |
def SetRadii(self, rx, ry): |
|---|
| 239 |
self.rx = rx |
|---|
| 240 |
self.ry = ry |
|---|
| 241 |
def GetRadii(self): |
|---|
| 242 |
return self.rx, self.ry |
|---|
| 243 |
|
|---|
| 244 |
def SetSweepAngles(self, startangle, endangle): |
|---|
| 245 |
self.SetSweep(math.radians(startangle), math.radians(endangle)) |
|---|
| 246 |
def SetSweep(self, startradians, endradians): |
|---|
| 247 |
self.sweep = [startradians, endradians] |
|---|
| 248 |
def GetSweep(self): |
|---|
| 249 |
return self.sweep |
|---|
| 250 |
|
|---|
| 251 |
def SetXRotationAngle(self, xrotationAngle): |
|---|
| 252 |
self.SetXRotation(math.radians(xrotationAngle)) |
|---|
| 253 |
def SetXRotation(self, xrotationRadians): |
|---|
| 254 |
self.xrotation = xrotationRadians |
|---|
| 255 |
def GetXRotation(self): |
|---|
| 256 |
return self.xrotation |
|---|
| 257 |
|
|---|
| 258 |
def asCurve(self): |
|---|
| 259 |
sweep = self._GetSweepVector() |
|---|
| 260 |
rx, ry = self.GetRadii() |
|---|
| 261 |
xrotation = self.GetXRotation() |
|---|
| 262 |
if xrotation: |
|---|
| 263 |
ellipse = Numeric.asarray([rx*Numeric.cos(sweep), ry*Numeric.sin(sweep), Numeric.ones(len(sweep), self.NumericType)], self.NumericType) |
|---|
| 264 |
rot = self._RotTranMatrix(xrotation, self.GetCenter()) |
|---|
| 265 |
ellipse = Numeric.dot(rot, ellipse) |
|---|
| 266 |
return Numeric.transpose(ellipse[:-1,:]) |
|---|
| 267 |
else: |
|---|
| 268 |
cx, cy = self.GetCenter() |
|---|
| 269 |
ellipse = Numeric.asarray([rx*Numeric.cos(sweep)+cx, ry*Numeric.sin(sweep)+cy], self.NumericType) |
|---|
| 270 |
return Numeric.transpose(ellipse) |
|---|
| 271 |
|
|---|
| 272 |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 273 |
|
|---|
| 274 |
def _GetSweepVector(self): |
|---|
| 275 |
sweep = getattr(self, '_sweepvector', ()) |
|---|
| 276 |
if len(sweep) != self.GetSteps(): |
|---|
| 277 |
sweep = self._sweepvector = self._CreateSweepVector() |
|---|
| 278 |
return sweep |
|---|
| 279 |
|
|---|
| 280 |
def _CreateSweepVector(self): |
|---|
| 281 |
steps = self.GetSteps()-1 # we append the last answer so it is always end-correct... even if the last step is a little off |
|---|
| 282 |
startrad, endrad = self.GetSweep() |
|---|
| 283 |
sweep = Numeric.arrayrange(startrad, endrad, (endrad-startrad)/steps, self.NumericType) |
|---|
| 284 |
sweep = Numeric.concatenate((sweep, [endrad]), 0) |
|---|
| 285 |
return sweep |
|---|
| 286 |
|
|---|
| 287 |
def _RotationMatrix(klass, radians): |
|---|
| 288 |
"""Count clockwise rotation""" |
|---|
| 289 |
cosr = Numeric.cos(radians) |
|---|
| 290 |
sinr = Numeric.sin(radians) |
|---|
| 291 |
return Numeric.asarray([[cosr, -sinr],[sinr, cosr]], klass.NumericType) |
|---|
| 292 |
_RotationMatrix = classmethod(_RotationMatrix) |
|---|
| 293 |
|
|---|
| 294 |
def _RotTranMatrix(klass, radians, (tx, ty)=(0.,0.)): |
|---|
| 295 |
"""Count clockwise rotation""" |
|---|
| 296 |
cosr = Numeric.cos(radians) |
|---|
| 297 |
sinr = Numeric.sin(radians) |
|---|
| 298 |
return Numeric.asarray([[cosr, -sinr, tx],[sinr, cosr, ty], [0., 0., 1.]], klass.NumericType) |
|---|
| 299 |
_RotTranMatrix = classmethod(_RotTranMatrix) |
|---|
| 300 |
|
|---|
| 301 |
def _FindAngle(klass, u, v): |
|---|
| 302 |
cosangle = Numeric.dot(u,v)/Numeric.sqrt(Numeric.dot(u,u)*Numeric.dot(v,v)) |
|---|
| 303 |
sign = Numeric.sign(u[0]*v[1]-u[1]*v[0]) or 1. # 0 is neither poistive or negative... but just make it positive for argument's sake... |
|---|
| 304 |
return sign*Numeric.arccos(cosangle) |
|---|
| 305 |
_FindAngle = classmethod(_FindAngle) |
|---|
| 306 |
|
|---|
| 307 |
def _Invalidate(self): |
|---|
| 308 |
try: delattr(self, '_sweepvector') |
|---|
| 309 |
except AttributeError: pass |
|---|
| 310 |
|
|---|