File: segment.py

package info (click to toggle)
python-beziers 0.5.0%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 652 kB
  • sloc: python: 2,850; makefile: 20
file content (183 lines) | stat: -rw-r--r-- 5,700 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
from beziers.point import Point
from beziers.affinetransformation import AffineTransformation
from beziers.utils.samplemixin import SampleMixin
from beziers.utils.intersectionsmixin import IntersectionsMixin
from beziers.boundingbox import BoundingBox

class Segment(IntersectionsMixin,SampleMixin,object):

  """A segment is part of a path. Although this package is called
  `beziers.py`, it's really for font people, and paths in the font
  world are made up of cubic Bezier curves, lines and (if you're
  dealing with TrueType) quadratic Bezier curves. Each of these
  things is represented as an object derived from the Segment base
  class. So, when you inspect the path in the segment representation,
  you will get a list of CubicBezier, Line and QuadraticBezier objects,
  all of which derive from Segment.

  Because of this, a Segment can have two, three or four elements:
  lines have two end points; quadratic Beziers have a start, a control
  point and an end point; cubic have a start, two control points and
  an end point.

  You can pretend that a Segment object is an array and index it like
  one::

      q = CubicBezier(
        Point(122,102), Point(35,200), Point(228,145), Point(190,46)
      )

      start, cp1, cp2, end = q[0],q[1],q[2],q[3]

  You can also access the start and end points like so::

      start = q.start
      end = q.end

  """

  def __getitem__(self, item):
    return self.points[item]
  def __setitem__(self, key, item):
      self.points[key] = item
  def __len__(self):
    return len(self.points)
  def __eq__(self,other):
    if self.order != other.order: return False
    for p in range(0,self.order):
      if self[p] != other[p]: return False
    return True
  def __hash__(self):
    return hash(tuple(self.points))
  def __ne__(self,other):
    return not self.__eq__(other)

  def clone(self):
    """Returns a new Segment which is a copy of this segment."""
    klass = self.__class__
    return klass(*[ p.clone() for p in self.points ])

  def round(self):
    """Rounds the points of segment to integer coordinates."""
    self.points = [ p.rounded() for p in self.points ]

  @property
  def order(self):
    return len(self.points)

  @property
  def start(self):
    """Returns a Point object representing the start of this segment."""
    return self.points[0]

  @property
  def end(self):
    """Returns a Point object representing the end of this segment."""
    return self.points[-1]

  @property
  def startAngle(self):
    return (self.points[1]-self.points[0]).angle

  @property
  def endAngle(self):
    return (self.points[-1]-self.points[-2]).angle

  def tangentAtTime(self,t):
    """Returns a `Point` representing the unit vector of tangent at time `t`."""
    return self.derivative().pointAtTime(t).toUnitVector()

  def normalAtTime(self,t):
    """Returns a `Point` representing the normal (rotated tangent) at time `t`."""
    tan = self.tangentAtTime(t)
    return Point(-tan.y,tan.x)

  def translated(self,vector):
    """Returns a *new Segment object* representing the translation of
    this segment by the given vector. i.e.::

      >>> l = Line(Point(0,0), Point(10,10))
      >>> l.translated(Point(5,5))
      L<<5.0,5.0>--<15.0,15.0>>
      >>> l
      L<<0.0,0.0>--<10.0,10.0>>

    """

    klass = self.__class__
    return klass(*[ p+vector for p in self.points ])

  def rotated(self,around, by):
    """Returns a *new Segment object* representing the rotation of
    this segment around the given point and by the given angle. i.e.::

      >>> l = Line(Point(0,0), Point(10,10))
      >>> l.rotated(Point(5,5), math.pi/2)
      L<<10.0,-8.881784197e-16>--<-8.881784197e-16,10.0>>

    """

    klass = self.__class__
    pNew = [ p.clone() for p in self.points]
    for p in pNew: p.rotate(around,by)
    return klass(*pNew)

  def scaled(self,bx):
    """Returns a *new Segment object* representing the scaling of
    this segment by the given magnification. i.e.::

      >>> l = Line(Point(0,0), Point(10,10))
      >>> l.scaled(2)
      L<<0,0>--<20,20>>

    """

    klass = self.__class__
    pNew = [ p * bx for p in self.points]
    return klass(*pNew)

  def transformed(self, transformation):
    """Returns a *new Segment object* transformed by the given AffineTransformation matrix."""
    klass = self.__class__
    pNew = [ p.clone() for p in self.points]
    for p in pNew: p.transform(transformation)
    return klass(*pNew)

  def alignmentTransformation(self):
    m = AffineTransformation.translation(self.start * -1)
    m.rotate((self.end.transformed(m)).angle * -1)
    return m

  def aligned(self):
    """Returns a *new Segment object* aligned to the origin. i.e.
    with the first point translated to the origin (0,0) and the
    last point with y=0. Obviously, for a `Line` this is a bit pointless,
    but it's quite handy for higher-order curves."""
    return self.transformed(self.alignmentTransformation())

  def lengthAtTime(self, t):
    """Returns the length of the subset of the path from the start
    up to the point t (0->1), where 1 is the end of the whole curve."""
    s1,_ = self.splitAtTime(t)
    return s1.length

  def reversed(self):
    """Returns a new segment with the points reversed."""
    klass = self.__class__
    return klass(*list(reversed(self.points)))

  def bounds(self):
    """Returns a BoundingBox object for this segment."""
    bounds = BoundingBox()
    ex = self.findExtremes()
    ex.append(0)
    ex.append(1)
    for t in ex:
      bounds.extend(self.pointAtTime(t))
    return bounds

  @property
  def hasLoop(self):
    """Returns True if the segment has a loop. (Only possible for cubics.)"""
    return False