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 #####
21 """Geometry classes and operations.
22 Also, vector file representation (Art).
25 __author__
= "howard.trickey@gmail.com"
29 # distances less than about DISTTOL will be considered
36 """Container of points without duplication, each mapped to an int.
38 Points are either have dimension at least 2, maybe more.
41 In order to efficiently find duplicates, we quantize the points
42 to triples of ints and map from quantized triples to vertex
46 pos: list of tuple of float - coordinates indexed by
48 invmap: dict of (int, int, int) to int - quantized coordinates
52 def __init__(self
, initlist
=[]):
60 """Quantize the float tuple into an int tuple.
65 tuple of int - scaled by INVDISTTOL and rounded p
68 return tuple([int(round(v
* INVDISTTOL
)) for v
in p
])
70 def AddPoint(self
, p
, allowdups
= False):
71 """Add point p to the Points set and return vertex number.
73 If there is an existing point which quantizes the same,,
74 don't add a new one but instead return existing index.
75 Except if allowdups is True, don't do that deduping.
78 p: tuple of float - coordinates (2-tuple or 3-tuple)
80 int - the vertex number of added (or existing) point
83 qp
= Points
.Quantize(p
)
84 if qp
in self
.invmap
and not allowdups
:
85 return self
.invmap
[qp
]
87 self
.invmap
[qp
] = len(self
.pos
)
89 return len(self
.pos
) - 1
91 def AddPoints(self
, points
, allowdups
= False):
92 """Add another set of points to this set.
94 We need to return a mapping from indices
95 in the argument points space into indices
99 points: Points - to union into this set
101 list of int: maps added indices to new ones
104 vmap
= [0] * len(points
.pos
)
105 for i
in range(len(points
.pos
)):
106 vmap
[i
] = self
.AddPoint(points
.pos
[i
], allowdups
)
109 def AddZCoord(self
, z
):
110 """Change this in place to have a z coordinate, with value z.
112 Assumes the coordinates are currently 2d.
115 z: the value of the z coordinate to add
117 self now has a z-coordinate added
120 assert(len(self
.pos
) == 0 or len(self
.pos
[0]) == 2)
122 for i
, (x
, y
) in enumerate(self
.pos
):
125 newinvmap
[self
.Quantize(newp
)] = i
126 self
.invmap
= newinvmap
128 def AddToZCoord(self
, i
, delta
):
129 """Change the z-coordinate of point with index i to add delta.
131 Assumes the coordinates are currently 3d.
134 i: int - index of a point
135 delta: float - value to add to z-coord
138 (x
, y
, z
) = self
.pos
[i
]
139 self
.pos
[i
] = (x
, y
, z
+ delta
)
142 class PolyArea(object):
143 """Contains a Polygonal Area (polygon with possible holes).
145 A polygon is a list of vertex ids, each an index given by
146 a Points object. The list represents a CCW-oriented
147 outer boundary (implicitly closed).
148 If there are holes, they are lists of CW-oriented vertices
149 that should be contained in the outer boundary.
150 (So the left face of both the poly and the holes is
155 poly: list of vertex ids
156 holes: list of lists of vertex ids (each a hole in poly)
157 data: any - application data (can hold color, e.g.)
160 def __init__(self
, points
=None, poly
=None, holes
=None, data
=None):
161 self
.points
= points
if points
else Points()
162 self
.poly
= poly
if poly
else []
163 self
.holes
= holes
if holes
else []
166 def AddHole(self
, holepa
):
167 """Add a PolyArea's poly as a hole of self.
169 Need to reverse the contour and
170 adjust the the point indexes and self.points.
176 vmap
= self
.points
.AddPoints(holepa
.points
)
177 holepoly
= [vmap
[i
] for i
in holepa
.poly
]
179 self
.holes
.append(holepoly
)
181 def ContainsPoly(self
, poly
, points
):
182 """Tests if poly is contained within self.poly.
185 poly: list of int - indices into points
186 points: Points - maps to coords
188 bool - True if poly is fully contained within self.poly
192 if PointInside(points
.pos
[v
], self
.poly
, self
.points
) == -1:
197 """Returns the normal of the polyarea's main poly."""
199 pos
= self
.points
.pos
201 if len(pos
) == 0 or len(pos
[0]) == 2 or len(poly
) == 0:
202 print("whoops, not enough info to calculate normal")
203 return (0.0, 0.0, 1.0)
204 return Newell(poly
, self
.points
)
207 class PolyAreas(object):
208 """Contains a list of PolyAreas and a shared Points.
211 polyareas: list of PolyArea
217 self
.points
= Points()
219 def scale_and_center(self
, scaled_side_target
):
220 """Adjust the coordinates of the polyareas so that
221 it is centered at the origin and has its longest
222 dimension scaled to be scaled_side_target."""
224 if len(self
.points
.pos
) == 0:
226 (minv
, maxv
) = self
.bounds()
227 maxside
= max([maxv
[i
] - minv
[i
] for i
in range(2)])
229 scale
= scaled_side_target
/ maxside
232 translate
= [-0.5 * (maxv
[i
] + minv
[i
]) for i
in range(2)]
233 dim
= len(self
.points
.pos
[0])
235 translate
.append([0.0])
236 for v
in range(len(self
.points
.pos
)):
237 self
.points
.pos
[v
] = tuple([scale
* (self
.points
.pos
[v
][i
] + \
238 translate
[i
]) for i
in range(dim
)])
241 """Find bounding box of polyareas in xy.
244 ([minx,miny],[maxx,maxy]) - all floats
249 maxv
= [-huge
, -huge
]
250 for pa
in self
.polyareas
:
251 for face
in [pa
.poly
] + pa
.holes
:
253 vcoords
= self
.points
.pos
[v
]
255 if vcoords
[i
] < minv
[i
]:
257 if vcoords
[i
] > maxv
[i
]:
267 """Contains a generic 3d model.
269 A generic 3d model has vertices with 3d coordinates.
270 Each vertex gets a 'vertex id', which is an index that
271 can be used to refer to the vertex and can be used
272 to retrieve the 3d coordinates of the point.
274 The actual visible part of the geometry are the faces,
275 which are n-gons (n>2), specified by a vector of the
277 Faces may also have data associated with them,
278 and the data will be copied into newly created faces
279 from the most likely neighbor faces..
282 points: geom.Points - the 3d vertices
283 faces: list of list of indices (each a CCW traversal of a face)
284 face_data: list of any - if present, is parallel to
285 faces list and holds arbitrary data
289 self
.points
= Points()
295 """Contains a vector art diagram.
298 paths: list of Path objects
306 """A color or pattern to fill or stroke with.
308 For now, just do colors, but could later do
309 patterns or images too.
312 color: (r,g,b) triple of floats, 0.0=no color, 1.0=max color
315 def __init__(self
, r
=0.0, g
=0.0, b
=0.0):
316 self
.color
= (r
, g
, b
)
319 def CMYK(c
, m
, y
, k
):
320 """Return Paint specified in CMYK model.
322 Uses formula from 6.2.4 of PDF Reference.
325 c, m, y, k: float - in range [0, 1]
327 Paint - with components in rgb form now
330 return Paint(1.0 - min(1.0, c
+ k
),
331 1.0 - min(1.0, m
+ k
), 1.0 - min(1.0, y
+ k
))
333 black_paint
= Paint()
334 white_paint
= Paint(1.0, 1.0, 1.0)
337 'aqua': Paint(0.0, 1.0, 1.0),
338 'black': Paint(0.0, 0.0, 0.0),
339 'blue': Paint(0.0, 0.0, 1.0),
340 'fuchsia': Paint(1.0, 0.0, 1.0),
341 'gray': Paint(0.5, 0.5, 0.5),
342 'green': Paint(0.0, 0.5, 0.0),
343 'lime': Paint(0.0, 1.0, 0.0),
344 'maroon': Paint(0.5, 0.0, 0.0),
345 'navy': Paint(0.0, 0.0, 0.5),
346 'olive': Paint(0.5, 0.5, 0.0),
347 'purple': Paint(0.5, 0.0, 0.5),
348 'red': Paint(1.0, 0.0, 0.0),
349 'silver': Paint(0.75, 0.75, 0.75),
350 'teal': Paint(0.0, 0.5, 0.5),
351 'white': Paint(1.0, 1.0, 1.0),
352 'yellow': Paint(1.0, 1.0, 0.0)
357 """Represents a path in the PDF sense, with painting instructions.
360 subpaths: list of Subpath objects
361 filled: True if path is to be filled
362 fillevenodd: True if use even-odd rule to fill (else non-zero winding)
363 stroked: True if path is to be stroked
364 fillpaint: Paint to fill with
365 strokepaint: Paint to stroke with
371 self
.fillevenodd
= False
373 self
.fillpaint
= black_paint
374 self
.strokepaint
= black_paint
376 def AddSubpath(self
, subpath
):
377 """"Add a subpath."""
379 self
.subpaths
.append(subpath
)
382 """Returns True if this Path as no subpaths."""
384 return not self
.subpaths
387 class Subpath(object):
388 """Represents a subpath in PDF sense, either open or closed.
390 We'll represent lines, bezier pieces, circular arc pieces
391 as tuples with letters giving segment type in first position
392 and coordinates (2-tuples of floats) in the other positions.
395 ('L', a, b) - line from a to b
396 ('B', a, b, c, d) - cubic bezier from a to b, with control points c,d
397 ('Q', a, b, c) - quadratic bezier from a to b, with 1 control point c
398 ('A', a, b, rad, xrot, large-arc, ccw) - elliptical arc from a to b,
399 with rad=(rx, ry) as radii, xrot is x-axis rotation in degrees,
400 large-arc is True if arc should be >= 180 degrees,
401 ccw is True if start->end follows counter-clockwise direction
402 (see SVG spec); note that after rad,
403 the rest are floats or bools, not coordinate pairs
404 Note that s[1] and s[2] are the start and end points for any segment s.
407 segments: list of segment tuples (see above)
408 closed: True if closed
416 """Returns True if this subpath as no segments."""
418 return not self
.segments
420 def AddSegment(self
, seg
):
423 self
.segments
.append(seg
)
427 """Return start point for segment.
432 (float, float): the coordinates of the segment's start point
439 """Return end point for segment.
444 (float, float): the coordinates of the segment's end point
450 class TransformMatrix(object):
451 """Transformation matrix for 2d coordinates.
453 The transform matrix is:
457 and coordinate transformation is defined by:
458 [x' y' 1] = [x y 1] x TransformMatrix
461 a, b, c, d, e, f: floats
464 def __init__(self
, a
=1.0, b
=0.0, c
=0.0, d
=1.0, e
=0.0, f
=0.0):
473 return str([self
.a
, self
.b
, self
.c
, self
.d
, self
.e
, self
.f
])
476 """Return a copy of this matrix."""
478 return TransformMatrix(self
.a
, self
.b
, self
.c
, self
.d
, self
.e
, self
.f
)
480 def ComposeTransform(self
, a
, b
, c
, d
, e
, f
):
481 """Apply the transform given the the arguments on top of this one.
483 This is accomplished by returning t x sel
484 where t is the transform matrix that would be formed from the args.
487 a, b, c, d, e, f: float - defines a composing TransformMatrix
490 newa
= a
* self
.a
+ b
* self
.c
491 newb
= a
* self
.b
+ b
* self
.d
492 newc
= c
* self
.a
+ d
* self
.c
493 newd
= c
* self
.b
+ d
* self
.d
494 newe
= e
* self
.a
+ f
* self
.c
+ self
.e
495 newf
= e
* self
.b
+ f
* self
.d
+ self
.f
504 """Return the result of applying this transform to pt = (x,y).
507 (x, y) : (float, float)
509 (x', y'): 2-tuple of floats, the result of [x y 1] x self
513 return (self
.a
* x
+ self
.c
* y
+ self
.e
, \
514 self
.b
* x
+ self
.d
* y
+ self
.f
)
517 def ApproxEqualPoints(p
, q
):
518 """Return True if p and q are approximately the same points.
524 bool - True if the 1-norm <= DISTTOL
527 for i
in range(len(p
)):
528 if abs(p
[i
] - q
[i
]) > DISTTOL
:
533 def PointInside(v
, a
, points
):
534 """Return 1, 0, or -1 as v is inside, on, or outside polygon.
536 Cf. Eric Haines ptinpoly in Graphics Gems IV.
539 v : (float, float) or (float, float, float) - coordinates of a point
540 a : list of vertex indices defining polygon (assumed CCW)
541 points: Points - to get coordinates for polygon
543 1, 0, -1: as v is inside, on, or outside polygon a
546 (xv
, yv
) = (v
[0], v
[1])
547 vlast
= points
.pos
[a
[-1]]
548 (x0
, y0
) = (vlast
[0], vlast
[1])
549 if x0
== xv
and y0
== yv
:
554 for i
in range(0, n
):
555 vi
= points
.pos
[a
[i
]]
556 (x1
, y1
) = (vi
[0], vi
[1])
557 if x1
== xv
and y1
== yv
:
567 z
= x1
- (y1
- yv
) * (x0
- x1
) / (y0
- y1
)
579 def SignedArea(polygon
, points
):
580 """Return the area of the polgon, positive if CCW, negative if CW.
583 polygon: list of vertex indices
586 float - area of polygon, positive if it was CCW, else negative
591 for i
in range(0, n
):
592 u
= points
.pos
[polygon
[i
]]
593 v
= points
.pos
[polygon
[(i
+ 1) % n
]]
594 a
+= u
[0] * v
[1] - u
[1] * v
[0]
599 """Return vector a-b.
605 n-tuple of floats - pairwise addition a+b
610 return tuple([a
[i
] + b
[i
] for i
in range(n
)])
614 """Return vector a-b.
620 n-tuple of floats - pairwise subtraction a-b
625 return tuple([a
[i
] - b
[i
] for i
in range(n
)])
629 """Return the dot product of two vectors.
635 n-tuple of floats - dot product of a and b
647 """Return the Euclidean length of the argument vector.
652 float: the 2-norm of a
661 def Newell(poly
, points
):
662 """Use Newell method to find polygon normal.
664 Assume poly has length at least 3 and points are 3d.
667 poly: list of int - indices into points.pos
668 points: Points - assumed 3d
670 (float, float, float) - the average normal
678 for i
, ai
in enumerate(poly
):
679 bi
= poly
[(i
+ 1) % n
]
682 sumx
+= (a
[1] - b
[1]) * (a
[2] + b
[2])
683 sumy
+= (a
[2] - b
[2]) * (a
[0] + b
[0])
684 sumz
+= (a
[0] - b
[0]) * (a
[1] + b
[1])
685 return Norm3(sumx
, sumy
, sumz
)
689 """Return vector (x,y,z) normalized by dividing by squared length.
690 Return (0.0, 0.0, 1.0) if the result is undefined."""
691 sqrlen
= x
* x
+ y
* y
+ z
* z
693 return (0.0, 0.0, 1.0)
696 d
= math
.sqrt(sqrlen
)
697 return (x
/ d
, y
/ d
, z
/ d
)
699 return (0.0, 0.0, 1.0)
702 # We're using right-hand coord system, where
703 # forefinger=x, middle=y, thumb=z on right hand.
704 # Then, e.g., (1,0,0) x (0,1,0) = (0,0,1)
706 """Return the cross product of two vectors, a x b."""
710 return (ay
* bz
- az
* by
, az
* bx
- ax
* bz
, ax
* by
- ay
* bx
)
714 """Return matrix multiplication of p times m
715 where m is a 4x3 matrix and p is a 3d point, extended with 1."""
718 return (x
* m
[0] + y
* m
[3] + z
* m
[6] + m
[9],
719 x
* m
[1] + y
* m
[4] + z
* m
[7] + m
[10],
720 x
* m
[2] + y
* m
[5] + z
* m
[8] + m
[11])