1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 """Geometry classes and operations.
20 Also, vector file representation (Art).
23 __author__
= "howard.trickey@gmail.com"
27 # distances less than about DISTTOL will be considered
34 """Container of points without duplication, each mapped to an int.
36 Points are either have dimension at least 2, maybe more.
39 In order to efficiently find duplicates, we quantize the points
40 to triples of ints and map from quantized triples to vertex
44 pos: list of tuple of float - coordinates indexed by
46 invmap: dict of (int, int, int) to int - quantized coordinates
50 def __init__(self
, initlist
=[]):
58 """Quantize the float tuple into an int tuple.
63 tuple of int - scaled by INVDISTTOL and rounded p
66 return tuple([int(round(v
* INVDISTTOL
)) for v
in p
])
68 def AddPoint(self
, p
):
69 """Add point p to the Points set and return vertex number.
71 If there is an existing point which quantizes the same,,
72 don't add a new one but instead return existing index.
75 p: tuple of float - coordinates (2-tuple or 3-tuple)
77 int - the vertex number of added (or existing) point
80 qp
= Points
.Quantize(p
)
82 return self
.invmap
[qp
]
84 self
.invmap
[qp
] = len(self
.pos
)
86 return len(self
.pos
) - 1
88 def AddPoints(self
, points
):
89 """Add another set of points to this set.
91 We need to return a mapping from indices
92 in the argument points space into indices
96 points: Points - to union into this set
98 list of int: maps added indices to new ones
101 vmap
= [0] * len(points
.pos
)
102 for i
in range(len(points
.pos
)):
103 vmap
[i
] = self
.AddPoint(points
.pos
[i
])
106 def AddZCoord(self
, z
):
107 """Change this in place to have a z coordinate, with value z.
109 Assumes the coordinates are currently 2d.
112 z: the value of the z coordinate to add
114 self now has a z-coordinate added
117 assert(len(self
.pos
) == 0 or len(self
.pos
[0]) == 2)
119 for i
, (x
, y
) in enumerate(self
.pos
):
122 newinvmap
[self
.Quantize(newp
)] = i
123 self
.invmap
= newinvmap
125 def AddToZCoord(self
, i
, delta
):
126 """Change the z-coordinate of point with index i to add delta.
128 Assumes the coordinates are currently 3d.
131 i: int - index of a point
132 delta: float - value to add to z-coord
135 (x
, y
, z
) = self
.pos
[i
]
136 self
.pos
[i
] = (x
, y
, z
+ delta
)
139 class PolyArea(object):
140 """Contains a Polygonal Area (polygon with possible holes).
142 A polygon is a list of vertex ids, each an index given by
143 a Points object. The list represents a CCW-oriented
144 outer boundary (implicitly closed).
145 If there are holes, they are lists of CW-oriented vertices
146 that should be contained in the outer boundary.
147 (So the left face of both the poly and the holes is
152 poly: list of vertex ids
153 holes: list of lists of vertex ids (each a hole in poly)
154 data: any - application data (can hold color, e.g.)
157 def __init__(self
, points
=None, poly
=None, holes
=None, data
=None):
158 self
.points
= points
if points
else Points()
159 self
.poly
= poly
if poly
else []
160 self
.holes
= holes
if holes
else []
163 def AddHole(self
, holepa
):
164 """Add a PolyArea's poly as a hole of self.
166 Need to reverse the contour and
167 adjust the the point indexes and self.points.
173 vmap
= self
.points
.AddPoints(holepa
.points
)
174 holepoly
= [vmap
[i
] for i
in holepa
.poly
]
176 self
.holes
.append(holepoly
)
178 def ContainsPoly(self
, poly
, points
):
179 """Tests if poly is contained within self.poly.
182 poly: list of int - indices into points
183 points: Points - maps to coords
185 bool - True if poly is fully contained within self.poly
189 if PointInside(points
.pos
[v
], self
.poly
, self
.points
) == -1:
194 """Returns the normal of the polyarea's main poly."""
196 pos
= self
.points
.pos
198 if len(pos
) == 0 or len(pos
[0]) == 2 or len(poly
) == 0:
199 print("whoops, not enough info to calculate normal")
200 return (0.0, 0.0, 1.0)
201 return Newell(poly
, self
.points
)
204 class PolyAreas(object):
205 """Contains a list of PolyAreas and a shared Points.
208 polyareas: list of PolyArea
214 self
.points
= Points()
216 def scale_and_center(self
, scaled_side_target
):
217 """Adjust the coordinates of the polyareas so that
218 it is centered at the origin and has its longest
219 dimension scaled to be scaled_side_target."""
221 if len(self
.points
.pos
) == 0:
223 (minv
, maxv
) = self
.bounds()
224 maxside
= max([maxv
[i
] - minv
[i
] for i
in range(2)])
226 scale
= scaled_side_target
/ maxside
229 translate
= [-0.5 * (maxv
[i
] + minv
[i
]) for i
in range(2)]
230 dim
= len(self
.points
.pos
[0])
232 translate
.append([0.0])
233 for v
in range(len(self
.points
.pos
)):
234 self
.points
.pos
[v
] = tuple([scale
* (self
.points
.pos
[v
][i
] + \
235 translate
[i
]) for i
in range(dim
)])
238 """Find bounding box of polyareas in xy.
241 ([minx,miny],[maxx,maxy]) - all floats
246 maxv
= [-huge
, -huge
]
247 for pa
in self
.polyareas
:
248 for face
in [pa
.poly
] + pa
.holes
:
250 vcoords
= self
.points
.pos
[v
]
252 if vcoords
[i
] < minv
[i
]:
254 if vcoords
[i
] > maxv
[i
]:
264 """Contains a generic 3d model.
266 A generic 3d model has vertices with 3d coordinates.
267 Each vertex gets a 'vertex id', which is an index that
268 can be used to refer to the vertex and can be used
269 to retrieve the 3d coordinates of the point.
271 The actual visible part of the geometry are the faces,
272 which are n-gons (n>2), specified by a vector of the
274 Faces may also have data associated with them,
275 and the data will be copied into newly created faces
276 from the most likely neighbor faces..
279 points: geom.Points - the 3d vertices
280 faces: list of list of indices (each a CCW traversal of a face)
281 face_data: list of any - if present, is parallel to
282 faces list and holds arbitrary data
286 self
.points
= Points()
292 """Contains a vector art diagram.
295 paths: list of Path objects
303 """A color or pattern to fill or stroke with.
305 For now, just do colors, but could later do
306 patterns or images too.
309 color: (r,g,b) triple of floats, 0.0=no color, 1.0=max color
312 def __init__(self
, r
=0.0, g
=0.0, b
=0.0):
313 self
.color
= (r
, g
, b
)
316 def CMYK(c
, m
, y
, k
):
317 """Return Paint specified in CMYK model.
319 Uses formula from 6.2.4 of PDF Reference.
322 c, m, y, k: float - in range [0, 1]
324 Paint - with components in rgb form now
327 return Paint(1.0 - min(1.0, c
+ k
),
328 1.0 - min(1.0, m
+ k
), 1.0 - min(1.0, y
+ k
))
330 black_paint
= Paint()
331 white_paint
= Paint(1.0, 1.0, 1.0)
334 'aqua': Paint(0.0, 1.0, 1.0),
335 'black': Paint(0.0, 0.0, 0.0),
336 'blue': Paint(0.0, 0.0, 1.0),
337 'fuchsia': Paint(1.0, 0.0, 1.0),
338 'gray': Paint(0.5, 0.5, 0.5),
339 'green': Paint(0.0, 0.5, 0.0),
340 'lime': Paint(0.0, 1.0, 0.0),
341 'maroon': Paint(0.5, 0.0, 0.0),
342 'navy': Paint(0.0, 0.0, 0.5),
343 'olive': Paint(0.5, 0.5, 0.0),
344 'purple': Paint(0.5, 0.0, 0.5),
345 'red': Paint(1.0, 0.0, 0.0),
346 'silver': Paint(0.75, 0.75, 0.75),
347 'teal': Paint(0.0, 0.5, 0.5),
348 'white': Paint(1.0, 1.0, 1.0),
349 'yellow': Paint(1.0, 1.0, 0.0)
354 """Represents a path in the PDF sense, with painting instructions.
357 subpaths: list of Subpath objects
358 filled: True if path is to be filled
359 fillevenodd: True if use even-odd rule to fill (else non-zero winding)
360 stroked: True if path is to be stroked
361 fillpaint: Paint to fill with
362 strokepaint: Paint to stroke with
368 self
.fillevenodd
= False
370 self
.fillpaint
= black_paint
371 self
.strokepaint
= black_paint
373 def AddSubpath(self
, subpath
):
374 """"Add a subpath."""
376 self
.subpaths
.append(subpath
)
379 """Returns True if this Path as no subpaths."""
381 return not self
.subpaths
384 class Subpath(object):
385 """Represents a subpath in PDF sense, either open or closed.
387 We'll represent lines, bezier pieces, circular arc pieces
388 as tuples with letters giving segment type in first position
389 and coordinates (2-tuples of floats) in the other positions.
392 ('L', a, b) - line from a to b
393 ('B', a, b, c, d) - cubic bezier from a to b, with control points c,d
394 ('Q', a, b, c) - quadratic bezier from a to b, with 1 control point c
395 ('A', a, b, rad, xrot, large-arc, ccw) - elliptical arc from a to b,
396 with rad=(rx, ry) as radii, xrot is x-axis rotation in degrees,
397 large-arc is True if arc should be >= 180 degrees,
398 ccw is True if start->end follows counter-clockwise direction
399 (see SVG spec); note that after rad,
400 the rest are floats or bools, not coordinate pairs
401 Note that s[1] and s[2] are the start and end points for any segment s.
404 segments: list of segment tuples (see above)
405 closed: True if closed
413 """Returns True if this subpath as no segments."""
415 return not self
.segments
417 def AddSegment(self
, seg
):
420 self
.segments
.append(seg
)
424 """Return start point for segment.
429 (float, float): the coordinates of the segment's start point
436 """Return end point for segment.
441 (float, float): the coordinates of the segment's end point
447 class TransformMatrix(object):
448 """Transformation matrix for 2d coordinates.
450 The transform matrix is:
454 and coordinate tranformation is defined by:
455 [x' y' 1] = [x y 1] x TransformMatrix
458 a, b, c, d, e, f: floats
461 def __init__(self
, a
=1.0, b
=0.0, c
=0.0, d
=1.0, e
=0.0, f
=0.0):
470 return str([self
.a
, self
.b
, self
.c
, self
.d
, self
.e
, self
.f
])
473 """Return a copy of this matrix."""
475 return TransformMatrix(self
.a
, self
.b
, self
.c
, self
.d
, self
.e
, self
.f
)
477 def ComposeTransform(self
, a
, b
, c
, d
, e
, f
):
478 """Apply the transform given the the arguments on top of this one.
480 This is accomplished by returning t x sel
481 where t is the transform matrix that would be formed from the args.
484 a, b, c, d, e, f: float - defines a composing TransformMatrix
487 newa
= a
* self
.a
+ b
* self
.c
488 newb
= a
* self
.b
+ b
* self
.d
489 newc
= c
* self
.a
+ d
* self
.c
490 newd
= c
* self
.b
+ d
* self
.d
491 newe
= e
* self
.a
+ f
* self
.c
+ self
.e
492 newf
= e
* self
.b
+ f
* self
.d
+ self
.f
501 """Return the result of applying this tranform to pt = (x,y).
504 (x, y) : (float, float)
506 (x', y'): 2-tuple of floats, the result of [x y 1] x self
510 return (self
.a
* x
+ self
.c
* y
+ self
.e
, \
511 self
.b
* x
+ self
.d
* y
+ self
.f
)
514 def ApproxEqualPoints(p
, q
):
515 """Return True if p and q are approximately the same points.
521 bool - True if the 1-norm <= DISTTOL
524 for i
in range(len(p
)):
525 if abs(p
[i
] - q
[i
]) > DISTTOL
:
530 def PointInside(v
, a
, points
):
531 """Return 1, 0, or -1 as v is inside, on, or outside polygon.
533 Cf. Eric Haines ptinpoly in Graphics Gems IV.
536 v : (float, float) or (float, float, float) - coordinates of a point
537 a : list of vertex indices defining polygon (assumed CCW)
538 points: Points - to get coordinates for polygon
540 1, 0, -1: as v is inside, on, or outside polygon a
543 (xv
, yv
) = (v
[0], v
[1])
544 vlast
= points
.pos
[a
[-1]]
545 (x0
, y0
) = (vlast
[0], vlast
[1])
546 if x0
== xv
and y0
== yv
:
551 for i
in range(0, n
):
552 vi
= points
.pos
[a
[i
]]
553 (x1
, y1
) = (vi
[0], vi
[1])
554 if x1
== xv
and y1
== yv
:
564 z
= x1
- (y1
- yv
) * (x0
- x1
) / (y0
- y1
)
576 def SignedArea(polygon
, points
):
577 """Return the area of the polgon, positive if CCW, negative if CW.
580 polygon: list of vertex indices
583 float - area of polygon, positive if it was CCW, else negative
588 for i
in range(0, n
):
589 u
= points
.pos
[polygon
[i
]]
590 v
= points
.pos
[polygon
[(i
+ 1) % n
]]
591 a
+= u
[0] * v
[1] - u
[1] * v
[0]
596 """Return vector a-b.
602 n-tuple of floats - pairwise addition a+b
607 return tuple([a
[i
] + b
[i
] for i
in range(n
)])
611 """Return vector a-b.
617 n-tuple of floats - pairwise subtraction a-b
622 return tuple([a
[i
] - b
[i
] for i
in range(n
)])
626 """Return the dot product of two vectors.
632 n-tuple of floats - dot product of a and b
644 """Return the Euclidean length of the argument vector.
649 float: the 2-norm of a
658 def Newell(poly
, points
):
659 """Use Newell method to find polygon normal.
661 Assume poly has length at least 3 and points are 3d.
664 poly: list of int - indices into points.pos
665 points: Points - assumed 3d
667 (float, float, float) - the average normal
675 for i
, ai
in enumerate(poly
):
676 bi
= poly
[(i
+ 1) % n
]
679 sumx
+= (a
[1] - b
[1]) * (a
[2] + b
[2])
680 sumy
+= (a
[2] - b
[2]) * (a
[0] + b
[0])
681 sumz
+= (a
[0] - b
[0]) * (a
[1] + b
[1])
682 return Norm3(sumx
, sumy
, sumz
)
686 """Return vector (x,y,z) normalized by dividing by squared length.
687 Return (0.0, 0.0, 1.0) if the result is undefined."""
688 sqrlen
= x
* x
+ y
* y
+ z
* z
690 return (0.0, 0.0, 1.0)
693 d
= math
.sqrt(sqrlen
)
694 return (x
/ d
, y
/ d
, z
/ d
)
696 return (0.0, 0.0, 1.0)
699 # We're using right-hand coord system, where
700 # forefinger=x, middle=y, thumb=z on right hand.
701 # Then, e.g., (1,0,0) x (0,1,0) = (0,0,1)
703 """Return the cross product of two vectors, a x b."""
707 return (ay
* bz
- az
* by
, az
* bx
- ax
* bz
, ax
* by
- ay
* bx
)
711 """Return matrix multiplication of p times m
712 where m is a 4x3 matrix and p is a 3d point, extended with 1."""
715 return (x
* m
[0] + y
* m
[3] + z
* m
[6] + m
[9],
716 x
* m
[1] + y
* m
[4] + z
* m
[7] + m
[10],
717 x
* m
[2] + y
* m
[5] + z
* m
[8] + m
[11])